├── .github └── workflows │ ├── apply-downstream.yml │ ├── create-release.yml │ ├── update-cron.yml │ ├── update-submodules.yml │ └── validate.yml ├── .gitmodules ├── README.md ├── Usage.md └── schema.json /.github/workflows/apply-downstream.yml: -------------------------------------------------------------------------------- 1 | name: Apply Downstream Changes 2 | on: 3 | workflow_dispatch: 4 | 5 | concurrency: 6 | group: ${{github.workflow}} 7 | cancel-in-progress: true 8 | 9 | permissions: 10 | contents: write 11 | packages: write 12 | 13 | jobs: 14 | apply-downstream: 15 | name: Apply Downstream 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | submodules: recursive 23 | 24 | - name: Download EXDTools 25 | uses: robinraju/release-downloader@v1 26 | with: 27 | repository: xivdev/EXDTools 28 | fileName: 'EXDTooler' 29 | latest: true 30 | 31 | - name: Chmod Executable 32 | run: chmod +x EXDTooler 33 | 34 | - name: Checkout submodules to branch 35 | run: git submodule foreach 'git checkout $(git describe --all HEAD | sed -En "s/(remotes\/origin\/|heads\/)//p")' 36 | 37 | - name: Run Column Merger 38 | run: ./EXDTooler merge-columns -b schemas 39 | 40 | - name: Commit Changes 41 | run: | 42 | rm EXDTooler 43 | git config --global user.name github-actions[bot] 44 | git config --global user.email 41898282+github-actions[bot]@users.noreply.github.com 45 | git submodule foreach ' 46 | git add . 47 | git commit -m "Upstream changes" || : 48 | git push 49 | ' 50 | git add --all 51 | git commit -m "Update submodules" || : 52 | git push -------------------------------------------------------------------------------- /.github/workflows/create-release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | on: 3 | schedule: 4 | - cron: '0 0 1 * *' # Midnight on the 1st of the month 5 | workflow_dispatch: 6 | 7 | concurrency: 8 | group: ${{github.workflow}} 9 | cancel-in-progress: true 10 | 11 | permissions: 12 | contents: write 13 | packages: write 14 | 15 | jobs: 16 | create-release: 17 | name: Create Release 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | submodules: recursive 25 | 26 | - name: Get tag metadata 27 | id: tag-meta 28 | run: | 29 | if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then 30 | echo "tag_name=$(date +'%Y.%m')-pre${{github.run_number}}" >> "$GITHUB_OUTPUT" 31 | echo "tag_prerelease=true" >> "$GITHUB_OUTPUT" 32 | echo "apply_breaking=false" >> "$GITHUB_OUTPUT" 33 | else 34 | echo "tag_name=$(date +'%Y.%m')" >> "$GITHUB_OUTPUT" 35 | echo "tag_prerelease=false" >> "$GITHUB_OUTPUT" 36 | echo "apply_breaking=true" >> "$GITHUB_OUTPUT" 37 | fi 38 | 39 | - name: Download apply.py 40 | run: | 41 | pip install ruamel.yaml 42 | wget https://raw.githubusercontent.com/xivdev/EXDTools/refs/heads/main/apply.py 43 | 44 | - name: Download EXDTools 45 | if: ${{ github.event_name == 'schedule' }} 46 | uses: robinraju/release-downloader@v1 47 | with: 48 | repository: xivdev/EXDTools 49 | fileName: 'EXDTooler' 50 | latest: true 51 | 52 | - name: Chmod Executable 53 | if: ${{ github.event_name == 'schedule' }} 54 | run: chmod +x EXDTooler 55 | 56 | - name: Checkout submodules to branch 57 | run: git submodule foreach 'git checkout $(git describe --all HEAD | sed -En "s/(remotes\/origin\/|heads\/)//p")' 58 | 59 | - name: Run Column Merger 60 | if: ${{ github.event_name == 'schedule' }} 61 | run: ./EXDTooler merge-columns -b schemas 62 | 63 | - name: Run Apply 64 | run: python apply.py ${{steps.tag-meta.outputs.apply_breaking}} 65 | 66 | - name: Commit Changes 67 | run: | 68 | rm EXDTooler || : 69 | rm apply.py 70 | git config --global user.name github-actions[bot] 71 | git config --global user.email 41898282+github-actions[bot]@users.noreply.github.com 72 | git submodule foreach ' 73 | git add . 74 | git commit -m "Creating release ${{steps.tag-meta.outputs.tag_name}}" || : 75 | git push 76 | ' 77 | git add --all 78 | git commit -m "Update submodules" || : 79 | git push 80 | 81 | - name: Zip submodules 82 | run: | 83 | mkdir -p artifacts 84 | 85 | git submodule foreach ' 86 | find . -maxdepth 1 -type f -name "*.yml" -print \ 87 | | zip -@ "$toplevel/artifacts/$(basename "$sm_path").zip" 88 | ' 89 | 90 | - name: Create Release 91 | uses: ncipollo/release-action@v1 92 | with: 93 | artifacts: artifacts/*.zip 94 | tag: ${{steps.tag-meta.outputs.tag_name}} 95 | prerelease: ${{steps.tag-meta.outputs.tag_prerelease}} 96 | 97 | 98 | -------------------------------------------------------------------------------- /.github/workflows/update-cron.yml: -------------------------------------------------------------------------------- 1 | name: Update Versions 2 | on: 3 | schedule: 4 | - cron: '40 */2 * * *' # Every 2 hours at :40 5 | workflow_dispatch: 6 | 7 | permissions: 8 | contents: write 9 | packages: write 10 | 11 | jobs: 12 | update: 13 | name: Update Versions 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | ref: latest 20 | fetch-depth: 0 21 | 22 | - name: Download EXDTools 23 | uses: robinraju/release-downloader@v1 24 | with: 25 | repository: xivdev/EXDTools 26 | fileName: 'EXDTooler' 27 | latest: true 28 | 29 | - name: Chmod Executable 30 | run: chmod +x EXDTooler 31 | 32 | - name: Retrieve cache 33 | id: exd-cache 34 | uses: actions/cache@v4 35 | with: 36 | path: data 37 | key: exd-latest 38 | 39 | - name: Download Latest Game Data 40 | id: downloader 41 | uses: WorkingRobot/ffxiv-downloader@v3 42 | with: 43 | output-path: data 44 | regex: '^sqpack\/ffxiv\/0a0000\..+$' 45 | 46 | - name: Hash Columns Old 47 | id: hash-columns-old 48 | run: | 49 | echo "hash=${{hashFiles('.github/columns.yml')}}" >> "$GITHUB_OUTPUT" 50 | 51 | - name: Run Column Exporter 52 | run: ./EXDTooler export-columns -g data/sqpack -o .github/columns.yml 53 | 54 | - name: Hash Columns New 55 | id: hash-columns-new 56 | run: | 57 | echo "hash=${{hashFiles('.github/columns.yml')}}" >> "$GITHUB_OUTPUT" 58 | 59 | - name: Create Branch 60 | if: steps.hash-columns-old.outputs.hash != steps.hash-columns-new.outputs.hash 61 | run: git branch ver/${{steps.downloader.outputs.version}} 62 | 63 | - name: Commit Files 64 | if: steps.hash-columns-old.outputs.hash != steps.hash-columns-new.outputs.hash 65 | id: commit 66 | run: | 67 | git config user.name github-actions[bot] 68 | git config user.email 41898282+github-actions[bot]@users.noreply.github.com 69 | git add .github/columns.yml 70 | git commit -m "Add column data for ${{steps.downloader.outputs.version}}" --author="${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>" 71 | 72 | - name: Push Changes 73 | if: steps.hash-columns-old.outputs.hash != steps.hash-columns-new.outputs.hash 74 | run: | 75 | git checkout ver/${{steps.downloader.outputs.version}} 76 | git reset --hard latest 77 | git push origin --all 78 | 79 | - name: Add submodule to main 80 | if: steps.hash-columns-old.outputs.hash != steps.hash-columns-new.outputs.hash 81 | run: | 82 | git checkout main 83 | git submodule add -b ver/${{steps.downloader.outputs.version}} -- https://github.com/${{github.repository}}.git schemas/${{steps.downloader.outputs.version}} 84 | git add schemas 85 | git commit -m "Add version ${{steps.downloader.outputs.version}}" --author="${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>" 86 | 87 | - name: Push new submodule 88 | if: steps.hash-columns-old.outputs.hash != steps.hash-columns-new.outputs.hash 89 | run: git push origin main 90 | 91 | -------------------------------------------------------------------------------- /.github/workflows/update-submodules.yml: -------------------------------------------------------------------------------- 1 | name: Update Submodules 2 | on: 3 | repository_dispatch: 4 | types: [version-push] 5 | 6 | concurrency: 7 | group: ${{github.workflow}} 8 | cancel-in-progress: true 9 | 10 | jobs: 11 | update-submodule: 12 | name: Update Submodules 13 | runs-on: ubuntu-latest 14 | 15 | permissions: 16 | actions: write 17 | contents: write 18 | 19 | steps: 20 | - name: Print Dispatch Info 21 | run: | 22 | echo "Actor: ${{github.event.client_payload.actor}}" 23 | echo "Head Ref: ${{github.event.client_payload.head_ref}}" 24 | echo "Head Sha: ${{github.event.client_payload.head_sha}}" 25 | echo "Base Ref: ${{github.event.client_payload.base_ref}}" 26 | 27 | - name: Checkout Main 28 | uses: actions/checkout@v4 29 | with: 30 | ref: main 31 | fetch-depth: 0 32 | submodules: recursive 33 | 34 | - name: Update Submodules 35 | run: | 36 | git submodule update --init --recursive 37 | git submodule update --recursive --remote 38 | 39 | - name: Commit Changes 40 | id: commit 41 | run: | 42 | git config user.name github-actions[bot] 43 | git config user.email 41898282+github-actions[bot]@users.noreply.github.com 44 | git add --all 45 | git commit -m "Update submodules" --author="${{github.event.client_payload.actor}} <${{github.event.client_payload.actor}}@users.noreply.github.com>" || : 46 | 47 | - name: Push Changes 48 | run: | 49 | git push 50 | 51 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | run-name: "${{github.event.client_payload.pr_number && format('PR #{0} ({1})', github.event.client_payload.pr_number, github.event.client_payload.head_ref) || format('Push (@{0})', github.event.client_payload.actor)}}" 3 | on: 4 | repository_dispatch: 5 | types: [version-pr, version-push] 6 | 7 | permissions: 8 | statuses: write 9 | pull-requests: write 10 | issues: write 11 | discussions: write 12 | contents: write 13 | checks: write 14 | 15 | concurrency: 16 | group: ${{github.workflow}}-${{github.event.client_payload.head_ref}} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | validate: 21 | name: Validate 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - name: Register Check 26 | uses: myrotvorets/set-commit-status-action@v2.0.1 27 | with: 28 | context: Validate Schemas 29 | sha: ${{github.event.client_payload.head_sha}} 30 | status: pending 31 | 32 | - name: Print Dispatch Info 33 | run: | 34 | echo "Actor: ${{github.event.client_payload.actor}}" 35 | echo "PR Number: ${{github.event.client_payload.pr_number}}" 36 | echo "Head Ref: ${{github.event.client_payload.head_ref}}" 37 | echo "Head Sha: ${{github.event.client_payload.head_sha}}" 38 | echo "Base Ref: ${{github.event.client_payload.base_ref}}" 39 | 40 | - name: Checkout Head (${{github.event.client_payload.head_ref}}) 41 | uses: actions/checkout@v4 42 | with: 43 | ref: ${{github.event.client_payload.head_sha}} 44 | persist-credentials: false 45 | path: pr 46 | 47 | - name: Checkout Base (${{github.event.client_payload.base_ref}}) 48 | uses: actions/checkout@v4 49 | with: 50 | ref: ${{github.event.client_payload.base_ref}} 51 | persist-credentials: false 52 | path: base 53 | 54 | - name: Download EXDTools 55 | uses: robinraju/release-downloader@v1 56 | with: 57 | repository: xivdev/EXDTools 58 | fileName: 'EXDTooler' 59 | latest: true 60 | 61 | - name: Chmod Executable 62 | run: chmod +x EXDTooler 63 | 64 | - name: Run Validation 65 | id: validation 66 | run: | 67 | ./EXDTooler --gha --debug --verbose validate -c base/.github/columns.yml -s pr -b base 68 | 69 | - name: Write out summary 70 | if: always() 71 | run: | 72 | echo "${{steps.validation.outputs.summary}}" > summary.txt 73 | cat summary.txt >> $GITHUB_STEP_SUMMARY 74 | 75 | - uses: myrotvorets/set-commit-status-action@v2.0.1 76 | if: always() 77 | with: 78 | sha: ${{github.event.client_payload.head_sha}} 79 | context: Validate Schemas 80 | status: ${{job.status}} 81 | 82 | - name: Wipe previous comments (PR) 83 | if: always() && github.event.action == 'version-pr' 84 | uses: actions/github-script@v7 85 | with: 86 | script: | 87 | const comments = (await github.rest.issues.listComments({ 88 | issue_number: context.payload.client_payload.pr_number, 89 | owner: context.repo.owner, 90 | repo: context.repo.repo 91 | })).data 92 | const comment = comments.filter(comment => comment.user.login === 'github-actions[bot]'); 93 | if (comment.length > 0) { 94 | for (const c of comment) { 95 | await github.graphql(` 96 | mutation MinimizeComment($classifier: ReportedContentClassifiers!, $id: ID!) { 97 | minimizeComment(input:{classifier: $classifier, subjectId: $id}) { 98 | minimizedComment { 99 | isMinimized 100 | viewerCanMinimize 101 | } 102 | } 103 | } 104 | `, { classifier: 'OUTDATED', id: c.node_id }); 105 | } 106 | } 107 | 108 | - name: Post Validation Results (PR) 109 | if: always() && github.event.action == 'version-pr' 110 | uses: actions/github-script@v7 111 | with: 112 | script: | 113 | const summary = require('fs').readFileSync('summary.txt', 'utf-8'); 114 | github.rest.issues.createComment({ 115 | issue_number: context.payload.client_payload.pr_number, 116 | owner: context.repo.owner, 117 | repo: context.repo.repo, 118 | body: summary 119 | }); 120 | 121 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "schemas/2023.07.26.0000.0000"] 2 | path = schemas/2023.07.26.0000.0000 3 | url = https://github.com/xivdev/EXDSchema.git 4 | branch = ver/2023.07.26.0000.0000 5 | [submodule "schemas/2023.09.28.0000.0000"] 6 | path = schemas/2023.09.28.0000.0000 7 | url = https://github.com/xivdev/EXDSchema.git 8 | branch = ver/2023.09.28.0000.0000 9 | [submodule "schemas/2023.11.09.0000.0000"] 10 | path = schemas/2023.11.09.0000.0000 11 | url = https://github.com/xivdev/EXDSchema.git 12 | branch = ver/2023.11.09.0000.0000 13 | [submodule "schemas/2024.02.05.0000.0000"] 14 | path = schemas/2024.02.05.0000.0000 15 | url = https://github.com/xivdev/EXDSchema.git 16 | branch = ver/2024.02.05.0000.0000 17 | [submodule "schemas/2024.03.27.0000.0000"] 18 | path = schemas/2024.03.27.0000.0000 19 | url = https://github.com/xivdev/EXDSchema.git 20 | branch = ver/2024.03.27.0000.0000 21 | [submodule "schemas/2024.04.23.0000.0000"] 22 | path = schemas/2024.04.23.0000.0000 23 | url = https://github.com/xivdev/EXDSchema.git 24 | branch = ver/2024.04.23.0000.0000 25 | [submodule "schemas/2024.06.18.0000.0000"] 26 | path = schemas/2024.06.18.0000.0000 27 | url = https://github.com/xivdev/EXDSchema.git 28 | branch = ver/2024.06.18.0000.0000 29 | [submodule "schemas/2024.07.06.0000.0000"] 30 | path = schemas/2024.07.06.0000.0000 31 | url = https://github.com/xivdev/EXDSchema.git 32 | branch = ver/2024.07.06.0000.0000 33 | [submodule "schemas/2024.07.10.0001.0000"] 34 | path = schemas/2024.07.10.0001.0000 35 | url = https://github.com/xivdev/EXDSchema.git 36 | branch = ver/2024.07.10.0001.0000 37 | [submodule "schemas/2024.07.24.0000.0000"] 38 | path = schemas/2024.07.24.0000.0000 39 | url = https://github.com/xivdev/EXDSchema.git 40 | branch = ver/2024.07.24.0000.0000 41 | [submodule "schemas/2024.08.02.0000.0000"] 42 | path = schemas/2024.08.02.0000.0000 43 | url = https://github.com/xivdev/EXDSchema.git 44 | branch = ver/2024.08.02.0000.0000 45 | [submodule "schemas/2024.11.06.0000.0000"] 46 | path = schemas/2024.11.06.0000.0000 47 | url = https://github.com/xivdev/EXDSchema.git 48 | branch = ver/2024.11.06.0000.0000 49 | [submodule "schemas/latest"] 50 | path = schemas/latest 51 | url = https://github.com/xivdev/EXDSchema.git 52 | branch = latest 53 | [submodule "schemas/2025.03.18.0000.0000"] 54 | path = schemas/2025.03.18.0000.0000 55 | url = https://github.com/xivdev/EXDSchema.git 56 | branch = ver/2025.03.18.0000.0000 57 | [submodule "schemas/2025.04.16.0000.0000"] 58 | path = schemas/2025.04.16.0000.0000 59 | url = https://github.com/xivdev/EXDSchema.git 60 | branch = ver/2025.04.16.0000.0000 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EXDSchema 2 | ## Introduction 3 | This is the schema repository for SqPack [Excel data](https://xiv.dev/game-data/file-formats/excel). 4 | 5 | > [!WARNING] 6 | > The following versions are considered deprecated and will be removed on the day of 7.3's release: 7 | > - 2023.11.09.0000.0000 8 | > - 2024.02.05.0000.0000 9 | > - 2024.03.27.0000.0000 10 | > - 2024.04.23.0000.0000 11 | > - 2024.07.06.0000.0000 12 | > - 2024.07.10.0001.0000 13 | > - 2024.07.24.0000.0000 14 | > - 2024.08.02.0000.0000 15 | > 16 | > These versions have identical column definitions to a previous version, and will always be aliased to a previous version. 17 | 18 | ## Sheets 19 | Inside SqPack, category 0A (`0a0000.win32...` files) consists of Excel sheets serialized into a proprietary binary format read by the game. 20 | The development cycle generates header files for each sheet, which are then compiled into the game, thus, all structure information 21 | is lost on the client side when the game is compiled. This repository is an attempt to consolidate efforts into a 22 | language agnostic schema, easily parsed into any language that wishes to consume it, that accurately describes the structure 23 | of the EXH files as they are provided to the client. 24 | 25 | ## Schema 26 | Schemas are written in [YAML](https://yaml.org/) to define the fields of the structure in an EXH file and the links between different fields. 27 | The schema provides a number of features, all of which is enforced by the [provided JSON schema](/schema.json) for the schema. When applied 28 | against an EXD schema file, it will provide IDE completion and error-checking to improve the manual editing experience. 29 | 30 | ## Features 31 | Since EXH files define the data types for each column, the schema does not care or define any data types in the standard sense. 32 | Instead, it focuses on declaratively defining the structure of the compiled EXH data structures and the relationships between fields. 33 | 34 | The schema includes the following: 35 | - Full declaration of fields is required, nothing can be omitted 36 | - Support for a few common types across sheets, such as `modelId`, `color`, and `icon` 37 | - While these do not affect the overall parsing, they are 38 | useful for research as they provide an important hint for the purpose of the data 39 | - Field names 40 | - Arrays 41 | - Links between fields 42 | - Multi-targeting another sheet 43 | - Complex linking between fields based on a `switch` conditional 44 | - Comment support on any schema object 45 | - Maps out-of-the-box to a very simple object mapping 46 | - JSON schema for the schema itself, providing IDE completion and error-checking 47 | - Relations to [group identically sized arrays together](https://en.wikipedia.org/wiki/AoS_and_SoA) 48 | - Version consistency via `pendingName` and `pendingFields` 49 | 50 | This repository hosts the schema files for each game version. Each change produces a new release of every known game version's schema and removes the old release. 51 | Due to the structure of this repository, any change to a given game version is meant to supercede all others. 52 | 53 | ## Code 54 | The associated repository [EXDTools](https://github.com/xivdev/EXDTools) contains all EXD tooling required to maintain EXDSchema. EXDSchema maintains its versioning through Github Actions, and the PR and release process is (planned to be) automated. 55 | 56 | A tool for editing schemas and viewing the results of parsing on-the-fly is planned, but not yet started. 57 | 58 | ## Usage 59 | See [Usage.md](/Usage.md). -------------------------------------------------------------------------------- /Usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ## Initial Creation 4 | To define a schema, you should create a file with the same name as the sheet it is defining. 5 | The name field must contain the name of the sheet as well. If we were to write a schema for `AozActionTransient`, we would write the following in `AozActionTransient.yml`: 6 | 7 | ```yml 8 | name: AozActionTransient 9 | fields: 10 | - name: Field1 11 | - name: Field2 12 | - name: Field3 13 | # etc ... 14 | ``` 15 | 16 | #### DisplayField 17 | The `displayField` key is provided for consumers that wish to resolve a sheet reference within a single cell. It provides a hint 18 | of what a user will *most likely* want to see when the current sheet is targeted by a link. For example, when linking to `BNpcName`, 19 | the most likely column to reference would be `Name`. For `Item`, the most likely column might be `Name` or `Singular`. 20 | 21 | ## Defining Fields 22 | All sheets must have a number of field entries that corresponds to the number of columns in that sheet. 23 | If not, parsing should fail. 24 | 25 | We can define fields like this: 26 | ```yml 27 | type: sheet 28 | fields: 29 | - name: Stats 30 | - name: Description 31 | - name: Icon 32 | - name: RequiredForQuest 33 | - name: PreviousQuest 34 | - name: Location 35 | - name: Number 36 | - name: LocationKey 37 | - name: CauseStun 38 | - name: CauseBlind 39 | - name: CauseInterrupt 40 | - name: CauseParalysis 41 | - name: TargetsSelfOrAlly 42 | - name: CauseSlow 43 | - name: TargetsEnemy 44 | - name: CausePetrify 45 | - name: CauseHeavy 46 | - name: CauseSleepy 47 | - name: CauseBind 48 | - name: CauseDeath 49 | ``` 50 | This schema is valid because it is accurate in structure. It defines a field for each column in the EXH file as of 6.48. 51 | 52 | ### Types 53 | Valid types for fields in a schema are `scalar`, `link`, `array`, `icon`, `modelId`, and `color`. 54 | 55 | #### scalar 56 | The default type. If the `type` is omitted from a field, it will be assumed to be a `scalar`. Effectively does nothing except tell consumers that 57 | "this field is not an `array`". 58 | 59 | #### icon : uint32 60 | In the above AozActionTransient example, 61 | ```yml 62 | - name: Icon 63 | ``` 64 | can become 65 | ```yml 66 | - name: Icon 67 | type: icon 68 | ``` 69 | While this may seem redundant, there are many fields in sheets that refer to an icon within the `06`, or the `ui/` category, 70 | but the field itself is just a `uint32`. This is a hint for any consumer that attempts to display this field that the data in this column 71 | can be used to format an icon path, like generating `ui/icon/132000/132122_hr1.tex` when the field contains `132122`, without the consumer having 72 | to manually determine which columns contain icons. 73 | 74 | #### modelId : uint32, uint64 75 | Model IDs in the game are packed into either a `uint32` or a `uint64`. 76 | 77 | `uint32` packing is like so: 78 | ``` 79 | uint16 modelId 80 | uint8 variantId 81 | uint8 stain 82 | ``` 83 | `uint64` packing is like so: 84 | ``` 85 | uint16 skeletonId 86 | uint16 modelId 87 | uint16 variantId 88 | uint16 stainId 89 | ``` 90 | To anyone *viewing* the data for research, the packed values are useless, so consumers that provide a view into sheet data can opt 91 | to unpack these values and display them as their unpacked counterparts. Many tools utilize these values individually rather than packed, 92 | so it's important to have the ability to define a field this way. 93 | 94 | #### color : uint32 95 | Some fields contain an RGB value for color in the ARGB format with no alpha. This is simply a hint if a consumer opts to display these 96 | columns' fields as actual colors rather than the raw value. 97 | 98 | #### array 99 | Array fields provide the ability to group and repeat nested structures. These are the methods of declaring an array: 100 | ```yml 101 | name: ExampleSheet 102 | fields: 103 | - name: Array of scalars 104 | comment: This array is just an array of scalars 105 | type: array 106 | count: 2 107 | - name: Erroneous array 108 | comment: This array fails schema validation because it contains the fields key with no fields 109 | type: array 110 | count: 2 111 | fields: [] 112 | - name: Array of single explicit column 113 | comment: Schema consumers should consider this an array of scalars that are also a link 114 | type: array 115 | count: 2 116 | fields: 117 | - type: link 118 | targets: [Item] 119 | - name: Array of structs 120 | comment: This array is a list of structs 121 | type: array 122 | count: 2 123 | fields: 124 | - type: scalar 125 | - type: scalar 126 | ``` 127 | The comment on each array declaration describes what the array is declaring. 128 | 129 | For a more concrete example, let's look at `SpecialShop`: 130 | ```yml 131 | name: SpecialShop 132 | fields: 133 | - name: Name 134 | - name: Item 135 | type: array 136 | count: 60 137 | fields: 138 | - name: ReceiveCount 139 | type: array 140 | count: 2 141 | - name: CurrencyCost 142 | type: array 143 | count: 3 144 | - name: Item 145 | type: array 146 | count: 2 147 | fields: 148 | - type: link 149 | targets: [Item] 150 | - name: Category 151 | type: array 152 | count: 2 153 | fields: 154 | - type: link 155 | targets: [SpecialShopItemCategory] 156 | - name: ItemCost 157 | type: array 158 | count: 3 159 | - name: Quest 160 | type: link 161 | targets: [Quest] 162 | - name: Unknown 163 | - name: AchievementUnlock 164 | type: link 165 | targets: [Achievement] 166 | - name: CollectabilityCost 167 | type: array 168 | count: 3 169 | - name: PatchNumber 170 | - name: HqCost 171 | type: array 172 | count: 3 173 | - type: array 174 | count: 3 175 | - name: ReceiveHq 176 | type: array 177 | count: 2 178 | - name: Quest 179 | type: link 180 | targets: [Quest] 181 | - type: scalar 182 | - type: scalar 183 | - name: CompleteText 184 | - name: NotCompleteText 185 | - type: scalar 186 | - name: UseCurrencyType 187 | - type: scalar 188 | - type: scalar 189 | ``` 190 | As you can see, we have nested arrays in this structure. This means that the in-memory structure follows like so: 191 | ```C 192 | struct SpecialShop 193 | { 194 | struct 195 | { 196 | example_type ReceiveCount[2]; 197 | example_type CurrencyCost[3]; 198 | example_type Item[2]; 199 | example_type Category[2]; 200 | example_type ItemCost[3]; 201 | example_type Quest; 202 | example_type Unknown; 203 | example_type AchievementUnlock; 204 | example_type CollectabilityCost[3]; 205 | example_type PatchNumber; 206 | example_type HqCost[3]; 207 | example_type Unknown2[3]; 208 | example_type ReceiveHq[2]; 209 | } Items[60]; 210 | example_type Quest; 211 | example_type Unknown; 212 | example_type Unknown2; 213 | example_type CompleteText; 214 | example_type NotCompleteText; 215 | example_type Unknown3; 216 | example_type UseCurrencyType; 217 | example_type Unknown4; 218 | example_type Unknown5; 219 | }; 220 | ``` 221 | As you can see, the overall schema is similar to defining structures in YML but omitting the actual data type. 222 | This nested capability allows you to define complex structures. From experience, we have seen that 223 | you should not need to nest more than 2 levels deep, but schema consumers should still support this. 224 | 225 | ### Linking 226 | The sheets that power the game are relational in nature, so the schema supports a few different kinds of linking. 227 | 228 | #### Single Link 229 | To define a single link, set the `type` to `link` and define the `targets` array: 230 | ```yml 231 | - name: Quest 232 | type: link 233 | targets: [Quest] 234 | ``` 235 | Note that the link targets is an array of strings. They must be sheet names, and there must be at least one sheet. To link to one sheet, leave a single sheet in the array. 236 | 237 | #### Multi Link 238 | A sheet's single column can link to multiple columns: 239 | ```yml 240 | - name: Requirement 241 | type: link 242 | targets: [Quest, GrandCompany] 243 | ``` 244 | In this case, disparate sheet key ranges will provide the ability for consumers to determine which sheet a link should resolve to. 245 | For example, if a row's `Requirement` is `2`, it will resolve to `GrandCompany`, because row `2` exists in `GrandCompany` and not in `Quest.` 246 | The same thing happens in the other direction: if `Requirement` is `69208`, it will link to `Quest` and not `GrandCompany` for the same reason. 247 | 248 | #### Conditional Link 249 | A sheet's single column can link to multiple columns depending on another field in the sheet: 250 | ```yml 251 | - name: Location 252 | comment: PlaceName when LocationKey is 1, ContentFinderCondition when LocationKey is 4 253 | type: link 254 | condition: 255 | switch: LocationKey 256 | cases: 257 | 1: [PlaceName] 258 | 4: [ContentFinderCondition] 259 | ``` 260 | The targets array is not required for conditional links, and if both are specified, the file will fail schema validation. 261 | When defining the link, add a `condition` object with a `switch` key that defines the field to switch on the value of. 262 | The `cases` dictionary contains arrays of the sheet to reference when the case matches. 263 | 264 | Yes, the `case` dictionary may contain an *array*. This means that each case can be a [multi link](#multi-link) as well. Take `Item` for example: 265 | ```yml 266 | - name: AdditionalData 267 | link: 268 | condition: 269 | switch: FilterGroup 270 | cases: 271 | 14: [HousingExterior, HousingInterior, 272 | HousingYardObject, HousingFurniture, 273 | HousingFurniture, HousingPreset, 274 | HousingUnitedExterior] 275 | 15: [Stain] 276 | 18: [TreasureHuntRank] 277 | 20: [GardeningSeed] 278 | 25: [AetherialWheel] 279 | 26: [CompanyAction] 280 | 27: [TripleTriadCard] 281 | 28: [AirshipExplorationPart] 282 | 32: [Orchestrion] 283 | 36: [SubmarinePart] 284 | ``` 285 | The `AdditionalData` column in `Item` does a lot of heavy lifting. We can assume during game execution that the use of the field is heavily based on context, 286 | but for research and data exploration, having the ability to define the exact sheet is useful. Here, we can see that when `FilterGroup` is `14`, 287 | we can link to any of `HousingExterior`, `HousingInterior`, `HousingYardObject`, `HousingFurniture`, `HousingPreset`, or finally `HousingUnitedExterior`. 288 | This works because the value for `AdditionalData` are distinct ranges, even when `FilterGroup` is `14`, thus allowing the definition here to behave like a multi link. 289 | 290 | ## Relations 291 | Relations are used to group different arrays together of the same size. They are supported on every sheet and in every array declaration with more than one field. 292 | 293 | To best explain relations, here's an example with `ItemFood`: 294 | ```yml 295 | name: ItemFood 296 | fields: 297 | - name: Max 298 | type: array 299 | count: 3 300 | - name: MaxHQ 301 | type: array 302 | count: 3 303 | - name: EXPBonusPercent 304 | - name: BaseParam 305 | type: array 306 | count: 3 307 | fields: 308 | - type: link 309 | targets: [BaseParam] 310 | - name: Value 311 | type: array 312 | count: 3 313 | - name: ValueHQ 314 | type: array 315 | count: 3 316 | - name: IsRelative 317 | type: array 318 | count: 3 319 | ``` 320 | Here, `ItemFood` contains several arrays of size 3. Each index has one `BaseParam` and its accompanying `Max`, `MaxHQ`, `Value`, `ValueHQ`, and `IsRelative` values. 321 | These should all be related to one another, but they're instead spread out across 6 different arrays. This is a perfect example of the downsides of 322 | [Structs of Arrays](https://en.wikipedia.org/wiki/AoS_and_SoA), since our data is best formatted using Arrays of Structs. 323 | 324 | Using relations, we can circumvent this issue by explicitly grouping these 6 arrays together into one array with 3 structs. 325 | To do so, we can add the following to the end of the schema file: 326 | ```yml 327 | relations: 328 | Params: 329 | - BaseParam 330 | - IsRelative 331 | - Value 332 | - Max 333 | - ValueHQ 334 | - MaxHQ 335 | ``` 336 | Now, instead of accessing each array individually, `Params` is the only available field, where every element of `Params` contains all the related columns. 337 | 338 | ## Version Consistency 339 | To maintain version consistency and backwards compatibility, the schema provides two keys: `pendingName` and `pendingFields`. 340 | 341 | #### pendingName 342 | When a field is renamed, the `pendingName` key should be used to provide the new name. This allows advanced consumers to mark the old name as deprecated and provide the new name as a hint without causing breaking changes to their users. 343 | 344 | This feature is especially useful for new sheets that still contain Unknownxx fields. For example: 345 | ```yml 346 | name: WKSWhatever 347 | fields: 348 | - name: Unknown32 349 | pendingName: NewField 350 | - name: Unknown33 351 | pendingName: OtherNewField 352 | ``` 353 | 354 | #### pendingFields 355 | In the event that a simple rename is not enough, the `pendingFields` key can be used to provide a brand new field structure. This is required for anything that changes the structure of the sheet, such as changing the type of a field or adding a relation. This key should be used sparingly, as it can cause sudden breaking changes to consumers when updating to a new release. 356 | 357 | For example, let's say we have a sheet `WKSWhatever` that has a field `Unknown32` that is a scalar, but you notice that it should be a link to `Item`. Thus, you'll need to add a `pendingFields` key: 358 | ```yml 359 | name: WKSWhatever 360 | fields: 361 | - name: Unknown32 362 | # Adding a pendingName and comment is a courtesy to consumers to help minimize friction when updating 363 | comment: Will be a link to Item in the future 364 | pendingName: NewField 365 | - name: Unknown33 366 | comment: Will be a link to Item in the future 367 | pendingName: OtherNewField 368 | pendingFields: 369 | - name: NewField 370 | type: link 371 | targets: [Item] 372 | - name: OtherNewField 373 | type: link 374 | targets: [Item] 375 | ``` 376 | 377 | > [!IMPORTANT] 378 | > `pendingFields` key is meant to be a complete replacement for `fields`. When creating a `pendingFields` key, make sure to copy the entire structure of `fields`. -------------------------------------------------------------------------------- /schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "title": "EXDSchema", 4 | "type": "object", 5 | "additionalProperties": false, 6 | "required": [ 7 | "name", 8 | "fields" 9 | ], 10 | "properties": { 11 | "name": { 12 | "description": "Name of the underlying .exd sheet", 13 | "type": "string", 14 | "pattern": "^\\w+$" 15 | }, 16 | "displayField": { 17 | "description": "Field to display in a UI. Completely optional, but must link to a valid top-level field name.", 18 | "type": "string", 19 | "pattern": "^\\w+$" 20 | }, 21 | "fields": { 22 | "description": "A list of fields in the sheet, ordered by offset", 23 | "type": "array", 24 | "uniqueItems": true, 25 | "minItems": 1, 26 | "items": { 27 | "$ref": "#/$defs/namedField" 28 | } 29 | }, 30 | "pendingFields": { 31 | "description": "A list of new fields in the sheet, ordered by offset. When a new release is made, fields will be replaced with pendingFields.", 32 | "type": "array", 33 | "uniqueItems": true, 34 | "minItems": 1, 35 | "items": { 36 | "$ref": "#/$defs/namedField" 37 | } 38 | }, 39 | "relations": { 40 | "$ref": "#/$defs/relations" 41 | } 42 | }, 43 | "$defs": { 44 | "baseField": { 45 | "type": "object", 46 | "properties": { 47 | "type": { 48 | "description": "Type of the field", 49 | "type": "string", 50 | "enum": [ 51 | "scalar", 52 | "link", 53 | "array", 54 | "icon", 55 | "modelId", 56 | "color" 57 | ], 58 | "default": "scalar" 59 | }, 60 | "comment": { 61 | "description": "Developer-readable comment", 62 | "type": "string" 63 | } 64 | }, 65 | "allOf": [ 66 | { 67 | "if": { 68 | "properties": { 69 | "type": { 70 | "const": "scalar" 71 | } 72 | } 73 | }, 74 | "then": {} 75 | }, 76 | { 77 | "if": { 78 | "properties": { 79 | "type": { 80 | "const": "link" 81 | } 82 | }, 83 | "required": [ 84 | "type" 85 | ] 86 | }, 87 | "then": { 88 | "oneOf": [ 89 | { 90 | "properties": { 91 | "targets": { 92 | "description": "List of sheets that this field links to", 93 | "type": "array", 94 | "minItems": 1, 95 | "items": { 96 | "description": "Sheet name", 97 | "type": "string", 98 | "pattern": "^\\w+$" 99 | } 100 | } 101 | }, 102 | "required": [ 103 | "targets" 104 | ] 105 | }, 106 | { 107 | "properties": { 108 | "condition": { 109 | "description": "Switch case conditional for what sheet(s) this field links to, based on the value of 'switch'", 110 | "type": "object", 111 | "additionalProperties": false, 112 | "required": [ 113 | "switch", 114 | "cases" 115 | ], 116 | "properties": { 117 | "switch": { 118 | "description": "Field to switch on", 119 | "type": "string", 120 | "pattern": "^\\w+$" 121 | }, 122 | "cases": { 123 | "description": "List of cases to switch on. The key is switch's value, and the value is a list of sheet names", 124 | "type": "object", 125 | "patternProperties": { 126 | "^[1-9]\\d*$": { 127 | "type": "array", 128 | "minItems": 1, 129 | "items": { 130 | "description": "Sheet name", 131 | "type": "string", 132 | "pattern": "^\\w+$" 133 | } 134 | } 135 | } 136 | } 137 | } 138 | } 139 | }, 140 | "required": [ 141 | "condition" 142 | ] 143 | } 144 | ] 145 | } 146 | }, 147 | { 148 | "if": { 149 | "properties": { 150 | "type": { 151 | "const": "array" 152 | } 153 | }, 154 | "required": [ 155 | "type" 156 | ] 157 | }, 158 | "then": { 159 | "required": [ 160 | "count" 161 | ], 162 | "properties": { 163 | "count": { 164 | "description": "Number of elements in the array", 165 | "type": "number", 166 | "exclusiveMinimum": 1 167 | }, 168 | "fields": { 169 | "type": "array", 170 | "uniqueItems": true, 171 | "minItems": 1 172 | }, 173 | "relations": { 174 | "$ref": "#/$defs/relations" 175 | } 176 | }, 177 | "if": { 178 | "properties": { 179 | "fields": { 180 | "type": "array", 181 | "maxItems": 1 182 | } 183 | } 184 | }, 185 | "then": { 186 | "properties": { 187 | "fields": { 188 | "type": "array", 189 | "description": "Field type of the array (single item per array element)", 190 | "items": { 191 | "$ref": "#/$defs/unnamedField" 192 | } 193 | } 194 | } 195 | }, 196 | "else": { 197 | "properties": { 198 | "fields": { 199 | "type": "array", 200 | "description": "Fields in the array (multiple items per array element)", 201 | "items": { 202 | "$ref": "#/$defs/namedField" 203 | } 204 | } 205 | } 206 | } 207 | } 208 | }, 209 | { 210 | "if": { 211 | "properties": { 212 | "type": { 213 | "const": "icon" 214 | } 215 | }, 216 | "required": [ 217 | "type" 218 | ] 219 | }, 220 | "then": {} 221 | }, 222 | { 223 | "if": { 224 | "properties": { 225 | "type": { 226 | "const": "modelId" 227 | } 228 | }, 229 | "required": [ 230 | "type" 231 | ] 232 | }, 233 | "then": {} 234 | }, 235 | { 236 | "if": { 237 | "properties": { 238 | "type": { 239 | "const": "color" 240 | } 241 | }, 242 | "required": [ 243 | "type" 244 | ] 245 | }, 246 | "then": {} 247 | } 248 | ] 249 | }, 250 | "unnamedField": { 251 | "type": "object", 252 | "unevaluatedProperties": false, 253 | "allOf": [ 254 | { 255 | "$ref": "#/$defs/baseField" 256 | } 257 | ] 258 | }, 259 | "namedField": { 260 | "type": "object", 261 | "unevaluatedProperties": false, 262 | "required": [ 263 | "name" 264 | ], 265 | "properties": { 266 | "name": { 267 | "description": "Name of the field", 268 | "type": "string", 269 | "pattern": "^\\w+$" 270 | }, 271 | "pendingName": { 272 | "description": "New name of the field. If specified, the old name will be considered deprecated.", 273 | "type": "string", 274 | "pattern": "^\\w+$" 275 | } 276 | }, 277 | "allOf": [ 278 | { 279 | "$ref": "#/$defs/baseField" 280 | } 281 | ] 282 | }, 283 | "relations": { 284 | "description": "Relations between fields in the sheet. Helps with the https://en.wikipedia.org/wiki/AoS_and_SoA problem.", 285 | "type": "object", 286 | "unevaluatedProperties": false, 287 | "patternProperties": { 288 | "^\\w+$": { 289 | "description": "List of fields to move into a relation. All fields must be arrays that have the same count.", 290 | "type": "array", 291 | "minItems": 1, 292 | "items": { 293 | "description": "Field name (cannot be nested)", 294 | "type": "string", 295 | "pattern": "^\\w+$" 296 | } 297 | } 298 | } 299 | } 300 | } 301 | } --------------------------------------------------------------------------------