Skip to content

Instantly share code, notes, and snippets.

@YellowAfterlife YellowAfterlife/Player.hx
Last active Dec 1, 2017

Embed
What would you like to do?
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

This comment has been minimized.

Copy link

commented Oct 25, 2017

// where did you find this anyway

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

@YellowAfterlife

This comment has been minimized.

Copy link
Owner Author

commented Dec 1, 2017

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.