scummvm/devtools/director-generate-xobj-stub.py
Scott Percival 89ba335860 DIRECTOR: XOBJ: Make object name match library name
It is possible for games to call factory("libName") to check if an
XObject has been opened, which requires the names to match.

Fixes the XObject loader in Virtual Nightclub.
2024-04-25 00:54:17 +02:00

987 lines
31 KiB
Python
Executable File

#!/usr/bin/env python
from __future__ import annotations
import argparse
import os
import re
import struct
from typing import Any, BinaryIO, Literal
from typing_extensions import TypedDict
XCodeType = Literal["XFCN", "XCMD", "XObject", "Xtra"]
class XCode(TypedDict):
type: XCodeType
name: str
slug: str
filename: str
method_table: list[str]
class PESection(TypedDict):
name: str
virt_size: int
virt_addr: int
raw_size: int
raw_ptr: int
DIRECTOR_SRC_PATH = os.path.abspath(
os.path.join(__file__, "..", "..", "engines", "director")
)
MAKEFILE_PATH = os.path.join(DIRECTOR_SRC_PATH, "module.mk")
LINGO_XLIBS_PATH = os.path.join(DIRECTOR_SRC_PATH, "lingo", "xlibs")
LINGO_XTRAS_PATH = os.path.join(DIRECTOR_SRC_PATH, "lingo", "xtras")
LINGO_OBJECT_PATH = os.path.join(DIRECTOR_SRC_PATH, "lingo", "lingo-object.cpp")
LEGAL = """/* ScummVM - Graphic Adventure Engine
*
* ScummVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
"""
TEMPLATE_H = (
LEGAL
+ """
#ifndef DIRECTOR_LINGO_{base_upper}_{slug_upper}_H
#define DIRECTOR_LINGO_{base_upper}_{slug_upper}_H
namespace Director {{
class {xobject_class} : public Object<{xobject_class}> {{
public:
{xobject_class}(ObjectType objType);
}};
namespace {xobj_class} {{
extern const char *xlibName;
extern const char *fileNames[];
void open(ObjectType type, const Common::Path &path);
void close(ObjectType type);
{methlist}
}} // End of namespace {xobj_class}
}} // End of namespace Director
#endif
"""
)
TEMPLATE_HEADER_METH = """void m_{methname}(int nargs);"""
TEMPLATE = (
LEGAL
+ """
#include "common/system.h"
#include "director/director.h"
#include "director/lingo/lingo.h"
#include "director/lingo/lingo-object.h"
#include "director/lingo/lingo-utils.h"
#include "director/lingo/{base}/{slug}.h"
/**************************************************
*
* USED IN:
* [insert game here]
*
**************************************************/
/*
{xmethtable}
*/
namespace Director {{
const char *{xobj_class}::xlibName = "{name}";
const char *{xobj_class}::fileNames[] = {{
"{filename}",
nullptr
}};
static MethodProto xlibMethods[] = {{
{xlib_methods}
{{ nullptr, nullptr, 0, 0, 0 }}
}};
static BuiltinProto xlibBuiltins[] = {{
{xlib_builtins}
{{ nullptr, nullptr, 0, 0, 0, VOIDSYM }}
}};
static BuiltinProto xlibTopLevel[] = {{
{xlib_toplevels}
{{ nullptr, nullptr, 0, 0, 0, VOIDSYM }}
}};
{xobject_class}::{xobject_class}(ObjectType ObjectType) :Object<{xobject_class}>("{name}") {{
_objType = ObjectType;
}}
void {xobj_class}::open(ObjectType type, const Common::Path &path) {{
{xobject_class}::initMethods(xlibMethods);
{xobject_class} *xobj = new {xobject_class}(type);
g_lingo->exposeXObject(xlibName, xobj);
g_lingo->initBuiltIns(xlibBuiltins);
}}
void {xobj_class}::close(ObjectType type) {{
{xobject_class}::cleanupMethods();
g_lingo->_globalvars[xlibName] = Datum();
}}
{xobj_new}
{xobj_stubs}
}}
"""
)
XLIB_METHOD_TEMPLATE = """ {{ "{methname}", {xobj_class}::m_{methname}, {min_args}, {max_args}, {director_version} }},"""
XLIB_NEW_TEMPLATE = """void {xobj_class}::m_new(int nargs) {{
g_lingo->printSTUBWithArglist("{xobj_class}::m_new", nargs);
g_lingo->dropStack(nargs);
g_lingo->push(g_lingo->_state->me);
}}"""
XCMD_TEMPLATE_H = (
LEGAL
+ """
#ifndef DIRECTOR_LINGO_XLIBS_{slug_upper}_H
#define DIRECTOR_LINGO_XLIBS_{slug_upper}_H
namespace Director {{
namespace {xobj_class} {{
extern const char *xlibName;
extern const char *fileNames[];
void open(ObjectType type, const Common::Path &path);
void close(ObjectType type);
{methlist}
}} // End of namespace {xobj_class}
}} // End of namespace Director
#endif
"""
)
XCMD_TEMPLATE = (
LEGAL
+ """
#include "common/system.h"
#include "director/director.h"
#include "director/lingo/lingo.h"
#include "director/lingo/lingo-object.h"
#include "director/lingo/lingo-utils.h"
#include "director/lingo/xlibs/{slug}.h"
/**************************************************
*
* USED IN:
* [insert game here]
*
**************************************************/
namespace Director {{
const char *{xobj_class}::xlibName = "{name}";
const char *{xobj_class}::fileNames[] = {{
"{filename}",
nullptr
}};
static BuiltinProto builtins[] = {{
{xlib_builtins}
{{ nullptr, nullptr, 0, 0, 0, VOIDSYM }}
}};
void {xobj_class}::open(ObjectType type, const Common::Path &path) {{
g_lingo->initBuiltIns(builtins);
}}
void {xobj_class}::close(ObjectType type) {{
g_lingo->cleanupBuiltIns(builtins);
}}
{xobj_stubs}
}}
"""
)
BUILTIN_TEMPLATE = """ {{ "{name}", {xobj_class}::m_{name}, {min_args}, {max_args}, {director_version}, {methtype} }},"""
XOBJ_STUB_TEMPLATE = """XOBJSTUB({xobj_class}::m_{methname}, {default})"""
XOBJ_NR_STUB_TEMPLATE = """XOBJSTUBNR({xobj_class}::m_{methname})"""
def read_uint8(data: bytes) -> int:
return struct.unpack("B", data)[0]
def read_uint16_le(data: bytes) -> int:
return struct.unpack("<H", data)[0]
def read_uint16_be(data: bytes) -> int:
return struct.unpack(">H", data)[0]
def read_uint32_le(data: bytes) -> int:
return struct.unpack("<L", data)[0]
def read_uint32_be(data: bytes) -> int:
return struct.unpack(">L", data)[0]
def inject_makefile(slug: str, xcode_type: XCodeType) -> None:
make_contents = open(MAKEFILE_PATH, "r").readlines()
storage_path = "lingo/xtras" if xcode_type == "Xtra" else "lingo/xlibs"
expr = re.compile(f"^\t{storage_path}/([a-zA-Z0-9\\-]+).o( \\\\|)")
for i in range(len(make_contents)):
m = expr.match(make_contents[i])
if m:
if slug == m.group(1):
# file already in makefile
print(f"{storage_path}/{slug}.o already in {MAKEFILE_PATH}, skipping")
return
elif slug < m.group(1):
make_contents.insert(i, f"\t{storage_path}/{slug}.o \\\n")
with open(MAKEFILE_PATH, "w") as f:
f.writelines(make_contents)
return
elif m.group(2) == "":
# final entry in the list
make_contents[i] += " \\"
make_contents.insert(i + 1, f"\t{storage_path}/{slug}.o\n")
with open(MAKEFILE_PATH, "w") as f:
f.writelines(make_contents)
return
def inject_lingo_object(slug: str, xobj_class: str, director_version: int, xcode_type: XCodeType) -> None:
# write include statement for the object header
lo_contents = open(LINGO_OBJECT_PATH, "r").readlines()
storage_path = "director/lingo/xtras" if xcode_type == "Xtra" else "director/lingo/xlibs"
obj_type = "kXtraObj" if xcode_type == "Xtra" else "kXObj"
expr = re.compile(f'^#include "{storage_path}/([a-zA-Z0-9\\-]+)\\.h"')
in_xlibs = False
for i in range(len(lo_contents)):
m = expr.match(lo_contents[i])
if m:
in_xlibs = True
if slug == m.group(1):
print(
f"{storage_path}/{slug}.h import already in {LINGO_OBJECT_PATH}, skipping"
)
break
elif slug < m.group(1):
lo_contents.insert(i, f'#include "{storage_path}/{slug}.h"\n')
with open(LINGO_OBJECT_PATH, "w") as f:
f.writelines(lo_contents)
break
elif in_xlibs:
# final entry in the list
lo_contents.insert(i, f'#include "{storage_path}/{slug}.h"\n')
with open(LINGO_OBJECT_PATH, "w") as f:
f.writelines(lo_contents)
break
# write entry in the XLibProto table
lo_contents = open(LINGO_OBJECT_PATH, "r").readlines()
expr = re.compile("^\t\\{ ([a-zA-Z0-9_]+)::fileNames")
in_xlibs = False
for i in range(len(lo_contents)):
m = expr.match(lo_contents[i])
if m:
in_xlibs = True
if xobj_class == m.group(1):
print(
f"{xobj_class} proto import already in {LINGO_OBJECT_PATH}, skipping"
)
break
elif xobj_class < m.group(1):
lo_contents.insert(
i,
f" {{ {xobj_class}::fileNames, {xobj_class}::open, {xobj_class}::close, {obj_type}, {director_version} }}, // D{director_version // 100}\n",
)
with open(LINGO_OBJECT_PATH, "w") as f:
f.writelines(lo_contents)
break
elif in_xlibs:
# final entry in the list
lo_contents.insert(
i,
f" {{ {xobj_class}::fileNames, {xobj_class}::open, {xobj_class}::close, {obj_type}, {director_version} }}, // D{director_version // 100}\n",
)
with open(LINGO_OBJECT_PATH, "w") as f:
f.writelines(lo_contents)
break
def extract_xcode_macbinary(
file: BinaryIO, resource_offset: int, xobj_id: str | None = None
) -> XCode:
file.seek(resource_offset)
resource_data_offset = read_uint32_be(file.read(4))
resource_map_offset = read_uint32_be(file.read(4))
resource_data_size = read_uint32_be(file.read(4))
resource_map_size = read_uint32_be(file.read(4))
file.seek(resource_offset + resource_map_offset + 24)
type_list_offset = read_uint16_be(file.read(2))
name_list_offset = read_uint16_be(file.read(2))
file.seek(resource_offset + resource_map_offset + type_list_offset)
type_count = read_uint16_be(file.read(2))
types = {}
for _ in range(type_count + 1):
key = file.read(4)
types[key] = (read_uint16_be(file.read(2)) + 1, read_uint16_be(file.read(2)))
xobj: dict[str, dict[str, Any]] = {}
for chunk_type in [b"XCOD", b"XFCN", b"XCMD"]:
if chunk_type in types:
print(f"Found {chunk_type.decode('utf8')} resources!")
file.seek(
resource_offset
+ resource_map_offset
+ type_list_offset
+ types[chunk_type][1]
)
resources: list[tuple[str, int, int]] = []
for _ in range(types[chunk_type][0]):
id = f"{chunk_type.decode('utf8')}_{read_uint16_be(file.read(2))}"
name_offset = read_uint16_be(file.read(2))
file.read(1)
data_offset = (read_uint8(file.read(1)) << 16) + read_uint16_be(
file.read(2)
)
file.read(4)
resources.append((id, data_offset, name_offset))
for id, data_offset, name_offset in resources:
xobj[id] = {}
if name_offset != 0xFFFF:
file.seek(
resource_offset
+ resource_map_offset
+ name_list_offset
+ name_offset
)
name_size = read_uint8(file.read(1))
xobj[id]["name"] = file.read(name_size).decode("macroman")
else:
xobj[id]["name"] = "<unknown>"
file.seek(resource_offset + resource_data_offset + data_offset)
xobj[id]["dump"] = file.read(read_uint32_be(file.read(4)) - 4)
file.seek(resource_offset + resource_data_offset + data_offset)
size = read_uint32_be(file.read(4)) - 12
file.read(12)
xobj[id]["xmethtable"] = []
while size > 0:
count = read_uint8(file.read(1))
if count == 0:
break
xobj[id]["xmethtable"].append(file.read(count).decode("macroman"))
size -= 1 + count
if not xobj:
raise ValueError("No extension resources found!")
if xobj_id is None or xobj_id not in xobj:
print("Please re-run with one of the following resource IDs:")
for id, data in xobj.items():
print(f"{id} - {data['name']}")
raise ValueError("Need to specify resource ID")
type: XCodeType = (
"XFCN"
if xobj_id.startswith("XFCN_")
else "XCMD"
if xobj_id.startswith("XCMD_")
else "XObject"
)
if type == "XObject":
for entry in xobj[xobj_id]["xmethtable"]:
print(entry)
slug = xobj[xobj_id]["name"].lower()
if type in ["XFCN", "XCMD"]:
slug += type.lower()
return {
"type": type,
"name": xobj[xobj_id]["name"],
"slug": slug,
"filename": xobj[xobj_id]["name"],
"method_table": xobj[xobj_id]["xmethtable"],
}
def extract_xcode_win16(file: BinaryIO, ne_offset: int) -> XCode:
# get resource table
file.seek(ne_offset + 0x24, os.SEEK_SET)
restable_offset = read_uint16_le(file.read(0x2))
resident_names_offset = read_uint16_le(file.read(0x2))
file.seek(ne_offset + restable_offset)
shift_count = read_uint16_le(file.read(0x2))
# read each resource
resources: list[dict[str, Any]] = []
while file.tell() < ne_offset + resident_names_offset:
type_id = read_uint16_le(file.read(0x2)) # should be 0x800a for XMETHTABLE
if type_id == 0:
break
count = read_uint16_le(file.read(0x2))
file.read(0x4) # reserved
entries = []
for i in range(count):
file_offset = read_uint16_le(file.read(0x2))
file_length = read_uint16_le(file.read(0x2))
entries.append(
dict(
offset=file_offset << shift_count, length=file_length << shift_count
)
)
file.read(0x2) # flagword
file.read(0x2) # resource_id
file.read(0x2) # handle
file.read(0x2) # usage
resources.append(dict(type_id=type_id, entries=entries))
resource_names = []
while file.tell() < ne_offset + resident_names_offset:
length = read_uint8(file.read(0x1))
if length == 0:
break
resource_names.append(file.read(length).decode("ASCII"))
print("Resources found:")
print(resources, resource_names)
xmethtable_exists = "XMETHTABLE" in resource_names
file.seek(ne_offset + resident_names_offset)
name_length = read_uint8(file.read(0x1))
file_name = file.read(name_length).decode("ASCII")
# Borland C++ can put the XMETHTABLE token into a weird nonstandard resource
for x in filter(lambda d: d["type_id"] == 0x800F, resources):
for y in x["entries"]:
file.seek(y["offset"], os.SEEK_SET)
data = file.read(y["length"])
xmethtable_exists |= b"XMETHTABLE" in data
if not xmethtable_exists:
raise ValueError("XMETHTABLE not found!")
resources = list(filter(lambda x: x["type_id"] == 0x800A, resources))
if len(resources) != 1:
raise ValueError("Expected a single matching resource type entry!")
xmethtable_offset = resources[0]["entries"][0]["offset"]
xmethtable_length = resources[0]["entries"][0]["length"]
print(f"Found XMETHTABLE for XObject library {file_name}!")
file.seek(xmethtable_offset, os.SEEK_SET)
xmethtable_raw = file.read(xmethtable_length)
xmethtable = [
entry.decode("iso-8859-1")
for entry in xmethtable_raw.strip(b"\x00").split(b"\x00")
]
for entry in xmethtable:
print(entry)
library_name = xmethtable[1]
xmethtable[1] = "--" + library_name
return {
"type": "XObject",
"name": library_name,
"slug": file_name.lower(),
"filename": file_name,
"method_table": xmethtable,
}
def extract_xcode_win32(file: BinaryIO, pe_offset: int) -> XCode:
file.seek(pe_offset + 4)
# read the COFF Header, perform basic sanity checks
machine_type = read_uint16_le(file.read(0x2))
if machine_type != 0x14c:
raise ValueError(f"PE file is not 32-bit Intel x86")
section_count = read_uint16_le(file.read(0x2))
file.seek(12, os.SEEK_CUR)
optional_size = read_uint16_le(file.read(0x2))
characteristics = read_uint16_le(file.read(0x2))
if not (characteristics & 0x2000):
raise ValueError("DLL flag not set")
if not (characteristics & 0x0100):
raise ValueError("32-bit flag not set")
# read the Optional Header to get the image base address
optional = file.read(optional_size)
image_base = 0
if read_uint16_le(optional[0:2]) == 0x10b:
image_base = read_uint32_le(optional[28:32])
print(f"Found PE32, image base {image_base:08x}")
elif read_uint16_le(optional[0:2]) == 0x20b:
raise ValueError("PE32+ not supported")
else:
raise ValueError("Unknown optional header magic number")
# read each Section Header from the Section Table
sections: dict[str, PESection] = {}
for i in range(section_count):
segment: PESection = {
"name": file.read(0x8).strip(b'\x00').decode('utf8'),
"virt_size": read_uint32_le(file.read(0x4)),
"virt_addr": read_uint32_le(file.read(0x4)),
"raw_size": read_uint32_le(file.read(0x4)),
"raw_ptr": read_uint32_le(file.read(0x4)),
}
file.seek(16, os.SEEK_CUR)
sections[segment["name"]] = segment
print(f"{segment['name']}: {segment['virt_addr']:08x} {segment['virt_size']:08x}")
# grab the .text section; this contains the program instructions
if ".text" not in sections:
raise ValueError(".text section not found")
file.seek(sections[".text"]["raw_ptr"])
code = file.read(sections[".text"]["raw_size"])
# Lingo Xtras are COM libraries with a generic calling API.
# Director discovers what functions are available by requesting
# a msgTable, which unfortunately for us is done with code.
# We need to find the following x86 assembly:
# 68 [ u32 addr 1 ] ; push offset "msgTable"
# 6a 00 ; push 0
# 68 [ u32 addr 2 ] ; push offset xtra_methtable
# 6a 09 ; push 9
instr = re.compile(rb"\x68(.{4})\x6a\x00\x68(.{4})\x6a\x09", flags=re.DOTALL)
methtable_found = False
methtable = []
for msgtable_raw, methtable_raw in instr.findall(code):
# should be the offset to the string "msgTable"
msgtable_offset = read_uint32_le(msgtable_raw) - image_base
# should be the offset to the full method table
methtable_offset = read_uint32_le(methtable_raw) - image_base
msgtable_found = False
for s in sections.values():
if msgtable_offset in range(s["virt_addr"], s["virt_addr"]+s["virt_size"]):
file.seek(s["raw_ptr"])
data = file.read(s["raw_size"])
start = msgtable_offset - s["virt_addr"]
end = data.find(b"\x00", start)
if data[start:end] != b"msgTable":
continue
print(f"Found msgTable!")
msgtable_found = True
if not msgtable_found:
continue
# If we found the text "msgTable" at the first address, we know we've found the right call.
for s in sections.values():
if methtable_offset in range(s["virt_addr"], s["virt_addr"]+s["virt_size"]):
file.seek(s["raw_ptr"])
data = file.read(s["raw_size"])
start = methtable_offset - s["virt_addr"]
end = data.find(b"\x00", start)
methtable_found = True
methtable = data[start:end].decode('iso-8859-1').split('\n')
if not methtable_found:
raise ValueError("Could not find msgTable!")
for entry in methtable:
print(entry)
library_name = methtable[0].split()[1].capitalize()
methtable[0] = "-- " + methtable[0]
return {
"type": "Xtra",
"name": library_name,
"slug": library_name.lower(),
"filename": library_name.lower(),
"method_table": methtable
}
def extract_xcode(path: str, resid: str) -> XCode:
with open(path, "rb") as file:
magic = file.read(0x2)
if magic == b"MZ":
file.seek(0x3C, os.SEEK_SET)
header_offset = read_uint16_le(file.read(0x2))
file.seek(header_offset, os.SEEK_SET)
magic = file.read(0x2)
if magic == b"NE":
print("Found Win16 NE DLL!")
return extract_xcode_win16(file, header_offset)
elif magic == b"PE":
print("Found Win32 PE DLL!")
return extract_xcode_win32(file, header_offset)
file.seek(0)
header = file.read(124)
if (
len(header) == 124
and header[0] == 0
and header[74] == 0
and header[82] == 0
and header[122] in [129, 130]
and header[123] in [129, 130]
):
print("Found MacBinary!")
data_size = read_uint32_be(header[83:87])
resource_size = read_uint32_be(header[87:91])
resource_offset = (
128
+ data_size
+ ((128 - (data_size % 128)) if (data_size % 128) else 0)
)
print(f"resource offset: {resource_offset}")
return extract_xcode_macbinary(file, resource_offset, resid)
raise ValueError("Unknown filetype")
def generate_xobject_stubs(
xmethtable: list[str],
slug: str,
name: str,
filename: str,
director_version: int = 400,
dry_run: bool = False,
) -> None:
meths = []
for e in xmethtable:
if not e.strip():
break
elems = e.split()
if not elems or elems[0].startswith("--"):
continue
first = elems[0]
if first.startswith("/"):
first = first[1:]
returnval = first[0]
args = first[1:]
methname = elems[1].split(",")[0]
if methname.startswith("+"):
methname = methname[1:]
if methname.startswith("m"):
methname = methname[1].lower() + methname[2:]
meths.append(
dict(
methname=methname,
args=args,
min_args=len(args),
max_args=len(args),
returnval=returnval,
default='""' if returnval == "S" else "0",
)
)
xobject_class = f"{name}XObject"
xobj_class = f"{name}XObj"
cpp_text = TEMPLATE.format(
base="xlibs",
slug=slug,
name=name,
filename=filename,
xmethtable="\n".join(xmethtable),
xobject_class=xobject_class,
xobj_class=xobj_class,
xlib_builtins="",
xlib_toplevels="",
xlib_methods="\n".join(
[
XLIB_METHOD_TEMPLATE.format(
xobj_class=xobj_class, director_version=director_version, **x
)
for x in meths
]
),
xobj_new=XLIB_NEW_TEMPLATE.format(xobj_class=xobj_class),
xobj_stubs="\n".join(
[
XOBJ_NR_STUB_TEMPLATE.format(xobj_class=xobj_class, **x)
if x["returnval"] == "X"
else XOBJ_STUB_TEMPLATE.format(xobj_class=xobj_class, **x)
for x in meths
if x["methname"] != "new"
]
),
)
if dry_run:
print("C++ output:")
print(cpp_text)
print()
else:
with open(os.path.join(LINGO_XLIBS_PATH, f"{slug}.cpp"), "w") as cpp:
cpp.write(cpp_text)
header_text = TEMPLATE_H.format(
base_upper="XLIBS",
slug_upper=slug.upper(),
xobject_class=xobject_class,
xobj_class=xobj_class,
methlist="\n".join([TEMPLATE_HEADER_METH.format(**x) for x in meths]),
)
if dry_run:
print("Header output:")
print(header_text)
print()
else:
with open(os.path.join(LINGO_XLIBS_PATH, f"{slug}.h"), "w") as header:
header.write(header_text)
if not dry_run:
inject_makefile(slug, "XObject")
inject_lingo_object(slug, xobj_class, director_version, "XObject")
def generate_xcmd_stubs(
type: Literal["XCMD", "XFCN"],
slug: str,
name: str,
filename: str,
director_version: int = 400,
dry_run: bool = False,
) -> None:
xobj_class = f"{name}{type}"
methtype = "CBLTIN" if type == "XCMD" else "HBLTIN"
cpp_text = XCMD_TEMPLATE.format(
slug=slug,
name=name,
filename=filename,
xobj_class=xobj_class,
xlib_builtins=BUILTIN_TEMPLATE.format(
name=name,
xobj_class=xobj_class,
min_args=-1,
max_args=0,
director_version=director_version,
methtype=methtype,
),
xobj_stubs=XOBJ_STUB_TEMPLATE.format(
xobj_class=xobj_class, methname=name, default=0
),
)
if dry_run:
print("C++ output:")
print(cpp_text)
print()
else:
with open(os.path.join(LINGO_XLIBS_PATH, f"{slug}.cpp"), "w") as cpp:
cpp.write(cpp_text)
header_text = XCMD_TEMPLATE_H.format(
slug_upper=slug.upper(),
xobj_class=xobj_class,
methlist=TEMPLATE_HEADER_METH.format(methname=name),
)
if dry_run:
print("Header output:")
print(header_text)
print()
else:
with open(os.path.join(LINGO_XLIBS_PATH, f"{slug}.h"), "w") as header:
header.write(header_text)
if not dry_run:
inject_makefile(slug, type)
inject_lingo_object(slug, xobj_class, director_version, "XObject")
def generate_xtra_stubs(
msgtable: list[str],
slug: str,
name: str,
filename: str,
director_version: int = 500,
dry_run: bool = False,
) -> None:
meths = []
for e in msgtable:
elem = e.split("--", 1)[0].strip()
if not elem:
continue
functype = "method"
if elem.startswith("+"):
elem = elem[1:].strip()
functype = "toplevel"
elif elem.startswith("*"):
elem = elem[1:].strip()
functype = "global"
if " " not in elem:
methname, argv = elem, []
else:
methname, args = elem.split(" ", 1)
argv = args.split(",")
min_args = len(argv)
max_args = len(argv)
if argv and argv[-1].strip() == "*":
min_args = -1
max_args = 0
elif functype == "method":
min_args -= 1
max_args = 0
meths.append(
dict(
functype=functype,
methname=methname,
args=argv,
min_args=min_args,
max_args=max_args,
default="0",
)
)
xobject_class = f"{name}XtraObject"
xobj_class = f"{name}Xtra"
cpp_text = TEMPLATE.format(
base="xtras",
slug=slug,
name=name,
filename=filename,
xmethtable="\n".join(msgtable),
xobject_class=xobject_class,
xobj_class=xobj_class,
xlib_methods="\n".join(
[
XLIB_METHOD_TEMPLATE.format(
xobj_class=xobj_class, director_version=director_version, **x
)
for x in meths if x["functype"] == "method"
]
),
xlib_builtins="\n".join([BUILTIN_TEMPLATE.format(
name=x["methname"],
xobj_class=xobj_class,
min_args=x["min_args"],
max_args=x["max_args"],
director_version=director_version,
methtype="HBLTIN",
) for x in meths if x["functype"] == "global"]),
xlib_toplevels="\n".join([BUILTIN_TEMPLATE.format(
name=x["methname"],
xobj_class=xobj_class,
min_args=x["min_args"],
max_args=x["max_args"],
director_version=director_version,
methtype="HBLTIN",
) for x in meths if x["functype"] == "toplevel"]),
xobj_new=XLIB_NEW_TEMPLATE.format(xobj_class=xobj_class),
xobj_stubs="\n".join(
[
XOBJ_STUB_TEMPLATE.format(xobj_class=xobj_class, **x)
for x in meths
if x["methname"] != "new"
]
),
)
if dry_run:
print("C++ output:")
print(cpp_text)
print()
else:
with open(os.path.join(LINGO_XTRAS_PATH, f"{slug}.cpp"), "w") as cpp:
cpp.write(cpp_text)
header_text = TEMPLATE_H.format(
base_upper="XTRAS",
slug_upper=slug.upper(),
xobject_class=xobject_class,
xobj_class=xobj_class,
methlist="\n".join([TEMPLATE_HEADER_METH.format(**x) for x in meths]),
)
if dry_run:
print("Header output:")
print(header_text)
print()
else:
with open(os.path.join(LINGO_XTRAS_PATH, f"{slug}.h"), "w") as header:
header.write(header_text)
if not dry_run:
inject_makefile(slug, "Xtra")
inject_lingo_object(slug, xobj_class, director_version, "Xtra")
def main() -> None:
parser = argparse.ArgumentParser(
description="Extract the method table from a Macromedia Director XObject/XLib and generate method stubs."
)
parser.add_argument("XOBJ_FILE", help="XObject/XLib file to test")
parser.add_argument(
"--resid", help="Resource ID (for MacBinary)", type=str, default=None
)
parser.add_argument(
"--slug", help="Slug to use for files (e.g. {slug}.cpp, {slug}.h)"
)
parser.add_argument(
"--name", help="Base name to use for classes (e.g. {name}XObj, {name}XObject)"
)
parser.add_argument(
"--version",
metavar="VER",
help="Minimum Director version (default: 400)",
type=int,
default=400,
)
parser.add_argument(
"--write",
help="Write generated stubs to the source tree",
dest="dry_run",
action="store_false",
)
args = parser.parse_args()
xcode = extract_xcode(args.XOBJ_FILE, args.resid)
slug = args.slug or xcode["slug"]
name = args.name or xcode["name"]
if xcode["type"] == "XObject":
generate_xobject_stubs(
xcode["method_table"],
slug,
name,
xcode["filename"],
args.version,
args.dry_run,
)
elif xcode["type"] == "Xtra":
generate_xtra_stubs(
xcode["method_table"], slug, name, xcode["filename"], args.version, args.dry_run
)
elif xcode["type"] == "XFCN" or xcode["type"] == "XCMD":
generate_xcmd_stubs(
xcode["type"], slug, name, xcode["filename"], args.version, args.dry_run
)
if __name__ == "__main__":
main()