用於解析FBNeo游戲數據的Python3腳本


FBNeo在代碼中存儲了游戲的元數據, 其數據格式為

struct BurnDriver BurnDrvCpsStriderua = {
	"striderua", "strider", NULL, NULL, "1989",
	"Strider (US set 2)\0", NULL, "Capcom", "CPS1",
	NULL, NULL, NULL, NULL,
	BDF_GAME_WORKING | BDF_CLONE | BDF_HISCORE_SUPPORTED, 2, HARDWARE_CAPCOM_CPS1, GBF_PLATFORM, 0,
	NULL, StrideruaRomInfo, StrideruaRomName, NULL, NULL, NULL, NULL, StriderInputInfo, StrideruaDIPInfo,
	StriderInit, DrvExit, Cps1Frame, CpsRedraw, CpsAreaScan,
	&CpsRecalcPal, 0x1000, 384, 224, 4, 3
};

struct BurnDriver {
	char* szShortName;			// The filename of the zip file (without extension)
	char* szParent;				// The filename of the parent (without extension, NULL if not applicable)
	char* szBoardROM;			// The filename of the board ROMs (without extension, NULL if not applicable)
	char* szSampleName;			// The filename of the samples zip file (without extension, NULL if not applicable)
	char* szDate;

	// szFullNameA, szCommentA, szManufacturerA and szSystemA should always contain valid info
	// szFullNameW, szCommentW, szManufacturerW and szSystemW should be used only if characters or scripts are needed that ASCII can't handle
	char*    szFullNameA; char*    szCommentA; char*    szManufacturerA; char*    szSystemA;
	wchar_t* szFullNameW; wchar_t* szCommentW; wchar_t* szManufacturerW; wchar_t* szSystemW;

	INT32 Flags;			// See burn.h
	INT32 Players;		// Max number of players a game supports (so we can remove single player games from netplay)
	INT32 Hardware;		// Which type of hardware the game runs on
	INT32 Genre;
	INT32 Family;
	INT32 (*GetZipName)(char** pszName, UINT32 i);				// Function to get possible zip names
	INT32 (*GetRomInfo)(struct BurnRomInfo* pri, UINT32 i);		// Function to get the length and crc of each rom
	INT32 (*GetRomName)(char** pszName, UINT32 i, INT32 nAka);	// Function to get the possible names for each rom
	INT32 (*GetHDDInfo)(struct BurnHDDInfo* pri, UINT32 i);			// Function to get hdd info
	INT32 (*GetHDDName)(char** pszName, UINT32 i, INT32 nAka);		// Function to get the possible names for each hdd
	INT32 (*GetSampleInfo)(struct BurnSampleInfo* pri, UINT32 i);		// Function to get the sample flags
	INT32 (*GetSampleName)(char** pszName, UINT32 i, INT32 nAka);	// Function to get the possible names for each sample
	INT32 (*GetInputInfo)(struct BurnInputInfo* pii, UINT32 i);	// Function to get the input info for the game
	INT32 (*GetDIPInfo)(struct BurnDIPInfo* pdi, UINT32 i);		// Function to get the input info for the game
	INT32 (*Init)(); INT32 (*Exit)(); INT32 (*Frame)(); INT32 (*Redraw)(); INT32 (*AreaScan)(INT32 nAction, INT32* pnMin);
	UINT8* pRecalcPal; UINT32 nPaletteEntries;										// Set to 1 if the palette needs to be fully re-calculated
	INT32 nWidth, nHeight; INT32 nXAspect, nYAspect;					// Screen width, height, x/y aspect
};

#define BurnDriverD BurnDriver		// Debug status
#define BurnDriverX BurnDriver		// Exclude from build

可以用Python的正則將其解出, 可以用於加工輸出成其他軟件的游戲列表文件格式.

 

下面的腳本, 用於解析其數據后, 生成EmulationStation使用的gamelist.xml, 並將游戲文件和配圖復制到指定目錄

#!/usr/bin/python3
# -*- coding: UTF-8 -*-

import os
import time
import re
from shutil import copyfile
from xml.etree import ElementTree as et
from xml.dom import minidom
from datetime import datetime

do_copy = 1
target_folder = r'D:\temp\toaplan'
sub_roms_folder = 'toaplan'
fbneo_src_folder = r'drv\toaplan'
sub_images_folder = 'toaplan_images'

fbneo_rom_folder = r'D:\Backup\ent\Games\fbneo\roms'
fbneo_preview_folder = r'D:\Backup\ent\Games\fbneo\support\previews'
fbneo_title_folder = r'D:\Backup\ent\Games\fbneo\support\titles'


fbneo_genres = {
    'GBF_HORSHOOT': 'Shooter / Horizontal / Sh\'mup',
    'GBF_VERSHOOT': 'Shooter / Vertical / Sh\'mup',
    'GBF_SCRFIGHT': 'Fighting / Beat \'em Up',
    'GBF_VSFIGHT': 'Fighting / Versus',
    'GBF_BIOS': 'BIOS',
    'GBF_BREAKOUT': 'Breakout',
    'GBF_CASINO': 'Casino',
    'GBF_BALLPADDLE': 'Ball & Paddle',
    'GBF_MAZE': 'Maze',
    'GBF_MINIGAMES': 'Mini-Games',
    'GBF_PINBALL': 'Pinball',
    'GBF_PLATFORM': 'Platform',
    'GBF_PUZZLE': 'Puzzle',
    'GBF_QUIZ': 'Quiz',
    'GBF_SPORTSMISC': 'Sports',
    'GBF_SPORTSFOOTBALL': 'Sports / Football',
    'GBF_MISC': 'Misc',
    'GBF_MAHJONG': 'Mahjong',
    'GBF_RACING': 'Racing',
    'GBF_SHOOT': 'Shooter',
    'GBF_ACTION': 'Action (Classic)',
    'GBF_RUNGUN': 'Run \'n Gun (Shooter)',
    'GBF_STRATEGY': 'Strategy',
    'GBF_VECTOR': 'Vector'
}

fbneo_genres_cn = {
    'GBF_HORSHOOT': '橫向射擊',
    'GBF_VERSHOOT': '豎向射擊',
    'GBF_SCRFIGHT': '戰斗擊打',
    'GBF_VSFIGHT': '對戰格斗',
    'GBF_BIOS': 'BIOS',
    'GBF_BREAKOUT': '休閑',
    'GBF_CASINO': '博彩',
    'GBF_BALLPADDLE': '擊球',
    'GBF_MAZE': '迷宮',
    'GBF_MINIGAMES': '迷你小游戲',
    'GBF_PINBALL': '彈球',
    'GBF_PLATFORM': '平台',
    'GBF_PUZZLE': '猜謎',
    'GBF_QUIZ': '問答',
    'GBF_SPORTSMISC': '運動',
    'GBF_SPORTSFOOTBALL': '足球運動',
    'GBF_MISC': '其他',
    'GBF_MAHJONG': '麻將',
    'GBF_RACING': '賽道',
    'GBF_SHOOT': '射擊',
    'GBF_ACTION': '動作(經典)',
    'GBF_RUNGUN': '運動射擊',
    'GBF_STRATEGY': '策略',
    'GBF_VECTOR': 'Vector'
}


def read_file(file_path, encoding='UTF-8'):
    root_path = os.path.dirname(__file__)
    real_path = os.path.join(root_path, file_path)
    f = open(real_path, encoding=encoding)
    print(real_path)
    content = f.read()
    return content


def list_all_files(file_path):
    root_path = os.path.dirname(__file__)
    real_path = os.path.join(root_path, file_path)
    files = []
    for f in os.listdir(real_path):
        f_path = os.path.join(real_path, f)
        if os.path.isfile(f_path):
            files.append(os.path.join(file_path, f))
        else:
            files.extend(list_all_files(os.path.join(file_path, f)))
    return files


def to_datetime(str):
    if not re.match('^\d{4}$', str) is None:
        t = datetime.strptime(str, '%Y')
        return t.strftime('%Y%m%dT%H%M%S')
    else:
        return None


def get_genre(str):
    if str is None:
        return None
    else:
        keys = str.split('|')
        for key in keys:
            key = key.strip()
            if key == 'GBF_VECTOR':
                continue
            if key in fbneo_genres:
                # print(fbneo_genres[key])
                return fbneo_genres[key]
        return None


def dequote(str):
    if (str == 'NULL'):
        return None
    else:
        match = re.match(r'L?"(.*)"', str)
        return match.group(1).strip()


def dequote2(str):
    if (str == 'NULL'):
        return None
    else:
        match = re.match(r'L?"(.*)\\0"', str)
        return match.group(1).strip()


def dequote2unicode(str):
    if (str == 'NULL'):
        return None
    else:
        match = re.match(r'L?"(.*)\\0"', str)
        return match.group(1).strip().replace(r'\0', ' ').encode('utf-8').decode('unicode_escape')


def dict_to_elem(dictionary):
    item = et.Element('game')
    for key in dictionary:
        field = et.Element(key)
        field.text = dictionary[key]
        item.append(field)
    return item


def prettify(elem):
    rough_string = et.tostring(elem, 'utf-8')
    reparsed = minidom.parseString(rough_string)
    return reparsed.toprettyxml(indent="  ")

def check_and_mkdir(path):
    if not os.path.isdir(path):
        try:
            os.mkdir(path)
        except OSError:
            print("Creation of the directory %s failed" % path)
        else:
            print("Successfully created the directory %s " % path)


print('[*] Fetch games')

files = list_all_files(fbneo_src_folder)

games = []
for f in files:
    content = read_file(f, 'iso-8859-1')
    result = re.compile(r'struct\s+BurnDriverD?\s+Burn.*\s+=\s+\{[^}]+\};').findall(content)
    if (len(result) > 0):
        for idx, line in enumerate(result):
            print(idx, line)
            # remote the comments
            line = re.sub(r'(\/\/.*\n|\n+)', r'\n', line)  # remove the // comments
            line = re.sub(r'\/\*[\w\s=,|]+\*\/', '', line)  # remove the  /* */ comments
            line = re.sub(r'\s+,', ',', line)  # remove the space before comma
            line = re.sub(r'\s+', ' ', line)  # remove all new lines

            # print(idx, line)
            # BurnDriver, BurnDriverD
            match = re.match(r'struct\s+BurnDriverD?\s+Burn.*\s+=\s+\{'
                             r'\s+(["\w]+|NULL),\s*(["\w]+|NULL),\s*(["\w]+|NULL),\s*(["\w]+|NULL),\s*(["\w\?\+\s]+|NULL),'
                             r'\s*("(?:\\.|[^"\\])*"|NULL),\s*("(?:\\.|[^"\\])*"|NULL),\s*("(?:\\.|[^"\\])*"|NULL),\s*("(?:\\.|[^"\\])*"|NULL),'
                             r'\s*(L"(?:\\.|[^"\\])*"|NULL),\s*(L"(?:\\.|[^"\\])*"|NULL),\s*(L"(?:\\.|[^"\\])*"|NULL),\s*(L"(?:\\.|[^"\\])*"|NULL),'
                             r'\s*([0-9A-Z_\s|/\*]+),\s*(\d+),\s*([\w\s|/]+),\s*([0-9A-Z_\s|]+),\s*([0-9A-Z_\s|]+),'
                             r'\s*(\w+|NULL),\s*(\w+|NULL),\s*(\w+|NULL),\s*(\w+|NULL),\s*(\w+|NULL),\s*(\w+|NULL),\s*(\w+|NULL),\s*(\w+|NULL),\s*(\w+|NULL),'
                             r'\s*(\w+),\s*(\w+),\s*(\w+),\s*(\w+),\s*(\w+),'
                             r'\s*(&\w+|NULL),\s*([\w\s\*]+),'
                             r'\s*(\d+|\w+|\d+\*2),\s*(\d+|\w+),\s*(\d+),\s*(\d+)'
                             r'([^}]+)\};', line)
            game = {}
            game['shortName'] = dequote(match.group(1))
            game['parent'] = dequote(match.group(2))
            game['boardRom'] = dequote(match.group(3))
            game['sampleName'] = dequote(match.group(4))
            game['date'] = dequote(match.group(5))
            game['datetime'] = to_datetime(game['date'])

            game['fullNameA'] = dequote2(match.group(6))
            game['fullCommentA'] = dequote(match.group(7))
            game['manufacturerA'] = dequote(match.group(8))
            game['systemA'] = dequote(match.group(9))

            game['fullNameW'] = dequote2unicode(match.group(10))
            game['fullCommentW'] = match.group(11)
            game['manufacturerW'] = match.group(12)
            game['systemW'] = match.group(13)

            game['flags'] = match.group(14)
            game['players'] = match.group(15)
            game['hardware'] = match.group(16)
            game['genre'] = match.group(17)
            game['genre_text'] = get_genre(game['genre'])
            game['family'] = match.group(18)

            game['GetZipName'] = match.group(19)
            game['GetRomInfo'] = match.group(20)
            game['GetRomName'] = match.group(21)
            game['GetHDDInfo'] = match.group(22)
            game['GetHDDName'] = match.group(23)
            game['GetSampleInfo'] = match.group(24)
            game['GetSampleName'] = match.group(25)
            game['GetInputInfo'] = match.group(26)
            game['GetDIPInfo'] = match.group(27)

            game['init'] = match.group(28)
            game['exit'] = match.group(29)
            game['frame'] = match.group(30)
            game['redraw'] = match.group(31)
            game['areaScan'] = match.group(32)
            # UINT8* pRecalcPal; UINT32 nPaletteEntries
            game['recalcPal'] = match.group(33)
            game['paletteEntries'] = match.group(34)
            # INT32 nWidth, nHeight; INT32 nXAspect, nYAspect;
            game['width'] = match.group(35)
            game['height'] = match.group(36)
            game['xaspect'] = match.group(37)
            game['yaspect'] = match.group(38)

            # print(game['fullNameW'],game['fullCommentW'],game['manufacturerW'],game['systemW'])
            # print(game['fullNameW'], game['fullNameW'].replace(r'\0', ' ').encode('utf-8').decode('unicode_escape'))
            print(game)
            games.append(game)

target_roms_folder = os.path.join(target_folder, sub_roms_folder)
check_and_mkdir(target_roms_folder)
target_images_folder = os.path.join(target_folder, sub_images_folder)
check_and_mkdir(target_images_folder)

# compose the xml file
root = et.Element('gameList')  # create the element first...
tree = et.ElementTree(root)  # and pass it to the created tree

for game in games:
    rom_file = os.path.join(fbneo_rom_folder, game['shortName'] + '.zip')
    preview_file = os.path.join(fbneo_preview_folder, game['shortName'] + '.png')
    title_file = os.path.join(fbneo_title_folder, game['shortName'] + '.png')
    if not os.path.isfile(rom_file):
        print('nonexists: {}'.format(rom_file))
        continue
    elif do_copy == 1:
        # Do the copy
        copyfile(rom_file, os.path.join(target_roms_folder, game['shortName'] + '.zip'))

    if not os.path.isfile(preview_file):
        image_str = None
        print('nonexists: {}'.format(preview_file))
    else:
        image_str = './' + sub_images_folder +'/' + game['shortName'] + '.png'
        if do_copy == 1:
            copyfile(preview_file, os.path.join(target_images_folder, game['shortName'] + '.png'))

    if not os.path.isfile(title_file):
        marquee_str = None
        print('nonexists: {}'.format(title_file))
    else:
        marquee_str = './' + sub_images_folder + '/' + game['shortName'] + '_marquee.png'
        if do_copy == 1:
            copyfile(title_file, os.path.join(target_images_folder, game['shortName'] + '_marquee.png'))

    node = {
        'path': './' + sub_roms_folder + '/' + game['shortName'] + '.zip',
        'name': game['fullNameA'] if game['fullNameW'] is None else game['fullNameW'],
        'desc': '' if game['fullCommentA'] is None else game['fullCommentA'],
        'image': image_str,
        'marquee': marquee_str,
        'releasedate': game['datetime'],
        'developer': '' if game['manufacturerA'] is None else game['manufacturerA'],
        'publisher': '' if game['systemA'] is None else game['systemA'],
        'genre': game['genre_text'],
        'players': game['players']
    }
    root.append(dict_to_elem(node))

xml_content = prettify(root)

filename = os.path.join(target_folder, 'gamelist.xml')
with open(filename, 'w', newline = '\n', encoding='utf-8') as file:
    file.write(xml_content)

print('Done')

  

其中用到了

遞歸列出目錄下的所有文件

讀出文件內容至字符串

將\u1234 格式的Unicode轉為可讀字符

將dictionary轉為xml

將xml進行格式化

獲取當前腳本的絕對路徑, 拼接路徑

檢查文件, 目錄是否存在

創建目錄

復制文件到其他目錄

 

對雙引號內帶轉義的字符串的匹配

"(?:\\.|[^"\\])*"

這個正則的解析

"       # Match a quote.
(?:     # Either match...
 \\.    # an escaped character
|       # or
 [^"\\] # any character except quote or backslash.
)*      # Repeat any number of times.
"       # Match another quote.

輸出的xml為EmulationStation的gamelist.xml, 其格式為

<game>

    name - string, the displayed name for the game.
    desc - string, a description of the game. Longer descriptions will automatically scroll, so don't worry about size.
    image - image_path, the path to an image to display for the game (like box art or a screenshot).
    thumbnail - image_path, the path to a smaller image, displayed in image lists like the grid view. Should be small to ensure quick loading. Currently not used.
    rating - float, the rating for the game, expressed as a floating point number between 0 and 1. Arbitrary values are fine (ES can display half-stars, quarter-stars, etc).
    releasedate - datetime, the date the game was released. Displayed as date only, time is ignored.
    developer - string, the developer for the game.
    publisher - string, the publisher for the game.
    genre - string, the (primary) genre for the game.
    players - integer, the number of players the game supports.
    playcount - statistic, integer, the number of times this game has been played
    lastplayed - statistic, datetime, the last date and time this game was played.

<folder>

    name - string, the displayed name for the folder.
    desc - string, the description for the folder.
    image - image_path, the path to an image to display for the folder.
    thumbnail - image_path, the path to a smaller image to display for the folder. Currently not used.

  

寫這個腳本的原因, 是因為收集到了一個FBNeo 0.2.97.44游戲全集, 以及較完整的preview和title配圖, 希望能分機種將其游戲port到自己運行EmuELEC的盒子中, 保留其多國化游戲名, 並且在列表中配圖. 原本打算把相關代碼抽離出來, 直接在c代碼的基礎上輸出數據, 但是發現關聯較多, 而且c也不是很熟悉, 退而求其次, 用python正則來抽取. 花了一天多時間寫解析腳本, 以及xml輸出, unicode解碼, 目錄文件操作等. 

因為想基於EmuELEC默認的目錄結構來放置rom, 而EmuELEC除了cps1, cps2, cps3, neogeo, 並未給其它機種單獨設置目錄, 所以目錄的組織考慮了很久, 最后決定將pgm單獨放到arcade, 而其他的cave, irem, taito, toaplan, psikyo, pre90s, pst90s都以子目錄的形式放到fbneo下.

因為涉及到子目錄, 所以還需要給folder單獨做配圖, 做xml, ES官網上對<folder>語焉不詳, 嘗試多次失敗后終於搞定, 

產生的合集打包已經發布在 right.com.cn 和 ppxclub.com.

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM