-
-
Save YellowAfterlife/ced985a0d2f770c37b07 to your computer and use it in GitHub Desktop.
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 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.
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...
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
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.
Not sure, just got here on the internet somehow. Interesting file format, though.