Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions docs/architecture/ghidra-winphone-quick-ref.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Ghidra Windows Phone quick reference

The reverse-engineering toolchain is installed only in the enclosing workspace,
not globally.

| Component | Workspace path |
|---|---|
| Ghidra 12.1.2 | `tools/ghidra_12.1.2_PUBLIC` |
| Ghidra projects | `tools/ghidra-projects` |
| v1.2.4.3 project | `WinPhoneTerraria` |
| v1.0.0.0 project | `WinPhoneTerraria1000` |
| Extraction script | `tools/ghidra-scripts/WinPhoneExtract.java` |
| ghidra-mcp v3.0.0 | `tools/ghidra-mcp` |
| MCP virtual environment | `tools/ghidra-mcp/.venv` |

The Ghidra archive SHA-256 is
`b62e81a0390618466c019c60d8c2f796ced2509c4c1aea4a37644a77272cf99d`.

## Extract from a saved project

Set the workspace-local application directories before running headless Ghidra:

```powershell
$env:APPDATA = 'D:\dev\ai\tedit\tools\ghidra-user\appdata'
$env:LOCALAPPDATA = 'D:\dev\ai\tedit\tools\ghidra-user\localappdata'

& 'D:\dev\ai\tedit\tools\ghidra_12.1.2_PUBLIC\support\analyzeHeadless.bat' `
'D:\dev\ai\tedit\tools\ghidra-projects' 'WinPhoneTerraria1000' `
-process 'WindowsPhone.exe' -noanalysis `
-scriptPath 'D:\dev\ai\tedit\tools\ghidra-scripts' `
-postScript WinPhoneExtract.java `
'output:D:\dev\ai\tedit\Terraria-Map-Editor\docs\reverse-engineering\winphone\ghidra\v1.0.0.0' `
save:0076758c savecalls:0076758c
```

Extraction arguments include:

- `ADDRESS`: print a decompilation.
- `save:ADDRESS`: save a decompilation in the selected output directory.
- `calls:ADDRESS`: print direct callees.
- `savecalls:ADDRESS`: save direct callees.
- `xref:ADDRESS`: list references to an address.
- `range:START:END`: list functions in an address range.
- `string:TEXT`: find defined strings and their callers.
- `scalar:VALUE`: find instructions using a numeric value.

Durable extracts and the current function index are in
`docs/reverse-engineering/winphone/ghidra/README.md`.

## Local MCP bridge

Start the workspace server with `tools/Start-GhidraMcp.ps1`. Configure a project-
local MCP client to run:

```text
D:\dev\ai\tedit\tools\ghidra-mcp\.venv\Scripts\python.exe
D:\dev\ai\tedit\tools\ghidra-mcp\bridge_mcp_ghidra.py
--ghidra-server http://127.0.0.1:8089/
```

Do not add this server or its Python environment to global configuration.
93 changes: 93 additions & 0 deletions docs/architecture/win-phone-field-map.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Windows Phone editor field map

This map records how the typed pre-tile primitive sequence is connected to
TEdit's `World` model. Primitive indices are logical entries, not byte offsets.
All unknown entries retain their original kind and value when saving.

Status values:

- **Editable**: loaded into an existing `World` property and written back from it.
- **Derived**: written from structural world data but not independently editable.
- **Opaque**: preserved exactly; no editor property is assigned yet.

## Container fields

| Field | v49 | v60 | Status |
|---|---|---|---|
| Version | `uint32` | `uint32` | Structural; changing version is rejected |
| CRC | zero in tutorial | reflected CRC-32 | Recalculated for nonzero-CRC containers |
| Title | UTF-8 | UTF-16LE | Editable through `World.Title` |
| Cloud flag/history | absent | native header values | Opaque |

## Common metadata primitives

| Index | Kind | Meaning | TEdit property | Status |
|---:|---|---|---|---|
| 0 | `int32` | World ID | `WorldId` | Editable |
| 1 | `int32` | Native revision/legacy value | — | Opaque |
| 2 | `int32` | Right pixel bound | `RightWorld` | Derived |
| 3 | `int16` | Bottom pixel bound | `BottomWorld` | Derived |
| 4 | `int16` | Tile height | `TilesHigh` | Derived; resizing not supported |
| 5 | `int16` | Tile width | `TilesWide` | Derived; resizing not supported |
| 6 | `int16` | Dungeon X | `DungeonX` | Editable model field |
| 7 | `int16` | Dungeon Y | `DungeonY` | Editable model field |
| 8 | `int16` | Surface level | `GroundLevel` | Editable |
| 9 | `int16` | Rock level | `RockLevel` | Editable |
| 10 | `int32` bits | Time as `float32` | `Time` | Editable |
| 11 | `byte` | Daytime flag | `DayTime` | Editable through time control |
| 12 | `byte` | Moon phase | `MoonPhase` | Editable |
| 13 | `byte` | Blood moon flag | `BloodMoon` | Editable |
| 14 | `int16` | Compact time/moon helper state | — | Opaque |

## v49 tail

| Indices | Meaning | Status |
|---|---|---|
| 15–16 | Spawn X/Y (`int16`) | Editable through spawn tool |
| 17–19 | Eye of Cthulhu, Eater of Worlds, Skeletron defeated | Editable |
| 20–22 | Goblin, Wizard, Mechanic rescued | Editable |
| 23–25 | Goblin Army, Clown, Frost Legion defeated | Editable |
| 26 | Shadow orb smashed | Editable |
| 27 | Meteor queued | Editable |
| 28 | Shadow orb count (`byte`) | Editable |
| 29 | Altar count (`int32`) | Editable |
| 30 | Hardmode | Editable |
| 31 | Invasion delay (`byte`) | Editable model field; not shown separately in the editor |
| 32 | Invasion size (`int16`) | Editable |
| 33 | Invasion type (`byte`) | Editable |
| 34 | Invasion X as `float32` bits | Editable |

## v60 tail

| Indices | Meaning | Status |
|---|---|---|
| 15–16 | Additional compact helper bytes | Opaque |
| 17–18 | Spawn X/Y (`int16`) | Editable through spawn tool |
| 19 | Crimson-world flag | Editable through `IsCrimson` |
| 20 | Raining flag | Editable through `IsRaining` |
| 21 | Rain time | Editable through `TempRainTime` |
| 22 | Maximum rain intensity as `float32` bits | Editable through `TempMaxRain` |
| 23 | Native weather scheduling value | Opaque |
| 24–32 | Same nine common progression flags as v49 indices 17–25 | Editable |
| 33–34 | Lepus and Turkor defeated | Opaque; mobile-only with no TEdit model field |
| 35 | Queen Bee defeated | Editable |
| 36–38 | Destroyer, Twins, Skeletron Prime defeated | Editable |
| 39 | Any mechanical boss defeated | Derived from the three mechanical boss fields |
| 40–42 | Plantera, Golem, Pirate Invasion defeated | Editable |
| 43–46 | Shadow orb smashed, meteor queued, orb count, altar count | Editable |
| 47–49 | Cobalt, Mythril, Adamantite ore tier IDs (`int16`) | Editable |
| 50 | Hardmode | Editable |
| 51 | Additional time-state snapshot value (`int16`) | Opaque |
| 52–55 | Invasion delay, size, type, and X (`byte`, `int16`, `byte`, `float32`) | Editable |
| 56–69 | Two compact biome-state blocks | Opaque |
| 70–78 | Packed styles and eight background bytes | Opaque |

## Editable sections

Tiles, chests, signs, NPCs, and character names use the existing editor models.
Chests and signs are reconciled into their fixed 1,000 native slots at save time.
For tiles, unchanged columns preserve native records; edited columns are written
as validated one-tile records to avoid stale RLE boundaries.

The corresponding raw Ghidra functions and direct-call indexes are stored under
`docs/reverse-engineering/winphone/ghidra/`.
190 changes: 190 additions & 0 deletions docs/architecture/win-phone.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# Windows Phone world format

Status: Windows Phone v49 and v60 containers are detected and decoded through their character-name tables. The native header, metadata primitives, tiles, chests, signs, NPCs, and names are reconstructed by a section-aware serializer. All three fixtures save byte-for-byte through `World.Save`.

The implementation is in `src/TEdit.Terraria/World.FileWinPhone.cs`. Reverse-engineering notes for the local Ghidra setup are in `ghidra-winphone-quick-ref.md`.
The editor-to-native metadata mapping is tracked in `win-phone-field-map.md`, and
raw saved Ghidra decompilations are indexed under
`docs/reverse-engineering/winphone/ghidra/`.

## Verified fixtures

| Fixture | Version | Size | Result |
|---|---:|---:|---|
| `_reference_sources/winphone/Terraria v1.0.0.0/Tutorial.world` | 49 | 1,538,009 | Full tile load and byte-identical save |
| `_reference_sources/winphone/Terraria v1.2.4.3/Tutorial.world` | 49 | 1,538,009 | Full tile load and byte-identical save; identical to the v1.0 copy |
| `src/TEdit.Tests/WorldFiles/win-phone.world` | 60 | 4,210,977 | Full tile load and byte-identical save |

The two tutorial files have SHA-256 `2E06CDBDBB29604831AA149A94CDBA8A5C06A20C9DF6D0CE46F0BFEDBC2061F9`.

## Native container header

All integers are little-endian.

| Field | Encoding |
|---|---|
| Version | `uint32` |
| CRC-32 | `uint32`; reflected polynomial `0xEDB88320`, calculated over bytes from offset 8 through EOF |
| Title length | `int32` character/byte count |
| Title | UTF-8 for versions below 50; UTF-16LE for version 50 and newer |
| Cloud flag | One byte for version 53 and newer |
| History | Version 53 has one `uint32`; later versions have `int32 count` followed by that many `uint32` values |

The v49 tutorial CRC field is zero. The v60 fixture contains and passes the native CRC calculation. The value previously described as a fixed magic number is therefore a file-specific CRC, not a format signature.

The body begins with these verified fields:

| Relative offset | Field | Encoding |
|---:|---|---|
| `+0` | World ID | `int32` |
| `+4` | Revision/legacy value | `int32` where present |
| `+8` | Right pixel bound | `int32` |
| `+12` | Bottom pixel bound | `int16` |
| `+14` | Tile height | `int16` |
| `+16` | Tile width | `int16` |
| `+18` | Dungeon X | `int16` |
| `+20` | Dungeon Y | `int16` |
| `+22` | Surface level | `int16` |
| `+24` | Rock level | `int16` |

Detection validates `right == width * 16` and `bottom == height * 16`. This prevents ordinary desktop V1 files in the same version range from being claimed as Windows Phone files.

The remaining pre-tile metadata has an exact versioned primitive layout:

| Block | v49 | v60 |
|---|---:|---:|
| Common bounds and layers above | 26 bytes | 26 bytes |
| World-state helper (`FUN_0073527c`/`FUN_00735318`) | 9 bytes | 11 bytes |
| Spawn X/Y | 4 bytes | 4 bytes |
| Extra v58 state plus weather (`FUN_00749890`/`FUN_00749d20`) | — | 14 bytes |
| Progression flags (`FUN_006598b4`/`FUN_00659a38`) | 9 bytes | 19 bytes |
| Moon/time/invasion/ore fields | 16 bytes | 24 bytes |
| Two biome-state blocks, packed styles, eight background bytes | — | 29 bytes |
| Total body prefix before the first marker | 64 bytes | 127 bytes |

Fields without a confirmed gameplay name are retained as typed `byte`, `int16`, or `int32` primitives rather than assigned speculative names. The serializer writes the primitive sequence, not copied source bytes.

Confirmed editor-backed fields are now materialized from the prefix and written
back from the normal `World` model: world ID, dungeon and spawn coordinates,
surface and rock levels, time and moon state, progression flags, rescued NPCs,
orb/meteor/altar state, hardmode, ore tiers, and the active invasion state.
The compact time value is a `float32` stored in the primitive sequence as its raw
four-byte bit pattern. Spawn follows the compact time/moon helper (9 bytes below
v58 and 11 bytes at v58+); it is not the pair at body offsets `+18/+20`.

The native body is divided by the little-endian `int32` marker `0x162E`. Its complete sequence is:

1. Metadata, marker.
2. Column-major tile grid, marker.
3. 1,000 sparse chest slots, marker.
4. 1,000 sparse sign slots, marker.
5. Sentinel-terminated NPC records, marker.
6. Character-name strings: 10 for v49 and 18 for v60.

## Two tile encodings

The v1.0 executable does not use one version-conditional tile reader. Its body loader dispatches versions below 58 to a legacy reader and versions 58 or newer to a compact reader.

### Versions below 58

Recovered from `FUN_00733bac` in the v1.0.0.0 executable. Each record describes one tile followed by zero or more vertical duplicates.

| Condition | Field | Encoding |
|---|---|---|
| Always | Active | `byte`, zero or nonzero |
| Active | Stored tile type | `byte` |
| Active and legacy-framed | Frame U, frame V | Two `int16` values |
| Always | Wall type | `byte` |
| Version > 50 | Wall color | `byte` |
| Always | Liquid amount | `byte` |
| Liquid amount != 0 | Lava flag | `byte`, zero for water and nonzero for lava |
| Always | Tile flags | `byte` |
| Always | Vertical repeat count | `int16` |

The repeat count excludes the first tile, so a value of zero represents one tile. Runs cannot cross a column boundary.

The legacy frame predicate is recovered exactly from `FUN_0072ad80`. It includes IDs 3–5; 10–18 except 19; 20, 21, 24, 26–29, 31, 33–36, 42, 50, 55, 61, 71–74, 77–79, 81–106, 110, 113, 114, 125–129, 132–139, 141–144, 149, and 150.

The native migration maps stored types 35 and 36 to type 34 while shifting their frame V by 54 and 108 respectively. Stored type 150 maps to the internal placeholder type 500. Type 127 is decoded and then deactivated.

### Versions 58 and newer

Recovered from `FUN_00733858`. The v60 fixture uses this encoding.

| Condition | Field | Encoding |
|---|---|---|
| Always | Header 1 | `byte`: active, red/green/blue wire, actuator, inactive flags |
| Active | Tile type | `uint16`; the native tile field is nine bits wide |
| Active and frame-important | Frame U, frame V | Two `int16` values |
| Active | Tile color | `byte` |
| Always | Wall type | `byte` |
| Wall != 0 | Wall color | `byte` |
| Always | Liquid amount | `byte` |
| Liquid amount != 0 | Liquid kind | `byte`; native value plus one maps to TEdit's enum |
| Always | Header 2 | `byte`; includes yellow wire |
| Versions 60–68 | Shape | `byte`; high nibble is the brick style |
| Always | Vertical repeat count | One or two bytes, 7-bit continuation encoding |

Type 500 is a valid framed internal placeholder in the v60 stream, not corrupt input.

The original RLE boundaries are encoding metadata. The v49 tutorial sometimes ends a run before a following semantically identical tile, so maximizing every run changes six bytes even though the decoded grid is unchanged. TEdit retains and validates those native boundaries.

## Chests, signs, NPCs, and names

Each chest slot begins with a presence byte and, when present, `int16 X`, `int16 Y`.

- v49 has 20 items per chest. Every slot stores a one-byte stack; nonempty items then store `int16 net ID` and `byte prefix`.
- v60 has 40 items per chest. A one-byte mask length and five mask bytes select populated slots; each selected item stores `int16 stack`, `int16 net ID`, and `byte prefix`.

Each sign slot begins with a presence byte and, when present, `int16 X`, `int16 Y`. v60 then has a text-present byte. Text uses the container string encoding: `int32 length` plus UTF-8 below version 50 or UTF-16LE at version 50 and newer.

NPC records begin with byte `1`, then contain `byte sprite ID`, two `float32` positions, a homeless byte, and two `int16` home coordinates. A zero presence byte terminates the list.

The character-name IDs are `17, 18, 19, 20, 22, 54, 38, 107, 108, 124, 160, 178, 207, 208, 209, 227, 228, 229`. v49 stores the first 10; v60 stores all 18.

The v49 `Tutorial.world` is a fat bundled resource. The native v49 reader stops at offset 201,842 after its tenth name and ignores the remaining 1,336,167 bytes. TEdit reconstructs the complete parsed world and preserves this ignored bundle suffix verbatim. The v60 file ends at its eighteenth name.

## Save behavior

`World.SaveWinPhoneUnchanged` remains an explicit raw-copy utility. The normal
`World.Save` and `World.SaveAsync` paths require a full load, validate supported
edits, and reconstruct the header, metadata, tiles, section markers, chests,
signs, NPCs, and character names. They do not emit the original parsed bytes.

Header edits for the confirmed editor-backed fields are serialized, and nonzero
native CRC values are regenerated. Chests and signs are reconciled back into the
native 1,000-slot sparse tables; NPCs and character names serialize from their
normal TEdit collections. A tile edit causes that native column to be rewritten
as one-tile records while unchanged columns retain their original flags and RLE
boundaries. The writer validates native type, coordinate, item-stack, liquid,
paint, and shape limits rather than silently truncating unsupported values.

## Recovered native functions

Addresses below are from the v1.0.0.0 Ghidra project.

| Address | Purpose |
|---|---|
| `00768ed0` | Read the container version/header |
| `0076a604` | Dispatch body loader at version 58 |
| `00767f5c` | Legacy body reader |
| `00768798` | New body reader |
| `00733bac` | Legacy tile reader used by v49 |
| `00733858` | Compact tile reader used by v58+ |
| `0072ad80` | Legacy framed-tile predicate |
| `00733e90` | Current compact tile writer |
| `0076758c` | World body writer and section markers |
| `00605a60` / `00605b9c` | Read/write one chest inventory |
| `006082a4` / `00606768` | Read/write 1,000 chest slots |
| `00726440` / `007260ec` | Read/write one sign slot |
| `00735318` / `0073527c` | Read/write the compact world-state helper |
| `00749d20` / `00749890` | Read/write weather state |
| `00659a38` / `006598b4` | Read/write progression flags |

## Tests

`WinPhoneWorldTests` verifies header dispatch, complete tile allocation, decoded
section counts, native validation, byte-for-byte no-op saves, and edit/reload
behavior for metadata, progression, world state, tiles, and sparse entity data.

The strongest serializer test loads each file, overwrites every cached source byte from offset zero through the end of the parsed world with `0xA5`, and reconstructs it. Output still matches the original byte-for-byte. For v60 this means no original file bytes participate in reconstruction; for v49 only the native-reader-ignored bundle suffix is preserved.
Loading
Loading