--[[
  KX SMSG Static Schema Decoder (RVA Version)
  Part of the kx-packet-inspector project.

  This script decodes a schema from a RELATIVE VIRTUAL ADDRESS (RVA).
  This allows you to copy the offset directly from Ghidra without needing
  to manually calculate the live address.

  --- HOW TO USE ---
  1. EDIT the SCHEMA_RVA_TO_DECODE and OUTPUT_FILE_PATH variables below.
  2. Paste the entire script into the Cheat Engine Lua Engine.
  3. Click "Execute".
  4. The output file will be created at the path you specified.
]]

-- ======================================================================
-- ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼ EDIT THESE TWO LINES ▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼▼
-- ======================================================================

-- Paste the RELATIVE address from Ghidra here (e.g., 25700A0 for the performance schema)
local SCHEMA_RVA_TO_DECODE = 0x25700F0 
local OUTPUT_FILE_PATH = "C:\\SMSG_Schema_Dump.md" -- Set where to save the file

-- ======================================================================
-- ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲ END OF USER EDIT ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲
-- ======================================================================

-- --- Configuration & Setup (Do not edit below here) ---
local GAME_MODULE = "Gw2-64.exe"
local SCHEMA_FIELD_DEF_SIZE = 40
local MAX_RECURSION_DEPTH = 10

local SMSG_TYPECODE_MAP = {
    [0x01] = "short", [0x02] = "byte", [0x03] = "int", [0x04] = "compressed_int",
    [0x05] = "long long", [0x06] = "float", [0x07] = "float[2]", [0x08] = "long long",
    [0x09] = "float[4]", [0x0A] = "special_vec3_and_compressed_int", [0x0B] = "float[4]",
    [0x0C] = "guid", [0x0D] = "string", [0x0E] = "string_utf8", [0x0F] = "Optional Block",
    [0x10] = "Fixed Array", [0x11] = "Variable Array (byte count)",
    [0x12] = "Variable Array (short count)", [0x13] = "Fixed Buffer",
    [0x14] = "Variable Buffer (byte count)", [0x15] = "Variable Buffer (short count)",
    [0x18] = "terminator",
}

local function isAddressValid(addr) return readBytes(addr, 1, false) ~= nil end
local function getPointer(base, offset)
  if not base or base == 0 then return nil end
  local addr = base + (offset or 0)
  if not isAddressValid(addr) then return nil end
  return readPointer(addr)
end

local function parse_schema(file, schema_address, field_prefix, parsed_schemas, depth)
    if not schema_address or schema_address == 0 then return end
    depth = depth or 0
    if depth > MAX_RECURSION_DEPTH then return end
    if parsed_schemas[schema_address] then return end
    parsed_schemas[schema_address] = true
    local field_index = 0
    while true do
        local field_def_addr = schema_address + (field_index * SCHEMA_FIELD_DEF_SIZE)
        if not isAddressValid(field_def_addr) then break end
        local typecode = readInteger(field_def_addr)
        if not typecode or typecode == 0 or typecode == 0x18 then break end
        local type_info = SMSG_TYPECODE_MAP[typecode] or string.format("unknown (0x%02X)", typecode)
        local current_field_id = (field_prefix ~= "") and (field_prefix .. tostring(field_index)) or tostring(field_index)
        file:write(string.format("| %-8s | `0x%02X` | `%s`\n", current_field_id, typecode, type_info))
        if typecode == 0x0F or typecode == 0x10 or typecode == 0x11 or typecode == 0x12 then
            local sub_schema_ptr = getPointer(field_def_addr, 24)
            if sub_schema_ptr and sub_schema_ptr ~= 0 then
                parse_schema(file, sub_schema_ptr, current_field_id .. ".", parsed_schemas, depth + 1)
            end
        end
        field_index = field_index + 1
    end
end

-- --- Main Script Logic ---
local function main()
    print("--- KX SMSG Static Decoder (RVA Version) ---")

    local module_base = getAddress(GAME_MODULE)
    if not module_base or module_base == 0 then
        print(string.format("ERROR: Could not find module '%s'. Is the game running?", GAME_MODULE))
        return
    end

    -- NEW: Calculate the live address from the module base and the provided RVA
    local schema_addr = module_base + SCHEMA_RVA_TO_DECODE
    
    if not schema_addr or not isAddressValid(schema_addr) then
        print(string.format("ERROR: The calculated address 0x%X (from RVA 0x%X) is invalid or unreadable.", schema_addr, SCHEMA_RVA_TO_DECODE))
        return
    end

    local file, err = io.open(OUTPUT_FILE_PATH, "w")
    if not file then
        print("ERROR: Could not open file for writing: " .. tostring(err))
        return
    end

    print(string.format("Decoding schema for RVA 0x%X (Live: 0x%X) and saving to %s", SCHEMA_RVA_TO_DECODE, schema_addr, OUTPUT_FILE_PATH))

    file:write(string.format("# SMSG Schema Dump: `\"%s\"+0x%X`\n\n", GAME_MODULE, SCHEMA_RVA_TO_DECODE))
    file:write("Generated by `KX_SMSG_Static_Decoder_RVA.lua`.\n\n")
    file:write("| Field #  | Typecode | Type Name        |\n")
    file:write("|:---------|:---------|:-----------------|\n")

    local parsed_schemas = {}
    parse_schema(file, schema_addr, "", parsed_schemas, 0)
    file:close()
    print("--- Decode Complete! ---")
end

main()