Merge pull request 'dev/studio-tiled-parser-assets-scene-asset-type' (#6) from dev/studio-tiled-parser-assets-scene-asset-type into master
All checks were successful
JaCoCo Coverage #### Project Overview
No changes detected, that affect the code coverage.
* Line Coverage: 58.63% (16555/28235)
* Branch Coverage: 51.34% (6336/12341)
* Lines of Code: 28235
* Cyclomatic Complexity: 11205
#### Quality Gates Summary
Output truncated.
Test / Build skipped: 11, passed: 585
Intrepid/Prometeu/Studio/pipeline/head This commit looks good
All checks were successful
JaCoCo Coverage #### Project Overview
No changes detected, that affect the code coverage.
* Line Coverage: 58.63% (16555/28235)
* Branch Coverage: 51.34% (6336/12341)
* Lines of Code: 28235
* Cyclomatic Complexity: 11205
#### Quality Gates Summary
Output truncated.
Test / Build skipped: 11, passed: 585
Intrepid/Prometeu/Studio/pipeline/head This commit looks good
Reviewed-on: #6
This commit is contained in:
commit
5eb8749a90
@ -1,19 +1,19 @@
|
||||
{"type":"meta","next_id":{"DSC":30,"AGD":32,"DEC":28,"PLN":56,"LSN":42,"CLSN":1}}
|
||||
{"type":"meta","next_id":{"DSC":30,"AGD":32,"DEC":29,"PLN":59,"LSN":46,"CLSN":1}}
|
||||
{"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":"open","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-15","tags":["studio","assets","scene","tiled","parser","asset-type"],"agendas":[{"id":"AGD-0030","file":"AGD-0030-tiled-parser-and-scene-asset-type.md","status":"open","created_at":"2026-04-15","updated_at":"2026-04-15"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"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":[]}
|
||||
{"type":"discussion","id":"DSC-0026","status":"done","ticket":"glyph-bank-naming-alignment-with-runtime","title":"Glyph Bank Naming Alignment with Runtime DEC-0006","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["packer","studio","naming","asset-contract","runtime-alignment","glyph-bank"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0040","file":"discussion/lessons/DSC-0026-glyph-bank-naming-alignment-with-runtime/LSN-0040-glyph-bank-artifact-naming-alignment.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
|
||||
{"type":"discussion","id":"DSC-0025","status":"done","ticket":"packer-pipeline-metadata-ownership","title":"Pipeline Metadata Ownership and Runtime Contract","created_at":"2026-04-09","updated_at":"2026-04-10","tags":["packer","metadata","runtime-contract","tooling","studio"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0039","file":"discussion/lessons/DSC-0025-packer-pipeline-metadata-ownership/LSN-0039-runtime-header-boundary-and-tooling-owned-pipeline-metadata.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
|
||||
{"type":"discussion","id":"DSC-0024","status":"done","ticket":"jacoco-reports-consolidation","title":"JaCoCo Reports Consolidation in Gradle","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["infra","gradle","jacoco","coverage","jenkins"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0038","file":"discussion/lessons/DSC-0024-jacoco-reports-consolidation/LSN-0038-jacoco-reports-consolidation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
|
||||
{"type":"discussion","id":"DSC-0024","status":"done","ticket":"jacoco-reports-consolidation","title":"JaCoCo Reports Consolidation in Gradle","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["infra","gradle","jacoco","coverage","jenkins"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0038","file":"discussion/lessons/DSC-0024-jacoco-reports-consolidation/LSN-0038-jacoco-reports-consolidation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"},{"id":"LSN-0044","file":"discussion/lessons/DSC-0024-jacoco-reports-consolidation/LSN-0044-jenkins-jacoco-alignment.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
|
||||
{"type":"discussion","id":"DSC-0001","status":"done","ticket":"studio-docs-import","title":"Import docs/studio into discussion-framework artifacts","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["studio","migration","discussion-framework","docs-import"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0001","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0001-assets-workspace-execution-wave-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0002","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0002-bank-composition-editor-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0003","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0003-mental-model-asset-mutations-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0004","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0004-mental-model-assets-workspace-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0005","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0005-mental-model-studio-events-and-components-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0006","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0006-mental-model-studio-shell-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0007","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0007-pack-wizard-shell-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0008","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0008-project-scoped-state-and-activity-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0016","file":"discussion/lessons/DSC-0001-studio-docs-import/LSN-0016-studio-docs-import-pattern.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"}]}
|
||||
{"type":"discussion","id":"DSC-0002","status":"open","ticket":"palette-management-in-studio","title":"Palette Management in Studio","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["studio","legacy-import","palette-management","tile-bank","packer-boundary"],"agendas":[{"id":"AGD-0002","file":"AGD-0002-palette-management-in-studio.md","status":"open","created_at":"2026-03-26","updated_at":"2026-03-26"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0002","status":"done","ticket":"palette-management-in-studio","title":"Palette Management in Studio","created_at":"2026-03-26","updated_at":"2026-04-23","tags":["studio","legacy-import","palette-management","tile-bank","packer-boundary"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0042","file":"discussion/lessons/DSC-0002-palette-management-in-studio/LSN-0042-schema-driven-palette-authoring-and-local-metadata-events.md","status":"done","created_at":"2026-04-23","updated_at":"2026-04-23"}]}
|
||||
{"type":"discussion","id":"DSC-0003","status":"done","ticket":"packer-docs-import","title":"Import docs/packer into discussion-framework artifacts","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["packer","migration","discussion-framework","docs-import"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0009","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0009-mental-model-packer-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0010","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0010-asset-identity-and-runtime-contract-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0011","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0011-foundations-workspace-runtime-and-build-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0012","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0012-runtime-ownership-and-studio-boundary-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0013","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0013-metadata-convergence-and-runtime-sink-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0014","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0014-pack-wizard-summary-validation-and-pack-execution-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0015","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0015-tile-bank-packing-contract-legacy.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"},{"id":"LSN-0017","file":"discussion/lessons/DSC-0003-packer-docs-import/LSN-0017-packer-docs-import-pattern.md","status":"done","created_at":"2026-03-26","updated_at":"2026-03-26"}]}
|
||||
{"type":"discussion","id":"DSC-0004","status":"open","ticket":"tilemap-and-metatile-runtime-binary-layout","title":"Tilemap and Metatile Runtime Binary Layout","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["packer","legacy-import","tilemap","metatile","runtime-layout"],"agendas":[{"id":"AGD-0004","file":"AGD-0004-tilemap-and-metatile-runtime-binary-layout.md","status":"open","created_at":"2026-03-26","updated_at":"2026-03-26"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0005","status":"open","ticket":"variable-tile-bank-palette-serialization","title":"Variable Tile Bank Palette Serialization","created_at":"2026-03-26","updated_at":"2026-03-26","tags":["packer","legacy-import","tile-bank","palette-serialization","versioning"],"agendas":[{"id":"AGD-0005","file":"AGD-0005-variable-tile-bank-palette-serialization.md","status":"open","created_at":"2026-03-26","updated_at":"2026-03-26"}],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0006","status":"done","ticket":"pbs-game-facing-asset-refs-and-call-result-discard","title":"PBS Game-Facing Asset References and Ignored Call Result Lowering","created_at":"2026-03-27","updated_at":"2026-03-30","tags":["compiler","pbs","ergonomics","lowering","runtime","asset-identity","expression-statements"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0024","file":"discussion/lessons/DSC-0006-pbs-game-facing-asset-refs-and-call-result-discard/LSN-0024-addressable-surface-host-metadata-and-ignored-value-discipline.md","status":"done","created_at":"2026-03-30","updated_at":"2026-03-30"}]}
|
||||
{"type":"discussion","id":"DSC-0007","status":"done","ticket":"pbs-learn-to-discussion-lessons-migration","title":"Migrate PBS Learn Documents into Discussion Lessons","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["compiler","pbs","migration","discussion-framework","lessons","learn-prune"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0018","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0018-pbs-ast-and-parser-contract-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0019","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0019-pbs-name-resolution-and-linking-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0020","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0020-pbs-runtime-values-identity-memory-boundaries-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0021","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0021-pbs-diagnostics-and-conformance-governance-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0022","file":"discussion/lessons/DSC-0007-pbs-learn-to-discussion-lessons-migration/LSN-0022-pbs-globals-lifecycle-and-published-entrypoint-legacy.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]}
|
||||
{"type":"discussion","id":"DSC-0008","status":"done","ticket":"pbs-low-level-asset-manager-surface","title":"PBS Low-Level Asset Manager Surface for Runtime AssetManager","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["compiler","pbs","runtime","asset-manager","host-abi","stdlib","asset"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0023","file":"discussion/lessons/DSC-0008-pbs-low-level-asset-manager-surface/LSN-0023-lowassets-runtime-aligned-sdk-surface.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]}
|
||||
{"type":"discussion","id":"DSC-0009","status":"done","ticket":"studio-debugger-workspace-integration","title":"Integrate ../debugger into Studio as a dedicated workspace","created_at":"2026-03-30","updated_at":"2026-04-07","tags":["studio","debugger","workspace","integration","shell"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0022","file":"discussion/lessons/DSC-0022-studio-play-stop-and-debugger-integration/LSN-0022-studio-play-stop-and-debugger-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
|
||||
{"type":"discussion","id":"DSC-0009","status":"done","ticket":"studio-debugger-workspace-integration","title":"Integrate ../debugger into Studio as a dedicated workspace","created_at":"2026-03-30","updated_at":"2026-04-07","tags":["studio","debugger","workspace","integration","shell"],"agendas":[],"decisions":[],"plans":[],"lessons":[]}
|
||||
{"type":"discussion","id":"DSC-0010","status":"done","ticket":"studio-code-editor-workspace-foundations","title":"Establish Code Editor workspace foundations in Studio without LSP","created_at":"2026-03-30","updated_at":"2026-03-31","tags":["studio","editor","workspace","multi-frontend","lsp-deferred"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0026","file":"discussion/lessons/DSC-0010-studio-code-editor-workspace-foundations/LSN-0026-read-only-editor-foundations-and-semantic-deferral.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31"}]}
|
||||
{"type":"discussion","id":"DSC-0011","status":"done","ticket":"compiler-analyze-compile-build-pipeline-split","title":"Split compiler pipeline into analyze, compile, and build entrypoints","created_at":"2026-03-30","updated_at":"2026-03-30","tags":["compiler","pipeline","artifacts","build","analysis"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0025","file":"discussion/lessons/DSC-0011-compiler-analyze-compile-build-pipeline-split/LSN-0025-compiler-pipeline-entrypoints-and-result-boundaries.md","status":"done","created_at":"2026-03-30","updated_at":"2026-03-30"}]}
|
||||
{"type":"discussion","id":"DSC-0012","status":"done","ticket":"studio-editor-document-vfs-boundary","title":"Definir um boundary de VFS documental para tree/view/open files no Code Editor do Studio","created_at":"2026-03-31","updated_at":"2026-03-31","tags":["studio","editor","workspace","vfs","filesystem","boundary"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0027","file":"discussion/lessons/DSC-0012-studio-editor-document-vfs-boundary/LSN-0027-project-document-vfs-and-session-owned-editor-boundary.md","status":"done","created_at":"2026-03-31","updated_at":"2026-03-31"}]}
|
||||
@ -26,5 +26,5 @@
|
||||
{"type":"discussion","id":"DSC-0019","status":"done","ticket":"studio-project-local-setup-separate-from-main-studio-state","title":"Separate project-local setup from the main Studio state under .studio","created_at":"2026-04-04","updated_at":"2026-04-04","tags":["studio","project-session","project-state","persistence","dot-studio","setup","boundary"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0032","file":"discussion/lessons/DSC-0019-studio-project-local-setup-separate-from-main-studio-state/LSN-0032-separate-project-config-from-session-restoration-state.md","status":"done","created_at":"2026-04-04","updated_at":"2026-04-04"}]}
|
||||
{"type":"discussion","id":"DSC-0020","status":"done","ticket":"studio-editor-indentation-policy-and-project-setup","title":"Indentation Policy, Status-Bar Semantics, and Project-Local Editor Setup","created_at":"2026-04-04","updated_at":"2026-04-04","tags":["studio","editor","indentation","tabs","setup","dot-studio","configuration","ux"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0033","file":"discussion/lessons/DSC-0020-studio-editor-indentation-policy-and-project-setup/LSN-0033-setup-owned-indentation-policy-and-project-bootstrap-defaults.md","status":"done","created_at":"2026-04-04","updated_at":"2026-04-04"}]}
|
||||
{"type":"discussion","id":"DSC-0021","status":"done","ticket":"studio-frontend-editor-write-and-save-wave","title":"Enable Frontend Editing and Save in the Studio Code Editor","created_at":"2026-04-04","updated_at":"2026-04-06","tags":["studio","editor","frontend","write","save","vfs","lsp","pbs","access-policy"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0036","file":"discussion/lessons/DSC-0021-studio-frontend-editor-write-and-save-wave/LSN-0036-vfs-owned-frontend-edit-and-save-wave.md","status":"done","created_at":"2026-04-06","updated_at":"2026-04-06"}]}
|
||||
{"type":"discussion","id":"DSC-0022","status":"done","ticket":"studio-play-stop-cartridge-flow","title":"Wire the Studio Play/Stop button to shipper phase 1 cartridge preparation and runtime execution","created_at":"2026-04-06","updated_at":"2026-04-07","tags":["studio","play-stop","shipper","runtime","cartridge","shell"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0022","file":"discussion/lessons/DSC-0022-studio-play-stop-and-debugger-integration/LSN-0022-studio-play-stop-and-debugger-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
|
||||
{"type":"discussion","id":"DSC-0022","status":"done","ticket":"studio-play-stop-cartridge-flow","title":"Wire the Studio Play/Stop button to shipper phase 1 cartridge preparation and runtime execution","created_at":"2026-04-06","updated_at":"2026-04-07","tags":["studio","play-stop","shipper","runtime","cartridge","shell"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0043","file":"discussion/lessons/DSC-0022-studio-play-stop-and-debugger-integration/LSN-0043-studio-play-stop-and-debugger-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
|
||||
{"type":"discussion","id":"DSC-0023","status":"done","ticket":"gradle-version-catalog-migration","title":"Move dependency version management to Version Catalog (TOML)","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["gradle","buildSrc","dependency-management","version-catalog"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0037","file":"discussion/lessons/DSC-0023-gradle-version-catalog-migration/LSN-0037-gradle-version-catalog-adoption.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
|
||||
|
||||
@ -0,0 +1,106 @@
|
||||
---
|
||||
id: LSN-0042
|
||||
ticket: palette-management-in-studio
|
||||
title: Schema-Driven Palette Authoring and Local Metadata Events
|
||||
created: 2026-04-23
|
||||
tags:
|
||||
- studio
|
||||
- glyph-bank
|
||||
- palette-management
|
||||
- color-schema
|
||||
- local-events
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The glyph-bank palette workflow originally treated palette candidates as extracted tile artifacts. That made grouping, recoloring, and final bank palette selection compete in the same surface.
|
||||
|
||||
The completed palette-management work split the workflow into two authored layers:
|
||||
|
||||
1. `Color Schema Groups`, where selected tiles are grouped into a shared index structure.
|
||||
2. `Palette Overhauling`, where palettes are authored and selected against already-ready schemas.
|
||||
|
||||
The durable implementation lesson is that schema edits, palette edits, and build selection are all local glyph-material metadata operations first. They should update local metadata projections directly instead of forcing a full asset-details reload.
|
||||
|
||||
## Key Decisions
|
||||
|
||||
### Schema Owns Index Semantics
|
||||
|
||||
**What:** A schema is the authored index structure for a group of selected glyph-bank tiles. It owns `material_indices`, artifact bindings, fingerprint snapshots, version, and status.
|
||||
|
||||
**Why:** The source PNG stays unchanged, while Studio records how tile colors map into stable bank-local indices. This keeps tile grouping separate from recoloring.
|
||||
|
||||
**Trade-offs:** Schema invalidation must be explicit. When selected tile fingerprints drift, the schema can become inconsistent and dependent palettes become invalid or stale.
|
||||
|
||||
### Palettes Are Schema-Scoped
|
||||
|
||||
**What:** A palette always belongs to exactly one schema. It stores concrete `#RRGGBB` colors for that schema's visible indices and carries the schema version it was authored against.
|
||||
|
||||
**Why:** Multiple recolors can safely exist for one index structure without duplicating tile grouping metadata.
|
||||
|
||||
**Trade-offs:** Duplicate detection must use the tuple `schemaId + colors + order`, not display name or source tile identity.
|
||||
|
||||
### Original Palettes Are Synthetic and Read-Only
|
||||
|
||||
**What:** The original palette is the synthetic `schema-<schemaId>-original` projection derived from the current schema colors. It is not a persisted user palette.
|
||||
|
||||
**Why:** Updating the schema should update the original palette automatically. The original represents the schema's source color meaning, not a user-authored recolor.
|
||||
|
||||
**Trade-offs:** Users cannot edit or delete the original palette directly. If they want a changed palette, they create or edit a separate persisted palette for the same schema.
|
||||
|
||||
### Local Metadata Events Beat Global Reloads
|
||||
|
||||
**What:** Schema and palette metadata mutations should publish `StudioGlyphMaterialMetadataChangedEvent` and let interested sections rebind local state.
|
||||
|
||||
**Why:** Global asset refresh and selection events can close edit mode, reset form state, and make small metadata edits feel destructive.
|
||||
|
||||
**Trade-offs:** Controls must keep their local coordinators robust: stale button handlers should read the current view model at click time, and metadata replacement must preserve edit-mode drafts where possible.
|
||||
|
||||
## Patterns and Algorithms
|
||||
|
||||
### `glyph-material.json`
|
||||
|
||||
`glyph-material.json` is the Studio-owned support file for glyph-bank material authoring. It persists schemas and user-authored palettes, while derived original palettes remain a view projection.
|
||||
|
||||
Schema records store:
|
||||
|
||||
- stable `schema_id`
|
||||
- `status`
|
||||
- `version`
|
||||
- dense visible `material_indices`
|
||||
- artifact bindings with fingerprint snapshots
|
||||
|
||||
Palette records store:
|
||||
|
||||
- stable `palette_id`
|
||||
- persisted `name`
|
||||
- parent `schema_id`
|
||||
- schema `version`
|
||||
- concrete `colors`
|
||||
|
||||
### Palette Names
|
||||
|
||||
Palette names are persisted even when the user leaves the name field blank. The blank-name path resolves to a deterministic, simple generated name such as `snow white`, `fog twist`, or `ember bloom`.
|
||||
|
||||
The generated name is a convenience label only. Palette identity remains `palette_id`, and duplicate detection remains schema plus ordered colors.
|
||||
|
||||
### Original Palette Projection
|
||||
|
||||
Every ready schema projects a synthetic original palette from the schema's current `material_indices`.
|
||||
|
||||
This projection must always exist in the palette view, even if a user-authored palette has the same colors. A saved palette with the same schema and colors is still user-authored and can be deleted.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- Do not treat same colors as proof that a palette is original. Original means "synthetic schema projection", not "matches current schema colors".
|
||||
- Do not publish global asset refresh events for local schema or palette metadata changes. Use local metadata events unless pack/runtime state truly changed.
|
||||
- Do not let button handlers capture stale preview objects. Fetch the current coordinator view model at click time for destructive actions.
|
||||
- Do not let blank palette names remain blank on disk. Resolve the generated name before persisting.
|
||||
- Do not open the palette wizard for synthetic original palettes. They are read-only projections.
|
||||
|
||||
## Takeaways
|
||||
|
||||
- Schema formation and palette authoring are separate lifecycle stages.
|
||||
- Original palettes are derived views, not persisted editable records.
|
||||
- User-created palettes remain editable and removable even when they share a schema or colors with an original.
|
||||
- Local metadata events preserve form continuity better than full asset refreshes.
|
||||
@ -1,5 +1,5 @@
|
||||
---
|
||||
id: LSN-0022
|
||||
id: LSN-0043
|
||||
ticket: studio-play-stop-cartridge-flow
|
||||
title: Studio Play/Stop and Debugger Workspace Integration
|
||||
status: done
|
||||
@ -1,16 +0,0 @@
|
||||
# LSN-0039: Jenkins + JaCoCo Integration with Multi-Module Gradle
|
||||
|
||||
- **Problem:** O Jenkinsfile estava desatualizado, tentando rodar uma task de cobertura inexistente (`testCodeCoverageReport`) em um subprojeto específico, ignorando a cobertura global e causando falhas no pipeline.
|
||||
- **Decision:** Alinhar o `Jenkinsfile` com a task de agregação customizada no root project (`jacocoTestReport`), removendo a dependência de subprojetos específicos para métricas globais.
|
||||
- **Implementation:**
|
||||
- Atualizado o comando de execução no Jenkins: `./gradlew clean test jacocoTestReport`.
|
||||
- Ajustado o `recordCoverage` para ler o XML consolidado em `build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml`.
|
||||
- Ajustado o `publishHTML` para exibir o relatório consolidado em `build/reports/jacoco/jacocoTestReport/html`.
|
||||
- **Example:**
|
||||
- Antes: `SRS_APP/build/reports/...` (específico por app).
|
||||
- Depois: `build/reports/...` (global do projeto).
|
||||
- **Pitfalls:**
|
||||
- Ao usar relatórios consolidados no root, deve-se garantir que o Gradle rode a task de agregação *após* os testes de todos os subprojetos.
|
||||
- O Jenkins precisa de acesso ao workspace root para encontrar esses arquivos; caminhos relativos no `Jenkinsfile` geralmente partem do root do checkout.
|
||||
- **References:**
|
||||
- `DSC-0024`, `DEC-0025`, `PLN-0048`.
|
||||
@ -0,0 +1,45 @@
|
||||
---
|
||||
id: LSN-0044
|
||||
ticket: jacoco-reports-consolidation
|
||||
title: Jenkins Alignment with Consolidated JaCoCo Reports
|
||||
created: 2026-04-07
|
||||
tags:
|
||||
- infra
|
||||
- gradle
|
||||
- jacoco
|
||||
- jenkins
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The Jenkins pipeline was still wired to an outdated coverage task and module-specific report paths. That made CI brittle after the project moved coverage reporting to the root-level consolidated `jacocoTestReport` task.
|
||||
|
||||
## Key Decisions
|
||||
|
||||
### Jenkins Reads Root-Level Coverage
|
||||
|
||||
**What:** Jenkins should run `./gradlew clean test jacocoTestReport` and publish coverage from the root project report directory.
|
||||
|
||||
**Why:** Coverage is aggregated across modules, so CI should not depend on a single application subproject path.
|
||||
|
||||
**Trade-offs:** The root task must run after all relevant tests. Jenkins paths must be rooted at the checkout workspace rather than a submodule directory.
|
||||
|
||||
## Patterns and Algorithms
|
||||
|
||||
The canonical report locations are:
|
||||
|
||||
- XML: `build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml`
|
||||
- HTML: `build/reports/jacoco/jacocoTestReport/html`
|
||||
|
||||
Jenkins `recordCoverage` should consume the XML report. Jenkins `publishHTML` should expose the HTML directory.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- Do not point Jenkins at `SRS_APP/build/reports/...` or another module-local report when the project expects consolidated root coverage.
|
||||
- Do not run only the reporting task without ensuring tests have executed first.
|
||||
- Do not assume Jenkins relative paths start inside a Gradle subproject; treat the checkout root as the base.
|
||||
|
||||
## Takeaways
|
||||
|
||||
- Root-level coverage aggregation needs root-level CI report paths.
|
||||
- The Jenkinsfile and Gradle coverage task names must move together.
|
||||
@ -0,0 +1,103 @@
|
||||
---
|
||||
id: LSN-0045
|
||||
ticket: studio-tiled-parser-assets-scene-asset-type
|
||||
title: Asset-Owned Scene Bank and Tiled XML Workflow
|
||||
created: 2026-04-23
|
||||
tags:
|
||||
- studio
|
||||
- assets
|
||||
- scene-bank
|
||||
- tiled
|
||||
- tmx
|
||||
- tsx
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The abandoned scene-workspace direction was replaced by an asset-first model. Scene authoring and validation now belong to the `Assets` workspace, while heavy map editing remains external in Tiled.
|
||||
|
||||
The final model keeps runtime asset families disciplined:
|
||||
|
||||
- `Scene Bank` is the scene-facing asset managed by Studio.
|
||||
- `Glyph Bank` remains the base family for tilesets.
|
||||
- `Tileset`, `Sprites`, and `UI` are Studio-only glyph-bank specializations, not new runtime families.
|
||||
|
||||
## Key Decisions
|
||||
|
||||
### Scene Banks Are Assets, Not Workspaces
|
||||
|
||||
**What:** Scene-bank lifecycle is owned by the `Assets` workspace: creation, metadata, layer configuration, validation, acceptance, and diagnostics.
|
||||
|
||||
**Why:** The product direction explicitly discarded a dedicated scene workspace. Asset ownership keeps scene work aligned with existing asset actions, summaries, diagnostics, and refresh flows.
|
||||
|
||||
**Trade-offs:** Studio does not provide a heavy map editor in this wave. Users edit `TMX` and `TSX` externally in Tiled, then return to Studio for validation and acceptance.
|
||||
|
||||
### TMX and TSX Are the Wave-1 XML Surfaces
|
||||
|
||||
**What:** Wave 1 supports XML read/write for `TMX` map documents and `TSX` tileset documents.
|
||||
|
||||
**Why:** Tiled is the interoperability surface. Keeping `TMX` as the maintained scene document avoids inventing a parallel normalized scene schema too early.
|
||||
|
||||
**Trade-offs:** Unsupported Tiled features must fail explicitly rather than being silently normalized away.
|
||||
|
||||
### Tileset Specialization Stays Studio-Only
|
||||
|
||||
**What:** A `Glyph Bank / Tileset` specialization generates the Tiled-facing `TSX` document for that glyph bank.
|
||||
|
||||
**Why:** Tileset generation is editorial Studio behavior over a glyph bank. It should not leak into runtime family identity.
|
||||
|
||||
**Trade-offs:** The asset summary must show both the base type and specialization so users understand the role without confusing it for a new runtime asset family.
|
||||
|
||||
### Scene Validation Requires Explicit Acceptance
|
||||
|
||||
**What:** A scene bank is not ready merely because files exist. Studio validates the current `TMX`/`TSX` state and requires explicit acceptance of the converged result.
|
||||
|
||||
**Why:** External editing can change files behind Studio's back. Acceptance makes the boundary deliberate and auditable.
|
||||
|
||||
**Trade-offs:** Users must perform an extra validation/accept step after external edits, but Studio avoids silently blessing invalid or unsupported scene state.
|
||||
|
||||
## Patterns and Algorithms
|
||||
|
||||
### Scene-Bank Support Metadata
|
||||
|
||||
Scene-bank support metadata lives with the scene asset and records the wave-1 layer model:
|
||||
|
||||
- fixed `1..4` scene layers;
|
||||
- per-layer enabled state;
|
||||
- per-layer `tilemap` filename;
|
||||
- per-layer linked `tileset_asset_root`;
|
||||
- map dimensions and explicit layer-to-tilemap binding.
|
||||
|
||||
This support file is the Studio-owned convention that lets a scene bank contain multiple maintained `TMX` documents while preserving deterministic layer mapping.
|
||||
|
||||
### Tiled File Generation
|
||||
|
||||
`Tileset` glyph banks generate `TSX` files. Scene banks generate or maintain layer-specific `TMX` files that reference those generated `TSX` files.
|
||||
|
||||
Each wave-1 tilemap references exactly one generated `TSX`. Multi-tileset maps remain outside this wave.
|
||||
|
||||
### Validation and Acceptance Files
|
||||
|
||||
Scene validation records the inspected file state and diagnostics. Acceptance records the validated state that Studio has explicitly approved.
|
||||
|
||||
This creates a useful external-edit loop:
|
||||
|
||||
1. generate or edit `TMX`/`TSX`;
|
||||
2. validate in Studio;
|
||||
3. inspect diagnostics;
|
||||
4. accept only after validation succeeds.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- Do not reintroduce a dedicated scene workspace for wave 1.
|
||||
- Do not turn `Tileset` specialization into a runtime asset family.
|
||||
- Do not silently auto-accept external Tiled edits after refresh.
|
||||
- Do not treat `TMX`/`TSX` generation as pack publication. Runtime scene packing remains a separate later concern.
|
||||
- Do not allow unsupported Tiled constructs to disappear quietly. They must surface as diagnostics.
|
||||
|
||||
## Takeaways
|
||||
|
||||
- Scene Bank belongs in Assets, not in a separate authoring workspace.
|
||||
- `TMX` remains the maintained scene document for wave 1.
|
||||
- `TSX` generation belongs to `Glyph Bank / Tileset`.
|
||||
- Validation plus explicit acceptance is the core safety gate for external Tiled editing.
|
||||
@ -1,116 +0,0 @@
|
||||
---
|
||||
id: AGD-0002
|
||||
ticket: palette-management-in-studio
|
||||
title: Palette Management in Studio
|
||||
status: open
|
||||
created: 2026-03-26
|
||||
resolved:
|
||||
decision:
|
||||
tags:
|
||||
- studio
|
||||
- legacy-import
|
||||
- palette-management
|
||||
- tile-bank
|
||||
- packer-boundary
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
Studio already has a retained agenda for palette management in the `glyph bank` form session, but that agenda exists only under `docs/studio/agendas/` and therefore sits outside the discussion-framework lifecycle.
|
||||
|
||||
Without importing it into `discussion/`, the canonical discussion surface would lose the still-open Studio question about:
|
||||
|
||||
- how candidate palettes are presented;
|
||||
- how `1..64` palette capacity is enforced and explained;
|
||||
- how preview and selection behave inside the asset workflow;
|
||||
- how Studio UX stays separate from packer-owned extraction, validation, identity, and persistence.
|
||||
|
||||
## Context
|
||||
|
||||
Legacy source: `docs/studio/agendas/Palette Management in Studio Agenda.md`
|
||||
|
||||
Domain owner: `docs/studio`
|
||||
Cross-domain impact: `docs/packer`
|
||||
|
||||
The retained agenda is intentionally scoped to `glyph bank`.
|
||||
|
||||
The imported context keeps the same baseline:
|
||||
|
||||
- every `*.png` input of a glyph bank is one tile;
|
||||
- each tile yields one extracted palette during cache construction;
|
||||
- developers may also work with favorite palettes and asset palettes;
|
||||
- each bank may integrate at most `64` palettes;
|
||||
- each palette contains `16` colors with a default color-key baseline;
|
||||
- Studio owns curation UX, while packer remains authoritative for extraction, identity, validation, and persistence.
|
||||
|
||||
The current UI shape already converged on:
|
||||
|
||||
- a left-side transfer list for candidate and selected palettes;
|
||||
- custom palette rendering with swatches and source indicators;
|
||||
- a right-side preview panel with tile/file selector, tile preview, and applied palette strip.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] How should Studio detect and present duplicate or near-equivalent palettes across tile-derived, favorite, and asset-level sources?
|
||||
- [ ] Are favorites project-scoped references to existing palettes, or can they diverge into editable copies before bank admission?
|
||||
- [ ] What is the canonical user-facing identity of a candidate palette in Studio: source bucket plus logical name, file path, extraction origin tile, stable id, or a combination?
|
||||
- [ ] Should extracted tile palettes always enter the candidate set automatically, or can the developer suppress some of them before curation?
|
||||
- [ ] Should clicking a palette on the `selected` side be the only interaction needed to change the previewed palette in the first wave?
|
||||
- [ ] Which palette operations are read-only in the first wave, and which require staged preview/apply treatment?
|
||||
- [ ] Which parts of this interaction model should remain explicitly `glyph bank`-specific, and which parts are reusable Studio form-session primitives?
|
||||
- [ ] Does the first release need only an asset-scoped `glyph bank` section, or also a project-level palette browser for discovery and favorites management?
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Automatic Integration
|
||||
- **Approach:** Treat extracted tile palettes as implicit inputs and integrate them automatically without explicit user curation.
|
||||
- **Pro:** Lowest immediate UX cost.
|
||||
- **Con:** Hides palette-budget reasoning, weakens deliberate composition, and makes source distinctions harder to understand.
|
||||
- **Maintainability:** Weak. It pushes important bank rules into implicit behavior.
|
||||
|
||||
### Option B - Asset-Scoped Curated Selection
|
||||
- **Approach:** Treat bank palettes as an asset-scoped curated selection built from tile-derived palettes, favorites, and asset palettes, with Studio owning the curation UX and packer owning persistence.
|
||||
- **Pro:** Matches the concrete `glyph bank` workflow and keeps authority boundaries clear.
|
||||
- **Con:** Requires explicit UX for capacity, identity, deduplication, and preview semantics.
|
||||
- **Maintainability:** Strong. It makes responsibilities explicit and keeps family-specific rules close to the workflow that needs them.
|
||||
|
||||
### Option C - Studio-Owned Palette Library
|
||||
- **Approach:** Treat palettes as a broader Studio-owned library/workspace with direct local editing and persistence behavior.
|
||||
- **Pro:** Potentially flexible for future palette-heavy features.
|
||||
- **Con:** Duplicates packer ownership and risks creating a second authority for bank composition.
|
||||
- **Maintainability:** Weak. It expands Studio responsibility before the persistence contract is closed.
|
||||
|
||||
## Discussion
|
||||
|
||||
The imported agenda keeps the original recommendation intact: palette management should be first-class for `glyph bank`, but Studio must not become the persistence authority.
|
||||
|
||||
That means the key open problem is not whether palettes exist. It is how Studio explains and curates them safely:
|
||||
|
||||
- candidate aggregation from multiple sources;
|
||||
- clear source and identity display;
|
||||
- explicit `1..64` capacity feedback;
|
||||
- a preview model that stays tied to the selected bank palettes;
|
||||
- a boundary between `glyph bank`-specific UX and reusable Studio primitives.
|
||||
|
||||
The retained source already converged on a pragmatic first wave:
|
||||
|
||||
- transfer-list selection on the left;
|
||||
- palette-focused preview on the right;
|
||||
- first selected palette as default preview;
|
||||
- clicking on a selected palette changes the previewed palette;
|
||||
- removing the current preview palette falls back to the first remaining selected palette.
|
||||
|
||||
## Resolution
|
||||
|
||||
Recommended direction: adopt **Option B**.
|
||||
|
||||
The imported agenda preserves the retained Studio direction:
|
||||
|
||||
1. palette management belongs to the `glyph bank` asset workflow;
|
||||
2. Studio aggregates candidate palettes from tile-derived extraction, favorites, and asset palettes;
|
||||
3. the first wave uses a custom-rendered transfer list with an explicit `1..64` selected range;
|
||||
4. the right-side preview panel stays tied to the current selected palette set;
|
||||
5. packer remains authoritative for extraction, identity, validation, and persistence;
|
||||
6. broader standalone palette-authoring workflows remain deferred.
|
||||
|
||||
Next step suggestion: convert this imported agenda into a decision that closes the ownership boundary, candidate-source model, first-wave transfer-list UX, preview behavior, and reusable-vs-family-specific primitive boundary.
|
||||
@ -1,115 +0,0 @@
|
||||
---
|
||||
id: AGD-0030
|
||||
ticket: studio-tiled-parser-assets-scene-asset-type
|
||||
title: Tiled Parser and Scene Asset-Type Ownership in Assets Workspace
|
||||
status: open
|
||||
created: 2026-04-15
|
||||
resolved:
|
||||
decision:
|
||||
tags:
|
||||
- studio
|
||||
- assets
|
||||
- scene
|
||||
- tiled
|
||||
- parser
|
||||
- asset-type
|
||||
---
|
||||
|
||||
## Pain
|
||||
|
||||
The current abandoned direction assumed a dedicated `Scene Workspace` as the primary authoring owner for scenes.
|
||||
|
||||
That no longer matches the intended product direction.
|
||||
|
||||
Without a new direction:
|
||||
|
||||
- the Studio has no agreed path for scene authoring;
|
||||
- the `Assets` workspace still lacks a defined ownership model for scene-like content;
|
||||
- there is no agreed contract for importing or interpreting `Tiled` data;
|
||||
- implementation risks rebuilding a custom scene editor model that the team has already decided to discard.
|
||||
|
||||
## Context
|
||||
|
||||
Domain owner: `studio`
|
||||
|
||||
Expected documentation owner:
|
||||
|
||||
- `docs/studio/specs`
|
||||
|
||||
Cross-domain impact:
|
||||
|
||||
- asset typing and creation flows in the `Assets` workspace;
|
||||
- Studio-side parsing/import rules for `Tiled`;
|
||||
- possible downstream alignment with runtime-facing `SCENE` publication, depending on the chosen import boundary.
|
||||
|
||||
User direction explicitly provided on 2026-04-15:
|
||||
|
||||
- abandon `DEC-0026` and its derived plans;
|
||||
- do not proceed with a dedicated `Scene Workspace`;
|
||||
- instead, write a parser for `Tiled`;
|
||||
- create a new asset type managed from the `Assets` workspace.
|
||||
|
||||
This agenda exists to turn that direction into a disciplined technical recommendation rather than jumping directly into implementation with undefined ownership and file-contract assumptions.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- [ ] Which `Tiled` surface is the primary input contract: TMX XML, JSON export, TSX, or a constrained subset of those?
|
||||
- [ ] Is the new asset type a raw imported `Tiled` document, a normalized Studio-owned intermediate asset, or a published runtime-facing `SCENE` derivative?
|
||||
- [ ] Should `Assets` own only creation/import and metadata management, or also the main editing/reimport flow for this new asset type?
|
||||
- [ ] How should tileset references inside `Tiled` map onto existing glyph-bank assets and asset identity rules in Studio?
|
||||
- [ ] What parts of `Tiled` are intentionally out of scope for the first wave: object layers, properties, flips, collisions, infinite maps, templates, Wang sets, animations?
|
||||
- [ ] Does the parser need to preserve full round-trip fidelity with `Tiled`, or only import the supported subset into Studio-owned structures?
|
||||
- [ ] What is the canonical failure model when a `Tiled` document references unsupported constructs or broken external assets?
|
||||
|
||||
## Options
|
||||
|
||||
### Option A - Import `Tiled` into a new Studio-owned scene asset type in `Assets`
|
||||
- **Approach:** Introduce a new asset type in `Assets`; parse supported `Tiled` files into a Studio-owned asset model managed from that workspace.
|
||||
- **Pro:** Aligns with the requested ownership shift, keeps authoring/import centered in `Assets`, and allows Studio to define a strict supported subset.
|
||||
- **Con:** Requires defining a clean normalized asset contract and a mapping layer from `Tiled` concepts to Studio/runtime concepts.
|
||||
- **Maintainability:** Strong, if the supported subset and ownership boundary are made explicit early.
|
||||
|
||||
### Option B - Treat `Tiled` files as external source documents and keep Studio as a thin importer/viewer
|
||||
- **Approach:** Keep `Tiled` as the primary authoring format and let Studio mostly import, validate, and publish from it.
|
||||
- **Pro:** Minimizes Studio-owned scene-model invention and stays close to existing `Tiled` authoring workflows.
|
||||
- **Con:** Couples Studio behavior heavily to `Tiled`'s full document model and makes partial support/error handling harder to reason about.
|
||||
- **Maintainability:** Medium, because unsupported `Tiled` breadth can leak into every implementation phase.
|
||||
|
||||
### Option C - Create a new Studio asset type unrelated to `Tiled` and only offer one-time conversion
|
||||
- **Approach:** Use `Tiled` only as an optional bootstrap/import tool, then move fully into a separate Studio-native asset format.
|
||||
- **Pro:** Gives Studio maximum control over the final asset model and future editing behavior.
|
||||
- **Con:** Moves away from the user's stated direction of writing a `Tiled` parser as a central path and may reintroduce the same custom-model burden the previous direction had.
|
||||
- **Maintainability:** Medium, but only if the conversion boundary is very clearly justified.
|
||||
|
||||
## Discussion
|
||||
|
||||
The key architectural change here is not only "use `Tiled`".
|
||||
|
||||
It is also a change in ownership:
|
||||
|
||||
- scene-related authoring/import should no longer justify a dedicated `Scene Workspace`;
|
||||
- the `Assets` workspace becomes the management surface for the relevant asset type;
|
||||
- any parser choice must therefore fit an asset-first workflow instead of a separate editor-first workflow.
|
||||
|
||||
That creates several decisions that should be made deliberately:
|
||||
|
||||
1. what exact `Tiled` contract Studio supports in wave 1;
|
||||
2. whether Studio stores `Tiled` data directly or through a normalized asset form;
|
||||
3. how glyph banks and tilesets map without creating identity ambiguity;
|
||||
4. how much of `Tiled` fidelity is preserved versus normalized away;
|
||||
5. whether editing is a reimport/update flow, a direct managed-asset flow, or some hybrid.
|
||||
|
||||
There is already one strong constraint from the user:
|
||||
|
||||
- the previous `Scene Workspace` split should be considered discarded;
|
||||
- the new work should be organized around `Assets` plus a `Tiled` parser.
|
||||
|
||||
That makes Option A the current best fit for convergence, but it still needs the agenda to narrow the supported `Tiled` subset and the resulting asset contract before a new decision can be written.
|
||||
|
||||
## Resolution
|
||||
|
||||
Recommended next step:
|
||||
|
||||
- converge this agenda around a first-wave `Tiled` subset;
|
||||
- define whether the new asset type is raw-imported or normalized;
|
||||
- then write a replacement decision for asset ownership, parser boundary, and wave-1 scope.
|
||||
@ -4,6 +4,7 @@ import java.util.Locale;
|
||||
|
||||
public enum AssetFamilyCatalog {
|
||||
GLYPH_BANK("glyph_bank"),
|
||||
SCENE_BANK("scene_bank"),
|
||||
SOUND_BANK("sound_bank"),
|
||||
UNKNOWN("unknown");
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ import java.util.Locale;
|
||||
|
||||
public enum OutputFormatCatalog {
|
||||
GLYPH_INDEXED_V1(AssetFamilyCatalog.GLYPH_BANK, "GLYPH/indexed_v1", "GLYPH/indexed_v1"),
|
||||
SCENE_TILED_V1(AssetFamilyCatalog.SCENE_BANK, "SCENE/tilemap_v1", "SCENE/tilemap_v1"),
|
||||
SOUND_V1(AssetFamilyCatalog.SOUND_BANK, "SOUND/v1", "SOUND/v1"),
|
||||
UNKNOWN(AssetFamilyCatalog.UNKNOWN, "unknown", "Unknown");
|
||||
|
||||
@ -53,6 +54,9 @@ public enum OutputFormatCatalog {
|
||||
return UNKNOWN;
|
||||
}
|
||||
final String normalized = manifestValue.trim().toLowerCase(Locale.ROOT);
|
||||
if ("scene/tiled_v1".equals(normalized)) {
|
||||
return SCENE_TILED_V1;
|
||||
}
|
||||
if ("sound/bank_v1".equals(normalized)) {
|
||||
return SOUND_V1;
|
||||
}
|
||||
|
||||
@ -3,11 +3,15 @@ package p.packer.models;
|
||||
import java.util.Objects;
|
||||
|
||||
public record PackerAssetArtifactSelection(
|
||||
String artifactId,
|
||||
String file,
|
||||
int index) {
|
||||
int index,
|
||||
String fingerprint) {
|
||||
|
||||
public PackerAssetArtifactSelection {
|
||||
artifactId = artifactId == null || artifactId.isBlank() ? null : artifactId.trim();
|
||||
file = Objects.requireNonNull(file, "file").trim();
|
||||
fingerprint = fingerprint == null || fingerprint.isBlank() ? null : fingerprint.trim();
|
||||
if (file.isBlank()) {
|
||||
throw new IllegalArgumentException("file must not be blank");
|
||||
}
|
||||
|
||||
@ -100,6 +100,9 @@ public class PackerAssetWalker {
|
||||
diagnostics.addAll(walkResult.diagnostics());
|
||||
return new PackerWalkResult(walkResult.probeResults(), diagnostics);
|
||||
}
|
||||
case SCENE_BANK -> {
|
||||
return new PackerWalkResult(List.of(), diagnostics);
|
||||
}
|
||||
case UNKNOWN -> {
|
||||
diagnostics.add(new PackerDiagnostic(
|
||||
PackerDiagnosticSeverity.WARNING,
|
||||
|
||||
@ -8,6 +8,10 @@ import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
public final class PackerRuntimeAssetMaterializer {
|
||||
private static final Set<String> RUNTIME_RESERVED_FILE_NAMES = Set.of(
|
||||
"asset.json",
|
||||
"studio.asset.json");
|
||||
|
||||
private final PackerAssetWalker assetWalker;
|
||||
|
||||
public PackerRuntimeAssetMaterializer(PackerAssetWalker assetWalker) {
|
||||
@ -114,7 +118,7 @@ public final class PackerRuntimeAssetMaterializer {
|
||||
private List<String> listAvailableFiles(Path assetRoot) {
|
||||
try (var paths = Files.list(assetRoot)
|
||||
.filter(Files::isRegularFile)
|
||||
.filter(path -> !path.getFileName().toString().equalsIgnoreCase("asset.json"))
|
||||
.filter(path -> !isRuntimeReservedFile(path))
|
||||
.map(path -> assetRoot.relativize(path.toAbsolutePath().normalize()).toString().replace('\\', '/'))
|
||||
.sorted(String.CASE_INSENSITIVE_ORDER)) {
|
||||
return paths.toList();
|
||||
@ -123,6 +127,10 @@ public final class PackerRuntimeAssetMaterializer {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isRuntimeReservedFile(Path path) {
|
||||
return RUNTIME_RESERVED_FILE_NAMES.contains(path.getFileName().toString().toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
private PackerRuntimeWalkFile toRuntimeWalkFile(
|
||||
Path assetRoot,
|
||||
PackerProbeResult probeResult,
|
||||
|
||||
@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import p.packer.PackerWorkspacePaths;
|
||||
import p.packer.PackerWorkspaceService;
|
||||
import p.packer.events.PackerEventKind;
|
||||
@ -38,6 +39,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 final ObjectMapper mapper;
|
||||
private final PackerWorkspaceFoundation workspaceFoundation;
|
||||
@ -48,6 +50,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
private final FileSystemPackerCacheRepository cacheRepository;
|
||||
private final PackerProjectWriteCoordinator writeCoordinator;
|
||||
private final PackerEventSink eventSink;
|
||||
private final PackerGlyphMaterialSupport glyphMaterialSupport;
|
||||
|
||||
public FileSystemPackerWorkspaceService(
|
||||
ObjectMapper mapper,
|
||||
@ -68,6 +71,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
this.cacheRepository = Objects.requireNonNull(cacheRepository, "cacheRepository");
|
||||
this.writeCoordinator = Objects.requireNonNull(writeCoordinator, "writeCoordinator");
|
||||
this.eventSink = Objects.requireNonNull(eventSink, "eventSink");
|
||||
this.glyphMaterialSupport = new PackerGlyphMaterialSupport(mapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -81,7 +85,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
public ListAssetsResult listAssets(ListAssetsRequest request) {
|
||||
final var project = Objects.requireNonNull(request, "request").project();
|
||||
final var snapshot = request.deepSync()
|
||||
? runtimeRegistry.refresh(project).snapshot()
|
||||
? writeCoordinator.execute(project, () -> refreshAfterArtifactIdentityBackfill(project))
|
||||
: runtimeRegistry.getOrLoad(project).snapshot();
|
||||
final var events = new PackerOperationEventEmitter(project, eventSink);
|
||||
final var registry = snapshot.registry();
|
||||
@ -145,7 +149,11 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
|
||||
@Override
|
||||
public GetAssetDetailsResult getAssetDetails(GetAssetDetailsRequest request) {
|
||||
return detailsService.getAssetDetails(request);
|
||||
final GetAssetDetailsRequest safeRequest = Objects.requireNonNull(request, "request");
|
||||
return writeCoordinator.execute(safeRequest.project(), () -> {
|
||||
backfillLoadedArtifactIdentity(safeRequest.project());
|
||||
return detailsService.getAssetDetails(safeRequest);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -171,7 +179,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
@Override
|
||||
public ValidatePackWorkspaceResult validatePackWorkspace(ValidatePackWorkspaceRequest request) {
|
||||
final PackerProjectContext project = Objects.requireNonNull(request, "request").project();
|
||||
final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot();
|
||||
final PackerRuntimeSnapshot snapshot = writeCoordinator.execute(project, () -> refreshAfterArtifactIdentityBackfill(project));
|
||||
final List<PackerPackValidationAssetDTO> blockedAssets = snapshot.assets().stream()
|
||||
.filter(this::isEffectivelyIncludedInPackSet)
|
||||
.map(this::toPackValidationAssetDTO)
|
||||
@ -287,6 +295,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
final PackerRegistryEntry entry = workspaceFoundation.allocateIdentity(project, registry, assetRoot);
|
||||
Files.createDirectories(assetRoot);
|
||||
writeManifest(manifestPath, request, entry.assetUuid());
|
||||
writeStudioSupportFiles(assetRoot, request);
|
||||
final PackerRegistryState updated = workspaceFoundation.appendAllocatedEntry(registry, entry);
|
||||
workspaceFoundation.saveRegistry(project, updated);
|
||||
final var runtime = runtimeRegistry.update(project, (snapshot, generation) -> runtimePatchService.afterCreateAsset(
|
||||
@ -320,7 +329,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
final long startedAt = System.currentTimeMillis();
|
||||
workspaceFoundation.initWorkspace(new InitWorkspaceRequest(project));
|
||||
|
||||
final PackerRuntimeSnapshot refreshedSnapshot = runtimeRegistry.refresh(project).snapshot();
|
||||
final PackerRuntimeSnapshot refreshedSnapshot = refreshAfterArtifactIdentityBackfill(project);
|
||||
final PackerRuntimeSnapshot frozenSnapshot = createFrozenPackingSnapshot(refreshedSnapshot);
|
||||
final List<PackerPackValidationAssetDTO> blockedAssets = frozenSnapshot.assets().stream()
|
||||
.filter(this::isEffectivelyIncludedInPackSet)
|
||||
@ -498,6 +507,12 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
}
|
||||
|
||||
try {
|
||||
if (request.includedInBuild() && !sceneBankCanEnterBuild(assetRoot)) {
|
||||
return new UpdateAssetBuildParticipationResult(
|
||||
PackerOperationStatus.FAILED,
|
||||
"Scene Bank must enable at least one layer before entering the build.",
|
||||
null);
|
||||
}
|
||||
final PackerRegistryEntry currentEntry = evaluation.resolved().registryEntry().orElseThrow();
|
||||
final PackerRegistryEntry updatedEntry = new PackerRegistryEntry(
|
||||
currentEntry.assetId(),
|
||||
@ -667,6 +682,57 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
|
||||
}
|
||||
|
||||
private void writeStudioSupportFiles(Path assetRoot, CreateAssetRequest request) throws IOException {
|
||||
if (request.assetFamily() != AssetFamilyCatalog.SCENE_BANK) {
|
||||
return;
|
||||
}
|
||||
final Map<String, Object> supportFile = new LinkedHashMap<>();
|
||||
supportFile.put("schema_version", 1);
|
||||
supportFile.put("map_width", 16);
|
||||
supportFile.put("map_height", 16);
|
||||
supportFile.put("layer_count", 4);
|
||||
final List<Map<String, Object>> layers = new ArrayList<>();
|
||||
for (int index = 1; index <= 4; index += 1) {
|
||||
layers.add(Map.of(
|
||||
"index", index,
|
||||
"enabled", false,
|
||||
"name", "Layer " + index,
|
||||
"width", 16,
|
||||
"height", 16,
|
||||
"tilemap", "layer-" + index + ".tmx",
|
||||
"tileset_asset_root", ""));
|
||||
}
|
||||
supportFile.put("layers", layers);
|
||||
mapper.writerWithDefaultPrettyPrinter().writeValue(
|
||||
assetRoot.resolve(SCENE_BANK_SUPPORT_FILE).toFile(),
|
||||
supportFile);
|
||||
}
|
||||
|
||||
private boolean sceneBankCanEnterBuild(Path assetRoot) {
|
||||
final Path supportFilePath = assetRoot.resolve(SCENE_BANK_SUPPORT_FILE);
|
||||
if (!Files.isRegularFile(supportFilePath)) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
final JsonNode root = mapper.readTree(supportFilePath.toFile());
|
||||
if (!(root instanceof ObjectNode objectNode)) {
|
||||
return false;
|
||||
}
|
||||
final JsonNode layersNode = objectNode.get("layers");
|
||||
if (!(layersNode instanceof ArrayNode layers)) {
|
||||
return false;
|
||||
}
|
||||
for (JsonNode layer : layers) {
|
||||
if (layer.path("enabled").asBoolean(false)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (IOException exception) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeRelativeAssetRoot(String candidate) {
|
||||
final String raw = Objects.requireNonNullElse(candidate, "").trim().replace('\\', '/');
|
||||
if (raw.isBlank()) {
|
||||
@ -781,6 +847,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
parsed.diagnostics(),
|
||||
runtimeAsset.walkDiagnostics(),
|
||||
collectPackRelevantWalkFileDiagnostics(runtimeAsset),
|
||||
glyphMaterialSupport.validate(runtimeAsset),
|
||||
packExecutionSupportDiagnostics(parsed.declaration(), runtimeAsset.manifestPath()),
|
||||
identityMismatchDiagnostics(registryEntry, parsed, runtimeAsset.manifestPath()))
|
||||
.flatMap(Collection::stream)
|
||||
@ -831,6 +898,84 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
cacheRepository.save(project, snapshot.cacheState());
|
||||
}
|
||||
|
||||
private PackerRuntimeSnapshot refreshAfterArtifactIdentityBackfill(PackerProjectContext project) {
|
||||
final PackerRuntimeSnapshot snapshot = runtimeRegistry.refresh(project).snapshot();
|
||||
return backfillArtifactIdentity(snapshot)
|
||||
? runtimeRegistry.refresh(project).snapshot()
|
||||
: snapshot;
|
||||
}
|
||||
|
||||
private PackerRuntimeSnapshot backfillLoadedArtifactIdentity(PackerProjectContext project) {
|
||||
final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot();
|
||||
return backfillArtifactIdentity(snapshot)
|
||||
? runtimeRegistry.refresh(project).snapshot()
|
||||
: snapshot;
|
||||
}
|
||||
|
||||
private boolean backfillArtifactIdentity(PackerRuntimeSnapshot snapshot) {
|
||||
boolean changed = false;
|
||||
for (PackerRuntimeAsset runtimeAsset : snapshot.assets()) {
|
||||
try {
|
||||
changed = backfillArtifactIdentity(runtimeAsset) || changed;
|
||||
} catch (IOException exception) {
|
||||
throw new IllegalStateException("Unable to update artifact fingerprints for " + runtimeAsset.manifestPath(), exception);
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private boolean backfillArtifactIdentity(PackerRuntimeAsset runtimeAsset) throws IOException {
|
||||
if (runtimeAsset.parsedDeclaration().declaration() == null || runtimeAsset.parsedDeclaration().declaration().artifacts().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
final Path manifestPath = runtimeAsset.manifestPath();
|
||||
if (!Files.isRegularFile(manifestPath)) {
|
||||
return false;
|
||||
}
|
||||
final JsonNode rawManifest = mapper.readTree(manifestPath.toFile());
|
||||
if (!(rawManifest instanceof ObjectNode manifest) || !(manifest.path("artifacts") instanceof ArrayNode artifactsNode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final Map<String, PackerRuntimeWalkFile> walkFilesByPath = new LinkedHashMap<>();
|
||||
runtimeAsset.walkProjection().buildCandidateFiles().forEach(file -> walkFilesByPath.put(file.relativePath(), file));
|
||||
final Set<String> artifactIds = new HashSet<>();
|
||||
for (JsonNode artifactNode : artifactsNode) {
|
||||
if (artifactNode.isObject()) {
|
||||
final String artifactId = artifactNode.path("artifact_id").asText("").trim();
|
||||
if (!artifactId.isBlank()) {
|
||||
artifactIds.add(artifactId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean changed = false;
|
||||
for (JsonNode artifactNode : artifactsNode) {
|
||||
if (!(artifactNode instanceof ObjectNode artifactObject)) {
|
||||
continue;
|
||||
}
|
||||
final String file = artifactObject.path("file").asText("").trim();
|
||||
if (file.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
if (artifactObject.path("artifact_id").asText("").isBlank()) {
|
||||
artifactObject.put("artifact_id", nextArtifactId(artifactIds));
|
||||
changed = true;
|
||||
}
|
||||
final String fingerprint = Optional.ofNullable(walkFilesByPath.get(file))
|
||||
.map(PackerRuntimeWalkFile::fingerprint)
|
||||
.orElse(null);
|
||||
if (fingerprint != null && !fingerprint.isBlank() && !fingerprint.equals(artifactObject.path("fingerprint").asText(null))) {
|
||||
artifactObject.put("fingerprint", fingerprint);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private PackerRuntimeSnapshot createFrozenPackingSnapshot(PackerRuntimeSnapshot snapshot) {
|
||||
final PackerRuntimeAssetMaterializer materializer = new PackerRuntimeAssetMaterializer(new PackerAssetWalker(mapper));
|
||||
final List<PackerRuntimeAsset> assets = snapshot.assets().stream()
|
||||
@ -1256,7 +1401,7 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
}
|
||||
|
||||
try {
|
||||
patchManifestArtifacts(manifest, request);
|
||||
patchManifestArtifacts(manifest, request, evaluation.resolved().runtimeAsset());
|
||||
mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
|
||||
final var runtime = runtimeRegistry.update(project, (currentSnapshot, generation) -> runtimePatchService.afterUpdateAssetContract(
|
||||
currentSnapshot,
|
||||
@ -1367,21 +1512,81 @@ public final class FileSystemPackerWorkspaceService implements PackerWorkspaceSe
|
||||
});
|
||||
}
|
||||
|
||||
private void patchManifestArtifacts(ObjectNode manifest, ApplyBankCompositionRequest request) {
|
||||
private void patchManifestArtifacts(
|
||||
ObjectNode manifest,
|
||||
ApplyBankCompositionRequest request,
|
||||
Optional<PackerRuntimeAsset> runtimeAsset) {
|
||||
manifest.remove("inputs");
|
||||
|
||||
final Map<String, ExistingArtifactEntry> existingArtifactsByFile = existingArtifactsByFile(manifest.path("artifacts"));
|
||||
final Map<String, PackerRuntimeWalkFile> walkFilesByPath = new LinkedHashMap<>();
|
||||
runtimeAsset.ifPresent(asset -> asset.walkProjection().buildCandidateFiles().forEach(file -> walkFilesByPath.put(file.relativePath(), file)));
|
||||
final var artifactsNode = manifest.putArray("artifacts");
|
||||
for (int index = 0; index < request.selectedFiles().size(); index += 1) {
|
||||
final String file = Objects.requireNonNullElse(request.selectedFiles().get(index), "").trim();
|
||||
if (file.isBlank() || !isTrustedRelativePath(file)) {
|
||||
throw new IllegalArgumentException("Selected artifact file paths must stay inside the asset root.");
|
||||
}
|
||||
final ExistingArtifactEntry existing = existingArtifactsByFile.get(file);
|
||||
final var artifactNode = artifactsNode.addObject();
|
||||
artifactNode.put("artifact_id", existing != null && existing.artifactId() != null ? existing.artifactId() : nextArtifactId());
|
||||
artifactNode.put("file", file);
|
||||
artifactNode.put("index", index);
|
||||
final String fingerprint = Optional.ofNullable(walkFilesByPath.get(file))
|
||||
.map(PackerRuntimeWalkFile::fingerprint)
|
||||
.orElse(existing == null ? null : existing.fingerprint());
|
||||
if (fingerprint != null && !fingerprint.isBlank()) {
|
||||
artifactNode.put("fingerprint", fingerprint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, ExistingArtifactEntry> existingArtifactsByFile(JsonNode artifactsNode) {
|
||||
if (!(artifactsNode instanceof ArrayNode artifactsArray)) {
|
||||
return Map.of();
|
||||
}
|
||||
final Map<String, ExistingArtifactEntry> existing = new LinkedHashMap<>();
|
||||
for (JsonNode entry : artifactsArray) {
|
||||
if (!entry.isObject()) {
|
||||
continue;
|
||||
}
|
||||
final String file = entry.path("file").asText("").trim();
|
||||
if (file.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
existing.put(file, new ExistingArtifactEntry(
|
||||
blankToNull(entry.path("artifact_id").asText(null)),
|
||||
blankToNull(entry.path("fingerprint").asText(null))));
|
||||
}
|
||||
return Map.copyOf(existing);
|
||||
}
|
||||
|
||||
private String nextArtifactId() {
|
||||
return "art-" + UUID.randomUUID().toString().replace("-", "").substring(0, 12);
|
||||
}
|
||||
|
||||
private String nextArtifactId(Set<String> existingArtifactIds) {
|
||||
String artifactId = nextArtifactId();
|
||||
while (existingArtifactIds.contains(artifactId)) {
|
||||
artifactId = nextArtifactId();
|
||||
}
|
||||
existingArtifactIds.add(artifactId);
|
||||
return artifactId;
|
||||
}
|
||||
|
||||
private String blankToNull(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
final String trimmed = value.trim();
|
||||
return trimmed.isBlank() ? null : trimmed;
|
||||
}
|
||||
|
||||
private record ExistingArtifactEntry(
|
||||
String artifactId,
|
||||
String fingerprint) {
|
||||
}
|
||||
|
||||
private void patchManifestPipelinePalettes(ObjectNode manifest, ApplyPaletteOverhaulingRequest request) {
|
||||
final ObjectNode outputNode = mutableObject(manifest, "output");
|
||||
final ObjectNode pipelineNode = mutableObject(outputNode, "pipeline");
|
||||
|
||||
@ -121,7 +121,7 @@ public final class PackerAssetDeclarationParser {
|
||||
diagnostics.add(new PackerDiagnostic(
|
||||
PackerDiagnosticSeverity.ERROR,
|
||||
PackerDiagnosticCategory.STRUCTURAL,
|
||||
"Field 'type' must be one of: glyph_bank, palette_bank, sound_bank.",
|
||||
"Field 'type' must be one of: glyph_bank, scene_bank, sound_bank.",
|
||||
manifestPath,
|
||||
true));
|
||||
return null;
|
||||
@ -404,8 +404,10 @@ public final class PackerAssetDeclarationParser {
|
||||
true));
|
||||
continue;
|
||||
}
|
||||
final JsonNode artifactIdNode = entry.path("artifact_id");
|
||||
final JsonNode fileNode = entry.path("file");
|
||||
final JsonNode indexNode = entry.path("index");
|
||||
final JsonNode fingerprintNode = entry.path("fingerprint");
|
||||
if (!fileNode.isTextual() || fileNode.asText().isBlank() || !indexNode.isInt() || indexNode.asInt() < 0) {
|
||||
diagnostics.add(new PackerDiagnostic(
|
||||
PackerDiagnosticSeverity.ERROR,
|
||||
@ -425,7 +427,13 @@ public final class PackerAssetDeclarationParser {
|
||||
true));
|
||||
continue;
|
||||
}
|
||||
result.add(new PackerAssetArtifactSelection(relativePath, indexNode.asInt()));
|
||||
final String artifactId = artifactIdNode.isTextual() && !artifactIdNode.asText().isBlank()
|
||||
? artifactIdNode.asText().trim()
|
||||
: null;
|
||||
final String fingerprint = fingerprintNode.isTextual() && !fingerprintNode.asText().isBlank()
|
||||
? fingerprintNode.asText().trim()
|
||||
: null;
|
||||
result.add(new PackerAssetArtifactSelection(artifactId, relativePath, indexNode.asInt(), fingerprint));
|
||||
}
|
||||
result.sort(Comparator.comparingInt(PackerAssetArtifactSelection::index));
|
||||
return List.copyOf(result);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package p.packer.services;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
@ -16,6 +17,8 @@ import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
public final class PackerAssetDetailsService {
|
||||
private static final ObjectMapper GLYPH_MATERIAL_MAPPER = new ObjectMapper();
|
||||
|
||||
private final PackerRuntimeRegistry runtimeRegistry;
|
||||
private final PackerAssetReferenceResolver assetReferenceResolver;
|
||||
|
||||
@ -48,6 +51,7 @@ public final class PackerAssetDetailsService {
|
||||
diagnostics.addAll(parsed.diagnostics());
|
||||
diagnostics.addAll(runtimeAsset.walkDiagnostics());
|
||||
appendWalkFileDiagnostics(runtimeAsset, diagnostics);
|
||||
diagnostics.addAll(new PackerGlyphMaterialSupport(GLYPH_MATERIAL_MAPPER).validate(runtimeAsset));
|
||||
if (!parsed.valid()) {
|
||||
return failureResult(project, request.assetReference(), resolved, diagnostics);
|
||||
}
|
||||
@ -149,8 +153,9 @@ public final class PackerAssetDetailsService {
|
||||
.map(this::toBankCompositionFile)
|
||||
.toList();
|
||||
|
||||
final List<PackerBankCompositionFile> selectedFiles = selectedPathsFor(declaration).stream()
|
||||
.map(path -> resolveSelectedBankFile(runtimeAsset.assetRoot(), path, walkFilesByPath, assetCacheEntry))
|
||||
final List<PackerBankCompositionFile> selectedFiles = declaration.artifacts().stream()
|
||||
.sorted(Comparator.comparingInt(PackerAssetArtifactSelection::index))
|
||||
.map(artifact -> resolveSelectedBankFile(runtimeAsset.assetRoot(), artifact, walkFilesByPath, assetCacheEntry))
|
||||
.flatMap(Optional::stream)
|
||||
.toList();
|
||||
|
||||
@ -160,13 +165,6 @@ public final class PackerAssetDetailsService {
|
||||
walkProjection.measuredBankSizeBytes());
|
||||
}
|
||||
|
||||
private List<String> selectedPathsFor(PackerAssetDeclaration declaration) {
|
||||
return declaration.artifacts().stream()
|
||||
.sorted(Comparator.comparingInt(PackerAssetArtifactSelection::index))
|
||||
.map(PackerAssetArtifactSelection::file)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> resolvePipelinePalettes(PackerAssetDeclaration declaration) {
|
||||
final JsonNode palettesNode = declaration.outputPipelineMetadata().get("palettes");
|
||||
if (!(palettesNode instanceof ArrayNode palettesArray)) {
|
||||
@ -219,16 +217,17 @@ public final class PackerAssetDetailsService {
|
||||
|
||||
private Optional<PackerBankCompositionFile> resolveSelectedBankFile(
|
||||
Path assetRoot,
|
||||
String relativePath,
|
||||
PackerAssetArtifactSelection artifact,
|
||||
Map<String, PackerRuntimeWalkFile> walkFilesByPath,
|
||||
Optional<PackerAssetCacheEntry> assetCacheEntry) {
|
||||
final String relativePath = artifact.file();
|
||||
final PackerRuntimeWalkFile runtimeWalkFile = walkFilesByPath.get(relativePath);
|
||||
if (runtimeWalkFile != null) {
|
||||
return Optional.of(toBankCompositionFile(runtimeWalkFile));
|
||||
return Optional.of(toSelectedBankCompositionFile(runtimeWalkFile, artifact));
|
||||
}
|
||||
final Optional<PackerFileCacheEntry> cachedFile = assetCacheEntry.flatMap(entry -> entry.findFile(relativePath));
|
||||
if (cachedFile.isPresent()) {
|
||||
return Optional.of(toBankCompositionFile(cachedFile.get()));
|
||||
return Optional.of(toSelectedBankCompositionFile(cachedFile.get(), artifact));
|
||||
}
|
||||
|
||||
final Path filePath = assetRoot.resolve(relativePath).toAbsolutePath().normalize();
|
||||
@ -242,8 +241,8 @@ public final class PackerAssetDetailsService {
|
||||
filePath.getFileName().toString(),
|
||||
Files.size(filePath),
|
||||
Files.getLastModifiedTime(filePath).toMillis(),
|
||||
null,
|
||||
Map.of()));
|
||||
artifact.fingerprint(),
|
||||
selectedArtifactMetadata(artifact, Map.of())));
|
||||
} catch (Exception exception) {
|
||||
return Optional.empty();
|
||||
}
|
||||
@ -269,6 +268,43 @@ public final class PackerAssetDetailsService {
|
||||
file.metadata());
|
||||
}
|
||||
|
||||
private PackerBankCompositionFile toSelectedBankCompositionFile(
|
||||
PackerRuntimeWalkFile file,
|
||||
PackerAssetArtifactSelection artifact) {
|
||||
return new PackerBankCompositionFile(
|
||||
file.relativePath(),
|
||||
Path.of(file.relativePath()).getFileName().toString(),
|
||||
file.size(),
|
||||
file.lastModified(),
|
||||
file.fingerprint(),
|
||||
selectedArtifactMetadata(artifact, file.metadata()));
|
||||
}
|
||||
|
||||
private PackerBankCompositionFile toSelectedBankCompositionFile(
|
||||
PackerFileCacheEntry file,
|
||||
PackerAssetArtifactSelection artifact) {
|
||||
return new PackerBankCompositionFile(
|
||||
file.relativePath(),
|
||||
Path.of(file.relativePath()).getFileName().toString(),
|
||||
file.size(),
|
||||
file.lastModified(),
|
||||
file.fingerprint(),
|
||||
selectedArtifactMetadata(artifact, file.metadata()));
|
||||
}
|
||||
|
||||
private Map<String, Object> selectedArtifactMetadata(
|
||||
PackerAssetArtifactSelection artifact,
|
||||
Map<String, Object> metadata) {
|
||||
final Map<String, Object> decorated = new LinkedHashMap<>(metadata);
|
||||
if (artifact.artifactId() != null) {
|
||||
decorated.put("artifactId", artifact.artifactId());
|
||||
}
|
||||
if (artifact.fingerprint() != null) {
|
||||
decorated.put("selectedFingerprint", artifact.fingerprint());
|
||||
}
|
||||
return Map.copyOf(decorated);
|
||||
}
|
||||
|
||||
private List<PackerCodecConfigurationField> metadataFields(
|
||||
List<PackerCodecConfigurationField> definitions,
|
||||
Map<String, String> outputMetadata) {
|
||||
|
||||
@ -0,0 +1,333 @@
|
||||
package p.packer.services;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||
import p.packer.messages.diagnostics.PackerDiagnosticCategory;
|
||||
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
|
||||
import p.packer.models.PackerAssetArtifactSelection;
|
||||
import p.packer.models.PackerAssetDeclaration;
|
||||
import p.packer.models.PackerDiagnostic;
|
||||
import p.packer.models.PackerRuntimeAsset;
|
||||
import p.packer.models.PackerRuntimeWalkFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
final class PackerGlyphMaterialSupport {
|
||||
static final String GLYPH_MATERIAL_SUPPORT_FILE = "glyph-material.json";
|
||||
|
||||
private final ObjectMapper mapper;
|
||||
|
||||
PackerGlyphMaterialSupport(ObjectMapper mapper) {
|
||||
this.mapper = Objects.requireNonNull(mapper, "mapper");
|
||||
}
|
||||
|
||||
List<PackerDiagnostic> validate(PackerRuntimeAsset runtimeAsset) {
|
||||
final PackerAssetDeclaration declaration = runtimeAsset.parsedDeclaration().declaration();
|
||||
if (declaration == null || declaration.assetFamily() != AssetFamilyCatalog.GLYPH_BANK) {
|
||||
return List.of();
|
||||
}
|
||||
final Path supportFile = runtimeAsset.assetRoot().resolve(GLYPH_MATERIAL_SUPPORT_FILE);
|
||||
if (!Files.isRegularFile(supportFile)) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
final List<PackerDiagnostic> diagnostics = new ArrayList<>();
|
||||
final Optional<GlyphMaterialFile> parsed = readSupportFile(supportFile, diagnostics);
|
||||
if (parsed.isEmpty()) {
|
||||
return List.copyOf(diagnostics);
|
||||
}
|
||||
|
||||
final Map<String, PackerAssetArtifactSelection> selectedArtifactsById = new LinkedHashMap<>();
|
||||
for (PackerAssetArtifactSelection artifact : declaration.artifacts()) {
|
||||
if (artifact.artifactId() != null) {
|
||||
selectedArtifactsById.put(artifact.artifactId(), artifact);
|
||||
}
|
||||
}
|
||||
final Map<String, PackerRuntimeWalkFile> selectedFilesByPath = new LinkedHashMap<>();
|
||||
runtimeAsset.walkProjection().buildCandidateFiles().forEach(file -> selectedFilesByPath.put(file.relativePath(), file));
|
||||
|
||||
final Map<String, SchemaEvaluation> schemaEvaluations = new LinkedHashMap<>();
|
||||
for (GlyphMaterialSchema schema : parsed.get().schemas()) {
|
||||
final boolean declaredInconsistent = schema.status() == GlyphMaterialSchemaStatus.INCONSISTENT;
|
||||
boolean drifted = false;
|
||||
for (GlyphMaterialArtifactBinding binding : schema.artifactBindings()) {
|
||||
final PackerAssetArtifactSelection artifact = selectedArtifactsById.get(binding.artifactId());
|
||||
if (artifact == null) {
|
||||
diagnostics.add(blocking(
|
||||
"Glyph material schema '" + schema.schemaId() + "' references unknown selected artifact id '" + binding.artifactId() + "'.",
|
||||
supportFile));
|
||||
drifted = true;
|
||||
continue;
|
||||
}
|
||||
final PackerRuntimeWalkFile currentFile = selectedFilesByPath.get(artifact.file());
|
||||
final String currentFingerprint = currentFile != null
|
||||
? currentFile.fingerprint()
|
||||
: artifact.fingerprint();
|
||||
if (currentFingerprint == null || !currentFingerprint.equals(binding.fingerprint())) {
|
||||
diagnostics.add(blocking(
|
||||
"Glyph material schema '" + schema.schemaId() + "' is inconsistent because artifact '" + binding.artifactId() + "' drifted.",
|
||||
supportFile));
|
||||
drifted = true;
|
||||
}
|
||||
}
|
||||
if (declaredInconsistent) {
|
||||
diagnostics.add(blocking(
|
||||
"Glyph material schema '" + schema.schemaId() + "' is marked inconsistent.",
|
||||
supportFile));
|
||||
}
|
||||
schemaEvaluations.put(schema.schemaId(), new SchemaEvaluation(schema, declaredInconsistent || drifted));
|
||||
}
|
||||
|
||||
for (GlyphMaterialPalette palette : parsed.get().palettes()) {
|
||||
final SchemaEvaluation schema = schemaEvaluations.get(palette.schemaId());
|
||||
if (schema == null) {
|
||||
diagnostics.add(blocking(
|
||||
"Glyph material palette '" + palette.paletteId() + "' references unknown schema '" + palette.schemaId() + "'.",
|
||||
supportFile));
|
||||
continue;
|
||||
}
|
||||
if (palette.colors().size() != schema.schema().materialIndices().size()) {
|
||||
diagnostics.add(blocking(
|
||||
"Glyph material palette '" + palette.paletteId() + "' must define exactly "
|
||||
+ schema.schema().materialIndices().size() + " colors for schema '" + palette.schemaId() + "'.",
|
||||
supportFile));
|
||||
}
|
||||
if (palette.version() < schema.schema().version()) {
|
||||
diagnostics.add(blocking(
|
||||
"Glyph material palette '" + palette.paletteId() + "' is stale because version "
|
||||
+ palette.version() + " is older than schema '" + palette.schemaId() + "' version "
|
||||
+ schema.schema().version() + ".",
|
||||
supportFile));
|
||||
}
|
||||
if (schema.inconsistent()) {
|
||||
diagnostics.add(blocking(
|
||||
"Glyph material palette '" + palette.paletteId() + "' is invalid because schema '"
|
||||
+ palette.schemaId() + "' is inconsistent.",
|
||||
supportFile));
|
||||
}
|
||||
}
|
||||
|
||||
return List.copyOf(diagnostics);
|
||||
}
|
||||
|
||||
private Optional<GlyphMaterialFile> readSupportFile(Path supportFile, List<PackerDiagnostic> diagnostics) {
|
||||
final JsonNode root;
|
||||
try {
|
||||
root = mapper.readTree(supportFile.toFile());
|
||||
} catch (IOException exception) {
|
||||
diagnostics.add(blocking("Unable to parse glyph-material.json: " + exception.getMessage(), supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
if (root == null || !root.isObject()) {
|
||||
diagnostics.add(blocking("glyph-material.json must contain a JSON object at the root.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
if (!(root.path("schemas") instanceof ArrayNode schemasNode) || !(root.path("palettes") instanceof ArrayNode palettesNode)) {
|
||||
diagnostics.add(blocking("glyph-material.json must define array fields 'schemas' and 'palettes'.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
final List<GlyphMaterialSchema> schemas = new ArrayList<>();
|
||||
final Set<String> schemaIds = new HashSet<>();
|
||||
for (JsonNode schemaNode : schemasNode) {
|
||||
final Optional<GlyphMaterialSchema> schema = parseSchema(schemaNode, supportFile, diagnostics);
|
||||
if (schema.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
if (!schemaIds.add(schema.get().schemaId())) {
|
||||
diagnostics.add(blocking("glyph-material.json contains duplicate schema_id '" + schema.get().schemaId() + "'.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
schemas.add(schema.get());
|
||||
}
|
||||
|
||||
final List<GlyphMaterialPalette> palettes = new ArrayList<>();
|
||||
final Set<String> paletteIds = new HashSet<>();
|
||||
for (JsonNode paletteNode : palettesNode) {
|
||||
final Optional<GlyphMaterialPalette> palette = parsePalette(paletteNode, schemaIds, supportFile, diagnostics);
|
||||
if (palette.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
if (!paletteIds.add(palette.get().paletteId())) {
|
||||
diagnostics.add(blocking("glyph-material.json contains duplicate palette_id '" + palette.get().paletteId() + "'.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
palettes.add(palette.get());
|
||||
}
|
||||
return Optional.of(new GlyphMaterialFile(List.copyOf(schemas), List.copyOf(palettes)));
|
||||
}
|
||||
|
||||
private Optional<GlyphMaterialSchema> parseSchema(
|
||||
JsonNode schemaNode,
|
||||
Path supportFile,
|
||||
List<PackerDiagnostic> diagnostics) {
|
||||
if (!schemaNode.isObject()) {
|
||||
diagnostics.add(blocking("Each glyph material schema must be an object.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
final String schemaId = schemaNode.path("schema_id").asText("").trim();
|
||||
final GlyphMaterialSchemaStatus status = GlyphMaterialSchemaStatus.fromManifestValue(schemaNode.path("status").asText(null));
|
||||
final int version = schemaNode.path("version").asInt(0);
|
||||
if (schemaId.isBlank() || status == null || version <= 0) {
|
||||
diagnostics.add(blocking("Each glyph material schema must define non-blank 'schema_id', valid 'status', and positive 'version'.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
final JsonNode materialIndicesNode = schemaNode.path("material_indices");
|
||||
if (!materialIndicesNode.isObject()) {
|
||||
diagnostics.add(blocking("Schema '" + schemaId + "' must define object 'material_indices'.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
final Map<Integer, String> materialIndices = new TreeMap<>();
|
||||
materialIndicesNode.fields().forEachRemaining(entry -> {
|
||||
try {
|
||||
final int index = Integer.parseInt(entry.getKey());
|
||||
final String color = normalizeHtmlColor(entry.getValue().asText(null));
|
||||
if (index >= 1 && index <= 15 && color != null) {
|
||||
materialIndices.put(index, color);
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
// validated below
|
||||
}
|
||||
});
|
||||
if (materialIndices.isEmpty()
|
||||
|| materialIndices.size() != materialIndicesNode.size()
|
||||
|| !denseIndices(materialIndices.keySet())) {
|
||||
diagnostics.add(blocking("Schema '" + schemaId + "' must define dense material_indices in range 1..15 using #RRGGBB values.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
if (!(schemaNode.path("artifact_bindings") instanceof ArrayNode bindingsNode) || bindingsNode.isEmpty()) {
|
||||
diagnostics.add(blocking("Schema '" + schemaId + "' must define non-empty 'artifact_bindings'.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
final List<GlyphMaterialArtifactBinding> bindings = new ArrayList<>();
|
||||
final Set<String> artifactIds = new HashSet<>();
|
||||
for (JsonNode bindingNode : bindingsNode) {
|
||||
final String artifactId = bindingNode.path("artifact_id").asText("").trim();
|
||||
final String fingerprint = bindingNode.path("fingerprint").asText("").trim();
|
||||
if (artifactId.isBlank() || fingerprint.isBlank() || !artifactIds.add(artifactId)) {
|
||||
diagnostics.add(blocking("Schema '" + schemaId + "' must define unique artifact bindings with 'artifact_id' and 'fingerprint'.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
bindings.add(new GlyphMaterialArtifactBinding(artifactId, fingerprint));
|
||||
}
|
||||
return Optional.of(new GlyphMaterialSchema(schemaId, status, version, Map.copyOf(materialIndices), List.copyOf(bindings)));
|
||||
}
|
||||
|
||||
private Optional<GlyphMaterialPalette> parsePalette(
|
||||
JsonNode paletteNode,
|
||||
Set<String> schemaIds,
|
||||
Path supportFile,
|
||||
List<PackerDiagnostic> diagnostics) {
|
||||
if (!paletteNode.isObject()) {
|
||||
diagnostics.add(blocking("Each glyph material palette must be an object.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
final String paletteId = paletteNode.path("palette_id").asText("").trim();
|
||||
final String schemaId = paletteNode.path("schema_id").asText("").trim();
|
||||
final int version = paletteNode.path("version").asInt(0);
|
||||
if (paletteId.isBlank() || schemaId.isBlank() || version <= 0 || !schemaIds.contains(schemaId)) {
|
||||
diagnostics.add(blocking("Each glyph material palette must define non-blank 'palette_id', valid 'schema_id', and positive 'version'.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
if (!(paletteNode.path("colors") instanceof ArrayNode colorsNode)) {
|
||||
diagnostics.add(blocking("Palette '" + paletteId + "' must define array 'colors'.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
final List<String> colors = new ArrayList<>();
|
||||
for (JsonNode colorNode : colorsNode) {
|
||||
final String color = normalizeHtmlColor(colorNode.asText(null));
|
||||
if (color == null) {
|
||||
diagnostics.add(blocking("Palette '" + paletteId + "' must use #RRGGBB colors.", supportFile));
|
||||
return Optional.empty();
|
||||
}
|
||||
colors.add(color);
|
||||
}
|
||||
return Optional.of(new GlyphMaterialPalette(paletteId, schemaId, version, List.copyOf(colors)));
|
||||
}
|
||||
|
||||
private static PackerDiagnostic blocking(String message, Path path) {
|
||||
return new PackerDiagnostic(
|
||||
PackerDiagnosticSeverity.ERROR,
|
||||
PackerDiagnosticCategory.STRUCTURAL,
|
||||
message,
|
||||
path,
|
||||
true);
|
||||
}
|
||||
|
||||
private boolean denseIndices(Set<Integer> indices) {
|
||||
int expected = 1;
|
||||
for (Integer index : new TreeSet<>(indices)) {
|
||||
if (index == null || index != expected) {
|
||||
return false;
|
||||
}
|
||||
expected += 1;
|
||||
}
|
||||
return !indices.isEmpty();
|
||||
}
|
||||
|
||||
private String normalizeHtmlColor(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
final String normalized = value.trim().toUpperCase();
|
||||
return normalized.matches("^#[0-9A-F]{6}$") ? normalized : null;
|
||||
}
|
||||
|
||||
private record GlyphMaterialFile(
|
||||
List<GlyphMaterialSchema> schemas,
|
||||
List<GlyphMaterialPalette> palettes) {
|
||||
}
|
||||
|
||||
private record GlyphMaterialSchema(
|
||||
String schemaId,
|
||||
GlyphMaterialSchemaStatus status,
|
||||
int version,
|
||||
Map<Integer, String> materialIndices,
|
||||
List<GlyphMaterialArtifactBinding> artifactBindings) {
|
||||
}
|
||||
|
||||
private record GlyphMaterialPalette(
|
||||
String paletteId,
|
||||
String schemaId,
|
||||
int version,
|
||||
List<String> colors) {
|
||||
}
|
||||
|
||||
private record GlyphMaterialArtifactBinding(
|
||||
String artifactId,
|
||||
String fingerprint) {
|
||||
}
|
||||
|
||||
private record SchemaEvaluation(
|
||||
GlyphMaterialSchema schema,
|
||||
boolean inconsistent) {
|
||||
}
|
||||
|
||||
private enum GlyphMaterialSchemaStatus {
|
||||
OK("ok"),
|
||||
INCONSISTENT("inconsistent");
|
||||
|
||||
private final String manifestValue;
|
||||
|
||||
GlyphMaterialSchemaStatus(String manifestValue) {
|
||||
this.manifestValue = manifestValue;
|
||||
}
|
||||
|
||||
static GlyphMaterialSchemaStatus fromManifestValue(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
for (GlyphMaterialSchemaStatus candidate : values()) {
|
||||
if (candidate.manifestValue.equalsIgnoreCase(value.trim())) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -76,12 +76,13 @@ final class PackerRuntimeAssetMaterializerTest {
|
||||
final Path assetRoot = Files.createDirectories(tempDir.resolve("runtime-default"));
|
||||
final Path manifestPath = assetRoot.resolve("asset.json");
|
||||
Files.writeString(manifestPath, "{}");
|
||||
Files.writeString(assetRoot.resolve("studio.asset.json"), "{}");
|
||||
writeTile(assetRoot.resolve("selected.png"), 16);
|
||||
writeTile(assetRoot.resolve("extra.png"), 16);
|
||||
|
||||
final PackerAssetDeclaration declaration = declaration(
|
||||
Map.of("tile_size", "16x16"),
|
||||
List.of(new PackerAssetArtifactSelection("selected.png", 0)),
|
||||
List.of(new PackerAssetArtifactSelection(null, "selected.png", 0, null)),
|
||||
palettePipeline());
|
||||
final var materialized = materializer.materialize(
|
||||
assetRoot,
|
||||
@ -91,6 +92,7 @@ final class PackerRuntimeAssetMaterializerTest {
|
||||
Optional.empty());
|
||||
|
||||
assertEquals(2, materialized.runtimeAsset().walkProjection().buildCandidateFiles().size());
|
||||
assertEquals(List.of("extra.png", "selected.png"), materialized.runtimeAsset().walkProjection().availableFiles());
|
||||
assertTrue(materialized.runtimeAsset().walkProjection().buildCandidateFiles().stream()
|
||||
.allMatch(file -> file.contentBytes().isEmpty()));
|
||||
}
|
||||
@ -106,7 +108,7 @@ final class PackerRuntimeAssetMaterializerTest {
|
||||
|
||||
final PackerAssetDeclaration declaration = declaration(
|
||||
Map.of("tile_size", "16x16"),
|
||||
List.of(new PackerAssetArtifactSelection("selected.png", 0)),
|
||||
List.of(new PackerAssetArtifactSelection(null, "selected.png", 0, null)),
|
||||
palettePipeline());
|
||||
final var materialized = materializer.materialize(
|
||||
assetRoot,
|
||||
|
||||
@ -512,6 +512,35 @@ final class FileSystemPackerWorkspaceServiceTest {
|
||||
assertTrue(events.stream().anyMatch(event -> event.kind() == PackerEventKind.ACTION_APPLIED));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createsSceneBankAssetAndWritesStudioSupportFile() throws Exception {
|
||||
final Path projectRoot = tempDir.resolve("created-scene-bank");
|
||||
final FileSystemPackerWorkspaceService service = service();
|
||||
|
||||
final var result = service.createAsset(new CreateAssetRequest(
|
||||
project(projectRoot),
|
||||
"scenes/overworld",
|
||||
"overworld",
|
||||
AssetFamilyCatalog.SCENE_BANK,
|
||||
OutputFormatCatalog.SCENE_TILED_V1,
|
||||
OutputCodecCatalog.NONE,
|
||||
false));
|
||||
|
||||
assertEquals(PackerOperationStatus.SUCCESS, result.status());
|
||||
final Path assetRoot = projectRoot.resolve("assets/scenes/overworld");
|
||||
assertTrue(Files.isRegularFile(assetRoot.resolve("asset.json")));
|
||||
assertTrue(Files.isRegularFile(assetRoot.resolve("scene-bank.studio.json")));
|
||||
final var supportFile = MAPPER.readTree(assetRoot.resolve("scene-bank.studio.json").toFile());
|
||||
assertEquals(16, supportFile.path("map_width").asInt());
|
||||
assertEquals(16, supportFile.path("map_height").asInt());
|
||||
assertEquals(4, supportFile.path("layer_count").asInt());
|
||||
assertEquals(false, supportFile.path("layers").get(0).path("enabled").asBoolean());
|
||||
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());
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsCreatedAssetThroughRuntimeBackedDetailsWithoutRescanMismatch() throws Exception {
|
||||
final Path projectRoot = tempDir.resolve("created-details");
|
||||
@ -696,6 +725,31 @@ final class FileSystemPackerWorkspaceServiceTest {
|
||||
assertFalse(registry.path("assets").get(0).path("included_in_build").asBoolean(true));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateAssetBuildParticipationBlocksSceneBankWithoutEnabledLayers() throws Exception {
|
||||
final Path projectRoot = tempDir.resolve("update-build-participation-scene");
|
||||
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());
|
||||
|
||||
final var result = service.updateAssetBuildParticipation(new UpdateAssetBuildParticipationRequest(
|
||||
project(projectRoot),
|
||||
createResult.assetReference(),
|
||||
true));
|
||||
|
||||
assertEquals(PackerOperationStatus.FAILED, result.status());
|
||||
assertTrue(result.summary().contains("enable at least one layer"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateAssetContractDropsPersistedBankCompositionWhenContractFingerprintChanges() throws Exception {
|
||||
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("update-contract-drops-bank"));
|
||||
@ -734,8 +788,8 @@ final class FileSystemPackerWorkspaceServiceTest {
|
||||
void applyBankCompositionWritesArtifactsAndRefreshesSnapshotWithoutWholeProjectReload() throws Exception {
|
||||
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("apply-bank-composition"));
|
||||
final Path assetRoot = projectRoot.resolve("assets/ui/atlas");
|
||||
Files.writeString(assetRoot.resolve("confirm.png"), "fixture");
|
||||
Files.writeString(assetRoot.resolve("cancel.png"), "fixture");
|
||||
writeTilePng(assetRoot.resolve("confirm.png"), 16);
|
||||
writeTilePng(assetRoot.resolve("cancel.png"), 16);
|
||||
final CountingLoader loader = countingLoader();
|
||||
final FileSystemPackerWorkspaceService service = service(ignored -> { }, loader);
|
||||
|
||||
@ -753,10 +807,14 @@ final class FileSystemPackerWorkspaceServiceTest {
|
||||
|
||||
final var manifest = MAPPER.readTree(assetRoot.resolve("asset.json").toFile());
|
||||
assertTrue(manifest.path("inputs").isMissingNode());
|
||||
assertTrue(manifest.path("artifacts").get(0).hasNonNull("artifact_id"));
|
||||
assertEquals("cancel.png", manifest.path("artifacts").get(0).path("file").asText());
|
||||
assertEquals(0, manifest.path("artifacts").get(0).path("index").asInt());
|
||||
assertTrue(manifest.path("artifacts").get(0).hasNonNull("fingerprint"));
|
||||
assertTrue(manifest.path("artifacts").get(1).hasNonNull("artifact_id"));
|
||||
assertEquals("confirm.png", manifest.path("artifacts").get(1).path("file").asText());
|
||||
assertEquals(1, manifest.path("artifacts").get(1).path("index").asInt());
|
||||
assertTrue(manifest.path("artifacts").get(1).hasNonNull("fingerprint"));
|
||||
|
||||
final var detailsResult = service.getAssetDetails(new GetAssetDetailsRequest(
|
||||
project(projectRoot),
|
||||
@ -767,9 +825,154 @@ final class FileSystemPackerWorkspaceServiceTest {
|
||||
assertEquals(
|
||||
List.of("cancel.png", "confirm.png"),
|
||||
detailsResult.details().bankComposition().selectedFiles().stream().map(file -> file.path()).toList());
|
||||
assertNotNull(detailsResult.details().bankComposition().selectedFiles().getFirst().metadata().get("artifactId"));
|
||||
assertEquals(1, loader.loadCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAssetDetailsBackfillsLegacyArtifactIdsAndFingerprints() throws Exception {
|
||||
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("legacy-artifact-identity"));
|
||||
final Path assetRoot = projectRoot.resolve("assets/ui/atlas");
|
||||
writeTilePng(assetRoot.resolve("confirm.png"), 16);
|
||||
final ObjectNode manifest = (ObjectNode) MAPPER.readTree(assetRoot.resolve("asset.json").toFile());
|
||||
final var artifacts = manifest.putArray("artifacts");
|
||||
artifacts.addObject()
|
||||
.put("file", "confirm.png")
|
||||
.put("index", 0);
|
||||
MAPPER.writerWithDefaultPrettyPrinter().writeValue(assetRoot.resolve("asset.json").toFile(), manifest);
|
||||
final FileSystemPackerWorkspaceService service = service();
|
||||
|
||||
final var detailsResult = service.getAssetDetails(new GetAssetDetailsRequest(
|
||||
project(projectRoot),
|
||||
AssetReference.forAssetId(1)));
|
||||
|
||||
final var migrated = MAPPER.readTree(assetRoot.resolve("asset.json").toFile());
|
||||
final var migratedArtifact = migrated.path("artifacts").get(0);
|
||||
assertTrue(migratedArtifact.hasNonNull("artifact_id"));
|
||||
assertTrue(migratedArtifact.hasNonNull("fingerprint"));
|
||||
final var selectedFile = detailsResult.details().bankComposition().selectedFiles().getFirst();
|
||||
assertEquals(migratedArtifact.path("artifact_id").asText(), selectedFile.metadata().get("artifactId"));
|
||||
assertEquals(migratedArtifact.path("fingerprint").asText(), selectedFile.metadata().get("selectedFingerprint"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void packValidationBlocksGlyphMaterialPaletteWhenSchemaVersionIsNewer() throws Exception {
|
||||
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("glyph-material-stale-palette"));
|
||||
final Path assetRoot = projectRoot.resolve("assets/ui/atlas");
|
||||
writeTilePng(assetRoot.resolve("confirm.png"), 16);
|
||||
final FileSystemPackerWorkspaceService service = service();
|
||||
|
||||
assertTrue(service.applyBankComposition(new ApplyBankCompositionRequest(
|
||||
project(projectRoot),
|
||||
AssetReference.forAssetId(1),
|
||||
List.of("confirm.png"))).success());
|
||||
assertTrue(service.applyPaletteOverhauling(new ApplyPaletteOverhaulingRequest(
|
||||
project(projectRoot),
|
||||
AssetReference.forAssetId(1),
|
||||
List.of(Map.of(
|
||||
"originalArgb8888", List.of(0xFFFF0000),
|
||||
"convertedRgb565", List.of(0xF800))))).success());
|
||||
|
||||
final var manifest = MAPPER.readTree(assetRoot.resolve("asset.json").toFile());
|
||||
final var selectedArtifact = manifest.path("artifacts").get(0);
|
||||
writeGlyphMaterialJson(
|
||||
assetRoot,
|
||||
"""
|
||||
{
|
||||
"schema_version": 1,
|
||||
"schemas": [
|
||||
{
|
||||
"schema_id": "schema-1",
|
||||
"status": "ok",
|
||||
"version": 2,
|
||||
"material_indices": { "1": "#FF0000" },
|
||||
"artifact_bindings": [
|
||||
{ "artifact_id": "%s", "fingerprint": "%s" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"palettes": [
|
||||
{
|
||||
"palette_id": "palette-1",
|
||||
"schema_id": "schema-1",
|
||||
"version": 1,
|
||||
"colors": ["#FF0000"]
|
||||
}
|
||||
]
|
||||
}
|
||||
""".formatted(
|
||||
selectedArtifact.path("artifact_id").asText(),
|
||||
selectedArtifact.path("fingerprint").asText()));
|
||||
|
||||
final var result = service.validatePackWorkspace(new ValidatePackWorkspaceRequest(project(projectRoot)));
|
||||
|
||||
assertEquals(PackerOperationStatus.PARTIAL, result.status());
|
||||
assertFalse(result.canPack());
|
||||
assertTrue(result.assets().getFirst().diagnostics().stream()
|
||||
.anyMatch(diagnostic -> diagnostic.message().contains("is stale because version 1 is older than schema 'schema-1' version 2")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void packValidationBlocksGlyphMaterialSchemaWhenArtifactFingerprintDrifts() throws Exception {
|
||||
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("glyph-material-drift"));
|
||||
final Path assetRoot = projectRoot.resolve("assets/ui/atlas");
|
||||
final Path tilePath = assetRoot.resolve("confirm.png");
|
||||
writeTilePng(tilePath, 16);
|
||||
final FileSystemPackerWorkspaceService service = service();
|
||||
|
||||
assertTrue(service.applyBankComposition(new ApplyBankCompositionRequest(
|
||||
project(projectRoot),
|
||||
AssetReference.forAssetId(1),
|
||||
List.of("confirm.png"))).success());
|
||||
assertTrue(service.applyPaletteOverhauling(new ApplyPaletteOverhaulingRequest(
|
||||
project(projectRoot),
|
||||
AssetReference.forAssetId(1),
|
||||
List.of(Map.of(
|
||||
"originalArgb8888", List.of(0xFFFF0000),
|
||||
"convertedRgb565", List.of(0xF800))))).success());
|
||||
|
||||
final var manifest = MAPPER.readTree(assetRoot.resolve("asset.json").toFile());
|
||||
final var selectedArtifact = manifest.path("artifacts").get(0);
|
||||
writeGlyphMaterialJson(
|
||||
assetRoot,
|
||||
"""
|
||||
{
|
||||
"schema_version": 1,
|
||||
"schemas": [
|
||||
{
|
||||
"schema_id": "schema-1",
|
||||
"status": "ok",
|
||||
"version": 1,
|
||||
"material_indices": { "1": "#FF0000" },
|
||||
"artifact_bindings": [
|
||||
{ "artifact_id": "%s", "fingerprint": "%s" }
|
||||
]
|
||||
}
|
||||
],
|
||||
"palettes": [
|
||||
{
|
||||
"palette_id": "palette-1",
|
||||
"schema_id": "schema-1",
|
||||
"version": 1,
|
||||
"colors": ["#FF0000"]
|
||||
}
|
||||
]
|
||||
}
|
||||
""".formatted(
|
||||
selectedArtifact.path("artifact_id").asText(),
|
||||
selectedArtifact.path("fingerprint").asText()));
|
||||
writeSolidTilePng(tilePath, 16, 0xFF00FF00);
|
||||
|
||||
final var result = service.validatePackWorkspace(new ValidatePackWorkspaceRequest(project(projectRoot)));
|
||||
|
||||
assertEquals(PackerOperationStatus.PARTIAL, result.status());
|
||||
assertFalse(result.canPack());
|
||||
assertTrue(result.assets().getFirst().diagnostics().stream()
|
||||
.anyMatch(diagnostic -> diagnostic.message().contains("artifact '" + selectedArtifact.path("artifact_id").asText() + "' drifted")));
|
||||
assertTrue(result.assets().getFirst().diagnostics().stream()
|
||||
.anyMatch(diagnostic -> diagnostic.message().contains("palette 'palette-1' is invalid because schema 'schema-1' is inconsistent")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyPaletteOverhaulingWritesPipelinePalettesInOrder() throws Exception {
|
||||
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("apply-palette-overhauling"));
|
||||
@ -1350,11 +1553,15 @@ final class FileSystemPackerWorkspaceServiceTest {
|
||||
}
|
||||
|
||||
private void writeTilePng(Path path, int tileSize) throws Exception {
|
||||
writeSolidTilePng(path, tileSize, 0xFFFF0000);
|
||||
}
|
||||
|
||||
private void writeSolidTilePng(Path path, int tileSize, int argb) throws Exception {
|
||||
Files.createDirectories(path.getParent());
|
||||
final BufferedImage image = new BufferedImage(tileSize, tileSize, BufferedImage.TYPE_INT_ARGB);
|
||||
for (int y = 0; y < tileSize; y += 1) {
|
||||
for (int x = 0; x < tileSize; x += 1) {
|
||||
image.setRGB(x, y, 0xFFFF0000);
|
||||
image.setRGB(x, y, argb);
|
||||
}
|
||||
}
|
||||
ImageIO.write(image, "png", path.toFile());
|
||||
@ -1371,6 +1578,10 @@ final class FileSystemPackerWorkspaceServiceTest {
|
||||
ImageIO.write(image, "png", path.toFile());
|
||||
}
|
||||
|
||||
private void writeGlyphMaterialJson(Path assetRoot, String content) throws Exception {
|
||||
Files.writeString(assetRoot.resolve("glyph-material.json"), content);
|
||||
}
|
||||
|
||||
private static int readLeInt(byte[] bytes, int offset) {
|
||||
return (bytes[offset] & 0xFF)
|
||||
| ((bytes[offset + 1] & 0xFF) << 8)
|
||||
|
||||
@ -67,6 +67,56 @@ final class PackerAssetDeclarationParserTest {
|
||||
assertEquals(128, result.declaration().outputPipelineMetadata().get("samples").path("1").path("length").asInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsesSceneBankDeclaration() throws Exception {
|
||||
final Path manifest = tempDir.resolve("scene-asset.json");
|
||||
Files.writeString(manifest, """
|
||||
{
|
||||
"schema_version": 1,
|
||||
"asset_uuid": "uuid-scene",
|
||||
"name": "overworld_scene",
|
||||
"type": "scene_bank",
|
||||
"output": {
|
||||
"format": "SCENE/tilemap_v1",
|
||||
"codec": "NONE"
|
||||
},
|
||||
"preload": { "enabled": false }
|
||||
}
|
||||
""");
|
||||
|
||||
final var result = parser.parse(manifest);
|
||||
|
||||
assertTrue(result.valid());
|
||||
assertEquals(AssetFamilyCatalog.SCENE_BANK, result.declaration().assetFamily());
|
||||
assertEquals("SCENE/tilemap_v1", result.declaration().outputFormat().displayName());
|
||||
assertEquals(OutputCodecCatalog.NONE, result.declaration().outputCodec());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parsesLegacySceneBankDeclarationFormatAsTilemapContract() throws Exception {
|
||||
final Path manifest = tempDir.resolve("legacy-scene-asset.json");
|
||||
Files.writeString(manifest, """
|
||||
{
|
||||
"schema_version": 1,
|
||||
"asset_uuid": "uuid-scene-legacy",
|
||||
"name": "overworld_scene",
|
||||
"type": "scene_bank",
|
||||
"output": {
|
||||
"format": "SCENE/tiled_v1",
|
||||
"codec": "NONE"
|
||||
},
|
||||
"preload": { "enabled": false }
|
||||
}
|
||||
""");
|
||||
|
||||
final var result = parser.parse(manifest);
|
||||
|
||||
assertTrue(result.valid());
|
||||
assertEquals(AssetFamilyCatalog.SCENE_BANK, result.declaration().assetFamily());
|
||||
assertEquals("SCENE/tilemap_v1", result.declaration().outputFormat().displayName());
|
||||
assertEquals(OutputCodecCatalog.NONE, result.declaration().outputCodec());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsNonObjectPipelineMetadata() throws Exception {
|
||||
final Path manifest = tempDir.resolve("asset.json");
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"glyph_bank_specialization" : "sprites"
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"glyph_bank_specialization" : "sprites"
|
||||
}
|
||||
@ -4,6 +4,7 @@ import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
@ -35,6 +36,8 @@ public abstract class StudioDualListView<T> extends HBox {
|
||||
private Consumer<List<T>> onMoveToLeft = ignored -> { };
|
||||
private Consumer<T> onLeftSelectionChanged = ignored -> { };
|
||||
private Consumer<T> onRightSelectionChanged = ignored -> { };
|
||||
private Consumer<T> onLeftItemActivated = ignored -> { };
|
||||
private Consumer<T> onRightItemActivated = ignored -> { };
|
||||
private IntConsumer onMoveUp = ignored -> { };
|
||||
private IntConsumer onMoveDown = ignored -> { };
|
||||
private boolean interactionEnabled = true;
|
||||
@ -145,6 +148,14 @@ public abstract class StudioDualListView<T> extends HBox {
|
||||
this.onRightSelectionChanged = Objects.requireNonNull(onRightSelectionChanged, "onRightSelectionChanged");
|
||||
}
|
||||
|
||||
public void setOnLeftItemActivated(Consumer<T> onLeftItemActivated) {
|
||||
this.onLeftItemActivated = Objects.requireNonNull(onLeftItemActivated, "onLeftItemActivated");
|
||||
}
|
||||
|
||||
public void setOnRightItemActivated(Consumer<T> onRightItemActivated) {
|
||||
this.onRightItemActivated = Objects.requireNonNull(onRightItemActivated, "onRightItemActivated");
|
||||
}
|
||||
|
||||
public void setOnMoveUp(IntConsumer onMoveUp) {
|
||||
this.onMoveUp = Objects.requireNonNull(onMoveUp, "onMoveUp");
|
||||
}
|
||||
@ -188,6 +199,19 @@ public abstract class StudioDualListView<T> extends HBox {
|
||||
|
||||
private ListCell<T> createCell(boolean indexed) {
|
||||
return new ListCell<>() {
|
||||
{
|
||||
setOnMouseClicked(event -> {
|
||||
if (event.getButton() != MouseButton.PRIMARY || event.getClickCount() != 2 || getItem() == null) {
|
||||
return;
|
||||
}
|
||||
if (indexed) {
|
||||
onRightItemActivated.accept(getItem());
|
||||
} else {
|
||||
onLeftItemActivated.accept(getItem());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(T item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
@ -145,6 +145,9 @@ public enum I18n {
|
||||
ASSETS_SECTION_SUMMARY("assets.section.summary"),
|
||||
ASSETS_SECTION_RUNTIME_CONTRACT("assets.section.runtimeContract"),
|
||||
ASSETS_SECTION_BANK_COMPOSITION("assets.section.bankComposition"),
|
||||
ASSETS_SECTION_SCENE_LAYERS("assets.section.sceneLayers"),
|
||||
ASSETS_SECTION_COLOR_SCHEMA_GROUPS("assets.section.colorSchemaGroups"),
|
||||
ASSETS_SECTION_LAYER_CONFIGURATION("assets.section.layerConfiguration"),
|
||||
ASSETS_SECTION_PALETTE_OVERHAULING("assets.section.paletteOverhauling"),
|
||||
ASSETS_SUBSECTION_CODEC_CONFIGURATION("assets.subsection.codecConfiguration"),
|
||||
ASSETS_SUBSECTION_METADATA("assets.subsection.metadata"),
|
||||
@ -154,10 +157,17 @@ 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_VALIDATE_SCENE_BANK("assets.action.validateSceneBank"),
|
||||
ASSETS_ACTION_ACCEPT_SCENE_BANK("assets.action.acceptSceneBank"),
|
||||
ASSETS_ACTION_VALIDATE_LAYER("assets.action.validateLayer"),
|
||||
ASSETS_ACTION_ACCEPT_LAYER("assets.action.acceptLayer"),
|
||||
ASSETS_ACTION_DELETE("assets.action.delete"),
|
||||
ASSETS_ACTION_INCLUDE_IN_BUILD("assets.action.includeInBuild"),
|
||||
ASSETS_ACTION_EXCLUDE_FROM_BUILD("assets.action.excludeFromBuild"),
|
||||
ASSETS_ACTION_RELOCATE("assets.action.relocate"),
|
||||
ASSETS_ACTION_REMOVE("assets.action.remove"),
|
||||
ASSETS_DELETE_DIALOG_TITLE("assets.deleteDialog.title"),
|
||||
ASSETS_DELETE_DIALOG_DESCRIPTION("assets.deleteDialog.description"),
|
||||
ASSETS_DELETE_DIALOG_PROMPT("assets.deleteDialog.prompt"),
|
||||
@ -190,10 +200,32 @@ public enum I18n {
|
||||
ASSETS_LABEL_BUILD_PARTICIPATION("assets.label.buildParticipation"),
|
||||
ASSETS_LABEL_ASSET_ID("assets.label.assetId"),
|
||||
ASSETS_LABEL_TYPE("assets.label.type"),
|
||||
ASSETS_LABEL_STUDIO_ROLE("assets.label.studioRole"),
|
||||
ASSETS_LABEL_SCENE_STATUS("assets.label.sceneStatus"),
|
||||
ASSETS_LABEL_SCENE_LAYERS("assets.label.sceneLayers"),
|
||||
ASSETS_LABEL_SCENE_DIMENSIONS("assets.label.sceneDimensions"),
|
||||
ASSETS_LABEL_LAYER("assets.label.layer"),
|
||||
ASSETS_LABEL_LAYER_OVERVIEW("assets.label.layerOverview"),
|
||||
ASSETS_LABEL_TILEMAPS("assets.label.tilemaps"),
|
||||
ASSETS_LABEL_TILEMAP("assets.label.tilemap"),
|
||||
ASSETS_LABEL_TILESET("assets.label.tileset"),
|
||||
ASSETS_LABEL_SUPPORT_FILE("assets.label.supportFile"),
|
||||
ASSETS_LABEL_USE_LAYER("assets.label.useLayer"),
|
||||
ASSETS_VALUE_NOT_CONFIGURED("assets.value.notConfigured"),
|
||||
ASSETS_VALUE_LAYER_OVERVIEW_PENDING("assets.value.layerOverviewPending"),
|
||||
ASSETS_TYPE_GLYPH_BANK("assets.type.glyphBank"),
|
||||
ASSETS_TYPE_SCENE_BANK("assets.type.sceneBank"),
|
||||
ASSETS_TYPE_PALETTE_BANK("assets.type.paletteBank"),
|
||||
ASSETS_TYPE_SOUND_BANK("assets.type.soundBank"),
|
||||
ASSETS_TYPE_UNKNOWN("assets.type.unknown"),
|
||||
ASSETS_SPECIALIZATION_NONE("assets.specialization.none"),
|
||||
ASSETS_SPECIALIZATION_TILESET("assets.specialization.tileset"),
|
||||
ASSETS_SPECIALIZATION_SPRITES("assets.specialization.sprites"),
|
||||
ASSETS_SPECIALIZATION_UI("assets.specialization.ui"),
|
||||
ASSETS_SCENE_STATUS_PENDING_VALIDATION("assets.sceneStatus.pendingValidation"),
|
||||
ASSETS_SCENE_STATUS_VALIDATED_PENDING_ACCEPTANCE("assets.sceneStatus.validatedPendingAcceptance"),
|
||||
ASSETS_SCENE_STATUS_READY("assets.sceneStatus.ready"),
|
||||
ASSETS_SCENE_STATUS_VALIDATION_FAILED("assets.sceneStatus.validationFailed"),
|
||||
ASSETS_LABEL_LOCATION("assets.label.location"),
|
||||
ASSETS_LABEL_BANK("assets.label.bank"),
|
||||
ASSETS_LABEL_TARGET_LOCATION("assets.label.targetLocation"),
|
||||
@ -241,6 +273,22 @@ public enum I18n {
|
||||
ASSETS_PALETTE_OVERHAULING_TILE_SELECTOR("assets.paletteOverhauling.tileSelector"),
|
||||
ASSETS_PALETTE_OVERHAULING_APPLIED_PALETTE("assets.paletteOverhauling.appliedPalette"),
|
||||
ASSETS_PALETTE_OVERHAULING_PREVIEW_EMPTY("assets.paletteOverhauling.previewEmpty"),
|
||||
ASSETS_PALETTE_WIZARD_CREATE_TITLE("assets.paletteWizard.createTitle"),
|
||||
ASSETS_PALETTE_WIZARD_UPDATE_TITLE("assets.paletteWizard.updateTitle"),
|
||||
ASSETS_PALETTE_WIZARD_NAME("assets.paletteWizard.name"),
|
||||
ASSETS_PALETTE_WIZARD_SCHEMA("assets.paletteWizard.schema"),
|
||||
ASSETS_PALETTE_WIZARD_ORIGINAL("assets.paletteWizard.original"),
|
||||
ASSETS_PALETTE_WIZARD_EDITABLE("assets.paletteWizard.editable"),
|
||||
ASSETS_PALETTE_WIZARD_APPLY_COLOR("assets.paletteWizard.applyColor"),
|
||||
ASSETS_PALETTE_WIZARD_DUPLICATE("assets.paletteWizard.duplicate"),
|
||||
ASSETS_PALETTE_WIZARD_READY("assets.paletteWizard.ready"),
|
||||
ASSETS_COLOR_SCHEMA_EMPTY("assets.colorSchema.empty"),
|
||||
ASSETS_COLOR_SCHEMA_SUMMARY("assets.colorSchema.summary"),
|
||||
ASSETS_COLOR_SCHEMA_REMOVE_BLOCKED("assets.colorSchema.removeBlocked"),
|
||||
ASSETS_COLOR_SCHEMA_DIALOG_AVAILABLE_TILES("assets.colorSchema.dialog.availableTiles"),
|
||||
ASSETS_COLOR_SCHEMA_DIALOG_ASSIGNED_TILES("assets.colorSchema.dialog.assignedTiles"),
|
||||
ASSETS_COLOR_SCHEMA_DIALOG_COLOR_COUNT("assets.colorSchema.dialog.colorCount"),
|
||||
ASSETS_COLOR_SCHEMA_DIALOG_TOO_MANY_COLORS("assets.colorSchema.dialog.tooManyColors"),
|
||||
ASSETS_DETAILS_CODEC_CONFIGURATION_EMPTY("assets.details.codecConfiguration.empty"),
|
||||
ASSETS_DETAILS_METADATA_EMPTY("assets.details.metadata.empty"),
|
||||
ASSETS_ADD_WIZARD_TITLE("assets.addWizard.title"),
|
||||
@ -254,10 +302,12 @@ public enum I18n {
|
||||
ASSETS_ADD_WIZARD_LABEL_NAME("assets.addWizard.label.name"),
|
||||
ASSETS_ADD_WIZARD_LABEL_ROOT("assets.addWizard.label.root"),
|
||||
ASSETS_ADD_WIZARD_LABEL_TYPE("assets.addWizard.label.type"),
|
||||
ASSETS_ADD_WIZARD_LABEL_SPECIALIZATION("assets.addWizard.label.specialization"),
|
||||
ASSETS_ADD_WIZARD_LABEL_FORMAT("assets.addWizard.label.format"),
|
||||
ASSETS_ADD_WIZARD_LABEL_CODEC("assets.addWizard.label.codec"),
|
||||
ASSETS_ADD_WIZARD_LABEL_PRELOAD("assets.addWizard.label.preload"),
|
||||
ASSETS_ADD_WIZARD_PROMPT_TYPE("assets.addWizard.prompt.type"),
|
||||
ASSETS_ADD_WIZARD_PROMPT_SPECIALIZATION("assets.addWizard.prompt.specialization"),
|
||||
ASSETS_ADD_WIZARD_PROMPT_FORMAT("assets.addWizard.prompt.format"),
|
||||
ASSETS_ADD_WIZARD_PROMPT_CODEC("assets.addWizard.prompt.codec"),
|
||||
ASSETS_ADD_WIZARD_ASSETS_ROOT_HINT("assets.addWizard.assetsRootHint"),
|
||||
@ -270,6 +320,7 @@ public enum I18n {
|
||||
ASSETS_ADD_WIZARD_ERROR_ROOT_OUTSIDE_ASSETS("assets.addWizard.error.rootOutsideAssets"),
|
||||
ASSETS_ADD_WIZARD_ERROR_ROOT_ALREADY_ASSET("assets.addWizard.error.rootAlreadyAsset"),
|
||||
ASSETS_ADD_WIZARD_ERROR_TYPE("assets.addWizard.error.type"),
|
||||
ASSETS_ADD_WIZARD_ERROR_SPECIALIZATION("assets.addWizard.error.specialization"),
|
||||
ASSETS_ADD_WIZARD_ERROR_FORMAT("assets.addWizard.error.format"),
|
||||
ASSETS_ADD_WIZARD_ERROR_CODEC("assets.addWizard.error.codec"),
|
||||
ASSETS_ADD_WIZARD_ERROR_UNSUPPORTED_COMBINATION("assets.addWizard.error.unsupportedCombination"),
|
||||
|
||||
@ -12,6 +12,7 @@ import p.packer.dtos.PackerAssetActionAvailabilityDTO;
|
||||
import p.packer.dtos.PackerAssetDetailsDTO;
|
||||
import p.packer.dtos.PackerDiagnosticDTO;
|
||||
import p.packer.messages.*;
|
||||
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||
import p.studio.Container;
|
||||
import p.studio.controls.forms.StudioFormEditScopeChangedEvent;
|
||||
import p.studio.controls.forms.StudioSection;
|
||||
@ -22,22 +23,34 @@ import p.studio.workspaces.assets.dialogs.AssetDiagnosticsDialog;
|
||||
import p.studio.workspaces.assets.details.bank.AssetDetailsBankCompositionControl;
|
||||
import p.studio.workspaces.assets.details.contract.AssetDetailsContractControl;
|
||||
import p.studio.workspaces.assets.details.palette.AssetDetailsPaletteOverhaulingControl;
|
||||
import p.studio.workspaces.assets.details.schema.AssetDetailsColorSchemaGroupsControl;
|
||||
import p.studio.workspaces.assets.details.scene.AssetDetailsSceneLayersControl;
|
||||
import p.studio.workspaces.assets.details.summary.AssetDetailsSummaryControl;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioMetadataService;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioMetadataSnapshot;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetAction;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionDetails;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceSceneBankValidation;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsStatus;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBuildParticipation;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetLogEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetsRefreshRequestedEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetsWorkspaceSelectionRequestedEvent;
|
||||
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.wizards.DeleteAssetDialog;
|
||||
import p.studio.workspaces.assets.wizards.MoveAssetWizard;
|
||||
import p.studio.workspaces.framework.StudioEventAware;
|
||||
import p.studio.workspaces.framework.StudioEventBindings;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
@ -52,7 +65,12 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
private final AssetDetailsSummaryControl summaryControl;
|
||||
private final AssetDetailsContractControl contractControl;
|
||||
private final AssetDetailsBankCompositionControl bankCompositionControl;
|
||||
private final AssetDetailsColorSchemaGroupsControl colorSchemaGroupsControl;
|
||||
private final AssetDetailsPaletteOverhaulingControl paletteOverhaulingControl;
|
||||
private final AssetDetailsSceneLayersControl sceneLayersControl;
|
||||
private final AssetStudioMetadataService studioMetadataService = new AssetStudioMetadataService();
|
||||
private final TiledAssetGenerationService tiledGenerationService = new TiledAssetGenerationService();
|
||||
private final SceneBankWorkflowService sceneBankWorkflowService = new SceneBankWorkflowService();
|
||||
private final VBox actionsContent = new VBox(10);
|
||||
private final ScrollPane actionsScroll = new ScrollPane();
|
||||
private final VBox actionsSection;
|
||||
@ -72,7 +90,12 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
this.summaryControl = new AssetDetailsSummaryControl(projectReference, workspaceBus);
|
||||
this.contractControl = new AssetDetailsContractControl(projectReference, workspaceBus);
|
||||
this.bankCompositionControl = new AssetDetailsBankCompositionControl(projectReference, workspaceBus);
|
||||
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");
|
||||
@ -256,13 +279,43 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
renderActions();
|
||||
if (!readyMounted) {
|
||||
readyMounted = true;
|
||||
detailsContent.getChildren().setAll(primarySectionsRow, contractControl, bankCompositionControl, paletteOverhaulingControl);
|
||||
detailsContent.getChildren().setAll(buildDetailsSections());
|
||||
}
|
||||
syncActionsSectionHeight(summaryControl.getHeight());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Node[] buildDetailsSections() {
|
||||
final java.util.List<Node> sections = new java.util.ArrayList<>();
|
||||
sections.add(primarySectionsRow);
|
||||
sections.add(contractControl);
|
||||
if (showsSceneLayers(viewState.selectedAssetDetails())) {
|
||||
sections.add(sceneLayersControl);
|
||||
} else if (!hidesBankComposition(viewState.selectedAssetDetails())) {
|
||||
sections.add(bankCompositionControl);
|
||||
}
|
||||
if (showsColorSchemaGroups(viewState.selectedAssetDetails())) {
|
||||
sections.add(colorSchemaGroupsControl);
|
||||
}
|
||||
sections.add(paletteOverhaulingControl);
|
||||
return sections.toArray(Node[]::new);
|
||||
}
|
||||
|
||||
private boolean hidesBankComposition(AssetWorkspaceAssetDetails details) {
|
||||
return details != null && details.summary().assetFamily() == AssetFamilyCatalog.SCENE_BANK;
|
||||
}
|
||||
|
||||
private boolean showsSceneLayers(AssetWorkspaceAssetDetails details) {
|
||||
return details != null
|
||||
&& details.summary().assetFamily() == AssetFamilyCatalog.SCENE_BANK
|
||||
&& details.sceneBankMetadata() != null;
|
||||
}
|
||||
|
||||
private boolean showsColorSchemaGroups(AssetWorkspaceAssetDetails details) {
|
||||
return details != null && details.summary().assetFamily() == AssetFamilyCatalog.GLYPH_BANK;
|
||||
}
|
||||
|
||||
private VBox createActionsSection() {
|
||||
actionsContent.getStyleClass().add("assets-details-actions-content");
|
||||
actionsScroll.setContent(actionsContent);
|
||||
@ -303,9 +356,6 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
|
||||
private java.util.List<Node> buildActionNodes() {
|
||||
final java.util.List<Node> nodes = new java.util.ArrayList<>();
|
||||
if (actionFeedbackMessage != null && !actionFeedbackMessage.isBlank()) {
|
||||
nodes.add(AssetDetailsUiSupport.createSectionMessage(actionFeedbackMessage));
|
||||
}
|
||||
if (viewState.selectedAssetDetails() == null) {
|
||||
nodes.add(AssetDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_ACTIONS_EMPTY)));
|
||||
return nodes;
|
||||
@ -324,7 +374,12 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
analyseButton.setDisable(actionRunning || viewState.selectedAssetDetails().diagnostics().isEmpty());
|
||||
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 buildParticipationButton = AssetDetailsUiSupport.createActionButton(buildParticipationActionLabel());
|
||||
AssetDetailsUiSupport.applyActionTone(
|
||||
buildParticipationButton,
|
||||
@ -413,7 +468,16 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
|
||||
private boolean canToggleBuildParticipation() {
|
||||
return viewState.selectedAssetDetails() != null
|
||||
&& viewState.selectedAssetDetails().summary().state() == p.studio.workspaces.assets.messages.AssetWorkspaceAssetState.REGISTERED;
|
||||
&& viewState.selectedAssetDetails().summary().state() == p.studio.workspaces.assets.messages.AssetWorkspaceAssetState.REGISTERED
|
||||
&& (!isSceneBankAwaitingActiveLayerForBuild());
|
||||
}
|
||||
|
||||
private boolean isSceneBankAwaitingActiveLayerForBuild() {
|
||||
return viewState.selectedAssetDetails() != null
|
||||
&& viewState.selectedAssetDetails().summary().assetFamily() == AssetFamilyCatalog.SCENE_BANK
|
||||
&& currentBuildParticipation() != AssetWorkspaceBuildParticipation.INCLUDED
|
||||
&& (viewState.selectedAssetDetails().sceneBankMetadata() == null
|
||||
|| viewState.selectedAssetDetails().sceneBankMetadata().enabledLayerCount() == 0);
|
||||
}
|
||||
|
||||
private AssetWorkspaceBuildParticipation currentBuildParticipation() {
|
||||
@ -449,6 +513,98 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
});
|
||||
}
|
||||
|
||||
private boolean canGenerateTsx() {
|
||||
return viewState.selectedAssetDetails() != null
|
||||
&& viewState.selectedAssetDetails().summary().assetFamily() == p.packer.messages.assets.AssetFamilyCatalog.GLYPH_BANK
|
||||
&& viewState.selectedAssetDetails().summary().glyphSpecialization() == AssetStudioGlyphSpecialization.TILESET;
|
||||
}
|
||||
|
||||
private boolean canGenerateTmx() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean canValidateSceneBank() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private void generateTsx() {
|
||||
if (actionRunning || viewState.selectedAssetDetails() == null) {
|
||||
return;
|
||||
}
|
||||
actionRunning = true;
|
||||
actionFeedbackMessage = null;
|
||||
renderActions();
|
||||
final AssetWorkspaceAssetDetails details = viewState.selectedAssetDetails();
|
||||
Container.backgroundTasks().submit(() -> {
|
||||
final TiledAssetGenerationResult result = tiledGenerationService.generateTilesetTsx(details);
|
||||
Platform.runLater(() -> applyTiledGenerationResult("glyph-bank-tileset", result));
|
||||
});
|
||||
}
|
||||
|
||||
private void generateTmx(int layerIndex) {
|
||||
if (actionRunning || viewState.selectedAssetDetails() == null) {
|
||||
return;
|
||||
}
|
||||
actionRunning = true;
|
||||
actionFeedbackMessage = null;
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
||||
private void applyTiledGenerationResult(String source, TiledAssetGenerationResult result) {
|
||||
actionRunning = false;
|
||||
actionFeedbackMessage = result.message();
|
||||
workspaceBus.publish(new StudioAssetLogEvent(source, result.message()));
|
||||
renderActions();
|
||||
if (result.success() && viewState.selectedAssetReference() != null) {
|
||||
workspaceBus.publish(new StudioAssetsRefreshRequestedEvent(viewState.selectedAssetReference()));
|
||||
workspaceBus.publish(new StudioAssetsWorkspaceSelectionRequestedEvent(viewState.selectedAssetReference(), true));
|
||||
}
|
||||
}
|
||||
|
||||
private void validateSceneBank(int layerIndex) {
|
||||
if (actionRunning || viewState.selectedAssetDetails() == null) {
|
||||
return;
|
||||
}
|
||||
actionRunning = true;
|
||||
actionFeedbackMessage = null;
|
||||
renderActions();
|
||||
final AssetWorkspaceAssetDetails details = viewState.selectedAssetDetails();
|
||||
Container.backgroundTasks().submit(() -> {
|
||||
final SceneBankWorkflowResult result = sceneBankWorkflowService.validateLayer(projectReference, details, layerIndex);
|
||||
Platform.runLater(() -> applySceneBankWorkflowResult("scene-bank-validate-layer-" + layerIndex, result));
|
||||
});
|
||||
}
|
||||
|
||||
private void acceptSceneBank(int layerIndex) {
|
||||
if (actionRunning || viewState.selectedAssetDetails() == null) {
|
||||
return;
|
||||
}
|
||||
actionRunning = true;
|
||||
actionFeedbackMessage = null;
|
||||
renderActions();
|
||||
final AssetWorkspaceAssetDetails details = viewState.selectedAssetDetails();
|
||||
Container.backgroundTasks().submit(() -> {
|
||||
final SceneBankWorkflowResult result = sceneBankWorkflowService.acceptLayer(projectReference, details, layerIndex);
|
||||
Platform.runLater(() -> applySceneBankWorkflowResult("scene-bank-accept-layer-" + layerIndex, result));
|
||||
});
|
||||
}
|
||||
|
||||
private void applySceneBankWorkflowResult(String source, SceneBankWorkflowResult result) {
|
||||
actionRunning = false;
|
||||
actionFeedbackMessage = result.message();
|
||||
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 void applyBuildParticipationResult(UpdateAssetBuildParticipationResult result) {
|
||||
actionRunning = false;
|
||||
if (result.status() == p.packer.messages.PackerOperationStatus.SUCCESS && result.assetReference() != null) {
|
||||
@ -534,29 +690,71 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
|
||||
PackerAssetDetailsDTO details,
|
||||
java.util.List<PackerDiagnosticDTO> diagnostics,
|
||||
java.util.List<PackerAssetActionAvailabilityDTO> actions) {
|
||||
final java.util.List<PackerDiagnosticDTO> mergedDiagnostics = new java.util.ArrayList<>(details.diagnostics());
|
||||
for (PackerDiagnosticDTO diagnostic : diagnostics) {
|
||||
if (!mergedDiagnostics.contains(diagnostic)) {
|
||||
mergedDiagnostics.add(diagnostic);
|
||||
}
|
||||
}
|
||||
return new AssetWorkspaceAssetDetails(
|
||||
AssetListPackerMappings.mapSummary(details.summary()),
|
||||
actions.stream()
|
||||
.map(action -> new AssetWorkspaceAssetAction(
|
||||
action.action(),
|
||||
action.enabled(),
|
||||
action.visible(),
|
||||
action.reason()))
|
||||
.toList(),
|
||||
final var baseSummary = AssetListPackerMappings.mapSummary(details.summary());
|
||||
final AssetStudioMetadataSnapshot studioMetadata = studioMetadataService.read(
|
||||
baseSummary.assetRoot(),
|
||||
baseSummary.assetFamily());
|
||||
final java.util.List<AssetWorkspaceAssetAction> mappedActions = actions.stream()
|
||||
.map(action -> new AssetWorkspaceAssetAction(
|
||||
action.action(),
|
||||
action.enabled(),
|
||||
action.visible(),
|
||||
action.reason()))
|
||||
.toList();
|
||||
final p.studio.workspaces.assets.messages.AssetWorkspaceAssetSummary mappedSummary =
|
||||
new p.studio.workspaces.assets.messages.AssetWorkspaceAssetSummary(
|
||||
baseSummary.assetReference(),
|
||||
baseSummary.assetName(),
|
||||
baseSummary.state(),
|
||||
baseSummary.buildParticipation(),
|
||||
baseSummary.assetId(),
|
||||
baseSummary.assetFamily(),
|
||||
studioMetadata.glyphSpecialization(),
|
||||
baseSummary.assetRoot(),
|
||||
baseSummary.preload(),
|
||||
baseSummary.hasDiagnostics());
|
||||
final AssetWorkspaceBankCompositionDetails bankComposition = mapBankComposition(details.bankComposition());
|
||||
final AssetWorkspaceAssetDetails draftDetails = new AssetWorkspaceAssetDetails(
|
||||
mappedSummary,
|
||||
mappedActions,
|
||||
details.outputFormat(),
|
||||
details.outputCodec(),
|
||||
details.availableOutputCodecs(),
|
||||
details.codecConfigurationFieldsByCodec(),
|
||||
details.metadataFields(),
|
||||
details.outputPipeline(),
|
||||
mapBankComposition(details.bankComposition()),
|
||||
bankComposition,
|
||||
details.pipelinePalettes(),
|
||||
studioMetadata.glyphMaterialMetadata(),
|
||||
studioMetadata.sceneBankMetadata(),
|
||||
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||
List.of());
|
||||
final SceneBankWorkflowResult sceneBankWorkflow = sceneBankWorkflowService.inspect(projectReference, draftDetails);
|
||||
final java.util.List<PackerDiagnosticDTO> mergedDiagnostics = new java.util.ArrayList<>(details.diagnostics());
|
||||
for (PackerDiagnosticDTO diagnostic : diagnostics) {
|
||||
if (!mergedDiagnostics.contains(diagnostic)) {
|
||||
mergedDiagnostics.add(diagnostic);
|
||||
}
|
||||
}
|
||||
for (PackerDiagnosticDTO diagnostic : sceneBankWorkflow.diagnostics()) {
|
||||
if (!mergedDiagnostics.contains(diagnostic)) {
|
||||
mergedDiagnostics.add(diagnostic);
|
||||
}
|
||||
}
|
||||
return new AssetWorkspaceAssetDetails(
|
||||
mappedSummary,
|
||||
mappedActions,
|
||||
details.outputFormat(),
|
||||
details.outputCodec(),
|
||||
details.availableOutputCodecs(),
|
||||
details.codecConfigurationFieldsByCodec(),
|
||||
details.metadataFields(),
|
||||
details.outputPipeline(),
|
||||
bankComposition,
|
||||
details.pipelinePalettes(),
|
||||
studioMetadata.glyphMaterialMetadata(),
|
||||
studioMetadata.sceneBankMetadata(),
|
||||
sceneBankWorkflow.validation(),
|
||||
mergedDiagnostics);
|
||||
}
|
||||
|
||||
|
||||
@ -16,6 +16,8 @@ import p.studio.controls.forms.StudioFormSection;
|
||||
import p.studio.controls.forms.StudioSection;
|
||||
import p.studio.projects.ProjectReference;
|
||||
import p.studio.utilities.i18n.I18n;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceSceneBankStatus;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetState;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBuildParticipation;
|
||||
|
||||
@ -29,6 +31,10 @@ public final class AssetDetailsUiSupport {
|
||||
return StudioSection.createSection(title, content);
|
||||
}
|
||||
|
||||
public static VBox createSectionWithoutHeader(Node content) {
|
||||
return StudioSection.createSectionWithoutHeader(content);
|
||||
}
|
||||
|
||||
public static VBox createSection(String title, Node headerActions, Node content) {
|
||||
return StudioFormSection.createEditableSection(title, headerActions, content, StudioFormMode.READ_ONLY);
|
||||
}
|
||||
@ -74,6 +80,14 @@ public final class AssetDetailsUiSupport {
|
||||
return chip;
|
||||
}
|
||||
|
||||
public static Label createChip(String toneClass, String text, String... extraClasses) {
|
||||
final Label chip = createChip(toneClass, text);
|
||||
if (extraClasses != null && extraClasses.length > 0) {
|
||||
chip.getStyleClass().addAll(extraClasses);
|
||||
}
|
||||
return chip;
|
||||
}
|
||||
|
||||
public static Button createActionButton(String text) {
|
||||
final Button button = new Button(text);
|
||||
button.getStyleClass().addAll("studio-button", "studio-button-primary", "assets-details-action-button");
|
||||
@ -114,6 +128,7 @@ public final class AssetDetailsUiSupport {
|
||||
public static String typeLabel(AssetFamilyCatalog assetFamily) {
|
||||
return switch (assetFamily) {
|
||||
case GLYPH_BANK -> Container.i18n().text(I18n.ASSETS_TYPE_GLYPH_BANK);
|
||||
case SCENE_BANK -> Container.i18n().text(I18n.ASSETS_TYPE_SCENE_BANK);
|
||||
case SOUND_BANK -> Container.i18n().text(I18n.ASSETS_TYPE_SOUND_BANK);
|
||||
case UNKNOWN -> Container.i18n().text(I18n.ASSETS_TYPE_UNKNOWN);
|
||||
};
|
||||
@ -122,11 +137,31 @@ public final class AssetDetailsUiSupport {
|
||||
public static String typeChipTone(AssetFamilyCatalog assetFamily) {
|
||||
return switch (assetFamily) {
|
||||
case GLYPH_BANK -> "assets-details-chip-image";
|
||||
case SCENE_BANK -> "assets-details-chip-scene";
|
||||
case SOUND_BANK -> "assets-details-chip-audio";
|
||||
case UNKNOWN -> "assets-details-chip-generic";
|
||||
};
|
||||
}
|
||||
|
||||
public static String specializationLabel(AssetStudioGlyphSpecialization specialization) {
|
||||
return switch (specialization) {
|
||||
case NONE -> Container.i18n().text(I18n.ASSETS_SPECIALIZATION_NONE);
|
||||
case TILESET -> Container.i18n().text(I18n.ASSETS_SPECIALIZATION_TILESET);
|
||||
case SPRITES -> Container.i18n().text(I18n.ASSETS_SPECIALIZATION_SPRITES);
|
||||
case UI -> Container.i18n().text(I18n.ASSETS_SPECIALIZATION_UI);
|
||||
};
|
||||
}
|
||||
|
||||
public static String sceneBankStatusLabel(AssetWorkspaceSceneBankStatus status) {
|
||||
return switch (status) {
|
||||
case NOT_APPLICABLE -> "";
|
||||
case PENDING_VALIDATION -> Container.i18n().text(I18n.ASSETS_SCENE_STATUS_PENDING_VALIDATION);
|
||||
case VALIDATED_PENDING_ACCEPTANCE -> Container.i18n().text(I18n.ASSETS_SCENE_STATUS_VALIDATED_PENDING_ACCEPTANCE);
|
||||
case READY -> Container.i18n().text(I18n.ASSETS_SCENE_STATUS_READY);
|
||||
case VALIDATION_FAILED -> Container.i18n().text(I18n.ASSETS_SCENE_STATUS_VALIDATION_FAILED);
|
||||
};
|
||||
}
|
||||
|
||||
public static String actionLabel(AssetAction action) {
|
||||
return switch (action) {
|
||||
case REGISTER -> Container.i18n().text(I18n.ASSETS_ACTION_REGISTER);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package p.studio.workspaces.assets.details;
|
||||
|
||||
import p.packer.dtos.PackerAssetSummaryDTO;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetState;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetSummary;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBuildParticipation;
|
||||
@ -25,6 +26,7 @@ public final class AssetListPackerMappings {
|
||||
buildParticipation,
|
||||
summary.identity().assetId(),
|
||||
summary.assetFamily(),
|
||||
AssetStudioGlyphSpecialization.NONE,
|
||||
summary.identity().assetRoot(),
|
||||
summary.preloadEnabled(),
|
||||
summary.hasDiagnostics());
|
||||
|
||||
@ -21,6 +21,12 @@ public final class AssetBankCapacityService {
|
||||
final Map<String, Object> safePipeline = Map.copyOf(Objects.requireNonNull(outputPipeline, "outputPipeline"));
|
||||
return switch (safeFamily) {
|
||||
case GLYPH_BANK -> evaluateGlyphBank(artifactCount, safeMetadata);
|
||||
case SCENE_BANK -> new AssetDetailsBankCompositionCapacityState(
|
||||
0.0d,
|
||||
StudioAssetCapacitySeverity.GREEN,
|
||||
false,
|
||||
artifactCount + " support files",
|
||||
"");
|
||||
case SOUND_BANK -> evaluateSoundBank(resolveSoundBankUsedBytes(safePipeline, usedBytes));
|
||||
case UNKNOWN -> new AssetDetailsBankCompositionCapacityState(
|
||||
0.0d,
|
||||
|
||||
@ -5,7 +5,10 @@ import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import p.packer.dtos.PackerBankCompositionDetailsDTO;
|
||||
import p.packer.dtos.PackerBankCompositionFileDTO;
|
||||
import p.packer.messages.ApplyBankCompositionRequest;
|
||||
import p.packer.messages.GetAssetDetailsRequest;
|
||||
import p.studio.Container;
|
||||
import p.studio.controls.banks.StudioAssetCapacityMeter;
|
||||
import p.studio.controls.forms.StudioFormEditScopeChangedEvent;
|
||||
@ -15,12 +18,14 @@ import p.studio.lsp.events.StudioWorkspaceEventBus;
|
||||
import p.studio.projects.ProjectReference;
|
||||
import p.studio.utilities.i18n.I18n;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionDetails;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetBankCompositionAppliedEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetBankCompositionApplyFailedEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetBankCompositionApplyRequestedEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetBankCompositionCapacityChangedEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetBankCompositionDraftChangedEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetsRefreshRequestedEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioBankCompositionChangedEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent;
|
||||
import p.studio.workspaces.framework.StudioSubscriptionBag;
|
||||
|
||||
@ -76,6 +81,19 @@ public final class AssetDetailsBankCompositionControl extends StudioFormSection
|
||||
}));
|
||||
subscriptions.add(workspaceBus.subscribe(StudioFormEditScopeChangedEvent.class,
|
||||
event -> handleEditScopeChanged(event, currentScopeKey())));
|
||||
subscriptions.add(workspaceBus.subscribe(StudioBankCompositionChangedEvent.class, event -> {
|
||||
if (viewState == null || !Objects.equals(viewState.selectedAssetReference(), event.assetReference())) {
|
||||
return;
|
||||
}
|
||||
viewState = new AssetWorkspaceDetailsViewState(
|
||||
viewState.detailsStatus(),
|
||||
viewState.selectedAssetReference(),
|
||||
viewState.selectedAssetDetails().withBankComposition(event.bankComposition()),
|
||||
viewState.detailsErrorMessage());
|
||||
coordinator.replaceDetails(viewState.selectedAssetDetails());
|
||||
renderSection();
|
||||
publishStateNotifications();
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -181,12 +199,9 @@ public final class AssetDetailsBankCompositionControl extends StudioFormSection
|
||||
final var response = Container.packer().workspaceService().applyBankComposition(request);
|
||||
Platform.runLater(() -> {
|
||||
if (response.success()) {
|
||||
coordinator.apply();
|
||||
publishEditScope(workspaceBus, currentScopeKey(), null);
|
||||
renderSection();
|
||||
publishStateNotifications();
|
||||
workspaceBus.publish(new StudioAssetBankCompositionAppliedEvent(assetReference));
|
||||
workspaceBus.publish(new StudioAssetsRefreshRequestedEvent(assetReference));
|
||||
Container.backgroundTasks().submit(() -> publishAppliedBankComposition(assetReference));
|
||||
} else {
|
||||
workspaceBus.publish(new StudioAssetBankCompositionApplyFailedEvent(
|
||||
assetReference,
|
||||
@ -201,6 +216,37 @@ public final class AssetDetailsBankCompositionControl extends StudioFormSection
|
||||
});
|
||||
}
|
||||
|
||||
private void publishAppliedBankComposition(p.packer.messages.AssetReference assetReference) {
|
||||
try {
|
||||
final var detailsResult = Container.packer().workspaceService().getAssetDetails(new GetAssetDetailsRequest(
|
||||
projectReference.toPackerProjectContext(),
|
||||
assetReference));
|
||||
final AssetWorkspaceBankCompositionDetails bankComposition = mapBankComposition(detailsResult.details().bankComposition());
|
||||
Platform.runLater(() -> workspaceBus.publish(new StudioBankCompositionChangedEvent(assetReference, bankComposition)));
|
||||
} catch (Exception exception) {
|
||||
Platform.runLater(() -> workspaceBus.publish(new StudioAssetBankCompositionApplyFailedEvent(
|
||||
assetReference,
|
||||
exception.getMessage())));
|
||||
}
|
||||
}
|
||||
|
||||
private AssetWorkspaceBankCompositionDetails mapBankComposition(PackerBankCompositionDetailsDTO details) {
|
||||
return new AssetWorkspaceBankCompositionDetails(
|
||||
details.availableFiles().stream().map(this::mapBankCompositionFile).toList(),
|
||||
details.selectedFiles().stream().map(this::mapBankCompositionFile).toList(),
|
||||
details.measuredBankSizeBytes());
|
||||
}
|
||||
|
||||
private AssetWorkspaceBankCompositionFile mapBankCompositionFile(PackerBankCompositionFileDTO file) {
|
||||
return new AssetWorkspaceBankCompositionFile(
|
||||
file.path(),
|
||||
file.displayName(),
|
||||
file.size(),
|
||||
file.lastModified(),
|
||||
file.fingerprint(),
|
||||
file.metadata());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reset() {
|
||||
coordinator.reset();
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package p.studio.workspaces.assets.details.palette;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.VBox;
|
||||
@ -18,8 +19,12 @@ 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.StudioGlyphMaterialMetadataChangedEvent;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialMetadata;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioMetadataService;
|
||||
import p.studio.workspaces.framework.StudioSubscriptionBag;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
@ -31,9 +36,12 @@ public final class AssetDetailsPaletteOverhaulingControl extends StudioFormSecti
|
||||
private final StudioWorkspaceEventBus workspaceBus;
|
||||
private final StudioSubscriptionBag subscriptions = new StudioSubscriptionBag();
|
||||
private final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
private final AssetStudioMetadataService metadataService = new AssetStudioMetadataService();
|
||||
private final AssetDetailsPaletteOverhaulingDualListView dualListView = new AssetDetailsPaletteOverhaulingDualListView();
|
||||
private final AssetDetailsPaletteOverhaulingPreviewPane previewPane = new AssetDetailsPaletteOverhaulingPreviewPane();
|
||||
private final HBox body = new HBox(16, dualListView, previewPane);
|
||||
private final VBox paletteActions = new VBox(6);
|
||||
private final HBox listWithActions = new HBox(8, paletteActions, dualListView);
|
||||
private final HBox body = new HBox(16, listWithActions, previewPane);
|
||||
private final VBox content = new VBox(12, body);
|
||||
|
||||
private AssetWorkspaceDetailsViewState viewState;
|
||||
@ -42,9 +50,12 @@ public final class AssetDetailsPaletteOverhaulingControl extends StudioFormSecti
|
||||
this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
|
||||
this.workspaceBus = Objects.requireNonNull(workspaceBus, "workspaceBus");
|
||||
body.getStyleClass().add("assets-details-palette-overhauling-body");
|
||||
paletteActions.getStyleClass().add("assets-details-palette-overhauling-toolbar");
|
||||
listWithActions.getStyleClass().add("assets-details-list-with-actions");
|
||||
content.setFillWidth(true);
|
||||
HBox.setHgrow(dualListView, Priority.ALWAYS);
|
||||
HBox.setHgrow(listWithActions, Priority.ALWAYS);
|
||||
HBox.setHgrow(previewPane, Priority.ALWAYS);
|
||||
HBox.setHgrow(dualListView, Priority.ALWAYS);
|
||||
dualListView.setMaxWidth(Double.MAX_VALUE);
|
||||
previewPane.setMaxWidth(Double.MAX_VALUE);
|
||||
}
|
||||
@ -61,6 +72,13 @@ public final class AssetDetailsPaletteOverhaulingControl extends StudioFormSecti
|
||||
}));
|
||||
subscriptions.add(workspaceBus.subscribe(StudioFormEditScopeChangedEvent.class,
|
||||
event -> handleEditScopeChanged(event, currentScopeKey())));
|
||||
subscriptions.add(workspaceBus.subscribe(StudioGlyphMaterialMetadataChangedEvent.class, event -> {
|
||||
if (viewState == null || !Objects.equals(viewState.selectedAssetReference(), event.assetReference())) {
|
||||
return;
|
||||
}
|
||||
coordinator.replaceMaterialMetadata(event.metadata());
|
||||
renderSection();
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -110,6 +128,8 @@ public final class AssetDetailsPaletteOverhaulingControl extends StudioFormSecti
|
||||
coordinator.viewModel().renderableFileCount());
|
||||
}
|
||||
});
|
||||
dualListView.setOnLeftItemActivated(item -> openPaletteWizard(item));
|
||||
dualListView.setOnRightItemActivated(item -> openPaletteWizard(item));
|
||||
if (viewModel.previewPaletteFile() != null) {
|
||||
if (viewModel.selectedFiles().contains(viewModel.previewPaletteFile())) {
|
||||
dualListView.selectRightItem(viewModel.previewPaletteFile());
|
||||
@ -134,6 +154,17 @@ public final class AssetDetailsPaletteOverhaulingControl extends StudioFormSecti
|
||||
viewModel.previewPaletteFile(),
|
||||
viewModel.renderableFileCount());
|
||||
|
||||
final Button addButton = new Button("+");
|
||||
addButton.getStyleClass().addAll("studio-button", "studio-button-secondary", "assets-details-mini-action-button");
|
||||
addButton.setDisable(!viewModel.editing() || viewState == null || viewState.selectedAssetDetails() == null);
|
||||
addButton.setOnAction(ignored -> openPaletteWizard(null));
|
||||
|
||||
final Button removeButton = new Button("-");
|
||||
removeButton.getStyleClass().addAll("studio-button", "studio-button-secondary", "assets-details-mini-action-button");
|
||||
removeButton.setDisable(!viewModel.editing() || viewModel.previewPaletteFile() == null);
|
||||
removeButton.setOnAction(ignored -> removePalette(coordinator.viewModel().previewPaletteFile()));
|
||||
paletteActions.getChildren().setAll(addButton, removeButton);
|
||||
|
||||
renderFormSection(content);
|
||||
}
|
||||
|
||||
@ -232,6 +263,67 @@ public final class AssetDetailsPaletteOverhaulingControl extends StudioFormSecti
|
||||
: "asset-details:" + viewState.selectedAssetReference();
|
||||
}
|
||||
|
||||
private void openPaletteWizard(p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile paletteFile) {
|
||||
if (getScene() == null || !coordinator.ready() || viewState == null || viewState.selectedAssetDetails() == null) {
|
||||
return;
|
||||
}
|
||||
if (AssetDetailsPaletteOverhaulingCoordinator.paletteIsOriginal(paletteFile)) {
|
||||
return;
|
||||
}
|
||||
final var result = AssetDetailsPaletteWizardDialog.showAndWait(
|
||||
getScene().getWindow(),
|
||||
coordinator,
|
||||
paletteFile);
|
||||
result.ifPresent(edit -> Container.backgroundTasks().submit(() -> {
|
||||
try {
|
||||
final AssetStudioGlyphMaterialMetadata metadata = coordinator.upsertPalette(
|
||||
edit.paletteId(),
|
||||
edit.displayName(),
|
||||
edit.schemaId(),
|
||||
edit.colors());
|
||||
metadataService.writeGlyphMaterialMetadata(viewState.selectedAssetDetails().summary().assetRoot(), metadata);
|
||||
Platform.runLater(() -> {
|
||||
workspaceBus.publish(new StudioAssetLogEvent("palette-overhauling", "Palette saved: " + edit.paletteId()));
|
||||
workspaceBus.publish(new StudioGlyphMaterialMetadataChangedEvent(viewState.selectedAssetReference(), metadata));
|
||||
});
|
||||
} catch (IOException exception) {
|
||||
Platform.runLater(() -> workspaceBus.publish(new StudioAssetLogEvent(
|
||||
"palette-overhauling",
|
||||
"Unable to save palette: " + Objects.requireNonNullElse(exception.getMessage(), "unknown error"))));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private void removePalette(p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile paletteFile) {
|
||||
if (paletteFile == null || viewState == null || viewState.selectedAssetDetails() == null || viewState.selectedAssetReference() == null) {
|
||||
return;
|
||||
}
|
||||
if (AssetDetailsPaletteOverhaulingCoordinator.paletteIsOriginal(paletteFile)) {
|
||||
workspaceBus.publish(new StudioAssetLogEvent(
|
||||
"palette-overhauling",
|
||||
"Original schema palettes cannot be removed."));
|
||||
return;
|
||||
}
|
||||
final Object paletteIdValue = paletteFile.metadata().get("paletteId");
|
||||
if (!(paletteIdValue instanceof String paletteId) || paletteId.isBlank()) {
|
||||
return;
|
||||
}
|
||||
Container.backgroundTasks().submit(() -> {
|
||||
try {
|
||||
final AssetStudioGlyphMaterialMetadata metadata = coordinator.removePalette(paletteId);
|
||||
metadataService.writeGlyphMaterialMetadata(viewState.selectedAssetDetails().summary().assetRoot(), metadata);
|
||||
Platform.runLater(() -> {
|
||||
workspaceBus.publish(new StudioAssetLogEvent("palette-overhauling", "Palette removed: " + paletteId));
|
||||
workspaceBus.publish(new StudioGlyphMaterialMetadataChangedEvent(viewState.selectedAssetReference(), metadata));
|
||||
});
|
||||
} catch (Exception exception) {
|
||||
Platform.runLater(() -> workspaceBus.publish(new StudioAssetLogEvent(
|
||||
"palette-overhauling",
|
||||
"Unable to remove palette: " + Objects.requireNonNullElse(exception.getMessage(), "unknown error"))));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Map<String, Object> palettePayload(Map<String, Object> metadata) {
|
||||
final Object palette = metadata.get("palette");
|
||||
|
||||
@ -5,24 +5,40 @@ import p.studio.controls.forms.StudioFormMode;
|
||||
import p.studio.controls.forms.StudioFormSession;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialArtifactBinding;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialMetadata;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialPalette;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialSchema;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialSchemaStatus;
|
||||
import p.studio.workspaces.assets.details.schema.AssetDetailsColorSchemaCoordinator;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.*;
|
||||
|
||||
public final class AssetDetailsPaletteOverhaulingCoordinator {
|
||||
private StudioFormSession<AssetDetailsPaletteOverhaulingDraft> formSession;
|
||||
private AssetWorkspaceAssetDetails details;
|
||||
private AssetStudioGlyphMaterialMetadata localMaterialMetadata;
|
||||
|
||||
public void replaceDetails(AssetWorkspaceAssetDetails details) {
|
||||
this.details = details;
|
||||
if (details == null || details.summary().assetFamily() != AssetFamilyCatalog.GLYPH_BANK) {
|
||||
formSession = null;
|
||||
localMaterialMetadata = null;
|
||||
return;
|
||||
}
|
||||
localMaterialMetadata = details.glyphMaterialMetadata();
|
||||
rebuildSession();
|
||||
}
|
||||
|
||||
public void replaceMaterialMetadata(AssetStudioGlyphMaterialMetadata metadata) {
|
||||
if (details == null || details.summary().assetFamily() != AssetFamilyCatalog.GLYPH_BANK) {
|
||||
return;
|
||||
}
|
||||
localMaterialMetadata = Objects.requireNonNull(metadata, "metadata");
|
||||
rebuildSession();
|
||||
}
|
||||
|
||||
private void rebuildSession() {
|
||||
final List<AssetWorkspaceBankCompositionFile> allFiles = paletteFiles(details);
|
||||
final List<AssetWorkspaceBankCompositionFile> previewTileFiles = details.bankComposition().selectedFiles().stream()
|
||||
.filter(AssetDetailsPaletteOverhaulingCoordinator::supportsPalettePreview)
|
||||
@ -37,12 +53,21 @@ public final class AssetDetailsPaletteOverhaulingCoordinator {
|
||||
available,
|
||||
initialSelected,
|
||||
previewTileFiles.isEmpty() ? null : previewTileFiles.getFirst().path(),
|
||||
null);
|
||||
initialSelected.isEmpty()
|
||||
? allFiles.isEmpty() ? null : allFiles.getFirst().path()
|
||||
: initialSelected.getFirst().path());
|
||||
if (formSession == null) {
|
||||
formSession = new StudioFormSession<>(source);
|
||||
return;
|
||||
}
|
||||
if (!Objects.equals(formSession.source(), source)) {
|
||||
if (formSession.mode() == StudioFormMode.EDITING) {
|
||||
final AssetDetailsPaletteOverhaulingDraft currentDraft = formSession.draft();
|
||||
formSession = new StudioFormSession<>(source);
|
||||
formSession.beginEdit();
|
||||
formSession.setDraft(rebindDraft(currentDraft, source));
|
||||
return;
|
||||
}
|
||||
formSession.replaceSource(source);
|
||||
}
|
||||
}
|
||||
@ -51,6 +76,10 @@ public final class AssetDetailsPaletteOverhaulingCoordinator {
|
||||
return formSession != null;
|
||||
}
|
||||
|
||||
public AssetWorkspaceAssetDetails details() {
|
||||
return details;
|
||||
}
|
||||
|
||||
public void beginEdit() {
|
||||
if (ready()) {
|
||||
formSession.beginEdit();
|
||||
@ -77,7 +106,10 @@ public final class AssetDetailsPaletteOverhaulingCoordinator {
|
||||
|
||||
public void moveToSelected(List<AssetWorkspaceBankCompositionFile> files) {
|
||||
if (ready() && formSession.mode() == StudioFormMode.EDITING) {
|
||||
formSession.updateDraft(current -> current.moveToSelected(files));
|
||||
final List<AssetWorkspaceBankCompositionFile> validFiles = files.stream()
|
||||
.filter(AssetDetailsPaletteOverhaulingCoordinator::paletteIsValid)
|
||||
.toList();
|
||||
formSession.updateDraft(current -> current.moveToSelected(validFiles));
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,27 +137,118 @@ public final class AssetDetailsPaletteOverhaulingCoordinator {
|
||||
}
|
||||
|
||||
final AssetDetailsPaletteOverhaulingDraft draft = formSession.draft();
|
||||
final AssetWorkspaceBankCompositionFile previewPaletteFile = fileByPath(draft.allFiles(), draft.previewPalettePath());
|
||||
final List<AssetWorkspaceBankCompositionFile> schemaPreviewTileFiles = previewTileFilesForPalette(
|
||||
draft.previewTileFiles(),
|
||||
previewPaletteFile);
|
||||
final AssetWorkspaceBankCompositionFile previewTileFile = Optional
|
||||
.ofNullable(fileByPath(schemaPreviewTileFiles, draft.previewTilePath()))
|
||||
.orElse(schemaPreviewTileFiles.isEmpty() ? null : schemaPreviewTileFiles.getFirst());
|
||||
return new AssetDetailsPaletteOverhaulingViewModel(
|
||||
formSession.mode() == StudioFormMode.EDITING,
|
||||
formSession.isDirty(),
|
||||
draft.previewTileFiles(),
|
||||
schemaPreviewTileFiles,
|
||||
draft.allFiles(),
|
||||
draft.availableFiles(),
|
||||
draft.selectedFiles(),
|
||||
fileByPath(draft.previewTileFiles(), draft.previewTilePath()),
|
||||
fileByPath(draft.allFiles(), draft.previewPalettePath()),
|
||||
previewTileFile,
|
||||
previewPaletteFile,
|
||||
draft.allFiles().size());
|
||||
}
|
||||
|
||||
static boolean supportsPalettePreview(AssetWorkspaceBankCompositionFile file) {
|
||||
public List<AssetStudioGlyphMaterialSchema> readySchemas() {
|
||||
return materialMetadata().schemas().stream()
|
||||
.filter(schema -> schema.status() == AssetStudioGlyphMaterialSchemaStatus.OK)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public String nextPaletteId() {
|
||||
int next = 1;
|
||||
final Set<String> ids = materialMetadata().palettes().stream()
|
||||
.map(AssetStudioGlyphMaterialPalette::paletteId)
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
while (ids.contains("palette-" + next)) {
|
||||
next += 1;
|
||||
}
|
||||
return "palette-" + next;
|
||||
}
|
||||
|
||||
public boolean duplicatePaletteExists(String paletteId, String schemaId, List<String> colors) {
|
||||
final List<String> normalized = colors.stream().map(AssetDetailsPaletteOverhaulingCoordinator::normalizeHtmlColor).toList();
|
||||
return allPaletteRecords().stream()
|
||||
.filter(record -> !Objects.equals(record.paletteId(), paletteId))
|
||||
.anyMatch(record -> record.schemaId().equals(schemaId) && record.colors().equals(normalized));
|
||||
}
|
||||
|
||||
public AssetStudioGlyphMaterialMetadata upsertPalette(String paletteId, String displayName, String schemaId, List<String> colors) {
|
||||
final AssetStudioGlyphMaterialMetadata current = materialMetadata();
|
||||
final List<String> normalized = colors.stream().map(AssetDetailsPaletteOverhaulingCoordinator::normalizeHtmlColor).toList();
|
||||
final int schemaVersion = current.findSchema(schemaId).map(AssetStudioGlyphMaterialSchema::version).orElse(1);
|
||||
final String resolvedDisplayName = displayName == null || displayName.trim().isBlank()
|
||||
? generatedDisplayName(normalized)
|
||||
: displayName.trim();
|
||||
final AssetStudioGlyphMaterialPalette next = new AssetStudioGlyphMaterialPalette(
|
||||
paletteId,
|
||||
resolvedDisplayName,
|
||||
schemaId,
|
||||
schemaVersion,
|
||||
normalized);
|
||||
final List<AssetStudioGlyphMaterialPalette> palettes = new ArrayList<>();
|
||||
boolean replaced = false;
|
||||
for (AssetStudioGlyphMaterialPalette palette : current.palettes()) {
|
||||
if (palette.paletteId().equals(paletteId)) {
|
||||
palettes.add(next);
|
||||
replaced = true;
|
||||
} else {
|
||||
palettes.add(palette);
|
||||
}
|
||||
}
|
||||
if (!replaced) {
|
||||
palettes.add(next);
|
||||
}
|
||||
palettes.sort(Comparator.comparing(AssetStudioGlyphMaterialPalette::paletteId, String.CASE_INSENSITIVE_ORDER));
|
||||
return new AssetStudioGlyphMaterialMetadata(current.schemas(), palettes, current.supportFile());
|
||||
}
|
||||
|
||||
public AssetStudioGlyphMaterialMetadata removePalette(String paletteId) {
|
||||
final AssetWorkspaceBankCompositionFile paletteFile = paletteFileById(paletteId);
|
||||
if (paletteFile != null && Boolean.TRUE.equals(paletteFile.metadata().get("derivedOriginal"))) {
|
||||
throw new IllegalStateException("Original schema palettes are derived and cannot be removed.");
|
||||
}
|
||||
final AssetStudioGlyphMaterialMetadata current = materialMetadata();
|
||||
final List<AssetStudioGlyphMaterialPalette> palettes = current.palettes().stream()
|
||||
.filter(palette -> !palette.paletteId().equals(paletteId))
|
||||
.toList();
|
||||
return new AssetStudioGlyphMaterialMetadata(current.schemas(), palettes, current.supportFile());
|
||||
}
|
||||
|
||||
public AssetWorkspaceBankCompositionFile paletteFileById(String paletteId) {
|
||||
if (paletteId == null || !ready()) {
|
||||
return null;
|
||||
}
|
||||
return paletteFiles(details).stream()
|
||||
.filter(file -> paletteId.equals(file.metadata().get("paletteId")))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public static boolean supportsPalettePreview(AssetWorkspaceBankCompositionFile file) {
|
||||
if (file == null) {
|
||||
return false;
|
||||
}
|
||||
return nestedMap(file.metadata(), "tile") != null && nestedMap(file.metadata(), "palette") != null;
|
||||
}
|
||||
|
||||
public static boolean paletteIsValid(AssetWorkspaceBankCompositionFile file) {
|
||||
return !Boolean.FALSE.equals(file.metadata().get("valid"));
|
||||
}
|
||||
|
||||
public static boolean paletteIsOriginal(AssetWorkspaceBankCompositionFile file) {
|
||||
return file != null && Boolean.TRUE.equals(file.metadata().get("derivedOriginal"));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Map<String, Object> nestedMap(Map<String, Object> metadata, String key) {
|
||||
static Map<String, Object> nestedMap(Map<String, Object> metadata, String key) {
|
||||
final Object value = metadata.get(key);
|
||||
if (value instanceof Map<?, ?> nested) {
|
||||
return (Map<String, Object>) nested;
|
||||
@ -133,37 +256,236 @@ public final class AssetDetailsPaletteOverhaulingCoordinator {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static List<AssetWorkspaceBankCompositionFile> paletteFiles(AssetWorkspaceAssetDetails details) {
|
||||
final Map<String, AssetWorkspaceBankCompositionFile> ordered = new LinkedHashMap<>();
|
||||
// `bankComposition` files are already projected by the packer read model from the current cache/walk state.
|
||||
for (AssetWorkspaceBankCompositionFile file : details.bankComposition().availableFiles()) {
|
||||
if (supportsPalettePreview(file)) {
|
||||
ordered.putIfAbsent(paletteHash(file), canonicalPaletteFile(file));
|
||||
}
|
||||
private AssetStudioGlyphMaterialMetadata materialMetadata() {
|
||||
if (details == null) {
|
||||
return new AssetStudioGlyphMaterialMetadata(List.of(), List.of(), null);
|
||||
}
|
||||
for (AssetWorkspaceBankCompositionFile file : details.bankComposition().selectedFiles()) {
|
||||
if (supportsPalettePreview(file)) {
|
||||
ordered.putIfAbsent(paletteHash(file), canonicalPaletteFile(file));
|
||||
}
|
||||
return localMaterialMetadata == null
|
||||
? new AssetStudioGlyphMaterialMetadata(List.of(), List.of(), null)
|
||||
: localMaterialMetadata;
|
||||
}
|
||||
|
||||
private List<AssetWorkspaceBankCompositionFile> paletteFiles(AssetWorkspaceAssetDetails details) {
|
||||
final Map<String, AssetWorkspaceBankCompositionFile> ordered = new LinkedHashMap<>();
|
||||
for (PaletteRecord record : allPaletteRecords()) {
|
||||
final AssetWorkspaceBankCompositionFile file = toPaletteFile(record);
|
||||
ordered.putIfAbsent(file.path(), file);
|
||||
}
|
||||
return List.copyOf(ordered.values());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static AssetWorkspaceBankCompositionFile canonicalPaletteFile(AssetWorkspaceBankCompositionFile file) {
|
||||
final String hash = paletteHash(file);
|
||||
final Map<String, Object> metadata = new LinkedHashMap<>(file.metadata());
|
||||
metadata.put("paletteHash", hash);
|
||||
private List<PaletteRecord> allPaletteRecords() {
|
||||
final AssetStudioGlyphMaterialMetadata metadata = materialMetadata();
|
||||
final List<PaletteRecord> records = new ArrayList<>();
|
||||
for (AssetStudioGlyphMaterialSchema schema : metadata.schemas()) {
|
||||
final AssetStudioGlyphMaterialSchemaStatus effectiveSchemaStatus = AssetDetailsColorSchemaCoordinator.effectiveStatus(
|
||||
schema,
|
||||
details.bankComposition().selectedFiles());
|
||||
final List<String> originalColors = schema.materialIndices().entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.map(Map.Entry::getValue)
|
||||
.toList();
|
||||
records.add(new PaletteRecord(
|
||||
"schema-" + schema.schemaId() + "-original",
|
||||
null,
|
||||
schema.schemaId(),
|
||||
schema.version(),
|
||||
originalColors,
|
||||
schema.materialIndices(),
|
||||
schemaArtifactIds(schema),
|
||||
true,
|
||||
effectiveSchemaStatus == AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
effectiveSchemaStatus == AssetStudioGlyphMaterialSchemaStatus.OK
|
||||
? null
|
||||
: "Schema is inconsistent."));
|
||||
}
|
||||
for (AssetStudioGlyphMaterialPalette palette : metadata.palettes()) {
|
||||
final AssetStudioGlyphMaterialSchema schema = metadata.findSchema(palette.schemaId()).orElse(null);
|
||||
final AssetStudioGlyphMaterialSchemaStatus effectiveSchemaStatus = schema == null
|
||||
? AssetStudioGlyphMaterialSchemaStatus.INCONSISTENT
|
||||
: AssetDetailsColorSchemaCoordinator.effectiveStatus(schema, details.bankComposition().selectedFiles());
|
||||
final boolean valid = schema != null
|
||||
&& effectiveSchemaStatus == AssetStudioGlyphMaterialSchemaStatus.OK
|
||||
&& palette.version() >= schema.version();
|
||||
final String invalidReason;
|
||||
if (schema == null) {
|
||||
invalidReason = "Schema is missing.";
|
||||
} else if (effectiveSchemaStatus != AssetStudioGlyphMaterialSchemaStatus.OK) {
|
||||
invalidReason = "Schema is inconsistent.";
|
||||
} else if (palette.version() < schema.version()) {
|
||||
invalidReason = "Palette is stale and must be updated.";
|
||||
} else {
|
||||
invalidReason = null;
|
||||
}
|
||||
records.add(new PaletteRecord(
|
||||
palette.paletteId(),
|
||||
palette.displayName(),
|
||||
palette.schemaId(),
|
||||
palette.version(),
|
||||
palette.colors(),
|
||||
schema == null ? Map.of() : schema.materialIndices(),
|
||||
schema == null ? Set.of() : schemaArtifactIds(schema),
|
||||
false,
|
||||
valid,
|
||||
invalidReason));
|
||||
}
|
||||
records.sort(Comparator.comparing(PaletteRecord::paletteId, String.CASE_INSENSITIVE_ORDER));
|
||||
return List.copyOf(records);
|
||||
}
|
||||
|
||||
private AssetWorkspaceBankCompositionFile toPaletteFile(PaletteRecord record) {
|
||||
final List<Integer> argb = record.colors().stream()
|
||||
.map(AssetDetailsPaletteOverhaulingCoordinator::htmlToArgb8888)
|
||||
.toList();
|
||||
final List<Integer> rgb565 = record.colors().stream()
|
||||
.map(AssetDetailsPaletteOverhaulingCoordinator::htmlToRgb565)
|
||||
.toList();
|
||||
final Map<String, Object> palette = Map.of(
|
||||
"originalArgb8888", argb,
|
||||
"convertedRgb565", rgb565);
|
||||
final Map<String, Object> metadata = new LinkedHashMap<>();
|
||||
metadata.put("palette", palette);
|
||||
metadata.put("paletteId", record.paletteId());
|
||||
metadata.put("paletteDisplayName", displayName(record));
|
||||
if (record.displayName() != null) {
|
||||
metadata.put("explicitPaletteDisplayName", record.displayName());
|
||||
}
|
||||
metadata.put("schemaId", record.schemaId());
|
||||
metadata.put("schemaMaterialIndices", record.materialIndices());
|
||||
metadata.put("schemaArtifactIds", List.copyOf(record.artifactIds()));
|
||||
metadata.put("paletteVersion", record.version());
|
||||
metadata.put("derivedOriginal", record.derivedOriginal());
|
||||
metadata.put("valid", record.valid());
|
||||
if (record.invalidReason() != null) {
|
||||
metadata.put("invalidReason", record.invalidReason());
|
||||
}
|
||||
return new AssetWorkspaceBankCompositionFile(
|
||||
hash,
|
||||
hash,
|
||||
file.size(),
|
||||
file.lastModified(),
|
||||
file.fingerprint(),
|
||||
record.paletteId(),
|
||||
displayName(record),
|
||||
0L,
|
||||
0L,
|
||||
null,
|
||||
metadata);
|
||||
}
|
||||
|
||||
private static List<AssetWorkspaceBankCompositionFile> initialSelectedPaletteFiles(
|
||||
static String generatedDisplayName(List<String> colors) {
|
||||
final List<ColorSample> samples = colors.stream()
|
||||
.map(AssetDetailsPaletteOverhaulingCoordinator::toColorSample)
|
||||
.toList();
|
||||
if (samples.isEmpty()) {
|
||||
return "quiet shade";
|
||||
}
|
||||
final ColorSample anchor = samples.stream()
|
||||
.min(Comparator.comparingDouble(sample -> sample.saturation < 0.16 ? sample.lightness : -sample.saturation))
|
||||
.orElse(samples.getFirst());
|
||||
final double averageLightness = samples.stream()
|
||||
.mapToDouble(sample -> sample.lightness)
|
||||
.average()
|
||||
.orElse(anchor.lightness);
|
||||
final double averageSaturation = samples.stream()
|
||||
.mapToDouble(sample -> sample.saturation)
|
||||
.average()
|
||||
.orElse(anchor.saturation);
|
||||
final double lightnessRange = samples.stream().mapToDouble(sample -> sample.lightness).max().orElse(anchor.lightness)
|
||||
- samples.stream().mapToDouble(sample -> sample.lightness).min().orElse(anchor.lightness);
|
||||
return baseName(anchor, averageLightness, averageSaturation) + " " + moodName(averageLightness, averageSaturation, lightnessRange, samples.size());
|
||||
}
|
||||
|
||||
private static String baseName(ColorSample sample, double averageLightness, double averageSaturation) {
|
||||
if (averageSaturation < 0.12) {
|
||||
if (averageLightness > 0.88) {
|
||||
return "snow";
|
||||
}
|
||||
if (averageLightness > 0.68) {
|
||||
return "fog";
|
||||
}
|
||||
if (averageLightness < 0.24) {
|
||||
return "ink";
|
||||
}
|
||||
return "ash";
|
||||
}
|
||||
final double hue = sample.hue;
|
||||
if (hue < 18 || hue >= 345) {
|
||||
return sample.lightness > 0.62 ? "rose" : "ember";
|
||||
}
|
||||
if (hue < 42) {
|
||||
return sample.lightness > 0.62 ? "honey" : "copper";
|
||||
}
|
||||
if (hue < 72) {
|
||||
return sample.lightness > 0.68 ? "sun" : "ochre";
|
||||
}
|
||||
if (hue < 155) {
|
||||
return sample.lightness > 0.58 ? "moss" : "pine";
|
||||
}
|
||||
if (hue < 195) {
|
||||
return sample.lightness > 0.62 ? "mint" : "jade";
|
||||
}
|
||||
if (hue < 245) {
|
||||
return sample.lightness > 0.62 ? "sky" : "cobalt";
|
||||
}
|
||||
if (hue < 290) {
|
||||
return sample.lightness > 0.62 ? "lilac" : "violet";
|
||||
}
|
||||
return sample.lightness > 0.62 ? "orchid" : "plum";
|
||||
}
|
||||
|
||||
private static String moodName(double averageLightness, double averageSaturation, double lightnessRange, int colorCount) {
|
||||
if (averageSaturation < 0.12 && averageLightness > 0.86) {
|
||||
return "white";
|
||||
}
|
||||
if (colorCount > 1 && lightnessRange > 0.42) {
|
||||
return "twist";
|
||||
}
|
||||
if (averageLightness > 0.78) {
|
||||
return averageSaturation > 0.45 ? "glow" : "veil";
|
||||
}
|
||||
if (averageLightness < 0.26) {
|
||||
return averageSaturation > 0.34 ? "deep" : "shade";
|
||||
}
|
||||
if (averageSaturation > 0.62) {
|
||||
return "bloom";
|
||||
}
|
||||
if (averageSaturation < 0.22) {
|
||||
return "haze";
|
||||
}
|
||||
return "drift";
|
||||
}
|
||||
|
||||
private static ColorSample toColorSample(String htmlColor) {
|
||||
final int rgb = Integer.parseInt(normalizeHtmlColor(htmlColor).substring(1), 16);
|
||||
final double red = ((rgb >> 16) & 0xFF) / 255.0;
|
||||
final double green = ((rgb >> 8) & 0xFF) / 255.0;
|
||||
final double blue = (rgb & 0xFF) / 255.0;
|
||||
final double max = Math.max(red, Math.max(green, blue));
|
||||
final double min = Math.min(red, Math.min(green, blue));
|
||||
final double delta = max - min;
|
||||
final double lightness = (max + min) / 2.0;
|
||||
final double saturation = delta == 0.0
|
||||
? 0.0
|
||||
: delta / (1.0 - Math.abs((2.0 * lightness) - 1.0));
|
||||
final double hue;
|
||||
if (delta == 0.0) {
|
||||
hue = 0.0;
|
||||
} else if (max == red) {
|
||||
hue = 60.0 * (((green - blue) / delta) % 6.0);
|
||||
} else if (max == green) {
|
||||
hue = 60.0 * (((blue - red) / delta) + 2.0);
|
||||
} else {
|
||||
hue = 60.0 * (((red - green) / delta) + 4.0);
|
||||
}
|
||||
return new ColorSample(hue < 0.0 ? hue + 360.0 : hue, saturation, lightness);
|
||||
}
|
||||
|
||||
private static String displayName(PaletteRecord record) {
|
||||
if (record.derivedOriginal()) {
|
||||
return record.schemaId() + " / original";
|
||||
}
|
||||
return record.displayName() == null || record.displayName().isBlank()
|
||||
? generatedDisplayName(record.colors())
|
||||
: record.displayName();
|
||||
}
|
||||
|
||||
private List<AssetWorkspaceBankCompositionFile> initialSelectedPaletteFiles(
|
||||
AssetWorkspaceAssetDetails details,
|
||||
List<AssetWorkspaceBankCompositionFile> allFiles) {
|
||||
final List<AssetWorkspaceBankCompositionFile> selected = new ArrayList<>();
|
||||
@ -186,32 +508,21 @@ public final class AssetDetailsPaletteOverhaulingCoordinator {
|
||||
&& Objects.equals(left.get("convertedRgb565"), right.get("convertedRgb565"));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static String paletteHash(AssetWorkspaceBankCompositionFile file) {
|
||||
final Map<String, Object> palette = nestedMap(file.metadata(), "palette");
|
||||
final Object convertedValue = palette == null ? null : palette.get("convertedRgb565");
|
||||
final List<Integer> colors = convertedValue instanceof List<?> converted
|
||||
? (List<Integer>) converted
|
||||
: List.of();
|
||||
try {
|
||||
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
for (int index = 0; index < 15; index++) {
|
||||
final int color = index < colors.size() && colors.get(index) != null ? colors.get(index) : 0;
|
||||
digest.update(ByteBuffer.allocate(4).putInt(color).array());
|
||||
}
|
||||
long value = 0L;
|
||||
final byte[] bytes = digest.digest();
|
||||
for (int index = 0; index < 8; index++) {
|
||||
value = (value << 8) | Byte.toUnsignedLong(bytes[index]);
|
||||
}
|
||||
value &= 0x0FFFFFFFFFFFFFFFL;
|
||||
final String base36 = Long.toUnsignedString(value, 36).toUpperCase();
|
||||
return base36.length() >= 10
|
||||
? base36.substring(0, 10)
|
||||
: "0".repeat(10 - base36.length()) + base36;
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalStateException("Unable to hash palette colors", exception);
|
||||
}
|
||||
private static int htmlToArgb8888(String htmlColor) {
|
||||
final int rgb = Integer.parseInt(normalizeHtmlColor(htmlColor).substring(1), 16);
|
||||
return 0xFF000000 | rgb;
|
||||
}
|
||||
|
||||
private static int htmlToRgb565(String htmlColor) {
|
||||
final int rgb = Integer.parseInt(normalizeHtmlColor(htmlColor).substring(1), 16);
|
||||
final int red = (rgb >> 16) & 0xFF;
|
||||
final int green = (rgb >> 8) & 0xFF;
|
||||
final int blue = rgb & 0xFF;
|
||||
return ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
|
||||
}
|
||||
|
||||
private static String normalizeHtmlColor(String color) {
|
||||
return Objects.requireNonNull(color, "color").trim().toUpperCase();
|
||||
}
|
||||
|
||||
private static AssetWorkspaceBankCompositionFile fileByPath(List<AssetWorkspaceBankCompositionFile> files, String path) {
|
||||
@ -223,4 +534,93 @@ public final class AssetDetailsPaletteOverhaulingCoordinator {
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private static AssetDetailsPaletteOverhaulingDraft rebindDraft(
|
||||
AssetDetailsPaletteOverhaulingDraft draft,
|
||||
AssetDetailsPaletteOverhaulingDraft source) {
|
||||
final Set<String> selectedPaths = draft.selectedFiles().stream()
|
||||
.map(AssetWorkspaceBankCompositionFile::path)
|
||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
||||
final List<AssetWorkspaceBankCompositionFile> selected = source.allFiles().stream()
|
||||
.filter(file -> selectedPaths.contains(file.path()))
|
||||
.toList();
|
||||
final List<AssetWorkspaceBankCompositionFile> available = source.allFiles().stream()
|
||||
.filter(file -> !selectedPaths.contains(file.path()))
|
||||
.toList();
|
||||
final String previewPalettePath = fileByPath(source.allFiles(), draft.previewPalettePath()) == null
|
||||
? source.previewPalettePath()
|
||||
: draft.previewPalettePath();
|
||||
final String previewTilePath = fileByPath(source.previewTileFiles(), draft.previewTilePath()) == null
|
||||
? source.previewTilePath()
|
||||
: draft.previewTilePath();
|
||||
return new AssetDetailsPaletteOverhaulingDraft(
|
||||
source.previewTileFiles(),
|
||||
source.allFiles(),
|
||||
available,
|
||||
selected,
|
||||
previewTilePath,
|
||||
previewPalettePath);
|
||||
}
|
||||
|
||||
private static Set<String> schemaArtifactIds(AssetStudioGlyphMaterialSchema schema) {
|
||||
return schema.artifactBindings().stream()
|
||||
.map(AssetStudioGlyphMaterialArtifactBinding::artifactId)
|
||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
||||
}
|
||||
|
||||
private static List<AssetWorkspaceBankCompositionFile> previewTileFilesForPalette(
|
||||
List<AssetWorkspaceBankCompositionFile> previewTileFiles,
|
||||
AssetWorkspaceBankCompositionFile previewPaletteFile) {
|
||||
if (previewPaletteFile == null) {
|
||||
return previewTileFiles;
|
||||
}
|
||||
final Set<String> schemaArtifactIds = schemaArtifactIds(previewPaletteFile);
|
||||
if (schemaArtifactIds.isEmpty()) {
|
||||
return previewTileFiles;
|
||||
}
|
||||
return previewTileFiles.stream()
|
||||
.filter(file -> schemaArtifactIds.contains(artifactId(file)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static Set<String> schemaArtifactIds(AssetWorkspaceBankCompositionFile previewPaletteFile) {
|
||||
final Object artifactIds = previewPaletteFile.metadata().get("schemaArtifactIds");
|
||||
if (!(artifactIds instanceof Iterable<?> iterable)) {
|
||||
return Set.of();
|
||||
}
|
||||
final Set<String> ids = new LinkedHashSet<>();
|
||||
for (Object artifactId : iterable) {
|
||||
if (artifactId instanceof String value && !value.isBlank()) {
|
||||
ids.add(value.trim());
|
||||
}
|
||||
}
|
||||
return Set.copyOf(ids);
|
||||
}
|
||||
|
||||
private static String artifactId(AssetWorkspaceBankCompositionFile file) {
|
||||
if (file == null) {
|
||||
return null;
|
||||
}
|
||||
final Object artifactId = file.metadata().get("artifactId");
|
||||
if (artifactId instanceof String value && !value.isBlank()) {
|
||||
return value.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private record PaletteRecord(
|
||||
String paletteId,
|
||||
String displayName,
|
||||
String schemaId,
|
||||
int version,
|
||||
List<String> colors,
|
||||
Map<Integer, String> materialIndices,
|
||||
Set<String> artifactIds,
|
||||
boolean derivedOriginal,
|
||||
boolean valid,
|
||||
String invalidReason) {
|
||||
}
|
||||
|
||||
private record ColorSample(double hue, double saturation, double lightness) {
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@ import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import p.studio.Container;
|
||||
import p.studio.controls.banks.StudioDualListView;
|
||||
import p.studio.utilities.i18n.I18n;
|
||||
@ -28,8 +29,15 @@ public final class AssetDetailsPaletteOverhaulingDualListView extends StudioDual
|
||||
final Label hashLabel = new Label(item.displayName());
|
||||
hashLabel.getStyleClass().add("assets-details-palette-hash");
|
||||
final HBox strip = createPaletteStrip(item);
|
||||
final HBox root = new HBox(8, hashLabel, strip);
|
||||
root.getStyleClass().add("assets-details-palette-item");
|
||||
final HBox top = new HBox(8, hashLabel, strip);
|
||||
top.getStyleClass().add("assets-details-palette-item");
|
||||
final Object invalidReason = item.metadata().get("invalidReason");
|
||||
if (!(invalidReason instanceof String reason) || reason.isBlank()) {
|
||||
return top;
|
||||
}
|
||||
final Label invalidLabel = new Label(reason);
|
||||
invalidLabel.getStyleClass().add("assets-details-palette-invalid");
|
||||
final VBox root = new VBox(4, top, invalidLabel);
|
||||
return root;
|
||||
}
|
||||
|
||||
|
||||
@ -146,6 +146,8 @@ public final class AssetDetailsPaletteOverhaulingPreviewPane extends VBox {
|
||||
final Integer height = asInteger(tile.get("height"));
|
||||
final List<Integer> indices = paletteIndices(tile.get("paletteIndices"));
|
||||
final List<Integer> colors = paletteDisplayColors(paletteFile.metadata());
|
||||
final List<Integer> sourceColors = tileDisplayColors(tileFile.metadata());
|
||||
final Map<String, Integer> schemaIndicesByColor = schemaIndicesByColor(paletteFile.metadata());
|
||||
if (width == null || height == null || width <= 0 || height <= 0 || indices.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@ -153,7 +155,7 @@ public final class AssetDetailsPaletteOverhaulingPreviewPane extends VBox {
|
||||
final double pixelWidth = PREVIEW_SIZE / width;
|
||||
final double pixelHeight = PREVIEW_SIZE / height;
|
||||
for (int index = 0; index < indices.size(); index++) {
|
||||
final int paletteIndex = indices.get(index);
|
||||
final int paletteIndex = schemaPaletteIndex(indices.get(index), sourceColors, schemaIndicesByColor);
|
||||
final int x = index % width;
|
||||
final int y = index / width;
|
||||
graphics.setFill(toFxColor(paletteIndexToArgb(colors, paletteIndex)));
|
||||
@ -206,6 +208,35 @@ public final class AssetDetailsPaletteOverhaulingPreviewPane extends VBox {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<Integer> tileDisplayColors(Map<String, Object> metadata) {
|
||||
final Map<String, Object> palette = nestedMap(metadata, "palette");
|
||||
if (palette == null) {
|
||||
return List.of();
|
||||
}
|
||||
final Object original = palette.get("originalArgb8888");
|
||||
if (original instanceof List<?> list) {
|
||||
return (List<Integer>) list;
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private Map<String, Integer> schemaIndicesByColor(Map<String, Object> metadata) {
|
||||
final Object materialIndices = metadata.get("schemaMaterialIndices");
|
||||
if (!(materialIndices instanceof Map<?, ?> map)) {
|
||||
return Map.of();
|
||||
}
|
||||
final Map<String, Integer> indicesByColor = new java.util.LinkedHashMap<>();
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
final Integer index = asInteger(entry.getKey());
|
||||
if (index == null || index <= 0 || !(entry.getValue() instanceof String color) || color.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
indicesByColor.put(color.trim().toUpperCase(), index);
|
||||
}
|
||||
return Map.copyOf(indicesByColor);
|
||||
}
|
||||
|
||||
private Integer asInteger(Object value) {
|
||||
if (value instanceof Number number) {
|
||||
return number.intValue();
|
||||
@ -220,6 +251,17 @@ public final class AssetDetailsPaletteOverhaulingPreviewPane extends VBox {
|
||||
return colorAt(colors, paletteIndex - 1);
|
||||
}
|
||||
|
||||
private int schemaPaletteIndex(int tilePaletteIndex, List<Integer> sourceColors, Map<String, Integer> schemaIndicesByColor) {
|
||||
if (tilePaletteIndex <= 0 || sourceColors.isEmpty() || schemaIndicesByColor.isEmpty()) {
|
||||
return tilePaletteIndex;
|
||||
}
|
||||
final int sourceColorIndex = tilePaletteIndex - 1;
|
||||
if (sourceColorIndex < 0 || sourceColorIndex >= sourceColors.size()) {
|
||||
return 0;
|
||||
}
|
||||
return schemaIndicesByColor.getOrDefault(cssColor(sourceColors.get(sourceColorIndex)), 0);
|
||||
}
|
||||
|
||||
private int colorAt(List<Integer> colors, int index) {
|
||||
if (index < 0 || index >= colors.size()) {
|
||||
return 0x00000000;
|
||||
|
||||
@ -0,0 +1,281 @@
|
||||
package p.studio.workspaces.assets.details.palette;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ColorPicker;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
import p.studio.Container;
|
||||
import p.studio.utilities.i18n.I18n;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialSchema;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class AssetDetailsPaletteWizardDialog {
|
||||
private final Stage stage = new Stage();
|
||||
private final ComboBox<AssetStudioGlyphMaterialSchema> schemaComboBox = new ComboBox<>();
|
||||
private final TextField nameField = new TextField();
|
||||
private final ObservableList<String> editableColors = FXCollections.observableArrayList();
|
||||
private final ObservableList<String> indexItems = FXCollections.observableArrayList();
|
||||
private final ListView<String> indexListView = new ListView<>(indexItems);
|
||||
private final ColorPicker colorPicker = new ColorPicker(Color.WHITE);
|
||||
private final Label summaryLabel = new Label();
|
||||
private final HBox originalStrip = new HBox(4);
|
||||
private final HBox editableStrip = new HBox(4);
|
||||
|
||||
private PaletteEditResult result;
|
||||
private final String paletteId;
|
||||
private final boolean editing;
|
||||
|
||||
private AssetDetailsPaletteWizardDialog(
|
||||
Window owner,
|
||||
AssetDetailsPaletteOverhaulingCoordinator coordinator,
|
||||
AssetWorkspaceBankCompositionFile paletteFile) {
|
||||
this.editing = paletteFile != null;
|
||||
this.paletteId = editing && paletteFile.metadata().get("paletteId") instanceof String id && !id.isBlank()
|
||||
? id
|
||||
: coordinator.nextPaletteId();
|
||||
stage.initOwner(owner);
|
||||
stage.initModality(Modality.WINDOW_MODAL);
|
||||
stage.setTitle(Container.i18n().text(editing
|
||||
? I18n.ASSETS_PALETTE_WIZARD_UPDATE_TITLE
|
||||
: I18n.ASSETS_PALETTE_WIZARD_CREATE_TITLE));
|
||||
|
||||
final List<AssetStudioGlyphMaterialSchema> schemas = coordinator.readySchemas();
|
||||
schemaComboBox.getItems().setAll(schemas);
|
||||
schemaComboBox.setCellFactory(ignored -> new javafx.scene.control.ListCell<>() {
|
||||
@Override
|
||||
protected void updateItem(AssetStudioGlyphMaterialSchema item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
setText(empty || item == null ? null : item.schemaId());
|
||||
}
|
||||
});
|
||||
schemaComboBox.setButtonCell(schemaComboBox.getCellFactory().call(null));
|
||||
|
||||
final AssetStudioGlyphMaterialSchema initialSchema = resolveInitialSchema(coordinator, paletteFile, schemas);
|
||||
if (initialSchema != null) {
|
||||
schemaComboBox.getSelectionModel().select(initialSchema);
|
||||
}
|
||||
editableColors.setAll(initialEditableColors(paletteFile, initialSchema));
|
||||
nameField.setText(initialDisplayName(paletteFile));
|
||||
nameField.setPromptText(AssetDetailsPaletteOverhaulingCoordinator.generatedDisplayName(editableColors));
|
||||
schemaComboBox.valueProperty().addListener((ignored, previous, current) -> {
|
||||
if (current == null) {
|
||||
editableColors.clear();
|
||||
} else if (!editing || previous == null || !Objects.equals(previous.schemaId(), current.schemaId())) {
|
||||
editableColors.setAll(current.materialIndices().entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.map(Map.Entry::getValue)
|
||||
.toList());
|
||||
}
|
||||
refreshPreview(coordinator);
|
||||
});
|
||||
nameField.textProperty().addListener((ignored, previous, current) -> refreshPreview(coordinator));
|
||||
indexListView.getSelectionModel().selectedIndexProperty().addListener((ignored, previous, current) -> {
|
||||
final int selected = current == null ? -1 : current.intValue();
|
||||
if (selected >= 0 && selected < editableColors.size()) {
|
||||
colorPicker.setValue(Color.web(editableColors.get(selected)));
|
||||
}
|
||||
});
|
||||
|
||||
final Button applyColorButton = new Button(Container.i18n().text(I18n.ASSETS_PALETTE_WIZARD_APPLY_COLOR));
|
||||
applyColorButton.getStyleClass().addAll("studio-button", "studio-button-secondary");
|
||||
applyColorButton.setOnAction(ignored -> {
|
||||
final int index = indexListView.getSelectionModel().getSelectedIndex();
|
||||
if (index < 0 || index >= editableColors.size()) {
|
||||
return;
|
||||
}
|
||||
editableColors.set(index, toHtmlColor(colorPicker.getValue()));
|
||||
refreshPreview(coordinator);
|
||||
indexListView.getSelectionModel().select(index);
|
||||
});
|
||||
|
||||
final Button cancelButton = new Button(Container.i18n().text(I18n.FORM_ACTION_CANCEL));
|
||||
cancelButton.getStyleClass().addAll("studio-button", "studio-button-secondary");
|
||||
cancelButton.setOnAction(ignored -> {
|
||||
result = null;
|
||||
stage.close();
|
||||
});
|
||||
final Button applyButton = new Button(Container.i18n().text(I18n.FORM_ACTION_APPLY));
|
||||
applyButton.getStyleClass().addAll("studio-button", "studio-button-primary");
|
||||
applyButton.setOnAction(ignored -> {
|
||||
final AssetStudioGlyphMaterialSchema selectedSchema = schemaComboBox.getSelectionModel().getSelectedItem();
|
||||
if (selectedSchema == null || coordinator.duplicatePaletteExists(paletteId, selectedSchema.schemaId(), editableColors)) {
|
||||
return;
|
||||
}
|
||||
result = new PaletteEditResult(paletteId, normalizedDisplayName(), selectedSchema.schemaId(), List.copyOf(editableColors));
|
||||
stage.close();
|
||||
});
|
||||
final Region spacer = new Region();
|
||||
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||
final HBox actions = new HBox(8, spacer, cancelButton, applyButton);
|
||||
actions.getStyleClass().add("assets-details-palette-wizard-actions");
|
||||
|
||||
final Label schemaLabel = fieldLabel(I18n.ASSETS_PALETTE_WIZARD_SCHEMA);
|
||||
final Label nameLabel = fieldLabel(I18n.ASSETS_PALETTE_WIZARD_NAME);
|
||||
final Label originalLabel = fieldLabel(I18n.ASSETS_PALETTE_WIZARD_ORIGINAL);
|
||||
final Label editableLabel = fieldLabel(I18n.ASSETS_PALETTE_WIZARD_EDITABLE);
|
||||
final VBox colorEditor = new VBox(8, colorPicker, applyColorButton);
|
||||
colorEditor.getStyleClass().add("assets-details-palette-wizard-color-editor");
|
||||
final HBox editorBody = new HBox(12, indexListView, colorEditor);
|
||||
editorBody.getStyleClass().add("assets-details-palette-wizard-editor");
|
||||
final VBox root = new VBox(
|
||||
12,
|
||||
nameLabel,
|
||||
nameField,
|
||||
schemaLabel,
|
||||
schemaComboBox,
|
||||
originalLabel,
|
||||
originalStrip,
|
||||
editableLabel,
|
||||
editableStrip,
|
||||
editorBody,
|
||||
summaryLabel,
|
||||
actions);
|
||||
root.setPadding(new Insets(16));
|
||||
root.getStyleClass().add("assets-details-palette-wizard");
|
||||
originalStrip.getStyleClass().add("assets-details-palette-wizard-strip");
|
||||
editableStrip.getStyleClass().add("assets-details-palette-wizard-strip");
|
||||
summaryLabel.getStyleClass().add("assets-details-palette-wizard-summary");
|
||||
HBox.setHgrow(indexListView, Priority.ALWAYS);
|
||||
HBox.setHgrow(schemaComboBox, Priority.ALWAYS);
|
||||
HBox.setHgrow(nameField, Priority.ALWAYS);
|
||||
refreshPreview(coordinator);
|
||||
final Scene scene = new Scene(root, 620, 570);
|
||||
scene.getStylesheets().add(Container.theme().getDefaultTheme());
|
||||
stage.setScene(scene);
|
||||
stage.setOnShown(ignored -> stage.centerOnScreen());
|
||||
}
|
||||
|
||||
public static Optional<PaletteEditResult> showAndWait(
|
||||
Window owner,
|
||||
AssetDetailsPaletteOverhaulingCoordinator coordinator,
|
||||
AssetWorkspaceBankCompositionFile paletteFile) {
|
||||
final AssetDetailsPaletteWizardDialog dialog = new AssetDetailsPaletteWizardDialog(owner, coordinator, paletteFile);
|
||||
dialog.stage.showAndWait();
|
||||
return Optional.ofNullable(dialog.result);
|
||||
}
|
||||
|
||||
private Label fieldLabel(I18n label) {
|
||||
final Label node = new Label(Container.i18n().text(label));
|
||||
node.getStyleClass().add("assets-details-palette-wizard-label");
|
||||
return node;
|
||||
}
|
||||
|
||||
private AssetStudioGlyphMaterialSchema resolveInitialSchema(
|
||||
AssetDetailsPaletteOverhaulingCoordinator coordinator,
|
||||
AssetWorkspaceBankCompositionFile paletteFile,
|
||||
List<AssetStudioGlyphMaterialSchema> schemas) {
|
||||
if (paletteFile != null && paletteFile.metadata().get("schemaId") instanceof String schemaId) {
|
||||
return coordinator.readySchemas().stream()
|
||||
.filter(schema -> schema.schemaId().equals(schemaId))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
return schemas.isEmpty() ? null : schemas.getFirst();
|
||||
}
|
||||
|
||||
private List<String> initialEditableColors(
|
||||
AssetWorkspaceBankCompositionFile paletteFile,
|
||||
AssetStudioGlyphMaterialSchema initialSchema) {
|
||||
if (paletteFile != null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
final Map<String, Object> palette = (Map<String, Object>) paletteFile.metadata().get("palette");
|
||||
final Object original = palette == null ? null : palette.get("originalArgb8888");
|
||||
if (original instanceof Iterable<?> iterable) {
|
||||
final List<String> colors = new ArrayList<>();
|
||||
for (Object value : iterable) {
|
||||
if (value instanceof Number number) {
|
||||
colors.add(String.format("#%06X", number.intValue() & 0x00FFFFFF));
|
||||
}
|
||||
}
|
||||
return List.copyOf(colors);
|
||||
}
|
||||
}
|
||||
if (initialSchema == null) {
|
||||
return List.of();
|
||||
}
|
||||
return initialSchema.materialIndices().entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.map(Map.Entry::getValue)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private String initialDisplayName(AssetWorkspaceBankCompositionFile paletteFile) {
|
||||
if (paletteFile == null) {
|
||||
return "";
|
||||
}
|
||||
final Object explicitName = paletteFile.metadata().get("explicitPaletteDisplayName");
|
||||
return explicitName instanceof String value ? value : "";
|
||||
}
|
||||
|
||||
private void refreshPreview(AssetDetailsPaletteOverhaulingCoordinator coordinator) {
|
||||
final AssetStudioGlyphMaterialSchema schema = schemaComboBox.getSelectionModel().getSelectedItem();
|
||||
final List<String> originalColors = schema == null
|
||||
? List.of()
|
||||
: schema.materialIndices().entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.map(Map.Entry::getValue)
|
||||
.toList();
|
||||
originalStrip.getChildren().setAll(originalColors.stream().map(this::swatch).toList());
|
||||
editableStrip.getChildren().setAll(editableColors.stream().map(this::swatch).toList());
|
||||
nameField.setPromptText(AssetDetailsPaletteOverhaulingCoordinator.generatedDisplayName(editableColors));
|
||||
indexItems.setAll(java.util.stream.IntStream.range(0, editableColors.size())
|
||||
.mapToObj(index -> (index + 1) + " -> " + editableColors.get(index))
|
||||
.toList());
|
||||
final boolean duplicate = schema != null && coordinator.duplicatePaletteExists(paletteId, schema.schemaId(), editableColors);
|
||||
summaryLabel.setText(duplicate
|
||||
? Container.i18n().text(I18n.ASSETS_PALETTE_WIZARD_DUPLICATE)
|
||||
: Container.i18n().text(I18n.ASSETS_PALETTE_WIZARD_READY));
|
||||
}
|
||||
|
||||
private String normalizedDisplayName() {
|
||||
final String value = nameField.getText();
|
||||
return value == null || value.trim().isBlank()
|
||||
? AssetDetailsPaletteOverhaulingCoordinator.generatedDisplayName(editableColors)
|
||||
: value.trim();
|
||||
}
|
||||
|
||||
private Region swatch(String color) {
|
||||
final Region swatch = new Region();
|
||||
swatch.setMinSize(22, 22);
|
||||
swatch.setPrefSize(22, 22);
|
||||
swatch.setMaxSize(22, 22);
|
||||
swatch.getStyleClass().add("assets-details-palette-wizard-swatch");
|
||||
swatch.setStyle("-fx-background-color: " + color + ";");
|
||||
return swatch;
|
||||
}
|
||||
|
||||
private String toHtmlColor(Color color) {
|
||||
return String.format(
|
||||
"#%02X%02X%02X",
|
||||
(int) Math.round(color.getRed() * 255),
|
||||
(int) Math.round(color.getGreen() * 255),
|
||||
(int) Math.round(color.getBlue() * 255));
|
||||
}
|
||||
|
||||
public record PaletteEditResult(
|
||||
String paletteId,
|
||||
String displayName,
|
||||
String schemaId,
|
||||
List<String> colors) {
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,545 @@
|
||||
package p.studio.workspaces.assets.details.scene;
|
||||
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import p.studio.Container;
|
||||
import p.studio.controls.forms.StudioFormMode;
|
||||
import p.studio.controls.lifecycle.StudioControlLifecycle;
|
||||
import p.studio.controls.lifecycle.StudioControlLifecycleSupport;
|
||||
import p.studio.lsp.events.StudioWorkspaceEventBus;
|
||||
import p.studio.projects.ProjectReference;
|
||||
import p.studio.utilities.i18n.I18n;
|
||||
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.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;
|
||||
import p.packer.messages.ListAssetsRequest;
|
||||
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.IntConsumer;
|
||||
|
||||
public final class AssetDetailsSceneLayersControl extends VBox implements StudioControlLifecycle {
|
||||
private final ProjectReference projectReference;
|
||||
private final StudioWorkspaceEventBus workspaceBus;
|
||||
private final StudioSubscriptionBag subscriptions = new StudioSubscriptionBag();
|
||||
private final AssetStudioMetadataService metadataService = new AssetStudioMetadataService();
|
||||
|
||||
private AssetWorkspaceDetailsViewState viewState;
|
||||
private IntConsumer generateLayerAction = ignored -> {
|
||||
};
|
||||
private IntConsumer validateLayerAction = ignored -> {
|
||||
};
|
||||
private IntConsumer acceptLayerAction = ignored -> {
|
||||
};
|
||||
private Integer editingLayerIndex;
|
||||
private LayerDraft editingDraft;
|
||||
private int selectedLayerIndex = 1;
|
||||
|
||||
public AssetDetailsSceneLayersControl(ProjectReference projectReference, StudioWorkspaceEventBus workspaceBus) {
|
||||
StudioControlLifecycleSupport.install(this, this);
|
||||
this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
|
||||
this.workspaceBus = Objects.requireNonNull(workspaceBus, "workspaceBus");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribe() {
|
||||
subscriptions.add(workspaceBus.subscribe(StudioAssetsDetailsViewStateChangedEvent.class, event -> {
|
||||
viewState = event.viewState();
|
||||
render();
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unsubscribe() {
|
||||
subscriptions.clear();
|
||||
}
|
||||
|
||||
public void setGenerateLayerAction(IntConsumer generateLayerAction) {
|
||||
this.generateLayerAction = Objects.requireNonNull(generateLayerAction, "generateLayerAction");
|
||||
}
|
||||
|
||||
public void setValidateLayerAction(IntConsumer validateLayerAction) {
|
||||
this.validateLayerAction = Objects.requireNonNull(validateLayerAction, "validateLayerAction");
|
||||
}
|
||||
|
||||
public void setAcceptLayerAction(IntConsumer acceptLayerAction) {
|
||||
this.acceptLayerAction = Objects.requireNonNull(acceptLayerAction, "acceptLayerAction");
|
||||
}
|
||||
|
||||
private void render() {
|
||||
final AssetStudioSceneBankMetadata metadata = viewState == null || viewState.selectedAssetDetails() == null
|
||||
? null
|
||||
: viewState.selectedAssetDetails().sceneBankMetadata();
|
||||
if (metadata == null || viewState.selectedAssetDetails() == null) {
|
||||
getChildren().clear();
|
||||
return;
|
||||
}
|
||||
|
||||
final VBox content = new VBox(10);
|
||||
content.getChildren().add(createLayerSelector(
|
||||
viewState.selectedAssetDetails().summary().assetRoot(),
|
||||
metadata));
|
||||
content.getStyleClass().add("assets-scene-layer-shell");
|
||||
|
||||
getChildren().setAll(AssetDetailsUiSupport.createSectionWithoutHeader(content));
|
||||
}
|
||||
|
||||
private Node createLayerSelector(Path sceneAssetRoot, AssetStudioSceneBankMetadata metadata) {
|
||||
if (selectedLayerIndex < 1 || selectedLayerIndex > metadata.layerCount()) {
|
||||
selectedLayerIndex = 1;
|
||||
}
|
||||
final HBox selector = new HBox(8);
|
||||
selector.getStyleClass().add("assets-scene-layer-selector");
|
||||
for (int index = 1; index <= 4; index += 1) {
|
||||
final int layerIndex = index;
|
||||
final Button button = new Button(Container.i18n().text(I18n.ASSETS_LABEL_LAYER) + " " + layerIndex);
|
||||
button.getStyleClass().addAll("studio-button", "assets-scene-layer-selector-button");
|
||||
if (layerIndex == selectedLayerIndex) {
|
||||
button.getStyleClass().add("assets-scene-layer-selector-button-active");
|
||||
}
|
||||
button.setOnAction(ignored -> {
|
||||
selectedLayerIndex = layerIndex;
|
||||
render();
|
||||
});
|
||||
selector.getChildren().add(button);
|
||||
}
|
||||
|
||||
final AssetStudioSceneLayerBinding binding = metadata.layerBindings().stream()
|
||||
.filter(candidate -> candidate.index() == selectedLayerIndex)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
final VBox container = new VBox(10);
|
||||
container.getChildren().addAll(selector, createLayerCard(sceneAssetRoot, selectedLayerIndex, binding));
|
||||
return container;
|
||||
}
|
||||
|
||||
private Node createLayerCard(Path sceneAssetRoot, int layerIndex, AssetStudioSceneLayerBinding binding) {
|
||||
final boolean editing = Objects.equals(editingLayerIndex, layerIndex) && binding != null && editingDraft != null;
|
||||
final LayerDraft draft = editing ? editingDraft : LayerDraft.from(binding);
|
||||
final VBox formContent = buildLayerFormContent(sceneAssetRoot, binding, draft, editing);
|
||||
final VBox formSection = AssetDetailsUiSupport.createEditableSection(
|
||||
Container.i18n().text(I18n.ASSETS_SECTION_LAYER_CONFIGURATION),
|
||||
createLayerFormHeaderActions(layerIndex, binding, editing),
|
||||
formContent,
|
||||
editing ? StudioFormMode.EDITING : StudioFormMode.READ_ONLY);
|
||||
formSection.getStyleClass().add("assets-scene-layer-config-section");
|
||||
|
||||
final VBox leftColumn = new VBox(10);
|
||||
leftColumn.getStyleClass().add("assets-scene-layer-config-column");
|
||||
leftColumn.getChildren().addAll(
|
||||
formSection,
|
||||
createLayerActions(sceneAssetRoot, layerIndex, draft));
|
||||
|
||||
final VBox overview = createLayerOverviewPlaceholder();
|
||||
final HBox layout = new HBox(12, leftColumn, overview);
|
||||
layout.getStyleClass().add("assets-scene-layer-layout");
|
||||
HBox.setHgrow(leftColumn, Priority.ALWAYS);
|
||||
return layout;
|
||||
}
|
||||
|
||||
private VBox buildLayerFormContent(Path sceneAssetRoot, AssetStudioSceneLayerBinding binding, LayerDraft draft, boolean editing) {
|
||||
final VBox card = new VBox(8);
|
||||
card.getStyleClass().addAll("assets-scene-layer-card", "assets-scene-layer-config");
|
||||
|
||||
final CheckBox enabledCheckBox = new CheckBox(Container.i18n().text(I18n.ASSETS_LABEL_USE_LAYER));
|
||||
enabledCheckBox.setSelected(draft != null && draft.enabled());
|
||||
enabledCheckBox.getStyleClass().add("assets-scene-layer-enabled");
|
||||
enabledCheckBox.setDisable(!editing);
|
||||
enabledCheckBox.setOnAction(ignored -> {
|
||||
if (draft != null) {
|
||||
draft.setEnabled(enabledCheckBox.isSelected());
|
||||
}
|
||||
});
|
||||
card.getChildren().add(enabledCheckBox);
|
||||
|
||||
card.getChildren().addAll(
|
||||
AssetDetailsUiSupport.createKeyValueRow(
|
||||
Container.i18n().text(I18n.ASSETS_LABEL_SCENE_DIMENSIONS),
|
||||
editing ? createLayerSizeEditor(draft) : new Label(draft.widthText() + " x " + draft.heightText())),
|
||||
AssetDetailsUiSupport.createKeyValueRow(
|
||||
Container.i18n().text(I18n.ASSETS_LABEL_TILEMAP),
|
||||
binding == null
|
||||
? Container.i18n().text(I18n.ASSETS_VALUE_NOT_CONFIGURED)
|
||||
: AssetDetailsUiSupport.projectRelativePath(projectReference, sceneAssetRoot.resolve(binding.tilemap()))),
|
||||
AssetDetailsUiSupport.createKeyValueRow(
|
||||
Container.i18n().text(I18n.ASSETS_LABEL_TILESET),
|
||||
editing ? createTilesetCombo(draft, true) : createTilesetReadOnlyValue(binding, draft)));
|
||||
return card;
|
||||
}
|
||||
|
||||
private VBox createLayerOverviewPlaceholder() {
|
||||
final VBox overview = new VBox(10);
|
||||
overview.getStyleClass().add("assets-scene-layer-overview");
|
||||
|
||||
final Label title = new Label(Container.i18n().text(I18n.ASSETS_LABEL_LAYER_OVERVIEW));
|
||||
title.getStyleClass().add("assets-scene-layer-overview-title");
|
||||
|
||||
final Region surface = new Region();
|
||||
surface.getStyleClass().add("assets-scene-layer-overview-surface");
|
||||
VBox.setVgrow(surface, Priority.ALWAYS);
|
||||
|
||||
overview.getChildren().addAll(
|
||||
title,
|
||||
surface,
|
||||
AssetDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_VALUE_LAYER_OVERVIEW_PENDING)));
|
||||
return overview;
|
||||
}
|
||||
|
||||
private Node createLayerFormHeaderActions(int layerIndex, AssetStudioSceneLayerBinding binding, boolean editing) {
|
||||
final HBox actions = new HBox(8);
|
||||
actions.getStyleClass().add("assets-scene-layer-form-actions");
|
||||
if (!editing) {
|
||||
final Button changeButton = createFormActionButton(I18n.FORM_ACTION_CHANGE);
|
||||
changeButton.setDisable(binding == null);
|
||||
changeButton.setOnAction(ignored -> beginEdit(layerIndex, binding));
|
||||
actions.getChildren().add(changeButton);
|
||||
return actions;
|
||||
}
|
||||
|
||||
final Button applyButton = createFormActionButton(I18n.FORM_ACTION_APPLY);
|
||||
applyButton.setOnAction(ignored -> applyEdit(layerIndex, binding));
|
||||
final Button resetButton = createFormActionButton(I18n.FORM_ACTION_RESET);
|
||||
resetButton.setOnAction(ignored -> resetEdit(binding));
|
||||
final Button cancelButton = createFormActionButton(I18n.FORM_ACTION_CANCEL);
|
||||
cancelButton.setOnAction(ignored -> cancelEdit());
|
||||
actions.getChildren().addAll(applyButton, resetButton, cancelButton);
|
||||
return actions;
|
||||
}
|
||||
|
||||
private Node createLayerActions(Path sceneAssetRoot, int layerIndex, LayerDraft draft) {
|
||||
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);
|
||||
return actions;
|
||||
}
|
||||
|
||||
private Node createLayerSizeEditor(LayerDraft draft) {
|
||||
final HBox editor = new HBox(8);
|
||||
editor.getStyleClass().add("assets-scene-layer-size-editor");
|
||||
|
||||
final TextField widthField = createSizeField(draft.widthText(), draft::setWidthText);
|
||||
final Label separator = new Label("x");
|
||||
separator.getStyleClass().add("assets-scene-layer-size-separator");
|
||||
final TextField heightField = createSizeField(draft.heightText(), draft::setHeightText);
|
||||
|
||||
editor.getChildren().addAll(widthField, separator, heightField);
|
||||
return editor;
|
||||
}
|
||||
|
||||
private TextField createSizeField(String value, java.util.function.Consumer<String> updater) {
|
||||
final TextField textField = new TextField(value);
|
||||
textField.getStyleClass().add("assets-scene-layer-size-field");
|
||||
textField.textProperty().addListener((ignored, oldValue, newValue) -> updater.accept(newValue));
|
||||
return textField;
|
||||
}
|
||||
|
||||
private ComboBox<TilesetOption> createTilesetCombo(LayerDraft draft, boolean editing) {
|
||||
final ComboBox<TilesetOption> combo = new ComboBox<>();
|
||||
combo.setMaxWidth(Double.MAX_VALUE);
|
||||
combo.getStyleClass().add("assets-details-combo");
|
||||
final List<TilesetOption> options = loadTilesetOptions();
|
||||
combo.setItems(FXCollections.observableArrayList(options));
|
||||
combo.setCellFactory(ignored -> new ListCell<>() {
|
||||
@Override
|
||||
protected void updateItem(TilesetOption item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
setText(empty || item == null ? null : item.label());
|
||||
}
|
||||
});
|
||||
combo.setButtonCell(new ListCell<>() {
|
||||
@Override
|
||||
protected void updateItem(TilesetOption item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
setText(empty || item == null ? null : item.label());
|
||||
}
|
||||
});
|
||||
final String currentTileset = draft == null ? "" : draft.tilesetAssetRoot();
|
||||
options.stream()
|
||||
.filter(option -> option.relativeAssetRoot().equals(currentTileset))
|
||||
.findFirst()
|
||||
.ifPresentOrElse(
|
||||
combo.getSelectionModel()::select,
|
||||
() -> combo.getSelectionModel().clearSelection());
|
||||
combo.setDisable(!editing || draft == null);
|
||||
if (draft == null) {
|
||||
combo.setDisable(true);
|
||||
return combo;
|
||||
}
|
||||
combo.valueProperty().addListener((ignored, oldValue, newValue) -> {
|
||||
if (Objects.equals(oldValue, newValue)) {
|
||||
return;
|
||||
}
|
||||
draft.setTilesetAssetRoot(newValue == null ? "" : newValue.relativeAssetRoot());
|
||||
});
|
||||
return combo;
|
||||
}
|
||||
|
||||
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)
|
||||
: tilesetPath(draft.tilesetAssetRoot());
|
||||
return new Label(value);
|
||||
}
|
||||
|
||||
private List<TilesetOption> loadTilesetOptions() {
|
||||
try {
|
||||
final var response = Container.packer().workspaceService().listAssets(new ListAssetsRequest(projectReference.toPackerProjectContext()));
|
||||
return response.assets().stream()
|
||||
.filter(asset -> asset.assetFamily() == AssetFamilyCatalog.GLYPH_BANK)
|
||||
.filter(asset -> metadataService.read(asset.identity().assetRoot(), AssetFamilyCatalog.GLYPH_BANK).glyphSpecialization()
|
||||
== p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization.TILESET)
|
||||
.map(asset -> {
|
||||
final String relativeAssetRoot = projectReference.rootPath()
|
||||
.resolve("assets")
|
||||
.relativize(asset.identity().assetRoot())
|
||||
.toString()
|
||||
.replace('\\', '/');
|
||||
return new TilesetOption(asset.identity().assetName(), relativeAssetRoot);
|
||||
})
|
||||
.sorted(Comparator.comparing(TilesetOption::label, String.CASE_INSENSITIVE_ORDER))
|
||||
.toList();
|
||||
} catch (RuntimeException exception) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateLayerBinding(
|
||||
int layerIndex,
|
||||
java.util.function.Function<AssetStudioSceneLayerBinding, AssetStudioSceneLayerBinding> mutator,
|
||||
String successMessage) {
|
||||
if (viewState == null || viewState.selectedAssetDetails() == null || viewState.selectedAssetDetails().sceneBankMetadata() == null) {
|
||||
return;
|
||||
}
|
||||
final AssetStudioSceneBankMetadata metadata = viewState.selectedAssetDetails().sceneBankMetadata();
|
||||
final java.util.List<AssetStudioSceneLayerBinding> updatedBindings = new ArrayList<>();
|
||||
for (AssetStudioSceneLayerBinding binding : metadata.layerBindings()) {
|
||||
if (binding.index() == layerIndex) {
|
||||
updatedBindings.add(mutator.apply(binding));
|
||||
} else {
|
||||
updatedBindings.add(binding);
|
||||
}
|
||||
}
|
||||
final AssetStudioSceneLayerBinding firstBinding = updatedBindings.getFirst();
|
||||
final AssetStudioSceneBankMetadata updatedMetadata = new AssetStudioSceneBankMetadata(
|
||||
firstBinding.width(),
|
||||
firstBinding.height(),
|
||||
metadata.layerCount(),
|
||||
updatedBindings,
|
||||
metadata.supportFile());
|
||||
Container.backgroundTasks().submit(() -> {
|
||||
try {
|
||||
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) {
|
||||
Platform.runLater(() -> workspaceBus.publish(
|
||||
new StudioAssetLogEvent("scene-bank-layer", "Unable to update layer " + layerIndex + ": " + exception.getMessage())));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String tilesetPath(AssetStudioSceneLayerBinding binding) {
|
||||
return tilesetPath(binding.tilesetAssetRoot());
|
||||
}
|
||||
|
||||
private String tilesetPath(String tilesetAssetRoot) {
|
||||
if (tilesetAssetRoot.isBlank()) {
|
||||
return "—";
|
||||
}
|
||||
return AssetDetailsUiSupport.projectRelativePath(
|
||||
projectReference,
|
||||
projectReference.rootPath().resolve("assets").resolve(tilesetAssetRoot));
|
||||
}
|
||||
|
||||
private void beginEdit(int layerIndex, AssetStudioSceneLayerBinding binding) {
|
||||
editingLayerIndex = layerIndex;
|
||||
editingDraft = LayerDraft.from(binding);
|
||||
render();
|
||||
}
|
||||
|
||||
private void applyEdit(int layerIndex, AssetStudioSceneLayerBinding binding) {
|
||||
if (binding == null || editingDraft == null || !editingDraft.hasValidDimensions()) {
|
||||
workspaceBus.publish(new StudioAssetLogEvent(
|
||||
"scene-bank-layer",
|
||||
"Layer size must use positive integer values before applying changes."));
|
||||
return;
|
||||
}
|
||||
final LayerDraft draft = editingDraft.copy();
|
||||
editingLayerIndex = null;
|
||||
editingDraft = null;
|
||||
render();
|
||||
updateLayerBinding(layerIndex, current -> new AssetStudioSceneLayerBinding(
|
||||
draft.enabled(),
|
||||
current.index(),
|
||||
current.layerName(),
|
||||
draft.widthValue(),
|
||||
draft.heightValue(),
|
||||
current.tilemap(),
|
||||
draft.tilesetAssetRoot()),
|
||||
"Updated layer " + layerIndex + " configuration.");
|
||||
}
|
||||
|
||||
private void resetEdit(AssetStudioSceneLayerBinding binding) {
|
||||
editingDraft = LayerDraft.from(binding);
|
||||
render();
|
||||
}
|
||||
|
||||
private void cancelEdit() {
|
||||
editingLayerIndex = null;
|
||||
editingDraft = null;
|
||||
render();
|
||||
}
|
||||
|
||||
private Button createFormActionButton(I18n textKey) {
|
||||
final Button button = new Button(Container.i18n().text(textKey));
|
||||
button.getStyleClass().addAll("studio-button", "studio-button-secondary");
|
||||
return button;
|
||||
}
|
||||
|
||||
private boolean hasLayerTilemap(Path sceneAssetRoot, int layerIndex) {
|
||||
if (viewState == null || viewState.selectedAssetDetails() == null || viewState.selectedAssetDetails().sceneBankMetadata() == null) {
|
||||
return false;
|
||||
}
|
||||
return viewState.selectedAssetDetails().sceneBankMetadata().layerBindings().stream()
|
||||
.filter(binding -> binding.index() == layerIndex)
|
||||
.findFirst()
|
||||
.map(binding -> Files.isRegularFile(sceneAssetRoot.resolve(binding.tilemap())))
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
private record TilesetOption(String assetName, String relativeAssetRoot) {
|
||||
String label() {
|
||||
return assetName + " [" + relativeAssetRoot + "]";
|
||||
}
|
||||
}
|
||||
|
||||
private static final class LayerDraft {
|
||||
private boolean enabled;
|
||||
private String widthText;
|
||||
private String heightText;
|
||||
private String tilesetAssetRoot;
|
||||
|
||||
private LayerDraft(boolean enabled, String widthText, String heightText, String tilesetAssetRoot) {
|
||||
this.enabled = enabled;
|
||||
this.widthText = widthText;
|
||||
this.heightText = heightText;
|
||||
this.tilesetAssetRoot = Objects.requireNonNullElse(tilesetAssetRoot, "").trim();
|
||||
}
|
||||
|
||||
static LayerDraft from(AssetStudioSceneLayerBinding binding) {
|
||||
if (binding == null) {
|
||||
return new LayerDraft(false, "16", "16", "");
|
||||
}
|
||||
return new LayerDraft(
|
||||
binding.enabled(),
|
||||
Integer.toString(binding.width()),
|
||||
Integer.toString(binding.height()),
|
||||
binding.tilesetAssetRoot());
|
||||
}
|
||||
|
||||
LayerDraft copy() {
|
||||
return new LayerDraft(enabled, widthText, heightText, tilesetAssetRoot);
|
||||
}
|
||||
|
||||
boolean enabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
String widthText() {
|
||||
return widthText;
|
||||
}
|
||||
|
||||
void setWidthText(String widthText) {
|
||||
this.widthText = Objects.requireNonNullElse(widthText, "").trim();
|
||||
}
|
||||
|
||||
String heightText() {
|
||||
return heightText;
|
||||
}
|
||||
|
||||
void setHeightText(String heightText) {
|
||||
this.heightText = Objects.requireNonNullElse(heightText, "").trim();
|
||||
}
|
||||
|
||||
String tilesetAssetRoot() {
|
||||
return tilesetAssetRoot;
|
||||
}
|
||||
|
||||
void setTilesetAssetRoot(String tilesetAssetRoot) {
|
||||
this.tilesetAssetRoot = Objects.requireNonNullElse(tilesetAssetRoot, "").trim();
|
||||
}
|
||||
|
||||
boolean hasValidDimensions() {
|
||||
return parsePositiveInt(widthText) > 0 && parsePositiveInt(heightText) > 0;
|
||||
}
|
||||
|
||||
int widthValue() {
|
||||
return parsePositiveInt(widthText);
|
||||
}
|
||||
|
||||
int heightValue() {
|
||||
return parsePositiveInt(heightText);
|
||||
}
|
||||
|
||||
private static int parsePositiveInt(String value) {
|
||||
try {
|
||||
return Integer.parseInt(Objects.requireNonNullElse(value, "").trim());
|
||||
} catch (NumberFormatException exception) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,250 @@
|
||||
package p.studio.workspaces.assets.details.schema;
|
||||
|
||||
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialArtifactBinding;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialMetadata;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialPalette;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialSchema;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialSchemaStatus;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public final class AssetDetailsColorSchemaCoordinator {
|
||||
private AssetWorkspaceAssetDetails details;
|
||||
private AssetStudioGlyphMaterialMetadata localMaterialMetadata;
|
||||
|
||||
public void replaceDetails(AssetWorkspaceAssetDetails details) {
|
||||
this.details = details;
|
||||
this.localMaterialMetadata = details == null ? null : details.glyphMaterialMetadata();
|
||||
}
|
||||
|
||||
public void replaceMaterialMetadata(AssetStudioGlyphMaterialMetadata metadata) {
|
||||
this.localMaterialMetadata = Objects.requireNonNull(metadata, "metadata");
|
||||
}
|
||||
|
||||
public boolean ready() {
|
||||
return details != null && details.summary().assetFamily() == AssetFamilyCatalog.GLYPH_BANK;
|
||||
}
|
||||
|
||||
public AssetStudioGlyphMaterialMetadata metadata() {
|
||||
if (!ready() || localMaterialMetadata == null) {
|
||||
return new AssetStudioGlyphMaterialMetadata(List.of(), List.of(), null);
|
||||
}
|
||||
return localMaterialMetadata;
|
||||
}
|
||||
|
||||
public List<AssetStudioGlyphMaterialSchema> schemas() {
|
||||
return metadata().schemas();
|
||||
}
|
||||
|
||||
public List<AssetWorkspaceBankCompositionFile> selectedTiles() {
|
||||
if (!ready()) {
|
||||
return List.of();
|
||||
}
|
||||
return details.bankComposition().selectedFiles().stream()
|
||||
.filter(AssetDetailsColorSchemaCoordinator::supportsSchemaAuthoring)
|
||||
.filter(file -> artifactId(file) != null)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<AssetWorkspaceBankCompositionFile> availableTilesForCreate() {
|
||||
final Set<String> reservedArtifactIds = new HashSet<>();
|
||||
for (AssetStudioGlyphMaterialSchema schema : schemas()) {
|
||||
schema.artifactBindings().forEach(binding -> reservedArtifactIds.add(binding.artifactId()));
|
||||
}
|
||||
return selectedTiles().stream()
|
||||
.filter(file -> !reservedArtifactIds.contains(artifactId(file)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<AssetWorkspaceBankCompositionFile> availableTilesForEdit(String schemaId) {
|
||||
final AssetStudioGlyphMaterialSchema schema = findSchema(schemaId).orElse(null);
|
||||
if (schema == null) {
|
||||
return availableTilesForCreate();
|
||||
}
|
||||
final Set<String> current = schema.artifactBindings().stream()
|
||||
.map(AssetStudioGlyphMaterialArtifactBinding::artifactId)
|
||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
||||
final Set<String> reservedElsewhere = new HashSet<>();
|
||||
for (AssetStudioGlyphMaterialSchema candidate : schemas()) {
|
||||
if (candidate.schemaId().equals(schemaId)) {
|
||||
continue;
|
||||
}
|
||||
candidate.artifactBindings().forEach(binding -> reservedElsewhere.add(binding.artifactId()));
|
||||
}
|
||||
return selectedTiles().stream()
|
||||
.filter(file -> current.contains(artifactId(file)) || !reservedElsewhere.contains(artifactId(file)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public boolean canRemove(String schemaId) {
|
||||
return metadata().palettes().stream().noneMatch(palette -> palette.schemaId().equals(schemaId));
|
||||
}
|
||||
|
||||
public Optional<AssetStudioGlyphMaterialSchema> findSchema(String schemaId) {
|
||||
return schemas().stream().filter(schema -> schema.schemaId().equals(schemaId)).findFirst();
|
||||
}
|
||||
|
||||
public AssetStudioGlyphMaterialMetadata upsertSchema(
|
||||
String schemaId,
|
||||
List<AssetWorkspaceBankCompositionFile> assignedTiles,
|
||||
List<String> orderedColors) {
|
||||
final AssetStudioGlyphMaterialMetadata current = metadata();
|
||||
final AssetStudioGlyphMaterialSchema existing = current.findSchema(schemaId).orElse(null);
|
||||
final AssetStudioGlyphMaterialSchema next = buildSchema(schemaId, existing, assignedTiles, orderedColors);
|
||||
final List<AssetStudioGlyphMaterialSchema> schemas = new ArrayList<>();
|
||||
boolean replaced = false;
|
||||
for (AssetStudioGlyphMaterialSchema schema : current.schemas()) {
|
||||
if (schema.schemaId().equals(schemaId)) {
|
||||
schemas.add(next);
|
||||
replaced = true;
|
||||
} else {
|
||||
schemas.add(schema);
|
||||
}
|
||||
}
|
||||
if (!replaced) {
|
||||
schemas.add(next);
|
||||
}
|
||||
schemas.sort(Comparator.comparing(AssetStudioGlyphMaterialSchema::schemaId, String.CASE_INSENSITIVE_ORDER));
|
||||
return new AssetStudioGlyphMaterialMetadata(schemas, current.palettes(), current.supportFile());
|
||||
}
|
||||
|
||||
public AssetStudioGlyphMaterialMetadata removeSchema(String schemaId) {
|
||||
if (!canRemove(schemaId)) {
|
||||
throw new IllegalStateException("Schema is still referenced by one or more palettes.");
|
||||
}
|
||||
final AssetStudioGlyphMaterialMetadata current = metadata();
|
||||
final List<AssetStudioGlyphMaterialSchema> schemas = current.schemas().stream()
|
||||
.filter(schema -> !schema.schemaId().equals(schemaId))
|
||||
.toList();
|
||||
return new AssetStudioGlyphMaterialMetadata(schemas, current.palettes(), current.supportFile());
|
||||
}
|
||||
|
||||
public String nextSchemaId() {
|
||||
int next = 1;
|
||||
final Set<String> ids = schemas().stream().map(AssetStudioGlyphMaterialSchema::schemaId).collect(java.util.stream.Collectors.toSet());
|
||||
while (ids.contains("schema-" + next)) {
|
||||
next += 1;
|
||||
}
|
||||
return "schema-" + next;
|
||||
}
|
||||
|
||||
public int unassignedTileCount() {
|
||||
return availableTilesForCreate().size();
|
||||
}
|
||||
|
||||
public static AssetStudioGlyphMaterialSchemaStatus effectiveStatus(
|
||||
AssetStudioGlyphMaterialSchema schema,
|
||||
List<AssetWorkspaceBankCompositionFile> selectedTiles) {
|
||||
if (schema.status() == AssetStudioGlyphMaterialSchemaStatus.INCONSISTENT) {
|
||||
return AssetStudioGlyphMaterialSchemaStatus.INCONSISTENT;
|
||||
}
|
||||
final Map<String, AssetWorkspaceBankCompositionFile> selectedByArtifactId = new HashMap<>();
|
||||
for (AssetWorkspaceBankCompositionFile tile : selectedTiles) {
|
||||
final String artifactId = artifactId(tile);
|
||||
if (artifactId != null) {
|
||||
selectedByArtifactId.put(artifactId, tile);
|
||||
}
|
||||
}
|
||||
for (AssetStudioGlyphMaterialArtifactBinding binding : schema.artifactBindings()) {
|
||||
final AssetWorkspaceBankCompositionFile selectedTile = selectedByArtifactId.get(binding.artifactId());
|
||||
if (selectedTile == null || !Objects.equals(binding.fingerprint(), fingerprint(selectedTile))) {
|
||||
return AssetStudioGlyphMaterialSchemaStatus.INCONSISTENT;
|
||||
}
|
||||
}
|
||||
return AssetStudioGlyphMaterialSchemaStatus.OK;
|
||||
}
|
||||
|
||||
private AssetStudioGlyphMaterialSchema buildSchema(
|
||||
String schemaId,
|
||||
AssetStudioGlyphMaterialSchema existing,
|
||||
List<AssetWorkspaceBankCompositionFile> assignedTiles,
|
||||
List<String> orderedColors) {
|
||||
final Map<Integer, String> materialIndices = new LinkedHashMap<>();
|
||||
for (int index = 0; index < orderedColors.size(); index += 1) {
|
||||
materialIndices.put(index + 1, normalizeHtmlColor(orderedColors.get(index)));
|
||||
}
|
||||
final List<AssetStudioGlyphMaterialArtifactBinding> bindings = assignedTiles.stream()
|
||||
.map(file -> new AssetStudioGlyphMaterialArtifactBinding(artifactId(file), fingerprint(file)))
|
||||
.toList();
|
||||
final boolean unchanged = existing != null
|
||||
&& existing.materialIndices().equals(materialIndices)
|
||||
&& existing.artifactBindings().equals(bindings);
|
||||
final int version = existing == null
|
||||
? 1
|
||||
: unchanged ? existing.version() : existing.version() + 1;
|
||||
final AssetStudioGlyphMaterialSchemaStatus status = existing == null || !unchanged
|
||||
? AssetStudioGlyphMaterialSchemaStatus.OK
|
||||
: existing.status();
|
||||
return new AssetStudioGlyphMaterialSchema(schemaId, status, version, materialIndices, bindings);
|
||||
}
|
||||
|
||||
static boolean supportsSchemaAuthoring(AssetWorkspaceBankCompositionFile file) {
|
||||
return file != null
|
||||
&& artifactId(file) != null
|
||||
&& fingerprint(file) != null;
|
||||
}
|
||||
|
||||
static String artifactId(AssetWorkspaceBankCompositionFile file) {
|
||||
if (file == null) {
|
||||
return null;
|
||||
}
|
||||
final Object artifactId = file.metadata().get("artifactId");
|
||||
if (artifactId instanceof String value && !value.isBlank()) {
|
||||
return value.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static String fingerprint(AssetWorkspaceBankCompositionFile file) {
|
||||
if (file == null) {
|
||||
return null;
|
||||
}
|
||||
if (file.fingerprint() != null && !file.fingerprint().isBlank()) {
|
||||
return file.fingerprint().trim();
|
||||
}
|
||||
final Object selectedFingerprint = file.metadata().get("selectedFingerprint");
|
||||
if (selectedFingerprint instanceof String value && !value.isBlank()) {
|
||||
return value.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<String> tileColors(AssetWorkspaceBankCompositionFile file) {
|
||||
final Map<String, Object> palette = nestedMap(file.metadata(), "palette");
|
||||
if (palette == null) {
|
||||
return List.of();
|
||||
}
|
||||
final Object originalColors = palette.get("originalArgb8888");
|
||||
if (!(originalColors instanceof Iterable<?> iterable)) {
|
||||
return List.of();
|
||||
}
|
||||
final List<String> colors = new ArrayList<>();
|
||||
for (Object item : iterable) {
|
||||
if (!(item instanceof Number number)) {
|
||||
continue;
|
||||
}
|
||||
colors.add(String.format(
|
||||
"#%02X%02X%02X",
|
||||
(number.intValue() >> 16) & 0xFF,
|
||||
(number.intValue() >> 8) & 0xFF,
|
||||
number.intValue() & 0xFF));
|
||||
}
|
||||
return List.copyOf(colors);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Map<String, Object> nestedMap(Map<String, Object> metadata, String key) {
|
||||
final Object value = metadata.get(key);
|
||||
if (value instanceof Map<?, ?> nested) {
|
||||
return (Map<String, Object>) nested;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String normalizeHtmlColor(String color) {
|
||||
return Objects.requireNonNull(color, "color").trim().toUpperCase();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,129 @@
|
||||
package p.studio.workspaces.assets.details.schema;
|
||||
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
|
||||
record AssetDetailsColorSchemaDraft(
|
||||
String schemaId,
|
||||
boolean existingSchema,
|
||||
boolean removeRequested,
|
||||
List<AssetWorkspaceBankCompositionFile> availableTiles,
|
||||
List<AssetWorkspaceBankCompositionFile> assignedTiles,
|
||||
List<String> preferredColors,
|
||||
boolean deriveColorsFromTiles) {
|
||||
|
||||
AssetDetailsColorSchemaDraft {
|
||||
availableTiles = List.copyOf(availableTiles);
|
||||
assignedTiles = List.copyOf(assignedTiles);
|
||||
preferredColors = normalize(preferredColors);
|
||||
}
|
||||
|
||||
static AssetDetailsColorSchemaDraft noSelection() {
|
||||
return new AssetDetailsColorSchemaDraft(null, false, false, List.of(), List.of(), List.of(), false);
|
||||
}
|
||||
|
||||
boolean selected() {
|
||||
return schemaId != null && !schemaId.isBlank();
|
||||
}
|
||||
|
||||
AssetDetailsColorSchemaDraft moveToAssigned(List<AssetWorkspaceBankCompositionFile> files) {
|
||||
final List<AssetWorkspaceBankCompositionFile> nextAvailable = new ArrayList<>(availableTiles);
|
||||
final List<AssetWorkspaceBankCompositionFile> nextAssigned = new ArrayList<>(assignedTiles);
|
||||
for (AssetWorkspaceBankCompositionFile file : files) {
|
||||
if (nextAvailable.remove(file) && !nextAssigned.contains(file)) {
|
||||
nextAssigned.add(file);
|
||||
}
|
||||
}
|
||||
return withTiles(nextAvailable, nextAssigned);
|
||||
}
|
||||
|
||||
AssetDetailsColorSchemaDraft moveToAvailable(List<AssetWorkspaceBankCompositionFile> files) {
|
||||
final List<AssetWorkspaceBankCompositionFile> nextAvailable = new ArrayList<>(availableTiles);
|
||||
final List<AssetWorkspaceBankCompositionFile> nextAssigned = new ArrayList<>(assignedTiles);
|
||||
for (AssetWorkspaceBankCompositionFile file : files) {
|
||||
if (nextAssigned.remove(file) && !nextAvailable.contains(file)) {
|
||||
nextAvailable.add(file);
|
||||
}
|
||||
}
|
||||
return withTiles(nextAvailable, nextAssigned);
|
||||
}
|
||||
|
||||
AssetDetailsColorSchemaDraft moveAssignedUp(int index) {
|
||||
if (index <= 0 || index >= assignedTiles.size()) {
|
||||
return this;
|
||||
}
|
||||
final List<AssetWorkspaceBankCompositionFile> nextAssigned = new ArrayList<>(assignedTiles);
|
||||
final AssetWorkspaceBankCompositionFile file = nextAssigned.remove(index);
|
||||
nextAssigned.add(index - 1, file);
|
||||
return withTiles(availableTiles, nextAssigned);
|
||||
}
|
||||
|
||||
AssetDetailsColorSchemaDraft moveAssignedDown(int index) {
|
||||
if (index < 0 || index >= assignedTiles.size() - 1) {
|
||||
return this;
|
||||
}
|
||||
final List<AssetWorkspaceBankCompositionFile> nextAssigned = new ArrayList<>(assignedTiles);
|
||||
final AssetWorkspaceBankCompositionFile file = nextAssigned.remove(index);
|
||||
nextAssigned.add(index + 1, file);
|
||||
return withTiles(availableTiles, nextAssigned);
|
||||
}
|
||||
|
||||
AssetDetailsColorSchemaDraft markForRemoval() {
|
||||
if (!selected()) {
|
||||
return this;
|
||||
}
|
||||
return new AssetDetailsColorSchemaDraft(schemaId, existingSchema, true, availableTiles, assignedTiles, preferredColors, deriveColorsFromTiles);
|
||||
}
|
||||
|
||||
AssetDetailsColorSchemaDraft asUpdateDraft() {
|
||||
return new AssetDetailsColorSchemaDraft(schemaId, existingSchema, false, availableTiles, assignedTiles, orderedColors(), true);
|
||||
}
|
||||
|
||||
List<String> orderedColors() {
|
||||
if (!deriveColorsFromTiles) {
|
||||
return preferredColors;
|
||||
}
|
||||
final LinkedHashSet<String> active = new LinkedHashSet<>();
|
||||
for (AssetWorkspaceBankCompositionFile tile : assignedTiles) {
|
||||
AssetDetailsColorSchemaCoordinator.tileColors(tile).stream()
|
||||
.map(AssetDetailsColorSchemaDraft::normalize)
|
||||
.forEach(active::add);
|
||||
}
|
||||
final List<String> ordered = new ArrayList<>();
|
||||
for (String preferredColor : preferredColors) {
|
||||
if (active.contains(preferredColor) && !ordered.contains(preferredColor)) {
|
||||
ordered.add(preferredColor);
|
||||
}
|
||||
}
|
||||
for (String color : active) {
|
||||
if (!ordered.contains(color)) {
|
||||
ordered.add(color);
|
||||
}
|
||||
}
|
||||
return List.copyOf(ordered);
|
||||
}
|
||||
|
||||
private AssetDetailsColorSchemaDraft withTiles(
|
||||
List<AssetWorkspaceBankCompositionFile> nextAvailable,
|
||||
List<AssetWorkspaceBankCompositionFile> nextAssigned) {
|
||||
return new AssetDetailsColorSchemaDraft(schemaId, existingSchema, false, nextAvailable, nextAssigned, orderedColors(), true);
|
||||
}
|
||||
|
||||
private static List<String> normalize(List<String> colors) {
|
||||
final List<String> normalized = new ArrayList<>();
|
||||
for (String color : colors) {
|
||||
if (color == null || color.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
normalized.add(normalize(color));
|
||||
}
|
||||
return List.copyOf(normalized);
|
||||
}
|
||||
|
||||
private static String normalize(String color) {
|
||||
return color.trim().toUpperCase();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,521 @@
|
||||
package p.studio.workspaces.assets.details.schema;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import p.studio.Container;
|
||||
import p.studio.controls.banks.StudioDualListView;
|
||||
import p.studio.controls.forms.StudioFormEditScopeChangedEvent;
|
||||
import p.studio.controls.forms.StudioFormMode;
|
||||
import p.studio.controls.forms.StudioFormSection;
|
||||
import p.studio.controls.forms.StudioFormSession;
|
||||
import p.studio.lsp.events.StudioWorkspaceEventBus;
|
||||
import p.studio.projects.ProjectReference;
|
||||
import p.studio.utilities.i18n.I18n;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetLogEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent;
|
||||
import p.studio.workspaces.assets.messages.events.StudioGlyphMaterialMetadataChangedEvent;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialArtifactBinding;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialMetadata;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialSchema;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialSchemaStatus;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioMetadataService;
|
||||
import p.studio.workspaces.framework.StudioSubscriptionBag;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
public final class AssetDetailsColorSchemaGroupsControl extends StudioFormSection {
|
||||
private static final String SECTION_ID = "asset-details.color-schema-groups";
|
||||
|
||||
private final StudioWorkspaceEventBus workspaceBus;
|
||||
private final StudioSubscriptionBag subscriptions = new StudioSubscriptionBag();
|
||||
private final AssetStudioMetadataService metadataService = new AssetStudioMetadataService();
|
||||
private final AssetDetailsColorSchemaCoordinator coordinator = new AssetDetailsColorSchemaCoordinator();
|
||||
private final ListView<SchemaListItem> schemaListView = new ListView<>();
|
||||
private final SchemaTileDualListView tileDualListView = new SchemaTileDualListView();
|
||||
private final VBox schemaActions = new VBox(6);
|
||||
private final HBox schemaListPane = new HBox(8, schemaActions, schemaListView);
|
||||
private final HBox paletteStrip = new HBox(4);
|
||||
private final Label summaryLabel = new Label();
|
||||
private final Label schemaTitleLabel = new Label();
|
||||
private final Label paletteCounterLabel = new Label();
|
||||
private final Label warningLabel = new Label();
|
||||
private final VBox editorPane = new VBox(10);
|
||||
private final HBox body = new HBox(16);
|
||||
private final VBox content = new VBox(12, body, summaryLabel);
|
||||
|
||||
private AssetWorkspaceDetailsViewState viewState;
|
||||
private StudioFormSession<AssetDetailsColorSchemaDraft> formSession;
|
||||
private String selectedSchemaId;
|
||||
private boolean selectingSchema;
|
||||
|
||||
public AssetDetailsColorSchemaGroupsControl(ProjectReference projectReference, StudioWorkspaceEventBus workspaceBus) {
|
||||
Objects.requireNonNull(projectReference, "projectReference");
|
||||
this.workspaceBus = Objects.requireNonNull(workspaceBus, "workspaceBus");
|
||||
schemaListView.getStyleClass().add("assets-details-color-schema-list");
|
||||
schemaListView.setCellFactory(ignored -> createSchemaCell());
|
||||
schemaListView.getSelectionModel().selectedItemProperty().addListener((ignored, previous, current) -> {
|
||||
if (selectingSchema) {
|
||||
return;
|
||||
}
|
||||
selectedSchemaId = current == null ? null : current.schemaId();
|
||||
if (formMode() == StudioFormMode.EDITING) {
|
||||
formSession.setDraft(selectedSchemaId == null
|
||||
? AssetDetailsColorSchemaDraft.noSelection()
|
||||
: sourceDraft(selectedSchemaId).asUpdateDraft());
|
||||
} else {
|
||||
replaceFormSource();
|
||||
}
|
||||
renderSection();
|
||||
});
|
||||
schemaActions.getStyleClass().add("assets-details-color-schema-toolbar");
|
||||
schemaListPane.getStyleClass().add("assets-details-list-with-actions");
|
||||
content.getStyleClass().add("assets-details-color-schema-content");
|
||||
body.getStyleClass().add("assets-details-color-schema-body");
|
||||
paletteStrip.getStyleClass().add("assets-details-color-schema-strip");
|
||||
schemaTitleLabel.getStyleClass().add("assets-details-color-schema-title");
|
||||
paletteCounterLabel.getStyleClass().add("assets-details-color-schema-summary");
|
||||
summaryLabel.getStyleClass().add("assets-details-color-schema-summary");
|
||||
warningLabel.getStyleClass().add("assets-details-color-schema-warning");
|
||||
HBox.setHgrow(schemaListPane, Priority.ALWAYS);
|
||||
HBox.setHgrow(schemaListView, Priority.ALWAYS);
|
||||
HBox.setHgrow(editorPane, Priority.ALWAYS);
|
||||
schemaListView.setMaxWidth(Double.MAX_VALUE);
|
||||
editorPane.setMaxWidth(Double.MAX_VALUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribe() {
|
||||
subscriptions.add(workspaceBus.subscribe(StudioAssetsDetailsViewStateChangedEvent.class, event -> {
|
||||
viewState = event.viewState();
|
||||
coordinator.replaceDetails(viewState == null ? null : viewState.selectedAssetDetails());
|
||||
normalizeSelectedSchema();
|
||||
replaceFormSource();
|
||||
if (viewState == null || viewState.selectedAssetReference() == null) {
|
||||
publishEditScope(workspaceBus, currentScopeKey(), null);
|
||||
}
|
||||
renderSection();
|
||||
}));
|
||||
subscriptions.add(workspaceBus.subscribe(StudioFormEditScopeChangedEvent.class,
|
||||
event -> handleEditScopeChanged(event, currentScopeKey())));
|
||||
subscriptions.add(workspaceBus.subscribe(StudioGlyphMaterialMetadataChangedEvent.class, event -> {
|
||||
if (viewState == null || !Objects.equals(viewState.selectedAssetReference(), event.assetReference())) {
|
||||
return;
|
||||
}
|
||||
coordinator.replaceMaterialMetadata(event.metadata());
|
||||
if (formMode() != StudioFormMode.EDITING) {
|
||||
replaceFormSource();
|
||||
}
|
||||
renderSection();
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unsubscribe() {
|
||||
subscriptions.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void renderSection() {
|
||||
if (viewState == null || viewState.selectedAssetDetails() == null || !coordinator.ready()) {
|
||||
formSession = null;
|
||||
clearRenderedSection();
|
||||
return;
|
||||
}
|
||||
if (formSession == null) {
|
||||
replaceFormSource();
|
||||
}
|
||||
|
||||
final AssetDetailsColorSchemaDraft draft = formSession.draft();
|
||||
renderSchemaList();
|
||||
renderSchemaActions(draft);
|
||||
renderEditor(draft);
|
||||
summaryLabel.setText(Container.i18n().format(
|
||||
I18n.ASSETS_COLOR_SCHEMA_SUMMARY,
|
||||
coordinator.schemas().size(),
|
||||
coordinator.unassignedTileCount()));
|
||||
body.getChildren().setAll(schemaListPane, editorPane);
|
||||
renderFormSection(content);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String sectionTitle() {
|
||||
return Container.i18n().text(I18n.ASSETS_SECTION_COLOR_SCHEMA_GROUPS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected StudioFormMode formMode() {
|
||||
return formSession == null ? StudioFormMode.READ_ONLY : formSession.mode();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isDirty() {
|
||||
return formSession != null && formSession.isDirty();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void beginEdit() {
|
||||
if (formSession == null) {
|
||||
replaceFormSource();
|
||||
}
|
||||
selectedSchemaId = null;
|
||||
formSession.replaceSource(AssetDetailsColorSchemaDraft.noSelection());
|
||||
formSession.beginEdit();
|
||||
publishEditScope(workspaceBus, currentScopeKey(), SECTION_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void apply() {
|
||||
if (viewState == null || viewState.selectedAssetDetails() == null || viewState.selectedAssetReference() == null || formSession == null) {
|
||||
return;
|
||||
}
|
||||
final AssetDetailsColorSchemaDraft draft = formSession.draft();
|
||||
if (!draft.selected()) {
|
||||
workspaceBus.publish(new StudioAssetLogEvent("color-schema-groups", "Apply failed: select or create a schema first."));
|
||||
return;
|
||||
}
|
||||
if (!draft.removeRequested()) {
|
||||
final List<String> orderedColors = draft.orderedColors();
|
||||
if (draft.assignedTiles().isEmpty() || orderedColors.isEmpty()) {
|
||||
workspaceBus.publish(new StudioAssetLogEvent("color-schema-groups", "Apply failed: schema must contain at least one tile."));
|
||||
return;
|
||||
}
|
||||
if (orderedColors.size() > 15) {
|
||||
workspaceBus.publish(new StudioAssetLogEvent(
|
||||
"color-schema-groups",
|
||||
Container.i18n().text(I18n.ASSETS_COLOR_SCHEMA_DIALOG_TOO_MANY_COLORS)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final var assetDetails = viewState.selectedAssetDetails();
|
||||
final var assetReference = viewState.selectedAssetReference();
|
||||
Container.backgroundTasks().submit(() -> {
|
||||
try {
|
||||
final AssetStudioGlyphMaterialMetadata metadata = draft.removeRequested()
|
||||
? coordinator.removeSchema(draft.schemaId())
|
||||
: coordinator.upsertSchema(draft.schemaId(), draft.assignedTiles(), draft.orderedColors());
|
||||
metadataService.writeGlyphMaterialMetadata(assetDetails.summary().assetRoot(), metadata);
|
||||
Platform.runLater(() -> {
|
||||
formSession.apply();
|
||||
selectedSchemaId = draft.removeRequested() ? null : draft.schemaId();
|
||||
publishEditScope(workspaceBus, currentScopeKey(), null);
|
||||
workspaceBus.publish(new StudioAssetLogEvent(
|
||||
"color-schema-groups",
|
||||
draft.removeRequested() ? "Schema removed: " + draft.schemaId() : "Schema saved: " + draft.schemaId()));
|
||||
workspaceBus.publish(new StudioGlyphMaterialMetadataChangedEvent(assetReference, metadata));
|
||||
renderSection();
|
||||
});
|
||||
} catch (IOException | RuntimeException exception) {
|
||||
Platform.runLater(() -> workspaceBus.publish(new StudioAssetLogEvent(
|
||||
"color-schema-groups",
|
||||
"Unable to save schema: " + Objects.requireNonNullElse(exception.getMessage(), "unknown error"))));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reset() {
|
||||
if (formSession == null) {
|
||||
return;
|
||||
}
|
||||
formSession.resetDraft();
|
||||
selectedSchemaId = formSession.draft().selected() ? formSession.draft().schemaId() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cancel() {
|
||||
if (formSession == null) {
|
||||
return;
|
||||
}
|
||||
formSession.cancelEdit();
|
||||
selectedSchemaId = formSession.draft().selected() ? formSession.draft().schemaId() : null;
|
||||
publishEditScope(workspaceBus, currentScopeKey(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String sectionStyleClass() {
|
||||
return "assets-details-color-schema-section";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String formSectionId() {
|
||||
return SECTION_ID;
|
||||
}
|
||||
|
||||
private void renderSchemaList() {
|
||||
selectingSchema = true;
|
||||
schemaListView.getItems().setAll(schemaListItems());
|
||||
schemaListView.setDisable(false);
|
||||
if (selectedSchemaId != null) {
|
||||
schemaListView.getItems().stream()
|
||||
.filter(schema -> schema.schemaId().equals(selectedSchemaId))
|
||||
.findFirst()
|
||||
.ifPresentOrElse(
|
||||
schema -> schemaListView.getSelectionModel().select(schema),
|
||||
() -> schemaListView.getSelectionModel().clearSelection());
|
||||
} else {
|
||||
schemaListView.getSelectionModel().clearSelection();
|
||||
}
|
||||
selectingSchema = false;
|
||||
}
|
||||
|
||||
private void renderSchemaActions(AssetDetailsColorSchemaDraft draft) {
|
||||
final Button newButton = new Button("+");
|
||||
newButton.getStyleClass().addAll("studio-button", "studio-button-secondary", "assets-details-mini-action-button");
|
||||
newButton.setDisable(formMode() != StudioFormMode.EDITING);
|
||||
newButton.setOnAction(ignored -> {
|
||||
selectedSchemaId = coordinator.nextSchemaId();
|
||||
formSession.setDraft(sourceDraft(selectedSchemaId).asUpdateDraft());
|
||||
rerenderPreservingScrollPosition();
|
||||
});
|
||||
|
||||
final Button removeButton = new Button("-");
|
||||
removeButton.getStyleClass().addAll("studio-button", "studio-button-secondary", "assets-details-mini-action-button");
|
||||
removeButton.setDisable(formMode() != StudioFormMode.EDITING || !draft.selected() || (draft.existingSchema() && !coordinator.canRemove(draft.schemaId())));
|
||||
removeButton.setOnAction(ignored -> {
|
||||
if (draft.existingSchema()) {
|
||||
formSession.updateDraft(AssetDetailsColorSchemaDraft::markForRemoval);
|
||||
} else {
|
||||
selectedSchemaId = null;
|
||||
formSession.setDraft(AssetDetailsColorSchemaDraft.noSelection());
|
||||
}
|
||||
rerenderPreservingScrollPosition();
|
||||
});
|
||||
if (draft.existingSchema() && !coordinator.canRemove(draft.schemaId())) {
|
||||
removeButton.setTooltip(new javafx.scene.control.Tooltip(
|
||||
Container.i18n().format(I18n.ASSETS_COLOR_SCHEMA_REMOVE_BLOCKED, draft.schemaId())));
|
||||
}
|
||||
|
||||
schemaActions.getChildren().setAll(newButton, removeButton);
|
||||
}
|
||||
|
||||
private void renderEditor(AssetDetailsColorSchemaDraft draft) {
|
||||
editorPane.getChildren().clear();
|
||||
if (!draft.selected()) {
|
||||
tileDualListView.setLeftItems(List.of());
|
||||
tileDualListView.setRightItems(List.of());
|
||||
tileDualListView.setInteractionEnabled(false);
|
||||
paletteStrip.getChildren().clear();
|
||||
paletteCounterLabel.setText(Container.i18n().format(I18n.ASSETS_COLOR_SCHEMA_DIALOG_COLOR_COUNT, 0));
|
||||
editorPane.getChildren().addAll(
|
||||
emptyMessage("Select a schema or create a new one to edit its tile group."),
|
||||
tileDualListView,
|
||||
paletteCounterLabel,
|
||||
paletteStrip);
|
||||
return;
|
||||
}
|
||||
if (draft.removeRequested()) {
|
||||
editorPane.getChildren().add(emptyMessage("Schema " + draft.schemaId() + " will be removed when you apply this form."));
|
||||
return;
|
||||
}
|
||||
|
||||
final List<String> orderedColors = draft.orderedColors();
|
||||
schemaTitleLabel.setText(draft.schemaId() + (draft.existingSchema() ? "" : " (new)"));
|
||||
paletteCounterLabel.setText(Container.i18n().format(I18n.ASSETS_COLOR_SCHEMA_DIALOG_COLOR_COUNT, orderedColors.size()));
|
||||
warningLabel.setText(orderedColors.size() > 15
|
||||
? Container.i18n().text(I18n.ASSETS_COLOR_SCHEMA_DIALOG_TOO_MANY_COLORS)
|
||||
: "");
|
||||
paletteStrip.getChildren().setAll(orderedColors.stream().map(this::swatch).toList());
|
||||
|
||||
tileDualListView.setLeftItems(draft.availableTiles());
|
||||
tileDualListView.setRightItems(draft.assignedTiles());
|
||||
tileDualListView.setInteractionEnabled(formMode() == StudioFormMode.EDITING && draft.selected());
|
||||
tileDualListView.setMoveToRightAllowed(true);
|
||||
tileDualListView.setOnMoveToRight(items -> updateDraft(current -> current.moveToAssigned(items)));
|
||||
tileDualListView.setOnMoveToLeft(items -> updateDraft(current -> current.moveToAvailable(items)));
|
||||
tileDualListView.setOnMoveUp(index -> updateDraft(current -> current.moveAssignedUp(index)));
|
||||
tileDualListView.setOnMoveDown(index -> updateDraft(current -> current.moveAssignedDown(index)));
|
||||
|
||||
editorPane.getChildren().addAll(schemaTitleLabel, tileDualListView, paletteCounterLabel, paletteStrip, warningLabel);
|
||||
VBox.setVgrow(tileDualListView, Priority.ALWAYS);
|
||||
}
|
||||
|
||||
private void updateDraft(java.util.function.UnaryOperator<AssetDetailsColorSchemaDraft> update) {
|
||||
if (formMode() != StudioFormMode.EDITING || formSession == null) {
|
||||
return;
|
||||
}
|
||||
formSession.updateDraft(update);
|
||||
rerenderPreservingScrollPosition();
|
||||
}
|
||||
|
||||
private AssetDetailsColorSchemaDraft sourceDraft(String schemaId) {
|
||||
final AssetStudioGlyphMaterialSchema schema = coordinator.findSchema(schemaId).orElse(null);
|
||||
final List<AssetWorkspaceBankCompositionFile> selectableTiles = schema == null
|
||||
? coordinator.availableTilesForCreate()
|
||||
: coordinator.availableTilesForEdit(schema.schemaId());
|
||||
final Set<String> assignedArtifactIds = schema == null
|
||||
? Set.of()
|
||||
: schema.artifactBindings().stream()
|
||||
.map(AssetStudioGlyphMaterialArtifactBinding::artifactId)
|
||||
.collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
|
||||
final List<AssetWorkspaceBankCompositionFile> assignedTiles = selectableTiles.stream()
|
||||
.filter(file -> assignedArtifactIds.contains(AssetDetailsColorSchemaCoordinator.artifactId(file)))
|
||||
.toList();
|
||||
final List<AssetWorkspaceBankCompositionFile> availableTiles = selectableTiles.stream()
|
||||
.filter(file -> !assignedArtifactIds.contains(AssetDetailsColorSchemaCoordinator.artifactId(file)))
|
||||
.toList();
|
||||
final List<String> preferredColors = schema == null
|
||||
? List.of()
|
||||
: schema.materialIndices().entrySet().stream()
|
||||
.sorted(java.util.Map.Entry.comparingByKey())
|
||||
.map(java.util.Map.Entry::getValue)
|
||||
.toList();
|
||||
return new AssetDetailsColorSchemaDraft(schemaId, schema != null, false, availableTiles, assignedTiles, preferredColors, schema == null);
|
||||
}
|
||||
|
||||
private void replaceFormSource() {
|
||||
if (!coordinator.ready()) {
|
||||
formSession = null;
|
||||
return;
|
||||
}
|
||||
if (formSession != null && formSession.mode() == StudioFormMode.EDITING) {
|
||||
return;
|
||||
}
|
||||
final String schemaId = selectedSchemaId != null ? selectedSchemaId : defaultSchemaId();
|
||||
selectedSchemaId = schemaId;
|
||||
final AssetDetailsColorSchemaDraft source = schemaId == null
|
||||
? AssetDetailsColorSchemaDraft.noSelection()
|
||||
: sourceDraft(schemaId);
|
||||
if (formSession == null) {
|
||||
formSession = new StudioFormSession<>(source);
|
||||
} else {
|
||||
formSession.replaceSource(source);
|
||||
}
|
||||
}
|
||||
|
||||
private void normalizeSelectedSchema() {
|
||||
if (selectedSchemaId == null || coordinator.findSchema(selectedSchemaId).isPresent()) {
|
||||
return;
|
||||
}
|
||||
selectedSchemaId = null;
|
||||
}
|
||||
|
||||
private String defaultSchemaId() {
|
||||
return coordinator.schemas().stream()
|
||||
.map(AssetStudioGlyphMaterialSchema::schemaId)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private List<SchemaListItem> schemaListItems() {
|
||||
final List<SchemaListItem> items = new java.util.ArrayList<>(coordinator.schemas().stream()
|
||||
.map(schema -> SchemaListItem.fromSchema(schema, coordinator.selectedTiles()))
|
||||
.toList());
|
||||
if (formSession != null
|
||||
&& formSession.draft().selected()
|
||||
&& !formSession.draft().existingSchema()
|
||||
&& items.stream().noneMatch(item -> item.schemaId().equals(formSession.draft().schemaId()))) {
|
||||
items.add(SchemaListItem.fromDraft(formSession.draft()));
|
||||
}
|
||||
return List.copyOf(items);
|
||||
}
|
||||
|
||||
private ListCell<SchemaListItem> createSchemaCell() {
|
||||
return new ListCell<>() {
|
||||
@Override
|
||||
protected void updateItem(SchemaListItem item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || item == null) {
|
||||
setGraphic(null);
|
||||
setText(null);
|
||||
return;
|
||||
}
|
||||
final Label title = new Label(item.schemaId());
|
||||
title.getStyleClass().add("assets-details-color-schema-title");
|
||||
final Label version = new Label(item.existing() ? "v" + item.version() : "new");
|
||||
version.getStyleClass().add("assets-details-color-schema-version");
|
||||
final Label status = new Label(item.status());
|
||||
status.getStyleClass().addAll("studio-chip", Objects.equals(item.status(), AssetStudioGlyphMaterialSchemaStatus.OK.manifestValue())
|
||||
? "assets-details-color-schema-status-ok"
|
||||
: "assets-details-color-schema-status-inconsistent");
|
||||
final Region spacer = new Region();
|
||||
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||
final HBox top = new HBox(8, title, version, spacer, status);
|
||||
|
||||
final Label meta = new Label(item.tileCount() + " tiles · " + item.indexCount() + " indices");
|
||||
meta.getStyleClass().add("assets-details-color-schema-meta");
|
||||
final VBox root = new VBox(6, top, meta);
|
||||
root.getStyleClass().add("assets-details-color-schema-item");
|
||||
setGraphic(root);
|
||||
setText(null);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Region swatch(String color) {
|
||||
final Region swatch = new Region();
|
||||
swatch.getStyleClass().add("assets-details-color-schema-swatch");
|
||||
swatch.setMinSize(20, 20);
|
||||
swatch.setPrefSize(20, 20);
|
||||
swatch.setMaxSize(20, 20);
|
||||
swatch.setStyle("-fx-background-color: " + color + ";");
|
||||
return swatch;
|
||||
}
|
||||
|
||||
private Node emptyMessage(String text) {
|
||||
final Label label = new Label(text);
|
||||
label.getStyleClass().add("assets-details-color-schema-empty");
|
||||
return label;
|
||||
}
|
||||
|
||||
private String currentScopeKey() {
|
||||
return viewState == null || viewState.selectedAssetReference() == null
|
||||
? null
|
||||
: "asset-details:" + viewState.selectedAssetReference();
|
||||
}
|
||||
|
||||
private static final class SchemaTileDualListView extends StudioDualListView<AssetWorkspaceBankCompositionFile> {
|
||||
private SchemaTileDualListView() {
|
||||
setLeftTitle(Container.i18n().text(I18n.ASSETS_COLOR_SCHEMA_DIALOG_AVAILABLE_TILES));
|
||||
setRightTitle(Container.i18n().text(I18n.ASSETS_COLOR_SCHEMA_DIALOG_ASSIGNED_TILES));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String itemText(AssetWorkspaceBankCompositionFile item) {
|
||||
return item.displayName();
|
||||
}
|
||||
}
|
||||
|
||||
private record SchemaListItem(
|
||||
String schemaId,
|
||||
boolean existing,
|
||||
int version,
|
||||
String status,
|
||||
int tileCount,
|
||||
int indexCount) {
|
||||
|
||||
private static SchemaListItem fromSchema(AssetStudioGlyphMaterialSchema schema, List<AssetWorkspaceBankCompositionFile> selectedTiles) {
|
||||
return new SchemaListItem(
|
||||
schema.schemaId(),
|
||||
true,
|
||||
schema.version(),
|
||||
AssetDetailsColorSchemaCoordinator.effectiveStatus(schema, selectedTiles).manifestValue(),
|
||||
schema.artifactBindings().size(),
|
||||
schema.materialIndices().size());
|
||||
}
|
||||
|
||||
private static SchemaListItem fromDraft(AssetDetailsColorSchemaDraft draft) {
|
||||
return new SchemaListItem(
|
||||
draft.schemaId(),
|
||||
false,
|
||||
0,
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK.manifestValue(),
|
||||
draft.assignedTiles().size(),
|
||||
draft.orderedColors().size());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
package p.studio.workspaces.assets.details.summary;
|
||||
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import p.studio.Container;
|
||||
import p.studio.controls.lifecycle.StudioControlLifecycle;
|
||||
@ -8,6 +9,8 @@ import p.studio.lsp.events.StudioWorkspaceEventBus;
|
||||
import p.studio.projects.ProjectReference;
|
||||
import p.studio.utilities.i18n.I18n;
|
||||
import p.studio.workspaces.assets.details.AssetDetailsUiSupport;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneBankMetadata;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetSummary;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState;
|
||||
import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent;
|
||||
@ -56,11 +59,20 @@ public final class AssetDetailsSummaryControl extends VBox implements StudioCont
|
||||
content.getChildren().setAll(
|
||||
AssetDetailsUiSupport.createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_ASSET_ID), summary.assetId() == null ? "—" : String.valueOf(summary.assetId())),
|
||||
AssetDetailsUiSupport.createKeyValueRow(Container.i18n().text(I18n.ASSETS_LABEL_LOCATION), AssetDetailsUiSupport.projectRelativePath(projectReference, summary.assetRoot())));
|
||||
final HBox typeBox = new HBox(8);
|
||||
typeBox.getStyleClass().add("assets-details-chips");
|
||||
typeBox.getChildren().add(AssetDetailsUiSupport.createChip(
|
||||
AssetDetailsUiSupport.typeChipTone(summary.assetFamily()),
|
||||
AssetDetailsUiSupport.typeLabel(summary.assetFamily())));
|
||||
if (summary.glyphSpecialization() != AssetStudioGlyphSpecialization.NONE) {
|
||||
typeBox.getChildren().add(AssetDetailsUiSupport.createChip(
|
||||
AssetDetailsUiSupport.typeChipTone(summary.assetFamily()),
|
||||
AssetDetailsUiSupport.specializationLabel(summary.glyphSpecialization()),
|
||||
"assets-details-chip-fixed"));
|
||||
}
|
||||
content.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
|
||||
Container.i18n().text(I18n.ASSETS_LABEL_TYPE),
|
||||
AssetDetailsUiSupport.createChip(
|
||||
AssetDetailsUiSupport.typeChipTone(summary.assetFamily()),
|
||||
AssetDetailsUiSupport.typeLabel(summary.assetFamily()))));
|
||||
typeBox));
|
||||
content.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
|
||||
Container.i18n().text(I18n.ASSETS_LABEL_REGISTRATION),
|
||||
AssetDetailsUiSupport.createChip(
|
||||
|
||||
@ -113,6 +113,7 @@ public final class AssetListItemControl extends VBox {
|
||||
private String assetRowToneClass(AssetFamilyCatalog assetFamily) {
|
||||
return switch (assetFamily) {
|
||||
case GLYPH_BANK -> "assets-workspace-asset-row-tone-image";
|
||||
case SCENE_BANK -> "assets-workspace-asset-row-tone-scene";
|
||||
case SOUND_BANK -> "assets-workspace-asset-row-tone-audio";
|
||||
default -> "assets-workspace-asset-row-tone-generic";
|
||||
};
|
||||
@ -121,6 +122,7 @@ public final class AssetListItemControl extends VBox {
|
||||
private String assetNameToneClass(AssetFamilyCatalog assetFamily) {
|
||||
return switch (assetFamily) {
|
||||
case GLYPH_BANK -> "assets-workspace-asset-name-tone-image";
|
||||
case SCENE_BANK -> "assets-workspace-asset-name-tone-scene";
|
||||
case SOUND_BANK -> "assets-workspace-asset-name-tone-audio";
|
||||
default -> "assets-workspace-asset-name-tone-generic";
|
||||
};
|
||||
|
||||
@ -4,6 +4,8 @@ import p.packer.dtos.PackerCodecConfigurationFieldDTO;
|
||||
import p.packer.dtos.PackerDiagnosticDTO;
|
||||
import p.packer.messages.assets.OutputCodecCatalog;
|
||||
import p.packer.messages.assets.OutputFormatCatalog;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialMetadata;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneBankMetadata;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -20,6 +22,9 @@ public record AssetWorkspaceAssetDetails(
|
||||
Map<String, Object> outputPipeline,
|
||||
AssetWorkspaceBankCompositionDetails bankComposition,
|
||||
List<Map<String, Object>> pipelinePalettes,
|
||||
AssetStudioGlyphMaterialMetadata glyphMaterialMetadata,
|
||||
AssetStudioSceneBankMetadata sceneBankMetadata,
|
||||
AssetWorkspaceSceneBankValidation sceneBankValidation,
|
||||
List<PackerDiagnosticDTO> diagnostics) {
|
||||
|
||||
public AssetWorkspaceAssetDetails {
|
||||
@ -33,6 +38,43 @@ public record AssetWorkspaceAssetDetails(
|
||||
outputPipeline = Map.copyOf(Objects.requireNonNull(outputPipeline, "outputPipeline"));
|
||||
bankComposition = Objects.requireNonNull(bankComposition, "bankComposition");
|
||||
pipelinePalettes = List.copyOf(Objects.requireNonNull(pipelinePalettes, "pipelinePalettes"));
|
||||
sceneBankValidation = Objects.requireNonNullElse(sceneBankValidation, AssetWorkspaceSceneBankValidation.NOT_APPLICABLE);
|
||||
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
|
||||
}
|
||||
|
||||
public AssetWorkspaceAssetDetails withBankComposition(AssetWorkspaceBankCompositionDetails bankComposition) {
|
||||
return new AssetWorkspaceAssetDetails(
|
||||
summary,
|
||||
actions,
|
||||
outputFormat,
|
||||
outputCodec,
|
||||
availableOutputCodecs,
|
||||
codecConfigurationFieldsByCodec,
|
||||
metadataFields,
|
||||
outputPipeline,
|
||||
bankComposition,
|
||||
pipelinePalettes,
|
||||
glyphMaterialMetadata,
|
||||
sceneBankMetadata,
|
||||
sceneBankValidation,
|
||||
diagnostics);
|
||||
}
|
||||
|
||||
public AssetWorkspaceAssetDetails withGlyphMaterialMetadata(AssetStudioGlyphMaterialMetadata glyphMaterialMetadata) {
|
||||
return new AssetWorkspaceAssetDetails(
|
||||
summary,
|
||||
actions,
|
||||
outputFormat,
|
||||
outputCodec,
|
||||
availableOutputCodecs,
|
||||
codecConfigurationFieldsByCodec,
|
||||
metadataFields,
|
||||
outputPipeline,
|
||||
bankComposition,
|
||||
pipelinePalettes,
|
||||
glyphMaterialMetadata,
|
||||
sceneBankMetadata,
|
||||
sceneBankValidation,
|
||||
diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package p.studio.workspaces.assets.messages;
|
||||
|
||||
import p.packer.messages.AssetReference;
|
||||
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
@ -13,6 +14,7 @@ public record AssetWorkspaceAssetSummary(
|
||||
AssetWorkspaceBuildParticipation buildParticipation,
|
||||
Integer assetId,
|
||||
AssetFamilyCatalog assetFamily,
|
||||
AssetStudioGlyphSpecialization glyphSpecialization,
|
||||
Path assetRoot,
|
||||
boolean preload,
|
||||
boolean hasDiagnostics) {
|
||||
@ -23,6 +25,7 @@ public record AssetWorkspaceAssetSummary(
|
||||
Objects.requireNonNull(state, "state");
|
||||
Objects.requireNonNull(buildParticipation, "buildParticipation");
|
||||
assetFamily = Objects.requireNonNullElse(assetFamily, AssetFamilyCatalog.UNKNOWN);
|
||||
glyphSpecialization = Objects.requireNonNullElse(glyphSpecialization, AssetStudioGlyphSpecialization.NONE);
|
||||
assetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
|
||||
if (assetName.isBlank()) {
|
||||
throw new IllegalArgumentException("assetName must not be blank");
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
package p.studio.workspaces.assets.messages;
|
||||
|
||||
public enum AssetWorkspaceSceneBankStatus {
|
||||
NOT_APPLICABLE,
|
||||
PENDING_VALIDATION,
|
||||
VALIDATED_PENDING_ACCEPTANCE,
|
||||
READY,
|
||||
VALIDATION_FAILED
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package p.studio.workspaces.assets.messages;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record AssetWorkspaceSceneBankValidation(
|
||||
AssetWorkspaceSceneBankStatus status,
|
||||
boolean pendingExternalChanges,
|
||||
boolean canAccept,
|
||||
String currentFingerprint,
|
||||
String validatedFingerprint,
|
||||
String acceptedFingerprint) {
|
||||
|
||||
public static final AssetWorkspaceSceneBankValidation NOT_APPLICABLE = new AssetWorkspaceSceneBankValidation(
|
||||
AssetWorkspaceSceneBankStatus.NOT_APPLICABLE,
|
||||
false,
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
"");
|
||||
|
||||
public AssetWorkspaceSceneBankValidation {
|
||||
status = Objects.requireNonNullElse(status, AssetWorkspaceSceneBankStatus.NOT_APPLICABLE);
|
||||
currentFingerprint = Objects.requireNonNullElse(currentFingerprint, "");
|
||||
validatedFingerprint = Objects.requireNonNullElse(validatedFingerprint, "");
|
||||
acceptedFingerprint = Objects.requireNonNullElse(acceptedFingerprint, "");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package p.studio.workspaces.assets.messages.events;
|
||||
|
||||
import p.packer.messages.AssetReference;
|
||||
import p.studio.lsp.events.StudioEvent;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionDetails;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record StudioBankCompositionChangedEvent(
|
||||
AssetReference assetReference,
|
||||
AssetWorkspaceBankCompositionDetails bankComposition) implements StudioEvent {
|
||||
|
||||
public StudioBankCompositionChangedEvent {
|
||||
assetReference = Objects.requireNonNull(assetReference, "assetReference");
|
||||
bankComposition = Objects.requireNonNull(bankComposition, "bankComposition");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package p.studio.workspaces.assets.messages.events;
|
||||
|
||||
import p.packer.messages.AssetReference;
|
||||
import p.studio.lsp.events.StudioEvent;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphMaterialMetadata;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record StudioGlyphMaterialMetadataChangedEvent(
|
||||
AssetReference assetReference,
|
||||
AssetStudioGlyphMaterialMetadata metadata) implements StudioEvent {
|
||||
|
||||
public StudioGlyphMaterialMetadataChangedEvent {
|
||||
assetReference = Objects.requireNonNull(assetReference, "assetReference");
|
||||
metadata = Objects.requireNonNull(metadata, "metadata");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package p.studio.workspaces.assets.metadata;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record AssetStudioGlyphMaterialArtifactBinding(
|
||||
String artifactId,
|
||||
String fingerprint) {
|
||||
|
||||
public AssetStudioGlyphMaterialArtifactBinding {
|
||||
artifactId = Objects.requireNonNull(artifactId, "artifactId").trim();
|
||||
fingerprint = Objects.requireNonNull(Objects.requireNonNullElse(fingerprint, ""), "fingerprint").trim();
|
||||
if (artifactId.isBlank()) {
|
||||
throw new IllegalArgumentException("artifactId must not be blank");
|
||||
}
|
||||
if (fingerprint.isBlank()) {
|
||||
throw new IllegalArgumentException("fingerprint must not be blank");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package p.studio.workspaces.assets.metadata;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public record AssetStudioGlyphMaterialMetadata(
|
||||
List<AssetStudioGlyphMaterialSchema> schemas,
|
||||
List<AssetStudioGlyphMaterialPalette> palettes,
|
||||
Path supportFile) {
|
||||
|
||||
public AssetStudioGlyphMaterialMetadata {
|
||||
schemas = List.copyOf(Objects.requireNonNull(schemas, "schemas"));
|
||||
palettes = List.copyOf(Objects.requireNonNull(palettes, "palettes"));
|
||||
supportFile = supportFile == null ? null : supportFile.toAbsolutePath().normalize();
|
||||
}
|
||||
|
||||
public Optional<AssetStudioGlyphMaterialSchema> findSchema(String schemaId) {
|
||||
return schemas.stream().filter(schema -> schema.schemaId().equals(schemaId)).findFirst();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
package p.studio.workspaces.assets.metadata;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record AssetStudioGlyphMaterialPalette(
|
||||
String paletteId,
|
||||
String displayName,
|
||||
String schemaId,
|
||||
int version,
|
||||
List<String> colors) {
|
||||
|
||||
public AssetStudioGlyphMaterialPalette(String paletteId, String schemaId, int version, List<String> colors) {
|
||||
this(paletteId, null, schemaId, version, colors);
|
||||
}
|
||||
|
||||
public AssetStudioGlyphMaterialPalette {
|
||||
paletteId = Objects.requireNonNull(paletteId, "paletteId").trim();
|
||||
displayName = displayName == null || displayName.trim().isBlank() ? null : displayName.trim();
|
||||
schemaId = Objects.requireNonNull(schemaId, "schemaId").trim();
|
||||
if (paletteId.isBlank()) {
|
||||
throw new IllegalArgumentException("paletteId must not be blank");
|
||||
}
|
||||
if (schemaId.isBlank()) {
|
||||
throw new IllegalArgumentException("schemaId must not be blank");
|
||||
}
|
||||
if (version <= 0) {
|
||||
throw new IllegalArgumentException("version must be positive");
|
||||
}
|
||||
colors = List.copyOf(Objects.requireNonNull(colors, "colors"));
|
||||
}
|
||||
|
||||
public List<Integer> convertedRgb565Colors() {
|
||||
final List<Integer> converted = new ArrayList<>(colors.size());
|
||||
for (String color : colors) {
|
||||
converted.add(convertHtmlRgbToRgb565(color));
|
||||
}
|
||||
return List.copyOf(converted);
|
||||
}
|
||||
|
||||
static int convertHtmlRgbToRgb565(String htmlRgb) {
|
||||
final String value = Objects.requireNonNull(htmlRgb, "htmlRgb").trim();
|
||||
if (!value.matches("^#[0-9A-Fa-f]{6}$")) {
|
||||
throw new IllegalArgumentException("htmlRgb must use #RRGGBB format");
|
||||
}
|
||||
final int rgb = Integer.parseInt(value.substring(1), 16);
|
||||
final int red = (rgb >> 16) & 0xFF;
|
||||
final int green = (rgb >> 8) & 0xFF;
|
||||
final int blue = rgb & 0xFF;
|
||||
return ((red >> 3) << 11) | ((green >> 2) << 5) | (blue >> 3);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package p.studio.workspaces.assets.metadata;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public record AssetStudioGlyphMaterialSchema(
|
||||
String schemaId,
|
||||
AssetStudioGlyphMaterialSchemaStatus status,
|
||||
int version,
|
||||
Map<Integer, String> materialIndices,
|
||||
List<AssetStudioGlyphMaterialArtifactBinding> artifactBindings) {
|
||||
|
||||
public AssetStudioGlyphMaterialSchema {
|
||||
schemaId = Objects.requireNonNull(schemaId, "schemaId").trim();
|
||||
status = Objects.requireNonNull(status, "status");
|
||||
if (schemaId.isBlank()) {
|
||||
throw new IllegalArgumentException("schemaId must not be blank");
|
||||
}
|
||||
if (version <= 0) {
|
||||
throw new IllegalArgumentException("version must be positive");
|
||||
}
|
||||
materialIndices = Map.copyOf(new LinkedHashMap<>(Objects.requireNonNull(materialIndices, "materialIndices")));
|
||||
artifactBindings = List.copyOf(Objects.requireNonNull(artifactBindings, "artifactBindings"));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package p.studio.workspaces.assets.metadata;
|
||||
|
||||
public enum AssetStudioGlyphMaterialSchemaStatus {
|
||||
OK("ok"),
|
||||
INCONSISTENT("inconsistent");
|
||||
|
||||
private final String manifestValue;
|
||||
|
||||
AssetStudioGlyphMaterialSchemaStatus(String manifestValue) {
|
||||
this.manifestValue = manifestValue;
|
||||
}
|
||||
|
||||
public String manifestValue() {
|
||||
return manifestValue;
|
||||
}
|
||||
|
||||
public static AssetStudioGlyphMaterialSchemaStatus fromManifestValue(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
for (AssetStudioGlyphMaterialSchemaStatus candidate : values()) {
|
||||
if (candidate.manifestValue.equalsIgnoreCase(value.trim())) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package p.studio.workspaces.assets.metadata;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public enum AssetStudioGlyphSpecialization {
|
||||
NONE("none"),
|
||||
TILESET("tileset"),
|
||||
SPRITES("sprites"),
|
||||
UI("ui");
|
||||
|
||||
private final String manifestValue;
|
||||
|
||||
AssetStudioGlyphSpecialization(String manifestValue) {
|
||||
this.manifestValue = manifestValue;
|
||||
}
|
||||
|
||||
public String manifestValue() {
|
||||
return manifestValue;
|
||||
}
|
||||
|
||||
public static AssetStudioGlyphSpecialization fromManifestValue(String value) {
|
||||
if (value == null) {
|
||||
return NONE;
|
||||
}
|
||||
final String normalized = value.trim().toLowerCase(Locale.ROOT);
|
||||
for (AssetStudioGlyphSpecialization candidate : values()) {
|
||||
if (candidate.manifestValue.equals(normalized)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return NONE;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,290 @@
|
||||
package p.studio.workspaces.assets.metadata;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
public final class AssetStudioMetadataService {
|
||||
public static final String STUDIO_ASSET_METADATA_FILE = "studio.asset.json";
|
||||
public static final String SCENE_BANK_SUPPORT_FILE = "scene-bank.studio.json";
|
||||
public static final String GLYPH_MATERIAL_SUPPORT_FILE = "glyph-material.json";
|
||||
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
public AssetStudioMetadataSnapshot read(Path assetRoot, AssetFamilyCatalog assetFamily) {
|
||||
final Path normalizedAssetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
|
||||
return new AssetStudioMetadataSnapshot(
|
||||
readGlyphSpecialization(normalizedAssetRoot, assetFamily),
|
||||
readSceneBankMetadata(normalizedAssetRoot, assetFamily),
|
||||
readGlyphMaterialMetadata(normalizedAssetRoot, assetFamily));
|
||||
}
|
||||
|
||||
public void writeSceneBankMetadata(Path assetRoot, AssetStudioSceneBankMetadata metadata) throws IOException {
|
||||
final Path normalizedAssetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
|
||||
final AssetStudioSceneBankMetadata normalized = Objects.requireNonNull(metadata, "metadata");
|
||||
final var root = mapper.createObjectNode();
|
||||
root.put("schema_version", 1);
|
||||
root.put("map_width", normalized.mapWidth());
|
||||
root.put("map_height", normalized.mapHeight());
|
||||
root.put("layer_count", normalized.layerCount());
|
||||
final ArrayNode layers = root.putArray("layers");
|
||||
for (AssetStudioSceneLayerBinding binding : normalized.layerBindings()) {
|
||||
final var layer = layers.addObject();
|
||||
layer.put("index", binding.index());
|
||||
layer.put("enabled", binding.enabled());
|
||||
layer.put("name", binding.layerName());
|
||||
layer.put("width", binding.width());
|
||||
layer.put("height", binding.height());
|
||||
layer.put("tilemap", binding.tilemap());
|
||||
layer.put("tileset_asset_root", binding.tilesetAssetRoot());
|
||||
}
|
||||
mapper.writerWithDefaultPrettyPrinter().writeValue(
|
||||
normalizedAssetRoot.resolve(SCENE_BANK_SUPPORT_FILE).toFile(),
|
||||
root);
|
||||
}
|
||||
|
||||
public void writeGlyphSpecialization(Path assetRoot, AssetStudioGlyphSpecialization specialization) throws IOException {
|
||||
final Path metadataPath = Objects.requireNonNull(assetRoot, "assetRoot")
|
||||
.toAbsolutePath()
|
||||
.normalize()
|
||||
.resolve(STUDIO_ASSET_METADATA_FILE);
|
||||
final AssetStudioGlyphSpecialization normalized = Objects.requireNonNullElse(
|
||||
specialization,
|
||||
AssetStudioGlyphSpecialization.NONE);
|
||||
if (normalized == AssetStudioGlyphSpecialization.NONE) {
|
||||
Files.deleteIfExists(metadataPath);
|
||||
return;
|
||||
}
|
||||
final var root = mapper.createObjectNode();
|
||||
root.put("schema_version", 1);
|
||||
root.put("glyph_bank_specialization", normalized.manifestValue());
|
||||
mapper.writerWithDefaultPrettyPrinter().writeValue(metadataPath.toFile(), root);
|
||||
}
|
||||
|
||||
public void writeGlyphMaterialMetadata(Path assetRoot, AssetStudioGlyphMaterialMetadata metadata) throws IOException {
|
||||
final Path normalizedAssetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
|
||||
final AssetStudioGlyphMaterialMetadata normalized = Objects.requireNonNull(metadata, "metadata");
|
||||
final var root = mapper.createObjectNode();
|
||||
root.put("schema_version", 1);
|
||||
final ArrayNode schemas = root.putArray("schemas");
|
||||
for (AssetStudioGlyphMaterialSchema schema : normalized.schemas()) {
|
||||
final var schemaNode = schemas.addObject();
|
||||
schemaNode.put("schema_id", schema.schemaId());
|
||||
schemaNode.put("status", schema.status().manifestValue());
|
||||
schemaNode.put("version", schema.version());
|
||||
final var materialIndicesNode = schemaNode.putObject("material_indices");
|
||||
schema.materialIndices().entrySet().stream()
|
||||
.sorted(Map.Entry.comparingByKey())
|
||||
.forEach(entry -> materialIndicesNode.put(String.valueOf(entry.getKey()), entry.getValue()));
|
||||
final ArrayNode bindingsNode = schemaNode.putArray("artifact_bindings");
|
||||
for (AssetStudioGlyphMaterialArtifactBinding binding : schema.artifactBindings()) {
|
||||
final var bindingNode = bindingsNode.addObject();
|
||||
bindingNode.put("artifact_id", binding.artifactId());
|
||||
bindingNode.put("fingerprint", binding.fingerprint());
|
||||
}
|
||||
}
|
||||
final ArrayNode palettes = root.putArray("palettes");
|
||||
for (AssetStudioGlyphMaterialPalette palette : normalized.palettes()) {
|
||||
final var paletteNode = palettes.addObject();
|
||||
paletteNode.put("palette_id", palette.paletteId());
|
||||
if (palette.displayName() != null) {
|
||||
paletteNode.put("name", palette.displayName());
|
||||
}
|
||||
paletteNode.put("schema_id", palette.schemaId());
|
||||
paletteNode.put("version", palette.version());
|
||||
final ArrayNode colorsNode = paletteNode.putArray("colors");
|
||||
palette.colors().forEach(colorsNode::add);
|
||||
}
|
||||
mapper.writerWithDefaultPrettyPrinter().writeValue(
|
||||
normalizedAssetRoot.resolve(GLYPH_MATERIAL_SUPPORT_FILE).toFile(),
|
||||
root);
|
||||
}
|
||||
|
||||
private AssetStudioGlyphSpecialization readGlyphSpecialization(Path assetRoot, AssetFamilyCatalog assetFamily) {
|
||||
if (assetFamily != AssetFamilyCatalog.GLYPH_BANK) {
|
||||
return AssetStudioGlyphSpecialization.NONE;
|
||||
}
|
||||
final Path metadataPath = assetRoot.resolve(STUDIO_ASSET_METADATA_FILE);
|
||||
if (!Files.isRegularFile(metadataPath)) {
|
||||
return AssetStudioGlyphSpecialization.NONE;
|
||||
}
|
||||
try {
|
||||
final JsonNode root = mapper.readTree(metadataPath.toFile());
|
||||
if (root == null || !root.isObject()) {
|
||||
return AssetStudioGlyphSpecialization.NONE;
|
||||
}
|
||||
return AssetStudioGlyphSpecialization.fromManifestValue(root.path("glyph_bank_specialization").asText(null));
|
||||
} catch (IOException ignored) {
|
||||
return AssetStudioGlyphSpecialization.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
private AssetStudioSceneBankMetadata readSceneBankMetadata(Path assetRoot, AssetFamilyCatalog assetFamily) {
|
||||
if (assetFamily != AssetFamilyCatalog.SCENE_BANK) {
|
||||
return null;
|
||||
}
|
||||
final Path supportFile = assetRoot.resolve(SCENE_BANK_SUPPORT_FILE);
|
||||
if (!Files.isRegularFile(supportFile)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final JsonNode root = mapper.readTree(supportFile.toFile());
|
||||
if (root == null || !root.isObject()) {
|
||||
return null;
|
||||
}
|
||||
final int mapWidth = root.path("map_width").asInt(0);
|
||||
final int mapHeight = root.path("map_height").asInt(0);
|
||||
final int layerCount = root.path("layer_count").asInt(0);
|
||||
if (mapWidth <= 0 || mapHeight <= 0 || layerCount != 4) {
|
||||
return null;
|
||||
}
|
||||
if (!(root.path("layers") instanceof ArrayNode layersNode) || layersNode.size() != layerCount) {
|
||||
return null;
|
||||
}
|
||||
final List<AssetStudioSceneLayerBinding> bindings = new ArrayList<>();
|
||||
final Set<Integer> indexes = new HashSet<>();
|
||||
for (JsonNode layerNode : layersNode) {
|
||||
final int index = layerNode.path("index").asInt(0);
|
||||
final boolean enabled = layerNode.path("enabled").asBoolean(false);
|
||||
final String layerName = layerNode.path("name").asText("").trim();
|
||||
final int width = layerNode.path("width").asInt(mapWidth);
|
||||
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();
|
||||
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));
|
||||
}
|
||||
bindings.sort(Comparator.comparingInt(AssetStudioSceneLayerBinding::index));
|
||||
return new AssetStudioSceneBankMetadata(mapWidth, mapHeight, layerCount, bindings, supportFile);
|
||||
} catch (IOException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private AssetStudioGlyphMaterialMetadata readGlyphMaterialMetadata(Path assetRoot, AssetFamilyCatalog assetFamily) {
|
||||
if (assetFamily != AssetFamilyCatalog.GLYPH_BANK) {
|
||||
return null;
|
||||
}
|
||||
final Path supportFile = assetRoot.resolve(GLYPH_MATERIAL_SUPPORT_FILE);
|
||||
if (!Files.isRegularFile(supportFile)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final JsonNode root = mapper.readTree(supportFile.toFile());
|
||||
if (root == null || !root.isObject()) {
|
||||
return null;
|
||||
}
|
||||
if (!(root.path("schemas") instanceof ArrayNode schemasNode) || !(root.path("palettes") instanceof ArrayNode palettesNode)) {
|
||||
return null;
|
||||
}
|
||||
final List<AssetStudioGlyphMaterialSchema> schemas = new ArrayList<>();
|
||||
final Set<String> schemaIds = new HashSet<>();
|
||||
for (JsonNode schemaNode : schemasNode) {
|
||||
final String schemaId = schemaNode.path("schema_id").asText("").trim();
|
||||
final AssetStudioGlyphMaterialSchemaStatus status = AssetStudioGlyphMaterialSchemaStatus.fromManifestValue(
|
||||
schemaNode.path("status").asText(null));
|
||||
final int version = schemaNode.path("version").asInt(0);
|
||||
if (schemaId.isBlank() || status == null || version <= 0 || !schemaIds.add(schemaId)) {
|
||||
return null;
|
||||
}
|
||||
final JsonNode materialIndicesNode = schemaNode.path("material_indices");
|
||||
if (!materialIndicesNode.isObject()) {
|
||||
return null;
|
||||
}
|
||||
final Map<Integer, String> materialIndices = new java.util.TreeMap<>();
|
||||
materialIndicesNode.fields().forEachRemaining(entry -> {
|
||||
try {
|
||||
final int index = Integer.parseInt(entry.getKey());
|
||||
final String color = normalizeHtmlColor(entry.getValue().asText(null));
|
||||
if (index >= 1 && index <= 15 && color != null) {
|
||||
materialIndices.put(index, color);
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
// handled below through validation
|
||||
}
|
||||
});
|
||||
if (materialIndices.isEmpty()
|
||||
|| materialIndices.size() != materialIndicesNode.size()
|
||||
|| !denseIndices(materialIndices.keySet())) {
|
||||
return null;
|
||||
}
|
||||
if (!(schemaNode.path("artifact_bindings") instanceof ArrayNode bindingsNode) || bindingsNode.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
final List<AssetStudioGlyphMaterialArtifactBinding> bindings = new ArrayList<>();
|
||||
final Set<String> artifactIds = new HashSet<>();
|
||||
for (JsonNode bindingNode : bindingsNode) {
|
||||
final String artifactId = bindingNode.path("artifact_id").asText("").trim();
|
||||
final String fingerprint = bindingNode.path("fingerprint").asText("").trim();
|
||||
if (artifactId.isBlank() || fingerprint.isBlank() || !artifactIds.add(artifactId)) {
|
||||
return null;
|
||||
}
|
||||
bindings.add(new AssetStudioGlyphMaterialArtifactBinding(artifactId, fingerprint));
|
||||
}
|
||||
schemas.add(new AssetStudioGlyphMaterialSchema(schemaId, status, version, materialIndices, bindings));
|
||||
}
|
||||
final List<AssetStudioGlyphMaterialPalette> palettes = new ArrayList<>();
|
||||
final Set<String> paletteIds = new HashSet<>();
|
||||
for (JsonNode paletteNode : palettesNode) {
|
||||
final String paletteId = paletteNode.path("palette_id").asText("").trim();
|
||||
final String displayName = paletteNode.path("name").asText(null);
|
||||
final String schemaId = paletteNode.path("schema_id").asText("").trim();
|
||||
final int version = paletteNode.path("version").asInt(0);
|
||||
if (paletteId.isBlank() || schemaId.isBlank() || version <= 0 || !paletteIds.add(paletteId) || !schemaIds.contains(schemaId)) {
|
||||
return null;
|
||||
}
|
||||
if (!(paletteNode.path("colors") instanceof ArrayNode colorsNode)) {
|
||||
return null;
|
||||
}
|
||||
final List<String> colors = new ArrayList<>();
|
||||
for (JsonNode colorNode : colorsNode) {
|
||||
final String color = normalizeHtmlColor(colorNode.asText(null));
|
||||
if (color == null) {
|
||||
return null;
|
||||
}
|
||||
colors.add(color);
|
||||
}
|
||||
palettes.add(new AssetStudioGlyphMaterialPalette(paletteId, displayName, schemaId, version, colors));
|
||||
}
|
||||
return new AssetStudioGlyphMaterialMetadata(schemas, palettes, supportFile);
|
||||
} catch (IOException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean denseIndices(Set<Integer> indices) {
|
||||
if (indices.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
int expected = 1;
|
||||
for (Integer index : new java.util.TreeSet<>(indices)) {
|
||||
if (index == null || index != expected) {
|
||||
return false;
|
||||
}
|
||||
expected += 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private String normalizeHtmlColor(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
final String normalized = value.trim().toUpperCase();
|
||||
return normalized.matches("^#[0-9A-F]{6}$") ? normalized : null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package p.studio.workspaces.assets.metadata;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record AssetStudioMetadataSnapshot(
|
||||
AssetStudioGlyphSpecialization glyphSpecialization,
|
||||
AssetStudioSceneBankMetadata sceneBankMetadata,
|
||||
AssetStudioGlyphMaterialMetadata glyphMaterialMetadata) {
|
||||
|
||||
public AssetStudioMetadataSnapshot {
|
||||
glyphSpecialization = Objects.requireNonNullElse(glyphSpecialization, AssetStudioGlyphSpecialization.NONE);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package p.studio.workspaces.assets.metadata;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record AssetStudioSceneBankMetadata(
|
||||
int mapWidth,
|
||||
int mapHeight,
|
||||
int layerCount,
|
||||
List<AssetStudioSceneLayerBinding> layerBindings,
|
||||
Path supportFile) {
|
||||
|
||||
public AssetStudioSceneBankMetadata {
|
||||
layerBindings = List.copyOf(Objects.requireNonNull(layerBindings, "layerBindings"));
|
||||
supportFile = Objects.requireNonNull(supportFile, "supportFile").toAbsolutePath().normalize();
|
||||
if (mapWidth <= 0 || mapHeight <= 0) {
|
||||
throw new IllegalArgumentException("map dimensions must be positive");
|
||||
}
|
||||
if (layerCount != 4) {
|
||||
throw new IllegalArgumentException("layerCount must stay fixed at 4");
|
||||
}
|
||||
if (layerBindings.size() != layerCount) {
|
||||
throw new IllegalArgumentException("layerBindings must match layerCount");
|
||||
}
|
||||
}
|
||||
|
||||
public long enabledLayerCount() {
|
||||
return layerBindings.stream().filter(AssetStudioSceneLayerBinding::enabled).count();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package p.studio.workspaces.assets.metadata;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record AssetStudioSceneLayerBinding(
|
||||
boolean enabled,
|
||||
int index,
|
||||
String layerName,
|
||||
int width,
|
||||
int height,
|
||||
String tilemap,
|
||||
String tilesetAssetRoot) {
|
||||
public AssetStudioSceneLayerBinding {
|
||||
layerName = Objects.requireNonNull(layerName, "layerName").trim();
|
||||
tilemap = Objects.requireNonNull(tilemap, "tilemap").trim();
|
||||
tilesetAssetRoot = Objects.requireNonNull(tilesetAssetRoot, "tilesetAssetRoot").trim();
|
||||
if (index <= 0) {
|
||||
throw new IllegalArgumentException("index must be positive");
|
||||
}
|
||||
if (layerName.isBlank()) {
|
||||
throw new IllegalArgumentException("layerName must not be blank");
|
||||
}
|
||||
if (width <= 0 || height <= 0) {
|
||||
throw new IllegalArgumentException("layer dimensions must be positive");
|
||||
}
|
||||
if (tilemap.isBlank()) {
|
||||
throw new IllegalArgumentException("tilemap must not be blank");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package p.studio.workspaces.assets.scene;
|
||||
|
||||
import p.packer.dtos.PackerDiagnosticDTO;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceSceneBankValidation;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record SceneBankWorkflowResult(
|
||||
boolean success,
|
||||
String message,
|
||||
AssetWorkspaceSceneBankValidation validation,
|
||||
List<PackerDiagnosticDTO> diagnostics) {
|
||||
|
||||
public SceneBankWorkflowResult {
|
||||
message = Objects.requireNonNullElse(message, "");
|
||||
validation = Objects.requireNonNullElse(validation, AssetWorkspaceSceneBankValidation.NOT_APPLICABLE);
|
||||
diagnostics = List.copyOf(Objects.requireNonNullElse(diagnostics, List.of()));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,391 @@
|
||||
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.diagnostics.PackerDiagnosticCategory;
|
||||
import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
|
||||
import p.studio.projects.ProjectReference;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneBankMetadata;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneLayerBinding;
|
||||
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";
|
||||
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
private final TiledXmlCodec codec = new TiledXmlCodec();
|
||||
|
||||
public SceneBankWorkflowResult inspect(ProjectReference projectReference, AssetWorkspaceAssetDetails details) {
|
||||
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, 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);
|
||||
return new SceneBankWorkflowResult(
|
||||
diagnostics.stream().noneMatch(PackerDiagnosticDTO::blocking),
|
||||
messageFor(validation),
|
||||
validation,
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private List<PackerDiagnosticDTO> validateDiagnostics(ProjectReference projectReference, AssetWorkspaceAssetDetails details, Integer targetLayerIndex) {
|
||||
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")));
|
||||
return diagnostics;
|
||||
}
|
||||
for (AssetStudioSceneLayerBinding binding : metadata.layerBindings()) {
|
||||
if (!binding.enabled()) {
|
||||
continue;
|
||||
}
|
||||
if (targetLayerIndex != null && binding.index() != targetLayerIndex) {
|
||||
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));
|
||||
continue;
|
||||
}
|
||||
if (!Files.isRegularFile(tsxPath)) {
|
||||
diagnostics.add(error("Referenced TSX file is missing for layer " + binding.index() + ": " + binding.tilesetAssetRoot(), tsxPath));
|
||||
continue;
|
||||
}
|
||||
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 (map.width() != binding.width() || map.height() != binding.height()) {
|
||||
diagnostics.add(error("TMX dimensions do not match scene-bank support metadata: " + binding.tilemap(), tmxPath));
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
files.stream()
|
||||
.distinct()
|
||||
.sorted(Comparator.comparing(Path::toString))
|
||||
.forEach(path -> updateDigest(digest, path));
|
||||
return HexFormat.of().formatHex(digest.digest());
|
||||
} catch (Exception exception) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private String computeFingerprint(ProjectReference projectReference, AssetWorkspaceAssetDetails details, int layerIndex) {
|
||||
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());
|
||||
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 "";
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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,
|
||||
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";
|
||||
}
|
||||
|
||||
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 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,
|
||||
PackerDiagnosticCategory.STRUCTURAL,
|
||||
message,
|
||||
path,
|
||||
true);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package p.studio.workspaces.assets.tiled;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record TiledAssetGenerationResult(
|
||||
boolean success,
|
||||
String message,
|
||||
List<Path> writtenFiles) {
|
||||
|
||||
public TiledAssetGenerationResult {
|
||||
message = Objects.requireNonNullElse(message, "");
|
||||
writtenFiles = List.copyOf(Objects.requireNonNullElse(writtenFiles, List.of()));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,187 @@
|
||||
package p.studio.workspaces.assets.tiled;
|
||||
|
||||
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.AssetStudioSceneBankMetadata;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneLayerBinding;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails;
|
||||
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class TiledAssetGenerationService {
|
||||
public static final String GENERATED_TSX_FILE = "tileset.tsx";
|
||||
|
||||
private final TiledXmlCodec codec = new TiledXmlCodec();
|
||||
|
||||
public TiledAssetGenerationResult generateTilesetTsx(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());
|
||||
}
|
||||
final List<AssetWorkspaceBankCompositionFile> selectedFiles = details.bankComposition().selectedFiles();
|
||||
if (selectedFiles.isEmpty()) {
|
||||
return new TiledAssetGenerationResult(false, "The selected tileset bank has no included glyph artifacts.", List.of());
|
||||
}
|
||||
final int tileSize = parseTileSize(details.metadataFields());
|
||||
final List<TiledTilesetTile> tiles = new ArrayList<>();
|
||||
for (int index = 0; index < selectedFiles.size(); index += 1) {
|
||||
final AssetWorkspaceBankCompositionFile file = selectedFiles.get(index);
|
||||
tiles.add(new TiledTilesetTile(
|
||||
index,
|
||||
file.path(),
|
||||
tileSize,
|
||||
tileSize,
|
||||
List.of(new TiledProperty("glyph_id", "int", Integer.toString(index))),
|
||||
null));
|
||||
}
|
||||
final TiledTilesetDocument document = new TiledTilesetDocument(
|
||||
"1.10",
|
||||
"1.12.1",
|
||||
details.summary().assetName(),
|
||||
tileSize,
|
||||
tileSize,
|
||||
tiles.size(),
|
||||
0,
|
||||
List.of(),
|
||||
tiles);
|
||||
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));
|
||||
} catch (IOException exception) {
|
||||
return new TiledAssetGenerationResult(false, "Unable to generate 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());
|
||||
}
|
||||
}
|
||||
|
||||
public TiledAssetGenerationResult generateSceneBankTilemap(ProjectReference projectReference, AssetWorkspaceAssetDetails details, int layerIndex) {
|
||||
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 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());
|
||||
}
|
||||
if (!binding.enabled()) {
|
||||
return new TiledAssetGenerationResult(false, "Layer " + layerIndex + " is disabled.", 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 TiledMapDocument document = new TiledMapDocument(
|
||||
"1.10",
|
||||
"1.12.1",
|
||||
"orthogonal",
|
||||
"right-down",
|
||||
binding.width(),
|
||||
binding.height(),
|
||||
tileset.tileWidth(),
|
||||
tileset.tileHeight(),
|
||||
2,
|
||||
1,
|
||||
List.of(),
|
||||
List.of(new TiledTilesetReference(1, relativeTilesetPath)),
|
||||
List.of(new TiledTileLayer(
|
||||
1,
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
private int parseTileSize(List<PackerCodecConfigurationFieldDTO> metadataFields) {
|
||||
final String value = metadataFields.stream()
|
||||
.filter(field -> "tile_size".equals(field.key()))
|
||||
.map(PackerCodecConfigurationFieldDTO::value)
|
||||
.findFirst()
|
||||
.orElse("16x16");
|
||||
final String normalized = value.trim().toLowerCase();
|
||||
if (normalized.startsWith("8x8")) {
|
||||
return 8;
|
||||
}
|
||||
if (normalized.startsWith("32x32")) {
|
||||
return 32;
|
||||
}
|
||||
return 16;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package p.studio.workspaces.assets.tiled;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record TiledMapDocument(
|
||||
String version,
|
||||
String tiledVersion,
|
||||
String orientation,
|
||||
String renderOrder,
|
||||
int width,
|
||||
int height,
|
||||
int tileWidth,
|
||||
int tileHeight,
|
||||
int nextLayerId,
|
||||
int nextObjectId,
|
||||
List<TiledProperty> properties,
|
||||
List<TiledTilesetReference> tilesets,
|
||||
List<TiledTileLayer> tileLayers,
|
||||
List<TiledObjectLayer> objectLayers) {
|
||||
|
||||
public TiledMapDocument {
|
||||
version = Objects.requireNonNullElse(version, "1.10");
|
||||
tiledVersion = Objects.requireNonNullElse(tiledVersion, "1.12.1");
|
||||
orientation = Objects.requireNonNullElse(orientation, "orthogonal");
|
||||
renderOrder = Objects.requireNonNullElse(renderOrder, "right-down");
|
||||
properties = List.copyOf(Objects.requireNonNull(properties, "properties"));
|
||||
tilesets = List.copyOf(Objects.requireNonNull(tilesets, "tilesets"));
|
||||
tileLayers = List.copyOf(Objects.requireNonNull(tileLayers, "tileLayers"));
|
||||
objectLayers = List.copyOf(Objects.requireNonNull(objectLayers, "objectLayers"));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package p.studio.workspaces.assets.tiled;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record TiledObjectData(
|
||||
int id,
|
||||
String name,
|
||||
double x,
|
||||
double y,
|
||||
double width,
|
||||
double height,
|
||||
List<TiledPoint> polygonPoints,
|
||||
List<TiledProperty> properties) {
|
||||
|
||||
public TiledObjectData {
|
||||
name = Objects.requireNonNullElse(name, "").trim();
|
||||
polygonPoints = List.copyOf(Objects.requireNonNull(polygonPoints, "polygonPoints"));
|
||||
properties = List.copyOf(Objects.requireNonNull(properties, "properties"));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package p.studio.workspaces.assets.tiled;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record TiledObjectLayer(
|
||||
int id,
|
||||
String name,
|
||||
List<TiledObjectData> objects,
|
||||
List<TiledProperty> properties) {
|
||||
|
||||
public TiledObjectLayer {
|
||||
name = Objects.requireNonNullElse(name, "").trim();
|
||||
objects = List.copyOf(Objects.requireNonNull(objects, "objects"));
|
||||
properties = List.copyOf(Objects.requireNonNull(properties, "properties"));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
package p.studio.workspaces.assets.tiled;
|
||||
|
||||
public record TiledPoint(double x, double y) {
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package p.studio.workspaces.assets.tiled;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record TiledProperty(String name, String type, String value) {
|
||||
public TiledProperty {
|
||||
name = Objects.requireNonNull(name, "name").trim();
|
||||
type = Objects.requireNonNullElse(type, "string").trim();
|
||||
value = Objects.requireNonNullElse(value, "").trim();
|
||||
if (name.isBlank()) {
|
||||
throw new IllegalArgumentException("name must not be blank");
|
||||
}
|
||||
if (type.isBlank()) {
|
||||
type = "string";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package p.studio.workspaces.assets.tiled;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record TiledTileLayer(
|
||||
int id,
|
||||
String name,
|
||||
int width,
|
||||
int height,
|
||||
List<Long> gids,
|
||||
List<TiledProperty> properties) {
|
||||
|
||||
public TiledTileLayer {
|
||||
name = Objects.requireNonNullElse(name, "").trim();
|
||||
gids = List.copyOf(Objects.requireNonNull(gids, "gids"));
|
||||
properties = List.copyOf(Objects.requireNonNull(properties, "properties"));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package p.studio.workspaces.assets.tiled;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record TiledTilesetDocument(
|
||||
String version,
|
||||
String tiledVersion,
|
||||
String name,
|
||||
int tileWidth,
|
||||
int tileHeight,
|
||||
int tileCount,
|
||||
int columns,
|
||||
List<TiledProperty> properties,
|
||||
List<TiledTilesetTile> tiles) {
|
||||
|
||||
public TiledTilesetDocument {
|
||||
version = Objects.requireNonNullElse(version, "1.10");
|
||||
tiledVersion = Objects.requireNonNullElse(tiledVersion, "1.12.1");
|
||||
name = Objects.requireNonNull(name, "name").trim();
|
||||
properties = List.copyOf(Objects.requireNonNull(properties, "properties"));
|
||||
tiles = List.copyOf(Objects.requireNonNull(tiles, "tiles"));
|
||||
if (name.isBlank()) {
|
||||
throw new IllegalArgumentException("name must not be blank");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package p.studio.workspaces.assets.tiled;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record TiledTilesetReference(int firstGid, String source) {
|
||||
public TiledTilesetReference {
|
||||
source = Objects.requireNonNull(source, "source").trim();
|
||||
if (firstGid <= 0) {
|
||||
throw new IllegalArgumentException("firstGid must be positive");
|
||||
}
|
||||
if (source.isBlank()) {
|
||||
throw new IllegalArgumentException("source must not be blank");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package p.studio.workspaces.assets.tiled;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record TiledTilesetTile(
|
||||
int id,
|
||||
String imageSource,
|
||||
int imageWidth,
|
||||
int imageHeight,
|
||||
List<TiledProperty> properties,
|
||||
TiledObjectLayer collisionLayer) {
|
||||
|
||||
public TiledTilesetTile {
|
||||
imageSource = Objects.requireNonNull(imageSource, "imageSource").trim();
|
||||
properties = List.copyOf(Objects.requireNonNull(properties, "properties"));
|
||||
if (imageSource.isBlank()) {
|
||||
throw new IllegalArgumentException("imageSource must not be blank");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package p.studio.workspaces.assets.tiled;
|
||||
|
||||
public final class TiledUnsupportedFeatureException extends RuntimeException {
|
||||
public TiledUnsupportedFeatureException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,460 @@
|
||||
package p.studio.workspaces.assets.tiled;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.transform.OutputKeys;
|
||||
import javax.xml.transform.TransformerFactory;
|
||||
import javax.xml.transform.dom.DOMSource;
|
||||
import javax.xml.transform.stream.StreamResult;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class TiledXmlCodec {
|
||||
public TiledMapDocument readMap(Path path) throws IOException {
|
||||
final Document document = parseDocument(path);
|
||||
final Element root = document.getDocumentElement();
|
||||
if (!"map".equals(root.getTagName())) {
|
||||
throw new IOException("TMX root element must be <map>.");
|
||||
}
|
||||
if (!"0".equals(root.getAttribute("infinite")) && !root.getAttribute("infinite").isBlank()) {
|
||||
throw new TiledUnsupportedFeatureException("Infinite maps are not supported in wave 1.");
|
||||
}
|
||||
rejectMapUnsupportedChildren(root);
|
||||
return new TiledMapDocument(
|
||||
root.getAttribute("version"),
|
||||
root.getAttribute("tiledversion"),
|
||||
root.getAttribute("orientation"),
|
||||
root.getAttribute("renderorder"),
|
||||
intAttribute(root, "width", 0),
|
||||
intAttribute(root, "height", 0),
|
||||
intAttribute(root, "tilewidth", 0),
|
||||
intAttribute(root, "tileheight", 0),
|
||||
intAttribute(root, "nextlayerid", 0),
|
||||
intAttribute(root, "nextobjectid", 0),
|
||||
readProperties(child(root, "properties")),
|
||||
readTilesets(root),
|
||||
readTileLayers(root),
|
||||
readObjectLayers(root));
|
||||
}
|
||||
|
||||
public void writeMap(Path path, TiledMapDocument map) throws IOException {
|
||||
final Document document = newDocument();
|
||||
final Element root = document.createElement("map");
|
||||
document.appendChild(root);
|
||||
root.setAttribute("version", map.version());
|
||||
root.setAttribute("tiledversion", map.tiledVersion());
|
||||
root.setAttribute("orientation", map.orientation());
|
||||
root.setAttribute("renderorder", map.renderOrder());
|
||||
root.setAttribute("width", Integer.toString(map.width()));
|
||||
root.setAttribute("height", Integer.toString(map.height()));
|
||||
root.setAttribute("tilewidth", Integer.toString(map.tileWidth()));
|
||||
root.setAttribute("tileheight", Integer.toString(map.tileHeight()));
|
||||
root.setAttribute("infinite", "0");
|
||||
root.setAttribute("nextlayerid", Integer.toString(map.nextLayerId()));
|
||||
root.setAttribute("nextobjectid", Integer.toString(map.nextObjectId()));
|
||||
appendProperties(document, root, map.properties());
|
||||
for (TiledTilesetReference tileset : map.tilesets()) {
|
||||
final Element tilesetElement = document.createElement("tileset");
|
||||
tilesetElement.setAttribute("firstgid", Integer.toString(tileset.firstGid()));
|
||||
tilesetElement.setAttribute("source", tileset.source());
|
||||
root.appendChild(tilesetElement);
|
||||
}
|
||||
for (TiledTileLayer layer : map.tileLayers()) {
|
||||
final Element layerElement = document.createElement("layer");
|
||||
layerElement.setAttribute("id", Integer.toString(layer.id()));
|
||||
layerElement.setAttribute("name", layer.name());
|
||||
layerElement.setAttribute("width", Integer.toString(layer.width()));
|
||||
layerElement.setAttribute("height", Integer.toString(layer.height()));
|
||||
appendProperties(document, layerElement, layer.properties());
|
||||
final Element dataElement = document.createElement("data");
|
||||
dataElement.setAttribute("encoding", "csv");
|
||||
dataElement.setTextContent(csv(layer.gids(), layer.width()));
|
||||
layerElement.appendChild(dataElement);
|
||||
root.appendChild(layerElement);
|
||||
}
|
||||
for (TiledObjectLayer layer : map.objectLayers()) {
|
||||
root.appendChild(writeObjectLayer(document, layer, false));
|
||||
}
|
||||
writeDocument(path, document);
|
||||
}
|
||||
|
||||
public TiledTilesetDocument readTileset(Path path) throws IOException {
|
||||
final Document document = parseDocument(path);
|
||||
final Element root = document.getDocumentElement();
|
||||
if (!"tileset".equals(root.getTagName())) {
|
||||
throw new IOException("TSX root element must be <tileset>.");
|
||||
}
|
||||
rejectTilesetUnsupportedChildren(root);
|
||||
final List<TiledTilesetTile> tiles = new ArrayList<>();
|
||||
for (Element tileElement : children(root, "tile")) {
|
||||
if (child(tileElement, "animation") != null) {
|
||||
throw new TiledUnsupportedFeatureException("Tiled animations are not supported in wave 1.");
|
||||
}
|
||||
final Element imageElement = child(tileElement, "image");
|
||||
if (imageElement == null) {
|
||||
throw new IOException("TSX tile must contain an <image> element.");
|
||||
}
|
||||
tiles.add(new TiledTilesetTile(
|
||||
intAttribute(tileElement, "id", 0),
|
||||
imageElement.getAttribute("source"),
|
||||
intAttribute(imageElement, "width", 0),
|
||||
intAttribute(imageElement, "height", 0),
|
||||
readProperties(child(tileElement, "properties")),
|
||||
readEmbeddedObjectLayer(tileElement)));
|
||||
}
|
||||
tiles.sort(Comparator.comparingInt(TiledTilesetTile::id));
|
||||
return new TiledTilesetDocument(
|
||||
root.getAttribute("version"),
|
||||
root.getAttribute("tiledversion"),
|
||||
root.getAttribute("name"),
|
||||
intAttribute(root, "tilewidth", 0),
|
||||
intAttribute(root, "tileheight", 0),
|
||||
intAttribute(root, "tilecount", tiles.size()),
|
||||
intAttribute(root, "columns", 0),
|
||||
readProperties(child(root, "properties")),
|
||||
tiles);
|
||||
}
|
||||
|
||||
public void writeTileset(Path path, TiledTilesetDocument tileset) throws IOException {
|
||||
final Document document = newDocument();
|
||||
final Element root = document.createElement("tileset");
|
||||
document.appendChild(root);
|
||||
root.setAttribute("version", tileset.version());
|
||||
root.setAttribute("tiledversion", tileset.tiledVersion());
|
||||
root.setAttribute("name", tileset.name());
|
||||
root.setAttribute("tilewidth", Integer.toString(tileset.tileWidth()));
|
||||
root.setAttribute("tileheight", Integer.toString(tileset.tileHeight()));
|
||||
root.setAttribute("tilecount", Integer.toString(tileset.tileCount()));
|
||||
root.setAttribute("columns", Integer.toString(tileset.columns()));
|
||||
final Element grid = document.createElement("grid");
|
||||
grid.setAttribute("orientation", "orthogonal");
|
||||
grid.setAttribute("width", "1");
|
||||
grid.setAttribute("height", "1");
|
||||
root.appendChild(grid);
|
||||
appendProperties(document, root, tileset.properties());
|
||||
for (TiledTilesetTile tile : tileset.tiles()) {
|
||||
final Element tileElement = document.createElement("tile");
|
||||
tileElement.setAttribute("id", Integer.toString(tile.id()));
|
||||
appendProperties(document, tileElement, tile.properties());
|
||||
final Element imageElement = document.createElement("image");
|
||||
imageElement.setAttribute("source", tile.imageSource());
|
||||
imageElement.setAttribute("width", Integer.toString(tile.imageWidth()));
|
||||
imageElement.setAttribute("height", Integer.toString(tile.imageHeight()));
|
||||
tileElement.appendChild(imageElement);
|
||||
if (tile.collisionLayer() != null) {
|
||||
tileElement.appendChild(writeObjectLayer(document, tile.collisionLayer(), true));
|
||||
}
|
||||
root.appendChild(tileElement);
|
||||
}
|
||||
writeDocument(path, document);
|
||||
}
|
||||
|
||||
private Document parseDocument(Path path) throws IOException {
|
||||
try {
|
||||
final var factory = DocumentBuilderFactory.newInstance();
|
||||
factory.setNamespaceAware(false);
|
||||
return factory.newDocumentBuilder().parse(path.toFile());
|
||||
} catch (TiledUnsupportedFeatureException exception) {
|
||||
throw exception;
|
||||
} catch (Exception exception) {
|
||||
throw new IOException("Unable to parse Tiled XML: " + exception.getMessage(), exception);
|
||||
}
|
||||
}
|
||||
|
||||
private Document newDocument() throws IOException {
|
||||
try {
|
||||
return DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
|
||||
} catch (Exception exception) {
|
||||
throw new IOException("Unable to create XML document: " + exception.getMessage(), exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeDocument(Path path, Document document) throws IOException {
|
||||
try {
|
||||
Files.createDirectories(Objects.requireNonNull(path, "path").toAbsolutePath().normalize().getParent());
|
||||
final var transformer = TransformerFactory.newInstance().newTransformer();
|
||||
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
|
||||
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
|
||||
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
|
||||
transformer.transform(new DOMSource(document), new StreamResult(path.toFile()));
|
||||
} catch (Exception exception) {
|
||||
throw new IOException("Unable to write Tiled XML: " + exception.getMessage(), exception);
|
||||
}
|
||||
}
|
||||
|
||||
private void rejectMapUnsupportedChildren(Element root) {
|
||||
for (Element child : childElements(root)) {
|
||||
final String tag = child.getTagName();
|
||||
if ("imagelayer".equals(tag) || "group".equals(tag) || "template".equals(tag)) {
|
||||
throw new TiledUnsupportedFeatureException("Unsupported TMX feature in wave 1: " + tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void rejectTilesetUnsupportedChildren(Element root) {
|
||||
for (Element child : childElements(root)) {
|
||||
final String tag = child.getTagName();
|
||||
if ("wangsets".equals(tag) || "tileoffset".equals(tag) || "transformations".equals(tag)) {
|
||||
throw new TiledUnsupportedFeatureException("Unsupported TSX feature in wave 1: " + tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<TiledTilesetReference> readTilesets(Element root) {
|
||||
final List<TiledTilesetReference> tilesets = new ArrayList<>();
|
||||
for (Element tilesetElement : children(root, "tileset")) {
|
||||
if (!tilesetElement.hasAttribute("source")) {
|
||||
throw new TiledUnsupportedFeatureException("Inline tilesets are not supported in wave 1.");
|
||||
}
|
||||
tilesets.add(new TiledTilesetReference(
|
||||
intAttribute(tilesetElement, "firstgid", 1),
|
||||
tilesetElement.getAttribute("source")));
|
||||
}
|
||||
return List.copyOf(tilesets);
|
||||
}
|
||||
|
||||
private List<TiledTileLayer> readTileLayers(Element root) {
|
||||
final List<TiledTileLayer> layers = new ArrayList<>();
|
||||
for (Element layerElement : children(root, "layer")) {
|
||||
final Element dataElement = child(layerElement, "data");
|
||||
if (dataElement == null || !"csv".equalsIgnoreCase(dataElement.getAttribute("encoding"))) {
|
||||
throw new TiledUnsupportedFeatureException("Wave 1 supports only CSV tile layer encoding.");
|
||||
}
|
||||
layers.add(new TiledTileLayer(
|
||||
intAttribute(layerElement, "id", 0),
|
||||
layerElement.getAttribute("name"),
|
||||
intAttribute(layerElement, "width", 0),
|
||||
intAttribute(layerElement, "height", 0),
|
||||
parseCsvData(dataElement.getTextContent()),
|
||||
readProperties(child(layerElement, "properties"))));
|
||||
}
|
||||
return List.copyOf(layers);
|
||||
}
|
||||
|
||||
private List<TiledObjectLayer> readObjectLayers(Element root) {
|
||||
final List<TiledObjectLayer> layers = new ArrayList<>();
|
||||
for (Element objectGroup : children(root, "objectgroup")) {
|
||||
layers.add(readObjectLayer(objectGroup));
|
||||
}
|
||||
return List.copyOf(layers);
|
||||
}
|
||||
|
||||
private TiledObjectLayer readEmbeddedObjectLayer(Element parent) {
|
||||
final Element objectGroup = child(parent, "objectgroup");
|
||||
return objectGroup == null ? null : readObjectLayer(objectGroup);
|
||||
}
|
||||
|
||||
private TiledObjectLayer readObjectLayer(Element objectGroup) {
|
||||
final List<TiledObjectData> objects = new ArrayList<>();
|
||||
for (Element objectElement : children(objectGroup, "object")) {
|
||||
final Element polygon = child(objectElement, "polygon");
|
||||
if (child(objectElement, "ellipse") != null || child(objectElement, "point") != null || child(objectElement, "polyline") != null) {
|
||||
throw new TiledUnsupportedFeatureException("Unsupported object-layer geometry in wave 1.");
|
||||
}
|
||||
objects.add(new TiledObjectData(
|
||||
intAttribute(objectElement, "id", 0),
|
||||
objectElement.getAttribute("name"),
|
||||
doubleAttribute(objectElement, "x", 0.0d),
|
||||
doubleAttribute(objectElement, "y", 0.0d),
|
||||
doubleAttribute(objectElement, "width", 0.0d),
|
||||
doubleAttribute(objectElement, "height", 0.0d),
|
||||
polygon == null ? List.of() : parsePolygon(polygon.getAttribute("points")),
|
||||
readProperties(child(objectElement, "properties"))));
|
||||
}
|
||||
return new TiledObjectLayer(
|
||||
intAttribute(objectGroup, "id", 0),
|
||||
objectGroup.getAttribute("name"),
|
||||
objects,
|
||||
readProperties(child(objectGroup, "properties")));
|
||||
}
|
||||
|
||||
private Element writeObjectLayer(Document document, TiledObjectLayer layer, boolean embedded) {
|
||||
final Element objectGroup = document.createElement("objectgroup");
|
||||
if (layer.id() > 0) {
|
||||
objectGroup.setAttribute("id", Integer.toString(layer.id()));
|
||||
}
|
||||
if (!layer.name().isBlank()) {
|
||||
objectGroup.setAttribute("name", layer.name());
|
||||
}
|
||||
if (embedded) {
|
||||
objectGroup.setAttribute("draworder", "index");
|
||||
}
|
||||
appendProperties(document, objectGroup, layer.properties());
|
||||
for (TiledObjectData object : layer.objects()) {
|
||||
final Element objectElement = document.createElement("object");
|
||||
if (object.id() > 0) {
|
||||
objectElement.setAttribute("id", Integer.toString(object.id()));
|
||||
}
|
||||
if (!object.name().isBlank()) {
|
||||
objectElement.setAttribute("name", object.name());
|
||||
}
|
||||
objectElement.setAttribute("x", formatDecimal(object.x()));
|
||||
objectElement.setAttribute("y", formatDecimal(object.y()));
|
||||
if (object.width() > 0.0d) {
|
||||
objectElement.setAttribute("width", formatDecimal(object.width()));
|
||||
}
|
||||
if (object.height() > 0.0d) {
|
||||
objectElement.setAttribute("height", formatDecimal(object.height()));
|
||||
}
|
||||
appendProperties(document, objectElement, object.properties());
|
||||
if (!object.polygonPoints().isEmpty()) {
|
||||
final Element polygon = document.createElement("polygon");
|
||||
polygon.setAttribute("points", polygonPoints(object.polygonPoints()));
|
||||
objectElement.appendChild(polygon);
|
||||
}
|
||||
objectGroup.appendChild(objectElement);
|
||||
}
|
||||
return objectGroup;
|
||||
}
|
||||
|
||||
private void appendProperties(Document document, Element parent, List<TiledProperty> properties) {
|
||||
if (properties.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
final Element propertiesElement = document.createElement("properties");
|
||||
for (TiledProperty property : properties) {
|
||||
final Element propertyElement = document.createElement("property");
|
||||
propertyElement.setAttribute("name", property.name());
|
||||
if (!"string".equals(property.type())) {
|
||||
propertyElement.setAttribute("type", property.type());
|
||||
}
|
||||
propertyElement.setAttribute("value", property.value());
|
||||
propertiesElement.appendChild(propertyElement);
|
||||
}
|
||||
parent.appendChild(propertiesElement);
|
||||
}
|
||||
|
||||
private List<TiledProperty> readProperties(Element propertiesElement) {
|
||||
if (propertiesElement == null) {
|
||||
return List.of();
|
||||
}
|
||||
final List<TiledProperty> properties = new ArrayList<>();
|
||||
for (Element propertyElement : children(propertiesElement, "property")) {
|
||||
properties.add(new TiledProperty(
|
||||
propertyElement.getAttribute("name"),
|
||||
propertyElement.getAttribute("type"),
|
||||
propertyElement.hasAttribute("value")
|
||||
? propertyElement.getAttribute("value")
|
||||
: propertyElement.getTextContent()));
|
||||
}
|
||||
return List.copyOf(properties);
|
||||
}
|
||||
|
||||
private List<Long> parseCsvData(String text) {
|
||||
final List<Long> gids = new ArrayList<>();
|
||||
for (String token : Objects.requireNonNullElse(text, "").split(",")) {
|
||||
final String normalized = token.trim();
|
||||
if (normalized.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
gids.add(Long.parseLong(normalized));
|
||||
}
|
||||
return List.copyOf(gids);
|
||||
}
|
||||
|
||||
private List<TiledPoint> parsePolygon(String points) {
|
||||
final List<TiledPoint> parsed = new ArrayList<>();
|
||||
for (String segment : Objects.requireNonNullElse(points, "").trim().split(" ")) {
|
||||
final String normalized = segment.trim();
|
||||
if (normalized.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
final String[] pair = normalized.split(",");
|
||||
if (pair.length != 2) {
|
||||
throw new TiledUnsupportedFeatureException("Invalid polygon point format in Tiled XML.");
|
||||
}
|
||||
parsed.add(new TiledPoint(Double.parseDouble(pair[0]), Double.parseDouble(pair[1])));
|
||||
}
|
||||
return List.copyOf(parsed);
|
||||
}
|
||||
|
||||
private String csv(List<Long> gids, int width) {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
for (int index = 0; index < gids.size(); index += 1) {
|
||||
if (index > 0) {
|
||||
builder.append(index % Math.max(width, 1) == 0 ? ",\n" : ",");
|
||||
}
|
||||
builder.append(Long.toUnsignedString(gids.get(index)));
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String polygonPoints(List<TiledPoint> points) {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
for (int index = 0; index < points.size(); index += 1) {
|
||||
if (index > 0) {
|
||||
builder.append(' ');
|
||||
}
|
||||
builder.append(formatDecimal(points.get(index).x()))
|
||||
.append(',')
|
||||
.append(formatDecimal(points.get(index).y()));
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private int intAttribute(Element element, String attribute, int fallback) {
|
||||
final String value = element.getAttribute(attribute);
|
||||
if (value == null || value.isBlank()) {
|
||||
return fallback;
|
||||
}
|
||||
return Integer.parseInt(value.trim());
|
||||
}
|
||||
|
||||
private double doubleAttribute(Element element, String attribute, double fallback) {
|
||||
final String value = element.getAttribute(attribute);
|
||||
if (value == null || value.isBlank()) {
|
||||
return fallback;
|
||||
}
|
||||
return Double.parseDouble(value.trim());
|
||||
}
|
||||
|
||||
private String formatDecimal(double value) {
|
||||
if (Math.rint(value) == value) {
|
||||
return Long.toString(Math.round(value));
|
||||
}
|
||||
return String.format(Locale.ROOT, "%.6f", value)
|
||||
.replaceAll("0+$", "")
|
||||
.replaceAll("\\.$", "");
|
||||
}
|
||||
|
||||
private Element child(Element parent, String name) {
|
||||
for (Element child : childElements(parent)) {
|
||||
if (name.equals(child.getTagName())) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<Element> children(Element parent, String name) {
|
||||
final List<Element> elements = new ArrayList<>();
|
||||
for (Element child : childElements(parent)) {
|
||||
if (name.equals(child.getTagName())) {
|
||||
elements.add(child);
|
||||
}
|
||||
}
|
||||
return List.copyOf(elements);
|
||||
}
|
||||
|
||||
private List<Element> childElements(Element parent) {
|
||||
final NodeList children = parent.getChildNodes();
|
||||
final List<Element> elements = new ArrayList<>();
|
||||
for (int index = 0; index < children.getLength(); index += 1) {
|
||||
final Node node = children.item(index);
|
||||
if (node instanceof Element element) {
|
||||
elements.add(element);
|
||||
}
|
||||
}
|
||||
return List.copyOf(elements);
|
||||
}
|
||||
}
|
||||
@ -24,6 +24,8 @@ import p.studio.Container;
|
||||
import p.studio.projects.ProjectReference;
|
||||
import p.studio.utilities.i18n.I18n;
|
||||
import p.studio.workspaces.assets.details.AssetDetailsUiSupport;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioMetadataService;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@ -47,9 +49,11 @@ public final class AddAssetWizard {
|
||||
private final TextField assetRootField = new TextField();
|
||||
private final TextField assetNameField = new TextField();
|
||||
private final ComboBox<AssetFamilyCatalog> assetFamilyCombo = new ComboBox<>();
|
||||
private final ComboBox<AssetStudioGlyphSpecialization> glyphSpecializationCombo = new ComboBox<>();
|
||||
private final ComboBox<OutputFormatCatalog> outputFormatCombo = new ComboBox<>();
|
||||
private final ComboBox<OutputCodecCatalog> outputCodecCombo = new ComboBox<>();
|
||||
private final CheckBox preloadCheckBox = new CheckBox();
|
||||
private final AssetStudioMetadataService studioMetadataService = new AssetStudioMetadataService();
|
||||
|
||||
private int stepIndex;
|
||||
private boolean creating;
|
||||
@ -65,6 +69,7 @@ public final class AddAssetWizard {
|
||||
|
||||
preloadCheckBox.setSelected(false);
|
||||
configureAssetFamilyCombo();
|
||||
configureGlyphSpecializationCombo();
|
||||
configureOutputFormatCombo();
|
||||
configureOutputCodecCombo();
|
||||
renderStep();
|
||||
@ -131,7 +136,39 @@ public final class AddAssetWizard {
|
||||
});
|
||||
assetFamilyCombo.valueProperty().addListener((ignored, oldValue, newValue) -> {
|
||||
if (!Objects.equals(oldValue, newValue)) {
|
||||
if (newValue != AssetFamilyCatalog.GLYPH_BANK) {
|
||||
glyphSpecializationCombo.getSelectionModel().clearSelection();
|
||||
} else if (glyphSpecializationCombo.getValue() == null) {
|
||||
glyphSpecializationCombo.getSelectionModel().select(AssetStudioGlyphSpecialization.SPRITES);
|
||||
}
|
||||
refreshOutputFormats();
|
||||
if (stepIndex == 1) {
|
||||
renderStep();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void configureGlyphSpecializationCombo() {
|
||||
glyphSpecializationCombo.setItems(FXCollections.observableArrayList(
|
||||
java.util.Arrays.stream(AssetStudioGlyphSpecialization.values())
|
||||
.filter(candidate -> candidate != AssetStudioGlyphSpecialization.NONE)
|
||||
.toList()));
|
||||
glyphSpecializationCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
glyphSpecializationCombo.setPromptText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_PROMPT_SPECIALIZATION));
|
||||
glyphSpecializationCombo.getSelectionModel().select(AssetStudioGlyphSpecialization.SPRITES);
|
||||
glyphSpecializationCombo.setCellFactory(ignored -> new javafx.scene.control.ListCell<>() {
|
||||
@Override
|
||||
protected void updateItem(AssetStudioGlyphSpecialization item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
setText(empty || item == null ? null : AssetDetailsUiSupport.specializationLabel(item));
|
||||
}
|
||||
});
|
||||
glyphSpecializationCombo.setButtonCell(new javafx.scene.control.ListCell<>() {
|
||||
@Override
|
||||
protected void updateItem(AssetStudioGlyphSpecialization item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
setText(empty || item == null ? null : AssetDetailsUiSupport.specializationLabel(item));
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -224,17 +261,23 @@ public final class AddAssetWizard {
|
||||
|
||||
final Label nameLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_NAME));
|
||||
final Label typeLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_TYPE));
|
||||
final Label specializationLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_SPECIALIZATION));
|
||||
final Label formatLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_FORMAT));
|
||||
final Label codecLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_CODEC));
|
||||
final Label preloadLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_LABEL_PRELOAD));
|
||||
final Label noteLabel = new Label(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_NOTE));
|
||||
noteLabel.setWrapText(true);
|
||||
noteLabel.getStyleClass().add("studio-launcher-subtitle");
|
||||
final VBox specializationBox = new VBox(6, specializationLabel, glyphSpecializationCombo);
|
||||
final boolean specializationVisible = selectedFamily() == AssetFamilyCatalog.GLYPH_BANK;
|
||||
specializationBox.setVisible(specializationVisible);
|
||||
specializationBox.setManaged(specializationVisible);
|
||||
|
||||
preloadCheckBox.setText("");
|
||||
stepBody.getChildren().setAll(
|
||||
new VBox(6, nameLabel, assetNameField),
|
||||
new VBox(6, typeLabel, assetFamilyCombo),
|
||||
specializationBox,
|
||||
new VBox(6, formatLabel, outputFormatCombo),
|
||||
new VBox(6, codecLabel, outputCodecCombo),
|
||||
new VBox(6, preloadLabel, preloadCheckBox),
|
||||
@ -293,6 +336,11 @@ public final class AddAssetWizard {
|
||||
feedbackLabel.setText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_ERROR_TYPE));
|
||||
return false;
|
||||
}
|
||||
if (selectedFamily() == AssetFamilyCatalog.GLYPH_BANK
|
||||
&& (selectedGlyphSpecialization() == null || selectedGlyphSpecialization() == AssetStudioGlyphSpecialization.NONE)) {
|
||||
feedbackLabel.setText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_ERROR_SPECIALIZATION));
|
||||
return false;
|
||||
}
|
||||
if (selectedFormat() == null || selectedFormat() == OutputFormatCatalog.UNKNOWN) {
|
||||
feedbackLabel.setText(Container.i18n().text(I18n.ASSETS_ADD_WIZARD_ERROR_FORMAT));
|
||||
return false;
|
||||
@ -360,6 +408,15 @@ public final class AddAssetWizard {
|
||||
private void applyCreateResult(CreateAssetResult createResult) {
|
||||
creating = false;
|
||||
if (createResult.status() == PackerOperationStatus.SUCCESS && createResult.assetReference() != null) {
|
||||
try {
|
||||
persistStudioMetadata(createResult.assetRoot());
|
||||
} catch (IOException exception) {
|
||||
feedbackLabel.setText(exception.getMessage() == null || exception.getMessage().isBlank()
|
||||
? "Unable to persist Studio metadata."
|
||||
: exception.getMessage());
|
||||
renderStep();
|
||||
return;
|
||||
}
|
||||
result.set(createResult.assetReference());
|
||||
stage.close();
|
||||
return;
|
||||
@ -409,6 +466,17 @@ public final class AddAssetWizard {
|
||||
return outputCodecCombo.getValue();
|
||||
}
|
||||
|
||||
private AssetStudioGlyphSpecialization selectedGlyphSpecialization() {
|
||||
return glyphSpecializationCombo.getValue();
|
||||
}
|
||||
|
||||
private void persistStudioMetadata(Path assetRoot) throws IOException {
|
||||
if (selectedFamily() != AssetFamilyCatalog.GLYPH_BANK) {
|
||||
return;
|
||||
}
|
||||
studioMetadataService.writeGlyphSpecialization(assetRoot, selectedGlyphSpecialization());
|
||||
}
|
||||
|
||||
private String normalizedRelativeRoot(String candidate) {
|
||||
final String raw = Objects.requireNonNullElse(candidate, "").trim().replace('\\', '/');
|
||||
if (raw.isBlank()) {
|
||||
|
||||
@ -135,6 +135,9 @@ assets.badge.diagnostics=Diagnostics
|
||||
assets.section.summary=Summary
|
||||
assets.section.runtimeContract=Runtime Contract
|
||||
assets.section.bankComposition=Bank Composition
|
||||
assets.section.sceneLayers=Scene Layers
|
||||
assets.section.colorSchemaGroups=Color Schema Groups
|
||||
assets.section.layerConfiguration=Layer Configuration
|
||||
assets.section.paletteOverhauling=Palette Overhauling
|
||||
assets.subsection.codecConfiguration=Codec Configuration
|
||||
assets.subsection.metadata=Metadata
|
||||
@ -144,6 +147,12 @@ 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.validateSceneBank=Validate Scene Bank
|
||||
assets.action.acceptSceneBank=Accept Scene Bank
|
||||
assets.action.validateLayer=Validate Layer
|
||||
assets.action.acceptLayer=Accept Layer
|
||||
assets.action.delete=Delete
|
||||
assets.deleteDialog.title=Delete Asset
|
||||
assets.deleteDialog.description=Type the asset name below to confirm deletion of {0}.
|
||||
@ -181,10 +190,30 @@ assets.label.registration=Registration
|
||||
assets.label.buildParticipation=Build Participation
|
||||
assets.label.assetId=Asset ID
|
||||
assets.label.type=Type
|
||||
assets.label.studioRole=Studio Role
|
||||
assets.label.sceneStatus=Scene Status
|
||||
assets.label.sceneLayers=Scene Layers
|
||||
assets.label.sceneDimensions=Layer Size
|
||||
assets.label.layer=Layer
|
||||
assets.label.layerOverview=Layer Overview
|
||||
assets.label.tilemaps=Tilemaps
|
||||
assets.label.tilemap=Tilemap
|
||||
assets.label.tileset=Tileset
|
||||
assets.label.supportFile=Support File
|
||||
assets.label.useLayer=Use this layer
|
||||
assets.type.glyphBank=Glyph Bank
|
||||
assets.type.sceneBank=Scene Bank
|
||||
assets.type.paletteBank=Palette Bank
|
||||
assets.type.soundBank=Sound Bank
|
||||
assets.type.unknown=Unknown
|
||||
assets.specialization.none=None
|
||||
assets.specialization.tileset=Tileset
|
||||
assets.specialization.sprites=Sprites
|
||||
assets.specialization.ui=UI
|
||||
assets.sceneStatus.pendingValidation=Pending Validation
|
||||
assets.sceneStatus.validatedPendingAcceptance=Validated / Pending Acceptance
|
||||
assets.sceneStatus.ready=Ready
|
||||
assets.sceneStatus.validationFailed=Validation Failed
|
||||
assets.label.location=Location
|
||||
assets.label.bank=Bank
|
||||
assets.label.targetLocation=Target Location
|
||||
@ -197,6 +226,8 @@ assets.value.included=Included
|
||||
assets.value.excluded=Excluded
|
||||
assets.value.yes=Yes
|
||||
assets.value.no=No
|
||||
assets.value.notConfigured=Not configured
|
||||
assets.value.layerOverviewPending=Overview reserved for this layer. Preview data will appear here when the layer pipeline is connected.
|
||||
assets.progress.idle=Assets workspace idle.
|
||||
assets.progress.refreshing=Refreshing assets...
|
||||
assets.progress.loadingDetails=Loading selected asset details...
|
||||
@ -232,6 +263,22 @@ assets.paletteOverhauling.selected=Bank Palettes
|
||||
assets.paletteOverhauling.tileSelector=Preview Tile
|
||||
assets.paletteOverhauling.appliedPalette=Applied Palette
|
||||
assets.paletteOverhauling.previewEmpty=Select at least one palette to inspect it on a tile.
|
||||
assets.paletteWizard.createTitle=Create Palette
|
||||
assets.paletteWizard.updateTitle=Update Palette
|
||||
assets.paletteWizard.name=Name
|
||||
assets.paletteWizard.schema=Schema
|
||||
assets.paletteWizard.original=Original Palette
|
||||
assets.paletteWizard.editable=Editable Palette
|
||||
assets.paletteWizard.applyColor=Apply Color
|
||||
assets.paletteWizard.duplicate=An identical palette already exists for this schema.
|
||||
assets.paletteWizard.ready=Palette is ready to be saved.
|
||||
assets.colorSchema.empty=No schemas have been authored for this glyph bank yet.
|
||||
assets.colorSchema.summary={0} schema groups ready, {1} selected tiles still unassigned.
|
||||
assets.colorSchema.removeBlocked=Schema {0} cannot be removed while palettes still reference it.
|
||||
assets.colorSchema.dialog.availableTiles=Available Tiles
|
||||
assets.colorSchema.dialog.assignedTiles=Assigned Tiles
|
||||
assets.colorSchema.dialog.colorCount=Color count: {0}
|
||||
assets.colorSchema.dialog.tooManyColors=Color Schema Groups support at most 15 visible indices.
|
||||
assets.details.codecConfiguration.empty=This codec does not expose configuration fields yet.
|
||||
assets.details.metadata.empty=This asset does not expose metadata fields yet.
|
||||
assets.addWizard.title=Add Asset
|
||||
@ -245,10 +292,12 @@ assets.addWizard.step.summary.description=Confirm the registered asset you are a
|
||||
assets.addWizard.label.name=Asset Name
|
||||
assets.addWizard.label.root=Asset Root
|
||||
assets.addWizard.label.type=Asset Type
|
||||
assets.addWizard.label.specialization=Studio Specialization
|
||||
assets.addWizard.label.format=Output Format
|
||||
assets.addWizard.label.codec=Output Codec
|
||||
assets.addWizard.label.preload=Preload on startup
|
||||
assets.addWizard.prompt.type=Choose asset type
|
||||
assets.addWizard.prompt.specialization=Choose Studio specialization
|
||||
assets.addWizard.prompt.format=Choose output format
|
||||
assets.addWizard.prompt.codec=Choose output codec
|
||||
assets.addWizard.assetsRootHint=Assets root: {0}
|
||||
@ -261,6 +310,7 @@ assets.addWizard.error.rootRelative=Asset root must be a relative path inside as
|
||||
assets.addWizard.error.rootOutsideAssets=The selected directory must stay inside assets/.
|
||||
assets.addWizard.error.rootAlreadyAsset=The selected root already contains asset.json.
|
||||
assets.addWizard.error.type=Choose an asset type before continuing.
|
||||
assets.addWizard.error.specialization=Choose a Studio specialization before continuing.
|
||||
assets.addWizard.error.format=Choose an output format before continuing.
|
||||
assets.addWizard.error.codec=Choose an output codec before continuing.
|
||||
assets.addWizard.error.unsupportedCombination=The selected asset type, output format, and codec combination is not supported.
|
||||
|
||||
@ -114,6 +114,20 @@
|
||||
-fx-padding: 8;
|
||||
}
|
||||
|
||||
.studio-button.assets-details-mini-action-button {
|
||||
-fx-min-width: 26;
|
||||
-fx-pref-width: 26;
|
||||
-fx-max-width: 26;
|
||||
-fx-min-height: 24;
|
||||
-fx-pref-height: 24;
|
||||
-fx-max-height: 24;
|
||||
-fx-padding: 0;
|
||||
-fx-background-radius: 6;
|
||||
-fx-border-radius: 6;
|
||||
-fx-font-size: 12px;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.studio-button.studio-button-primary {
|
||||
-fx-background-color: #27507a;
|
||||
-fx-border-color: #5ea0de;
|
||||
@ -1213,6 +1227,15 @@
|
||||
-fx-background-color: #162a20;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-row-tone-scene {
|
||||
-fx-background-color: #1d1610;
|
||||
-fx-border-color: #b58931;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-row-tone-scene:hover {
|
||||
-fx-background-color: #2a2015;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-row-tone-generic {
|
||||
-fx-background-color: #11151b;
|
||||
-fx-border-color: #334050;
|
||||
@ -1240,6 +1263,10 @@
|
||||
-fx-text-fill: #aee8c5;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-name-tone-scene {
|
||||
-fx-text-fill: #f5d47a;
|
||||
}
|
||||
|
||||
.assets-workspace-asset-name-tone-generic {
|
||||
-fx-text-fill: #f6fbff;
|
||||
}
|
||||
@ -1457,6 +1484,12 @@
|
||||
-fx-max-height: 446;
|
||||
}
|
||||
|
||||
.assets-details-color-schema-section {
|
||||
-fx-min-height: 446;
|
||||
-fx-pref-height: 446;
|
||||
-fx-max-height: 446;
|
||||
}
|
||||
|
||||
.assets-details-bank-composition-body {
|
||||
-fx-alignment: top-left;
|
||||
}
|
||||
@ -1465,6 +1498,76 @@
|
||||
-fx-alignment: top-left;
|
||||
}
|
||||
|
||||
.assets-details-palette-overhauling-toolbar {
|
||||
-fx-alignment: top-left;
|
||||
-fx-padding: 25 0 0 0;
|
||||
}
|
||||
|
||||
.assets-details-color-schema-content {
|
||||
-fx-alignment: top-left;
|
||||
}
|
||||
|
||||
.assets-details-color-schema-toolbar {
|
||||
-fx-alignment: top-left;
|
||||
-fx-padding: 25 0 0 0;
|
||||
}
|
||||
|
||||
.assets-details-list-with-actions {
|
||||
-fx-alignment: top-left;
|
||||
}
|
||||
|
||||
.assets-details-color-schema-body {
|
||||
-fx-alignment: top-left;
|
||||
}
|
||||
|
||||
.assets-details-color-schema-list {
|
||||
-fx-pref-width: 280;
|
||||
-fx-max-width: 320;
|
||||
}
|
||||
|
||||
.assets-details-color-schema-item {
|
||||
-fx-padding: 8 10 8 10;
|
||||
}
|
||||
|
||||
.assets-details-color-schema-title {
|
||||
-fx-font-size: 12px;
|
||||
-fx-font-weight: bold;
|
||||
-fx-text-fill: #f2f6fb;
|
||||
}
|
||||
|
||||
.assets-details-color-schema-version {
|
||||
-fx-font-size: 11px;
|
||||
-fx-text-fill: #8fa4b8;
|
||||
}
|
||||
|
||||
.assets-details-color-schema-meta,
|
||||
.assets-details-color-schema-summary,
|
||||
.assets-details-color-schema-empty,
|
||||
.assets-details-color-schema-warning,
|
||||
.assets-details-color-schema-preview-title {
|
||||
-fx-font-size: 11px;
|
||||
-fx-text-fill: #9bb0c3;
|
||||
}
|
||||
|
||||
.assets-details-color-schema-status-ok {
|
||||
-fx-background-color: #203a2b;
|
||||
-fx-text-fill: #8ee0ac;
|
||||
}
|
||||
|
||||
.assets-details-color-schema-status-inconsistent {
|
||||
-fx-background-color: #4a261f;
|
||||
-fx-text-fill: #ffb29d;
|
||||
}
|
||||
|
||||
.assets-details-color-schema-strip {
|
||||
-fx-padding: 0 0 4 0;
|
||||
}
|
||||
|
||||
.assets-details-color-schema-swatch {
|
||||
-fx-border-color: #334150;
|
||||
-fx-border-width: 1;
|
||||
}
|
||||
|
||||
.assets-details-bank-file-cell {
|
||||
-fx-alignment: center-left;
|
||||
}
|
||||
@ -1500,6 +1603,43 @@
|
||||
-fx-alignment: center-left;
|
||||
}
|
||||
|
||||
.assets-details-palette-invalid {
|
||||
-fx-font-size: 10px;
|
||||
-fx-text-fill: #d9a58f;
|
||||
}
|
||||
|
||||
.assets-details-palette-wizard {
|
||||
-fx-background-color: #111821;
|
||||
-fx-border-color: #263545;
|
||||
-fx-border-width: 1;
|
||||
}
|
||||
|
||||
.assets-details-palette-wizard-label {
|
||||
-fx-font-size: 11px;
|
||||
-fx-font-weight: bold;
|
||||
-fx-text-fill: #9fc3e7;
|
||||
}
|
||||
|
||||
.assets-details-palette-wizard-strip,
|
||||
.assets-details-palette-wizard-editor,
|
||||
.assets-details-palette-wizard-color-editor {
|
||||
-fx-alignment: center-left;
|
||||
}
|
||||
|
||||
.assets-details-palette-wizard-summary {
|
||||
-fx-font-size: 11px;
|
||||
-fx-text-fill: #c0d0e0;
|
||||
}
|
||||
|
||||
.assets-details-palette-wizard-actions {
|
||||
-fx-alignment: center-right;
|
||||
}
|
||||
|
||||
.assets-details-palette-wizard-swatch {
|
||||
-fx-border-color: #334150;
|
||||
-fx-border-width: 1;
|
||||
}
|
||||
|
||||
.assets-details-palette-hash {
|
||||
-fx-text-fill: #9fc3e7;
|
||||
-fx-font-size: 10px;
|
||||
@ -1594,6 +1734,124 @@
|
||||
-fx-border-width: 1;
|
||||
}
|
||||
|
||||
.assets-details-chip-fixed {
|
||||
-fx-alignment: center;
|
||||
-fx-min-width: 88;
|
||||
-fx-pref-width: 88;
|
||||
-fx-max-width: 88;
|
||||
}
|
||||
|
||||
.assets-scene-layer-shell {
|
||||
-fx-spacing: 0;
|
||||
}
|
||||
|
||||
.assets-scene-layer-layout {
|
||||
-fx-alignment: top-left;
|
||||
}
|
||||
|
||||
.assets-scene-layer-config-column {
|
||||
-fx-spacing: 10;
|
||||
}
|
||||
|
||||
.assets-scene-layer-config {
|
||||
-fx-pref-width: 0;
|
||||
-fx-spacing: 8;
|
||||
}
|
||||
|
||||
.assets-scene-layer-config-section {
|
||||
-fx-pref-width: 0;
|
||||
}
|
||||
|
||||
.assets-scene-layer-chip {
|
||||
-fx-min-width: 70;
|
||||
-fx-pref-width: 70;
|
||||
-fx-max-width: 70;
|
||||
-fx-alignment: center;
|
||||
}
|
||||
|
||||
.assets-scene-layer-selector {
|
||||
-fx-alignment: center-left;
|
||||
}
|
||||
|
||||
.assets-scene-layer-selector-button {
|
||||
-fx-background-color: #17120d;
|
||||
-fx-border-color: #634d22;
|
||||
-fx-text-fill: #d9c28f;
|
||||
-fx-font-weight: bold;
|
||||
-fx-padding: 8 14 8 14;
|
||||
}
|
||||
|
||||
.assets-scene-layer-selector-button:hover {
|
||||
-fx-background-color: #221a10;
|
||||
-fx-border-color: #8f6f33;
|
||||
-fx-text-fill: #f5ddb0;
|
||||
}
|
||||
|
||||
.assets-scene-layer-selector-button-active {
|
||||
-fx-background-color: #2a1f12;
|
||||
-fx-border-color: #d4a94a;
|
||||
-fx-text-fill: #ffe5a6;
|
||||
}
|
||||
|
||||
.assets-scene-layer-enabled {
|
||||
-fx-text-fill: #d6e0ea;
|
||||
}
|
||||
|
||||
.assets-scene-layer-form-actions {
|
||||
-fx-alignment: center-right;
|
||||
-fx-spacing: 8;
|
||||
}
|
||||
|
||||
.assets-scene-layer-actions {
|
||||
-fx-padding: 6 0 0 0;
|
||||
}
|
||||
|
||||
.assets-scene-layer-action {
|
||||
-fx-max-width: 140;
|
||||
}
|
||||
|
||||
.assets-scene-layer-size-editor {
|
||||
-fx-alignment: center-left;
|
||||
}
|
||||
|
||||
.assets-scene-layer-size-field {
|
||||
-fx-pref-width: 88;
|
||||
}
|
||||
|
||||
.assets-scene-layer-size-separator {
|
||||
-fx-text-fill: #d6e0ea;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.assets-scene-layer-overview {
|
||||
-fx-background-color: #11151b;
|
||||
-fx-background-radius: 10;
|
||||
-fx-border-color: #26313d;
|
||||
-fx-border-radius: 10;
|
||||
-fx-border-width: 1;
|
||||
-fx-min-width: 260;
|
||||
-fx-pref-width: 260;
|
||||
-fx-max-width: 260;
|
||||
-fx-min-height: 220;
|
||||
-fx-padding: 10;
|
||||
-fx-spacing: 10;
|
||||
}
|
||||
|
||||
.assets-scene-layer-overview-title {
|
||||
-fx-text-fill: #d6e0ea;
|
||||
-fx-font-size: 12px;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.assets-scene-layer-overview-surface {
|
||||
-fx-background-color: #0f141a;
|
||||
-fx-background-radius: 8;
|
||||
-fx-border-color: #26313d;
|
||||
-fx-border-radius: 8;
|
||||
-fx-border-width: 1;
|
||||
-fx-min-height: 148;
|
||||
}
|
||||
|
||||
.assets-details-chip-image {
|
||||
-fx-background-color: #11283a;
|
||||
-fx-border-color: #5cb6ff;
|
||||
@ -1606,6 +1864,12 @@
|
||||
-fx-text-fill: #dde6ff;
|
||||
}
|
||||
|
||||
.assets-details-chip-scene {
|
||||
-fx-background-color: #2a1f12;
|
||||
-fx-border-color: #d4a94a;
|
||||
-fx-text-fill: #ffe5a6;
|
||||
}
|
||||
|
||||
.assets-details-chip-generic {
|
||||
-fx-background-color: #1e242c;
|
||||
-fx-border-color: #5f7388;
|
||||
|
||||
@ -7,6 +7,7 @@ import p.packer.messages.assets.AssetFamilyCatalog;
|
||||
import p.packer.messages.assets.OutputCodecCatalog;
|
||||
import p.packer.messages.assets.OutputFormatCatalog;
|
||||
import p.packer.messages.assets.PackerCodecConfigurationFieldType;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioGlyphSpecialization;
|
||||
import p.studio.workspaces.assets.messages.*;
|
||||
|
||||
import java.nio.file.Path;
|
||||
@ -114,6 +115,9 @@ final class AssetDetailsBankCompositionCoordinatorTest {
|
||||
List.of(),
|
||||
0L),
|
||||
List.of(),
|
||||
null,
|
||||
null,
|
||||
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||
List.of());
|
||||
}
|
||||
|
||||
@ -136,6 +140,9 @@ final class AssetDetailsBankCompositionCoordinatorTest {
|
||||
List.of(),
|
||||
0L),
|
||||
List.of(),
|
||||
null,
|
||||
null,
|
||||
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||
List.of());
|
||||
}
|
||||
|
||||
@ -159,6 +166,7 @@ final class AssetDetailsBankCompositionCoordinatorTest {
|
||||
AssetWorkspaceBuildParticipation.INCLUDED,
|
||||
1,
|
||||
family,
|
||||
AssetStudioGlyphSpecialization.NONE,
|
||||
Path.of("/tmp/bank"),
|
||||
false,
|
||||
false);
|
||||
|
||||
@ -5,6 +5,7 @@ import p.packer.messages.AssetReference;
|
||||
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||
import p.packer.messages.assets.OutputCodecCatalog;
|
||||
import p.packer.messages.assets.OutputFormatCatalog;
|
||||
import p.studio.workspaces.assets.metadata.*;
|
||||
import p.studio.workspaces.assets.messages.*;
|
||||
|
||||
import java.nio.file.Path;
|
||||
@ -15,64 +16,388 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
final class AssetDetailsPaletteOverhaulingCoordinatorTest {
|
||||
@Test
|
||||
void defaultsToFirstTilePaletteWhenNoSelectedFilesExist() {
|
||||
void derivesOriginalPaletteFromReadySchemaWhenNoPaletteRecordExists() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
coordinator.replaceDetails(tileDetails(List.of(), files("tile", 3)));
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000", 2, "#00FF00"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(),
|
||||
null),
|
||||
List.of()));
|
||||
|
||||
assertTrue(coordinator.ready());
|
||||
assertEquals(0, coordinator.viewModel().selectedFiles().size());
|
||||
assertNull(coordinator.viewModel().previewTileFile());
|
||||
assertNull(coordinator.viewModel().previewPaletteFile());
|
||||
assertEquals(1, coordinator.viewModel().allFiles().size());
|
||||
assertEquals("schema-1 / original", coordinator.viewModel().allFiles().getFirst().displayName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void clickingSelectedPaletteChangesOnlyPreviewPalette() {
|
||||
void initialSelectionStillMatchesBuildFacingPipelinePalettePayload() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
coordinator.replaceDetails(tileDetails(List.of(files("tile", 3).get(0), files("tile", 3).get(1)), List.of(files("tile", 3).get(2))));
|
||||
coordinator.beginEdit();
|
||||
|
||||
final AssetWorkspaceBankCompositionFile first = coordinator.viewModel().availableFiles().get(0);
|
||||
coordinator.moveToSelected(List.of(first));
|
||||
coordinator.selectPreviewPalette(first);
|
||||
|
||||
assertEquals(first.path(), coordinator.viewModel().previewPaletteFile().path());
|
||||
assertEquals(1, coordinator.viewModel().selectedFiles().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void removingPreviewedPaletteFallsBackToFirstRemainingSelectedPalette() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
final List<AssetWorkspaceBankCompositionFile> generated = files("tile", 3);
|
||||
coordinator.replaceDetails(tileDetails(List.of(generated.get(0), generated.get(1)), List.of(generated.get(2))));
|
||||
coordinator.beginEdit();
|
||||
final AssetWorkspaceBankCompositionFile first = coordinator.viewModel().availableFiles().get(0);
|
||||
coordinator.moveToSelected(List.of(first));
|
||||
coordinator.selectPreviewPalette(first);
|
||||
|
||||
coordinator.moveToAvailable(List.of(first));
|
||||
final AssetStudioGlyphMaterialPalette persisted = new AssetStudioGlyphMaterialPalette(
|
||||
"palette-1",
|
||||
"schema-1",
|
||||
1,
|
||||
List.of("#FF0000", "#00FF00"));
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000", 2, "#00FF00"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(persisted),
|
||||
null),
|
||||
List.of(Map.of(
|
||||
"originalArgb8888", List.of(0xFFFF0000, 0xFF00FF00),
|
||||
"convertedRgb565", List.of(0xF800, 0x07E0)))));
|
||||
|
||||
assertEquals(1, coordinator.viewModel().selectedFiles().size());
|
||||
assertEquals(first.path(), coordinator.viewModel().previewPaletteFile().path());
|
||||
assertEquals("palette-1", coordinator.viewModel().selectedFiles().getFirst().metadata().get("paletteId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deduplicatesPalettesByColorHash() {
|
||||
void previewPaletteDefaultsToAvailablePaletteWhenBuildSelectionIsEmpty() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
final AssetWorkspaceBankCompositionFile first = file("tile-a.png", List.of(0xFFFF0000, 0xFF00FF00));
|
||||
final AssetWorkspaceBankCompositionFile duplicate = file("tile-b.png", List.of(0xFFFF0000, 0xFF00FF00));
|
||||
final AssetWorkspaceBankCompositionFile unique = file("tile-c.png", List.of(0xFF0000FF));
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000", 2, "#00FF00"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(new AssetStudioGlyphMaterialPalette(
|
||||
"palette-1",
|
||||
"schema-1",
|
||||
1,
|
||||
List.of("#112233", "#445566"))),
|
||||
null),
|
||||
List.of()));
|
||||
|
||||
coordinator.replaceDetails(tileDetails(List.of(first), List.of(duplicate, unique)));
|
||||
assertNotNull(coordinator.viewModel().previewPaletteFile());
|
||||
assertTrue(coordinator.viewModel().availableFiles().contains(coordinator.viewModel().previewPaletteFile()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void originalPaletteRemainsSyntheticEvenWhenUserPaletteHasSameColors() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000", 2, "#00FF00"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(new AssetStudioGlyphMaterialPalette(
|
||||
"palette-1",
|
||||
"schema-1",
|
||||
1,
|
||||
List.of("#FF0000", "#00FF00"))),
|
||||
null),
|
||||
List.of()));
|
||||
|
||||
assertEquals(2, coordinator.viewModel().allFiles().size());
|
||||
assertEquals(10, coordinator.viewModel().allFiles().getFirst().displayName().length());
|
||||
assertEquals(coordinator.viewModel().allFiles().stream().map(AssetWorkspaceBankCompositionFile::displayName).distinct().count(),
|
||||
coordinator.viewModel().allFiles().size());
|
||||
final AssetWorkspaceBankCompositionFile original = coordinator.viewModel().allFiles().stream()
|
||||
.filter(AssetDetailsPaletteOverhaulingCoordinator::paletteIsOriginal)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
final AssetWorkspaceBankCompositionFile userPalette = coordinator.viewModel().allFiles().stream()
|
||||
.filter(file -> "palette-1".equals(file.metadata().get("paletteId")))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
assertEquals("schema-schema-1-original", original.metadata().get("paletteId"));
|
||||
assertFalse(AssetDetailsPaletteOverhaulingCoordinator.paletteIsOriginal(userPalette));
|
||||
}
|
||||
|
||||
private AssetWorkspaceAssetDetails tileDetails(
|
||||
@Test
|
||||
void originalPaletteTracksSchemaMaterialColorsAfterMetadataUpdate() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000", 2, "#00FF00"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(),
|
||||
null),
|
||||
List.of()));
|
||||
|
||||
coordinator.replaceMaterialMetadata(new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
2,
|
||||
Map.of(1, "#112233", 2, "#445566"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(),
|
||||
null));
|
||||
|
||||
final AssetWorkspaceBankCompositionFile original = coordinator.viewModel().allFiles().stream()
|
||||
.filter(AssetDetailsPaletteOverhaulingCoordinator::paletteIsOriginal)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
final Map<String, Object> palette = (Map<String, Object>) original.metadata().get("palette");
|
||||
assertEquals(List.of(0xFF112233, 0xFF445566), palette.get("originalArgb8888"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void duplicateDetectionUsesSameSchemaColorsAndOrder() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000", 2, "#00FF00"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(new AssetStudioGlyphMaterialPalette(
|
||||
"palette-1",
|
||||
"schema-1",
|
||||
1,
|
||||
List.of("#FF0000", "#00FF00"))),
|
||||
null),
|
||||
List.of()));
|
||||
|
||||
assertTrue(coordinator.duplicatePaletteExists(null, "schema-1", List.of("#FF0000", "#00FF00")));
|
||||
assertFalse(coordinator.duplicatePaletteExists(null, "schema-1", List.of("#00FF00", "#FF0000")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void paletteDisplayNameUsesExplicitNameWhenPresent() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000", 2, "#00FF00"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(new AssetStudioGlyphMaterialPalette(
|
||||
"palette-1",
|
||||
"Forest",
|
||||
"schema-1",
|
||||
1,
|
||||
List.of("#FF0000", "#00FF00"))),
|
||||
null),
|
||||
List.of()));
|
||||
|
||||
final AssetWorkspaceBankCompositionFile palette = coordinator.viewModel().allFiles().getFirst();
|
||||
assertEquals("Forest", palette.displayName());
|
||||
assertEquals("Forest", palette.metadata().get("explicitPaletteDisplayName"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void paletteDisplayNameFallsBackToColorsWhenNameIsBlank() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000", 2, "#00FF00"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(new AssetStudioGlyphMaterialPalette(
|
||||
"palette-1",
|
||||
"schema-1",
|
||||
1,
|
||||
List.of("#FF0000", "#00FF00"))),
|
||||
null),
|
||||
List.of()));
|
||||
|
||||
final AssetWorkspaceBankCompositionFile palette = coordinator.viewModel().allFiles().getFirst();
|
||||
assertEquals("ember bloom", palette.displayName());
|
||||
assertFalse(palette.metadata().containsKey("explicitPaletteDisplayName"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatedPaletteDisplayNameUsesPoeticColorWords() {
|
||||
assertEquals("snow white", AssetDetailsPaletteOverhaulingCoordinator.generatedDisplayName(List.of("#FFFFFF")));
|
||||
assertEquals("fog twist", AssetDetailsPaletteOverhaulingCoordinator.generatedDisplayName(List.of("#F2F4F4", "#707878")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void upsertPalettePersistsGeneratedDisplayNameWhenNameIsBlank() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#F2F4F4", 2, "#707878"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(),
|
||||
null),
|
||||
List.of()));
|
||||
|
||||
final AssetStudioGlyphMaterialMetadata metadata = coordinator.upsertPalette(
|
||||
"palette-1",
|
||||
" ",
|
||||
"schema-1",
|
||||
List.of("#F2F4F4", "#707878"));
|
||||
|
||||
assertEquals("fog twist", metadata.palettes().getFirst().displayName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void removePaletteDeletesUserPaletteEvenWhenSelectedForBuild() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000", 2, "#00FF00"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(new AssetStudioGlyphMaterialPalette(
|
||||
"palette-1",
|
||||
"schema-1",
|
||||
1,
|
||||
List.of("#112233", "#445566"))),
|
||||
null),
|
||||
List.of(Map.of(
|
||||
"originalArgb8888", List.of(0xFF112233, 0xFF445566),
|
||||
"convertedRgb565", List.of(0x1106, 0x42AC)))));
|
||||
|
||||
final AssetStudioGlyphMaterialMetadata metadata = coordinator.removePalette("palette-1");
|
||||
|
||||
assertTrue(metadata.palettes().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void removePaletteRejectsSyntheticOriginalPalette() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000", 2, "#00FF00"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(),
|
||||
null),
|
||||
List.of()));
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> coordinator.removePalette("schema-schema-1-original"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void stalePaletteIsProjectedAsInvalid() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
2,
|
||||
Map.of(1, "#FF0000"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(new AssetStudioGlyphMaterialPalette(
|
||||
"palette-1",
|
||||
"schema-1",
|
||||
1,
|
||||
List.of("#FF0000"))),
|
||||
null),
|
||||
List.of()));
|
||||
|
||||
final AssetWorkspaceBankCompositionFile palette = coordinator.viewModel().allFiles().getFirst();
|
||||
assertEquals(Boolean.FALSE, palette.metadata().get("valid"));
|
||||
assertTrue(((String) palette.metadata().get("invalidReason")).contains("stale"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void paletteIsInvalidWhenSchemaArtifactFingerprintDrifts() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-old")))),
|
||||
List.of(new AssetStudioGlyphMaterialPalette(
|
||||
"palette-1",
|
||||
"schema-1",
|
||||
1,
|
||||
List.of("#FF0000"))),
|
||||
null),
|
||||
List.of()));
|
||||
|
||||
final AssetWorkspaceBankCompositionFile palette = coordinator.viewModel().allFiles().getFirst();
|
||||
assertEquals(Boolean.FALSE, palette.metadata().get("valid"));
|
||||
assertTrue(((String) palette.metadata().get("invalidReason")).contains("inconsistent"));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void previewTilesAreScopedToSelectedPaletteSchema() {
|
||||
final AssetDetailsPaletteOverhaulingCoordinator coordinator = new AssetDetailsPaletteOverhaulingCoordinator();
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("tile-a.png", "artifact-a"), tile("tile-b.png", "artifact-b")),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(
|
||||
new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000", 2, "#00FF00"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a"))),
|
||||
new AssetStudioGlyphMaterialSchema(
|
||||
"schema-2",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#0000FF"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-b", "fp-b")))),
|
||||
List.of(),
|
||||
null),
|
||||
List.of()));
|
||||
|
||||
final AssetWorkspaceBankCompositionFile schema2Palette = coordinator.viewModel().allFiles().stream()
|
||||
.filter(file -> "schema-2".equals(file.metadata().get("schemaId")))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
coordinator.selectPreviewPalette(schema2Palette);
|
||||
|
||||
assertEquals(List.of("tile-b.png"), coordinator.viewModel().previewTileFiles().stream().map(AssetWorkspaceBankCompositionFile::path).toList());
|
||||
assertEquals("schema-2", coordinator.viewModel().previewPaletteFile().metadata().get("schemaId"));
|
||||
assertEquals(Map.of(1, "#0000FF"), coordinator.viewModel().previewPaletteFile().metadata().get("schemaMaterialIndices"));
|
||||
}
|
||||
|
||||
private AssetWorkspaceAssetDetails details(
|
||||
List<AssetWorkspaceBankCompositionFile> selectedFiles,
|
||||
List<AssetWorkspaceBankCompositionFile> availableFiles) {
|
||||
AssetStudioGlyphMaterialMetadata metadata,
|
||||
List<Map<String, Object>> pipelinePalettes) {
|
||||
return new AssetWorkspaceAssetDetails(
|
||||
new AssetWorkspaceAssetSummary(
|
||||
AssetReference.forAssetId(1),
|
||||
@ -81,6 +406,7 @@ final class AssetDetailsPaletteOverhaulingCoordinatorTest {
|
||||
AssetWorkspaceBuildParticipation.INCLUDED,
|
||||
1,
|
||||
AssetFamilyCatalog.GLYPH_BANK,
|
||||
AssetStudioGlyphSpecialization.SPRITES,
|
||||
Path.of("/tmp/bank"),
|
||||
false,
|
||||
false),
|
||||
@ -91,46 +417,29 @@ final class AssetDetailsPaletteOverhaulingCoordinatorTest {
|
||||
Map.of(OutputCodecCatalog.NONE, List.of()),
|
||||
List.of(),
|
||||
Map.of(),
|
||||
new AssetWorkspaceBankCompositionDetails(availableFiles, selectedFiles, 0L),
|
||||
selectedFiles.stream().map(file -> (Map<String, Object>) file.metadata().get("palette")).toList(),
|
||||
new AssetWorkspaceBankCompositionDetails(List.of(), selectedFiles, 0L),
|
||||
pipelinePalettes,
|
||||
metadata,
|
||||
null,
|
||||
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||
List.of());
|
||||
}
|
||||
|
||||
private List<AssetWorkspaceBankCompositionFile> files(String prefix, int count) {
|
||||
return java.util.stream.IntStream.range(0, count)
|
||||
.mapToObj(index -> file(
|
||||
prefix + "-" + index + ".png",
|
||||
List.of(
|
||||
0xFF000000 | ((index + 1) << 16),
|
||||
0xFF000000 | ((index + 1) << 8),
|
||||
0xFF000000 | (index + 1))))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private AssetWorkspaceBankCompositionFile file(String path, List<Integer> colors) {
|
||||
private AssetWorkspaceBankCompositionFile tile(String path, String artifactId) {
|
||||
return new AssetWorkspaceBankCompositionFile(
|
||||
path,
|
||||
path,
|
||||
256L,
|
||||
1L,
|
||||
null,
|
||||
"artifact-a".equals(artifactId) ? "fp-a" : "fp-b",
|
||||
Map.of(
|
||||
"artifactId", artifactId,
|
||||
"tile", Map.of(
|
||||
"width", 2,
|
||||
"height", 2,
|
||||
"paletteIndices", List.of(0, 1, 2, 0)),
|
||||
"palette", Map.of(
|
||||
"originalArgb8888", colors,
|
||||
"convertedRgb565", colors.stream().map(this::toRgb565).toList())));
|
||||
}
|
||||
|
||||
private int toRgb565(int argb8888) {
|
||||
final int red = (argb8888 >> 16) & 0xFF;
|
||||
final int green = (argb8888 >> 8) & 0xFF;
|
||||
final int blue = argb8888 & 0xFF;
|
||||
final int r5 = (red >> 3) & 0x1F;
|
||||
final int g6 = (green >> 2) & 0x3F;
|
||||
final int b5 = (blue >> 3) & 0x1F;
|
||||
return (r5 << 11) | (g6 << 5) | b5;
|
||||
"originalArgb8888", List.of(0xFFFF0000, 0xFF00FF00),
|
||||
"convertedRgb565", List.of(0xF800, 0x07E0))));
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,219 @@
|
||||
package p.studio.workspaces.assets.details.schema;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import p.packer.messages.AssetReference;
|
||||
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||
import p.packer.messages.assets.OutputCodecCatalog;
|
||||
import p.packer.messages.assets.OutputFormatCatalog;
|
||||
import p.studio.workspaces.assets.metadata.*;
|
||||
import p.studio.workspaces.assets.messages.*;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
final class AssetDetailsColorSchemaCoordinatorTest {
|
||||
@Test
|
||||
void createFlowExcludesTilesAlreadyBoundToAnotherSchema() {
|
||||
final AssetDetailsColorSchemaCoordinator coordinator = new AssetDetailsColorSchemaCoordinator();
|
||||
final AssetWorkspaceBankCompositionFile tileA = tile("a.png", "artifact-a", "fp-a", List.of("#FF0000"));
|
||||
final AssetWorkspaceBankCompositionFile tileB = tile("b.png", "artifact-b", "fp-b", List.of("#00FF00"));
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tileA, tileB),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(),
|
||||
null)));
|
||||
|
||||
assertEquals(List.of(tileB), coordinator.availableTilesForCreate());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createFlowListsSelectedTilesBeforePaletteMetadataExists() {
|
||||
final AssetDetailsColorSchemaCoordinator coordinator = new AssetDetailsColorSchemaCoordinator();
|
||||
final AssetWorkspaceBankCompositionFile tile = selectedArtifactOnly("a.png", "artifact-a", "fp-a");
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile),
|
||||
new AssetStudioGlyphMaterialMetadata(List.of(), List.of(), null)));
|
||||
|
||||
assertEquals(List.of(tile), coordinator.availableTilesForCreate());
|
||||
assertEquals(List.of(), AssetDetailsColorSchemaCoordinator.tileColors(tile));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createFlowUsesSelectedFingerprintWhenCurrentFileFingerprintIsMissing() {
|
||||
final AssetDetailsColorSchemaCoordinator coordinator = new AssetDetailsColorSchemaCoordinator();
|
||||
final AssetWorkspaceBankCompositionFile tile = new AssetWorkspaceBankCompositionFile(
|
||||
"a.png",
|
||||
"a.png",
|
||||
64L,
|
||||
1L,
|
||||
null,
|
||||
Map.of(
|
||||
"artifactId", "artifact-a",
|
||||
"selectedFingerprint", "fp-a"));
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile),
|
||||
new AssetStudioGlyphMaterialMetadata(List.of(), List.of(), null)));
|
||||
|
||||
assertEquals(List.of(tile), coordinator.availableTilesForCreate());
|
||||
final AssetStudioGlyphMaterialMetadata metadata = coordinator.upsertSchema(
|
||||
"schema-1",
|
||||
List.of(tile),
|
||||
List.of("#FF0000"));
|
||||
assertEquals("fp-a", metadata.schemas().getFirst().artifactBindings().getFirst().fingerprint());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updateFlowKeepsCurrentTilesAndIncrementsVersionWhenSchemaChanges() {
|
||||
final AssetDetailsColorSchemaCoordinator coordinator = new AssetDetailsColorSchemaCoordinator();
|
||||
final AssetWorkspaceBankCompositionFile tileA = tile("a.png", "artifact-a", "fp-a", List.of("#FF0000"));
|
||||
final AssetWorkspaceBankCompositionFile tileB = tile("b.png", "artifact-b", "fp-b", List.of("#00FF00"));
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tileA, tileB),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(),
|
||||
null)));
|
||||
|
||||
final AssetStudioGlyphMaterialMetadata updated = coordinator.upsertSchema(
|
||||
"schema-1",
|
||||
List.of(tileA, tileB),
|
||||
List.of("#FF0000", "#00FF00"));
|
||||
|
||||
final AssetStudioGlyphMaterialSchema schema = updated.schemas().getFirst();
|
||||
assertEquals(2, schema.version());
|
||||
assertEquals(Map.of(1, "#FF0000", 2, "#00FF00"), schema.materialIndices());
|
||||
assertEquals(List.of("artifact-a", "artifact-b"), schema.artifactBindings().stream().map(AssetStudioGlyphMaterialArtifactBinding::artifactId).toList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeIsBlockedWhenPalettesStillReferenceSchema() {
|
||||
final AssetDetailsColorSchemaCoordinator coordinator = new AssetDetailsColorSchemaCoordinator();
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(tile("a.png", "artifact-a", "fp-a", List.of("#FF0000"))),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-a")))),
|
||||
List.of(new AssetStudioGlyphMaterialPalette("palette-1", "schema-1", 1, List.of("#FF0000"))),
|
||||
null)));
|
||||
|
||||
assertFalse(coordinator.canRemove("schema-1"));
|
||||
assertThrows(IllegalStateException.class, () -> coordinator.removeSchema("schema-1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void effectiveStatusBecomesInconsistentWhenArtifactFingerprintDrifts() {
|
||||
final AssetStudioGlyphMaterialSchema schema = new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-old")));
|
||||
|
||||
assertEquals(
|
||||
AssetStudioGlyphMaterialSchemaStatus.INCONSISTENT,
|
||||
AssetDetailsColorSchemaCoordinator.effectiveStatus(
|
||||
schema,
|
||||
List.of(tile("a.png", "artifact-a", "fp-new", List.of("#FF0000", "#FF0001")))));
|
||||
}
|
||||
|
||||
@Test
|
||||
void schemaUpdateCanRefreshOnlyFingerprintAndIncrementVersion() {
|
||||
final AssetDetailsColorSchemaCoordinator coordinator = new AssetDetailsColorSchemaCoordinator();
|
||||
final AssetWorkspaceBankCompositionFile changedTile = tile("a.png", "artifact-a", "fp-new", List.of("#FF0000"));
|
||||
coordinator.replaceDetails(details(
|
||||
List.of(changedTile),
|
||||
new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
1,
|
||||
Map.of(1, "#FF0000"),
|
||||
List.of(new AssetStudioGlyphMaterialArtifactBinding("artifact-a", "fp-old")))),
|
||||
List.of(),
|
||||
null)));
|
||||
|
||||
final AssetStudioGlyphMaterialSchema updated = coordinator.upsertSchema(
|
||||
"schema-1",
|
||||
List.of(changedTile),
|
||||
List.of("#FF0000")).schemas().getFirst();
|
||||
|
||||
assertEquals(2, updated.version());
|
||||
assertEquals(AssetStudioGlyphMaterialSchemaStatus.OK, updated.status());
|
||||
assertEquals("fp-new", updated.artifactBindings().getFirst().fingerprint());
|
||||
}
|
||||
|
||||
private AssetWorkspaceAssetDetails details(
|
||||
List<AssetWorkspaceBankCompositionFile> selectedTiles,
|
||||
AssetStudioGlyphMaterialMetadata metadata) {
|
||||
return new AssetWorkspaceAssetDetails(
|
||||
new AssetWorkspaceAssetSummary(
|
||||
AssetReference.forAssetId(1),
|
||||
"glyph_bank",
|
||||
AssetWorkspaceAssetState.REGISTERED,
|
||||
AssetWorkspaceBuildParticipation.INCLUDED,
|
||||
1,
|
||||
AssetFamilyCatalog.GLYPH_BANK,
|
||||
AssetStudioGlyphSpecialization.SPRITES,
|
||||
Path.of("/tmp/glyph"),
|
||||
false,
|
||||
false),
|
||||
List.of(),
|
||||
OutputFormatCatalog.GLYPH_INDEXED_V1,
|
||||
OutputCodecCatalog.NONE,
|
||||
List.of(OutputCodecCatalog.NONE),
|
||||
Map.of(OutputCodecCatalog.NONE, List.of()),
|
||||
List.of(),
|
||||
Map.of(),
|
||||
new AssetWorkspaceBankCompositionDetails(List.of(), selectedTiles, 0L),
|
||||
List.of(),
|
||||
metadata,
|
||||
null,
|
||||
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||
List.of());
|
||||
}
|
||||
|
||||
private AssetWorkspaceBankCompositionFile tile(String path, String artifactId, String fingerprint, List<String> colors) {
|
||||
final List<Integer> argb = colors.stream()
|
||||
.map(color -> Integer.parseUnsignedInt("FF" + color.substring(1), 16))
|
||||
.map(Integer::valueOf)
|
||||
.toList();
|
||||
return new AssetWorkspaceBankCompositionFile(
|
||||
path,
|
||||
path,
|
||||
64L,
|
||||
1L,
|
||||
fingerprint,
|
||||
Map.of(
|
||||
"artifactId", artifactId,
|
||||
"tile", Map.of("width", 8, "height", 8, "paletteIndices", List.of(1, 1, 1)),
|
||||
"palette", Map.of("originalArgb8888", argb, "convertedRgb565", List.of())));
|
||||
}
|
||||
|
||||
private AssetWorkspaceBankCompositionFile selectedArtifactOnly(String path, String artifactId, String fingerprint) {
|
||||
return new AssetWorkspaceBankCompositionFile(
|
||||
path,
|
||||
path,
|
||||
64L,
|
||||
1L,
|
||||
fingerprint,
|
||||
Map.of("artifactId", artifactId));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,148 @@
|
||||
package p.studio.workspaces.assets.metadata;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
final class AssetStudioMetadataServiceTest {
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
private final AssetStudioMetadataService service = new AssetStudioMetadataService();
|
||||
|
||||
@Test
|
||||
void readsGlyphSpecializationFromStudioMetadataFile() throws Exception {
|
||||
final Path assetRoot = tempDir.resolve("tileset");
|
||||
Files.createDirectories(assetRoot);
|
||||
service.writeGlyphSpecialization(assetRoot, AssetStudioGlyphSpecialization.TILESET);
|
||||
|
||||
final AssetStudioMetadataSnapshot snapshot = service.read(assetRoot, AssetFamilyCatalog.GLYPH_BANK);
|
||||
|
||||
assertEquals(AssetStudioGlyphSpecialization.TILESET, snapshot.glyphSpecialization());
|
||||
assertNull(snapshot.sceneBankMetadata());
|
||||
assertNull(snapshot.glyphMaterialMetadata());
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsGlyphBankWithoutStudioMetadataAsNone() throws Exception {
|
||||
final Path assetRoot = tempDir.resolve("legacy-glyph");
|
||||
Files.createDirectories(assetRoot);
|
||||
|
||||
final AssetStudioMetadataSnapshot snapshot = service.read(assetRoot, AssetFamilyCatalog.GLYPH_BANK);
|
||||
|
||||
assertEquals(AssetStudioGlyphSpecialization.NONE, snapshot.glyphSpecialization());
|
||||
assertNull(snapshot.sceneBankMetadata());
|
||||
assertNull(snapshot.glyphMaterialMetadata());
|
||||
}
|
||||
|
||||
@Test
|
||||
void readsAndWritesGlyphMaterialSupportMetadata() throws Exception {
|
||||
final Path assetRoot = tempDir.resolve("glyph");
|
||||
Files.createDirectories(assetRoot);
|
||||
final AssetStudioGlyphMaterialMetadata metadata = new AssetStudioGlyphMaterialMetadata(
|
||||
List.of(new AssetStudioGlyphMaterialSchema(
|
||||
"schema-1",
|
||||
AssetStudioGlyphMaterialSchemaStatus.OK,
|
||||
2,
|
||||
Map.of(1, "#8B4513", 2, "#00AA00"),
|
||||
List.of(
|
||||
new AssetStudioGlyphMaterialArtifactBinding("artifact-1", "fp-1"),
|
||||
new AssetStudioGlyphMaterialArtifactBinding("artifact-2", "fp-2")))),
|
||||
List.of(new AssetStudioGlyphMaterialPalette(
|
||||
"palette-1",
|
||||
"Forest",
|
||||
"schema-1",
|
||||
2,
|
||||
List.of("#8B4513", "#00AA00"))),
|
||||
null);
|
||||
service.writeGlyphMaterialMetadata(assetRoot, metadata);
|
||||
|
||||
final AssetStudioMetadataSnapshot snapshot = service.read(assetRoot, AssetFamilyCatalog.GLYPH_BANK);
|
||||
|
||||
assertNotNull(snapshot.glyphMaterialMetadata());
|
||||
assertEquals(1, snapshot.glyphMaterialMetadata().schemas().size());
|
||||
assertEquals("schema-1", snapshot.glyphMaterialMetadata().schemas().getFirst().schemaId());
|
||||
assertEquals("#8B4513", snapshot.glyphMaterialMetadata().schemas().getFirst().materialIndices().get(1));
|
||||
assertEquals(1, snapshot.glyphMaterialMetadata().palettes().size());
|
||||
assertEquals("Forest", snapshot.glyphMaterialMetadata().palettes().getFirst().displayName());
|
||||
assertEquals(List.of("#8B4513", "#00AA00"), snapshot.glyphMaterialMetadata().palettes().getFirst().colors());
|
||||
}
|
||||
|
||||
@Test
|
||||
void keepsHtmlRgbAsCanonicalGlyphMaterialColorAndDerivesRgb565() {
|
||||
final AssetStudioGlyphMaterialPalette palette = new AssetStudioGlyphMaterialPalette(
|
||||
"palette-1",
|
||||
"schema-1",
|
||||
1,
|
||||
List.of("#FF0000", "#00FF00", "#0000FF"));
|
||||
|
||||
assertEquals(List.of("#FF0000", "#00FF00", "#0000FF"), palette.colors());
|
||||
assertEquals(List.of(0xF800, 0x07E0, 0x001F), palette.convertedRgb565Colors());
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void readsSceneBankSupportMetadataWhenContractIsValid() throws Exception {
|
||||
final Path assetRoot = tempDir.resolve("scene");
|
||||
Files.createDirectories(assetRoot);
|
||||
Files.writeString(assetRoot.resolve(AssetStudioMetadataService.SCENE_BANK_SUPPORT_FILE), """
|
||||
{
|
||||
"schema_version": 1,
|
||||
"map_width": 20,
|
||||
"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": "" }
|
||||
]
|
||||
}
|
||||
""");
|
||||
|
||||
final AssetStudioMetadataSnapshot snapshot = service.read(assetRoot, AssetFamilyCatalog.SCENE_BANK);
|
||||
|
||||
assertNotNull(snapshot.sceneBankMetadata());
|
||||
assertEquals(20, snapshot.sceneBankMetadata().mapWidth());
|
||||
assertEquals(12, snapshot.sceneBankMetadata().mapHeight());
|
||||
assertEquals(4, snapshot.sceneBankMetadata().layerCount());
|
||||
assertEquals(10, snapshot.sceneBankMetadata().layerBindings().get(1).width());
|
||||
assertEquals(8, snapshot.sceneBankMetadata().layerBindings().get(1).height());
|
||||
assertEquals(
|
||||
java.util.List.of("ground.tmx", "collision.tmx"),
|
||||
snapshot.sceneBankMetadata().layerBindings().stream()
|
||||
.filter(AssetStudioSceneLayerBinding::enabled)
|
||||
.map(AssetStudioSceneLayerBinding::tilemap)
|
||||
.toList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsSceneBankSupportMetadataWhenLayerCountIsNotFixedAtFour() throws Exception {
|
||||
final Path assetRoot = tempDir.resolve("scene-invalid");
|
||||
Files.createDirectories(assetRoot);
|
||||
Files.writeString(assetRoot.resolve(AssetStudioMetadataService.SCENE_BANK_SUPPORT_FILE), """
|
||||
{
|
||||
"schema_version": 1,
|
||||
"map_width": 16,
|
||||
"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": "" }
|
||||
]
|
||||
}
|
||||
""");
|
||||
|
||||
final AssetStudioMetadataSnapshot snapshot = service.read(assetRoot, AssetFamilyCatalog.SCENE_BANK);
|
||||
|
||||
assertNull(snapshot.sceneBankMetadata());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,204 @@
|
||||
package p.studio.workspaces.assets.scene;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import p.packer.dtos.PackerCodecConfigurationFieldDTO;
|
||||
import p.packer.messages.AssetReference;
|
||||
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||
import p.packer.messages.assets.OutputCodecCatalog;
|
||||
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.AssetStudioSceneBankMetadata;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneLayerBinding;
|
||||
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.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();
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void validateThenAcceptPromotesSceneBankToReadyUntilFilesChange() 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 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());
|
||||
}
|
||||
|
||||
@Test
|
||||
void validationFailsWhenReferencedTsxIsMissing() throws Exception {
|
||||
final ProjectReference project = projectReference();
|
||||
final Path sceneRoot = project.rootPath().resolve("assets/scenes/broken");
|
||||
Files.createDirectories(sceneRoot);
|
||||
final AssetWorkspaceAssetDetails brokenScene = new AssetWorkspaceAssetDetails(
|
||||
new AssetWorkspaceAssetSummary(
|
||||
AssetReference.forAssetId(2),
|
||||
"broken_scene",
|
||||
AssetWorkspaceAssetState.REGISTERED,
|
||||
AssetWorkspaceBuildParticipation.EXCLUDED,
|
||||
2,
|
||||
AssetFamilyCatalog.SCENE_BANK,
|
||||
AssetStudioGlyphSpecialization.NONE,
|
||||
sceneRoot,
|
||||
false,
|
||||
false),
|
||||
List.of(),
|
||||
OutputFormatCatalog.SCENE_TILED_V1,
|
||||
OutputCodecCatalog.NONE,
|
||||
List.of(OutputCodecCatalog.NONE),
|
||||
Map.of(OutputCodecCatalog.NONE, List.of()),
|
||||
List.of(),
|
||||
Map.of(),
|
||||
new AssetWorkspaceBankCompositionDetails(List.of(), List.of(), 0L),
|
||||
List.of(),
|
||||
null,
|
||||
new AssetStudioSceneBankMetadata(
|
||||
8,
|
||||
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", "")),
|
||||
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")));
|
||||
}
|
||||
|
||||
private ProjectReference projectReference() {
|
||||
return new ProjectReference("main", "1", "pbs", 1, tempDir.resolve("project"));
|
||||
}
|
||||
|
||||
private AssetWorkspaceAssetDetails createValidScene(ProjectReference project) throws Exception {
|
||||
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());
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
private AssetWorkspaceAssetDetails sceneDetails(Path assetRoot) {
|
||||
return new AssetWorkspaceAssetDetails(
|
||||
new AssetWorkspaceAssetSummary(
|
||||
AssetReference.forAssetId(2),
|
||||
"overworld_scene",
|
||||
AssetWorkspaceAssetState.REGISTERED,
|
||||
AssetWorkspaceBuildParticipation.EXCLUDED,
|
||||
2,
|
||||
AssetFamilyCatalog.SCENE_BANK,
|
||||
AssetStudioGlyphSpecialization.NONE,
|
||||
assetRoot,
|
||||
false,
|
||||
false),
|
||||
List.of(),
|
||||
OutputFormatCatalog.SCENE_TILED_V1,
|
||||
OutputCodecCatalog.NONE,
|
||||
List.of(OutputCodecCatalog.NONE),
|
||||
Map.of(OutputCodecCatalog.NONE, List.of()),
|
||||
List.of(),
|
||||
Map.of(),
|
||||
new AssetWorkspaceBankCompositionDetails(List.of(), List.of(), 0L),
|
||||
List.of(),
|
||||
null,
|
||||
new AssetStudioSceneBankMetadata(
|
||||
16,
|
||||
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", "")),
|
||||
assetRoot.resolve("scene-bank.studio.json")),
|
||||
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||
List.of());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,149 @@
|
||||
package p.studio.workspaces.assets.tiled;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import p.packer.dtos.PackerCodecConfigurationFieldDTO;
|
||||
import p.packer.messages.AssetReference;
|
||||
import p.packer.messages.assets.AssetFamilyCatalog;
|
||||
import p.packer.messages.assets.OutputCodecCatalog;
|
||||
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.AssetStudioSceneBankMetadata;
|
||||
import p.studio.workspaces.assets.metadata.AssetStudioSceneLayerBinding;
|
||||
import p.studio.workspaces.assets.messages.*;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
final class TiledAssetGenerationServiceTest {
|
||||
private final TiledAssetGenerationService service = new TiledAssetGenerationService();
|
||||
private final TiledXmlCodec codec = new TiledXmlCodec();
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void generatesTsxForTilesetSpecializedGlyphBank() 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));
|
||||
|
||||
assertTrue(result.success());
|
||||
final Path tsxPath = assetRoot.resolve(TiledAssetGenerationService.GENERATED_TSX_FILE);
|
||||
assertTrue(Files.isRegularFile(tsxPath));
|
||||
final TiledTilesetDocument tileset = codec.readTileset(tsxPath);
|
||||
assertEquals(2, tileset.tiles().size());
|
||||
assertEquals("a.png", tileset.tiles().get(0).imageSource());
|
||||
assertEquals("glyph_id", tileset.tiles().get(0).properties().getFirst().name());
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatesTmxFilesForSceneBankUsingReferencedTsx() 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());
|
||||
|
||||
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);
|
||||
|
||||
assertTrue(result.success());
|
||||
final Path tmxPath = sceneRoot.resolve("ground.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("Ground", map.tileLayers().getFirst().name());
|
||||
}
|
||||
|
||||
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()),
|
||||
new AssetWorkspaceBankCompositionFile("b.png", "b.png", 1L, 1L, null, Map.of())),
|
||||
0L),
|
||||
List.of(),
|
||||
null,
|
||||
null,
|
||||
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||
List.of());
|
||||
}
|
||||
|
||||
private AssetWorkspaceAssetDetails sceneDetails(Path assetRoot) {
|
||||
return new AssetWorkspaceAssetDetails(
|
||||
new AssetWorkspaceAssetSummary(
|
||||
AssetReference.forAssetId(2),
|
||||
"overworld_scene",
|
||||
AssetWorkspaceAssetState.REGISTERED,
|
||||
AssetWorkspaceBuildParticipation.EXCLUDED,
|
||||
2,
|
||||
AssetFamilyCatalog.SCENE_BANK,
|
||||
AssetStudioGlyphSpecialization.NONE,
|
||||
assetRoot,
|
||||
false,
|
||||
false),
|
||||
List.of(),
|
||||
OutputFormatCatalog.SCENE_TILED_V1,
|
||||
OutputCodecCatalog.NONE,
|
||||
List.of(OutputCodecCatalog.NONE),
|
||||
Map.of(OutputCodecCatalog.NONE, List.of()),
|
||||
List.of(),
|
||||
Map.of(),
|
||||
new AssetWorkspaceBankCompositionDetails(List.of(), List.of(), 0L),
|
||||
List.of(),
|
||||
null,
|
||||
new AssetStudioSceneBankMetadata(
|
||||
16,
|
||||
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", "")),
|
||||
assetRoot.resolve("scene-bank.studio.json")),
|
||||
AssetWorkspaceSceneBankValidation.NOT_APPLICABLE,
|
||||
List.of());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
package p.studio.workspaces.assets.tiled;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
final class TiledXmlCodecTest {
|
||||
private final TiledXmlCodec codec = new TiledXmlCodec();
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void readsFixtureTmxAndPreservesWaveOneSurfaces() throws Exception {
|
||||
final TiledMapDocument map = codec.readMap(Path.of("..", "test-projects", "main", "assets", "scenes", "primeiro mapa.tmx").toAbsolutePath().normalize());
|
||||
|
||||
assertEquals(25, map.width());
|
||||
assertEquals(25, map.height());
|
||||
assertEquals(1, map.tilesets().size());
|
||||
assertEquals("../Zelda3/primeiro tileset.tsx", map.tilesets().getFirst().source());
|
||||
assertEquals(1, map.tileLayers().size());
|
||||
assertEquals(625, map.tileLayers().getFirst().gids().size());
|
||||
assertEquals(1, map.objectLayers().size());
|
||||
assertEquals(9, map.objectLayers().getFirst().objects().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void readsFixtureTsxWithPropertiesAndCollisionObjects() throws Exception {
|
||||
final TiledTilesetDocument tileset = codec.readTileset(Path.of("..", "test-projects", "main", "assets", "Zelda3", "primeiro tileset.tsx").toAbsolutePath().normalize());
|
||||
|
||||
assertEquals(32, tileset.tileWidth());
|
||||
assertEquals(32, tileset.tileHeight());
|
||||
assertEquals(13, tileset.tileCount());
|
||||
assertEquals(13, tileset.tiles().size());
|
||||
assertEquals("glyph_id", tileset.tiles().getFirst().properties().getFirst().name());
|
||||
assertNotNull(tileset.tiles().getFirst().collisionLayer());
|
||||
assertEquals(2, tileset.tiles().getFirst().collisionLayer().objects().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnsupportedInfiniteMaps() throws Exception {
|
||||
final Path path = tempDir.resolve("infinite.tmx");
|
||||
Files.writeString(path, """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.1" orientation="orthogonal" renderorder="right-down"
|
||||
width="10" height="10" tilewidth="16" tileheight="16" infinite="1"/>
|
||||
""");
|
||||
|
||||
assertThrows(TiledUnsupportedFeatureException.class, () -> codec.readMap(path));
|
||||
}
|
||||
}
|
||||
@ -1592,5 +1592,319 @@
|
||||
"blocking" : true
|
||||
} ]
|
||||
} ]
|
||||
}, {
|
||||
"asset_id" : 16,
|
||||
"contract_fingerprint" : "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"files" : [ ]
|
||||
}, {
|
||||
"asset_id" : 17,
|
||||
"contract_fingerprint" : "058b188cc68f9b9780825f8072d7ab316b042d66c80a76a344e06313cb49ad01",
|
||||
"files" : [ {
|
||||
"relative_path" : "tileset-001.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 92,
|
||||
"last_modified" : 1776880392312,
|
||||
"fingerprint" : "8771c3e294d0edbc8dd3aaa6995a6db32166ae6bfb3e063170cc79b298d17e71",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQ=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -12019640 ],
|
||||
"convertedRgb565" : [ 19657 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.001.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 184,
|
||||
"last_modified" : 1776880490117,
|
||||
"fingerprint" : "5d81eef8be13043330b42b546e10beed8ba00dfaa42df67477a8778c2b9484d7",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQEBAQEBAQEBAQEBAQICAgECAgICAgICAgMEBQUFBQQDBAUGBgYGBQMEBQYGBwcIAwQFCAYGCAgBAwUFCAgIBw=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -12019640, -12552128, -14129088, -14133200, -14145496, -7835584, -5730208, -10465232 ],
|
||||
"convertedRgb565" : [ 19657, 17352, 11080, 10950, 10565, -29816, -21364, 25222 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.002.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 172,
|
||||
"last_modified" : 1776962843559,
|
||||
"fingerprint" : "3250840c4fe08c134ea966ce7c8fcaccae2c2a45660d7173c5e262a2e5076548",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQEBAQECAwMBAQEBAgMEBQEBAQIDBAUFAQIDAwQEBAQDAwYFBQUFBQQGBQUGBgYGBQUFBgYFBQUFBgYGBQUGBg=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -12552128, -14133200, -14145496, -10465232, -5730208, -7835584 ],
|
||||
"convertedRgb565" : [ 17352, 10950, 10565, 25222, -21364, -29816 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.003.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 191,
|
||||
"last_modified" : 1776880575940,
|
||||
"fingerprint" : "fe3ab2b7eb3b10a042de14484d53e718e6b505edb1ad19053668565bfe3a8f25",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQECAwMDAwMEBQECAwMDAwQEBQECAwMDBQUFBQEBAgMEBAQEBAYBAQYGBgYEBAYFBAQEBgYEBAQGBgQEBgYGBA=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -14145496, -14133200, -12552128, -5730208, -10465232, -7835584 ],
|
||||
"convertedRgb565" : [ 10565, 10950, 17352, -21364, 25222, -29816 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.004.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 193,
|
||||
"last_modified" : 1776880597961,
|
||||
"fingerprint" : "7fcbf90e1e9a69d6ce3be80ea1901d66c9992f68dc72e4c636dcd2ad52fa2898",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQEBAQEBAQECAgIBAQEBAQICAgICAgIBAwQEBAQDBQIEBgYGBgQDBQcICAYGBAMFBwcGBgcEAwUIBwcHBAQFAQ=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -12019640, -12552128, -14133200, -14145496, -14129088, -7835584, -10465232, -5730208 ],
|
||||
"convertedRgb565" : [ 19657, 17352, 10950, 10565, 11080, -29816, 25222, -21364 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.005.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 171,
|
||||
"last_modified" : 1776880624257,
|
||||
"fingerprint" : "451fc8d646049944d5aa7c7dc254636eec0151aaff6e2051059304b9d6ebc31e",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQIDBAQFBgYBAQIDBAYGBwEBAQMEBgcHAQEBBAQGBwcBAQQFBAYHBwEEBwUEBgcHBAcGBQQGBwcEBwYFBAYHBw=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -12019640, -14129088, -14133200, -14145496, -10465232, -5730208, -7835584 ],
|
||||
"convertedRgb565" : [ 19657, 11080, 10950, 10565, 25222, -21364, -29816 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.006.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 174,
|
||||
"last_modified" : 1776880647207,
|
||||
"fingerprint" : "aa757df4600823ec6655235a1081061777124ccee74b31481bbe402e53635f53",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQECAwMBAQEBAgIBAwMBAgICAQEBAwMCAgIBAQIDBAMCAgEBAgMEBAICAQECAwQEAgIBAQIDBAMCAgIBAwQDAg=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -7835584, -5730208, -10465232, -14145496 ],
|
||||
"convertedRgb565" : [ -29816, -21364, 25222, 10565 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.007.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 171,
|
||||
"last_modified" : 1776880669446,
|
||||
"fingerprint" : "38162157f5685cce25b835d3a2caee3465fd2ec3ee5db2a5089d7d5ae5ea3bb7",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQEBAgIDAQEDAQICAQMDAQMCAgEBAQMDAgQCAwEBAwMEBAIDAQEDAwQEAgMBAQMDAgQCAwEBAwMDAgQCAQMDAw=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -7835584, -10465232, -5730208, -14145496 ],
|
||||
"convertedRgb565" : [ -29816, 25222, -21364, 10565 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.008.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 170,
|
||||
"last_modified" : 1776880691747,
|
||||
"fingerprint" : "8cab5b3d553af9ed371bd19fc624763accc90e5d926d300ba31dd8d9d6bab803",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQECAwMEBQYHAQEDBAUGBgcHAQMEBgYGBwcBAwMGBgYHBwEDAgMGBgcHAQMCBwMGBwcBAwIBBwMHBwEDAgEHAw=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -5730208, -10465232, -14145496, -14133200, -14129088, -12019640, -7835584 ],
|
||||
"convertedRgb565" : [ -21364, 25222, 10565, 10950, 11080, 19657, -29816 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.009.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 201,
|
||||
"last_modified" : 1776881019843,
|
||||
"fingerprint" : "d9ff5a6df0f65f5fcfb8f22376beeb266d429e2c6e9c82f51c12bc6ee4c3fcdc",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQIDAgIBBAMFAQECAgEEBAUGBQECAgEEBwYFBQECAQEHBwYGBQEBAgcHBwYFAQMDBwcHBwEBAwMHBwcFAQICAw=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -14145496, -10465232, -7835584, -5730208, -14133200, -14129088, -12019640 ],
|
||||
"convertedRgb565" : [ 10565, 25222, -29816, -21364, 10950, 11080, 19657 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.010.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 195,
|
||||
"last_modified" : 1776881039443,
|
||||
"fingerprint" : "b9ff45c77f7779a6acebcc537c2ddac0e27e91162b656a04dc4772ec5f24fc26",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQICAwMBAQIBAQMDAgIBAQIBAQEBAgIBAgICAgEBAgIEAwMCAgEBAQMEAwMCAgEBAQMEAwMDAgIBAwMEAwMDAw=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -7835584, -5730208, -10465232, -14145496 ],
|
||||
"convertedRgb565" : [ -29816, -21364, 25222, 10565 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.011.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 188,
|
||||
"last_modified" : 1776881059396,
|
||||
"fingerprint" : "43f370324fedccda888d49409e2a64f182f08b4143b4bdd5bafaf498feebd21a",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQICAwMBAQICAgEBAwMCAgIBAQICAgIBAQECAgEBAQECAgIBAQMDBAICAQEDAwQDAQEDAwMEAwIDAwMDBAMDAg=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -5730208, -7835584, -10465232, -14145496 ],
|
||||
"convertedRgb565" : [ -21364, -29816, 25222, 10565 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.012.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 203,
|
||||
"last_modified" : 1776881081851,
|
||||
"fingerprint" : "cb047e655f4e196f22ca0048149df5dcb959f895faeaeb94920182d4a2d16b38",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQIDBAQBBAMCAgMEBAMDBQIDBAQDBQYFAwMEAwUFBgcEAwMFBgYHBwEBAwUGBwcHAQEDAwcHBwcBBAQDBQcHBw=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -7835584, -5730208, -14145496, -10465232, -14133200, -14129088, -12019640 ],
|
||||
"convertedRgb565" : [ -29816, -21364, 10565, 25222, 10950, 11080, 19657 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.013.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 186,
|
||||
"last_modified" : 1776881102592,
|
||||
"fingerprint" : "25eba17b67dd5159f5fb9d3edd531d54a44d1c76f997385840dac53517b98886",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQEBAgMDBAQBAQIDAwMDBAEBAgMDAwMEAQECAwMEAgQBAQIEAgUFAgEBBQUGBQYFAQEBAQEGAQYBAQEBAQEBAQ=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -12019640, -14145496, -7835584, -10465232, -14133200, -14129088 ],
|
||||
"convertedRgb565" : [ 19657, 10565, -29816, 25222, 10950, 11080 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.014.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 184,
|
||||
"last_modified" : 1776881122741,
|
||||
"fingerprint" : "32ca96c14290de8009fd2db623f55514bed2345b7d5881a80dd3fe6235012854",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQEBAgEBAgICAQICAQIBAQICAwICAQEEAgMDAwIBAQQDAwUDAwIBAQUFBgUFAgEEBgYGBgYDAgEGBgYGBgYDAg=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -10465232, -14145496, -14133200, -7835584, -14129088, -12019640 ],
|
||||
"convertedRgb565" : [ 25222, 10565, 10950, -29816, 11080, 19657 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.015.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 175,
|
||||
"last_modified" : 1776881143481,
|
||||
"fingerprint" : "7a093fe00c805bcf38d4facff14a6d4c2b8baeaa2d553fa8d3db9f2d76b32f3b",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQECAgECAgICAgECAQECAQMCAgEBBAEBAwICAQQEBAECAgEEBAUEBAMCAQUFBgUFAgEEBgYGBgYBBAYGBgYGBg=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -14145496, -10465232, -7835584, -14133200, -14129088, -12019640 ],
|
||||
"convertedRgb565" : [ 10565, 25222, -29816, 10950, 11080, 19657 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
}, {
|
||||
"relative_path" : "tileset-002.016.png",
|
||||
"mime_type" : "image/png",
|
||||
"size" : 192,
|
||||
"last_modified" : 1776881170765,
|
||||
"fingerprint" : "77c5277220421226d16f58b1bcfb4eaf157fa4f6c740c5aaebefaf88fb91c22e",
|
||||
"metadata" : {
|
||||
"tile" : {
|
||||
"width" : 8,
|
||||
"height" : 8,
|
||||
"paletteIndices" : "AQECAgMEBAQBAgICAgMEBAECAgICAwQEAQMBAgIDBAQDBQUDAQMEBAUGBQYFBQQEBgQGBAQEBAQEBAQEBAQEBA=="
|
||||
},
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -10465232, -7835584, -14145496, -12019640, -14133200, -14129088 ],
|
||||
"convertedRgb565" : [ 25222, -29816, 10565, 19657, 10950, 11080 ]
|
||||
}
|
||||
},
|
||||
"diagnostics" : [ ]
|
||||
} ]
|
||||
} ]
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"next_asset_id" : 16,
|
||||
"next_asset_id" : 18,
|
||||
"assets" : [ {
|
||||
"asset_id" : 3,
|
||||
"asset_uuid" : "21953cb8-4101-4790-9e5e-d95f5fbc9b5a",
|
||||
@ -41,5 +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
|
||||
} ]
|
||||
}
|
||||
@ -25,27 +25,43 @@
|
||||
},
|
||||
"artifacts" : [ {
|
||||
"file" : "link00.png",
|
||||
"index" : 0
|
||||
"index" : 0,
|
||||
"artifact_id" : "art-b82d5371eb9c",
|
||||
"fingerprint" : "c363f3ef7f32d9b249c7dc8babdb3eb5aae68c524099e5e97290202b054dda71"
|
||||
}, {
|
||||
"file" : "link01.png",
|
||||
"index" : 1
|
||||
"index" : 1,
|
||||
"artifact_id" : "art-acf4398779de",
|
||||
"fingerprint" : "e2130efbb9643b41a4fb62619f2713402f9622c7db040fe18d929609ef70b89a"
|
||||
}, {
|
||||
"file" : "link02.png",
|
||||
"index" : 2
|
||||
"index" : 2,
|
||||
"artifact_id" : "art-88d5de66ceea",
|
||||
"fingerprint" : "9216dfc6a6226fac398e9f781550d23fbfaa65bd377cdda69187149beca114ec"
|
||||
}, {
|
||||
"file" : "link03.png",
|
||||
"index" : 3
|
||||
"index" : 3,
|
||||
"artifact_id" : "art-18a3b61a9561",
|
||||
"fingerprint" : "ebc4b7ca7bb1455288681c5d71424f60f658a44cb343d0c3934329cec676c867"
|
||||
}, {
|
||||
"file" : "link04.png",
|
||||
"index" : 4
|
||||
"index" : 4,
|
||||
"artifact_id" : "art-231ecb85b204",
|
||||
"fingerprint" : "c363f3ef7f32d9b249c7dc8babdb3eb5aae68c524099e5e97290202b054dda71"
|
||||
}, {
|
||||
"file" : "link05.png",
|
||||
"index" : 5
|
||||
"index" : 5,
|
||||
"artifact_id" : "art-f36b8455db22",
|
||||
"fingerprint" : "1ba8ce75c445396334737143e0cfa56c87a1fb64cca8ad0962192594849249eb"
|
||||
}, {
|
||||
"file" : "link06.png",
|
||||
"index" : 6
|
||||
"index" : 6,
|
||||
"artifact_id" : "art-884d6b34d741",
|
||||
"fingerprint" : "5e0954447699c6d5eac7550f1a10cf520efcddad0b9fbd9a716434da9d9550d4"
|
||||
}, {
|
||||
"file" : "link07.png",
|
||||
"index" : 7
|
||||
"index" : 7,
|
||||
"artifact_id" : "art-ca5e39fb056c",
|
||||
"fingerprint" : "6104ba7f216a937994d478ed07766ec28f48ad72fdd6620508535a1f877b8ddb"
|
||||
} ]
|
||||
}
|
||||
47
test-projects/main/assets/Zelda3/glyph-material.json
Normal file
47
test-projects/main/assets/Zelda3/glyph-material.json
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"schemas" : [ {
|
||||
"schema_id" : "schema-1",
|
||||
"status" : "ok",
|
||||
"version" : 1,
|
||||
"material_indices" : {
|
||||
"1" : "#202020",
|
||||
"2" : "#519210",
|
||||
"3" : "#71B220",
|
||||
"4" : "#F3C320",
|
||||
"5" : "#C38200",
|
||||
"6" : "#B26120",
|
||||
"7" : "#F3F3F3",
|
||||
"8" : "#F3A261",
|
||||
"9" : "#E39251",
|
||||
"10" : "#614120",
|
||||
"11" : "#B28251"
|
||||
},
|
||||
"artifact_bindings" : [ {
|
||||
"artifact_id" : "art-b82d5371eb9c",
|
||||
"fingerprint" : "c363f3ef7f32d9b249c7dc8babdb3eb5aae68c524099e5e97290202b054dda71"
|
||||
}, {
|
||||
"artifact_id" : "art-acf4398779de",
|
||||
"fingerprint" : "e2130efbb9643b41a4fb62619f2713402f9622c7db040fe18d929609ef70b89a"
|
||||
}, {
|
||||
"artifact_id" : "art-88d5de66ceea",
|
||||
"fingerprint" : "9216dfc6a6226fac398e9f781550d23fbfaa65bd377cdda69187149beca114ec"
|
||||
}, {
|
||||
"artifact_id" : "art-18a3b61a9561",
|
||||
"fingerprint" : "ebc4b7ca7bb1455288681c5d71424f60f658a44cb343d0c3934329cec676c867"
|
||||
}, {
|
||||
"artifact_id" : "art-231ecb85b204",
|
||||
"fingerprint" : "c363f3ef7f32d9b249c7dc8babdb3eb5aae68c524099e5e97290202b054dda71"
|
||||
}, {
|
||||
"artifact_id" : "art-f36b8455db22",
|
||||
"fingerprint" : "1ba8ce75c445396334737143e0cfa56c87a1fb64cca8ad0962192594849249eb"
|
||||
}, {
|
||||
"artifact_id" : "art-884d6b34d741",
|
||||
"fingerprint" : "5e0954447699c6d5eac7550f1a10cf520efcddad0b9fbd9a716434da9d9550d4"
|
||||
}, {
|
||||
"artifact_id" : "art-ca5e39fb056c",
|
||||
"fingerprint" : "6104ba7f216a937994d478ed07766ec28f48ad72fdd6620508535a1f877b8ddb"
|
||||
} ]
|
||||
} ],
|
||||
"palettes" : [ ]
|
||||
}
|
||||
50
test-projects/main/assets/Zelda3/primeiro tileset.tsx
Normal file
50
test-projects/main/assets/Zelda3/primeiro tileset.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<tileset version="1.10" tiledversion="1.12.1" name="primeiro tileset" tilewidth="32" tileheight="32" tilecount="13" columns="0">
|
||||
<grid orientation="orthogonal" width="1" height="1"/>
|
||||
<tile id="1">
|
||||
<image source="flag00.png" width="32" height="32"/>
|
||||
</tile>
|
||||
<tile id="2">
|
||||
<image source="flag01.png" width="32" height="32"/>
|
||||
</tile>
|
||||
<tile id="3">
|
||||
<image source="flag02.png" width="32" height="32"/>
|
||||
</tile>
|
||||
<tile id="4">
|
||||
<image source="flag03.png" width="32" height="32"/>
|
||||
</tile>
|
||||
<tile id="5">
|
||||
<image source="link00.png" width="32" height="32"/>
|
||||
</tile>
|
||||
<tile id="6">
|
||||
<image source="link01.png" width="32" height="32"/>
|
||||
</tile>
|
||||
<tile id="7">
|
||||
<image source="link02.png" width="32" height="32"/>
|
||||
</tile>
|
||||
<tile id="8">
|
||||
<image source="link03.png" width="32" height="32"/>
|
||||
</tile>
|
||||
<tile id="9">
|
||||
<image source="link04.png" width="32" height="32"/>
|
||||
</tile>
|
||||
<tile id="10">
|
||||
<image source="link05.png" width="32" height="32"/>
|
||||
</tile>
|
||||
<tile id="11">
|
||||
<image source="link06.png" width="32" height="32"/>
|
||||
</tile>
|
||||
<tile id="12">
|
||||
<image source="link07.png" width="32" height="32"/>
|
||||
</tile>
|
||||
<tile id="0">
|
||||
<properties>
|
||||
<property name="glyph_id" type="int" value="0"/>
|
||||
</properties>
|
||||
<image source="../recovered/atlas2/confirm.png" width="16" height="16"/>
|
||||
<objectgroup draworder="index" id="2">
|
||||
<object id="1" x="4" y="4" width="8" height="8"/>
|
||||
<object id="2" x="1.59375" y="0.15625" width="2.75" height="2.90625"/>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
</tileset>
|
||||
4
test-projects/main/assets/Zelda3/studio.asset.json
Normal file
4
test-projects/main/assets/Zelda3/studio.asset.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"glyph_bank_specialization" : "sprites"
|
||||
}
|
||||
4
test-projects/main/assets/bigode/studio.asset.json
Normal file
4
test-projects/main/assets/bigode/studio.asset.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"glyph_bank_specialization" : "sprites"
|
||||
}
|
||||
@ -16,6 +16,8 @@
|
||||
},
|
||||
"artifacts" : [ {
|
||||
"file" : "confirm.png",
|
||||
"index" : 0
|
||||
"index" : 0,
|
||||
"artifact_id" : "art-8c6bdad24eb1",
|
||||
"fingerprint" : "aa7d241deabcebe29a6096e14eaf16fdc06cf06380c11a507620b00fc7bff094"
|
||||
} ]
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"glyph_bank_specialization" : "sprites"
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"glyph_bank_specialization" : "sprites"
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"glyph_bank_specialization" : "sprites"
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.12.1" orientation="orthogonal" renderorder="right-down" width="25" height="25" tilewidth="16" tileheight="16" infinite="0" nextlayerid="4" nextobjectid="12">
|
||||
<tileset firstgid="1" source="primeiro tileset.tsx"/>
|
||||
<map version="1.10" tiledversion="1.12.1" orientation="orthogonal" renderorder="right-down" width="25" height="25" tilewidth="16" tileheight="16" infinite="0" nextlayerid="6" nextobjectid="20">
|
||||
<tileset firstgid="1" source="../Zelda3/primeiro tileset.tsx"/>
|
||||
<layer id="1" name="Tile Layer 1" width="25" height="25">
|
||||
<data encoding="csv">
|
||||
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
|
||||
@ -32,5 +32,15 @@
|
||||
</layer>
|
||||
<objectgroup id="3" name="Object Layer 1">
|
||||
<object id="11" x="16" y="16" width="64" height="64"/>
|
||||
<object id="12" x="179.608" y="48.6275">
|
||||
<polygon points="0,0 1.17647,50.1961 46.2745,51.3725 44.7059,19.2157 12.9412,19.6078 12.549,-0.784314"/>
|
||||
</object>
|
||||
<object id="13" x="116.078" y="26.2745" width="34.902" height="39.2157"/>
|
||||
<object id="14" x="96" y="96" width="52.2353" height="45.9608"/>
|
||||
<object id="15" x="160" y="112" width="32" height="32"/>
|
||||
<object id="16" x="16" y="96" width="64" height="32"/>
|
||||
<object id="17" x="96" y="0" width="80" height="64"/>
|
||||
<object id="18" x="153.725" y="92.1569"/>
|
||||
<object id="19" x="98.0392" y="-48.2353" height="23.5294"/>
|
||||
</objectgroup>
|
||||
</map>
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<tileset version="1.10" tiledversion="1.12.1" name="primeiro tileset" tilewidth="16" tileheight="16" tilecount="1" columns="0">
|
||||
<grid orientation="orthogonal" width="1" height="1"/>
|
||||
<tile id="0">
|
||||
<image source="../recovered/atlas2/confirm.png" width="16" height="16"/>
|
||||
</tile>
|
||||
</tileset>
|
||||
@ -1,14 +0,0 @@
|
||||
{
|
||||
"automappingRulesFile": "",
|
||||
"commands": [
|
||||
],
|
||||
"compatibilityVersion": 1100,
|
||||
"extensionsPath": "extensions",
|
||||
"folders": [
|
||||
"."
|
||||
],
|
||||
"properties": [
|
||||
],
|
||||
"propertyTypes": [
|
||||
]
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
{
|
||||
"activeFile": "primeiro tileset.tsx",
|
||||
"expandedProjectPaths": [
|
||||
"."
|
||||
],
|
||||
"fileStates": {
|
||||
"primeiro mapa.tmx": {
|
||||
"scale": 2.55,
|
||||
"selectedLayer": 1,
|
||||
"viewCenter": {
|
||||
"x": 200,
|
||||
"y": 200.58823529411768
|
||||
}
|
||||
},
|
||||
"primeiro tileset.tsx": {
|
||||
"dynamicWrapping": true,
|
||||
"scaleInDock": 1,
|
||||
"scaleInEditor": 1
|
||||
}
|
||||
},
|
||||
"last.imagePath": "/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/studio/test-projects/main/assets/recovered/atlas2",
|
||||
"map.height": 25,
|
||||
"map.lastUsedFormat": "tmx",
|
||||
"map.tileHeight": 16,
|
||||
"map.tileWidth": 16,
|
||||
"map.width": 25,
|
||||
"openFiles": [
|
||||
"primeiro mapa.tmx",
|
||||
"primeiro tileset.tsx"
|
||||
],
|
||||
"project": "scene1.tiled-project",
|
||||
"recentFiles": [
|
||||
"primeiro mapa.tmx",
|
||||
"primeiro tileset.tsx"
|
||||
],
|
||||
"tileset.lastUsedFormat": "tsx",
|
||||
"tileset.type": 1
|
||||
}
|
||||
14
test-projects/main/assets/tiled/asset.json
Normal file
14
test-projects/main/assets/tiled/asset.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
24
test-projects/main/assets/tiled/layer-1.tmx
Normal file
24
test-projects/main/assets/tiled/layer-1.tmx
Normal file
@ -0,0 +1,24 @@
|
||||
<?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>
|
||||
39
test-projects/main/assets/tiled/scene-bank.studio.json
Normal file
39
test-projects/main/assets/tiled/scene-bank.studio.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"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" : ""
|
||||
} ]
|
||||
}
|
||||
112
test-projects/main/assets/tileset/asset.json
Normal file
112
test-projects/main/assets/tileset/asset.json
Normal file
@ -0,0 +1,112 @@
|
||||
{
|
||||
"schema_version" : 1,
|
||||
"asset_uuid" : "c12f67ad-0471-4ab1-96dd-a816cfc66462",
|
||||
"name" : "tileset",
|
||||
"type" : "glyph_bank",
|
||||
"output" : {
|
||||
"codec" : "NONE",
|
||||
"pipeline" : {
|
||||
"palettes" : [ {
|
||||
"index" : 0,
|
||||
"palette" : {
|
||||
"originalArgb8888" : [ -12019640, -12552128, -14129088, -14133200, -14145496, -7835584, -5730208, -10465232 ],
|
||||
"convertedRgb565" : [ 19657, 17352, 11080, 10950, 10565, 35720, 44172, 25222 ]
|
||||
}
|
||||
} ]
|
||||
},
|
||||
"format" : "GLYPH/indexed_v1",
|
||||
"codec_configuration" : { },
|
||||
"metadata" : {
|
||||
"tile_size" : "8x8"
|
||||
}
|
||||
},
|
||||
"preload" : {
|
||||
"enabled" : false
|
||||
},
|
||||
"artifacts" : [ {
|
||||
"artifact_id" : "art-67db3cae2605",
|
||||
"file" : "tileset-001.png",
|
||||
"index" : 0,
|
||||
"fingerprint" : "8771c3e294d0edbc8dd3aaa6995a6db32166ae6bfb3e063170cc79b298d17e71"
|
||||
}, {
|
||||
"artifact_id" : "art-ee733c010b67",
|
||||
"file" : "tileset-002.001.png",
|
||||
"index" : 1,
|
||||
"fingerprint" : "5d81eef8be13043330b42b546e10beed8ba00dfaa42df67477a8778c2b9484d7"
|
||||
}, {
|
||||
"artifact_id" : "art-ab1efa5013f0",
|
||||
"file" : "tileset-002.002.png",
|
||||
"index" : 2,
|
||||
"fingerprint" : "3250840c4fe08c134ea966ce7c8fcaccae2c2a45660d7173c5e262a2e5076548"
|
||||
}, {
|
||||
"artifact_id" : "art-453fed2836ce",
|
||||
"file" : "tileset-002.003.png",
|
||||
"index" : 3,
|
||||
"fingerprint" : "fe3ab2b7eb3b10a042de14484d53e718e6b505edb1ad19053668565bfe3a8f25"
|
||||
}, {
|
||||
"artifact_id" : "art-52434bb77d67",
|
||||
"file" : "tileset-002.004.png",
|
||||
"index" : 4,
|
||||
"fingerprint" : "7fcbf90e1e9a69d6ce3be80ea1901d66c9992f68dc72e4c636dcd2ad52fa2898"
|
||||
}, {
|
||||
"artifact_id" : "art-edc19227b5c8",
|
||||
"file" : "tileset-002.005.png",
|
||||
"index" : 5,
|
||||
"fingerprint" : "451fc8d646049944d5aa7c7dc254636eec0151aaff6e2051059304b9d6ebc31e"
|
||||
}, {
|
||||
"artifact_id" : "art-a1131a305e06",
|
||||
"file" : "tileset-002.006.png",
|
||||
"index" : 6,
|
||||
"fingerprint" : "aa757df4600823ec6655235a1081061777124ccee74b31481bbe402e53635f53"
|
||||
}, {
|
||||
"artifact_id" : "art-e8a9cff3fb90",
|
||||
"file" : "tileset-002.007.png",
|
||||
"index" : 7,
|
||||
"fingerprint" : "38162157f5685cce25b835d3a2caee3465fd2ec3ee5db2a5089d7d5ae5ea3bb7"
|
||||
}, {
|
||||
"artifact_id" : "art-b1513b30e866",
|
||||
"file" : "tileset-002.008.png",
|
||||
"index" : 8,
|
||||
"fingerprint" : "8cab5b3d553af9ed371bd19fc624763accc90e5d926d300ba31dd8d9d6bab803"
|
||||
}, {
|
||||
"artifact_id" : "art-ff5ad76c4e5e",
|
||||
"file" : "tileset-002.009.png",
|
||||
"index" : 9,
|
||||
"fingerprint" : "d9ff5a6df0f65f5fcfb8f22376beeb266d429e2c6e9c82f51c12bc6ee4c3fcdc"
|
||||
}, {
|
||||
"artifact_id" : "art-704961ee43df",
|
||||
"file" : "tileset-002.010.png",
|
||||
"index" : 10,
|
||||
"fingerprint" : "b9ff45c77f7779a6acebcc537c2ddac0e27e91162b656a04dc4772ec5f24fc26"
|
||||
}, {
|
||||
"artifact_id" : "art-895e14841cc2",
|
||||
"file" : "tileset-002.011.png",
|
||||
"index" : 11,
|
||||
"fingerprint" : "43f370324fedccda888d49409e2a64f182f08b4143b4bdd5bafaf498feebd21a"
|
||||
}, {
|
||||
"artifact_id" : "art-21bacf6b47b9",
|
||||
"file" : "tileset-002.012.png",
|
||||
"index" : 12,
|
||||
"fingerprint" : "cb047e655f4e196f22ca0048149df5dcb959f895faeaeb94920182d4a2d16b38"
|
||||
}, {
|
||||
"artifact_id" : "art-4c80bb3fc4ba",
|
||||
"file" : "tileset-002.013.png",
|
||||
"index" : 13,
|
||||
"fingerprint" : "25eba17b67dd5159f5fb9d3edd531d54a44d1c76f997385840dac53517b98886"
|
||||
}, {
|
||||
"artifact_id" : "art-58970a0dffaf",
|
||||
"file" : "tileset-002.014.png",
|
||||
"index" : 14,
|
||||
"fingerprint" : "32ca96c14290de8009fd2db623f55514bed2345b7d5881a80dd3fe6235012854"
|
||||
}, {
|
||||
"artifact_id" : "art-8d83870d938b",
|
||||
"file" : "tileset-002.015.png",
|
||||
"index" : 15,
|
||||
"fingerprint" : "7a093fe00c805bcf38d4facff14a6d4c2b8baeaa2d553fa8d3db9f2d76b32f3b"
|
||||
}, {
|
||||
"artifact_id" : "art-b94f1f9218ea",
|
||||
"file" : "tileset-002.016.png",
|
||||
"index" : 16,
|
||||
"fingerprint" : "77c5277220421226d16f58b1bcfb4eaf157fa4f6c740c5aaebefaf88fb91c22e"
|
||||
} ]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user