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.

用于解析FBNeo游戏数据的Python3脚本的更多相关文章

  1. 运维脚本-elasticsearch数据迁移python3脚本

    elasticsearch数据迁移python3脚本 #!/usr/bin/python3 #elsearch 数据迁移脚本 #迁移工具路径 import time,os #下面命令是用到了一个go语 ...

  2. C#开发Unity游戏教程之使用脚本变量

    C#开发Unity游戏教程之使用脚本变量 使用脚本变量 本章前面说了那么多关于变量的知识,那么在脚本中要如何编写关于变量的代码,有规章可循吗?答案是有的.本节会依次讲解变量的声明.初始化.赋值和运算. ...

  3. 【COCOS2DX-LUA 脚本开发之一】在Cocos2dX游戏中使用Lua脚本进行游戏开发(基础篇)并介绍脚本在游戏中详细用途!

    [COCOS2DX-LUA 脚本开发之一]在Cocos2dX游戏中使用Lua脚本进行游戏开发(基础篇)并介绍脚本在游戏中详细用途! 分类: [Cocos2dx Lua 脚本开发 ] 2012-04-1 ...

  4. Unity 3D 之通过序列化来存档游戏数据

    我们在使用u3d开发一些单机游戏的过程中,都会涉及到游戏数据的存单和加载.一般情况下,如果存储的数据不复杂,我们就可以用PlayerPrefs,但有时涉及到的数据更加复杂,使用PlayerPrefs难 ...

  5. js读取解析JSON类型数据(转)

    谢谢博主,转自http://blog.csdn.net/beyond0851/article/details/9285771 一.什么是JSON? JSON(JavaScript Object Not ...

  6. NSXMLParser解析本地.xml数据(由于like7xiaoben写的太好了,我从她那里粘贴过来的)

    NSXMLParser解析简要说明 .是sax方法解析 .需要创建NSXMLParser实例 (alloc) 并创建解析器 (initWithData:) 为解析器定义委托 (setDelegate: ...

  7. lua学习:使用Lua处理游戏数据

    在之前lua学习:lua作配置文件里,我们学会了用lua作配置文件. 其实lua在游戏开发中可以作为一个强大的保存.载入游戏数据的工具. 1.载入游戏数据 比如说,现在我有一份表单: data.xls ...

  8. SpringMVC(三)-- 视图和视图解析器、数据格式化标签、数据类型转换、SpringMVC处理JSON数据、文件上传

    1.视图和视图解析器 请求处理方法执行完成后,最终返回一个 ModelAndView 对象 对于那些返回 String,View 或 ModeMap 等类型的处理方法,SpringMVC 也会在内部将 ...

  9. Scrapy1.4爬取笑话网站数据,Python3.5+Django2.0构建笑话应用

    Part1:需求简要描述 1.抓取http://www.jokeji.cn网站的笑话 2.以瀑布流方式显示 Part2:安装爬虫框架Scrapy1.4 1. 安装Scrapy1.4 E:\django ...

  10. json进阶(一)js读取解析JSON类型数据

    js读取解析JSON类型数据 一.什么是JSON? JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式,采用完全独立于语言的文本格式,是理想的数据交换格式,同 ...

随机推荐

  1. vscode的配置文件

    vscode的配置文件 总述:vscode中一般会在项目文件夹下自动生成.vscode文件夹,其中存放若干配置文件(.json),一般有如下文件: 下面将解释每个文件的用途与表现. 1. c_cpp_ ...

  2. [转帖]History of Unicode Release and Publication Dates

    www.unicode.org For ease of reference, this page collects together information about the dates for v ...

  3. 神通数据库的varchar和nvarchar的验证

    神通数据库的varchar和nvarchar的验证 登录神通数据库 isql 注意 神通数据库的默认密码是 szoscar55 Welcome to isql 2.0.56 interactive t ...

  4. [转帖]mysql8.0 MySQL函数

    PART1. MySQL函数介绍 函数表示对输入参数值返回一个具有特定关系的值,MySQL提供了大量丰富的函数,在进行数据库管理以及数据的查询和操作时将会经常用到各种函数.各类函数从功能方面主要分为数 ...

  5. [转帖]TIDB_HOT_REGIONS

    https://docs.pingcap.com/zh/tidb/stable/information-schema-tidb-hot-regions TIDB_HOT_REGIONS 表提供了关于当 ...

  6. [转帖]VMware常用软件ISO下载汇总(2023年6月更新)

    https://www.dinghui.org/vmware-iso-download.html#vcsa-patch 请访问 www.dinghui.org 获得最新版,文章将持续更新.[最后更新时 ...

  7. Linux部分参数的学习

    Linux部分参数的学习 简介 之前总结过很多Nginx或者是部署软件时的一些注意事项. 但是感觉对linux系统参数部分掌握的不是很好. 今天周末想着整理一下,作为备忘. limits.conf i ...

  8. 文盘Rust -- r2d2 实现redis连接池

    作者:贾世闻 我们在开发应用后端系统的时候经常要和各种数据库.缓存等资源打交道.这一期,我们聊聊如何访问redis 并将资源池化. 在一个应用后端程序访问redis主要要做的工作有两个,单例和池化. ...

  9. csv用Excel打开出现乱码

    CSV用Excel打开出现乱码 今天出现一个问题 使用wps打开不会出现乱码.但使用 excel 打开的时候会出现乱码. 其实在我们把文件流转成文件的时候需要在bolb 对象前加上unicode标识, ...

  10. 【JS 逆向百例】XHR 断点调试,Steam 登录逆向

    声明 本文章中所有内容仅供学习交流,抓包内容.敏感网址.数据接口均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关,若有侵权,请联系我立即删除! 逆向目标 目标:Steam ...