# Converter: GFX paths → SFX paths in skill stubs This document describes how **skill `.cs` files** get **`m_szFlySfxPath` / `m_szHitGrndSfxPath` / `m_szHitSfxPath`** from the **same client `.gfx` assets** that supply **`m_sz*GfxPath`**, and how to extend or re-run the pipeline. ## Goal For each non-empty GFX path emitted from `.sgc` (`m_szFlyGfxPath`, `m_szHitGrndGfxPath`, `m_szHitGfxPath`), find the matching **`.gfx` file on disk** under the **GFX root** (`--gfx`, e.g. `C:\Users\BrewPC\Downloads\gfx`) and read the **classic file-based sound** path from the **Sound element** inside that `.gfx`. Emit the corresponding **`m_sz*SfxPath`** field on `SkillStub`. Example (skill 1): - `m_szHitGfxPath = "策划联入/人物技能/击中/虎击击中.gfx"` - On disk: `…\gfx\策划联入\人物技能\击中\虎击击中.gfx` - Inside `.gfx`: Sound element (`GFXELEMENTID: 170`) → `Path:` / `PathNum:` + `Path:` lines (see C++ `GFXSOUNDIMP::Load`) - Result: `m_szHitSfxPath = "Skill\\01Fighter\\虎击_C"` (exact string depends on asset data) ## Reference: client behavior See [C++_SkillGfx_Sfx_Play_Flow.md](C++_SkillGfx_Sfx_Play_Flow.md): the client plays **`Sfx\` + filename** from the Sound element in the GFX resource. ## Unity data model In [skill.cs](../Assets/PerfectWorld/Scripts/Skills/skill.cs), `SkillStub` defines: | Field | Source | |--------|--------| | `m_szFlyGfxPath` | `.sgc` line 1 `Path` | | `m_szHitGrndGfxPath` | `.sgc` line 2 `Path` | | `m_szHitGfxPath` | `.sgc` 3rd `Path` (after version/scales per format) | | `m_szFlySfxPath` | First classic SFX path parsed from **fly** `.gfx` | | `m_szHitGrndSfxPath` | Same for **hit-ground** `.gfx` | | `m_szHitSfxPath` | Same for **hit** `.gfx` | If a GFX path is empty, or the `.gfx` file is missing, or there is no parseable Sound element, the matching SFX field is **`string.Empty`**. ## Tool: `convert_skills_fixed.py` ### Inputs - `--cpp` — `CElementSkill` (skill `*.h`) - `--cs` — Unity `Scripts/Skills` (or subfolder layout you use) - `--gfx` — **GFX root** (folder that contains `sgc\` and the same tree as in-game paths, e.g. `策划联入\...`) ### Resolution rule (GFX file on disk) Game path uses **`/`**. On Windows, join under `gfx_root`: ```text gfx_root + path.replace("/", os.sep) ``` Example: `策划联入/人物技能/击中/虎击击中.gfx` → `gfx_root\策划联入\人物技能\击中\虎击击中.gfx` ### Text `.gfx` detection Text format starts with a line matching **`Version: `** (Angelica `A3DGFXEx::Load`). If the file does not match (likely **binary** `.gfx`), SFX extraction is **skipped** (empty string). ### Parsing Sound element (classic mode) 1. Split file into blocks after each line `GFXELEMENTID: `. 2. For blocks with **id = 170** (`ID_ELE_TYPE_SOUND` in client headers): - Take the substring **before** the first **`SoundVer:`** line (start of `GfxSoundParamInfo`). - If **`PathNum: N`**: read the next **N** lines **`Path: …`**; use the **first** path as the stub value (runtime may randomize among N). - Else: use the **last** **`Path: …`** line in that prefix (legacy single path). 3. Normalize: - `"\\"` → `"/"` for logical path (same separator style as `m_sz*GfxPath`) - Strip trailing **`.wav` / `.ogg` / `.mp3`** if present 4. Escape for C# with the same helper used for other strings in the generator. ### Limitations (v1) | Topic | Behavior | |--------|-----------| | Binary `.gfx` | No SFX extracted | | `GFXSOUNDIMP22` audio-event `Path:` after `SoundVer:` | Not used as classic SFX in v1 | | Multiple `GFXELEMENTID: 170` | First non-empty classic path wins | | No `--gfx` / missing file | All three `m_sz*SfxPath` = `string.Empty` | | Skills **without** `.sgc` data | No `gfx_path_code` block; stub defaults apply | ## Regenerating stubs From repo root (adjust paths): ```bash python convert_skills_fixed.py --stubs "E:\Projects\perfect-world-source\perfect-world-source\CElement\CElementSkill\stubs1.cpp" --gfx "C:\Users\BrewPC\Downloads\gfx" ``` Or `--ids 1` for a single skill smoke test. ## Future work (checklist) - [ ] **Binary `.gfx`**: port minimal binary reader for `GFXELEMENTID` + Sound chunk, or shell out to a small C++/CLI tool. - [ ] **Audio-event mode**: map `GFXSOUNDIMP22` `Path:` (after `SoundVer:`) to a separate field or naming convention if Unity uses FMOD/Wwise events. - [ ] **Multiple sounds**: emit multiple candidates or document “first only” vs random. - [ ] **Runtime**: wire `m_sz*SfxPath` into actual playback (load under `Sfx\` or Addressables) where `m_sz*GfxPath` is consumed today. ## Files touched by this feature | File | Role | |------|------| | [convert_skills_fixed.py](../../convert_skills_fixed.py) | `_resolve_gfx_path_on_disk`, text `.gfx` parse, `m_sz*SfxPath` emission | | [skill.cs](../Assets/PerfectWorld/Scripts/Skills/skill.cs) | `SkillStub` fields `m_szFlySfxPath`, `m_szHitGrndSfxPath`, `m_szHitSfxPath` | | Generated `skillN.cs` | Constructor assignments next to GFX paths |