#pragma author Scrivener07 and Jonathan Ostrus #pragma description Bethesda Papyrus executable compiled script file #pragma magic [FA 57 C0 DE] @ 0x00 #pragma magic [DE C0 57 FA] @ 0x00 import std.sys; import std.core; import std.string; import std.time; enum GAMEID : u16 { GAME_Skyrim = 1, GAME_Fallout4 = 2, GAME_Fallout76 = 3, GAME_Starfield = 4 }; GAMEID g_GameId; namespace formatter { fn pexversion(ref auto data) { return std::format("{}.{}", data.MajorVersion, data.MinorVersion); }; fn structname(ref auto data) { return StringLookup(data.name); }; fn typedstructname(ref auto data) { return std::format("{0} {1}", StringLookup(data.typeName), StringLookup(data.name)); }; fn time(ref auto data) { // We will only support dates back to the epoch std::time::Time time64 = std::time::to_utc(data.Time); return std::time::format(time64, "%c"); }; fn vartype(u8 data) { match (data) { (0x0): return "object"; (0x1): return "identifier"; (0x2): return "string"; (0x3): return "integer"; (0x4): return "float"; (0x5): return "bool"; (_): return "Unknown type"; } }; fn vartypestruct(ref auto data) { return formatter::vartype(data.varType); }; fn arraycount(ref auto data) { return std::format("[{}]", data.count); }; fn functiontype(u8 data) { match(data) { (0): return "regular"; (1): return "property getter"; (2): return "property setter"; (3): std::assert(true, "Unknown function type"); } }; fn resolveop(u8 op) { match (op) { (0x00): return "nop"; (0x01): return "iadd"; (0x02): return "fadd"; (0x03): return "isub"; (0x04): return "fsub"; (0x05): return "imul"; (0x06): return "fmul"; (0x07): return "idiv"; (0x08): return "fdiv"; (0x09): return "imod"; (0x0A): return "not"; (0x0B): return "ineg"; (0x0C): return "fneg"; (0x0D): return "assign"; (0x0E): return "cast"; (0x0F): return "cmp_eq"; (0x10): return "cmp_lt"; (0x11): return "cmp_lte"; (0x12): return "cmp_gt"; (0x13): return "comp_gte"; (0x14): return "jmp"; (0x15): return "jmpt"; (0x16): return "jmpf"; (0x17): return "callmethod"; (0x18): return "callparent"; (0x19): return "callstatic"; (0x1A): return "return"; (0x1B): return "strcat"; (0x1C): return "propget"; (0x1D): return "propset"; (0x1E): return "array_create"; (0x1F): return "array_length"; (0x20): return "array_getlement"; (0x21): return "array_setelement"; (0x22): return "array_findelement"; (0x23): return "array_rfindelement"; (0x24): return "is"; (0x25): return "struct_create"; (0x26): return "struct_get"; (0x27): return "struct_set"; (0x28): return "array_findstruct"; (0x29): return "array_rfindstruct"; (0x2A): return "array_add"; (0x2B): return "array_insert"; (0x2C): return "array_removelast"; (0x2D): return "array_remove"; (0x2E): return "array_clear"; (0x2F): return "array_getallmatchingstructs"; (0x30): return "lock_guards"; (0x31): return "unlock_guards"; (0x32): return "try_lock_guards"; (_): return "Unknown"; } }; fn resolveopstruct(auto data) { return formatter::resolveop(data.op); }; } fn StringLookup(u16 idx) { return pex.stringsTable.strings[idx]; }; struct PexVersion { u8 MajorVersion; u8 MinorVersion; } [[sealed, format("formatter::pexversion")]]; using StringReference = u16 [[format("StringLookup")]]; // Dynamic array of strings struct StringsTable { u16 count; if (count > 0) std::string::SizedString strings[count]; } [[format("formatter::arraycount")]]; struct Time { u64 Time; } [[sealed, format("formatter::time")]]; struct VariableData { u8 varType [[format("formatter::vartype")]]; match (varType) { (0x0): {} // object pointer (0x1): StringReference stringVal; // identifier (0x2): StringReference stringVal; // string (0x3): s32 intVal; // integer (0x4): float floatVal; (0x5): bool boolVal; (_): std::assert(false, "Unknown type for variable data"); } } [[format("formatter::vartypestruct")]]; struct VariableType { StringReference name; StringReference varType; }; struct VariableTypes { u16 count; if (count > 0) VariableType parameter[count] [[inline]]; } [[format("formatter::arraycount")]]; struct Instruction { u8 op [[format("formatter::resolveop")]]; match (op) { ( 0x00 // nop ): {} ( 0x01 | // iadd 0x02 | // fadd 0x03 | // isub 0x04 | // fsub 0x05 | // imul 0x06 | // fmul 0x07 | // idiv 0x08 | // fdiv 0x09 | // imod 0x0F | // cmp_eq 0x10 | // cmp_lt 0x11 | // cmp_lte 0x12 | // cmp_gt 0x13 | // comp_gte 0x1B | // strcat 0x1C | // propget 0x1D | // propset 0x20 | // array_getlement 0x21 | // array_setelement 0x24 | // is 0x26 | // struct_get 0x27 | // struct_set 0x2A | // array_add 0x2B | // array_insert 0x2D // array_remove ): VariableData arguments[3]; ( 0x22 | // array_findelement 0x23 // array_rfindelement ): VariableData arguments[4]; ( 0x14 | // jmp 0x1A | // return 0x25 | // struct_create 0x2C | // array_removelast 0x2E // array_clear ): VariableData arguments[1]; ( 0x0A | // not 0x0B | // ineg 0x0C | // fneg 0x0D | // assign 0x0E | // cast 0x15 | // jmpt 0x16 | // jmpf 0x1E | // array_create 0x1F // array_length ): VariableData arguments[2]; ( 0x28 | // array_findstruct 0x29 // array_rfindstruct ): VariableData arguments[5]; ( 0x2F // array_getallmatchingstructs ): VariableData arguments[6]; ( 0x17 | // callmethod 0x19 // callstatic ): { VariableData arguments[4]; std::assert(arguments[3].varType == 0x3, "VarArgs not integer"); if (arguments[3].intVal > 0) VariableData vararguments[arguments[3].intVal]; } ( 0x18 // callparent ): { VariableData arguments[3]; std::assert(arguments[2].varType == 0x3, "VarArgs not integer"); if (arguments[2].intVal > 0) VariableData vararguments[arguments[2].intVal]; } ( 0x30 | // lock_guards 0x31 // unlock_guards ): { VariableData arguments[1]; std::assert(arguments[0].varType == 0x3, "VarArgs not integer"); if (arguments[0].intVal > 0) VariableData vararguments[arguments[0].intVal]; } ( 0x32 // try_lock_guards ): { VariableData arguments[2]; std::assert(arguments[1].varType == 0x3, "VarArgs not integer"); if (arguments[1].intVal > 0) VariableData vararguments[arguments[1].intVal]; } (_): { std::print("Invalid opcode: {:02x}", op); std::assert(false, "Invalid opcode"); } } } [[format("formatter::resolveopstruct")]];; struct Instructions { u16 count; if (count > 0) Instruction instruction[count] [[inline]]; } [[format("formatter::arraycount")]]; /* this "BaseFunction" struct is used by the Papyrus struct property getter and setter functions, and by regular named functions. */ struct BaseFunction { StringReference returnType; StringReference docString; u32 userFlags; u8 flags; VariableTypes arguments; VariableTypes locals; Instructions instructions; }; struct NamedFunction { StringReference name; BaseFunction function [[inline]]; } [[format("formatter::structname")]]; struct NamedFunctions { u16 count; if (count > 0) NamedFunction function[count] [[inline]]; } [[format("formatter::arraycount")]]; struct State { StringReference name; NamedFunctions functions; } [[format("formatter::structname")]]; struct SyncStates { u16 count; if (count > 0) StringReference names[count] [[inline]]; } [[format("formatter::arraycount")]]; struct States { u16 count; if (count > 0) State state[count] [[inline]]; } [[format("formatter::arraycount")]]; struct Property { StringReference name; StringReference typeName; StringReference docString; u32 userFlags; u8 flags; if ((flags & 0x4) != 0) StringReference autoVarName; else { if ((flags & 0x1) != 0) BaseFunction readHandler; if ((flags & 0x2) != 0) BaseFunction writeHandler; } } [[format("formatter::structname")]]; struct Properties { u16 count; if (count > 0) Property property[count] [[inline]]; } [[format("formatter::arraycount")]]; struct Variable { StringReference name; StringReference typeName; u32 userFlags; VariableData data; if (g_GameId >= GAMEID::GAME_Fallout4) u8 constFlag; } [[format("formatter::structname")]];; struct Variables { u16 count; if (count > 0) Variable variable[count] [[inline]]; } [[format("formatter::arraycount")]]; struct ObjectStructMember { StringReference name; StringReference typeName; u32 userFlags; VariableData data; u8 constFlag; StringReference docString; } [[format("formatter::typedstructname")]]; struct ObjectStructMembers { u16 count; if (count > 0) ObjectStructMember members[count] [[inline]]; } [[format("formatter::arraycount")]]; struct ObjectStruct { StringReference name; ObjectStructMembers members; } [[format("formatter::structname")]]; struct ObjectStructs { u16 count; if (count > 0) ObjectStruct structs[count] [[inline]]; } [[format("formatter::arraycount")]]; struct Guards { u16 count; if (count > 0) StringReference names[count] [[inline]]; } [[format("formatter::arraycount")]]; struct ScriptObjectData { StringReference parentClassName; StringReference docString; if (g_GameId >= GAMEID::GAME_Fallout4) u8 constFlag; u32 userFlags; StringReference autoStateName; if (g_GameId >= GAMEID::GAME_Fallout4) ObjectStructs structs; Variables variables; if (g_GameId >= GAMEID::GAME_Starfield) Guards guards; Properties properties; States states; if (g_GameId == GAMEID::GAME_Fallout76) SyncStates syncStates; }; struct ScriptObject { StringReference name; u32 length; ScriptObjectData data; // The BGS Compiler sets length to sizeof(data) + sizeof(length) // The Caprica compiler sets the length to just sizeof(data) // Since we have no way to identify which compiler is used we'll check // both and just hope it isn't short exactly 4 bytes std::assert(sizeof(data) == length - 4 || sizeof(data) == length, "Length of object data in state missmatch."); }; struct ScriptObjects { u16 count; if (count > 0) ScriptObject scriptObjects[count] [[inline]]; } [[format("formatter::arraycount")]]; struct UserFlag { StringReference name; u8 flagIndex; } [[format("formatter::structname")]]; struct UserFlags { u16 count; if (count > 0) UserFlag userFlags[count] [[inline]]; } [[format("formatter::arraycount")]]; struct StringMembers { u16 count; if (count > 0) StringReference members[count] [[inline]]; } [[format("formatter::arraycount")]]; struct DebugPropertyGroup { StringReference objectName; StringReference name; StringReference docString; u32 userFlags; StringMembers members; } [[format("formatter::structname")]]; struct DebugInstructions { u16 count; if (count > 0) u16 lineNumbers[count] [[inline]]; } [[format("formatter::arraycount")]]; struct DebugFunction { StringReference objectName; StringReference stateName; StringReference name; u8 functionType [[format("formatter::functiontype")]]; DebugInstructions instructions; } [[format("formatter::structname")]]; struct DebugStruct { StringReference parentName; StringReference name; StringMembers members; } [[format("formatter::structname")]]; struct DebugFunctions { u16 count; if (count > 0) DebugFunction functions[count] [[inline]]; } [[format("formatter::arraycount")]]; struct DebugPropertyGroups { u16 count; if (count > 0) DebugPropertyGroup groups[count] [[inline]]; } [[format("formatter::arraycount")]]; struct DebugStructs { u16 count; if (count > 0) DebugStruct structs[count] [[inline]]; } [[format("formatter::arraycount")]]; struct DebugInfo { bool hasDebugInfo; if (hasDebugInfo) { Time modificationTime; DebugFunctions functions; if (g_GameId >= GAMEID::GAME_Fallout4) { DebugPropertyGroups groups; DebugStructs structs; } } }; // Header struct Header { u32 Magic; PexVersion pexVersion; GAMEID GameID; g_GameId = GameID; Time CompilationTime; std::string::SizedString SourceFileName; std::string::SizedString UserName; std::string::SizedString MachineName; }; struct PEX { if (std::mem::read_unsigned(0, 4) == le u32(0xDEC057FA)) std::core::set_endian(std::mem::Endian::Big); else std::core::set_endian(std::mem::Endian::Little); Header header; StringsTable stringsTable; DebugInfo debugInfo; UserFlags userFlags; ScriptObjects scriptObjects; }; PEX pex @ 0x00;