Skip to content

Instantly share code, notes, and snippets.

@Frityet
Created February 7, 2026 03:05
Show Gist options
  • Select an option

  • Save Frityet/ce80b9c18d63a4e962dcd81d1fc101bd to your computer and use it in GitHub Desktop.

Select an option

Save Frityet/ce80b9c18d63a4e962dcd81d1fc101bd to your computer and use it in GitHub Desktop.
#!/usr/bin/env luajit
local lfs = require("lfs")
local function fail(message)
error(message, 0)
end
local function printf(fmt, ...)
io.write(string.format(fmt, ...))
end
local function trim(s)
return (s:gsub("^%s+", ""):gsub("%s+$", ""))
end
local function shell_quote(s)
return "'" .. tostring(s):gsub("'", "'\\''") .. "'"
end
local function run_capture(cmd)
local handle = io.popen(cmd .. " 2>&1")
if not handle then
return false, "failed to start command: " .. cmd, 1
end
local output = handle:read("*a") or ""
local ok, _, status = handle:close()
if ok then
return true, output, 0
end
return false, output, status or 1
end
local function file_exists(path)
local f = io.open(path, "rb")
if not f then
return false
end
f:close()
return true
end
local function read_file(path)
local f = io.open(path, "rb")
if not f then
return nil
end
local text = f:read("*a")
f:close()
return text
end
local function write_file(path, text)
local f = io.open(path, "wb")
if not f then
return false
end
f:write(text)
f:close()
return true
end
local function mkdir_p(path)
if path == "" or path == "." then
return true
end
local is_abs = path:sub(1, 1) == "/"
local current = is_abs and "/" or ""
for part in path:gmatch("[^/]+") do
if current == "" or current == "/" then
current = current .. part
else
current = current .. "/" .. part
end
local attr = lfs.attributes(current)
if not attr then
local ok, err = lfs.mkdir(current)
if not ok then
return false, err
end
elseif attr.mode ~= "directory" then
return false, current .. " exists and is not a directory"
end
end
return true
end
local function split_lines(text)
local lines = {}
for line in text:gmatch("([^\n]*)\n?") do
if line == "" and #lines > 0 and lines[#lines] == "" and #text > 0 and text:sub(-1) ~= "\n" then
break
end
table.insert(lines, line)
end
return lines
end
local function load_json_decoder()
do
local ok, mod = pcall(require, "dkjson")
if ok and mod and type(mod.decode) == "function" then
local null = {}
local function decode(text)
local obj, _, err = mod.decode(text, 1, null)
if err then
return nil, err, null
end
return obj, nil, null
end
return "dkjson", decode
end
end
do
local ok, mod = pcall(require, "cjson.safe")
if ok and mod and type(mod.decode) == "function" then
local function decode(text)
local obj, err = mod.decode(text)
if not obj then
return nil, err or "cjson.safe decode failed", mod.null
end
return obj, nil, mod.null
end
return "cjson.safe", decode
end
end
do
local ok, mod = pcall(require, "cjson")
if ok and mod and type(mod.decode) == "function" then
local function decode(text)
local success, obj = pcall(mod.decode, text)
if not success then
return nil, tostring(obj), mod.null
end
return obj, nil, mod.null
end
return "cjson", decode
end
end
return nil, nil
end
local function unquote(s)
local q = s:match('^"(.*)"$')
if q then
return q
end
return s
end
local function unescape_asm_string(s)
return (s:gsub("\\\\", "\1")
:gsub('\\"', '"')
:gsub("\\n", "\n")
:gsub("\\r", "\r")
:gsub("\\t", "\t")
:gsub("\\0", "\0")
:gsub("\1", "\\"))
end
local function escape_cpp_string(s)
return (s:gsub("\\", "\\\\"):gsub('"', '\\"'))
end
local function usage()
printf("%s", [[
Usage:
luajit tools/nameobj_fuzzy.lua [options]
Options:
--source <path> Source path to check in objdiff report.
Default: src/Game/NameObj/NameObj.cpp
--header <path> Header path to verify exists.
Default: include/Game/NameObj/NameObj.hpp
--report <path> Report JSON output path.
Default: /tmp/nameobj_objdiff_report.json
--generate-stubs If fuzzy < 100, generate class stubs from unresolved create*<T> funcs.
--write Write generated stub header and patch source include/table.
--sync-create-table Sync NameObjFactory cCreateTable from asm ground truth.
--factory-source <path> Factory source to patch with stub include.
Default: src/Game/NameObj/NameObjFactory.cpp
--stub-header <path> Generated stub header path.
Default: include/Game/NameObj/NameObjFactoryStubs.hpp
--stub-dir <path> Directory for generated per-class stub headers.
Default: include/Game/NameObjFactoryStubs
--factory-asm <path> NameObjFactory asm path for table/size extraction.
Default: build/RMGK01/asm/Game/NameObj/NameObjFactory.s
--verbose Print extra command output.
--help Show this help.
]])
end
local cfg = {
source = "src/Game/NameObj/NameObj.cpp",
header = "include/Game/NameObj/NameObj.hpp",
report = "/tmp/nameobj_objdiff_report.json",
generate_stubs = false,
write = false,
sync_create_table = false,
factory_source = "src/Game/NameObj/NameObjFactory.cpp",
stub_header = "include/Game/NameObj/NameObjFactoryStubs.hpp",
stub_dir = "include/Game/NameObjFactoryStubs",
factory_asm = "build/RMGK01/asm/Game/NameObj/NameObjFactory.s",
verbose = false,
}
local legacy_stub_header = "include/Game/NameObj/AutoNameObjFactoryStubs.hpp"
local legacy_stub_dir = "include/Game/NameObj/AutoNameObjFactoryStubs"
local force_stub_classes = {
Flag = true,
IceVolcanoUpDownPlane = true,
}
local morph_item_kind_by_class = {
MorphItemNeoHopper = 1,
MorphItemNeoBee = 2,
MorphItemNeoTeresa = 3,
MorphItemNeoIce = 4,
MorphItemNeoFire = 5,
MorphItemNeoFoo = 6,
}
local effect_stub_specs = {
EffectObjR1000F50 = {
radius = 1000.0,
far = 50.0,
},
EffectObjR500F50 = {
radius = 500.0,
far = 50.0,
},
EffectObjR100F50SyncClipping = {
radius = 100.0,
far = 50.0,
sync = true,
},
EffectObj10x10x10SyncClipping = {
radius = 1000.0,
far = 50.0,
sync = true,
center_y = 580.0,
},
EffectObj20x20x10SyncClipping = {
radius = 1000.0,
far = 50.0,
sync = true,
center_y = 200.0,
},
EffectObj50x50x10SyncClipping = {
radius = 2500.0,
far = 50.0,
sync = true,
center_y = 200.0,
},
}
local special_header_paths = {
MorphItemObjNeo = "include/Game/Player/MorphItemObjNeo.hpp",
MorphItemNeoBee = "include/Game/Player/MorphItemNeoBee.hpp",
MorphItemNeoFire = "include/Game/Player/MorphItemNeoFire.hpp",
MorphItemNeoFoo = "include/Game/Player/MorphItemNeoFoo.hpp",
MorphItemNeoHopper = "include/Game/Player/MorphItemNeoHopper.hpp",
MorphItemNeoIce = "include/Game/Player/MorphItemNeoIce.hpp",
MorphItemNeoTeresa = "include/Game/Player/MorphItemNeoTeresa.hpp",
EffectObjR1000F50 = "include/Game/Effect/EffectObjR1000F50.hpp",
EffectObjR500F50 = "include/Game/Effect/EffectObjR500F50.hpp",
EffectObjR100F50SyncClipping = "include/Game/Effect/EffectObjR100F50SyncClipping.hpp",
EffectObj10x10x10SyncClipping = "include/Game/Effect/EffectObj10x10x10SyncClipping.hpp",
EffectObj20x20x10SyncClipping = "include/Game/Effect/EffectObj20x20x10SyncClipping.hpp",
EffectObj50x50x10SyncClipping = "include/Game/Effect/EffectObj50x50x10SyncClipping.hpp",
}
local json_backend, decode_json = load_json_decoder()
if not decode_json then
fail("missing Lua JSON module. Install one (for example: sudo luarocks --lua-version=5.1 install dkjson)")
end
local function read_value(i, option)
if i + 1 > #arg or arg[i + 1] == nil then
fail("missing value for option: " .. option)
end
return arg[i + 1]
end
do
local i = 1
while i <= #arg do
local a = arg[i]
if a == "--source" then
cfg.source = read_value(i, a)
i = i + 1
elseif a == "--header" then
cfg.header = read_value(i, a)
i = i + 1
elseif a == "--report" then
cfg.report = read_value(i, a)
i = i + 1
elseif a == "--factory-source" then
cfg.factory_source = read_value(i, a)
i = i + 1
elseif a == "--stub-header" then
cfg.stub_header = read_value(i, a)
i = i + 1
elseif a == "--stub-dir" then
cfg.stub_dir = read_value(i, a)
i = i + 1
elseif a == "--factory-asm" then
cfg.factory_asm = read_value(i, a)
i = i + 1
elseif a == "--generate-stubs" then
cfg.generate_stubs = true
elseif a == "--write" then
cfg.write = true
elseif a == "--sync-create-table" then
cfg.sync_create_table = true
elseif a == "--verbose" then
cfg.verbose = true
elseif a == "--help" or a == "-h" then
usage()
os.exit(0)
else
usage()
fail("unknown argument: " .. tostring(a))
end
i = i + 1
end
end
if not file_exists(cfg.source) then
fail("missing source: " .. cfg.source)
end
if not file_exists(cfg.header) then
fail("missing header: " .. cfg.header)
end
local is_nameobj_factory = cfg.source == cfg.factory_source
if cfg.write and is_nameobj_factory then
cfg.sync_create_table = true
end
do
local cmd = "./build/tools/objdiff-cli report generate -o " .. shell_quote(cfg.report)
local ok, out, code = run_capture(cmd)
if cfg.verbose or not ok then
printf("%s", out)
end
if not ok then
fail("objdiff report generation failed (exit " .. tostring(code) .. ")")
end
end
local report_text = read_file(cfg.report)
if not report_text then
fail("failed to read report file: " .. cfg.report)
end
local report, decode_err, json_null = decode_json(report_text)
if not report then
fail("failed to decode report JSON with " .. json_backend .. ": " .. tostring(decode_err))
end
local function find_unit_by_source(report_data, source_path)
local units = report_data and report_data.units
if type(units) ~= "table" then
return nil
end
for _, unit in ipairs(units) do
local md = unit and unit.metadata
if type(md) == "table" and md.source_path == source_path then
return unit
end
end
return nil
end
local unit = find_unit_by_source(report, cfg.source)
if not unit then
fail("could not find source in report: " .. cfg.source)
end
local fuzzy = tonumber(unit.measures and unit.measures.fuzzy_match_percent)
if not fuzzy then
fail("invalid fuzzy match value in report for source: " .. cfg.source)
end
printf("Source: %s\n", cfg.source)
printf("Header: %s\n", cfg.header)
printf("Fuzzy: %.4f%%\n", fuzzy)
printf("JSON: %s\n", json_backend)
if fuzzy >= 100.0 and not cfg.sync_create_table and not cfg.generate_stubs then
printf("Already at 100%% fuzzy. No scaffolding needed.\n")
os.exit(0)
end
local asm_text = nil
if is_nameobj_factory and (cfg.sync_create_table or cfg.generate_stubs) then
asm_text = read_file(cfg.factory_asm)
if not asm_text then
fail("failed to read factory asm: " .. cfg.factory_asm)
end
end
local function symbol_to_creator_expr(symbol_ref)
local sym = trim(symbol_ref)
if sym == "0x00000000" then
return "nullptr", nil, nil
end
sym = unquote(sym)
local creator, class_name = sym:match("^(create[%a_]+)<%d*([A-Za-z_][A-Za-z0-9_]*)>__")
if creator and class_name then
return creator .. "<" .. class_name .. ">", class_name, creator
end
local mr_name = sym:match("^([A-Za-z_][A-Za-z0-9_]*)__2MR")
if mr_name then
return "MR::" .. mr_name, nil, nil
end
local plain = sym:match("^([A-Za-z_][A-Za-z0-9_]*)__")
if plain then
return plain, nil, nil
end
return sym, nil, nil
end
local function parse_asm_create_table(text)
local in_table = false
local words = {}
for line in text:gmatch("([^\n\r]+)") do
if not in_table then
if line:find('.obj "cCreateTable__28@unnamed@NameObjFactory_cpp@", global', 1, true) then
in_table = true
end
else
if line:match("^%s*%.endobj") then
break
end
local ref = line:match("^%s*%.4byte%s+(.+)$")
if ref then
table.insert(words, trim(ref))
end
end
end
if #words == 0 then
fail("failed to parse cCreateTable words from asm")
end
if (#words % 3) ~= 0 then
fail("malformed cCreateTable words in asm: expected triple entries")
end
local entries = {}
local creator_kinds = {}
for i = 1, #words, 3 do
local name_ref = words[i]
local creator_ref = words[i + 1]
local archive_ref = words[i + 2]
local creator_expr, class_name, creator_kind = symbol_to_creator_expr(creator_ref)
table.insert(entries, {
name_ref = name_ref,
creator = creator_expr,
archive_ref = archive_ref,
})
if class_name and creator_kind then
creator_kinds[class_name] = creator_kinds[class_name] or {}
creator_kinds[class_name][creator_kind] = true
end
end
return entries, creator_kinds
end
local function parse_asm_creator_sizes(text)
local sizes = {}
local current_class = nil
for line in text:gmatch("([^\n\r]+)") do
local fn_sym = line:match('^%s*%.fn%s+"([^"]+)",')
if fn_sym then
local class_name = fn_sym:match("^create[%a_]+<%d*([A-Za-z_][A-Za-z0-9_]*)>__")
current_class = class_name
elseif line:match("^%s*%.endfn") then
current_class = nil
elseif current_class and not sizes[current_class] then
local size_hex = line:match("li%s+r3,%s*0x([0-9A-Fa-f]+)")
if size_hex then
sizes[current_class] = tonumber(size_hex, 16)
end
end
end
return sizes
end
local function parse_number(token)
token = trim(token)
if token:match("^0x[0-9A-Fa-f]+$") then
return tonumber(token, 16)
end
if token:match("^[0-9]+$") then
return tonumber(token, 10)
end
return nil
end
local function u32_bytes_be(value)
local b1 = math.floor(value / 0x1000000) % 0x100
local b2 = math.floor(value / 0x10000) % 0x100
local b3 = math.floor(value / 0x100) % 0x100
local b4 = value % 0x100
return b1, b2, b3, b4
end
local function u16_bytes_be(value)
local b1 = math.floor(value / 0x100) % 0x100
local b2 = value % 0x100
return b1, b2
end
local function parse_asm_data_memory(text)
local label_addr = {}
local memory = {}
local pending_addr = nil
local pending_size = nil
local current_label = nil
local current_addr = nil
local current_size = nil
local current_bytes = nil
local function flush_object()
if not current_label or not current_addr then
current_label = nil
current_addr = nil
current_size = nil
current_bytes = nil
return
end
local bytes = current_bytes or {}
if current_size and current_size > 0 then
if #bytes > current_size then
while #bytes > current_size do
table.remove(bytes)
end
elseif #bytes < current_size then
for _ = #bytes + 1, current_size do
table.insert(bytes, 0)
end
end
end
label_addr[current_label] = current_addr
for i = 1, #bytes do
memory[current_addr + (i - 1)] = bytes[i]
end
current_label = nil
current_addr = nil
current_size = nil
current_bytes = nil
end
for line in text:gmatch("([^\n\r]+)") do
local addr_hex, size_hex = line:match("^#%s+%.data:[^|]*|%s*0x([0-9A-Fa-f]+)%s*|%s*size:%s*0x([0-9A-Fa-f]+)")
if addr_hex and size_hex then
pending_addr = tonumber(addr_hex, 16)
pending_size = tonumber(size_hex, 16)
end
local label = line:match('^%s*%.obj%s+([A-Za-z_][A-Za-z0-9_]*),')
if not label then
label = line:match('^%s*%.obj%s+"([^"]+)",')
end
if label then
flush_object()
current_label = label
current_addr = pending_addr
current_size = pending_size
current_bytes = {}
pending_addr = nil
pending_size = nil
end
if current_bytes then
local string_payload = line:match('^%s*%.string%s+"(.*)"')
if string_payload then
local text_value = unescape_asm_string(string_payload)
for i = 1, #text_value do
table.insert(current_bytes, text_value:byte(i))
end
table.insert(current_bytes, 0)
end
local ascii_payload = line:match('^%s*%.ascii%s+"(.*)"')
if ascii_payload then
local text_value = unescape_asm_string(ascii_payload)
for i = 1, #text_value do
table.insert(current_bytes, text_value:byte(i))
end
end
local byte_payload = line:match('^%s*%.byte%s+(.+)$')
if byte_payload then
for token in byte_payload:gmatch("[^,]+") do
local value = parse_number(token)
if value then
table.insert(current_bytes, value % 0x100)
end
end
end
local half_payload = line:match('^%s*%.2byte%s+(.+)$')
if half_payload then
for token in half_payload:gmatch("[^,]+") do
local value = parse_number(token)
if value then
local b1, b2 = u16_bytes_be(value)
table.insert(current_bytes, b1)
table.insert(current_bytes, b2)
else
table.insert(current_bytes, 0)
table.insert(current_bytes, 0)
end
end
end
local word_payload = line:match('^%s*%.4byte%s+(.+)$')
if word_payload then
for token in word_payload:gmatch("[^,]+") do
local value = parse_number(token)
if value then
local b1, b2, b3, b4 = u32_bytes_be(value)
table.insert(current_bytes, b1)
table.insert(current_bytes, b2)
table.insert(current_bytes, b3)
table.insert(current_bytes, b4)
else
table.insert(current_bytes, 0)
table.insert(current_bytes, 0)
table.insert(current_bytes, 0)
table.insert(current_bytes, 0)
end
end
end
end
if line:match("^%s*%.endobj") then
flush_object()
end
end
flush_object()
return label_addr, memory
end
local function resolve_data_label_cstring(label, label_addr, memory)
local addr = label_addr[label]
if not addr then
fail("missing .data address for label: " .. tostring(label))
end
local chars = {}
local max_len = 0x800
for i = 0, max_len do
local b = memory[addr + i]
if b == nil then
fail("failed to resolve string bytes for label: " .. tostring(label))
end
if b == 0 then
return table.concat(chars)
end
chars[#chars + 1] = string.char(b)
end
fail("unterminated string while resolving label: " .. tostring(label))
end
local function find_initializer_range(text, declaration_line)
local decl_start = text:find(declaration_line, 1, true)
if not decl_start then
return nil
end
local brace_start = text:find("{", decl_start + #declaration_line - 1, true)
if not brace_start then
return nil
end
local depth = 0
local in_string = false
local escaped = false
local close_brace = nil
for pos = brace_start, #text do
local ch = text:sub(pos, pos)
if in_string then
if escaped then
escaped = false
elseif ch == "\\" then
escaped = true
elseif ch == '"' then
in_string = false
end
else
if ch == '"' then
in_string = true
elseif ch == "{" then
depth = depth + 1
elseif ch == "}" then
depth = depth - 1
if depth == 0 then
close_brace = pos
break
end
end
end
end
if not close_brace then
return nil
end
local semicolon = text:find(";", close_brace, true)
if not semicolon then
return nil
end
return decl_start, semicolon
end
local function build_create_table_block(entries)
local lines = {
"const NameObjFactory::Name2CreateFunc cCreateTable[] = {",
}
for _, entry in ipairs(entries) do
table.insert(lines, " {")
table.insert(lines, ' "' .. escape_cpp_string(entry.name) .. '",')
table.insert(lines, " " .. entry.creator .. ",")
if entry.archive then
table.insert(lines, ' "' .. escape_cpp_string(entry.archive) .. '",')
else
table.insert(lines, " nullptr,")
end
table.insert(lines, " },")
end
table.insert(lines, "};")
return table.concat(lines, "\n")
end
local function is_header_or_source(path)
return path:match("%.h$") ~= nil
or path:match("%.hpp$") ~= nil
or path:match("%.c$") ~= nil
or path:match("%.cpp$") ~= nil
or path:match("%.inc$") ~= nil
end
local function walk_dir(path, fn)
for entry in lfs.dir(path) do
if entry ~= "." and entry ~= ".." then
local full = path .. "/" .. entry
local attr = lfs.attributes(full)
if attr then
if attr.mode == "directory" then
walk_dir(full, fn)
elseif attr.mode == "file" then
fn(full)
end
end
end
end
end
local function collect_class_names_from_text(text, out)
local function is_ident_char(ch)
return ch:match("[A-Za-z0-9_]")
end
local function scan(keyword)
local pos = 1
while true do
local s, e, name = text:find(keyword .. "%s+([A-Za-z_][A-Za-z0-9_]*)", pos)
if not s then
break
end
local i = e + 1
while i <= #text and text:sub(i, i):match("%s") do
i = i + 1
end
local maybe_final = text:sub(i, i + 4)
local next_char = text:sub(i + 5, i + 5)
if maybe_final == "final" and (next_char == "" or not is_ident_char(next_char)) then
i = i + 5
while i <= #text and text:sub(i, i):match("%s") do
i = i + 1
end
end
local ch = text:sub(i, i)
if ch == ":" or ch == "{" then
out[name] = true
end
pos = e + 1
end
end
scan("class")
scan("struct")
end
local function path_is_under(path, parent)
if not parent or parent == "" then
return false
end
if path == parent then
return true
end
local prefix = parent
if prefix:sub(-1) ~= "/" then
prefix = prefix .. "/"
end
return path:sub(1, #prefix) == prefix
end
local function strip_include_prefix(path)
return path:gsub("^include/", "")
end
local function load_class_definition_map(skip_paths, skip_prefixes)
local class_to_path = {}
local roots = { "include", "src" }
local skip_set = {}
for _, path in ipairs(skip_paths or {}) do
skip_set[path] = true
end
for _, root in ipairs(roots) do
local attr = lfs.attributes(root)
if attr and attr.mode == "directory" then
walk_dir(root, function(path)
if skip_set[path] then
return
end
for _, prefix in ipairs(skip_prefixes or {}) do
if path_is_under(path, prefix) then
return
end
end
if is_header_or_source(path) then
local text = read_file(path)
if text then
local local_defs = {}
collect_class_names_from_text(text, local_defs)
for class_name in pairs(local_defs) do
if not class_to_path[class_name] then
class_to_path[class_name] = path
end
end
end
end
end)
end
end
return class_to_path
end
local function load_function_name_set(skip_paths)
local names = {}
local skip = {}
for _, path in ipairs(skip_paths or {}) do
skip[path] = true
end
local roots = { "include", "src" }
for _, root in ipairs(roots) do
local attr = lfs.attributes(root)
if attr and attr.mode == "directory" then
walk_dir(root, function(path)
if skip[path] then
return
end
if is_header_or_source(path) then
local text = read_file(path)
if text then
for name in text:gmatch("([A-Za-z_][A-Za-z0-9_]*)%s*%(") do
names[name] = true
end
end
end
end)
end
end
return names
end
local function build_class_ctor_flags(creator_kinds, class_name)
local kinds = creator_kinds[class_name] or {}
local needs_name_ctor = kinds.createNameObj == true
local needs_shape_ctor = kinds.createBaseOriginCube == true
or kinds.createCenterOriginCube == true
or kinds.createSphere == true
or kinds.createBaseOriginCylinder == true
or kinds.createBowl == true
if not needs_name_ctor and not needs_shape_ctor then
needs_name_ctor = true
end
return needs_name_ctor, needs_shape_ctor
end
local function float_literal(value)
return string.format("%.1ff", value)
end
local function build_effect_stub_header(class_name, spec)
local lines = {
"#pragma once",
"",
'#include "Game/Effect/SimpleEffectObj.hpp"',
"",
"class " .. class_name .. " : public SimpleEffectObj {",
"public:",
" inline " .. class_name .. "(const char* pName) : SimpleEffectObj(pName) {}",
"",
" virtual ~" .. class_name .. "() {}",
" virtual f32 getClippingRadius() const { return " .. float_literal(spec.radius) .. "; };",
" virtual f32 getFarClipDistance() const { return " .. float_literal(spec.far) .. "; };",
}
if spec.center_y then
table.insert(lines, "")
table.insert(lines, " virtual TVec3f* getClippingCenterOffset() const {")
table.insert(lines, " TVec3f vec(0.0f, " .. float_literal(spec.center_y) .. ", 0.0f);")
table.insert(lines, " return &vec;")
table.insert(lines, " };")
end
if spec.sync then
table.insert(lines, " virtual bool isSyncClipping() const { return true; };")
end
table.insert(lines, "};")
table.insert(lines, "")
return table.concat(lines, "\n")
end
local function build_morph_item_base_stub_header()
local lines = {
"#pragma once",
"",
'#include "Game/NameObj/NameObj.hpp"',
'#include "Game/LiveActor/Nerve.hpp"',
"",
"class MorphItemObjNeo : public NameObj {",
"public:",
" MorphItemObjNeo(const char*, long);",
"",
" virtual ~MorphItemObjNeo();",
"",
" void exeWait();",
" void exeAppear();",
" void exeSwitchAppear();",
" void exeWait2();",
" void exeFly();",
" void exeDemo();",
"",
" /* 0x0C */ u8 _0C[0xFC - 0xC];",
"};",
"",
"namespace NrvMorphItemObjNeo {",
" NERVE_DECL_EXE(MorphItemObjNeoNrvWait, MorphItemObjNeo, Wait);",
" NERVE_DECL_EXE(MorphItemObjNeoNrvAppear, MorphItemObjNeo, Appear);",
" NERVE_DECL_EXE(MorphItemObjNeoNrvSwitchAppear, MorphItemObjNeo, SwitchAppear);",
" NERVE_DECL_EXE(MorphItemObjNeoNrvWait2, MorphItemObjNeo, Wait2);",
" NERVE_DECL_EXE(MorphItemObjNeoNrvFly, MorphItemObjNeo, Fly);",
" NERVE_DECL_EXE(MorphItemObjNeoNrvDemo, MorphItemObjNeo, Demo);",
"};",
"",
}
return table.concat(lines, "\n")
end
local function build_morph_item_stub_header(class_name, kind)
local lines = {
"#pragma once",
"",
'#include "Game/Player/MorphItemObjNeo.hpp"',
"",
"class " .. class_name .. " : public MorphItemObjNeo {",
"public:",
" inline " .. class_name .. "(const char* pName) : MorphItemObjNeo(pName, " .. tostring(kind) .. ") {}",
"",
" virtual ~" .. class_name .. "() {}",
"};",
"",
}
return table.concat(lines, "\n")
end
local function resolve_generated_header_path(class_name, cfg_data)
return special_header_paths[class_name] or (cfg_data.stub_dir .. "/" .. class_name .. ".hpp")
end
local function build_class_stub_header(class_name, creator_kinds, size_map)
if class_name == "MorphItemObjNeo" then
return build_morph_item_base_stub_header()
end
local morph_kind = morph_item_kind_by_class[class_name]
if morph_kind then
return build_morph_item_stub_header(class_name, morph_kind)
end
local effect_spec = effect_stub_specs[class_name]
if effect_spec then
return build_effect_stub_header(class_name, effect_spec)
end
local lines = {
"#pragma once",
"",
'#include "Game/NameObj/NameObj.hpp"',
"",
"class " .. class_name .. " : public NameObj {",
"public:",
}
local needs_name_ctor, needs_shape_ctor = build_class_ctor_flags(creator_kinds, class_name)
if needs_name_ctor then
table.insert(lines, " " .. class_name .. "(const char*);")
end
if needs_shape_ctor then
table.insert(lines, " " .. class_name .. "(int, const char*);")
end
local size = size_map[class_name]
local base_size = 0xC
if size then
if size < base_size then
fail(string.format("invalid parsed size for %s: 0x%X", class_name, size))
end
if size > base_size then
table.insert(lines, string.format(" /* 0x0C */ u8 _0C[0x%X - 0xC];", size))
end
end
table.insert(lines, "};")
table.insert(lines, "")
return table.concat(lines, "\n")
end
local function build_stub_header(include_paths, generated_header_paths, global_funcs, mr_funcs)
local lines = {
"#pragma once",
"",
'#include "Game/NameObj/NameObj.hpp"',
"",
}
if include_paths and #include_paths > 0 then
for _, include_path in ipairs(include_paths) do
table.insert(lines, '#include "' .. strip_include_prefix(include_path) .. '"')
end
table.insert(lines, "")
end
if generated_header_paths and #generated_header_paths > 0 then
for _, include_path in ipairs(generated_header_paths) do
table.insert(lines, '#include "' .. strip_include_prefix(include_path) .. '"')
end
table.insert(lines, "")
end
if mr_funcs and #mr_funcs > 0 then
table.insert(lines, "namespace MR {")
for _, fn in ipairs(mr_funcs) do
table.insert(lines, " NameObj* " .. fn .. "(const char*);")
end
table.insert(lines, "}")
table.insert(lines, "")
end
if global_funcs and #global_funcs > 0 then
for _, fn in ipairs(global_funcs) do
table.insert(lines, "NameObj* " .. fn .. "(const char*);")
end
table.insert(lines, "")
end
return table.concat(lines, "\n")
end
local function ensure_directory(path)
local ok, status = mkdir_p(path)
if not ok then
fail("failed to create directory: " .. path .. " (" .. tostring(status) .. ")")
end
end
local function dirname(path)
local dir = path:match("^(.*)/[^/]+$")
if not dir or dir == "" then
return "."
end
return dir
end
local asm_entries = nil
local asm_creator_kinds = nil
local asm_creator_sizes = nil
local asm_label_addr = nil
local asm_data_memory = nil
if asm_text then
asm_entries, asm_creator_kinds = parse_asm_create_table(asm_text)
asm_creator_sizes = parse_asm_creator_sizes(asm_text)
asm_label_addr, asm_data_memory = parse_asm_data_memory(asm_text)
end
if cfg.sync_create_table and not asm_entries then
fail("sync-create-table requested but asm parse data is unavailable")
end
local unresolved_classes = {}
if cfg.generate_stubs then
if asm_creator_kinds then
local set = {}
for class_name in pairs(asm_creator_kinds) do
set[class_name] = true
end
for class_name in pairs(set) do
table.insert(unresolved_classes, class_name)
end
table.sort(unresolved_classes)
else
local function is_null(value)
return value == nil or value == json_null
end
local funcs = unit and unit.functions
if type(funcs) == "table" then
local set = {}
for _, func in ipairs(funcs) do
local symbol = func and func.name
if type(symbol) == "string" and is_null(func.fuzzy_match_percent) then
local class_name = symbol:match("create[%a_]+<%d*([A-Za-z_][A-Za-z0-9_]*)>__")
if class_name then
set[class_name] = true
end
end
end
for class_name in pairs(set) do
table.insert(unresolved_classes, class_name)
end
table.sort(unresolved_classes)
end
end
printf("Template creator classes considered: %d\n", #unresolved_classes)
end
if cfg.generate_stubs and #unresolved_classes == 0 then
printf("No class scaffolding candidates found.\n")
if not cfg.sync_create_table then
os.exit(0)
end
end
local missing = {}
local include_paths = {}
local missing_mr_creators = {}
local missing_global_creators = {}
local generated_stub_header_paths = {}
local generated_stub_header_texts = {}
local unresolved_set = {}
for _, class_name in ipairs(unresolved_classes) do
unresolved_set[class_name] = true
end
if cfg.generate_stubs and #unresolved_classes > 0 then
local class_map = load_class_definition_map(
{ cfg.stub_header, legacy_stub_header },
{ cfg.stub_dir, legacy_stub_dir }
)
local include_set = {}
for _, class_name in ipairs(unresolved_classes) do
local path = class_map[class_name]
if force_stub_classes[class_name] then
table.insert(missing, class_name)
elseif path and path:match("^include/") then
include_set[path] = true
else
table.insert(missing, class_name)
end
end
local missing_set = {}
for _, class_name in ipairs(missing) do
missing_set[class_name] = true
end
local needs_morph_base = false
for class_name in pairs(missing_set) do
if morph_item_kind_by_class[class_name] then
needs_morph_base = true
break
end
end
if needs_morph_base and not missing_set.MorphItemObjNeo then
local morph_base_path = class_map.MorphItemObjNeo
if morph_base_path and morph_base_path:match("^include/") then
include_set[morph_base_path] = true
else
table.insert(missing, "MorphItemObjNeo")
missing_set.MorphItemObjNeo = true
end
end
for path in pairs(include_set) do
table.insert(include_paths, path)
end
table.sort(include_paths)
table.sort(missing)
local generated_path_set = {}
for _, class_name in ipairs(missing) do
local header_path = resolve_generated_header_path(class_name, cfg)
if not generated_path_set[header_path] then
table.insert(generated_stub_header_paths, header_path)
generated_path_set[header_path] = true
end
generated_stub_header_texts[header_path] = build_class_stub_header(
class_name,
asm_creator_kinds or {},
asm_creator_sizes or {}
)
end
table.sort(generated_stub_header_paths)
if asm_entries then
local fn_set = load_function_name_set({ cfg.source, cfg.stub_header, legacy_stub_header })
local mr_set = {}
local global_set = {}
for _, entry in ipairs(asm_entries) do
local mr_fn = entry.creator:match("^MR::([A-Za-z_][A-Za-z0-9_]*)$")
if mr_fn and not fn_set[mr_fn] then
mr_set[mr_fn] = true
end
local plain_fn = entry.creator:match("^([A-Za-z_][A-Za-z0-9_]*)$")
if plain_fn and plain_fn ~= "nullptr" and not fn_set[plain_fn] then
global_set[plain_fn] = true
end
end
for fn_name in pairs(mr_set) do
table.insert(missing_mr_creators, fn_name)
end
for fn_name in pairs(global_set) do
table.insert(missing_global_creators, fn_name)
end
table.sort(missing_mr_creators)
table.sort(missing_global_creators)
end
printf("Missing class declarations: %d\n", #missing)
printf("Extra headers to include in stub header: %d\n", #include_paths)
printf("Generated per-class stub headers: %d\n", #generated_stub_header_paths)
printf("Missing global creator declarations: %d\n", #missing_global_creators)
printf("Missing MR creator declarations: %d\n", #missing_mr_creators)
if cfg.verbose then
for class_name in pairs(force_stub_classes) do
printf(
"ForceStub[%s]: unresolved=%s missing=%s\n",
class_name,
unresolved_set[class_name] and "yes" or "no",
missing_set[class_name] and "yes" or "no"
)
end
end
end
local source_text = read_file(cfg.source)
if not source_text then
fail("failed to read source file: " .. cfg.source)
end
local patched_source = source_text
local patched_table = false
if cfg.sync_create_table then
local decl = "const NameObjFactory::Name2CreateFunc cCreateTable[] = {"
local s, e = find_initializer_range(patched_source, decl)
if not s then
fail("failed to locate cCreateTable initializer in source: " .. cfg.source)
end
local resolved_entries = {}
for i = 1, #asm_entries do
local entry = asm_entries[i]
local name_ref = entry.name_ref
local archive_ref = entry.archive_ref
if not name_ref or name_ref == "0x00000000" then
fail("invalid cCreateTable name reference at entry #" .. tostring(i))
end
local name = resolve_data_label_cstring(name_ref, asm_label_addr, asm_data_memory)
local archive = nil
if archive_ref and archive_ref ~= "0x00000000" then
archive = resolve_data_label_cstring(archive_ref, asm_label_addr, asm_data_memory)
end
resolved_entries[i] = {
name = name,
creator = entry.creator,
archive = archive,
}
end
local replacement = build_create_table_block(resolved_entries)
patched_source = patched_source:sub(1, s - 1) .. replacement .. patched_source:sub(e + 1)
patched_table = true
end
local stub_header_text = nil
if cfg.generate_stubs then
stub_header_text = build_stub_header(
include_paths,
generated_stub_header_paths,
missing_global_creators,
missing_mr_creators
)
end
if not cfg.write then
if cfg.sync_create_table then
printf("Dry-run only. cCreateTable sync is ready.\n")
end
if stub_header_text then
printf("Dry-run only. Generated stub header preview:\n")
local preview_lines = split_lines(stub_header_text)
local max_preview = 30
for i = 1, math.min(#preview_lines, max_preview) do
printf("%s\n", preview_lines[i])
end
if #preview_lines > max_preview then
printf("... (truncated preview)\n")
end
end
if #generated_stub_header_paths > 0 then
printf("Dry-run only. Generated per-class stub headers:\n")
local max_preview = 20
for i = 1, math.min(#generated_stub_header_paths, max_preview) do
printf("%s\n", generated_stub_header_paths[i])
end
if #generated_stub_header_paths > max_preview then
printf("... (truncated list)\n")
end
end
printf("Re-run with --write to apply changes.\n")
os.exit(0)
end
if stub_header_text then
local stub_header_dir = cfg.stub_header:match("^(.*)/[^/]+$") or "."
ensure_directory(stub_header_dir)
local wrote_generated = 0
local kept_existing_generated = 0
for _, header_path in ipairs(generated_stub_header_paths) do
ensure_directory(dirname(header_path))
local text = generated_stub_header_texts[header_path]
if not text then
fail("missing generated stub text for: " .. header_path)
end
if file_exists(header_path) then
kept_existing_generated = kept_existing_generated + 1
else
if not write_file(header_path, text) then
fail("failed to write generated stub header: " .. header_path)
end
wrote_generated = wrote_generated + 1
end
end
if not write_file(cfg.stub_header, stub_header_text) then
fail("failed to write stub header: " .. cfg.stub_header)
end
local include_line = '#include "' .. strip_include_prefix(cfg.stub_header) .. '"'
patched_source = patched_source:gsub('#include "Game/NameObj/AutoNameObjFactoryStubs%.hpp"%s*\n', "")
if not patched_source:find(include_line, 1, true) then
local own_header = '#include "Game/NameObj/NameObjFactory.hpp"\n'
local replacement = own_header .. include_line .. "\n"
local patched, n = patched_source:gsub(own_header, replacement, 1)
if n == 0 then
patched, n = patched_source:gsub("((#include .-\n)+)", "%1" .. include_line .. "\n", 1)
if n == 0 then
fail("failed to inject stub include into: " .. cfg.source)
end
end
patched_source = patched
end
printf("Wrote new generated per-class headers: %s\n", tostring(wrote_generated))
printf("Kept existing per-class headers without override: %s\n", tostring(kept_existing_generated))
end
if patched_source ~= source_text then
if not write_file(cfg.source, patched_source) then
fail("failed to write patched source: " .. cfg.source)
end
end
if patched_table then
printf("Synced cCreateTable from asm.\n")
end
if stub_header_text then
printf("Wrote stub header: %s\n", cfg.stub_header)
printf("Wrote per-class stub headers: %s\n", tostring(#generated_stub_header_paths))
printf("Ensured stub include in: %s\n", cfg.source)
end
printf("Changes applied. Rebuild and re-run to measure fuzzy delta.\n")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment