Skip to content

Instantly share code, notes, and snippets.

@YellowAfterlife
Last active February 27, 2021 21:37
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save YellowAfterlife/ced985a0d2f770c37b07 to your computer and use it in GitHub Desktop.
Save YellowAfterlife/ced985a0d2f770c37b07 to your computer and use it in GitHub Desktop.
Terraria 1.3 character file format implementation in Haxe
package terra;
import openfl.Lib;
import openfl.utils.ByteArray;
import openfl.utils.Endian;
import Ext.cfor;
using utils.ByteArrayTools;
/**
* ...
* @author YellowAfterlife
*/
@:publicFields
class Player {
/// Vanilla format version
var version:Int = v1340;
//{ version list (todo)
static inline var v1120 = 39;
static inline var v1200 = 58;
static inline var v1220 = 77;
static inline var v1230 = 93;
static inline var v1240 = 98;
static inline var v1241 = 101;
static inline var v1300 = 145;
static inline var v1310 = 168;
static inline var v1330 = 175;
static inline var v1340 = 184;
//}
/// tAPI format version
var tAPIrel:Int = 14;
var tAPIsub:String = "a";
/// Character name
var name:String = "Player";
/// Game difficulty { 0: Softcore, 1: Mediumcore, 2: Hardcore }
var difficulty:Int = 0;
/// Character gender+style
var gender:Int = 1;
//
var healthNow:Int = 100;
var healthMax:Int = 100;
var manaNow:Int = 20;
var manaMax:Int = 20;
//
var hairStyle:Int = 0;
var hairColor:Int = 0xd75a37;
/// [vanilla] hair dye index
var hairDye:Int = 0;
/// [tAPI] hair dye item name
var hairDyeName:String = "";
// hexadecimal RGB colors:
var skinColor:Int = 0xff7d5a;
var eyeColor:Int = 0x695A4B;
var shirtColor:Int = 0xAFA58C;
var underColor:Int = 0xA0B4D7;
var pantsColor:Int = 0xFFE6AF;
var shoesColor:Int = 0xA0693C;
/**
* Due to implementation specifics, all item slots are presented as a single long array.
* A list of constants with offsets can be found below.
* IO_ are section offsets.
* IL_ are section lengths.
*/
var items:Array<Slot>;
static inline var IO_ARM = 0;
static inline var IO_VIS = IO_ARM + 10;
static inline var IL_ARM = 20;
static inline var IO_DYE = IO_ARM + IL_ARM;
static inline var IL_DYE = 10;
static inline var IO_INV = IO_DYE + IL_DYE;
static inline var IL_INV = 50;
static inline var IO_COIN = IO_INV + IL_INV;
static inline var IL_COIN = 4;
static inline var IO_AMMO = IO_COIN + IL_COIN;
static inline var IL_AMMO = 4;
static inline var IO_MEQU = IO_AMMO + IL_AMMO;
static inline var IL_MEQU = 5;
static inline var IO_MDYE = IO_MEQU + IL_MEQU;
static inline var IL_MDYE = 5;
static inline var IO_ST1 = IO_MDYE + IL_MDYE;
static inline var IL_ST1 = 40;
static inline var IO_ST2 = IO_ST1 + IL_ST1;
static inline var IL_ST2 = 40;
static inline var IO_ST3 = IO_ST2 + IL_ST2;
static inline var IL_ST3 = 40;
static inline var IO_MAX = IO_ST3 + IL_ST3;
/** The list of buffs. "Buff" is a simple class containing name:Int and time:Float. */
var buffs:Array<Buff>;
var servers:Array<ServerEntry>;
var hotbarLocked:Bool = false;
var fishingQuestsCompleted:Int = 0;
// 1.2+:
var hideVisual:Int = 0;
var hideMisc:Int = 0;
/** Whether the expert-mode-only extra accessory slot is unlocked. */
var extraAccessory:Bool = false;
/** */
var finishedDD2Event:Bool = false;
/** Global accumulated tax money (in copper coins) */
var taxMoney:Int = 0;
// metadata (speculation):
var metaVersion:Int = 0;
var metaFlags1:Int = 0x0;
var metaFlags2:Int = 0x0;
// playtime (64-bit something):
var playTimeL:Int = 0;
var playTimeH:Int = 0;
// indicators hidden:
var hideInfo:Array<Bool>;
///
var dpadBindings:Array<Int> = [0, 0, 0, 0];
///
var builderAccStatus:Array<Int> = [];
//
var bartenderQuests:Int = 0;
/** trailing data from the end of the file (if any): */
var trail:ByteArray = null;
//
function new() {
items = [];
var i:Int = 0;
// equips:
while (i < IO_INV) items[i++] = new Slot();
// inventory, coin, and ammo slots:
while (i < IO_INV + 58) {
items[i] = new Slot();
items[i].multi = true;
items[i].hasFavFlag = true;
i++;
}
// misc equips / dyes:
while (i < IO_ST1) items[i++] = new Slot();
// storages:
while (i < IO_MAX) {
items[i] = new Slot();
items[i].multi = true;
i++;
}
i = -1; while (++i < IO_MAX) items[i].count = 0;
//
buffs = [];
i = -1; while (++i < 22) buffs[i] = new Buff();
//
servers = [];
//
hideInfo = [];
cfor(i = 0, i < 13, i++, hideInfo[i] = false);
//
cfor(i = 0, i < 10, i++, builderAccStatus.push(0));
}
function handle(d:ByteArray, out:Bool) {
d.endian = Endian.LITTLE_ENDIAN;
//{
inline function r_bool() return d.readBoolean();
inline function w_bool(b) d.writeBoolean(b);
inline function r_byte() return d.readUnsignedByte();
inline function w_byte(b) d.writeByte(b);
inline function r_int32() return d.readInt();
inline function w_int32(i) d.writeInt(i);
inline function r_uint32() return d.readUnsignedInt();
inline function w_uint32(u) d.writeUnsignedInt(u);
inline function r_string() return d.readSharpString();
inline function w_string(s) d.writeSharpString(s);
inline function r_color() return d.readColor();
inline function w_color(c) d.writeColor(c);
//}
/// version
var v:Int;
if (out) w_int32(v = version); else version = v = r_int32();
var maxIds = getMaxIds(v);
var maxBuff = maxIds.buff;
Slot.maxId = maxIds.item;
///
var i:Int, k:Int, n:Int;
var z:Bool;
if (v >= v1300) { // has metadata
if (out) {
w_uint32(0x6F6C6572);
w_uint32(0x03636967);
w_uint32(metaVersion);
w_uint32(metaFlags1);
w_uint32(metaFlags2);
} else {
i = r_uint32();
k = r_uint32();
if (i != 0x6F6C6572 || k != 0x03636967) {
throw "That doesn't seem to be a valid profile.";
}
metaVersion = r_uint32();
metaFlags1 = r_uint32();
metaFlags2 = r_uint32();
}
}
if (out) w_string(name); else name = r_string();
if (out) w_byte(difficulty); else difficulty = r_byte();
if (v >= v1300) { // play time
if (out) {
w_uint32(playTimeL);
w_uint32(playTimeH);
} else {
playTimeL = r_uint32();
playTimeH = r_uint32();
}
}
if (out) w_int32(hairStyle); else hairStyle = r_int32();
if (v >= 82) {
if (out) w_byte(hairDye); else hairDye = r_byte();
}
if (v >= 83) { // hidevisual
if (out) {
w_byte(hideVisual & 0xff);
if (v >= v1300) w_byte((hideVisual >> 8) & 0xff);
} else {
hideVisual = r_byte();
if (v >= v1300) hideVisual |= r_byte() << 8;
}
}
if (v >= v1300) {
if (out) w_byte(hideMisc); else hideMisc = r_byte();
}
if (v >= v1300) {
if (out) w_byte(gender); else gender = r_byte();
} else {
if (out) w_bool(gender < 4); else gender = r_bool() ? 0 : 4;
}
// health/mana:
if (out) w_int32(healthNow); else healthNow = r_int32();
if (out) w_int32(healthMax); else healthMax = r_int32();
if (out) w_int32(manaNow); else manaNow = r_int32();
if (out) w_int32(manaMax); else manaMax = r_int32();
if (v >= v1300) {
if (out) w_bool(extraAccessory); else extraAccessory = r_bool();
if (v >= v1340) {
if (out) w_bool(finishedDD2Event); else finishedDD2Event = r_bool();
}
if (out) w_int32(taxMoney); else taxMoney = r_int32();
}
// colors:
if (out) w_color(hairColor); else hairColor = r_color();
if (out) w_color(skinColor); else skinColor = r_color();
if (out) w_color(eyeColor); else eyeColor = r_color();
if (out) w_color(shirtColor); else shirtColor = r_color();
if (out) w_color(underColor); else underColor = r_color();
if (out) w_color(pantsColor); else pantsColor = r_color();
if (out) w_color(shoesColor); else shoesColor = r_color();
// armor/visual/dye (id + prefix):
if (v >= v1300) { // 3 armor + 3 socarm + 7 acc + 7 socacc?
cfor(i = 0, i < 10, i++, items[IO_ARM + i].handle(d, v, out));
cfor(i = 0, i < 10, i++, items[IO_VIS + i].handle(d, v, out));
cfor(i = 0, i < 10, i++, items[IO_DYE + i].handle(d, v, out));
} else if (v >= 81) { // 3 armor + 3 socarm + 5 acc + 5 socacc
cfor(i = 0, i < 8, i++, items[IO_ARM + i].handle(d, v, out));
cfor(i = 0, i < 8, i++, items[IO_VIS + i].handle(d, v, out));
cfor(i = 0, i < 8, i++, items[IO_DYE + i].handle(d, v, out));
} else { // 3 armor + 3 socarm + 5 acc
cfor(i = 0, i < 8, i++, items[IO_ARM + i].handle(d, v, out));
cfor(i = 0, i < 3, i++, items[IO_VIS + i].handle(d, v, out));
if (v > v1120) cfor(i = 0, i < 3, i++, items[IO_DYE + i].handle(d, v, out));
}
// inventory (id + quantity + prefix + flag):
if (v >= v1200) n = 58; else n = 48;
cfor(i = 0, i < n, i++, {
items[IO_INV + i].handle(d, v, out);
});
// miscEquips/miscDyes (id + prefix):
if (v >= v1300) cfor(i = 0, i < 5, i++, {
items[IO_MEQU + i].handle(d, v, out);
items[IO_MDYE + i].handle(d, v, out);
});
// bank/safe (id + quantity + prefix):
if (v >= v1200) n = 40; else n = 20;
if (v >= v1310) {
cfor(i = 0, i < n, i++, items[IO_ST1 + i].handle(d, v, out));
cfor(i = 0, i < n, i++, items[IO_ST2 + i].handle(d, v, out));
} else cfor(i = 0, i < n, i++, {
items[IO_ST1 + i].handle(d, v, out);
items[IO_ST2 + i].handle(d, v, out);
});
if (v >= v1340) {
cfor(i = 0, i < IL_ST3, i++, items[IO_ST3 + i].handle(d, v, out));
}
// buffs (id + duration):
if (v >= v1220) n = 22; else n = 10;
cfor(i = 0, i < n, i++, {
if (out) {
w_int32(buffs[i].id);
w_int32(Std.int(buffs[i].time));
} else {
k = r_int32();
buffs[i].set(k, r_int32());
}
});
// world list:
var sv:ServerEntry;
if (out) {
n = servers.length;
cfor(i = 0, i < n, i++, {
sv = servers[i];
if (sv.spawnX != 0 || sv.spawnY != 0 || sv.address != 0) {
w_int32(sv.spawnX);
w_int32(sv.spawnY);
w_int32(sv.address);
w_string(sv.name);
}
});
w_int32( -1);
} else cfor(i = 0, i < 200, i++, {
k = r_int32();
if (k == -1) break;
sv = new ServerEntry();
sv.spawnX = k;
sv.spawnY = r_int32();
sv.address = r_int32();
sv.name = r_string();
if (sv.spawnX != 0 || sv.spawnY != 0 || sv.address != 0) {
servers.push(sv);
}
});
//
if (out) w_bool(hotbarLocked); else hotbarLocked = r_bool();
// status labels near the map:
if (v >= v1300) {
n = 13;
cfor(i = 0, i < n, i++, {
if (out) w_bool(hideInfo[i]); else hideInfo[i] = r_bool();
});
}
//
if (v >= v1240) {
if (out) w_int32(fishingQuestsCompleted); else fishingQuestsCompleted = r_int32();
}
// detect padding:
var len:Int = d.length;
if (!out) {
var prev = d.position;
d.position = len - 1;
n = d.readUnsignedByte();
if (n > 0 && n < 16) {
d.position = len - n;
// verify that padding is actually padding:
cfor(i = 0, i < n, i++, {
if (d.readByte() != n) break;
});
if (i >= n) len -= n;
}
d.position = prev;
}
// 1.3.1:
if (v >= v1310) {
n = 4;
cfor(i = 0, i < n, i++, {
if (out) w_int32(dpadBindings[i]); else dpadBindings[i] = r_int32();
});
n = 10;
cfor(i = 0, i < n, i++, {
if (out) w_int32(builderAccStatus[i]); else builderAccStatus[i] = r_int32();
});
}
if (v >= v1340) {
if (out) w_int32(bartenderQuests); else bartenderQuests = r_int32();
}
// trailing data (mod compatibility):
if (out) {
if (trail != null) {
trail.position = 0;
n = trail.length;
while (--n >= 0) w_byte(trail.readByte());
}
} else {
n = len - d.position;
trace(n);
if (n > 0) {
trail = new ByteArray();
while (--n >= 0) trail.writeByte(r_byte());
} else trail = null;
}
// padding:
if (out) {
i = n = 16 - (d.length & 15);
while (--n >= 0) w_byte(i);
}
}
function load(d:ByteArray):Void {
handle(d, false);
}
function save(d:ByteArray):Void {
handle(d, true);
}
static function getMaxIds(v:Int):{ item:Int, buff:Int } {
var i:Int;
var b:Int;
if (v > v1340 + 10) { // next
i = 16384;
b = 1024;
} else if (v >= v1340) {
i = 3900; // 3893
b = 205;
} else if (v >= v1330) {
i = 3796;
b = 190;
} else if (v >= v1310) {
i = 3729;
b = 190;
} else if (v >= v1300) {
i = 3601;
b = 190;
} else if (v >= v1240) {
i = 2748;
b = 139;
} else if (v >= v1230) {
i = 2288;
b = 103;
} else if (v >= v1220) {
i = 1965;
b = 93;
} else if (v >= 70) {
i = 1725;
b = 80;
} else if (v >= 69) {
i = 1614;
b = 80;
} else if (v >= v1120) {
i = 603;
b = 40;
} else { // where did you find this anyway
i = 603;
b = 40;
}
return { item: i, buff: b };
}
}
@willeccles
Copy link

// where did you find this anyway

Not sure, just got here on the internet somehow. Interesting file format, though.

@YellowAfterlife
Copy link
Author

@willeccles that comment was in regards of it being a challenge to find builds of Terraria older than 1.1 (circa 2011), as well as arguable benefit of supporting file format from those.

@iconmaster5326
Copy link

Is there a 1.4 implementation of this hanging around anywhere? I'm looking to manipulate journey mode research completion programmatically, and literally all of the documentation I can find on Terraria file formats is for 1.3...

@iconmaster5326
Copy link

I just remembered that tModLoader is a thing that lets you decompile the source code directly, so I just went ahead and looked at that. I made a Kaitai Struct description file for version 1.4 player files: https://gist.github.com/iconmaster5326/3c723eba0b0ebfc41f85f6b1cc00df91

@YellowAfterlife
Copy link
Author

These days, using ILSpy to inspect the game's native implementation is a pretty good bet - thanks to improvements both to decompiler (at some point ILSpy struggled to open handle some of the game's larger functions due to sheer number of nested blocks in them) and the game code (which in many places now looks fit for the game's scale), you can make sense of it pretty well. I'd imagine that this is also the reason why people are no longer maintaining file format documentation by hand.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment