expand bios collection, retrobat at 93% coverage

This commit is contained in:
Abdessamad Derraz 2026-03-17 11:48:36 +01:00
parent 851a14e49a
commit e6ea0484a8
3946 changed files with 8119839 additions and 2930936 deletions

View file

@ -0,0 +1,245 @@
local default_text =
{
-- Alphabetic Buttons (NeoGeo): A~D,H,Z
["A"] = 1, -- BTN_A
["B"] = 2, -- BTN_B
["C"] = 3, -- BTN_C
["D"] = 4, -- BTN_D
["H"] = 8, -- BTN_H
["Z"] = 26, -- BTN_Z
-- Numerical Buttons (Capcom): 1~10
["a"] = 27, -- BTN_1
["b"] = 28, -- BTN_2
["c"] = 29, -- BTN_3
["d"] = 30, -- BTN_4
["e"] = 31, -- BTN_5
["f"] = 32, -- BTN_6
["g"] = 33, -- BTN_7
["h"] = 34, -- BTN_8
["i"] = 35, -- BTN_9
["j"] = 36, -- BTN_10
-- Directions of Arrow, Joystick Ball
["+"] = 39, -- BTN_+
["."] = 40, -- DIR_...
["1"] = 41, -- DIR_1
["2"] = 42, -- DIR_2
["3"] = 43, -- DIR_3
["4"] = 44, -- DIR_4
["5"] = 45, -- Joystick Ball
["6"] = 46, -- DIR_6
["7"] = 47, -- DIR_7
["8"] = 48, -- DIR_8
["9"] = 49, -- DIR_9
["N"] = 50, -- DIR_N
-- Special Buttons
["S"] = 51, -- BTN_START
["P"] = 53, -- BTN_PUNCH
["K"] = 54, -- BTN_KICK
["G"] = 55, -- BTN_GUARD
-- Composition of Arrow Directions
["!"] = 90, -- Arrow
["k"] = 100, -- Half Circle Back
["l"] = 101, -- Half Circle Front Up
["m"] = 102, -- Half Circle Front
["n"] = 103, -- Half Circle Back Up
["o"] = 104, -- 1/4 Cir For 2 Down
["p"] = 105, -- 1/4 Cir Down 2 Back
["q"] = 106, -- 1/4 Cir Back 2 Up
["r"] = 107, -- 1/4 Cir Up 2 For
["s"] = 108, -- 1/4 Cir Back 2 Down
["t"] = 109, -- 1/4 Cir Down 2 For
["u"] = 110, -- 1/4 Cir For 2 Up
["v"] = 111, -- 1/4 Cir Up 2 Back
["w"] = 112, -- Full Clock Forward
["x"] = 113, -- Full Clock Back
["y"] = 114, -- Full Count Forward
["z"] = 115, -- Full Count Back
["L"] = 116, -- 2x Forward
["M"] = 117, -- 2x Back
["Q"] = 118, -- Dragon Screw Forward
["R"] = 119, -- Dragon Screw Back
-- Big letter Text
["^"] = 121, -- AIR
["?"] = 122, -- DIR
["X"] = 124, -- TAP
-- Condition of Positions
["|"] = 125, -- Jump
["O"] = 126, -- Hold
["-"] = 127, -- Air
["="] = 128, -- Squatting
["~"] = 131, -- Charge
-- Special Character Text
["`"] = 135, -- Small Dot
["@"] = 136, -- Double Ball
[")"] = 137, -- Single Ball
["("] = 138, -- Solid Ball
["*"] = 139, -- Star
["&"] = 140, -- Solid star
["%"] = 141, -- Triangle
["$"] = 142, -- Solid Triangle
["#"] = 143, -- Double Square
["]"] = 144, -- Single Square
["["] = 145, -- Solid Square
["{"] = 146, -- Down Triangle
["}"] = 147, -- Solid Down Triangle
["<"] = 148, -- Diamond
[">"] = 149, -- Solid Diamond
}
local expand_text =
{
-- Alphabetic Buttons (NeoGeo): S (Slash Button)
["s"] = 19, -- BTN_S
-- Special Buttons
["S"] = 52, -- BTN_SELECT
-- Multiple Punches & Kicks
["E"] = 57, -- Light Punch
["F"] = 58, -- Middle Punch
["G"] = 59, -- Strong Punch
["H"] = 60, -- Light Kick
["I"] = 61, -- Middle Kick
["J"] = 62, -- Strong Kick
["T"] = 63, -- 3 Kick
["U"] = 64, -- 3 Punch
["V"] = 65, -- 2 Kick
["W"] = 66, -- 2 Pick
-- Composition of Arrow Directions
["!"] = 91, -- Continue Arrow
-- Charge of Arrow Directions
["1"] = 92, -- Charge DIR_1
["2"] = 93, -- Charge DIR_2
["3"] = 94, -- Charge DIR_3
["4"] = 95, -- Charge DIR_4
["6"] = 96, -- Charge DIR_6
["7"] = 97, -- Charge DIR_7
["8"] = 98, -- Charge DIR_8
["9"] = 99, -- Charge DIR_9
-- Big letter Text
["M"] = 123, -- MAX
-- Condition of Positions
["-"] = 129, -- Close
["="] = 130, -- Away
["*"] = 132, -- Serious Tap
["?"] = 133, -- Any Button
}
local convert_text =
{
-- Alphabetic Buttons: A~Z
["A-button"] = 1, -- BTN_A
["B-button"] = 2, -- BTN_B
["C-button"] = 3, -- BTN_C
["D-button"] = 4, -- BTN_D
["E-button"] = 5, -- BTN_E
["F-button"] = 6, -- BTN_F
["G-button"] = 7, -- BTN_G
["H-button"] = 8, -- BTN_H
["I-button"] = 9, -- BTN_I
["J-button"] = 10, -- BTN_J
["K-button"] = 11, -- BTN_K
["L-button"] = 12, -- BTN_L
["M-button"] = 13, -- BTN_M
["N-button"] = 14, -- BTN_N
["O-button"] = 15, -- BTN_O
["P-button"] = 16, -- BTN_P
["Q-button"] = 17, -- BTN_Q
["R-button"] = 18, -- BTN_R
["S-button"] = 19, -- BTN_S
["T-button"] = 20, -- BTN_T
["U-button"] = 21, -- BTN_U
["V-button"] = 22, -- BTN_V
["W-button"] = 23, -- BTN_W
["X-button"] = 24, -- BTN_X
["Y-button"] = 25, -- BTN_Y
["Z-button"] = 26, -- BTN_Z
-- Special Moves and Buttons
["decrease"] = 37, -- BTN_DEC
["increase"] = 38, -- BTN_INC
["BALL"] = 45, -- Joystick Ball
["start"] = 51, -- BTN_START
["select"] = 52, -- BTN_SELECT
["punch"] = 53, -- BTN_PUNCH
["kick"] = 54, -- BTN_KICK
["guard"] = 55, -- BTN_GUARD
["L-punch"] = 57, -- Light Punch
["M-punch"] = 58, -- Middle Punch
["S-punch"] = 59, -- Strong Punch
["L-kick"] = 60, -- Light Kick
["M-kick"] = 61, -- Middle Kick
["S-kick"] = 62, -- Strong Kick
["3-kick"] = 63, -- 3 Kick
["3-punch"] = 64, -- 3 Punch
["2-kick"] = 65, -- 2 Kick
["2-punch"] = 66, -- 2 Pick
-- Custom Buttons and Cursor Buttons
["custom1"] = 67, -- CUSTOM_1
["custom2"] = 68, -- CUSTOM_2
["custom3"] = 69, -- CUSTOM_3
["custom4"] = 70, -- CUSTOM_4
["custom5"] = 71, -- CUSTOM_5
["custom6"] = 72, -- CUSTOM_6
["custom7"] = 73, -- CUSTOM_7
["custom8"] = 74, -- CUSTOM_8
["up"] = 75, -- (Cursor Up)
["down"] = 76, -- (Cursor Down)
["left"] = 77, -- (Cursor Left)
["right"] = 78, -- (Cursor Right)
-- Player Lever
["lever"] = 79, -- Non Player Lever
["nplayer"] = 80, -- Gray Color Lever
["1player"] = 81, -- 1 Player Lever
["2player"] = 82, -- 2 Player Lever
["3player"] = 83, -- 3 Player Lever
["4player"] = 84, -- 4 Player Lever
["5player"] = 85, -- 5 Player Lever
["6player"] = 86, -- 6 Player Lever
["7player"] = 87, -- 7 Player Lever
["8player"] = 88, -- 8 Player Lever
-- Composition of Arrow Directions
["-->"] = 90, -- Arrow
["==>"] = 91, -- Continue Arrow
["hcb"] = 100, -- Half Circle Back
["huf"] = 101, -- Half Circle Front Up
["hcf"] = 102, -- Half Circle Front
["hub"] = 103, -- Half Circle Back Up
["qfd"] = 104, -- 1/4 Cir For 2 Down
["qdb"] = 105, -- 1/4 Cir Down 2 Back
["qbu"] = 106, -- 1/4 Cir Back 2 Up
["quf"] = 107, -- 1/4 Cir Up 2 For
["qbd"] = 108, -- 1/4 Cir Back 2 Down
["qdf"] = 109, -- 1/4 Cir Down 2 For
["qfu"] = 110, -- 1/4 Cir For 2 Up
["qub"] = 111, -- 1/4 Cir Up 2 Back
["fdf"] = 112, -- Full Clock Forward
["fub"] = 113, -- Full Clock Back
["fuf"] = 114, -- Full Count Forward
["fdb"] = 115, -- Full Count Back
["xff"] = 116, -- 2x Forward
["xbb"] = 117, -- 2x Back
["dsf"] = 118, -- Dragon Screw Forward
["dsb"] = 119, -- Dragon Screw Back
-- Big letter Text
["AIR"] = 121, -- AIR
["DIR"] = 122, -- DIR
["MAX"] = 123, -- MAX
["TAP"] = 124, -- TAP
-- Condition of Positions
["jump"] = 125, -- Jump
["hold"] = 126, -- Hold
["air"] = 127, -- Air
["sit"] = 128, -- Squatting
["close"] = 129, -- Close
["away"] = 130, -- Away
["charge"] = 131, -- Charge
["tap"] = 132, -- Serious Tap
["button"] = 133, -- Any Button
}
local function convert_char(str)
str = str:gsub("@(%g+)", function(s) if convert_text[s] then return utf8.char(convert_text[s] + 0xe000) end return s end)
str = str:gsub("_(%g)", function(s) if default_text[s] then return utf8.char(default_text[s] + 0xe000) end return s end)
str = str:gsub("%^(%g)", function(s) if expand_text[s] then return utf8.char(expand_text[s] + 0xe000) end return s end)
return str
end
return convert_char

View file

@ -0,0 +1,36 @@
local dat = {}
local info, ver
local datread = require('data/load_dat')
do
local buttonchar
local function convert(str)
if not buttonchar then
buttonchar = require("data/button_char")
end
return buttonchar(str)
end
datread, ver = datread.open('command.dat', '#[^V]*Ver[^.:]*[.:]', convert)
end
function dat.check(set, softlist)
if softlist or not datread then
return nil
end
local status
status, info = pcall(datread, 'cmd', 'info', set)
if not status or not info then
return nil
end
return _p('plugin-data', 'Command')
end
function dat.get()
return info
end
function dat.ver()
return ver
end
return dat

View file

@ -0,0 +1,27 @@
local dat = {}
local ver, info
local datread = require('data/load_dat')
datread, ver = datread.open('gameinit.dat', '# .-GAMEINIT.DAT')
function dat.check(set, softlist)
if softlist or not datread then
return nil
end
local status
status, info = pcall(datread, 'mame', 'info', set)
if not status or not info then
return nil
end
return _p('plugin-data', 'Gameinit')
end
function dat.get()
return info
end
function dat.ver()
return ver
end
return dat

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,218 @@
local dat = {}
local db = require('data/database')
local ver, info
local file = 'history.xml'
local tablename
local function init()
-- check for old history table
if db.get_version('history.dat') then
db.exec([[DROP TABLE "history.dat";]])
db.exec([[DROP TABLE "history.dat_idx";]])
db.set_version('history.dat', nil)
end
local fh, filepath, dbver
fh, filepath, tablename, dbver = db.open_data_file(file)
if not fh then
if dbver then
-- data in database but missing file, just use what we have
ver = dbver
end
return
end
-- scan file for version
for line in fh:lines() do
local match = line:match('<history([^>]*)>')
if match then
match = match:match('version="([^"]*)"')
if match then
ver = match
break
end
end
end
if (not ver) or (ver == dbver) then
fh:close()
ver = dbver
return
end
if not dbver then
db.exec(
string.format(
[[CREATE TABLE "%s_idx" (
name VARCHAR NOT NULL,
list VARCHAR NOT NULL,
data INTEGER NOT NULL);]],
tablename))
db.check(string.format('creating %s index table', file))
db.exec(string.format([[CREATE TABLE "%s" (data CLOB NOT NULL);]], tablename))
db.check(string.format('creating %s data table', file))
db.exec(
string.format(
[[CREATE INDEX "namelist_%s" ON "%s_idx" (name, list);]],
tablename, tablename))
db.check(string.format('creating %s name/list index', file))
end
local slaxml = require('xml')
db.exec([[BEGIN TRANSACTION;]])
if not db.check(string.format('starting %s transaction', file)) then
fh:close()
ver = dbver
return
end
-- clean out previous data and update the version
if dbver then
db.exec(string.format([[DELETE FROM "%s";]], tablename))
if not db.check(string.format('deleting previous %s data', file)) then
db.exec([[ROLLBACK TRANSACTION;]])
fh:close()
ver = dbver
return
end
db.exec(string.format([[DELETE FROM "%s_idx";]], tablename))
if not db.check(string.format('deleting previous %s data', file)) then
db.exec([[ROLLBACK TRANSACTION;]])
fh:close()
ver = dbver
return
end
end
db.set_version(file, ver)
if not db.check(string.format('updating %s version', file)) then
db.exec([[ROLLBACK TRANSACTION;]])
fh:close()
ver = dbver
return
end
fh:seek('set')
local buffer = fh:read('a')
local lasttag
local entry = {}
local rowid
local dataquery = db.prepare(
string.format([[INSERT INTO "%s" (data) VALUES (?);]], tablename))
local indexquery = db.prepare(
string.format([[INSERT INTO "%s_idx" (name, list, data) VALUES (?, ?, ?);]], tablename))
local parser = slaxml:parser{
startElement = function(name)
lasttag = name
if name == 'entry' then
entry = {}
rowid = nil
elseif (name == 'system') or (name == 'item') then
table.insert(entry, {})
end
end,
attribute = function(name, value)
if (name == 'name') or (name == 'list') then
entry[#entry][name] = value
end
end,
text = function(text, cdata)
if lasttag == 'text' then
text = text:gsub('\r', '') -- strip carriage returns
dataquery:bind_values(text)
while true do
local status = dataquery:step()
if status == db.DONE then
rowid = dataquery:last_insert_rowid();
break
elseif result == db.BUSY then
emu.print_error(string.format('Database busy: inserting %s data', file))
-- FIXME: how to abort parse and roll back?
break
elseif result ~= db.ROW then
db.check(string.format('inserting %s data', file))
break
end
end
dataquery:reset()
end
end,
closeElement = function(name)
if (name == 'entry') and rowid then
for num, entry in pairs(entry) do
indexquery:bind_values(entry.name, entry.list or '', rowid)
while true do
local status = indexquery:step()
if status == db.DONE then
break
elseif status == db.BUSY then
emu.print_error(string.format('Database busy: inserting %s data', file))
-- FIXME: how to abort parse and roll back?
break
elseif result ~= db.ROW then
db.check(string.format('inserting %s data', file))
break
end
end
indexquery:reset()
end
end
end
}
parser:parse(buffer, { stripWhitespace = true })
dataquery:finalize()
indexquery:finalize()
fh:close()
db.exec([[COMMIT TRANSACTION;]])
if not db.check(string.format('committing %s transaction', file)) then
db.exec([[ROLLBACK TRANSACTION;]])
ver = dbver
end
end
if db then
init()
end
function dat.check(set, softlist)
if not ver then
return nil
end
info = nil
local query = db.prepare(
string.format(
[[SELECT f.data
FROM "%s_idx" AS fi LEFT JOIN "%s" AS f ON fi.data = f.rowid
WHERE fi.name = ? AND fi.list = ?;]],
tablename, tablename))
query:bind_values(set, softlist or '')
while not info do
local status = query:step()
if status == db.ROW then
info = query:get_value(0)
elseif status == db.DONE then
break
elseif status ~= db.BUSY then
db.check(string.format('reading %s data', file))
break
end
end
query:finalize()
return info and _p('plugin-data', 'History') or nil
end
function dat.get()
return info
end
function dat.ver()
return ver
end
return dat

View file

@ -0,0 +1,38 @@
local dat = {}
local info, ver
local datread = require('data/load_dat')
datread, ver = datread.open(
'mameinfo.dat',
'# MAMEINFO.DAT',
function (str) return str:gsub('\n\n', '\n') end)
function dat.check(set, softlist)
if softlist or not datread then
return nil
end
local status, drvinfo
status, info = pcall(datread, 'mame', 'info', set)
if not status or not info then
return nil
end
local sourcefile = emu.driver_find(set).source_file:match('[^/\\]+[/\\\\][^/\\]*$')
status, drvinfo = pcall(datread, 'drv', 'info', sourcefile)
if not drvinfo then
status, drvinfo = pcall(datread, 'drv', 'info', sourcefile:match('[^/\\]*$'))
end
if drvinfo then
info = info .. _p('plugin-data', '\n\n--- DRIVER INFO ---\nDriver: ') .. sourcefile .. '\n\n' .. drvinfo
end
return _p('plugin-data', 'MAMEinfo')
end
function dat.get()
return info
end
function dat.ver()
return ver
end
return dat

View file

@ -0,0 +1,179 @@
-- get marp high score file from http://replay.marpirc.net/txt/scores3.htm
local dat = {}
local db = require("data/database")
local ver, info
local function init()
local file = 'scores3.htm'
local fh, filepath, tablename, dbver = db.open_data_file(file)
if not fh then
if dbver then
-- data in database but missing file, just use what we have
ver = dbver
end
return
end
-- scan file for version
for line in fh:lines() do
local match = line:match('Top Scores from the MAME Action Replay Page %(([%w :]+)%)')
if match then
ver = match
break
end
end
if (not ver) or (ver == dbver) then
fh:close()
ver = dbver
return
end
if not dbver then
db.exec(
string.format(
[[CREATE TABLE "%s" (
romset VARCHAR NOT NULL,
data CLOB NOT NULL,
PRIMARY KEY(romset));]],
tablename))
db.check('creating MARP table')
end
db.exec([[BEGIN TRANSACTION;]])
if not db.check('starting MARP transaction') then
fh:close()
ver = dbver
return
end
-- clean out previous data and update the version
if dbver then
db.exec(string.format([[DELETE FROM "%s";]], tablename))
if not db.check('deleting previous MARP data') then
db.exec([[ROLLBACK TRANSACTION;]])
fh:close()
ver = dbver
return
end
end
if not db.set_version(file, ver) then
db.check('updating MARP version')
db.exec([[ROLLBACK TRANSACTION;]])
fh:close()
ver = dbver
return
end
fh:seek('set')
local buffer = fh:read('a')
local function gmatchpos()
local pos = 1
local set = ''
local data = ''
local function iter()
local lastset = set
while true do
local spos, scr, plyr, stype, ltype
spos, pos, set, stype, scr, plyr, ltype = buffer:find('\n%s*([%w_]*)%-?(%w-) :%s*(%d+) [;:] ([^:]+): [^%[\n]*%[?([%w ]*)[^\n]*', pos)
if not spos then
return nil
end
local url = ''
if set ~= '' then
if ltype ~= '' then
url = ltype .. '\t\n'
end
url = url .. 'http://replay.marpirc.net/r/' .. set
if stype ~= '' then
url = url .. '-' .. stype
end
url = url .. '\t\n'
end
if (set ~= '') and (lastset ~= set) then
local lastdata = data
data = url .. plyr .. '\t' .. scr .. '\n'
return lastset, lastdata
end
if url ~= '' then
data = data .. '\n' .. url
end
data = data .. plyr .. '\t' .. scr .. '\n'
end
end
return iter
end
local query = db.prepare(
string.format([[INSERT INTO "%s" (romset, data) VALUES (?, ?)]], tablename))
for set, data in gmatchpos() do
query:bind_values(set, data)
while true do
local status = query:step()
if status == db.DONE then
break
elseif status == db.BUSY then
emu.print_error('Database busy: inserting MARP data')
query:finalize()
db.exec([[ROLLBACK TRANSACTION;]])
fh:close()
ver = dbver
return
elseif result ~= db.ROW then
db.check('inserting MARP data')
break
end
end
query:reset()
end
query:finalize()
fh:close()
db.exec([[COMMIT TRANSACTION;]])
if not db.check('committing MARP transaction') then
db.exec([[ROLLBACK TRANSACTION;]])
ver = dbver
end
end
if db then
init()
end
function dat.check(set, softlist)
if softlist or (not ver) then
return nil
end
info = nil
local query = db.prepare([[SELECT data FROM "scores3.htm" WHERE romset = ?;]])
query:bind_values(set)
while not info do
local status = query:step()
if status == db.ROW then
info = '#j2\n' .. query:get_value(0)
elseif status == db.DONE then
break
elseif status ~= db.BUSY then
db.check('reading MARP data')
break
end
end
query:finalize()
return info and _p('plugin-data', 'MARPScore') or nil
end
function dat.get()
return info
end
function dat.ver()
return ver
end
return dat

View file

@ -0,0 +1,38 @@
local dat = {}
local ver, info
local datread = require('data/load_dat')
datread, ver = datread.open(
'messinfo.dat',
'# MESSINFO.DAT',
function (str) return str:gsub('\n\n', '\n') end)
function dat.check(set, softlist)
if softlist or not datread then
return nil
end
local status, drvinfo
status, info = pcall(datread, 'mame', 'info', set)
if not status or not info then
return nil
end
local sourcefile = emu.driver_find(set).source_file:match('[^/\\]+[/\\\\][^/\\]*$')
status, drvinfo = pcall(datread, 'drv', 'info', sourcefile)
if not drvinfo then
status, drvinfo = pcall(datread, 'drv', 'info', sourcefile:match('[^/\\]*$'))
end
if drvinfo then
info = info .. _p('plugin-data', '\n\n--- DRIVER INFO ---\nDriver: ') .. sourcefile .. '\n\n' .. drvinfo
end
return _p('plugin-data', 'MESSinfo')
end
function dat.get()
return info
end
function dat.ver()
return ver
end
return dat

View file

@ -0,0 +1,35 @@
local dat = {}
local ver, info
local datread = require('data/load_dat')
datread, ver = datread.open('story.dat', '# version')
function dat.check(set, softlist)
if softlist or not datread then
return nil
end
local status, data = pcall(datread, 'story', 'info', set)
if not status or not data then
return nil
end
local lines = {}
data = data:gsub('MAMESCORE records : ([^\n]+)', 'MAMESCORE records :\t\n%1', 1)
for line in data:gmatch('[^\n]*') do
if (line ~= '') or ((#lines ~= 0) and (lines[#lines] ~= '')) then
line = line:gsub('^(.-)_+([0-9.]+)$', '%1\t%2')
table.insert(lines, line)
end
end
info = '#j2\n' .. table.concat(lines, '\n')
return _p('plugin-data', 'Mamescore')
end
function dat.get()
return info
end
function dat.ver()
return ver
end
return dat

View file

@ -0,0 +1,27 @@
local dat = {}
local ver, info
local datread = require('data/load_dat')
datread, ver = datread.open('sysinfo.dat', '# This file was generated on')
function dat.check(set, softlist)
if softlist or not datread then
return nil
end
local status
status, info = pcall(datread, 'bio', 'info', set)
if not status or not info then
return nil
end
return _p('plugin-data', 'Sysinfo')
end
function dat.get()
return info
end
function dat.ver()
return ver
end
return dat

View file

@ -0,0 +1,159 @@
local sql = require('lsqlite3')
local db
local function check_db_result(msg)
if db:errcode() > sql.OK then
emu.print_error(string.format('Error: %s (%s - %s)', msg, db:errcode(), db:errmsg()))
return false
end
return true
end
local function settings_path()
return manager.machine.options.entries.homepath:value():match('([^;]+)') .. '/data'
end
local function check_version_table()
local found = false
db:exec(
[[SELECT COUNT(*) FROM sqlite_master WHERE name = 'version' AND type = 'table';]],
function (udata, cols, values, names)
found = tonumber(values[1]) > 0
return 0
end)
if check_db_result('checking for "version" table') and (not found) then
db:exec(
[[CREATE TABLE version (
datfile VARCHAR NOT NULL,
version VARCHAR NOT NULL,
PRIMARY KEY (datfile));]])
found = check_db_result('creating "version" table')
end
if not found then
db:close()
db = nil
end
end
local function open_existing()
db = sql.open(settings_path() .. '/history.db', sql.OPEN_READWRITE)
if db then
check_version_table()
end
return db
end
local function open_create()
-- first try to create settings directory
local dir = settings_path()
local attr = lfs.attributes(dir)
if (not attr) and (not lfs.mkdir(dir)) then
emu.print_error(string.format('Error creating data plugin settings directory "%s"', dir))
elseif attr and (attr.mode ~= 'directory') then
emu.print_error(string.format('Error opening data plugin database: "%s" is not a directory', dir))
else
-- now try to open the database
local dbpath = dir .. '/history.db'
db = sql.open(dbpath, sql.OPEN_READWRITE | sql.OPEN_CREATE)
if not db then
emu.print_error(string.format('Error opening data plugin database "%s"', dbpath))
else
check_version_table()
end
end
return db
end
local dbtable = {
ROW = sql.ROW,
BUSY = sql.BUSY,
DONE = sql.DONE,
check = check_db_result }
function dbtable.sanitize_name(name)
return name:gsub('[^%w%."]', '_')
end
function dbtable.get_version(filename)
-- don't try to create the database here, just return nil if it doesn't exist
if (not db) and (not open_existing()) then
return nil
end
local query = db:prepare([[SELECT version FROM version WHERE datfile = ?;]])
query:bind_values(filename)
local result
while result == nil do
local status = query:step()
if status == sql.ROW then
result = query:get_value(0)
elseif status ~= sql.BUSY then
break
end
end
query:finalize()
return result
end
function dbtable.set_version(filename, version)
if (not db) and (not open_create()) then
return nil
end
local query
if version ~= nil then
query = db:prepare(
[[INSERT INTO version (datfile, version) VALUES (?1, ?2)
ON CONFLICT(datfile) DO UPDATE set version = ?2;]])
query:bind_values(filename, version)
else
query = db:prepare([[DELETE FROM version WHERE datfile = ?1;]])
query:bind_values(filename)
end
local result
while result == nil do
local status = query:step()
if status == sql.DONE then
result = true
elseif result ~= sql.ROW then
result = false
end
end
query:finalize()
return result
end
function dbtable.prepare(...)
if (not db) and open_create() then
dbtable.prepare = function (...) return db:prepare(...) end
end
if db then
return db:prepare(...)
else
return nil
end
end
function dbtable.exec(...)
if (not db) and open_create() then
dbtable.exec = function (...) return db:exec(...) end
end
if db then
return db:exec(...)
else
return nil
end
end
function dbtable.open_data_file(file)
local fh, filepath
for path in mame_manager.ui.options.entries.historypath:value():gmatch('([^;]+)') do
filepath = path .. '/' .. file
fh = io.open(filepath, 'r')
if fh then
break
end
end
return fh, filepath, dbtable.sanitize_name(file), dbtable.get_version(file)
end
return dbtable

View file

@ -0,0 +1,91 @@
-- license:BSD-3-Clause
-- copyright-holders:Carl
-- A data script should contain two functions check which takes a set name and returns the data
-- heading if it supports the set otherwise nil and get which returns the data
-- the script should be named data_<name>.lua
-- this is set default on in the plugin.json
local exports = {
name = 'data',
version = '0.0.2',
description = 'Data plugin',
license = 'BSD-3-Clause',
author = { name = 'Carl' } }
local data = exports
local plugindir
local reset_subscription
function data.set_folder(path)
plugindir = path
end
function data.startplugin()
local data_scr = {}
local valid_lst = {}
local cur_set
local cur_list
reset_subscription = emu.add_machine_reset_notifier(
function ()
data_scr = {}
for file in lfs.dir(plugindir) do
local name = string.match(file, '^(data_.*).lua$')
if name then
local script = require('data/' .. name)
if script then
table.insert(data_scr, script)
end
end
end
end)
emu.register_callback(
function (set)
local ret = {}
if set == '' then
set = emu.romname()
end
if set == cur_set then
return cur_list
end
cur_set = set
if not set then
return nil
end
valid_lst = {}
for num, scr in ipairs(data_scr) do
local setname, softname = set:match('^([^,]+),?(.*)$')
if softname == '' then
softname = nil
end
local name = scr.check(setname, softname)
if name then
table.insert(ret, name)
table.insert(valid_lst, scr)
end
end
cur_list = ret
return ret
end,
'data_list')
emu.register_callback(
function (num)
return valid_lst[num + 1].get()
end,
'data')
emu.register_callback(
function (num)
local ver
if valid_lst[num + 1].ver then
ver = valid_lst[num + 1].ver()
end
return ver or ''
end,
'data_version')
end
return exports

View file

@ -0,0 +1,260 @@
local datfile = {}
local db = require('data/database')
local function readret(file, tablename)
local query = db.prepare(
string.format(
[[SELECT f.data
FROM "%s_idx" AS fi LEFT JOIN "%s" AS f ON fi.data = f.rowid
WHERE fi.type = ? AND fi.val = ? AND fi.romset = ?;]],
tablename, tablename))
local function read(tag, val, set)
query:bind_values(tag, val, set)
local data
while not data do
local status = query:step()
if status == db.ROW then
data = query:get_value(0)
elseif status == db.DONE then
break
elseif status ~= db.BUSY then
db.check(string.format('reading %s data', file))
break
end
end
query:reset()
return data
end
return read
end
function datfile.open(file, vertag, fixupcb)
if not db then
return nil
end
local fh, filepath, tablename, dbver = db.open_data_file(file)
if not fh then
if dbver then
-- data in database but missing file, just use what we have
return readret(file, tablename), dbver
else
return nil
end
end
local ver
if vertag then
-- scan file for version
for line in fh:lines() do
local match = line:match(vertag .. '%s*(%S+)')
if match then
ver = match
break
end
end
end
if not ver then
-- fall back to file modification time for version
ver = tostring(lfs.attributes(filepath, 'change'))
end
if ver == dbver then
fh:close()
return readret(file, tablename), dbver
end
if not dbver then
db.exec(
string.format(
[[CREATE TABLE "%s_idx" (
type VARCHAR NOT NULL,
val VARCHAR NOT NULL,
romset VARCHAR NOT NULL,
data INTEGER NOT NULL);]],
tablename))
db.check(string.format('creating %s index table', file))
db.exec(string.format([[CREATE TABLE "%s" (data CLOB NOT NULL);]], tablename))
db.check(string.format('creating %s data table', file))
db.exec(
string.format(
[[CREATE INDEX "typeval_%s" ON "%s_idx" (type, val, romset);]],
tablename, tablename))
db.check(string.format('creating %s type/value index', file))
end
db.exec([[BEGIN TRANSACTION;]])
if not db.check(string.format('starting %s transaction', file)) then
fh:close()
if dbver then
return readret(file, tablename), dbver
else
return nil
end
end
-- clean out previous data and update the version
if dbver then
db.exec(string.format([[DELETE FROM "%s";]], tablename))
if not db.check(string.format('deleting previous %s data', file)) then
db.exec([[ROLLBACK TRANSACTION;]])
fh:close()
return readret(file, tablename), dbver
end
db.exec(string.format([[DELETE FROM "%s_idx";]], tablename))
if not db.check(string.format('deleting previous %s data', file)) then
db.exec([[ROLLBACK TRANSACTION;]])
fh:close()
return readret(file, tablename), dbver
end
end
db.set_version(file, ver)
if not db.check(string.format('updating %s version', file)) then
db.exec([[ROLLBACK TRANSACTION;]])
fh:close()
if dbver then
return readret(file, tablename), dbver
else
return nil
end
end
local dataquery = db.prepare(
string.format([[INSERT INTO "%s" (data) VALUES (?);]], tablename))
local indexquery = db.prepare(
string.format(
[[INSERT INTO "%s_idx" (type, val, romset, data) VALUES (?, ?, ?, ?)]],
tablename))
fh:seek('set')
local buffer = fh:read('a')
local function gmatchpos()
local pos = 1
local function iter()
local tags, data
while not data do
local npos
local spos, epos = buffer:find('[\n\r]$[^=\n\r]*=[^\n\r]*', pos)
if not spos then
return nil
end
npos, epos = buffer:find('[\n\r]$%w+%s*[\n\r]+', epos)
if not npos then
return nil
end
tags = buffer:sub(spos, epos)
spos, npos = buffer:find('[\n\r]$[^=\n\r]*=[^\n\r]*', epos)
if not spos then
return nil
end
data = buffer:sub(epos, spos)
pos = spos
end
return tags, data
end
return iter
end
for info, data in gmatchpos() do
local tags = {}
local infotype
info = info:gsub(utf8.char(0xfeff), '') -- remove byte order marks
data = data:gsub(utf8.char(0xfeff), '')
for s in info:gmatch('[\n\r]$([^\n\r]*)') do
if s:find('=', 1, true) then
local m1, m2 = s:match('([^=]*)=(.*)')
for tag in m1:gmatch('[^,]+') do
for set in m2:gmatch('[^,]+') do
table.insert(tags, { tag = tag, set = set })
end
end
else
infotype = s
break
end
end
data = data:gsub('[\n\r]$end%s*[\n\r]$%w+%s*[\n\r]', '\n')
data = data:gsub('[\n\r]$end%s*[\n\r].-[\n\r]$%w+%s*[\n\r]', '\n')
data = data:gsub('[\n\r]$end%s*[\n\r].*', '')
if (#tags > 0) and infotype then
data = data:gsub('\r', '') -- strip carriage returns
if fixupcb then
data = fixupcb(data)
end
dataquery:bind_values(data)
local row
while true do
local status = dataquery:step()
if status == db.DONE then
row = dataquery:last_insert_rowid();
break
elseif status == db.BUSY then
emu.print_error(string.format('Database busy: inserting %s data', file))
dataquery:finalize()
indexquery:finalize()
db.exec([[ROLLBACK TRANSACTION;]])
fh:close()
if dbver then
return readret(file, tablename), dbver
else
return nil
end
elseif result ~= db.ROW then
db.check(string.format('inserting %s data', file))
break
end
end
dataquery:reset()
if row then
for num, tag in pairs(tags) do
indexquery:bind_values(infotype, tag.tag, tag.set, row)
while true do
local status = indexquery:step()
if status == db.DONE then
break
elseif status == db.BUSY then
emu.print_error(string.format('Database busy: inserting %s data', file))
dataquery:finalize()
indexquery:finalize()
db.exec([[ROLLBACK TRANSACTION;]])
fh:close()
if dbver then
return readret(file, tablename), dbver
else
return nil
end
elseif result ~= db.ROW then
db.check(string.format('inserting %s data', file))
break
end
end
indexquery:reset()
end
end
end
end
dataquery:finalize()
indexquery:finalize()
fh:close()
db.exec([[COMMIT TRANSACTION;]])
if not db.check(string.format('committing %s transaction', file)) then
db.exec([[ROLLBACK TRANSACTION;]])
if dbver then
return readret(file, tablename), dbver
else
return nil
end
end
return readret(file, tablename), ver
end
return datfile

View file

@ -0,0 +1,10 @@
{
"plugin": {
"name": "data",
"description": "Data plugin",
"version": "0.0.1",
"author": "Carl",
"type": "plugin",
"start": "true"
}
}