Character & profile save editor for ARK: Survival Ascended (ASA). Decode .arkprofile player save files to editable JSON and encode them back, byte-accurate. Rename characters, change levels and stat points, transfer a character to another account, fix broken saves — as a VS Code extension or standalone CLI.
⚠️ AI-generated code. This entire project (binary format reverse-engineering, codec, VS Code extension, tests, and this README) was generated by Claude (Anthropic) in July 2026, reverse-engineered from four sample.arkprofilefiles (three different players, different sizes). It has passed byte-identical round-trip tests on all of them, but the format was inferred, not taken from official documentation. Always run Verify round-trip on a file before editing it, keep backups, and treat unexpected files with suspicion.
| Command | Effect |
|---|---|
ARK Profile: Decode to JSON |
Reads <name>.arkprofile, writes <name>.arkprofile.json next to it and opens it |
ARK Profile: Encode JSON back to .arkprofile |
Rebuilds the binary from the JSON. Previous binary is kept as .bak. Refuses to write if the rebuilt binary does not parse back cleanly |
ARK Profile: Verify round-trip |
Decodes + re-encodes a file in memory and checks the result is byte-identical to the original. Run this once on every new file before editing |
Commands are available from the Explorer right-click menu and the Command Palette.
Standalone CLI (no VS Code needed):
node cli.js decode file.arkprofile [out.json]
node cli.js encode file.arkprofile.json [out.arkprofile]
node cli.js verify file.arkprofile
Plain Node.js, no dependencies, no build step.
- VS Code 1.75+ for the extension
- Node.js (any currently supported version) for the CLI and tests — no packages to install
Extension — pick one:
code --install-extension arkprofile-editor-1.1.0.vsix
or in VS Code: Extensions sidebar → ⋯ menu → Install from VSIX... → select the file. Reload when prompted.
CLI only — nothing to install; run the scripts in this folder directly with node.
To rebuild the .vsix after changing the source, zip the package contents ([Content_Types].xml, extension.vsixmanifest, extension/ folder) or use vsce package if you have it.
| File | Role |
|---|---|
codec.js |
The heart: binary ⇄ JSON translation. Everything else is a thin wrapper |
extension.js |
VS Code glue. Core functions doDecode / doEncode / doVerify are exported and work without VS Code |
cli.js |
Command-line wrapper around the codec |
test.js |
Test suite (see below) |
package.json |
Extension manifest |
.arkprofile is an Unreal Engine binary save. Everything is little-endian.
[Header]
int32 version (7 in the sample)
int32 unknownA (522 in the sample – meaning unknown, preserved verbatim)
int32 unknownB (1013 in the sample – meaning unknown, preserved verbatim)
int32 objectCount
[Object table] × objectCount
byte[16] guid
UEString class e.g. ".../PrimalPlayerDataBP.PrimalPlayerDataBP_C"
int32 isItem
int32 nameCount, then nameCount × UEString
int32 f1, f2, f3 (zeros in the sample, preserved verbatim)
int32 propsOffset ABSOLUTE file offset of this object's property block
int32 unknown
[Property blocks] one per object, at each propsOffset
byte lead (0x00 in the sample)
...property list, terminated by a property named "None"...
trailer: either up to 8 zero bytes, or int32 1 + byte[16] guid
int32 n
n > 0 → n bytes UTF-8, includes trailing NUL
n < 0 → |n| UTF-16LE code units, includes trailing NUL
n = 0 → empty
The decoder marks UTF-16 strings with "_wide": true so they re-encode identically.
UEString name ("None" terminates a property list)
UEString type e.g. "IntProperty", "StructProperty"
repeat { int32 flag; flag==1 → read UEString typeArg; flag==0 → stop }
int32 dataSize
byte flags
bit 0x01: an int32 array index follows (used when the same
property name repeats, e.g. per-stat level-up points:
index 0 = Health, 1 = Stamina, ...)
for BoolProperty the remaining bits ARE the value
(0x10 = true, 0x00 = false); dataSize is 0
[int32 index] only if flags bit 0x01 set
byte[dataSize] payload
| Type | Payload |
|---|---|
| Int8/16/Int/UInt16/32, Float, Double | scalar |
| Int64 / UInt64 | scalar, stored as string in JSON (JS precision) |
| BoolProperty | no payload; value lives in the flags byte |
| StrProperty / NameProperty | UEString |
| ObjectProperty | int32 kind: 1 → UEString path, 0 → int32 index |
| ByteProperty | plain byte, or (when typeArg = enum name) a UEString enum value |
| StructProperty | either a nested property list (recursive, "None"-terminated), a raw fixed struct (Vector, Rotator, Quat, Vector2D, LinearColor, Color, IntPoint), or UniqueNetIdRepl (see below) |
| ArrayProperty | int32 count + items. Struct arrays = count × ("None"-terminated property list) |
UniqueNetIdRepl (the player's platform identity) has its own layout:
u8 flags (0xF9 observed)
UEString idType ("RedpointEOS")
u8 idLength (16 observed)
byte[idLength] id the player's EOS id — this usually matches the
.arkprofile filename
Decoded as { "_netIdFlags", "idType", "id" (hex) }. Editing the id re-parents the profile to another player — only do that deliberately.
Anything the codec cannot confidently decode is preserved as a "raw" hex blob — byte-exact, safe, just not editable. On all four samples coverage is 100%: zero raw blobs. If a raw blob ever appears, leave it alone.
Encoding is the exact inverse. All dataSize values are recomputed bottom-up and all propsOffset values are re-patched, so edits that change lengths (longer name, extra array item) are fine. Unknown header ints, flags, leads and trailers are written back verbatim.
index— array index for repeated property namesraw— undecoded payload as hex; do not touch (none present in the samples)- keys starting with
_(_wide,_g2,_lead,_trailer,_boolByte,_extra) — format plumbing; do not touch - Int64/UInt64 values are strings; keep them as strings
The .arkprofile is a template, not live state. While a character is alive in the world, its XP, stats and unlocks live in the map save (.ark). The profile is applied when the game rebuilds the character: on creation, download, or respawn.
Practical consequence, confirmed in the field: edits to unlocks appear to "not work" until the character dies or respawns once. Identity fields (name, ids) are read immediately. XP is the exception: the profile's XP value is a snapshot the game writes, not reads — editing it grants nothing, even after respawn. Grant XP in-game instead with an admin command, e.g. cheat GiveExpToPlayer <PlayerDataID> <amount> 0 0 (or cheat GiveExperience <amount> 0 0 as the player). So the workflow for stat edits is: stop server → edit → replace file → start server → kill/respawn the character once → give XP via command if needed.
| Field | Meaning | Notes |
|---|---|---|
PlayerName |
Account/player name shown in-game | safe to edit |
UniqueID → id |
The player's EOS id (hex) | must match the filename for the game to load it for that player. Changing it re-parents the profile to another account |
PlayerDataID |
Numeric character id | referenced by tribes/ownership in the map save; change only when deliberately transferring identity |
SavedNetworkAddress |
Last IP the player connected from | cosmetic, safe |
MyPlayerCharacterConfig |
Appearance: BodyColors (indexed), RawBoneModifiers (indexed sliders), DynamicMaterialBytes, PlayerCharacterName (the character's name, distinct from PlayerName), gender flag |
safe to edit; applied on rebuild |
MyPersistentCharacterStats |
The progression payload | see below |
Inside MyPersistentCharacterStats:
| Field | Meaning |
|---|---|
CharacterStatusComponent_ExtraCharacterLevel |
Level above base (level = this + 1) |
CharacterStatusComponent_ExperiencePoints |
Total XP — write-only snapshot: the game records it here but does not read it back, not even on respawn. To grant XP use an admin command (GiveExpToPlayer / GiveExperience) |
CharacterStatusComponent_NumberOfLevelUpPointsApplied (indexed) |
Points per stat; index 0 = Health (entry has no index key), 1 = Stamina, 2 = Torpidity, 3 = Oxygen, 4 = Food, 5 = Water, 6 = Temperature, 7 = Weight, 8 = Melee, 9 = Speed, 10 = Fortitude, 11 = Crafting. Entries are sparse: stats with 0 points have no entry — add one to grant points |
PlayerState_EngramBlueprints |
Learned engrams (array of blueprint paths); PlayerState_TotalEngramPoints = points earned |
CurrentMilestones / MilestoneProgress / CompletedMilestones / MilestoneLevelsAndIndexes |
Boss/ascension progression |
PerMapExplorerNoteUnlocks / PerMapNamedExplorerNoteUnlocks / EmoteUnlocks |
Explorer notes and emotes |
Exact stat-index mapping and some unlock field names may vary per game version — decode a known profile and inspect before mass-editing.
Rename a player — edit MyData/PlayerName (and optionally MyPlayerCharacterConfig/PlayerCharacterName for the character itself). Encode. Done.
Change level / stat points — edit ExtraCharacterLevel and/or the indexed NumberOfLevelUpPointsApplied entries. Encode. The character must respawn once before it shows. Note: level gained this way sets the level directly; XP shown may not match — top up with GiveExpToPlayer if you want consistent numbers.
Give a character to another account (the "lost account" rescue):
- Copy the donor
.arkprofile - Edit
UniqueID/idto the recipient's EOS id - Edit
PlayerDataIDto the recipient's old character id if known (tribe/structure ownership references it) - Edit
PlayerName, and appearance inMyPlayerCharacterConfigif desired - Encode, rename the file to
<recipient EOS id>.arkprofile, place inSavedArks/<map>/ - Unlocks (bosses, notes, engrams) apply only after the character dies/respawns once; XP does not transfer via the file at all — grant it afterwards with
cheat GiveExpToPlayer <PlayerDataID> <amount> 0 0
Change appearance — BodyColors are float RGBA 0–1; RawBoneModifiers are the character-creation sliders (roughly −0.5…0.5); DynamicMaterialBytes are 0–255 channels. Applied on rebuild.
rawblobs (none in current samples) and_-prefixed keys — format plumbing- Int64/UInt64 string values: change the digits, keep them strings
- Two live profiles on one cluster sharing a
PlayerDataID— untested, likely ownership conflicts
Run with:
node test.js path/to/some.arkprofile
The suite must be 100% green before you trust the codec with a file:
- Byte-identical round-trip —
encode(decode(file))equals the original file byte for byte. This is the core guarantee; if this fails, do not edit the file. - Decode coverage — reports how many properties decoded vs. how many stayed
raw. - Value spot-checks — a string property, an int property and (if present) duplicate-name indexed properties are readable.
- Edit survival — changes a string to a longer Unicode value (forces UTF-16 re-encode and shifts every size/offset after it), changes an int, flips a bool → encodes → re-decodes → asserts all edits are present.
- Edited file self-consistency — the edited binary also round-trips byte-identically through decode→encode.
- Backup behavior —
doEncodewrites a.bakof the previous binary. - Reparse guard — encode is preceded by a decode of the rebuilt bytes; a corrupt rebuild throws instead of writing.
Tests 1, 4 and 5 together prove decode and encode are exact inverses, both for the original bytes and for modified content.
Studio Wildcard can change the save format in any patch. If Verify round-trip starts failing on new files, take this whole folder plus one fresh .arkprofile file, open Claude (claude.ai or Claude Code in this repo), and use this prompt:
This repo contains a reverse-engineered codec for ARK: Survival Ascended
.arkprofile binary save files (see README.md for the current format spec,
codec.js for the implementation, test.js for the test suite).
The format has apparently changed: `node cli.js verify <file>` no longer
reports a byte-identical round trip on the attached .arkprofile file
(and/or decode throws an error).
Your task:
1. Read README.md (format spec) and codec.js first.
2. Run: node cli.js verify <file> and node cli.js decode <file> out.json
to see where it breaks.
3. Hex-dump the file around the failure offset and compare against the
spec in README.md to find what changed (new header fields, changed
property layout, new types, new flags bits, ...).
4. Update codec.js. Hard requirements:
- encode(decode(file)) must be BYTE-IDENTICAL to the original file
- anything you cannot decode confidently must be preserved as a
"raw" hex blob, never guessed
- unknown header/flag/trailer bytes are preserved verbatim
- plain Node.js, no dependencies, no build step
5. Run node test.js <file> and make all tests pass, including the
edit-survival test.
6. Update the format spec section of README.md with what changed.
Do not declare success until `node test.js <file>` is fully green.
Known weak points a format change would likely hit first:
- the two unknown header ints (
522,1013) — identical across all four samples, so likely game-version constants; could still be values that need recomputing after a patch - the flags byte — only values
0x00/0x01/0x08/0x09/0x10were observed; other bits may imply extra fields - the trailer after each property block — two variants observed, others may exist
- the
RAW_STRUCTStable — new fixed-size struct types would decode wrong or fall back to raw UniqueNetIdRepl— layout assumes a single (type, id) pair; console/crossplay ids may differ
- Stop the server / close the game before touching profile files.
- Run Verify round-trip on the file first.
- Encode always leaves a
.bak; keep your own backups anyway. - Never edit
rawor_-prefixed fields. - This was reverse-engineered from four files (same server/game version) by an AI. Files from other game versions, platforms, or mods may differ.
MIT. Not affiliated with Studio Wildcard; ARK: Survival Ascended is their trademark. Use on your own save files at your own risk.