Skip to content

资源转换管线 — 代码走读

脚本概览

本项目的资源转换由两个 Python 脚本完成:

generate_wasm_assets.py

整体结构

主函数

python
def main():
    """主入口:扫描源码并生成 WASM 资源文件"""
    # 1. 确定 C 源文件搜索路径
    source_dirs = ['src/', 'data/', 'graphics/']

    # 2. 收集所有资源引用
    resources = []
    for dir_path in source_dirs:
        for c_file in find_c_files(dir_path):
            resources.extend(scan_for_incgfx(c_file))
            resources.extend(scan_for_incbin(c_file))

    # 3. 去重并排序
    resources = deduplicate(resources)

    # 4. 处理每个资源
    for res in resources:
        if res.type == 'gfx':
            convert_gfx_resource(res)
        elif res.type == 'bin':
            convert_bin_resource(res)

    # 5. 生成输出文件
    generate_output_header(resources, 'generated/wasm_assets.h')
    generate_output_data(resources, 'generated/wasm_assets.c')

INCGFX 扫描

python
import re

# 匹配 INCGFX 宏调用的正则表达式
INCGFX_RE = re.compile(
    r'INCGFX\s*\(\s*"([^"]+)"\s*\)'
)

def scan_for_incgfx(c_file_path):
    """扫描 C 文件中的所有 INCGFX 宏调用"""
    results = []

    with open(c_file_path, 'r') as f:
        content = f.read()

    for match in INCGFX_RE.finditer(content):
        resource_path = match.group(1)
        results.append({
            'type': 'gfx',
            'path': resource_path,
            'source_file': c_file_path,
            'match_text': match.group(0),
        })

    return results

正则表达式解析

  • INCGFX — 匹配宏名称
  • \s*\(\s* — 匹配左括号及周围空白
  • "([^"]+)" — 匹配并捕获双引号内的文件路径
  • \s*\) — 匹配右括号及前面空白

INCBIN 扫描

python
INCBIN_RE = re.compile(
    r'INCBIN\s*\(\s*"([^"]+)"\s*\)'
)

def scan_for_incbin(c_file_path):
    """扫描 C 文件中的所有 INCBIN 宏调用"""
    results = []

    with open(c_file_path, 'r') as f:
        content = f.read()

    for match in INCBIN_RE.finditer(content):
        resource_path = match.group(1)

        # 根据后缀判断是否需要特殊处理
        suffix = resource_path.rsplit('.', 1)[-1] if '.' in resource_path else ''

        results.append({
            'type': 'bin',
            'path': resource_path,
            'source_file': c_file_path,
            'format': suffix,  # 'bin', 'lz77', 'rl', 等
        })

    return results

图形资源转换

python
def convert_gfx_resource(resource):
    """将 GBA 图形资源转换为 C 数组"""
    path = resource['path']

    # 根据文件扩展名确定格式
    if path.endswith('.4bpp'):
        # 4bpp: 每像素 4 位, 16 色
        data = read_binary_file(path)
        resource['data'] = data
        resource['element_type'] = 'u8'
        resource['format_desc'] = '4bpp (16 colors)'
    elif path.endswith('.8bpp'):
        # 8bpp: 每像素 8 位, 256 色
        data = read_binary_file(path)
        resource['data'] = data
        resource['element_type'] = 'u8'
        resource['format_desc'] = '8bpp (256 colors)'
    elif path.endswith('.pal'):
        # 调色板文件: 16bit BGR555 颜色数组
        data = read_binary_file(path)
        resource['data'] = data
        resource['element_type'] = 'u16'
        resource['format_desc'] = 'palette (BGR555)'

def read_binary_file(path):
    """读取二进制文件,返回字节数组"""
    with open(path, 'rb') as f:
        return f.read()

头文件生成

python
def generate_output_header(resources, output_path):
    """生成包含 extern 声明的头文件"""
    lines = [
        "// 自动生成 - 请勿手动修改",
        "// 由 generate_wasm_assets.py 生成",
        "#ifndef WASM_ASSETS_H",
        "#define WASM_ASSETS_H",
        "",
        '#include "global.h"',
        "",
    ]

    for res in resources:
        # 生成合法的 C 变量名
        var_name = sanitize_path_to_identifier(res['path'])
        elem_type = res.get('element_type', 'u8')
        data = res.get('data', b'')

        lines.append(f"// {res['path']} ({res.get('format_desc', 'binary')})")
        lines.append(f"extern const {elem_type} {var_name}[{len(data)}];")
        lines.append("")

    lines.append("#endif // WASM_ASSETS_H")

    with open(output_path, 'w') as f:
        f.write('\n'.join(lines))

wasm_asm_data.py

整体结构

汇编文件解析

python
class AsmDataParser:
    """GBA 汇编数据文件解析器"""

    def __init__(self):
        self.data_buffer = bytearray()
        self.symbols = {}        # 符号名 → 偏移量
        self.current_offset = 0
        self.output_sections = []  # 每个全局符号对应一个数据段

    def parse_file(self, filepath):
        """解析单个汇编文件"""
        with open(filepath, 'r') as f:
            lines = f.readlines()

        self._parse_lines(lines)

    def _parse_lines(self, lines):
        """逐行解析汇编指令"""
        for line in lines:
            # 去除注释和空白
            line = line.split('//')[0].split('@')[0].strip()
            if not line:
                continue

            # 标签
            if line.endswith(':') and not line.startswith('.'):
                label = line[:-1].strip()
                self.symbols[label] = self.current_offset
                continue

            # 数据指令
            if line.startswith('.byte'):
                self._parse_byte_directive(line)
            elif line.startswith('.halfword') or line.startswith('.hword'):
                self._parse_halfword_directive(line)
            elif line.startswith('.word'):
                self._parse_word_directive(line)
            elif line.startswith('.align'):
                self._parse_align_directive(line)
            elif line.startswith('.include'):
                self._parse_include_directive(line)
            elif line.startswith('.space'):
                self._parse_space_directive(line)
            elif line.startswith('.ascii') or line.startswith('.string'):
                self._parse_string_directive(line)

数据指令解析

python
    def _parse_byte_directive(self, line):
        """解析 .byte value1, value2, ..."""
        values_str = line[5:].strip()  # 去除 ".byte"
        values = self._parse_value_list(values_str)
        for val in values:
            self.data_buffer.append(val & 0xFF)
            self.current_offset += 1

    def _parse_halfword_directive(self, line):
        """解析 .halfword value1, value2, ..."""
        # 确保对齐到 2 字节
        self._align_to(2)

        values_str = line.split(None, 1)[1].strip()
        values = self._parse_value_list(values_str)
        for val in values:
            # GBA 为小端序
            self.data_buffer.append(val & 0xFF)
            self.data_buffer.append((val >> 8) & 0xFF)
            self.current_offset += 2

    def _parse_word_directive(self, line):
        """解析 .word value1, value2, ..."""
        # 确保对齐到 4 字节
        self._align_to(4)

        values_str = line.split(None, 1)[1].strip()
        values = self._parse_value_list(values_str)
        for val in values:
            # 32 位小端序
            self.data_buffer.append(val & 0xFF)
            self.data_buffer.append((val >> 8) & 0xFF)
            self.data_buffer.append((val >> 16) & 0xFF)
            self.data_buffer.append((val >> 24) & 0xFF)
            self.current_offset += 4

    def _parse_align_directive(self, line):
        """解析 .align n — 对齐到 2^n 字节边界"""
        n = int(line.split()[1])
        self._align_to(1 << n)

    def _align_to(self, boundary):
        """填充零字节直到对齐到指定边界"""
        padding = (boundary - (self.current_offset % boundary)) % boundary
        self.data_buffer.extend(b'\x00' * padding)
        self.current_offset += padding

C 代码输出

python
    def generate_c_output(self, output_path):
        """将解析的数据生成 C 源文件"""
        lines = [
            "// 自动生成 - 请勿手动修改",
            "// 由 wasm_asm_data.py 生成",
            '#include "global.h"',
            "",
        ]

        for symbol_name, offset in self.symbols.items():
            # 计算此符号的数据段大小
            # (到下一个符号或缓冲区末尾)
            next_offsets = [o for o in self.symbols.values() if o > offset]
            end = min(next_offsets) if next_offsets else len(self.data_buffer)
            data = self.data_buffer[offset:end]

            if not data:
                continue

            # 生成 C 数组
            lines.append(f"const u8 {symbol_name}[] = {{")

            # 每行 16 字节
            for i in range(0, len(data), 16):
                chunk = data[i:i+16]
                hex_values = ', '.join(f'0x{b:02X}' for b in chunk)
                lines.append(f"    {hex_values},")

            lines.append("};")
            lines.append("")

        with open(output_path, 'w') as f:
            f.write('\n'.join(lines))

值解析工具

python
    def _parse_value_list(self, values_str):
        """解析逗号分隔的值列表,支持十进制、十六进制和符号"""
        values = []
        for item in values_str.split(','):
            item = item.strip()
            if not item:
                continue

            # 十六进制: 0x...
            if item.startswith('0x') or item.startswith('0X'):
                values.append(int(item, 16))
            # 十进制数字
            elif item.isdigit() or (item.startswith('-') and item[1:].isdigit()):
                values.append(int(item))
            # 已知常量 (从常量头文件解析)
            elif item in self.known_constants:
                values.append(self.known_constants[item])
            # 未知符号 — 留空待后续解析
            else:
                values.append(0)  # 占位
                print(f"Warning: 未解析的符号: {item}")

        return values

关键函数索引

函数名所属脚本说明
main()generate_wasm_assets.py主入口:扫描源码并生成资源文件
scan_for_incgfx(c_file)generate_wasm_assets.py扫描单个 C 文件中的 INCGFX 宏
scan_for_incbin(c_file)generate_wasm_assets.py扫描单个 C 文件中的 INCBIN 宏
convert_gfx_resource(res)generate_wasm_assets.py转换图形资源为 C 数组
convert_bin_resource(res)generate_wasm_assets.py转换二进制资源
generate_output_header(res, path)generate_wasm_assets.py生成 extern 声明头文件
generate_output_data(res, path)generate_wasm_assets.py生成数据定义 C 文件
deduplicate(resources)generate_wasm_assets.py去除重复的资源引用
parse_file(filepath)wasm_asm_data.py (AsmDataParser)解析汇编文件入口
_parse_byte_directive(line)wasm_asm_data.py (AsmDataParser)解析 .byte 指令
_parse_halfword_directive(line)wasm_asm_data.py (AsmDataParser)解析 .halfword 指令
_parse_word_directive(line)wasm_asm_data.py (AsmDataParser)解析 .word 指令
_parse_align_directive(line)wasm_asm_data.py (AsmDataParser)解析 .align 对齐指令
_align_to(boundary)wasm_asm_data.py (AsmDataParser)计算并插入对齐填充
generate_c_output(path)wasm_asm_data.py (AsmDataParser)生成 C 源文件输出

数据流总览

相关阅读