mirror of
https://github.com/Abdess/retrobios.git
synced 2026-06-28 05:22:47 +00:00
expand bios collection, retrobat at 93% coverage
This commit is contained in:
parent
851a14e49a
commit
e6ea0484a8
3946 changed files with 8119839 additions and 2930936 deletions
3
bios/Arcade/MAME/plugins/README.md
Normal file
3
bios/Arcade/MAME/plugins/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# **Plugins** #
|
||||
|
||||
LUA plugins contains code from various sources so license is per file.
|
||||
387
bios/Arcade/MAME/plugins/autofire/autofire_menu.lua
Normal file
387
bios/Arcade/MAME/plugins/autofire/autofire_menu.lua
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
local lib = {}
|
||||
|
||||
-- Common UI helper library
|
||||
local commonui
|
||||
|
||||
-- Set of all menus
|
||||
local MENU_TYPES = { MAIN = 0, EDIT = 1, ADD = 2, BUTTON = 3 }
|
||||
|
||||
-- Set of sections within a menu
|
||||
local MENU_SECTIONS = { HEADER = 0, CONTENT = 1, FOOTER = 2 }
|
||||
|
||||
-- Last index of header items (above main content) in menu
|
||||
local header_height = 0
|
||||
|
||||
-- Last index of content items (below header, above footer) in menu
|
||||
local content_height = 0
|
||||
|
||||
-- Stack of menus (see MENU_TYPES)
|
||||
local menu_stack = { MENU_TYPES.MAIN }
|
||||
|
||||
-- Button to select when showing the main menu (so newly added button can be selected)
|
||||
local initial_button
|
||||
|
||||
-- Saved selection on main menu (to restore after configure menu is dismissed)
|
||||
local main_selection_save
|
||||
|
||||
-- Whether configure menu is active (so first item can be selected initially)
|
||||
local configure_menu_active = false
|
||||
|
||||
-- Saved selection on configure menu (to restore after button menu is dismissed)
|
||||
local configure_selection_save
|
||||
|
||||
-- Helper for polling for hotkeys
|
||||
local hotkey_poller
|
||||
|
||||
-- Button being created/edited
|
||||
local current_button = {}
|
||||
|
||||
-- Initial button to select when opening buttons menu
|
||||
local initial_input
|
||||
|
||||
-- Handler for BUTTON menu
|
||||
local input_menu
|
||||
|
||||
-- Returns the section (from MENU_SECTIONS) and the index within that section
|
||||
local function menu_section(index)
|
||||
if index <= header_height then
|
||||
return MENU_SECTIONS.HEADER, index
|
||||
elseif index <= content_height then
|
||||
return MENU_SECTIONS.CONTENT, index - header_height
|
||||
else
|
||||
return MENU_SECTIONS.FOOTER, index - content_height
|
||||
end
|
||||
end
|
||||
|
||||
local function create_new_button()
|
||||
return {
|
||||
on_frames = 1,
|
||||
off_frames = 1,
|
||||
counter = 0
|
||||
}
|
||||
end
|
||||
|
||||
local function is_button_complete(button)
|
||||
return button.port and button.mask and button.type and button.key and button.on_frames and button.off_frames and button.button and button.counter
|
||||
end
|
||||
|
||||
-- Main menu
|
||||
|
||||
local function populate_main_menu(buttons)
|
||||
local ioport = manager.machine.ioport
|
||||
local input = manager.machine.input
|
||||
local menu = {}
|
||||
table.insert(menu, {_p('plugin-autofire', 'Autofire buttons'), '', 'off'})
|
||||
table.insert(menu, {string.format(_p('plugin-autofire', 'Press %s to delete'), manager.ui:get_general_input_setting(ioport:token_to_input_type('UI_CLEAR'))), '', 'off'})
|
||||
table.insert(menu, {'---', '', ''})
|
||||
header_height = #menu
|
||||
|
||||
-- Use frame rate of first screen or 60Hz if no screens
|
||||
local freq = 60
|
||||
local screen = manager.machine.screens:at(1)
|
||||
if screen then
|
||||
freq = 1 / screen.frame_period
|
||||
end
|
||||
|
||||
if #buttons > 0 then
|
||||
for index, button in ipairs(buttons) do
|
||||
-- Round rate to two decimal places
|
||||
local rate = freq / (button.on_frames + button.off_frames)
|
||||
rate = math.floor(rate * 100) / 100
|
||||
local text
|
||||
if button.button then
|
||||
text = string.format(_p('plugin-autofire', '%s [%g Hz]'), button.button.name, rate)
|
||||
else
|
||||
text = string.format(_p('plugin-autofire', 'n/a [%g Hz]'), rate)
|
||||
end
|
||||
table.insert(menu, {text, input:seq_name(button.key), ''})
|
||||
if index == initial_button then
|
||||
main_selection_save = #menu
|
||||
end
|
||||
end
|
||||
else
|
||||
table.insert(menu, {_p('plugin-autofire', '[no autofire buttons]'), '', 'off'})
|
||||
end
|
||||
initial_button = nil
|
||||
content_height = #menu
|
||||
|
||||
table.insert(menu, {'---', '', ''})
|
||||
table.insert(menu, {_p('plugin-autofire', 'Add autofire button'), '', ''})
|
||||
|
||||
local selection = main_selection_save
|
||||
main_selection_save = nil
|
||||
return menu, selection
|
||||
end
|
||||
|
||||
local function handle_main_menu(index, event, buttons)
|
||||
local section, adjusted_index = menu_section(index)
|
||||
if section == MENU_SECTIONS.CONTENT then
|
||||
if event == 'select' then
|
||||
initial_button = adjusted_index
|
||||
main_selection_save = index
|
||||
current_button = buttons[adjusted_index]
|
||||
table.insert(menu_stack, MENU_TYPES.EDIT)
|
||||
return true
|
||||
elseif event == 'clear' then
|
||||
table.remove(buttons, adjusted_index)
|
||||
main_selection_save = index
|
||||
if adjusted_index > #buttons then
|
||||
main_selection_save = main_selection_save - 1
|
||||
end
|
||||
return true
|
||||
end
|
||||
elseif section == MENU_SECTIONS.FOOTER then
|
||||
if event == 'select' then
|
||||
main_selection_save = index
|
||||
current_button = create_new_button()
|
||||
table.insert(menu_stack, MENU_TYPES.ADD)
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Add/edit menus (mostly identical)
|
||||
|
||||
local function populate_configure_menu(menu)
|
||||
local button_name
|
||||
if current_button.button then
|
||||
button_name = current_button.button.name
|
||||
elseif current_button.port then
|
||||
button_name = _p('plugin-autofire', 'n/a')
|
||||
else
|
||||
button_name = _p('plugin-autofire', '[not set]')
|
||||
end
|
||||
local key_name = current_button.key and manager.machine.input:seq_name(current_button.key) or _p('plugin-autofire', '[not set]')
|
||||
table.insert(menu, {_p('plugin-autofire', 'Input'), button_name, ''})
|
||||
if not (configure_menu_active or configure_selection_save) then
|
||||
configure_selection_save = #menu
|
||||
end
|
||||
table.insert(menu, {_p('plugin-autofire', 'Hotkey'), key_name, hotkey_poller and 'lr' or ''})
|
||||
table.insert(menu, {_p('plugin-autofire', 'On frames'), tostring(current_button.on_frames), current_button.on_frames > 1 and 'lr' or 'r'})
|
||||
table.insert(menu, {_p('plugin-autofire', 'Off frames'), tostring(current_button.off_frames), current_button.off_frames > 1 and 'lr' or 'r'})
|
||||
configure_menu_active = true
|
||||
end
|
||||
|
||||
local function handle_configure_menu(index, event)
|
||||
if hotkey_poller then
|
||||
-- special handling for polling for hotkey
|
||||
if hotkey_poller:poll() then
|
||||
if hotkey_poller.sequence then
|
||||
current_button.key = hotkey_poller.sequence
|
||||
current_button.key_cfg = manager.machine.input:seq_to_tokens(hotkey_poller.sequence)
|
||||
end
|
||||
hotkey_poller = nil
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
if index == 1 then
|
||||
-- Input
|
||||
if event == 'select' then
|
||||
configure_selection_save = header_height + index
|
||||
table.insert(menu_stack, MENU_TYPES.BUTTON)
|
||||
if current_button.port and current_button.button then
|
||||
initial_input = current_button.button
|
||||
end
|
||||
return true
|
||||
end
|
||||
elseif index == 2 then
|
||||
-- Hotkey
|
||||
if event == 'select' then
|
||||
if not commonui then
|
||||
commonui = require('commonui')
|
||||
end
|
||||
hotkey_poller = commonui.switch_polling_helper()
|
||||
return true
|
||||
end
|
||||
elseif index == 3 then
|
||||
-- On frames
|
||||
manager.machine:popmessage(_p('plugin-autofire', 'Number of frames button will be pressed'))
|
||||
if event == 'left' then
|
||||
current_button.on_frames = current_button.on_frames - 1
|
||||
return true
|
||||
elseif event == 'right' then
|
||||
current_button.on_frames = current_button.on_frames + 1
|
||||
return true
|
||||
elseif event == 'clear' then
|
||||
current_button.on_frames = 1
|
||||
return true
|
||||
end
|
||||
elseif index == 4 then
|
||||
-- Off frames
|
||||
manager.machine:popmessage(_p('plugin-autofire', 'Number of frames button will be released'))
|
||||
if event == 'left' then
|
||||
current_button.off_frames = current_button.off_frames - 1
|
||||
return true
|
||||
elseif event == 'right' then
|
||||
current_button.off_frames = current_button.off_frames + 1
|
||||
return true
|
||||
elseif event == 'clear' then
|
||||
current_button.off_frames = 1
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function populate_edit_menu()
|
||||
local menu = {}
|
||||
table.insert(menu, {_p('plugin-autofire', 'Edit autofire button'), '', 'off'})
|
||||
table.insert(menu, {'---', '', ''})
|
||||
header_height = #menu
|
||||
|
||||
populate_configure_menu(menu)
|
||||
content_height = #menu
|
||||
|
||||
table.insert(menu, {'---', '', ''})
|
||||
table.insert(menu, {_p('plugin-autofire', 'Delete'), '', ''})
|
||||
|
||||
table.insert(menu, {'---', '', ''})
|
||||
table.insert(menu, {_p('plugin-autofire', 'Done'), '', ''})
|
||||
|
||||
local selection = configure_selection_save
|
||||
configure_selection_save = nil
|
||||
if hotkey_poller then
|
||||
return hotkey_poller:overlay(menu, selection, 'lrrepeat')
|
||||
else
|
||||
return menu, selection, 'lrrepeat'
|
||||
end
|
||||
end
|
||||
|
||||
local function handle_edit_menu(index, event, buttons)
|
||||
local section, adjusted_index = menu_section(index)
|
||||
if (section == MENU_SECTIONS.FOOTER) and (adjusted_index == 2) and (event == 'select') then
|
||||
table.remove(buttons, initial_button)
|
||||
if initial_button > #buttons then
|
||||
main_selection_save = main_selection_save - 1
|
||||
end
|
||||
initial_button = nil
|
||||
configure_menu_active = false
|
||||
table.remove(menu_stack)
|
||||
return true
|
||||
elseif ((section == MENU_SECTIONS.FOOTER) and (event == 'select')) or (event == 'back') then
|
||||
configure_menu_active = false
|
||||
initial_button = nil
|
||||
table.remove(menu_stack)
|
||||
return true
|
||||
elseif section == MENU_SECTIONS.CONTENT then
|
||||
return handle_configure_menu(adjusted_index, event)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function populate_add_menu()
|
||||
local menu = {}
|
||||
table.insert(menu, {_p('plugin-autofire', 'Add autofire button'), '', 'off'})
|
||||
table.insert(menu, {'---', '', ''})
|
||||
header_height = #menu
|
||||
|
||||
populate_configure_menu(menu)
|
||||
content_height = #menu
|
||||
|
||||
table.insert(menu, {'---', '', ''})
|
||||
if is_button_complete(current_button) then
|
||||
table.insert(menu, {_p('plugin-autofire', 'Create'), '', ''})
|
||||
else
|
||||
table.insert(menu, {_p('plugin-autofire', 'Cancel'), '', ''})
|
||||
end
|
||||
|
||||
local selection = configure_selection_save
|
||||
configure_selection_save = nil
|
||||
if hotkey_poller then
|
||||
return hotkey_poller:overlay(menu, selection, 'lrrepeat')
|
||||
else
|
||||
return menu, selection, 'lrrepeat'
|
||||
end
|
||||
end
|
||||
|
||||
local function handle_add_menu(index, event, buttons)
|
||||
local section, adjusted_index = menu_section(index)
|
||||
if ((section == MENU_SECTIONS.FOOTER) and (event == 'select')) or (event == 'back') then
|
||||
configure_menu_active = false
|
||||
table.remove(menu_stack)
|
||||
if is_button_complete(current_button) and (event == 'select') then
|
||||
table.insert(buttons, current_button)
|
||||
initial_button = #buttons
|
||||
end
|
||||
return true
|
||||
elseif section == MENU_SECTIONS.CONTENT then
|
||||
return handle_configure_menu(adjusted_index, event)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Button selection menu
|
||||
|
||||
local function populate_button_menu()
|
||||
local function is_supported_input(ioport_field)
|
||||
if ioport_field.is_analog or ioport_field.is_toggle then
|
||||
return false
|
||||
elseif (ioport_field.type_class == 'config') or (ioport_field.type_class == 'dipswitch') then
|
||||
return false
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local function action(field)
|
||||
if field then
|
||||
current_button.port = field.port.tag
|
||||
current_button.mask = field.mask
|
||||
current_button.type = field.type
|
||||
current_button.button = field
|
||||
end
|
||||
initial_input = nil
|
||||
input_menu = nil
|
||||
table.remove(menu_stack)
|
||||
end
|
||||
|
||||
if not commonui then
|
||||
commonui = require('commonui')
|
||||
end
|
||||
input_menu = commonui.input_selection_menu(action, _p('plugin-autofire', 'Select an input for autofire'), is_supported_input)
|
||||
return input_menu:populate(initial_input)
|
||||
end
|
||||
|
||||
local function handle_button_menu(index, event)
|
||||
return input_menu:handle(index, event)
|
||||
end
|
||||
|
||||
function lib:init_menu(buttons)
|
||||
header_height = 0
|
||||
content_height = 0
|
||||
menu_stack = { MENU_TYPES.MAIN }
|
||||
current_button = {}
|
||||
input_menu = nil
|
||||
end
|
||||
|
||||
function lib:populate_menu(buttons)
|
||||
local current_menu = menu_stack[#menu_stack]
|
||||
if current_menu == MENU_TYPES.MAIN then
|
||||
return populate_main_menu(buttons)
|
||||
elseif current_menu == MENU_TYPES.EDIT then
|
||||
return populate_edit_menu()
|
||||
elseif current_menu == MENU_TYPES.ADD then
|
||||
return populate_add_menu()
|
||||
elseif current_menu == MENU_TYPES.BUTTON then
|
||||
return populate_button_menu()
|
||||
end
|
||||
end
|
||||
|
||||
function lib:handle_menu_event(index, event, buttons)
|
||||
manager.machine:popmessage()
|
||||
local current_menu = menu_stack[#menu_stack]
|
||||
if current_menu == MENU_TYPES.MAIN then
|
||||
return handle_main_menu(index, event, buttons)
|
||||
elseif current_menu == MENU_TYPES.EDIT then
|
||||
return handle_edit_menu(index, event, buttons)
|
||||
elseif current_menu == MENU_TYPES.ADD then
|
||||
return handle_add_menu(index, event, buttons)
|
||||
elseif current_menu == MENU_TYPES.BUTTON then
|
||||
return handle_button_menu(index, event)
|
||||
end
|
||||
end
|
||||
|
||||
return lib
|
||||
101
bios/Arcade/MAME/plugins/autofire/autofire_save.lua
Normal file
101
bios/Arcade/MAME/plugins/autofire/autofire_save.lua
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
local lib = {}
|
||||
|
||||
local function get_settings_path()
|
||||
return manager.machine.options.entries.homepath:value():match('([^;]+)') .. '/autofire'
|
||||
end
|
||||
|
||||
local function get_settings_filename()
|
||||
return emu.romname() .. '.cfg'
|
||||
end
|
||||
|
||||
local function initialize_button(settings)
|
||||
if settings.port and settings.mask and settings.type and settings.key and settings.on_frames and settings.off_frames then
|
||||
local ioport = manager.machine.ioport
|
||||
local new_button = {
|
||||
port = settings.port,
|
||||
mask = settings.mask,
|
||||
type = ioport:token_to_input_type(settings.type),
|
||||
key = manager.machine.input:seq_from_tokens(settings.key),
|
||||
key_cfg = settings.key,
|
||||
on_frames = settings.on_frames,
|
||||
off_frames = settings.off_frames,
|
||||
counter = 0
|
||||
}
|
||||
local port = ioport.ports[settings.port]
|
||||
if port then
|
||||
local field = port:field(settings.mask)
|
||||
if field and (field.type == new_button.type) then
|
||||
new_button.button = field
|
||||
end
|
||||
end
|
||||
return new_button
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function serialize_settings(button_list)
|
||||
local settings = {}
|
||||
for index, button in ipairs(button_list) do
|
||||
local setting = {
|
||||
port = button.port,
|
||||
mask = button.mask,
|
||||
type = manager.machine.ioport:input_type_to_token(button.type),
|
||||
key = button.key_cfg,
|
||||
on_frames = button.on_frames,
|
||||
off_frames = button.off_frames
|
||||
}
|
||||
table.insert(settings, setting)
|
||||
end
|
||||
return settings
|
||||
end
|
||||
|
||||
function lib:load_settings()
|
||||
local buttons = {}
|
||||
local json = require('json')
|
||||
local filename = get_settings_path() .. '/' .. get_settings_filename()
|
||||
local file = io.open(filename, 'r')
|
||||
if not file then
|
||||
return buttons
|
||||
end
|
||||
local loaded_settings = json.parse(file:read('a'))
|
||||
file:close()
|
||||
if not loaded_settings then
|
||||
emu.print_error(string.format('Error loading autofire settings: error parsing file "%s" as JSON', filename))
|
||||
return buttons
|
||||
end
|
||||
for index, button_settings in ipairs(loaded_settings) do
|
||||
local new_button = initialize_button(button_settings)
|
||||
if new_button then
|
||||
buttons[#buttons + 1] = new_button
|
||||
end
|
||||
end
|
||||
return buttons
|
||||
end
|
||||
|
||||
function lib:save_settings(buttons)
|
||||
local path = get_settings_path()
|
||||
local attr = lfs.attributes(path)
|
||||
if attr and (attr.mode ~= 'directory') then
|
||||
emu.print_error(string.format('Error saving autofire settings: "%s" is not a directory', path))
|
||||
return
|
||||
end
|
||||
local filename = path .. '/' .. get_settings_filename()
|
||||
if #buttons == 0 then
|
||||
os.remove(filename)
|
||||
return
|
||||
elseif not attr then
|
||||
lfs.mkdir(path)
|
||||
end
|
||||
local json = require('json')
|
||||
local settings = serialize_settings(buttons)
|
||||
local data = json.stringify(settings, {indent = true})
|
||||
local file = io.open(filename, 'w')
|
||||
if not file then
|
||||
emu.print_error(string.format('Error saving autofire settings: error opening file "%s" for writing', filename))
|
||||
return
|
||||
end
|
||||
file:write(data)
|
||||
file:close()
|
||||
end
|
||||
|
||||
return lib
|
||||
115
bios/Arcade/MAME/plugins/autofire/init.lua
Normal file
115
bios/Arcade/MAME/plugins/autofire/init.lua
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
-- license:BSD-3-Clause
|
||||
-- copyright-holders:Jack Li
|
||||
local exports = {
|
||||
name = 'autofire',
|
||||
version = '0.0.4',
|
||||
description = 'Autofire plugin',
|
||||
license = 'BSD-3-Clause',
|
||||
author = { name = 'Jack Li' } }
|
||||
|
||||
local autofire = exports
|
||||
|
||||
local frame_subscription, stop_subscription
|
||||
|
||||
function autofire.startplugin()
|
||||
|
||||
-- List of autofire buttons, each being a table with keys:
|
||||
-- 'port' - port name of the button being autofired
|
||||
-- 'mask' - mask of the button field being autofired
|
||||
-- 'type' - input type of the button being autofired
|
||||
-- 'key' - input_seq of the keybinding
|
||||
-- 'key_cfg' - configuration string for the keybinding
|
||||
-- 'on_frames' - number of frames button is pressed
|
||||
-- 'off_frames' - number of frames button is released
|
||||
-- 'button' - reference to ioport_field
|
||||
-- 'counter' - position in autofire cycle
|
||||
local buttons = {}
|
||||
|
||||
local input_manager
|
||||
local menu_handler
|
||||
|
||||
local function process_frame()
|
||||
local function process_button(button)
|
||||
local pressed = input_manager:seq_pressed(button.key)
|
||||
if pressed then
|
||||
local state = button.counter < button.on_frames and 1 or 0
|
||||
button.counter = (button.counter + 1) % (button.on_frames + button.off_frames)
|
||||
return state
|
||||
else
|
||||
button.counter = 0
|
||||
return 0
|
||||
end
|
||||
end
|
||||
|
||||
-- Resolves conflicts between multiple autofire keybindings for the same button.
|
||||
local button_states = {}
|
||||
|
||||
for i, button in ipairs(buttons) do
|
||||
if button.button then
|
||||
local key = button.port .. '\0' .. button.mask .. '.' .. button.type
|
||||
local state = button_states[key] or {0, button.button}
|
||||
state[1] = process_button(button) | state[1]
|
||||
button_states[key] = state
|
||||
end
|
||||
end
|
||||
for i, state in pairs(button_states) do
|
||||
if state[1] ~= 0 then
|
||||
state[2]:set_value(state[1])
|
||||
else
|
||||
state[2]:clear_value()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function load_settings()
|
||||
local loader = require('autofire/autofire_save')
|
||||
if loader then
|
||||
buttons = loader:load_settings()
|
||||
end
|
||||
|
||||
input_manager = manager.machine.input
|
||||
end
|
||||
|
||||
local function save_settings()
|
||||
local saver = require('autofire/autofire_save')
|
||||
if saver then
|
||||
saver:save_settings(buttons)
|
||||
end
|
||||
|
||||
menu_handler = nil
|
||||
input_manager = nil
|
||||
buttons = {}
|
||||
end
|
||||
|
||||
local function menu_callback(index, event)
|
||||
if menu_handler then
|
||||
return menu_handler:handle_menu_event(index, event, buttons)
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
local function menu_populate()
|
||||
if not menu_handler then
|
||||
local status, msg = pcall(function () menu_handler = require('autofire/autofire_menu') end)
|
||||
if not status then
|
||||
emu.print_error(string.format('Error loading autofire menu: %s', msg))
|
||||
end
|
||||
if menu_handler then
|
||||
menu_handler:init_menu(buttons)
|
||||
end
|
||||
end
|
||||
if menu_handler then
|
||||
return menu_handler:populate_menu(buttons)
|
||||
else
|
||||
return {{_p('plugin-autofire', 'Failed to load autofire menu'), '', 'off'}}
|
||||
end
|
||||
end
|
||||
|
||||
frame_subscription = emu.add_machine_frame_notifier(process_frame)
|
||||
emu.register_prestart(load_settings)
|
||||
stop_subscription = emu.add_machine_stop_notifier(save_settings)
|
||||
emu.register_menu(menu_callback, menu_populate, _p('plugin-autofire', 'Autofire'))
|
||||
end
|
||||
|
||||
return exports
|
||||
10
bios/Arcade/MAME/plugins/autofire/plugin.json
Normal file
10
bios/Arcade/MAME/plugins/autofire/plugin.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"plugin": {
|
||||
"name": "autofire",
|
||||
"description": "Autofire plugin",
|
||||
"version": "0.0.4",
|
||||
"author": "Jack Li",
|
||||
"type": "plugin",
|
||||
"start": "false"
|
||||
}
|
||||
}
|
||||
29
bios/Arcade/MAME/plugins/boot.lua
Normal file
29
bios/Arcade/MAME/plugins/boot.lua
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
-- license:BSD-3-Clause
|
||||
-- copyright-holders:Miodrag Milanovic
|
||||
require('lfs')
|
||||
|
||||
_G._ = emu.lang_translate
|
||||
_G._p = emu.lang_translate
|
||||
_G.N_ = function (message) return message end
|
||||
_G.N_p = function (context, message) return message end
|
||||
_G.emu.plugin = {} -- table to contain plugin interfaces
|
||||
-- substitute environment variables in the plugins path from options
|
||||
local dirs = manager.options.entries.pluginspath:value()
|
||||
|
||||
-- and split the paths apart and make them suitable for package.path
|
||||
package.path = ""
|
||||
for dir in string.gmatch(dirs, "([^;]+)") do
|
||||
if (package.path ~= "") then
|
||||
package.path = package.path .. ";"
|
||||
end
|
||||
package.path = package.path .. dir .. "/?.lua;" .. dir .. "/?/init.lua"
|
||||
end
|
||||
|
||||
for _,entry in pairs(manager.plugins) do
|
||||
if (entry.type == "plugin" and entry.start) then
|
||||
emu.print_verbose("Starting plugin " .. entry.name .. "...")
|
||||
plugin = require(entry.name)
|
||||
if plugin.set_folder~=nil then plugin.set_folder(entry.directory) end
|
||||
plugin.startplugin();
|
||||
end
|
||||
end
|
||||
12
bios/Arcade/MAME/plugins/cheat/cheat_json.lua
Normal file
12
bios/Arcade/MAME/plugins/cheat/cheat_json.lua
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
local jsoncheat = {}
|
||||
|
||||
function jsoncheat.filename(name)
|
||||
return name .. ".json"
|
||||
end
|
||||
|
||||
function jsoncheat.conv_cheat(data)
|
||||
local json = require("json")
|
||||
return json.parse(data)
|
||||
end
|
||||
|
||||
return jsoncheat
|
||||
328
bios/Arcade/MAME/plugins/cheat/cheat_simple.lua
Normal file
328
bios/Arcade/MAME/plugins/cheat/cheat_simple.lua
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
-- converter for simple cheats
|
||||
-- simple cheats are single/linked address every frame ram, rom or gg,ar cheats in one file called cheat.simple
|
||||
--
|
||||
-- ram/rom cheat format: <set name>,<cputag|regiontag>,<hex offset>,<b|w|d|q - size>,<hex value>,<desc>
|
||||
-- only program address space is supported, comments are prepended with ;
|
||||
-- size is b - u8, w - u16, d - u32, q - u64
|
||||
--
|
||||
-- gg,ar cheat format: <set name>,<gg|ar - type>,<code>,<desc> like "nes/smb,gg,SXIOPO,Infinite Lives"
|
||||
-- gg for game genie -- nes, snes, megadriv, gamegear, gameboy
|
||||
-- ar for action replay -- nes, snes, megadriv, gamegear, sms
|
||||
--
|
||||
-- use "^" as description to link to previous cheat
|
||||
-- set name is <softlist>/<entry> like "nes/smb" for softlist items
|
||||
-- Don't use commas in the description
|
||||
|
||||
local simple = {}
|
||||
|
||||
simple.romset = "????"
|
||||
|
||||
function simple.filename(name)
|
||||
simple.romset = name
|
||||
return "cheat.simple"
|
||||
end
|
||||
|
||||
local codefuncs = {}
|
||||
local currcheat
|
||||
|
||||
local function prepare_rom_cheat(desc, region, addr, val, size, banksize, comp)
|
||||
local cheat
|
||||
if desc:sub(1,1) ~= "^" then
|
||||
currcheat = { desc = desc, region = { rom = region } }
|
||||
currcheat.script = { off = string.format([[
|
||||
if on then
|
||||
for k, v in pairs(addrs) do
|
||||
rom:write_u%d(v.addr, v.save)
|
||||
end
|
||||
end]], size),
|
||||
on = string.format([[
|
||||
addrs = {
|
||||
--flag
|
||||
}
|
||||
on = true
|
||||
for k, v in pairs(addrs) do
|
||||
v.save = rom:read_u%d(v.addr)
|
||||
rom:write_u%d(v.addr, v.val)
|
||||
end]], size, size) }
|
||||
cheat = currcheat
|
||||
|
||||
end
|
||||
if banksize and comp then
|
||||
local rom = manager.machine.memory.regions[region]
|
||||
local bankaddr = addr & (banksize - 1)
|
||||
addr = nil
|
||||
if not rom then
|
||||
error("rom cheat invalid region " .. desc)
|
||||
end
|
||||
for i = 0, rom.size, banksize do
|
||||
if rom:read_u8(i + bankaddr) == comp then
|
||||
addr = i + bankaddr
|
||||
break
|
||||
end
|
||||
end
|
||||
if not addr then
|
||||
error("rom cheat compare value not found " .. desc)
|
||||
end
|
||||
end
|
||||
currcheat.script.on = currcheat.script.on:gsub("%-%-flag", string.format("{addr = %d, val = %d},\n--flag", addr, val), 1)
|
||||
return cheat
|
||||
end
|
||||
|
||||
local function prepare_ram_cheat(desc, tag, addr, val, size)
|
||||
local cheat
|
||||
if desc:sub(1,1) ~= "^" then
|
||||
currcheat = { desc = desc, space = { cpup = { tag = tag, type = "program" } }, script = { run = "" } }
|
||||
cheat = currcheat
|
||||
end
|
||||
currcheat.script.run = currcheat.script.run .. " cpup:write_u" .. size .. "(" .. addr .. "," .. val .. ", true)"
|
||||
return cheat
|
||||
end
|
||||
|
||||
function codefuncs.nes_gg(desc, code)
|
||||
local xlate = { A = 0, P = 1, Z = 2, L = 3, G = 4, I = 5, T = 6, Y = 7, E = 8,
|
||||
O = 9, X = 10, U = 11, K = 12, S = 13, V = 14, N = 15 }
|
||||
local value = 0
|
||||
code:upper():gsub("(.)", function(s)
|
||||
if not xlate[s] then
|
||||
error("error parsing game genie cheat " .. desc)
|
||||
end
|
||||
value = (value << 4) | xlate[s]
|
||||
end)
|
||||
local addr, newval, comp
|
||||
if #code == 6 then
|
||||
addr = ((value >> 4) & 7) | ((value >> 8) & 0x78) | ((value >> 12) & 0x80) | ((value << 8) & 0x700) | ((value << 4) & 0x7800)
|
||||
newval = ((value >> 20) & 7) | (value & 8) | ((value >> 12) & 0x70) | ((value >> 16) & 0x80)
|
||||
if manager.machine.memory.regions[":nes_slot:cart:prg_rom"].size > 32768 then
|
||||
emu.print_verbose("warning: gamegenie 6 char code with banked rom " .. desc)
|
||||
end
|
||||
return prepare_rom_cheat(desc, ":nes_slot:cart:prg_rom", addr, newval, 8)
|
||||
elseif #code == 8 then
|
||||
addr = ((value >> 12) & 7) | ((value >> 16) & 0x78) | ((value >> 20) & 0x80) | (value & 0x700) | ((value >> 4) & 0x7800)
|
||||
newval = ((value >> 28) & 7) | (value & 8) | ((value >> 20) & 0x70) | ((value >> 24) & 0x80)
|
||||
comp = ((value >> 4) & 7) | ((value >> 8) & 8) | ((value << 4) & 0x70) | (value & 0x80)
|
||||
-- try 32K banks then 8K
|
||||
local status, cheat = pcall(prepare_rom_cheat, desc, ":nes_slot:cart:prg_rom", addr, newval, 8, 32768, comp)
|
||||
if not status then
|
||||
cheat = prepare_rom_cheat(desc, ":nes_slot:cart:prg_rom", addr, newval, 8, 8192, comp)
|
||||
end
|
||||
return cheat
|
||||
else
|
||||
error("error game genie cheat incorrect length " .. desc)
|
||||
end
|
||||
end
|
||||
|
||||
function codefuncs.nes_ar(desc, code)
|
||||
code = code:gsub("[: %-]", "")
|
||||
if #code ~= 8 then
|
||||
error("error action replay cheat incorrect length " .. desc)
|
||||
end
|
||||
local newval = tonumber(code:sub(7, 8), 16)
|
||||
local addr = tonumber(code:sub(3, 6), 16)
|
||||
if not newval or not addr then
|
||||
error("error parsing action replay cheat " .. desc)
|
||||
end
|
||||
return prepare_ram_cheat(desc, ":maincpu", addr, newval, 8)
|
||||
end
|
||||
|
||||
local function snes_prepare_cheat(desc, addr, val)
|
||||
local bank = addr >> 16
|
||||
local offset = addr & 0xffff
|
||||
if ((bank <= 0x3f) and (offset < 0x2000)) or ((bank & 0xfe) == 0x7e) then
|
||||
return prepare_ram_cheat(desc, ":maincpu", addr, val, 8)
|
||||
end
|
||||
if (manager.machine.devices[":maincpu"].spaces["program"]:read_u8(0xffd5) & 1) == 1 then --hirom
|
||||
if (bank & 0x7f) <= 0x3f and offset >= 0x8000 then
|
||||
-- direct map
|
||||
elseif (bank & 0x7f) >= 0x40 and (bank & 0x7f) <= 0x7d then
|
||||
addr = addr & 0x3fffff
|
||||
elseif bank >= 0xfe then
|
||||
addr = addr & 0x3fffff
|
||||
else
|
||||
error("error cheat not rom or ram addr " .. desc)
|
||||
end
|
||||
else --lorom
|
||||
if (bank & 0x7f) <= 0x3f and offset >= 0x8000 then
|
||||
addr = ((addr >> 1) & 0x3f8000) | (addr & 0x7fff)
|
||||
elseif (bank & 0x7f) >= 0x40 and (bank & 0x7f) <= 0x6f then
|
||||
addr = ((addr >> 1) & 0x3f8000) | (addr & 0x7fff)
|
||||
elseif (bank & 0x7f) >= 0x70 and (bank & 0x7f) <= 0x7d and offset >= 0x8000 then
|
||||
addr = ((addr >> 1) & 0x3f8000) | (addr & 0x7fff)
|
||||
elseif bank >= 0xfe and offset >= 0x8000 then
|
||||
addr = ((addr >> 1) & 0x3f8000) | (addr & 0x7fff)
|
||||
else
|
||||
error("error cheat not rom or ram addr " .. desc)
|
||||
end
|
||||
end
|
||||
return prepare_rom_cheat(desc, ":snsslot:cart:rom", addr, val, 8)
|
||||
end
|
||||
|
||||
function codefuncs.snes_gg(desc, code)
|
||||
local xlate = { D = 0, F = 1, ["4"] = 2, ["7"] = 3, ["0"] = 4, ["9"] = 5, ["1"] = 6, ["5"] = 7,
|
||||
["6"] = 8, B = 9, C = 10, ["8"] = 11, A = 12, ["2"] = 13, ["3"] = 14, E = 15 }
|
||||
local value = 0
|
||||
local count = 0
|
||||
code:upper():gsub("(.)", function(s)
|
||||
if s == "-" then
|
||||
return
|
||||
elseif not xlate[s] then
|
||||
error("error parsing game genie cheat " .. desc)
|
||||
end
|
||||
count = count + 1
|
||||
value = (value << 4) | xlate[s]
|
||||
end)
|
||||
if count ~= 8 then
|
||||
error("error game genie cheat incorrect length " .. desc)
|
||||
end
|
||||
local newval = (value >> 24) & 0xff
|
||||
local addr = ((value >> 6) & 0xf) | ((value >> 12) & 0xf0) | ((value >> 6) & 0x300) | ((value << 10) & 0xc00) |
|
||||
((value >> 8) & 0xf000) | ((value << 14) & 0xf0000) | ((value << 10) & 0xf00000)
|
||||
return snes_prepare_cheat(desc, addr, newval)
|
||||
end
|
||||
|
||||
function codefuncs.snes_ar(desc, code)
|
||||
code = code:gsub("[: %-]", "")
|
||||
if #code ~= 8 then
|
||||
error("error action replay cheat incorrect length " .. desc)
|
||||
end
|
||||
local addr = tonumber(code:sub(1, 6), 16)
|
||||
local val = tonumber(code:sub(7, 8), 16)
|
||||
if not addr or not val then
|
||||
error("error parsing action replay cheat " .. desc)
|
||||
end
|
||||
return snes_prepare_cheat(desc, addr, val)
|
||||
end
|
||||
|
||||
function codefuncs.megadriv_gg(desc, code)
|
||||
local xlate = { A = 0, B = 1, C = 2, D = 3, E = 4, F = 5, G = 6, H = 7, J = 8, K = 9, L = 10, M = 11, N = 12,
|
||||
P = 13, R = 14, S = 15, T = 16, V = 17, W = 18, X = 19, Y = 20, Z = 21, ["0"] = 22, ["1"] = 23,
|
||||
["2"] = 24, ["3"] = 25, ["4"] = 26, ["5"] = 27, ["6"] = 28, ["7"] = 29, ["8"] = 30, ["9"] = 31 }
|
||||
local value = 0
|
||||
local count = 0
|
||||
code:upper():gsub("(.)", function(s)
|
||||
if s == "-" then
|
||||
return
|
||||
elseif not xlate[s] then
|
||||
error("error parsing game genie cheat " .. desc)
|
||||
end
|
||||
count = count + 1
|
||||
value = (value << 5) | xlate[s]
|
||||
end)
|
||||
if count ~= 8 then
|
||||
error("error game genie cheat incorrect length " .. desc)
|
||||
end
|
||||
local newval = ((value >> 32) & 0xff) | ((value >> 3) & 0x1f00) | ((value << 5) & 0xe000)
|
||||
local addr = (value & 0xff00ff) | ((value >> 16) & 0xff00)
|
||||
return prepare_rom_cheat(desc, ":mdslot:cart:rom", addr, newval, 16)
|
||||
end
|
||||
|
||||
function codefuncs.megadriv_ar(desc, code)
|
||||
code = code:gsub("[: %-]", "")
|
||||
if #code ~= 10 then
|
||||
error("error action replay cheat incorrect length " .. desc)
|
||||
end
|
||||
local addr = tonumber(code:sub(1, 6), 16)
|
||||
local val = tonumber(code:sub(7, 10), 16)
|
||||
if addr < 0xff0000 then
|
||||
error("error action replay cheat not ram addr " .. desc)
|
||||
end
|
||||
return prepare_ram_cheat(desc, ":maincpu", addr, val, 16)
|
||||
end
|
||||
|
||||
local function gbgg_ggcodes(desc, code, region)
|
||||
code = code:gsub("%-", "")
|
||||
local comp
|
||||
if #code == 6 then
|
||||
comp = -1
|
||||
elseif #code == 9 then
|
||||
comp = ~tonumber(code:sub(7, 7) .. code:sub(9, 9), 16) & 0xff
|
||||
comp = ((comp >> 2) | ((comp << 6) & 0xc0)) ~ 0x45
|
||||
else
|
||||
error("error game genie cheat incorrect length " .. desc)
|
||||
end
|
||||
local newval = tonumber(code:sub(1, 2), 16)
|
||||
local addr = tonumber(code:sub(6, 6) .. code:sub(3, 5), 16)
|
||||
if not newval or not addr or not comp then
|
||||
error("error parsing game genie cheat " .. desc)
|
||||
end
|
||||
addr = (~addr & 0xf000) | (addr & 0xfff)
|
||||
if addr > 0x7fff then
|
||||
error("error game genie cheat bad addr " .. desc)
|
||||
end
|
||||
if comp == -1 then
|
||||
return prepare_rom_cheat(desc, region, addr, newval, 8)
|
||||
else
|
||||
-- assume 8K banks
|
||||
return prepare_rom_cheat(desc, region, addr, newval, 8, 8192, comp)
|
||||
end
|
||||
return cheat
|
||||
end
|
||||
|
||||
function codefuncs.gameboy_gg(desc, code)
|
||||
return gbgg_ggcodes(desc, code, ":gbslot:cart:rom")
|
||||
end
|
||||
|
||||
function codefuncs.gamegear_gg(desc, code)
|
||||
return gbgg_ggcodes(desc, code, ":slot:cart:rom")
|
||||
end
|
||||
|
||||
function codefuncs.gamegear_ar(desc, code)
|
||||
code = code:gsub("[: %-]", "")
|
||||
if #code ~= 8 then
|
||||
error("error action replay cheat incorrect length " .. desc)
|
||||
end
|
||||
local addr = tonumber(code:sub(1, 6), 16)
|
||||
local val = tonumber(code:sub(7, 8), 16)
|
||||
if addr < 0xc000 or addr >= 0xe000 then
|
||||
error("error action replay cheat not ram addr " .. desc)
|
||||
end
|
||||
return prepare_ram_cheat(desc, ":maincpu", addr, val, 8)
|
||||
end
|
||||
|
||||
codefuncs.sms_ar = codefuncs.gamegear_ar
|
||||
|
||||
function simple.conv_cheat(data)
|
||||
local cheats = {}
|
||||
for line in data:gmatch('([^\n;]+)') do
|
||||
local set, cputag, offset, size, val, desc = line:match('([^,]+),([^,]+),([^,]+),?([^,]*),?([^,]*),(.*)')
|
||||
if set == simple.romset then
|
||||
local cheat
|
||||
if cputag:sub(1,1) ~= ":" then
|
||||
local list, name = set:match('([^/]+)/(.+)')
|
||||
local func = list .. "_" .. cputag
|
||||
if list and desc and codefuncs[func] then
|
||||
local status
|
||||
status, cheat = pcall(codefuncs[func], desc, offset)
|
||||
if not status then
|
||||
emu.print_error(cheat)
|
||||
cheat = nil
|
||||
end
|
||||
end
|
||||
elseif size and val then
|
||||
if size == "w" then
|
||||
size = 16
|
||||
elseif size == "d" then
|
||||
size = 32
|
||||
elseif size == "q" then
|
||||
size = 64
|
||||
else
|
||||
size = 8
|
||||
end
|
||||
offset = tonumber(offset, 16)
|
||||
val = tonumber(val, 16)
|
||||
if manager.machine.devices[cputag] then
|
||||
cheat = prepare_ram_cheat(desc, cputag, offset, val, size)
|
||||
else
|
||||
cheat = prepare_rom_cheat(desc, cputag, offset, val, size)
|
||||
end
|
||||
end
|
||||
if cheat then
|
||||
cheats[#cheats + 1] = cheat
|
||||
end
|
||||
end
|
||||
end
|
||||
currcheat = nil
|
||||
return cheats
|
||||
end
|
||||
|
||||
return simple
|
||||
|
||||
285
bios/Arcade/MAME/plugins/cheat/cheat_xml.lua
Normal file
285
bios/Arcade/MAME/plugins/cheat/cheat_xml.lua
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
local xml = {}
|
||||
|
||||
function xml.filename(name)
|
||||
return name .. ".xml"
|
||||
end
|
||||
|
||||
-- basic xml parser for mamecheat only
|
||||
local function xml_parse(data)
|
||||
local function fix_gt(str)
|
||||
str = str:gsub(">=", " ge ")
|
||||
str = str:gsub(">", " gt ")
|
||||
return str
|
||||
end
|
||||
data = data:gsub("(condition=%b\"\")", fix_gt)
|
||||
local cheat_str = data:match("<mamecheat.->(.*)</ *mamecheat>")
|
||||
|
||||
local function get_tags(str)
|
||||
local arr = {}
|
||||
while str ~= "" do
|
||||
local tag, attr, stop
|
||||
tag, attr, stop, str = str:match("<([%w!%-]+) ?(.-)(/?)[ %-]->(.*)")
|
||||
|
||||
if not tag then
|
||||
return arr
|
||||
end
|
||||
if tag:sub(0, 3) ~= "!--" then
|
||||
local block = {}
|
||||
if stop ~= "/" then
|
||||
local nest
|
||||
nest, str = str:match("(.-)</ *" .. tag .. " *>(.*)")
|
||||
local children = get_tags(nest)
|
||||
if not next(children) then
|
||||
nest = nest:gsub("<!--.-%-%->", "")
|
||||
nest = nest:gsub("^%s*(.-)%s*$", "%1")
|
||||
block["text"] = nest
|
||||
else
|
||||
block = children
|
||||
end
|
||||
end
|
||||
if attr then
|
||||
for name, value in attr:gmatch("(%w-)=\"(.-)\"") do
|
||||
block[name] = value:gsub("^%s*(.-)%s*$", "%1")
|
||||
end
|
||||
end
|
||||
if not arr[tag] then
|
||||
arr[tag] = {}
|
||||
end
|
||||
arr[tag][#arr[tag] + 1] = block
|
||||
end
|
||||
end
|
||||
return arr
|
||||
end
|
||||
local xml_table = get_tags(cheat_str)
|
||||
return xml_table
|
||||
end
|
||||
|
||||
function xml.conv_cheat(data)
|
||||
local spaces, regions, output
|
||||
data = xml_parse(data)
|
||||
local cpu_spaces = {}
|
||||
|
||||
for tag, device in pairs(manager.machine.devices) do
|
||||
local sp
|
||||
for name, space in pairs(device.spaces) do
|
||||
if not sp then
|
||||
sp = {}
|
||||
cpu_spaces[tag] = sp
|
||||
end
|
||||
sp[space.index] = space.name
|
||||
end
|
||||
end
|
||||
|
||||
local function convert_expr(data)
|
||||
local write = false
|
||||
|
||||
local function convert_memref(cpu, phys, space, width, addr, rw)
|
||||
-- debug expressions address spaces by index not by name
|
||||
local function get_space_name(index)
|
||||
local prefix = cpu:sub(1, 1)
|
||||
if prefix == ":" then
|
||||
return cpu_spaces[cpu][index]
|
||||
else
|
||||
return cpu_spaces[":" .. cpu][index]
|
||||
end
|
||||
end
|
||||
|
||||
local mod = ""
|
||||
if space == "p" then
|
||||
fullspace = get_space_name(0)
|
||||
elseif space == "d" then
|
||||
fullspace = get_space_name(1)
|
||||
elseif space == "i" then
|
||||
fullspace = get_space_name(2)
|
||||
elseif space == "r" then
|
||||
fullspace = get_space_name(0)
|
||||
mod = "_direct"
|
||||
space = "p"
|
||||
elseif space == "o" then
|
||||
fullspace = get_space_name(3)
|
||||
mod = "_direct"
|
||||
space = "o"
|
||||
end
|
||||
if width == "b" then
|
||||
width = "u8"
|
||||
elseif width == "w" then
|
||||
width = "u16"
|
||||
elseif width == "d" then
|
||||
width = "u32"
|
||||
elseif width == "q" then
|
||||
width = "u64"
|
||||
end
|
||||
|
||||
local prefix = cpu:sub(1,1)
|
||||
if prefix == ":" then
|
||||
cpu = cpu:sub(2,cpu:len())
|
||||
end
|
||||
|
||||
local cpuname = cpu:gsub(":", "_")
|
||||
if space == "m" then
|
||||
regions[cpuname .. space] = ":" .. cpu
|
||||
else
|
||||
spaces[cpuname .. space] = { tag = ":" .. cpu, type = fullspace }
|
||||
if phys ~= "p" and mod == "" then
|
||||
mod = "v"
|
||||
end
|
||||
end
|
||||
local ret
|
||||
if rw == "=" then
|
||||
write = true
|
||||
ret = string.format("%s%s:write%s_%s(%s,", cpuname, space, mod, width, addr)
|
||||
else
|
||||
ret = string.format("%s%s:read%s_%s(%s)", cpuname, space, mod, width, addr)
|
||||
end
|
||||
if rw == "==" then
|
||||
ret = ret .. "=="
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
local function frame()
|
||||
output = true
|
||||
return "screen:frame_number()"
|
||||
end
|
||||
|
||||
data = data:lower()
|
||||
data = data:gsub("^[(](.-)[)]$", "%1")
|
||||
data = data:gsub("%f[%w]lt%f[%W]", "<")
|
||||
data = data:gsub("%f[%w]ge%f[%W]", ">=")
|
||||
data = data:gsub("%f[%w]gt%f[%W]", ">")
|
||||
data = data:gsub("%f[%w]le%f[%W]", "<=")
|
||||
data = data:gsub("%f[%w]eq%f[%W]", "==")
|
||||
data = data:gsub("%f[%w]ne%f[%W]", "~=")
|
||||
data = data:gsub("!=", "~=")
|
||||
data = data:gsub("||", " or ")
|
||||
data = data:gsub("%f[%w]frame%f[%W]", frame)
|
||||
data = data:gsub("%f[%w]band%f[%W]", "&")
|
||||
data = data:gsub("%f[%w]bor%f[%W]", "|")
|
||||
data = data:gsub("%f[%w]rshift%f[%W]", ">>")
|
||||
data = data:gsub("%f[%w]lshift%f[%W]", "<<")
|
||||
data = data:gsub("(%w-)%+%+", "%1 = %1 + 1")
|
||||
data = data:gsub("%f[%w](%x+)%f[%W]", "0x%1")
|
||||
-- 0?x? avoids an issue where db (data region byte) is interepeted as a hex number
|
||||
data = data:gsub("([%w_:]-)%.(p?)0?x?([pmrodi3])([bwdq])@(%w+) *(=*)", convert_memref)
|
||||
local count
|
||||
repeat
|
||||
data, count = data:gsub("([%w_:]-)%.(p?)0?x?([pmrodi3])([bwdq])@(%b()) *(=*)", convert_memref)
|
||||
until count == 0
|
||||
if write then
|
||||
data = data .. ")"
|
||||
end
|
||||
return data
|
||||
end
|
||||
|
||||
local function convert_output(data)
|
||||
local str = "draw_text(ui,"
|
||||
if data["align"] then
|
||||
str = str .. data["align"]
|
||||
else
|
||||
str = str .. "\"left\""
|
||||
end
|
||||
if data["line"] then
|
||||
str = str .. ",\"" .. data["line"] .. "\""
|
||||
else
|
||||
str = str .. ", \"auto\""
|
||||
end
|
||||
str = str .. ", nil,\"" .. data["format"] .. "\""
|
||||
if data["argument"] then
|
||||
for count, block in pairs(data["argument"]) do
|
||||
local expr = convert_expr(block["text"])
|
||||
if block["count"] then
|
||||
for i = 0, block["count"] - 1 do
|
||||
str = str .. "," .. expr:gsub("argindex", i)
|
||||
end
|
||||
else
|
||||
str = str .. "," .. expr
|
||||
end
|
||||
end
|
||||
end
|
||||
return str .. ")"
|
||||
end
|
||||
|
||||
local function convert_script(data)
|
||||
local str = ""
|
||||
local state = "run"
|
||||
for tag, block in pairs(data) do
|
||||
if tag == "state" then
|
||||
state = block
|
||||
elseif tag == "action" then
|
||||
for count, action in pairs(block) do
|
||||
if action["condition"] then
|
||||
str = str .. " if (" .. convert_expr(action["condition"]) .. ") then "
|
||||
for expr in action["text"]:gmatch("([^,]+)") do
|
||||
str = str .. convert_expr(expr) .. " "
|
||||
end
|
||||
str = str .. "end"
|
||||
else
|
||||
for expr in action["text"]:gmatch("([^,]+)") do
|
||||
str = str .. " " .. convert_expr(expr) .. " "
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif tag == "output" then
|
||||
output = true
|
||||
for count, output in pairs(block) do
|
||||
if output["condition"] then
|
||||
str = str .. " if " .. convert_expr(output["condition"]) .. " then "
|
||||
str = str .. convert_output(output) .. " end "
|
||||
else
|
||||
str = str .. " " .. convert_output(output) .. " "
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return state, str
|
||||
end
|
||||
|
||||
for count, cheat in pairs(data["cheat"]) do
|
||||
spaces = {}
|
||||
regions = {}
|
||||
output = false
|
||||
for tag, block in pairs(cheat) do
|
||||
if tag == "comment" then
|
||||
data["cheat"][count]["comment"] = block[1]["text"]
|
||||
elseif tag == "script" then
|
||||
local scripts = {}
|
||||
for count2, script in pairs(block) do
|
||||
local state, str = convert_script(script)
|
||||
scripts[state] = str
|
||||
end
|
||||
data["cheat"][count]["script"] = scripts
|
||||
elseif tag == "parameter" then
|
||||
if block[1]["min"] then
|
||||
block[1]["min"] = block[1]["min"]:gsub("%$","0x")
|
||||
end
|
||||
if block[1]["max"] then
|
||||
block[1]["max"] = block[1]["max"]:gsub("%$","0x")
|
||||
end
|
||||
if block[1]["step"] then
|
||||
block[1]["step"] = block[1]["step"]:gsub("%$","0x")
|
||||
end
|
||||
data["cheat"][count]["parameter"] = block[1]
|
||||
end
|
||||
end
|
||||
if next(spaces) then
|
||||
data["cheat"][count]["space"] = {}
|
||||
for name, space in pairs(spaces) do
|
||||
data["cheat"][count]["space"][name] = { type = space["type"], tag = space["tag"] }
|
||||
end
|
||||
end
|
||||
if next(regions) then
|
||||
data["cheat"][count]["region"] = {}
|
||||
for name, region in pairs(regions) do
|
||||
data["cheat"][count]["region"][name] = region
|
||||
end
|
||||
end
|
||||
if output then
|
||||
data["cheat"][count]["screen"] = {}
|
||||
data["cheat"][count]["screen"]["screen"] = ":screen"
|
||||
data["cheat"][count]["screen"]["ui"] = "ui"
|
||||
end
|
||||
end
|
||||
return data["cheat"]
|
||||
end
|
||||
|
||||
return xml
|
||||
985
bios/Arcade/MAME/plugins/cheat/init.lua
Normal file
985
bios/Arcade/MAME/plugins/cheat/init.lua
Normal file
|
|
@ -0,0 +1,985 @@
|
|||
-- license:BSD-3-Clause
|
||||
-- copyright-holders:Carl
|
||||
--
|
||||
-- json cheat file format
|
||||
-- [{
|
||||
-- "desc": "text",
|
||||
-- "parameter": {
|
||||
-- "min": "minval(0)",
|
||||
-- "max": "maxval(numitems)",
|
||||
-- "step": "stepval(1)",
|
||||
-- "item" [{
|
||||
-- "value": "itemval(index*stepval+minval)",
|
||||
-- "text": "text"
|
||||
-- },
|
||||
-- ... ]
|
||||
-- },
|
||||
-- "cpu": {
|
||||
-- "varname": "tag"
|
||||
-- ...
|
||||
-- }
|
||||
-- "space": {
|
||||
-- "varname": {
|
||||
-- "tag": "tag",
|
||||
-- "type": "program|data|io"
|
||||
-- },
|
||||
-- ...
|
||||
-- },
|
||||
-- "screen": {
|
||||
-- "varname": "tag",
|
||||
-- ...
|
||||
-- },
|
||||
-- "region": {
|
||||
-- "varname": "tag",
|
||||
-- ...
|
||||
-- },
|
||||
-- "ram": {
|
||||
-- "varname": "tag",
|
||||
-- ...
|
||||
-- },
|
||||
-- "share": {
|
||||
-- "varname": "tag",
|
||||
-- ...
|
||||
-- },
|
||||
-- "script": {
|
||||
-- "on|off|run|change": "script",
|
||||
-- ...
|
||||
-- },
|
||||
-- "comment": "text"
|
||||
-- },
|
||||
-- ... ]
|
||||
--
|
||||
-- Scripts are lua scripts with a limited api. Most library functions are unavailable.
|
||||
-- Like the XML cheats, param is the current parameter value and variables are shared between scripts within a cheat
|
||||
-- Differences from XML cheats:
|
||||
-- - actions are only one line which include the entire script
|
||||
-- - "condexpr" is replaced with lua control statements (if-then-else-end)
|
||||
-- - variables are only limited by the limits of the lua interperter, you can have strings and tables
|
||||
-- - the address spaces in the "space" blocks are accessible to the script if included,
|
||||
-- same with regions (the "m" space in debug expr)
|
||||
-- - frame is replaced by screen:frame_number() so if you use frame a screen needs to be in the device section
|
||||
-- - output is a function and argindex isn't supported, output args need to be explicit and a screen device
|
||||
-- must be provided
|
||||
-- - cpu is only used for break and watch points, if it is defined and the debugger is not enabled (-debugger none is enough)
|
||||
-- it will disable the cheat only if a point is set, check var for nil first
|
||||
-- - watch points require the address space that you want to set the watch on, wptype is "r"-read, "w"-write or "rw"-both
|
||||
|
||||
local exports = {}
|
||||
exports.name = "cheat"
|
||||
exports.version = "0.0.1"
|
||||
exports.description = "Cheat plugin"
|
||||
exports.license = "BSD-3-Clause"
|
||||
exports.author = { name = "Carl" }
|
||||
|
||||
local cheat = exports
|
||||
|
||||
local reset_subscription, stop_subscription, frame_subscription
|
||||
|
||||
function cheat.set_folder(path)
|
||||
cheat.path = path
|
||||
end
|
||||
|
||||
function cheat.startplugin()
|
||||
local cheats = {}
|
||||
local output = {}
|
||||
local line = 0
|
||||
local start_time = 0
|
||||
local stop = true
|
||||
local cheatname = ""
|
||||
local consolelog = nil
|
||||
local consolelast = 0
|
||||
local perodicset = false
|
||||
local watches = {}
|
||||
local breaks = {}
|
||||
local inputs = {}
|
||||
|
||||
local function load_cheats()
|
||||
local filename = emu.romname()
|
||||
local newcheats = {}
|
||||
local file = emu.file(manager.machine.options.entries.cheatpath:value():gsub("([^;]+)", "%1;%1/cheat") , 1)
|
||||
|
||||
for name, image in pairs(manager.machine.images) do
|
||||
if image.exists and image.software_list_name ~= "" then
|
||||
filename = image.software_list_name .. "/" .. image.filename
|
||||
end
|
||||
end
|
||||
|
||||
cheatname = filename
|
||||
local function add(addcheats)
|
||||
if not next(newcheats) then
|
||||
newcheats = addcheats
|
||||
else
|
||||
for num, cheat in pairs(addcheats) do
|
||||
newcheats[#newcheats + 1] = cheat
|
||||
end
|
||||
end
|
||||
end
|
||||
for scrfile in lfs.dir(cheat.path) do
|
||||
local name = string.match(scrfile, "^(cheat_.*).lua$")
|
||||
if name then
|
||||
local conv = require("cheat/" .. name)
|
||||
if conv then
|
||||
local ret = file:open(conv.filename(filename))
|
||||
while not ret do
|
||||
add(conv.conv_cheat(file:read(file:size())))
|
||||
ret = file:open_next()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return newcheats
|
||||
end
|
||||
|
||||
local function load_hotkeys()
|
||||
local json = require("json")
|
||||
local file = io.open(manager.machine.options.entries.cheatpath:value():match("([^;]+)") .. "/" .. cheatname .. "_hotkeys.json", "r")
|
||||
if not file then
|
||||
return
|
||||
end
|
||||
local hotkeys = json.parse(file:read("a"))
|
||||
for num, val in ipairs(hotkeys) do
|
||||
for num, cheat in pairs(cheats) do
|
||||
if val.desc == cheat.desc then
|
||||
cheat.hotkeys = {pressed = false, keys = manager.machine.input:seq_from_tokens(val.keys)}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function save_hotkeys()
|
||||
local hotkeys = {}
|
||||
for num, cheat in ipairs(cheats) do
|
||||
if cheat.hotkeys then
|
||||
local hotkey = {desc = cheat.desc, keys = manager.machine.input:seq_to_tokens(cheat.hotkeys.keys)}
|
||||
if hotkey.keys ~= "" then
|
||||
hotkeys[#hotkeys + 1] = hotkey
|
||||
end
|
||||
end
|
||||
end
|
||||
local path = manager.machine.options.entries.cheatpath:value():match("([^;]+)")
|
||||
local filepath = path .. "/" .. cheatname .. "_hotkeys.json"
|
||||
if #hotkeys > 0 then
|
||||
local json = require("json")
|
||||
local attr = lfs.attributes(path)
|
||||
if not attr then
|
||||
lfs.mkdir(path)
|
||||
elseif attr.mode ~= "directory" then -- uhhh?
|
||||
return
|
||||
end
|
||||
if cheatname:find("/", 1, true) then
|
||||
local softpath = path .. "/" .. cheatname:match("([^/]+)")
|
||||
attr = lfs.attributes(softpath)
|
||||
if not attr then
|
||||
lfs.mkdir(softpath)
|
||||
elseif attr.mode ~= "directory" then -- uhhh?
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local file = io.open(filepath, "w+")
|
||||
if file then
|
||||
file:write(json.stringify(hotkeys, {indent = true}))
|
||||
file:close()
|
||||
end
|
||||
else
|
||||
local attr = lfs.attributes(filepath)
|
||||
if attr and (attr.mode == "file") then
|
||||
local json = require("json")
|
||||
local file = io.open(filepath, "w+")
|
||||
if file then
|
||||
file:write(json.stringify(hotkeys, {indent = true}))
|
||||
file:close()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function cheat_error(cheat, msg)
|
||||
emu.print_error("error cheat script error: \"" .. cheat.desc .. "\" " .. msg)
|
||||
cheat.desc = cheat.desc .. " error"
|
||||
cheat.script = nil
|
||||
cheat.enabled = nil
|
||||
return
|
||||
end
|
||||
|
||||
local function run_if(cheat, func)
|
||||
if func then
|
||||
local stat, err = pcall(func)
|
||||
if not stat then
|
||||
cheat_error(cheat, err)
|
||||
end
|
||||
return func
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function draw_text(screen, x, y, color, form, ...)
|
||||
local str = form:format(...)
|
||||
if y == "auto" then
|
||||
y = line
|
||||
line = line + 1
|
||||
end
|
||||
if not screen then
|
||||
emu.print_verbose("draw_text: invalid screen")
|
||||
return
|
||||
end
|
||||
if type(x) == "string" then
|
||||
y = y * mame_manager.ui.line_height
|
||||
end
|
||||
output[#output + 1] = { type = "text", scr = screen, x = x, y = y, str = str, color = color }
|
||||
end
|
||||
|
||||
local function draw_line(screen, x1, y1, x2, y2, color)
|
||||
if not screen then
|
||||
emu.print_verbose("draw_line: invalid screen")
|
||||
return
|
||||
end
|
||||
output[#output + 1] = { type = "line", scr = screen, x1 = x1, x2 = x2, y1 = y1, y2 = y2, color = color }
|
||||
end
|
||||
|
||||
local function draw_box(screen, x1, y1, x2, y2, bgcolor, linecolor)
|
||||
if not screen then
|
||||
emu.print_verbose("draw_box: invalid screen")
|
||||
return
|
||||
end
|
||||
output[#output + 1] = { type = "box", scr = screen, x1 = x1, x2 = x2, y1 = y1, y2 = y2, bgcolor = bgcolor, linecolor = linecolor }
|
||||
end
|
||||
|
||||
local function tobcd(val)
|
||||
local result = 0
|
||||
local shift = 0
|
||||
while val ~= 0 do
|
||||
result = result + ((val % 10) << shift)
|
||||
val = val / 10
|
||||
shift = shift + 4
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local function frombcd(val)
|
||||
local result = 0
|
||||
local mul = 1
|
||||
while val ~= 0 do
|
||||
result = result + ((val % 16) * mul)
|
||||
val = val >> 4
|
||||
mul = mul * 10
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
local function time()
|
||||
return emu.time() - start_time
|
||||
end
|
||||
|
||||
local function periodiccb()
|
||||
local last = consolelast
|
||||
local msg = consolelog[#consolelog]
|
||||
consolelast = #consolelog
|
||||
if #consolelog > last and msg:find("Stopped at", 1, true) then
|
||||
local point = tonumber(msg:match("Stopped at breakpoint ([0-9]+)"))
|
||||
if not point then
|
||||
point = tonumber(msg:match("Stopped at watchpoint ([0-9]+"))
|
||||
if not point then
|
||||
return -- ??
|
||||
end
|
||||
local wp = watches[point]
|
||||
if wp then
|
||||
run_if(wp.cheat, wp.func)
|
||||
-- go in case a debugger other than "none" is enabled
|
||||
-- don't use an b/wpset action because that will supress the b/wp index
|
||||
manager.machine.debugger.execution_state = "run"
|
||||
end
|
||||
else
|
||||
local bp = breaks[point]
|
||||
if bp then
|
||||
run_if(bp.cheat, bp.func)
|
||||
manager.machine.debugger.execution_state = "run"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function bpset(cheat, dev, addr, func)
|
||||
if cheat.is_oneshot then
|
||||
error("bpset not permitted in oneshot cheat")
|
||||
return
|
||||
end
|
||||
local idx = dev.debug:bpset(addr)
|
||||
breaks[idx] = {cheat = cheat, func = func, dev = dev}
|
||||
end
|
||||
|
||||
local function wpset(cheat, dev, space, wptype, addr, len, func)
|
||||
if cheat.is_oneshot then
|
||||
error("wpset not permitted in oneshot cheat")
|
||||
return
|
||||
end
|
||||
if not space.name then
|
||||
error("bad space in wpset")
|
||||
return
|
||||
end
|
||||
local idx = dev.debug:wpset(space, wptype, addr, len)
|
||||
watches[idx] = {cheat = cheat, func = func, dev = dev}
|
||||
end
|
||||
|
||||
local function bwpclr(cheat)
|
||||
if not manager.machine.debugger then
|
||||
return
|
||||
end
|
||||
for num, bp in pairs(breaks) do
|
||||
if cheat == bp.cheat then
|
||||
bp.dev.debug:bpclr(num)
|
||||
end
|
||||
end
|
||||
for num, wp in pairs(watches) do
|
||||
if cheat == wp.cheat then
|
||||
wp.dev.debug:wpclr(num)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function input_trans(list)
|
||||
local xlate = { start = {}, stop = {}, last = 0 }
|
||||
local function errout(port, field)
|
||||
cheat:set_enabled(false)
|
||||
error(port .. field .. " not found")
|
||||
return
|
||||
end
|
||||
|
||||
for num, entry in ipairs(list) do
|
||||
if entry.port:sub(1, 1) ~= ":" then
|
||||
entry.port = ":" .. entry.port
|
||||
end
|
||||
local port = manager.machine.ioport.ports[entry.port]
|
||||
if not port then
|
||||
errout(entry.port, entry.field)
|
||||
end
|
||||
local field = port.fields[entry.field]
|
||||
if not field then
|
||||
errout(entry.port, entry.field)
|
||||
end
|
||||
if not xlate.start[entry.start] then
|
||||
xlate.start[entry.start] = {}
|
||||
end
|
||||
if not xlate.stop[entry.stop] then
|
||||
xlate.stop[entry.stop] = {}
|
||||
end
|
||||
local start = xlate.start[entry.start]
|
||||
local stop = xlate.stop[entry.stop]
|
||||
local ent = { port = port, field = field }
|
||||
stop[#stop + 1] = ent
|
||||
start[#start + 1] = ent
|
||||
if entry.stop > xlate.last then
|
||||
xlate.last = entry.stop
|
||||
end
|
||||
end
|
||||
return xlate
|
||||
end
|
||||
|
||||
local function input_run(cheat, list)
|
||||
if not cheat.is_oneshot then
|
||||
cheat.enabled = false
|
||||
error("input_run only allowed in one shot cheats")
|
||||
return
|
||||
end
|
||||
local _, screen = next(manager.machine.screens)
|
||||
list.begin = screen:frame_number()
|
||||
inputs[#inputs + 1] = list
|
||||
end
|
||||
|
||||
local function param_calc(param)
|
||||
if param.item then
|
||||
if not param.item[param.index] then -- uh oh
|
||||
param.index = 1
|
||||
end
|
||||
param.value = param.item[param.index].value
|
||||
return
|
||||
end
|
||||
param.value = param.min + (param.step * (param.index - 1))
|
||||
if param.value > param.max then
|
||||
param.value = param.max
|
||||
end
|
||||
end
|
||||
|
||||
-- return is current state, ui change
|
||||
local function set_enabled(cheat, state)
|
||||
if cheat.is_oneshot then
|
||||
if state then
|
||||
if cheat.parameter and cheat.script.change and cheat.parameter.index ~= 0 then
|
||||
param_calc(cheat.parameter)
|
||||
cheat.cheat_env.param = cheat.parameter.value
|
||||
cheat.script.change()
|
||||
elseif not cheat.parameter and cheat.script.on then
|
||||
cheat.script.on()
|
||||
end
|
||||
end
|
||||
return false, false
|
||||
end
|
||||
if cheat.enabled == state then
|
||||
return state, false
|
||||
end
|
||||
if not state then
|
||||
cheat.enabled = false
|
||||
run_if(cheat, cheat.script.off)
|
||||
bwpclr(cheat)
|
||||
else
|
||||
cheat.enabled = true
|
||||
run_if(cheat, cheat.script.on)
|
||||
end
|
||||
return state, true
|
||||
end
|
||||
|
||||
-- return is current index, ui change
|
||||
local function set_index(cheat, index)
|
||||
local param = cheat.parameter
|
||||
local oldindex = param.index
|
||||
if (index < 0) or (index > param.last) or (param.index == index) then
|
||||
return param.index, false
|
||||
end
|
||||
param.index = index
|
||||
if index == 0 then
|
||||
cheat.cheat_env.param = param.min
|
||||
cheat:set_enabled(false)
|
||||
else
|
||||
if oldindex == 0 then
|
||||
cheat:set_enabled(true)
|
||||
end
|
||||
param_calc(param)
|
||||
cheat.cheat_env.param = param.value
|
||||
if not cheat.is_oneshot then
|
||||
run_if(cheat, cheat.script.change)
|
||||
end
|
||||
end
|
||||
return index, true
|
||||
end
|
||||
|
||||
local function parse_cheat(cheat)
|
||||
cheat.cheat_env = {
|
||||
draw_text = draw_text,
|
||||
draw_line = draw_line,
|
||||
draw_box = draw_box,
|
||||
tobcd = tobcd,
|
||||
frombcd = frombcd,
|
||||
pairs = pairs,
|
||||
ipairs = ipairs,
|
||||
outputs = manager.machine.output,
|
||||
time = time,
|
||||
input_trans = input_trans,
|
||||
input_run = function(list) input_run(cheat, list) end,
|
||||
os = { time = os.time, date = os.date, difftime = os.difftime },
|
||||
table = { insert = table.insert, remove = table.remove },
|
||||
string = { format = string.format, char = string.char }
|
||||
}
|
||||
cheat.enabled = false
|
||||
cheat.set_enabled = set_enabled;
|
||||
cheat.get_enabled = function(cheat) return cheat.enabled end
|
||||
cheat.is_oneshot = cheat.script and not cheat.script.run and not cheat.script.off
|
||||
|
||||
-- verify scripts are valid first
|
||||
if not cheat.script then
|
||||
return
|
||||
end
|
||||
for name, script in pairs(cheat.script) do
|
||||
script, err = load(script, cheat.desc .. name, "t", cheat.cheat_env)
|
||||
if not script then
|
||||
cheat_error(cheat, err)
|
||||
return
|
||||
end
|
||||
cheat.script[name] = script
|
||||
end
|
||||
-- initialize temp[0-9] for backward compatbility reasons
|
||||
for i = 0, 9 do
|
||||
cheat.cheat_env["temp" .. i] = 0
|
||||
end
|
||||
if cheat.cpu then
|
||||
cheat.cpudev = {}
|
||||
for name, tag in pairs(cheat.cpu) do
|
||||
if manager.machine.debugger then
|
||||
local dev = manager.machine.devices[tag]
|
||||
if not dev or not dev.debug then
|
||||
cheat_error(cheat, "missing or invalid device " .. tag)
|
||||
return
|
||||
end
|
||||
cheat.cheat_env[name] = {
|
||||
bpset = function(addr, func) bpset(cheat, dev, addr, func) end,
|
||||
wpset = function(space, wptype, addr, len, func) wpset(cheat, dev, space, wptype, addr, len, func) end,
|
||||
regs = dev.state }
|
||||
cheat.bp = {}
|
||||
cheat.wp = {}
|
||||
if not periodicset then
|
||||
emu.register_periodic(periodic_cb)
|
||||
periodicset = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if cheat.space then
|
||||
for name, space in pairs(cheat.space) do
|
||||
local cpu, mem
|
||||
cpu = manager.machine.devices[space.tag]
|
||||
if not cpu then
|
||||
cheat_error(cheat, "missing device " .. space.tag)
|
||||
return
|
||||
end
|
||||
if space.type then
|
||||
mem = cpu.spaces[space.type]
|
||||
else
|
||||
space.type = "program"
|
||||
mem = cpu.spaces["program"]
|
||||
end
|
||||
if not mem then
|
||||
cheat_error(cheat, "missing space " .. space.type)
|
||||
return
|
||||
end
|
||||
cheat.cheat_env[name] = mem
|
||||
end
|
||||
end
|
||||
if cheat.screen then
|
||||
for name, screen in pairs(cheat.screen) do
|
||||
local scr = manager.machine.screens[screen]
|
||||
if screen == "ui" then
|
||||
scr = manager.machine.render.ui_container
|
||||
elseif not scr then
|
||||
local tag
|
||||
local nxt, coll = manager.machine.screens:pairs()
|
||||
tag, scr = nxt(coll) -- get any screen
|
||||
end
|
||||
cheat.cheat_env[name] = scr
|
||||
end
|
||||
end
|
||||
if cheat.region then
|
||||
for name, region in pairs(cheat.region) do
|
||||
local mem = manager.machine.memory.regions[region]
|
||||
if not mem then
|
||||
cheat_error(cheat, "missing region " .. region)
|
||||
return
|
||||
end
|
||||
cheat.cheat_env[name] = mem
|
||||
end
|
||||
end
|
||||
if cheat.ram then
|
||||
for name, tag in pairs(cheat.ram) do
|
||||
local ram = manager.machine.devices[tag]
|
||||
if not ram then
|
||||
cheat_error(cheat, "missing ram device " .. tag)
|
||||
return
|
||||
end
|
||||
cheat.cheat_env[name] = emu.item(ram.items["0/m_pointer"])
|
||||
end
|
||||
end
|
||||
if cheat.share then
|
||||
for name, tag in pairs(cheat.share) do
|
||||
local share = manager.machine.memory.shares[tag]
|
||||
if not share then
|
||||
cheat_error(cheat, "missing share " .. share)
|
||||
return
|
||||
end
|
||||
cheat.cheat_env[name] = share
|
||||
end
|
||||
end
|
||||
local param = cheat.parameter
|
||||
if not param then
|
||||
return
|
||||
end
|
||||
cheat.set_index = set_index;
|
||||
cheat.set_value = function(cheat, value)
|
||||
local idx = ((value - cheat.parameter.min) / cheat.parameter.step) + 1
|
||||
local chg = false
|
||||
if math.integer(idx) == idx then
|
||||
idx, chg = cheat:set_index(idx)
|
||||
end
|
||||
return cheat.parameter.value, chg
|
||||
end
|
||||
cheat.get_index = function(cheat) return cheat.parameter.index end
|
||||
cheat.get_value = function(cheat) return cheat.parameter.value end
|
||||
param.min = tonumber(param.min) or 0
|
||||
param.max = tonumber(param.max) or #param.item
|
||||
param.step = tonumber(param.step) or 1
|
||||
if param.item then
|
||||
for count, item in pairs(param.item) do
|
||||
if not item.value then
|
||||
item.value = (count * param.step) + param.min
|
||||
else
|
||||
item.value = tonumber(item.value)
|
||||
end
|
||||
end
|
||||
param.last = #param.item
|
||||
else
|
||||
param.last = ((param.max - param.min) / param.step) + 1
|
||||
end
|
||||
param.index = 0
|
||||
param.value = param.min
|
||||
cheat.cheat_env.param = param.min
|
||||
end
|
||||
|
||||
local hotkeymenu = false
|
||||
local hotkeylist = {}
|
||||
local commonui
|
||||
local poller
|
||||
|
||||
local function menu_populate()
|
||||
local menu = {}
|
||||
if hotkeymenu then
|
||||
local ioport = manager.machine.ioport
|
||||
local input = manager.machine.input
|
||||
|
||||
menu[1] = {_("Select cheat to set hotkey"), "", "off"}
|
||||
menu[2] = {string.format(_("Press %s to clear hotkey"), manager.ui:get_general_input_setting(ioport:token_to_input_type("UI_CLEAR"))), "", "off"}
|
||||
menu[3] = {"---", "", "off"}
|
||||
hotkeylist = {}
|
||||
|
||||
local function hkcbfunc(cheat, event)
|
||||
if poller then
|
||||
if poller:poll() then
|
||||
if poller.sequence then
|
||||
cheat.hotkeys = { pressed = false, keys = poller.sequence }
|
||||
end
|
||||
poller = nil
|
||||
return true
|
||||
end
|
||||
elseif event == "clear" then
|
||||
cheat.hotkeys = nil
|
||||
return true
|
||||
elseif event == "select" then
|
||||
if not commonui then
|
||||
commonui = require('commonui')
|
||||
end
|
||||
poller = commonui.switch_polling_helper()
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
for num, cheat in ipairs(cheats) do
|
||||
if cheat.script then
|
||||
local setting = cheat.hotkeys and input:seq_name(cheat.hotkeys.keys) or _("None")
|
||||
menu[#menu + 1] = {cheat.desc, setting, ""}
|
||||
hotkeylist[#hotkeylist + 1] = function(event) return hkcbfunc(cheat, event) end
|
||||
end
|
||||
end
|
||||
menu[#menu + 1] = {"---", "", ""}
|
||||
menu[#menu + 1] = {_("Done"), "", ""}
|
||||
if poller then
|
||||
return poller:overlay(menu)
|
||||
else
|
||||
return menu
|
||||
end
|
||||
end
|
||||
for num, cheat in ipairs(cheats) do
|
||||
menu[num] = {}
|
||||
menu[num][1] = cheat.desc
|
||||
if not cheat.parameter then
|
||||
if not cheat.script then
|
||||
if cheat.desc == "" then
|
||||
menu[num][1] = "---"
|
||||
end
|
||||
menu[num][2] = ""
|
||||
menu[num][3] = "off"
|
||||
elseif cheat.is_oneshot then
|
||||
menu[num][2] = _("Set")
|
||||
menu[num][3] = ""
|
||||
else
|
||||
if cheat.enabled then
|
||||
menu[num][2] = _("On")
|
||||
menu[num][3] = "l"
|
||||
else
|
||||
menu[num][2] = _("Off")
|
||||
menu[num][3] = "r"
|
||||
end
|
||||
end
|
||||
else
|
||||
if cheat.parameter.index == 0 then
|
||||
if cheat.is_oneshot then
|
||||
menu[num][2] = _("Set")
|
||||
else
|
||||
menu[num][2] = _("Off")
|
||||
end
|
||||
menu[num][3] = "r"
|
||||
else
|
||||
if cheat.parameter.item then
|
||||
menu[num][2] = cheat.parameter.item[cheat.parameter.index].text
|
||||
else
|
||||
menu[num][2] = cheat.parameter.value
|
||||
end
|
||||
menu[num][3] = "l"
|
||||
if cheat.parameter.index < cheat.parameter.last then
|
||||
menu[num][3] = "lr"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
menu[#menu + 1] = {"---", "", ""}
|
||||
menu[#menu + 1] = {_("Set hotkeys"), "", ""}
|
||||
menu[#menu + 1] = {_("Reset All"), "", ""}
|
||||
menu[#menu + 1] = {_("Reload All"), "", ""}
|
||||
return menu
|
||||
end
|
||||
|
||||
local function menu_callback(index, event)
|
||||
manager.machine:popmessage()
|
||||
if hotkeymenu then
|
||||
if event == "back" then
|
||||
hotkeymenu = false
|
||||
return true
|
||||
else
|
||||
index = index - 3
|
||||
if index >= 1 and index <= #hotkeylist then
|
||||
hotkeylist[index](event)
|
||||
return true
|
||||
elseif index == #hotkeylist + 2 and event == "select" then
|
||||
hotkeymenu = false
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
if index > #cheats and event == "select" then
|
||||
index = index - #cheats
|
||||
if index == 2 then
|
||||
hotkeymenu = true
|
||||
elseif index == 3 then
|
||||
for num, cheat in pairs(cheats) do
|
||||
cheat:set_enabled(false)
|
||||
if cheat.parameter then
|
||||
cheat:set_index(0)
|
||||
end
|
||||
end
|
||||
elseif index == 4 then
|
||||
for num, cheat in pairs(cheats) do
|
||||
cheat:set_enabled(false)
|
||||
end
|
||||
cheats = load_cheats()
|
||||
for num, cheat in pairs(cheats) do
|
||||
parse_cheat(cheat)
|
||||
end
|
||||
load_hotkeys()
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local cheat = cheats[index]
|
||||
if not cheat then
|
||||
return false
|
||||
end
|
||||
if event == "up" or event == "down" or event == "comment" then
|
||||
if cheat.comment then
|
||||
manager.machine:popmessage(string.format(_("Cheat Comment:\n%s"), cheat.comment))
|
||||
end
|
||||
elseif event == "left" then
|
||||
if cheat.parameter then
|
||||
local idx, chg = cheat:set_index(cheat:get_index() - 1)
|
||||
return chg
|
||||
else
|
||||
if not cheat.is_oneshot then
|
||||
local state, chg = cheat:set_enabled(false)
|
||||
return chg
|
||||
end
|
||||
return false
|
||||
end
|
||||
elseif event == "right" then
|
||||
if cheat.parameter then
|
||||
local idx, chg = cheat:set_index(cheat:get_index() + 1)
|
||||
return chg
|
||||
else
|
||||
if not cheat.is_oneshot then
|
||||
local state, chg = cheat:set_enabled(true)
|
||||
return chg
|
||||
end
|
||||
return false
|
||||
end
|
||||
elseif event == "select" then
|
||||
if cheat.is_oneshot then
|
||||
cheat:set_enabled(true)
|
||||
if cheat.parameter and cheat.script.change and cheat:get_index() ~= 0 then
|
||||
local itemtext
|
||||
if cheat.parameter.item then
|
||||
itemtext = cheat.parameter.item[cheat.parameter.index].text
|
||||
else
|
||||
itemtext = cheat.parameter.value
|
||||
end
|
||||
manager.machine:popmessage(string.format(_("Activated: %s = %s"), cheat.desc, itemtext))
|
||||
elseif not cheat.parameter and cheat.script.on then
|
||||
manager.machine:popmessage(string.format(_("Activated: %s"), cheat.desc))
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
emu.register_menu(function(index, event)
|
||||
return menu_callback(index, event)
|
||||
end,
|
||||
function()
|
||||
return menu_populate()
|
||||
end, _("Cheat"))
|
||||
|
||||
reset_subscription = emu.add_machine_reset_notifier(function ()
|
||||
if not stop then
|
||||
return
|
||||
end
|
||||
stop = false
|
||||
start_time = emu.time()
|
||||
cheats = load_cheats()
|
||||
local json = require("json")
|
||||
local file = io.open(manager.machine.options.entries.cheatpath:value():match("([^;]+)") .. "/output.json", "w")
|
||||
if file then
|
||||
file:write(json.stringify(cheats, {indent = true}))
|
||||
file:close()
|
||||
end
|
||||
for num, cheat in pairs(cheats) do
|
||||
parse_cheat(cheat)
|
||||
end
|
||||
load_hotkeys()
|
||||
if manager.machine.debugger then
|
||||
consolelog = manager.machine.debugger.consolelog
|
||||
consolelast = 0
|
||||
end
|
||||
end)
|
||||
|
||||
stop_subscription = emu.add_machine_stop_notifier(function ()
|
||||
stop = true
|
||||
consolelog = nil
|
||||
save_hotkeys()
|
||||
end)
|
||||
|
||||
frame_subscription = emu.add_machine_frame_notifier(function ()
|
||||
if stop then
|
||||
return
|
||||
end
|
||||
for num, cheat in pairs(cheats) do
|
||||
if cheat.enabled then
|
||||
run_if(cheat, cheat.script.run)
|
||||
end
|
||||
if cheat.hotkeys and cheat.hotkeys.keys then
|
||||
if manager.machine.input:seq_pressed(cheat.hotkeys.keys) then
|
||||
if not cheat.hotkeys.pressed then
|
||||
if cheat.is_oneshot then
|
||||
if not run_if(cheat, cheat.script.change) then
|
||||
run_if(cheat, cheat.script.on)
|
||||
end
|
||||
manager.machine:popmessage(string.format(_("Activated: %s"), cheat.desc))
|
||||
elseif not cheat.enabled then
|
||||
cheat.enabled = true
|
||||
run_if(cheat, cheat.script.on)
|
||||
manager.machine:popmessage(string.format(_("Enabled: %s"), cheat.desc))
|
||||
else
|
||||
cheat.enabled = false
|
||||
run_if(cheat, cheat.script.off)
|
||||
bwpclr(cheat)
|
||||
manager.machine:popmessage(string.format(_("Disabled: %s"), cheat.desc))
|
||||
end
|
||||
end
|
||||
cheat.hotkeys.pressed = true
|
||||
else
|
||||
cheat.hotkeys.pressed = false
|
||||
end
|
||||
end
|
||||
end
|
||||
for num, input in pairs(inputs) do
|
||||
local _, screen = next(manager.machine.screens)
|
||||
local framenum = screen:frame_number() - input.begin
|
||||
local enttab = input.start[framenum]
|
||||
if enttab then
|
||||
for num, entry in pairs(enttab) do
|
||||
entry.field:set_value(1)
|
||||
end
|
||||
end
|
||||
enttab = input.stop[framenum]
|
||||
if enttab then
|
||||
for num, entry in pairs(enttab) do
|
||||
entry.field:set_value(0)
|
||||
|
||||
end
|
||||
end
|
||||
if framenum >= input.last then
|
||||
table.remove(inputs, num)
|
||||
end
|
||||
end
|
||||
|
||||
end)
|
||||
|
||||
emu.register_frame_done(function()
|
||||
if stop then
|
||||
return
|
||||
end
|
||||
line = 0
|
||||
for num, draw in pairs(output) do
|
||||
if draw.type == "text" then
|
||||
if not draw.color then
|
||||
draw.scr:draw_text(draw.x, draw.y, draw.str)
|
||||
else
|
||||
draw.scr:draw_text(draw.x, draw.y, draw.str, draw.color)
|
||||
end
|
||||
elseif draw.type == "line" then
|
||||
draw.scr:draw_line(draw.x1, draw.y1, draw.x2, draw.y2, draw.color)
|
||||
elseif draw.type == "box" then
|
||||
draw.scr:draw_box(draw.x1, draw.y1, draw.x2, draw.y2, draw.linecolor, draw.bgcolor)
|
||||
end
|
||||
end
|
||||
output = {}
|
||||
end)
|
||||
|
||||
local ce = {}
|
||||
|
||||
-- interface to script cheat engine
|
||||
function ce.inject(newcheat)
|
||||
cheats[#cheats + 1] = newcheat
|
||||
parse_cheat(newcheat)
|
||||
manager.machine:popmessage(string.format(_("%s added"), newcheat.desc))
|
||||
end
|
||||
|
||||
function ce.get(index)
|
||||
local cheat = cheats[index]
|
||||
if not cheat then
|
||||
return nil
|
||||
end
|
||||
local intf = {
|
||||
get_enabled = function() return cheat:get_enabled() end,
|
||||
set_enabled = function(status) return cheat:set_enabled(status) end,
|
||||
desc = cheat.desc,
|
||||
is_oneshot = cheat.is_oneshot,
|
||||
comment = cheat.comment,
|
||||
get_hotkeys = function() if cheat.hotkeys then return cheat.hotkeys.keys end return nil end,
|
||||
set_hotkeys = function(seq) cheat.hotkeys = { pressed = false, keys = manager.machine.input:seq_clean(seq) } end
|
||||
}
|
||||
if cheat.script then
|
||||
intf.script = {}
|
||||
if cheat.script.on then intf.script.on = true end
|
||||
if cheat.script.off then intf.script.off = true end
|
||||
if cheat.script.run then intf.script.run = true end
|
||||
if cheat.script.change then intf.script.change = true end
|
||||
end
|
||||
|
||||
if cheat.parameter then
|
||||
intf.parameter = {}
|
||||
intf.get_value = function() return cheat:get_value() end
|
||||
intf.set_value = function(value) return cheat:set_value(value) end
|
||||
intf.get_index = function() return cheat:get_index() end
|
||||
intf.set_index = function(index) return cheat:set_index(index) end
|
||||
intf.parameter.min = cheat.parameter.min
|
||||
intf.parameter.max = cheat.parameter.max
|
||||
intf.parameter.step = cheat.parameter.step
|
||||
if cheat.parameter.item then
|
||||
intf.parameter.item = {}
|
||||
for idx, item in pairs(cheat.parameter.item) do
|
||||
intf.parameter.item[idx] = {}
|
||||
intf.parameter.item[idx].text = cheat.parameter.item[idx].text
|
||||
intf.parameter.item[idx].value = cheat.parameter.item[idx].value
|
||||
end
|
||||
end
|
||||
end
|
||||
return intf
|
||||
end
|
||||
|
||||
function ce.list()
|
||||
local list = {}
|
||||
for num, cheat in pairs(cheats) do
|
||||
list[num] = cheat.desc
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
_G.emu.plugin.cheat = ce
|
||||
|
||||
end
|
||||
|
||||
return exports
|
||||
10
bios/Arcade/MAME/plugins/cheat/plugin.json
Normal file
10
bios/Arcade/MAME/plugins/cheat/plugin.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"plugin": {
|
||||
"name": "cheat",
|
||||
"description": "Cheat plugin",
|
||||
"version": "0.0.1",
|
||||
"author": "Carl",
|
||||
"type": "plugin",
|
||||
"start": "false"
|
||||
}
|
||||
}
|
||||
11
bios/Arcade/MAME/plugins/cheat/xml_to_json.lua
Normal file
11
bios/Arcade/MAME/plugins/cheat/xml_to_json.lua
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
xml = require("cheat_xml")
|
||||
json = dofile("../json/init.lua")
|
||||
|
||||
function readAll(file)
|
||||
local f = io.open(file, "rb")
|
||||
local content = f:read("*all")
|
||||
f:close()
|
||||
return content
|
||||
end
|
||||
|
||||
print(json.stringify(xml.conv_cheat(readAll(arg[1])), {indent = true}))
|
||||
1079
bios/Arcade/MAME/plugins/cheatfind/init.lua
Normal file
1079
bios/Arcade/MAME/plugins/cheatfind/init.lua
Normal file
File diff suppressed because it is too large
Load diff
10
bios/Arcade/MAME/plugins/cheatfind/plugin.json
Normal file
10
bios/Arcade/MAME/plugins/cheatfind/plugin.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"plugin": {
|
||||
"name": "cheatfind",
|
||||
"description": "Cheat finder helper library",
|
||||
"version": "0.0.1",
|
||||
"author": "Carl",
|
||||
"type": "plugin",
|
||||
"start": "false"
|
||||
}
|
||||
}
|
||||
216
bios/Arcade/MAME/plugins/commonui/init.lua
Normal file
216
bios/Arcade/MAME/plugins/commonui/init.lua
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
-- license:BSD-3-Clause
|
||||
-- copyright-holders:Vas Crabb
|
||||
local exports = {
|
||||
name = 'commonui',
|
||||
version = '0.0.1',
|
||||
description = 'Common plugin UI helpers',
|
||||
license = 'BSD-3-Clause',
|
||||
author = { name = 'Vas Crabb' } }
|
||||
|
||||
|
||||
local commonui = exports
|
||||
|
||||
|
||||
function commonui.input_selection_menu(action, title, filter)
|
||||
menu = { }
|
||||
|
||||
local choices
|
||||
local index_first_choice
|
||||
local index_cancel
|
||||
|
||||
local function populate_choices()
|
||||
local ioport = manager.machine.ioport
|
||||
|
||||
local function compare(a, b)
|
||||
if a.device.tag < b.device.tag then
|
||||
return true
|
||||
elseif a.device.tag > b.device.tag then
|
||||
return false
|
||||
end
|
||||
groupa = ioport:type_group(a.type, a.player)
|
||||
groupb = ioport:type_group(b.type, b.player)
|
||||
if groupa < groupb then
|
||||
return true
|
||||
elseif groupa > groupb then
|
||||
return false
|
||||
elseif a.type < b.type then
|
||||
return true
|
||||
elseif a.type > b.type then
|
||||
return false
|
||||
else
|
||||
return a.name < b.name
|
||||
end
|
||||
end
|
||||
|
||||
choices = { }
|
||||
for tag, port in pairs(manager.machine.ioport.ports) do
|
||||
for name, field in pairs(port.fields) do
|
||||
if (not filter) or filter(field) then
|
||||
table.insert(choices, field)
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(choices, compare)
|
||||
|
||||
local index = 1
|
||||
local prev
|
||||
while index <= #choices do
|
||||
local current = choices[index]
|
||||
if (not prev) or (prev.device.tag ~= current.device.tag) then
|
||||
table.insert(choices, index, false)
|
||||
index = index + 2
|
||||
else
|
||||
index = index + 1
|
||||
end
|
||||
prev = current
|
||||
end
|
||||
end
|
||||
|
||||
function menu:populate(initial_selection)
|
||||
if not choices then
|
||||
populate_choices()
|
||||
end
|
||||
|
||||
local items = { }
|
||||
|
||||
if title then
|
||||
table.insert(items, { title, '', 'off' })
|
||||
table.insert(items, { '---', '', '' })
|
||||
end
|
||||
|
||||
index_first_choice = #items + 1
|
||||
local selection = index_first_choice
|
||||
for index, field in ipairs(choices) do
|
||||
if field then
|
||||
table.insert(items, { field.name, '', '' })
|
||||
if initial_selection and (field.port.tag == initial_selection.port.tag) and (field.mask == initial_selection.mask) and (field.type == initial_selection.type) then
|
||||
selection = #items
|
||||
initial_selection = nil
|
||||
end
|
||||
else
|
||||
local device = choices[index + 1].device
|
||||
if device.owner then
|
||||
table.insert(items, { string.format(_p('plugin-commonui', '%s [root%s]'), device.name, device.tag), '', 'heading' })
|
||||
else
|
||||
table.insert(items, { string.format(_p('plugin-commonui', '[root%s]'), device.tag), '', 'heading' })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(items, { '---', '', '' })
|
||||
table.insert(items, { _p('plugin-commonui', 'Cancel'), '', '' })
|
||||
index_cancel = #items
|
||||
|
||||
return items, selection
|
||||
end
|
||||
|
||||
function menu:handle(index, event)
|
||||
local selection
|
||||
if (event == 'back') or ((index == input_item_cancel) and (event == 'select')) then
|
||||
action(nil)
|
||||
return true
|
||||
elseif event == 'select' then
|
||||
local field = choices[index - index_first_choice + 1]
|
||||
if field then
|
||||
action(field)
|
||||
return true
|
||||
end
|
||||
elseif event == 'prevgroup' then
|
||||
local found_break = false
|
||||
while (index > index_first_choice) and (not selection) do
|
||||
index = index - 1
|
||||
if not choices[index - index_first_choice + 1] then
|
||||
if found_break then
|
||||
selection = index + 1
|
||||
else
|
||||
found_break = true
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif event == 'nextgroup' then
|
||||
while ((index - index_first_choice + 2) < #choices) and (not selection) do
|
||||
index = index + 1
|
||||
if not choices[index - index_first_choice + 1] then
|
||||
selection = index + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
return false, selection
|
||||
end
|
||||
|
||||
return menu
|
||||
end
|
||||
|
||||
|
||||
function commonui.switch_polling_helper(starting_sequence)
|
||||
helper = { }
|
||||
|
||||
local machine = manager.machine
|
||||
local cancel = machine.ioport:token_to_input_type('UI_CANCEL')
|
||||
local cancel_prompt = manager.ui:get_general_input_setting(cancel)
|
||||
local input = machine.input
|
||||
local uiinput = machine.uiinput
|
||||
local poller = input:switch_sequence_poller()
|
||||
local modified_ticks = 0
|
||||
|
||||
if starting_sequence then
|
||||
poller:start(starting_sequence)
|
||||
else
|
||||
poller:start()
|
||||
end
|
||||
|
||||
function helper:overlay(items, selection, flags)
|
||||
if flags then
|
||||
flags = flags .. " nokeys"
|
||||
else
|
||||
flags = "nokeys"
|
||||
end
|
||||
return items, selection, flags
|
||||
end
|
||||
|
||||
function helper:poll()
|
||||
-- prevent race condition between uiinput:pressed() and poll()
|
||||
if (modified_ticks == 0) and poller.modified then
|
||||
modified_ticks = emu.osd_ticks()
|
||||
end
|
||||
|
||||
if uiinput:pressed(cancel) then
|
||||
-- UI_CANCEL pressed, abort
|
||||
machine:popmessage()
|
||||
uiinput:reset()
|
||||
if (not poller.modified) or (modified_ticks == emu.osd_ticks()) then
|
||||
-- cancelled immediately
|
||||
self.sequence = nil -- TODO: communicate this better?
|
||||
return true
|
||||
else
|
||||
-- entered something before cancelling
|
||||
self.sequence = nil
|
||||
return true
|
||||
end
|
||||
elseif poller:poll() then
|
||||
uiinput:reset()
|
||||
if poller.valid then
|
||||
-- valid sequence entered
|
||||
machine:popmessage()
|
||||
self.sequence = poller.sequence
|
||||
return true
|
||||
else
|
||||
-- invalid sequence entered
|
||||
machine:popmessage(_p('plugin-commonui', 'Invalid combination entered'))
|
||||
self.sequence = nil
|
||||
return true
|
||||
end
|
||||
else
|
||||
machine:popmessage(string.format(
|
||||
_p('plugin-commonui', 'Enter combination or press %s to cancel\n%s'),
|
||||
cancel_prompt,
|
||||
input:seq_name(poller.sequence)))
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return helper
|
||||
end
|
||||
|
||||
|
||||
return exports
|
||||
9
bios/Arcade/MAME/plugins/commonui/plugin.json
Normal file
9
bios/Arcade/MAME/plugins/commonui/plugin.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"plugin": {
|
||||
"name": "commonui",
|
||||
"description": "Common plugin UI helpers",
|
||||
"version": "0.0.1",
|
||||
"author": "Vas Crabb",
|
||||
"type": "library"
|
||||
}
|
||||
}
|
||||
308
bios/Arcade/MAME/plugins/console/init.lua
Normal file
308
bios/Arcade/MAME/plugins/console/init.lua
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
-- license:MIT
|
||||
-- copyright-holders:Carl, Patrick Rapin, Reuben Thomas
|
||||
-- completion from https://github.com/rrthomas/lua-rlcompleter
|
||||
local exports = {}
|
||||
exports.name = "console"
|
||||
exports.version = "0.0.1"
|
||||
exports.description = "Console plugin"
|
||||
exports.license = "BSD-3-Clause"
|
||||
exports.author = { name = "Carl" }
|
||||
|
||||
local console = exports
|
||||
local history_file = "console_history"
|
||||
|
||||
local history_fullpath = nil
|
||||
|
||||
local reset_subscription, stop_subscription
|
||||
|
||||
function console.startplugin()
|
||||
local conth = emu.thread()
|
||||
local ln_started = false
|
||||
local started = false
|
||||
local stopped = false
|
||||
local ln = require("linenoise")
|
||||
local preload = false
|
||||
local matches = {}
|
||||
local lastindex = 0
|
||||
local consolebuf
|
||||
print(" /| /| /| /| /| _______")
|
||||
print(" / | / | / | / | / | / /")
|
||||
print(" / |/ | / | / |/ | / ____/ ")
|
||||
print(" / | / | / | / /_ ")
|
||||
print(" / |/ | / |/ __/ ")
|
||||
print(" / /| /| /| |/ /| /| /____ ")
|
||||
print(" / / | / | / | / | / | / ")
|
||||
print("/ _/ |/ / / |___/ |/ /_______/ ")
|
||||
print(" / / ")
|
||||
print(" / _/ \n")
|
||||
print(emu.app_name() .. " " .. emu.app_version(), "\nCopyright (C) Nicola Salmoria and the MAME team\n");
|
||||
print(_VERSION, "\nCopyright (C) Lua.org, PUC-Rio\n");
|
||||
-- linenoise isn't thread safe but that means history can handled here
|
||||
-- that also means that bad things will happen if anything outside lua tries to use it
|
||||
-- especially the completion callback
|
||||
ln.historysetmaxlen(50)
|
||||
local scr = [[
|
||||
local ln = require('linenoise')
|
||||
ln.setcompletion(
|
||||
function(c, str)
|
||||
status = str
|
||||
yield()
|
||||
for candidate in status:gmatch('([^\001]+)') do
|
||||
ln.addcompletion(c, candidate)
|
||||
end
|
||||
end)
|
||||
local ret = ln.linenoise('$PROMPT')
|
||||
if ret == nil then
|
||||
return "\n"
|
||||
end
|
||||
return ret
|
||||
]]
|
||||
local keywords = {
|
||||
'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 'for',
|
||||
'function', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat',
|
||||
'return', 'then', 'true', 'until', 'while'
|
||||
}
|
||||
local cmdbuf = ""
|
||||
|
||||
-- Main completion function. It evaluates the current sub-expression
|
||||
-- to determine its type. Currently supports tables fields, global
|
||||
-- variables and function prototype completion.
|
||||
local function contextual_list(expr, sep, str, word, strs)
|
||||
local function add(value)
|
||||
value = tostring(value)
|
||||
if value:match("^" .. word) then
|
||||
matches[#matches + 1] = value
|
||||
end
|
||||
end
|
||||
|
||||
-- This function is called in a context where a keyword or a global
|
||||
-- variable can be inserted. Local variables cannot be listed!
|
||||
local function add_globals()
|
||||
for _, k in ipairs(keywords) do
|
||||
add(k)
|
||||
end
|
||||
for k in pairs(_G) do
|
||||
add(k)
|
||||
end
|
||||
end
|
||||
|
||||
if expr and expr ~= "" then
|
||||
local v = load("local STRING = {'" .. table.concat(strs,"','") .. "'} return " .. expr)
|
||||
if v then
|
||||
err, v = pcall(v)
|
||||
if (not err) or (not v) then
|
||||
add_globals()
|
||||
return
|
||||
end
|
||||
local t = type(v)
|
||||
if sep == '.' or sep == ':' then
|
||||
if t == 'table' then
|
||||
for k, v in pairs(v) do
|
||||
if type(k) == 'string' and (sep ~= ':' or type(v) == "function") then
|
||||
add(k)
|
||||
end
|
||||
end
|
||||
elseif t == 'userdata' then
|
||||
for k, v in pairs(getmetatable(v)) do
|
||||
if type(k) == 'string' and (sep ~= ':' or type(v) == "function") then
|
||||
add(k)
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif sep == '[' then
|
||||
if t == 'table' then
|
||||
for k in pairs(v) do
|
||||
if type(k) == 'number' then
|
||||
add(k .. "]")
|
||||
end
|
||||
end
|
||||
if word ~= "" then add_globals() end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if #matches == 0 then
|
||||
add_globals()
|
||||
end
|
||||
end
|
||||
|
||||
local function find_unmatch(str, openpar, pair)
|
||||
local done = false
|
||||
if not str:match(openpar) then
|
||||
return str
|
||||
end
|
||||
local tmp = str:gsub(pair, "")
|
||||
if not tmp:match(openpar) then
|
||||
return str
|
||||
end
|
||||
repeat
|
||||
str = str:gsub(".-" .. openpar .. "(.*)", function (s)
|
||||
tmp = s:gsub(pair, "")
|
||||
if not tmp:match(openpar) then
|
||||
done = true
|
||||
end
|
||||
return s
|
||||
end)
|
||||
until done or str == ""
|
||||
return str
|
||||
end
|
||||
|
||||
-- This complex function tries to simplify the input line, by removing
|
||||
-- literal strings, full table constructors and balanced groups of
|
||||
-- parentheses. Returns the sub-expression preceding the word, the
|
||||
-- separator item ( '.', ':', '[', '(' ) and the current string in case
|
||||
-- of an unfinished string literal.
|
||||
local function simplify_expression(expr, word)
|
||||
local strs = {}
|
||||
-- Replace annoying sequences \' and \" inside literal strings
|
||||
expr = expr:gsub("\\(['\"])", function (c)
|
||||
return string.format("\\%03d", string.byte(c))
|
||||
end)
|
||||
local curstring
|
||||
-- Remove (finished and unfinished) literal strings
|
||||
while true do
|
||||
local idx1, _, equals = expr:find("%[(=*)%[")
|
||||
local idx2, _, sign = expr:find("(['\"])")
|
||||
if idx1 == nil and idx2 == nil then
|
||||
break
|
||||
end
|
||||
local idx, startpat, endpat
|
||||
if (idx1 or math.huge) < (idx2 or math.huge) then
|
||||
idx, startpat, endpat = idx1, "%[" .. equals .. "%[", "%]" .. equals .. "%]"
|
||||
else
|
||||
idx, startpat, endpat = idx2, sign, sign
|
||||
end
|
||||
if expr:sub(idx):find("^" .. startpat .. ".-" .. endpat) then
|
||||
expr = expr:gsub(startpat .. "(.-)" .. endpat, function (str)
|
||||
strs[#strs + 1] = str
|
||||
return " STRING[" .. #strs .. "] "
|
||||
end)
|
||||
else
|
||||
expr = expr:gsub(startpat .. "(.*)", function (str)
|
||||
curstring = str
|
||||
return "(CURSTRING "
|
||||
end)
|
||||
end
|
||||
end
|
||||
-- crop string at unmatched open paran
|
||||
expr = find_unmatch(expr, "%(", "%b()")
|
||||
expr = find_unmatch(expr, "%[", "%b[]")
|
||||
--expr = expr:gsub("%b()"," PAREN ") -- Remove groups of parentheses
|
||||
expr = expr:gsub("%b{}"," TABLE ") -- Remove table constructors
|
||||
-- Avoid two consecutive words without operator
|
||||
expr = expr:gsub("(%w)%s+(%w)","%1|%2")
|
||||
expr = expr:gsub("%s+", "") -- Remove now useless spaces
|
||||
-- This main regular expression looks for table indexes and function calls.
|
||||
return curstring, strs, expr:match("([%.:%w%(%)%[%]_]-)([:%.%[%(])" .. word .. "$")
|
||||
end
|
||||
|
||||
local function get_completions(line)
|
||||
matches = {}
|
||||
local start, word = line:match("^(.*[ \t\n\"\\'><=;:%+%-%*/%%^~#{}%(%)%[%].,])(.-)$")
|
||||
if not start then
|
||||
start = ""
|
||||
word = word or line
|
||||
else
|
||||
word = word or ""
|
||||
end
|
||||
|
||||
local str, strs, expr, sep = simplify_expression(line, word)
|
||||
contextual_list(expr, sep, str, word, strs)
|
||||
if #matches == 0 then
|
||||
return line
|
||||
elseif #matches == 1 then
|
||||
return start .. matches[1]
|
||||
end
|
||||
print("")
|
||||
result = { }
|
||||
for k, v in pairs(matches) do
|
||||
print(v)
|
||||
table.insert(result, start .. v)
|
||||
end
|
||||
return table.concat(result, '\001')
|
||||
end
|
||||
|
||||
reset_subscription = emu.add_machine_reset_notifier(function ()
|
||||
if not consolebuf and manager.machine.debugger then
|
||||
consolebuf = manager.machine.debugger.consolelog
|
||||
lastindex = 0
|
||||
end
|
||||
end)
|
||||
|
||||
stop_subscription = emu.add_machine_stop_notifier(function ()
|
||||
consolebuf = nil
|
||||
end)
|
||||
|
||||
emu.register_periodic(function ()
|
||||
if stopped then
|
||||
return
|
||||
end
|
||||
if (not started) then
|
||||
-- options are not available in startplugin, so we load the history here
|
||||
local homepath = manager.options.entries.homepath:value():match("([^;]+)")
|
||||
history_fullpath = homepath .. '/' .. history_file
|
||||
ln.loadhistory(history_fullpath)
|
||||
started = true
|
||||
end
|
||||
local prompt = "\x1b[1;36m[MAME]\x1b[0m> "
|
||||
if consolebuf and (#consolebuf > lastindex) then
|
||||
local last = #consolebuf
|
||||
print("\n")
|
||||
while lastindex < last do
|
||||
lastindex = lastindex + 1
|
||||
print(consolebuf[lastindex])
|
||||
end
|
||||
-- ln.refresh() FIXME: how to replicate this now that the API has been removed?
|
||||
end
|
||||
if conth.yield then
|
||||
conth:continue(get_completions(conth.result))
|
||||
return
|
||||
elseif conth.busy then
|
||||
return
|
||||
elseif ln_started then
|
||||
local cmd = conth.result
|
||||
if cmd == "\n" then
|
||||
stopped = true
|
||||
return
|
||||
elseif cmd == "" then
|
||||
if cmdbuf ~= "" then
|
||||
print("Incomplete command")
|
||||
cmdbuf = ""
|
||||
end
|
||||
else
|
||||
cmdbuf = cmdbuf .. "\n" .. cmd
|
||||
ln.historyadd(cmd)
|
||||
local func, err = load(cmdbuf)
|
||||
if not func then
|
||||
if err:match("<eof>") then
|
||||
prompt = "\x1b[1;36m[MAME]\x1b[0m>> "
|
||||
else
|
||||
print("error: ", err)
|
||||
cmdbuf = ""
|
||||
end
|
||||
else
|
||||
cmdbuf = ""
|
||||
stopped = true
|
||||
local status
|
||||
status, err = pcall(func)
|
||||
if not status then
|
||||
print("error: ", err)
|
||||
end
|
||||
stopped = false
|
||||
end
|
||||
end
|
||||
end
|
||||
conth:start(scr:gsub("$PROMPT", prompt))
|
||||
ln_started = true
|
||||
end)
|
||||
end
|
||||
|
||||
setmetatable(console, {
|
||||
__gc = function ()
|
||||
if history_fullpath then
|
||||
local ln = require("linenoise")
|
||||
ln.savehistory(history_fullpath)
|
||||
end
|
||||
end})
|
||||
|
||||
return exports
|
||||
10
bios/Arcade/MAME/plugins/console/plugin.json
Normal file
10
bios/Arcade/MAME/plugins/console/plugin.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"plugin": {
|
||||
"name": "console",
|
||||
"description": "Console plugin",
|
||||
"version": "0.0.1",
|
||||
"author": "Carl",
|
||||
"type": "plugin",
|
||||
"start": "false"
|
||||
}
|
||||
}
|
||||
245
bios/Arcade/MAME/plugins/data/button_char.lua
Normal file
245
bios/Arcade/MAME/plugins/data/button_char.lua
Normal 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
|
||||
36
bios/Arcade/MAME/plugins/data/data_command.lua
Normal file
36
bios/Arcade/MAME/plugins/data/data_command.lua
Normal 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
|
||||
27
bios/Arcade/MAME/plugins/data/data_gameinit.lua
Normal file
27
bios/Arcade/MAME/plugins/data/data_gameinit.lua
Normal 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
|
||||
1278
bios/Arcade/MAME/plugins/data/data_hiscore.lua
Normal file
1278
bios/Arcade/MAME/plugins/data/data_hiscore.lua
Normal file
File diff suppressed because it is too large
Load diff
218
bios/Arcade/MAME/plugins/data/data_history.lua
Normal file
218
bios/Arcade/MAME/plugins/data/data_history.lua
Normal 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
|
||||
38
bios/Arcade/MAME/plugins/data/data_mameinfo.lua
Normal file
38
bios/Arcade/MAME/plugins/data/data_mameinfo.lua
Normal 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
|
||||
179
bios/Arcade/MAME/plugins/data/data_marp.lua
Normal file
179
bios/Arcade/MAME/plugins/data/data_marp.lua
Normal 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
|
||||
38
bios/Arcade/MAME/plugins/data/data_messinfo.lua
Normal file
38
bios/Arcade/MAME/plugins/data/data_messinfo.lua
Normal 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
|
||||
35
bios/Arcade/MAME/plugins/data/data_story.lua
Normal file
35
bios/Arcade/MAME/plugins/data/data_story.lua
Normal 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
|
||||
27
bios/Arcade/MAME/plugins/data/data_sysinfo.lua
Normal file
27
bios/Arcade/MAME/plugins/data/data_sysinfo.lua
Normal 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
|
||||
159
bios/Arcade/MAME/plugins/data/database.lua
Normal file
159
bios/Arcade/MAME/plugins/data/database.lua
Normal 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
|
||||
91
bios/Arcade/MAME/plugins/data/init.lua
Normal file
91
bios/Arcade/MAME/plugins/data/init.lua
Normal 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
|
||||
260
bios/Arcade/MAME/plugins/data/load_dat.lua
Normal file
260
bios/Arcade/MAME/plugins/data/load_dat.lua
Normal 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
|
||||
10
bios/Arcade/MAME/plugins/data/plugin.json
Normal file
10
bios/Arcade/MAME/plugins/data/plugin.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"plugin": {
|
||||
"name": "data",
|
||||
"description": "Data plugin",
|
||||
"version": "0.0.1",
|
||||
"author": "Carl",
|
||||
"type": "plugin",
|
||||
"start": "true"
|
||||
}
|
||||
}
|
||||
117
bios/Arcade/MAME/plugins/discord/init.lua
Normal file
117
bios/Arcade/MAME/plugins/discord/init.lua
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
-- license:BSD-3-Clause
|
||||
-- copyright-holders:Carl
|
||||
local exports = {
|
||||
name = "discord",
|
||||
version = "0.0.1",
|
||||
description = "Discord presence",
|
||||
license = "BSD-3-Clause",
|
||||
author = { name = "Carl" } }
|
||||
|
||||
local discord = exports
|
||||
|
||||
local reset_subscription, pause_subscription, resume_subscription
|
||||
|
||||
function discord.startplugin()
|
||||
local pipe = emu.file("rw")
|
||||
local json = require("json")
|
||||
local nonce = 1
|
||||
local starttime = 0
|
||||
|
||||
local function init()
|
||||
local path
|
||||
if package.config:sub(1,1) == '\\' then
|
||||
path = "\\\\.\\pipe\\discord-ipc-0"
|
||||
else
|
||||
path = os.getenv("XDG_RUNTIME_DIR") or os.getenv("TMPDIR") or os.getenv("TMP") or os.getenv("TEMP") or '/tmp'
|
||||
path = "domain." .. path .. "/discord-ipc-0"
|
||||
end
|
||||
local err = pipe:open(path)
|
||||
if err then
|
||||
error("discord: unable to connect, " .. err .. "\n")
|
||||
end
|
||||
local output = json.stringify({v = 1, client_id = "453309506152169472"})
|
||||
--print(output)
|
||||
pipe:write(string.pack("<I4I4", 0, #output) .. output)
|
||||
local time = os.time()
|
||||
local data = ""
|
||||
repeat
|
||||
local res = pipe:read(100)
|
||||
data = data .. res
|
||||
until #res == 0 and #data > 0 or time + 1 < os.time()
|
||||
--print(data)
|
||||
if data:find("code", 1, true) then
|
||||
error("discord: bad RPC reply, " .. data:sub(8) .. "\n")
|
||||
end
|
||||
if #data == 0 then
|
||||
error("discord: timed out waiting for response\n");
|
||||
end
|
||||
end
|
||||
|
||||
local function update(status)
|
||||
if not pipe then return end
|
||||
local running = emu.romname() ~= "___empty"
|
||||
local state = not running and "In menu" or status
|
||||
local details = running and manager.machine.system.description or nil
|
||||
if emu.softname() ~= "" then
|
||||
for name, dev in pairs(manager.machine.images) do
|
||||
if dev.software_longname then
|
||||
details = details .. " (" .. dev.software_longname .. ")"
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
local status = {
|
||||
cmd = "SET_ACTIVITY",
|
||||
args = {
|
||||
pid = emu.pid(),
|
||||
activity = {
|
||||
state = state,
|
||||
details = details,
|
||||
timestamps = {
|
||||
start = starttime
|
||||
}
|
||||
}
|
||||
},
|
||||
nonce = nonce
|
||||
}
|
||||
nonce = nonce + 1
|
||||
local output = json.stringify(status)
|
||||
--print(output)
|
||||
pipe:write(string.pack("<I4I4", 1, #output) .. output)
|
||||
local time = os.time()
|
||||
local data = ""
|
||||
repeat
|
||||
local res = pipe:read(100)
|
||||
data = data .. res
|
||||
until #res == 0 and #data > 0 or time + 1 < os.time()
|
||||
if #data == 0 then
|
||||
emu.print_verbose("discord: timed out waiting for response, closing connection");
|
||||
pipe = nil
|
||||
end
|
||||
--print(data)
|
||||
end
|
||||
|
||||
do
|
||||
local stat, err = pcall(init)
|
||||
if not stat then
|
||||
emu.print_verbose(err)
|
||||
pipe = nil
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
reset_subscription = emu.add_machine_reset_notifier(function ()
|
||||
starttime = os.time()
|
||||
update("Playing")
|
||||
end)
|
||||
|
||||
pause_subscription = emu.add_machine_pause_notifier(function ()
|
||||
update("Paused")
|
||||
end)
|
||||
|
||||
resume_subscription = emu.add_machine_resume_notifier(function ()
|
||||
update("Playing")
|
||||
end)
|
||||
end
|
||||
|
||||
return exports
|
||||
10
bios/Arcade/MAME/plugins/discord/plugin.json
Normal file
10
bios/Arcade/MAME/plugins/discord/plugin.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"plugin": {
|
||||
"name": "discord",
|
||||
"description": "Discord presence",
|
||||
"version": "0.0.1",
|
||||
"author": "Carl",
|
||||
"type": "plugin",
|
||||
"start": "false"
|
||||
}
|
||||
}
|
||||
37
bios/Arcade/MAME/plugins/dummy/init.lua
Normal file
37
bios/Arcade/MAME/plugins/dummy/init.lua
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
-- license:BSD-3-Clause
|
||||
-- copyright-holders:Miodrag Milanovic
|
||||
local exports = {
|
||||
name = "dummy",
|
||||
version = "0.0.1",
|
||||
description = "A dummy example",
|
||||
license = "BSD-3-Clause",
|
||||
author = { name = "Miodrag Milanovic" }}
|
||||
|
||||
local dummy = exports
|
||||
|
||||
local reset_subscription, stop_subscription
|
||||
|
||||
function dummy.startplugin()
|
||||
reset_subscription = emu.add_machine_reset_notifier(
|
||||
function ()
|
||||
emu.print_info("Starting " .. emu.gamename())
|
||||
end)
|
||||
|
||||
stop_subscription = emu.add_machine_stop_notifier(
|
||||
function ()
|
||||
emu.print_info("Exiting " .. emu.gamename())
|
||||
end)
|
||||
|
||||
local function menu_populate()
|
||||
return {{ "This is a", "test", "off" }, { "Also a", "test", "" }}
|
||||
end
|
||||
|
||||
local function menu_callback(index, event)
|
||||
emu.print_info("index: " .. index .. " event: " .. event)
|
||||
return false
|
||||
end
|
||||
|
||||
emu.register_menu(menu_callback, menu_populate, "Dummy")
|
||||
end
|
||||
|
||||
return exports
|
||||
10
bios/Arcade/MAME/plugins/dummy/plugin.json
Normal file
10
bios/Arcade/MAME/plugins/dummy/plugin.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"plugin": {
|
||||
"name": "dummy",
|
||||
"description": "Dummy test plugin",
|
||||
"version": "0.0.1",
|
||||
"author": "Miodrag Milanovic",
|
||||
"type": "plugin",
|
||||
"start": "false"
|
||||
}
|
||||
}
|
||||
291
bios/Arcade/MAME/plugins/gdbstub/init.lua
Normal file
291
bios/Arcade/MAME/plugins/gdbstub/init.lua
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
-- license:BSD-3-Clause
|
||||
-- copyright-holders: Carl
|
||||
local exports = {
|
||||
name = "gdbstub",
|
||||
version = "0.0.1",
|
||||
description = "GDB stub plugin",
|
||||
license = "BSD-3-Clause",
|
||||
author = { name = "Carl" } }
|
||||
|
||||
local gdbstub = exports
|
||||
|
||||
-- percpu mapping of mame registers to gdb register order
|
||||
local regmaps = {
|
||||
i386 = {
|
||||
togdb = {
|
||||
EAX = 1, ECX = 2, EDX = 3, EBX = 4, ESP = 5, EBP = 6, ESI = 7, EDI = 8, EIP = 9, EFLAGS = 10, CS = 11, SS = 12,
|
||||
DS = 13, ES = 14, FS = 15, GS = 16 },
|
||||
fromgdb = {
|
||||
"EAX", "ECX", "EDX", "EBX", "ESP", "EBP", "ESI", "EDI", "EIP", "EFLAGS", "CS", "SS", "DS", "ES", "FS", "GS" },
|
||||
regsize = 4,
|
||||
addrsize = 4,
|
||||
pcreg = "EIP"
|
||||
}
|
||||
}
|
||||
regmaps.i486 = regmaps.i386
|
||||
regmaps.pentium = regmaps.i386
|
||||
|
||||
local reset_subscription, stop_subscription
|
||||
|
||||
function gdbstub.startplugin()
|
||||
local debugger
|
||||
local debug
|
||||
local cpu
|
||||
local breaks
|
||||
local watches
|
||||
local consolelog
|
||||
local consolelast
|
||||
local running
|
||||
|
||||
reset_subscription = emu.add_machine_reset_notifier(function ()
|
||||
debugger = manager.machine.debugger
|
||||
if not debugger then
|
||||
print("gdbstub: debugger not enabled")
|
||||
return
|
||||
end
|
||||
cpu = manager.machine.devices[":maincpu"]
|
||||
if not cpu then
|
||||
print("gdbstub: maincpu not found")
|
||||
end
|
||||
if not regmaps[cpu.shortname] then
|
||||
print("gdbstub: no register map for cpu " .. cpu.shortname)
|
||||
cpu = nil
|
||||
end
|
||||
consolelog = debugger.consolelog
|
||||
consolelast = 0
|
||||
breaks = {byaddr = {}, byidx = {}}
|
||||
watches = {byaddr = {}, byidx = {}}
|
||||
running = false
|
||||
end)
|
||||
|
||||
stop_subscription = emu.add_machine_stop_notifier(function ()
|
||||
consolelog = nil
|
||||
cpu = nil
|
||||
debug = nil
|
||||
end)
|
||||
|
||||
local socket = emu.file("", 7)
|
||||
local connected = false
|
||||
socket:open("socket.127.0.0.1:2159")
|
||||
|
||||
emu.register_periodic(function ()
|
||||
if not cpu then
|
||||
return
|
||||
end
|
||||
|
||||
if running and debugger.execution_state == "stop" then
|
||||
socket:write("$S05#B8")
|
||||
running = false
|
||||
return
|
||||
elseif debugger.execution_state == "run" then
|
||||
running = true
|
||||
end
|
||||
|
||||
local function chksum(str)
|
||||
local sum = 0
|
||||
str:gsub(".", function(s) sum = sum + s:byte() end)
|
||||
return string.format("%.2x", sum & 0xff)
|
||||
end
|
||||
|
||||
local function makebestr(val, len)
|
||||
local str = ""
|
||||
for count = 0, len - 1 do
|
||||
str = str .. string.format("%.2x", (val >> (count * 8)) & 0xff)
|
||||
end
|
||||
return str
|
||||
end
|
||||
|
||||
local last = consolelast
|
||||
local msg = consolelog[#consolelog]
|
||||
consolelast = #consolelog
|
||||
if #consolelog > last and msg:find("Stopped at", 1, true) then
|
||||
local point = tonumber(msg:match("Stopped at breakpoint ([0-9]+)"))
|
||||
local map = regmaps[cpu.shortname]
|
||||
running = false
|
||||
if not point then
|
||||
point = tonumber(msg:match("Stopped at watchpoint ([0-9]+"))
|
||||
if not point then
|
||||
return -- ??
|
||||
end
|
||||
local wp = watches.byidx[point]
|
||||
if wp then
|
||||
local reply = "T05" .. wp.type .. ":" .. makebestr(wp.addr, map.addrsize)
|
||||
socket:write("$" .. reply .. "#" .. chksum(reply))
|
||||
else
|
||||
socket:write("$S05#B8")
|
||||
end
|
||||
return
|
||||
else
|
||||
local bp = breaks.byidx[point]
|
||||
if bp then
|
||||
local reply = "T05hwbreak:" .. makebestr(cpu.state[map.pcreg].value, map.regsize)
|
||||
socket:write("$" .. reply .. "#" .. chksum(reply))
|
||||
else
|
||||
socket:write("$S05#B8")
|
||||
end
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
if running and debugger.execution_state == "stop" then
|
||||
socket:write("$S05#B8")
|
||||
running = false
|
||||
return
|
||||
elseif debugger.execution_state == "run" then
|
||||
running = true
|
||||
end
|
||||
|
||||
local data = ""
|
||||
|
||||
repeat
|
||||
local read = socket:read(100)
|
||||
data = data .. read
|
||||
until #read == 0
|
||||
if #data == 0 then
|
||||
return
|
||||
end
|
||||
if data == "\x03" then
|
||||
debugger.execution_state = "stop"
|
||||
socket:write("$S05#B8")
|
||||
running = false
|
||||
return
|
||||
end
|
||||
local packet, checksum = data:match("%$([^#]+)#(%x%x)")
|
||||
if packet then
|
||||
packet:gsub("}(.)", function(s) return string.char(string.byte(s) ~ 0x20) end)
|
||||
local cmd = packet:sub(1, 1)
|
||||
local map = regmaps[cpu.shortname]
|
||||
if cmd == "g" then
|
||||
local regs = {}
|
||||
for reg, idx in pairs(map.togdb) do
|
||||
regs[idx] = makebestr(cpu.state[reg].value, map.regsize)
|
||||
end
|
||||
local data = table.concat(regs)
|
||||
socket:write("+$" .. data .. "#" .. chksum(data))
|
||||
elseif cmd == "G" then
|
||||
local count = 0
|
||||
packet:sub(2):gsub(string.rep("%x", map.regsize * 2), function(s)
|
||||
count = count + 1
|
||||
cpu.state[map.fromgdb[count]].value = tonumber(s,16)
|
||||
end)
|
||||
socket:write("+$OK#9a")
|
||||
elseif cmd == "m" then
|
||||
local addr, len = packet:match("m(%x+),(%x+)")
|
||||
if addr and len then
|
||||
addr = tonumber(addr, 16)
|
||||
len = tonumber(len, 16)
|
||||
local data = ""
|
||||
local space = cpu.spaces["program"]
|
||||
for count = 1, len do
|
||||
data = data .. string.format("%.2x", space:readv_u8(addr))
|
||||
addr = addr + 1
|
||||
end
|
||||
socket:write("+$" .. data .. "#" .. chksum(data))
|
||||
else
|
||||
socket:write("+$E00#a5") -- fix error
|
||||
end
|
||||
elseif cmd == "M" then
|
||||
local count = 0
|
||||
local addr, len, data = packet:match("M(%x+),(%x+),(%x+)")
|
||||
if addr and len and data then
|
||||
addr = tonumber(addr, 16)
|
||||
local space = cpu.spaces["program"]
|
||||
data:gsub("%x%x", function(s) space:writev_u8(addr + count, tonumber(s, 16)) count = count + 1 end)
|
||||
socket:write("+$OK#9a")
|
||||
else
|
||||
socket:write("+$E00#a5")
|
||||
end
|
||||
elseif cmd == "s" then
|
||||
if #packet == 1 then
|
||||
cpu.debug:step()
|
||||
socket:write("+$OK#9a")
|
||||
socket:write("$S05#B8")
|
||||
running = false
|
||||
else
|
||||
socket:write("+$E00#a5")
|
||||
end
|
||||
elseif cmd == "c" then
|
||||
if #packet == 1 then
|
||||
cpu.debug:go()
|
||||
socket:write("+$OK#9a")
|
||||
else
|
||||
socket:write("+$E00#a5")
|
||||
end
|
||||
elseif cmd == "Z" then
|
||||
local btype, addr, kind = packet:match("Z([0-4]),(%x+),(.*)")
|
||||
addr = tonumber(addr, 16)
|
||||
if btype == "0" then
|
||||
socket:write("") -- is machine dependant
|
||||
elseif btype == "1" then
|
||||
if breaks.byaddr[addr] then
|
||||
socket:write("+$E00#a5")
|
||||
return
|
||||
end
|
||||
local idx = cpu.debug:bpset(addr)
|
||||
breaks.byaddr[addr] = idx
|
||||
breaks.byidx[idx] = addr
|
||||
socket:write("+$OK#9a")
|
||||
elseif btype == "2" then
|
||||
if watches.byaddr[addr] then
|
||||
socket:write("+$E00#a5")
|
||||
return
|
||||
end
|
||||
local idx = cpu.debug:wpset(cpu.spaces["program"], "w", addr, 1)
|
||||
watches.byaddr[addr] = idx
|
||||
watches.byidx[idx] = {addr = addr, type = "watch"}
|
||||
socket:write("+$OK#9a")
|
||||
elseif btype == "3" then
|
||||
if watches.byaddr[addr] then
|
||||
socket:write("+$E00#a5")
|
||||
return
|
||||
end
|
||||
local idx = cpu.debug:wpset(cpu.spaces["program"], "r", addr, 1)
|
||||
watches.byaddr[addr] = idx
|
||||
watches.byidx[idx] = {addr = addr, type = "rwatch"}
|
||||
socket:write("+$OK#9a")
|
||||
elseif btype == "4" then
|
||||
if watches.byaddr[addr] then
|
||||
socket:write("+$E00#a5")
|
||||
return
|
||||
end
|
||||
local idx = cpu.debug:wpset(cpu.spaces["program"], "rw", addr, 1)
|
||||
watches.byaddr[addr] = idx
|
||||
watches.byidx[idx] = {addr = addr, type = "awatch"}
|
||||
socket:write("+$OK#9a")
|
||||
end
|
||||
elseif cmd == "z" then
|
||||
local btype, addr, kind = packet:match("z([0-4]),(%x+),(.*)")
|
||||
addr = tonumber(addr, 16)
|
||||
if btype == "0" then
|
||||
socket:write("") -- is machine dependent
|
||||
elseif btype == "1" then
|
||||
if not breaks.byaddr[addr] then
|
||||
socket:write("+$E00#a5")
|
||||
return
|
||||
end
|
||||
local idx = breaks.byaddr[addr]
|
||||
cpu.debug:bpclr(idx)
|
||||
breaks.byaddr[addr] = nil
|
||||
breaks.byidx[idx] = nil
|
||||
socket:write("+$OK#9a")
|
||||
elseif btype == "2" or btype == "3" or btype == "4" then
|
||||
if not watches.byaddr[addr] then
|
||||
socket:write("+$E00#a5")
|
||||
return
|
||||
end
|
||||
local idx = watches.byaddr[addr]
|
||||
cpu.debug:wpclr(idx)
|
||||
watches.byaddr[addr] = nil
|
||||
watches.byidx[idx] = nil
|
||||
socket:write("+$OK#9a")
|
||||
end
|
||||
elseif cmd == "?" then
|
||||
socket:write("+$S05#B8")
|
||||
else
|
||||
socket:write("+$#00")
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return exports
|
||||
10
bios/Arcade/MAME/plugins/gdbstub/plugin.json
Normal file
10
bios/Arcade/MAME/plugins/gdbstub/plugin.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"plugin": {
|
||||
"name": "gdbstub",
|
||||
"description": "GDB stub plugin",
|
||||
"version": "0.0.1",
|
||||
"author": "Carl",
|
||||
"type": "plugin",
|
||||
"start": "false"
|
||||
}
|
||||
}
|
||||
376
bios/Arcade/MAME/plugins/hiscore/init.lua
Normal file
376
bios/Arcade/MAME/plugins/hiscore/init.lua
Normal file
|
|
@ -0,0 +1,376 @@
|
|||
-- hiscore.lua
|
||||
-- by borgar@borgar.net, CC0 license
|
||||
--
|
||||
-- This uses MAME's built-in Lua scripting to implement
|
||||
-- high-score saving with hiscore.dat infom just as older
|
||||
-- builds did in the past.
|
||||
--
|
||||
local exports = {
|
||||
name = 'hiscore',
|
||||
version = '1.0.1',
|
||||
description = 'Hiscore',
|
||||
license = 'CC0',
|
||||
author = { name = 'borgar@borgar.net' } }
|
||||
|
||||
local hiscore = exports
|
||||
|
||||
local hiscore_plugin_path
|
||||
local reset_subscription, frame_subscription, stop_subscription
|
||||
|
||||
function hiscore.set_folder(path)
|
||||
hiscore_plugin_path = path
|
||||
end
|
||||
|
||||
function hiscore.startplugin()
|
||||
|
||||
local function get_data_path()
|
||||
return manager.machine.options.entries.homepath:value():match('([^;]+)') .. '/hiscore'
|
||||
end
|
||||
|
||||
-- configuration
|
||||
local config_read = false
|
||||
local timed_save = true
|
||||
|
||||
-- read configuration file from data directory
|
||||
local function read_config()
|
||||
if config_read then
|
||||
return true
|
||||
end
|
||||
local filename = get_data_path() .. '/plugin.cfg'
|
||||
local file = io.open(filename, 'r')
|
||||
if file then
|
||||
local json = require('json')
|
||||
local parsed_settings = json.parse(file:read('a'))
|
||||
file:close()
|
||||
if parsed_settings then
|
||||
if parsed_settings.only_save_at_exit and (parsed_settings.only_save_at_exit ~= 0) then
|
||||
timed_save = false
|
||||
end
|
||||
-- TODO: other settings? maybe path overrides for hiscore.dat or the hiscore data?
|
||||
config_read = true
|
||||
return true
|
||||
else
|
||||
emu.print_error(string.format('Error loading hiscore plugin settings: error parsing file "%s" as JSON', filename))
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- save configuration file
|
||||
local function save_config()
|
||||
local path = get_data_path()
|
||||
local attr = lfs.attributes(path)
|
||||
if not attr then
|
||||
lfs.mkdir(path)
|
||||
elseif attr.mode ~= 'directory' then
|
||||
emu.print_error(string.format('Error saving hiscore plugin settings: "%s" is not a directory', path))
|
||||
return
|
||||
end
|
||||
local settings = { only_save_at_exit = not timed_save }
|
||||
-- TODO: other settings?
|
||||
local filename = path .. '/plugin.cfg'
|
||||
local json = require('json')
|
||||
local data = json.stringify(settings, { indent = true })
|
||||
local file = io.open(filename, 'w')
|
||||
if not file then
|
||||
emu.print_error(string.format('Error saving hiscore plugin settings: error opening file "%s" for writing', filename))
|
||||
return
|
||||
end
|
||||
file:write(data)
|
||||
file:close()
|
||||
end
|
||||
|
||||
-- build menu
|
||||
local function populate_menu()
|
||||
local items = { }
|
||||
local setting = timed_save and _p('plugin-hiscore', 'When updated') or _p('plugin-hiscore', 'On exit')
|
||||
table.insert(items, { _p('plugin-hiscore', 'Hiscore Support Options'), '', 'off' })
|
||||
table.insert(items, { '---', '', '' })
|
||||
table.insert(items, { _p('plugin-hiscore', 'Save scores'), setting, timed_save and 'l' or 'r' })
|
||||
return items
|
||||
end
|
||||
|
||||
-- handle menu events
|
||||
local function handle_menu(index, event)
|
||||
if event == 'left' then
|
||||
timed_save = false
|
||||
return true
|
||||
elseif event == 'right' then
|
||||
timed_save = true
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local hiscoredata_path = "hiscore.dat";
|
||||
|
||||
local current_checksum = 0;
|
||||
local default_checksum = 0;
|
||||
|
||||
local scores_have_been_read = false;
|
||||
local mem_check_passed = false;
|
||||
local found_hiscore_entry = false;
|
||||
local delaytime = 0;
|
||||
|
||||
local function parse_table ( dsting )
|
||||
local _table = {}
|
||||
for line in string.gmatch(dsting, '([^\n]+)') do
|
||||
local delay = line:match('^@delay=([.%d]*)')
|
||||
if delay and #delay > 0 then
|
||||
delaytime = emu.time() + tonumber(delay)
|
||||
else
|
||||
local cpu, mem;
|
||||
local cputag, space, offs, len, chk_st, chk_ed, fill = string.match(line, '^@([^,]+),([^,]+),([^,]+),([^,]+),([^,]+),([^,]+),?(%x?%x?)');
|
||||
cpu = manager.machine.devices[cputag];
|
||||
if not cpu then
|
||||
error(cputag .. " device not found")
|
||||
end
|
||||
local rgnname, rgntype = space:match("([^/]*)/?([^/]*)")
|
||||
if rgntype == "share" then
|
||||
mem = manager.machine.memory.shares[rgnname]
|
||||
else
|
||||
mem = cpu.spaces[space]
|
||||
end
|
||||
if not mem then
|
||||
error(space .. " space not found")
|
||||
end
|
||||
_table[ #_table + 1 ] = {
|
||||
mem = mem,
|
||||
addr = tonumber(offs, 16),
|
||||
size = tonumber(len, 16),
|
||||
c_start = tonumber(chk_st, 16),
|
||||
c_end = tonumber(chk_ed, 16),
|
||||
fill = tonumber(fill, 16)
|
||||
};
|
||||
end
|
||||
end
|
||||
return _table;
|
||||
end
|
||||
|
||||
|
||||
local function read_hiscore_dat ()
|
||||
local file = io.open( hiscoredata_path, "r" );
|
||||
local rm_match;
|
||||
if not file then
|
||||
file = io.open( hiscore_plugin_path .. "/hiscore.dat", "r" );
|
||||
end
|
||||
if emu.softname() ~= "" then
|
||||
local soft = emu.softname():match("([^:]*)$")
|
||||
rm_match = '^' .. emu.romname() .. ',' .. soft .. ':';
|
||||
else
|
||||
rm_match = '^' .. emu.romname() .. ':';
|
||||
end
|
||||
local cluster = "";
|
||||
local current_is_match = false;
|
||||
if file then
|
||||
repeat
|
||||
line = file:read("*l");
|
||||
if line then
|
||||
-- remove comments
|
||||
line = line:gsub( '[ \t\r\n]*;.+$', '' );
|
||||
-- handle lines
|
||||
if string.find(line, '^@') then -- data line
|
||||
if current_is_match then
|
||||
cluster = cluster .. "\n" .. line;
|
||||
end
|
||||
elseif string.find(line, rm_match) then --- match this game
|
||||
current_is_match = true;
|
||||
elseif string.find(line, '^[a-z0-9_,]+:') then --- some game
|
||||
if current_is_match and string.len(cluster) > 0 then
|
||||
break; -- we're done
|
||||
end
|
||||
else --- empty line or garbage
|
||||
-- noop
|
||||
end
|
||||
end
|
||||
until not line;
|
||||
file:close();
|
||||
end
|
||||
return cluster;
|
||||
end
|
||||
|
||||
|
||||
local function check_mem ( posdata )
|
||||
if #posdata < 1 then
|
||||
return false;
|
||||
end
|
||||
for ri,row in ipairs(posdata) do
|
||||
-- must pass mem check
|
||||
if row["c_start"] ~= row["mem"]:read_u8(row["addr"]) then
|
||||
return false;
|
||||
end
|
||||
if row["c_end"] ~= row["mem"]:read_u8(row["addr"]+row["size"]-1) then
|
||||
return false;
|
||||
end
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
|
||||
local function get_file_name()
|
||||
local r;
|
||||
if emu.softname() ~= "" then
|
||||
local soft = emu.softname():match("([^:]*)$")
|
||||
r = get_data_path() .. '/' .. emu.romname() .. "_" .. soft .. ".hi";
|
||||
else
|
||||
r = get_data_path() .. '/' .. emu.romname() .. ".hi";
|
||||
end
|
||||
return r;
|
||||
end
|
||||
|
||||
|
||||
local function write_scores ( posdata )
|
||||
emu.print_verbose("hiscore: write_scores")
|
||||
local output = io.open(get_file_name(), "wb");
|
||||
if not output then
|
||||
-- attempt to create the directory, and try again
|
||||
lfs.mkdir(get_data_path());
|
||||
output = io.open(get_file_name(), "wb");
|
||||
end
|
||||
emu.print_verbose("hiscore: write_scores output")
|
||||
if output then
|
||||
for ri,row in ipairs(posdata) do
|
||||
t = {}
|
||||
for i=0,row["size"]-1 do
|
||||
t[i+1] = row["mem"]:read_u8(row["addr"] + i)
|
||||
end
|
||||
output:write(string.char(table.unpack(t)));
|
||||
end
|
||||
output:close();
|
||||
end
|
||||
emu.print_verbose("hiscore: write_scores end")
|
||||
end
|
||||
|
||||
|
||||
local function read_scores ( posdata )
|
||||
local input = io.open(get_file_name(), "rb");
|
||||
if input then
|
||||
for ri,row in ipairs(posdata) do
|
||||
local str = input:read(row["size"]);
|
||||
for i=0,row["size"]-1 do
|
||||
local b = str:sub(i+1,i+1):byte();
|
||||
row["mem"]:write_u8( row["addr"] + i, b );
|
||||
end
|
||||
end
|
||||
input:close();
|
||||
return true;
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
|
||||
local function check_scores ( posdata )
|
||||
local r = 0;
|
||||
for ri,row in ipairs(posdata) do
|
||||
for i=0,row["size"]-1 do
|
||||
r = r + row["mem"]:read_u8( row["addr"] + i );
|
||||
end
|
||||
end
|
||||
return r;
|
||||
end
|
||||
|
||||
|
||||
local function init ()
|
||||
if not scores_have_been_read then
|
||||
if (delaytime <= emu.time()) and check_mem( positions ) then
|
||||
default_checksum = check_scores( positions );
|
||||
if read_scores( positions ) then
|
||||
emu.print_verbose( "hiscore: scores read OK" );
|
||||
else
|
||||
-- likely there simply isn't a .hi file around yet
|
||||
emu.print_verbose( "hiscore: scores read FAIL" );
|
||||
end
|
||||
scores_have_been_read = true;
|
||||
current_checksum = check_scores( positions );
|
||||
mem_check_passed = true;
|
||||
else
|
||||
-- memory check can fail while the game is still warming up
|
||||
-- TODO: only allow it to fail N many times
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local last_write_time = -10;
|
||||
local function tick ()
|
||||
-- set up scores if they have been
|
||||
init();
|
||||
-- only allow save check to run when
|
||||
if mem_check_passed and timed_save then
|
||||
-- The reason for this complicated mess is that
|
||||
-- MAME does expose a hook for "exit". Once it does,
|
||||
-- this should obviously just be done when the emulator
|
||||
-- shuts down (or reboots).
|
||||
local checksum = check_scores( positions );
|
||||
if checksum ~= current_checksum and checksum ~= default_checksum then
|
||||
-- 5 sec grace time so we don't clobber io and cause
|
||||
-- latency. This would be bad as it would only ever happen
|
||||
-- to players currently reaching a new highscore
|
||||
if emu.time() > last_write_time + 5 then
|
||||
write_scores( positions );
|
||||
current_checksum = checksum;
|
||||
last_write_time = emu.time();
|
||||
-- emu.print_verbose( "SAVE SCORES EVENT!", last_write_time );
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function reset()
|
||||
-- the notifier will still be attached even if the running game has no hiscore.dat entry
|
||||
if mem_check_passed and found_hiscore_entry then
|
||||
local checksum = check_scores(positions)
|
||||
if checksum ~= current_checksum and checksum ~= default_checksum then
|
||||
write_scores(positions)
|
||||
end
|
||||
end
|
||||
found_hiscore_entry = false
|
||||
mem_check_passed = false
|
||||
scores_have_been_read = false;
|
||||
end
|
||||
|
||||
reset_subscription = emu.add_machine_reset_notifier(function ()
|
||||
found_hiscore_entry = false
|
||||
mem_check_passed = false
|
||||
scores_have_been_read = false;
|
||||
last_write_time = -10
|
||||
emu.print_verbose("Starting " .. emu.gamename())
|
||||
read_config();
|
||||
local dat = read_hiscore_dat()
|
||||
if dat and dat ~= "" then
|
||||
emu.print_verbose( "hiscore: found hiscore.dat entry for " .. emu.romname() );
|
||||
res, positions = pcall(parse_table, dat);
|
||||
if not res then
|
||||
emu.print_error("hiscore: hiscore.dat parse error " .. positions);
|
||||
return;
|
||||
end
|
||||
for i, row in pairs(positions) do
|
||||
if row.fill then
|
||||
for i=0,row["size"]-1 do
|
||||
row["mem"]:write_u8(row["addr"] + i, row.fill)
|
||||
end
|
||||
end
|
||||
end
|
||||
found_hiscore_entry = true
|
||||
end
|
||||
end)
|
||||
|
||||
frame_subscription = emu.add_machine_frame_notifier(function ()
|
||||
if found_hiscore_entry then
|
||||
tick()
|
||||
end
|
||||
end)
|
||||
|
||||
stop_subscription = emu.add_machine_stop_notifier(function ()
|
||||
reset()
|
||||
save_config()
|
||||
end)
|
||||
|
||||
emu.register_prestart(function ()
|
||||
reset()
|
||||
end)
|
||||
|
||||
emu.register_menu(handle_menu, populate_menu, _p('plugin-hiscore', 'Hiscore Support'))
|
||||
end
|
||||
|
||||
return exports
|
||||
10
bios/Arcade/MAME/plugins/hiscore/plugin.json
Normal file
10
bios/Arcade/MAME/plugins/hiscore/plugin.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"plugin": {
|
||||
"name": "hiscore",
|
||||
"description": "Hiscore support",
|
||||
"version": "1.0.1",
|
||||
"author": "borgar@borgar.net",
|
||||
"type": "plugin",
|
||||
"start": "false"
|
||||
}
|
||||
}
|
||||
217
bios/Arcade/MAME/plugins/hiscore/sort_hiscore.lua
Normal file
217
bios/Arcade/MAME/plugins/hiscore/sort_hiscore.lua
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
if #arg ~= 2 then
|
||||
print("usage: lua sort_hiscore.lua hiscore.dat mame.lst")
|
||||
return
|
||||
end
|
||||
|
||||
local datfile = io.open(arg[1])
|
||||
|
||||
if not datfile then
|
||||
return
|
||||
end
|
||||
|
||||
local entries = {{}}
|
||||
local namelist = false
|
||||
local comment = false
|
||||
local entry = entries[1]
|
||||
for line in datfile:lines() do
|
||||
local function next_entry()
|
||||
entries[#entries + 1] = {}
|
||||
comment = false
|
||||
namelist = false
|
||||
return entries[#entries]
|
||||
end
|
||||
|
||||
local function additem(table, item)
|
||||
if not entry[table] then
|
||||
entry[table] = {}
|
||||
end
|
||||
entry[table][#entry[table] + 1 ] = item
|
||||
end
|
||||
|
||||
local function clean(data)
|
||||
return data:match("^([^%s;]+)"):lower()
|
||||
end
|
||||
|
||||
if line:match("^%w") then
|
||||
if not comment and not namelist then
|
||||
entry = next_entry()
|
||||
end
|
||||
namelist = true
|
||||
else
|
||||
namelist = false
|
||||
end
|
||||
if line:match("^%s*$") then
|
||||
entry = next_entry()
|
||||
end
|
||||
line = line:match("^%s*.-%s*$")
|
||||
local char = line:sub(1,1)
|
||||
if char == ";" then
|
||||
additem("comment", line)
|
||||
comment = true
|
||||
else
|
||||
comment = false
|
||||
end
|
||||
if char:match("%w") then
|
||||
additem("name", clean(line):sub(1, -2))
|
||||
end
|
||||
if char == "@" then
|
||||
additem("data", clean(line))
|
||||
end
|
||||
end
|
||||
|
||||
datfile:close()
|
||||
|
||||
local lstfile = io.open(arg[2])
|
||||
|
||||
local list = {}
|
||||
local src = "error"
|
||||
for line in lstfile:lines() do
|
||||
if not line:match("^[%s/*]") then
|
||||
if line:sub(1,1) == "@" then
|
||||
src = line:match("^@source:(.*)")
|
||||
else
|
||||
local set = line:match("^([^%s/]*)")
|
||||
if set and set ~= "" then
|
||||
list[set] = src
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
lstfile:close()
|
||||
|
||||
local sorted = {}
|
||||
local sindex = {}
|
||||
local comments = ""
|
||||
|
||||
for num, entry in pairs(entries) do
|
||||
if not entry.name then
|
||||
if entry.comment then
|
||||
if entry.comment[1]:sub(2,4) ~= "@s:" then
|
||||
comments = comments .. table.concat(entry.comment, "\n") .. "\n"
|
||||
end
|
||||
end
|
||||
else
|
||||
table.sort(entry.name)
|
||||
entry.src = "source not found"
|
||||
for num, name in pairs(entry.name) do
|
||||
name = name:match("[^,]*")
|
||||
if not list[name] then
|
||||
entry.name[num] = entry.name[num] .. ": ; missing"
|
||||
else
|
||||
entry.name[num] = entry.name[num] .. ":"
|
||||
entry.src = list[name]
|
||||
end
|
||||
end
|
||||
entry.data = table.concat(entry.data, "\n")
|
||||
if entry.comment then
|
||||
entry.comment = table.concat(entry.comment, "\n")
|
||||
end
|
||||
sorted[#sorted + 1] = entry
|
||||
if not sindex[entry.src] then
|
||||
sindex[entry.src] = {}
|
||||
end
|
||||
sindex[entry.src][#sorted] = entry
|
||||
end
|
||||
end
|
||||
|
||||
for src, entries in pairs(sindex) do
|
||||
for num1, entry in pairs(entries) do
|
||||
for num2, entry2 in pairs(entries) do
|
||||
if entry.name and entry ~= entry2 and entry.data == entry2.data then
|
||||
for num3, name in pairs(entry2.name) do
|
||||
entry.name[#entry.name + 1] = name
|
||||
end
|
||||
if entry2.comment then
|
||||
if not entry.comment then
|
||||
entry.comment = entry2.comment
|
||||
elseif entry.comment ~= entry2.comment then
|
||||
entry.comment = entry.comment .. "\n" .. entry2.comment
|
||||
end
|
||||
end
|
||||
sorted[num2] = {}
|
||||
entries[num2] = {}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local nindex = {}
|
||||
|
||||
for num1, entry in pairs(sorted) do
|
||||
if entry.name then
|
||||
for num2, name in pairs(entry.name) do
|
||||
local curname = name:match("[^:]*")
|
||||
if nindex[curname] then
|
||||
if nindex[curname] == entry then
|
||||
entry.name[num2] = ""
|
||||
else
|
||||
print(curname, "duplicate name")
|
||||
end
|
||||
else
|
||||
nindex[curname] = entry
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- copyright 2010 Uli Schlachter GPLv2
|
||||
local function stable_sort(list, comp)
|
||||
-- A table could contain non-integer keys which we have to ignore.
|
||||
local num = 0
|
||||
for k, v in ipairs(list) do
|
||||
num = num + 1
|
||||
end
|
||||
|
||||
if num <= 1 then
|
||||
-- Nothing to do
|
||||
return
|
||||
end
|
||||
|
||||
-- Sort until everything is sorted :)
|
||||
local sorted = false
|
||||
local n = num
|
||||
while not sorted do
|
||||
sorted = true
|
||||
for i = 1, n - 1 do
|
||||
-- Two equal elements won't be swapped -> we are stable
|
||||
if comp(list[i+1], list[i]) then
|
||||
local tmp = list[i]
|
||||
list[i] = list[i+1]
|
||||
list[i+1] = tmp
|
||||
|
||||
sorted = false
|
||||
end
|
||||
end
|
||||
-- The last element is now guaranteed to be in the right spot
|
||||
n = n - 1
|
||||
end
|
||||
end
|
||||
stable_sort(sorted, function(a,b)
|
||||
if a.src and b.src then
|
||||
return a.src < b.src
|
||||
elseif not a.src then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end)
|
||||
|
||||
local src = "error";
|
||||
|
||||
print(comments)
|
||||
|
||||
for num, entry in ipairs(sorted) do
|
||||
if entry.name then
|
||||
if entry.src and entry.src ~= src then
|
||||
print(";@s:" .. entry.src .. "\n")
|
||||
src = entry.src
|
||||
end
|
||||
if entry.comment then
|
||||
print(entry.comment)
|
||||
end
|
||||
print((table.concat(entry.name, "\n"):gsub("\n+","\n"):gsub("\n$","")))
|
||||
print(entry.data)
|
||||
print("\n")
|
||||
end
|
||||
end
|
||||
140
bios/Arcade/MAME/plugins/inputmacro/init.lua
Normal file
140
bios/Arcade/MAME/plugins/inputmacro/init.lua
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
-- license:BSD-3-Clause
|
||||
-- copyright-holders:Vas Crabb
|
||||
local exports = {
|
||||
name = 'inputmacro',
|
||||
version = '0.0.1',
|
||||
description = 'Input macro plugin',
|
||||
license = 'BSD-3-Clause',
|
||||
author = { name = 'Vas Crabb' } }
|
||||
|
||||
|
||||
local inputmacro = exports
|
||||
|
||||
local frame_subscription, stop_subscription
|
||||
|
||||
function inputmacro.startplugin()
|
||||
--[[
|
||||
Configuration data:
|
||||
* name: display name (string)
|
||||
* binding: activation sequence (input sequence)
|
||||
* bindingcfg: activation sequence configuration (string)
|
||||
* earlycancel: cancel or complete on release (Boolean)
|
||||
* loop: -1 = release, 0 = prolong, >0 = loop to step on hold (integer)
|
||||
* steps:
|
||||
* inputs:
|
||||
* port: port tag (string)
|
||||
* mask: port field mask (integer)
|
||||
* type: port field type (integer)
|
||||
* field: field (I/O port field)
|
||||
* delay: delay before activating inputs in frames (integer)
|
||||
* duration: duration to activate inputs for (integer)
|
||||
|
||||
Live state:
|
||||
* step: current step (integer or nil)
|
||||
* frame: frame of current step, starting at 1 (integer)
|
||||
]]
|
||||
local macros = { }
|
||||
local active_inputs = { }
|
||||
local menu
|
||||
local input
|
||||
|
||||
local function activate_inputs(inputs)
|
||||
for index, input in ipairs(inputs) do
|
||||
if input.field then
|
||||
active_inputs[string.format('%s.%d.%d', input.port, input.mask, input.type)] = input.field
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function process_frame()
|
||||
local previous_inputs = active_inputs
|
||||
active_inputs = { }
|
||||
|
||||
for index, macro in ipairs(macros) do
|
||||
if macro.step then
|
||||
if macro.earlycancel and (not input:seq_pressed(macro.binding)) then
|
||||
-- stop immediately on release if early cancel set
|
||||
macro.step = nil
|
||||
else
|
||||
-- advance frame
|
||||
macro.frame = macro.frame + 1
|
||||
local step = macro.steps[macro.step]
|
||||
if macro.frame > (step.delay + step.duration) then
|
||||
if macro.step < #macro.steps then
|
||||
-- not the last step, advance step
|
||||
macro.step = macro.step + 1
|
||||
macro.frame = 1
|
||||
step = macro.steps[macro.step]
|
||||
elseif not input:seq_pressed(macro.binding) then
|
||||
-- input released and macro completed
|
||||
macro.step = nil
|
||||
step = nil
|
||||
elseif macro.loop > 0 then
|
||||
-- loop to step
|
||||
macro.step = macro.loop
|
||||
macro.frame = 1
|
||||
elseif macro.loop < 0 then
|
||||
-- release if held
|
||||
step = nil
|
||||
end
|
||||
end
|
||||
if step and (macro.frame > step.delay) then
|
||||
activate_inputs(step.inputs)
|
||||
end
|
||||
end
|
||||
elseif input:seq_pressed(macro.binding) then
|
||||
-- initial activation
|
||||
macro.step = 1
|
||||
macro.frame = 1
|
||||
local step = macro.steps[1]
|
||||
if step.delay == 0 then
|
||||
-- no delay on first step, activate inputs
|
||||
activate_inputs(step.inputs)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for key, field in pairs(active_inputs) do
|
||||
field:set_value(1)
|
||||
end
|
||||
for key, field in pairs(previous_inputs) do
|
||||
if not active_inputs[key] then
|
||||
field:clear_value()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function start()
|
||||
input = manager.machine.input
|
||||
local persister = require('inputmacro/inputmacro_persist')
|
||||
macros = persister.load_settings()
|
||||
end
|
||||
|
||||
local function stop()
|
||||
local persister = require('inputmacro/inputmacro_persist')
|
||||
persister:save_settings(macros)
|
||||
|
||||
macros = { }
|
||||
active_inputs = { }
|
||||
menu = nil
|
||||
end
|
||||
|
||||
local function menu_callback(index, event)
|
||||
return menu:handle_event(index, event)
|
||||
end
|
||||
|
||||
local function menu_populate()
|
||||
if not menu then
|
||||
menu = require('inputmacro/inputmacro_menu')
|
||||
menu:init(macros)
|
||||
end
|
||||
return menu:populate()
|
||||
end
|
||||
|
||||
frame_subscription = emu.add_machine_frame_notifier(process_frame)
|
||||
emu.register_prestart(start)
|
||||
stop_subscription = emu.add_machine_stop_notifier(stop)
|
||||
emu.register_menu(menu_callback, menu_populate, _p('plugin-inputmacro', 'Input Macros'))
|
||||
end
|
||||
|
||||
return exports
|
||||
640
bios/Arcade/MAME/plugins/inputmacro/inputmacro_menu.lua
Normal file
640
bios/Arcade/MAME/plugins/inputmacro/inputmacro_menu.lua
Normal file
|
|
@ -0,0 +1,640 @@
|
|||
-- license:BSD-3-Clause
|
||||
-- copyright-holders:Vas Crabb
|
||||
|
||||
|
||||
-- Constants
|
||||
|
||||
local MENU_TYPES = { MACROS = 0, ADD = 1, EDIT = 2, INPUT = 3 }
|
||||
|
||||
|
||||
-- Globals
|
||||
|
||||
local commonui
|
||||
local macros
|
||||
local menu_stack
|
||||
|
||||
local macros_start_macro -- really for the macros menu, but have to be declared local before edit menu functions
|
||||
local macros_item_first_macro
|
||||
local macros_selection_save
|
||||
|
||||
|
||||
-- Helpers
|
||||
|
||||
local function new_macro()
|
||||
local function check_name(n)
|
||||
for index, macro in ipairs(macros) do
|
||||
if macro.name == n then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local name = _p('plugin-inputmacro', 'New macro')
|
||||
local number = 1
|
||||
while not check_name(name) do
|
||||
number = number + 1
|
||||
name = string.format(_p('plugin-inputmacro', 'New macro %d'), number)
|
||||
end
|
||||
return {
|
||||
name = name,
|
||||
binding = nil,
|
||||
bindingcfg = '',
|
||||
earlycancel = true,
|
||||
loop = -1,
|
||||
steps = {
|
||||
{
|
||||
inputs = {
|
||||
{
|
||||
port = nil,
|
||||
mask = nil,
|
||||
type = nil,
|
||||
field = nil } },
|
||||
delay = 0,
|
||||
duration = 1 } } }
|
||||
end
|
||||
|
||||
|
||||
-- Input menu
|
||||
|
||||
local input_menu
|
||||
local input_start_field
|
||||
|
||||
function start_input_menu(handler, start_field)
|
||||
local function supported(f)
|
||||
if f.is_analog or f.is_toggle then
|
||||
return false
|
||||
elseif (f.type_class == 'config') or (f.type_class == 'dipswitch') then
|
||||
return false
|
||||
else
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local function action(field)
|
||||
if field then
|
||||
handler(field)
|
||||
end
|
||||
table.remove(menu_stack)
|
||||
input_menu = nil
|
||||
input_start_field = nil
|
||||
end
|
||||
|
||||
if not commonui then
|
||||
commonui = require('commonui')
|
||||
end
|
||||
input_menu = commonui.input_selection_menu(action, _p('plugin-inputmacro', 'Set Input'), supported)
|
||||
input_start_field = start_field
|
||||
table.insert(menu_stack, MENU_TYPES.INPUT)
|
||||
end
|
||||
|
||||
local function handle_input(index, action)
|
||||
return input_menu:handle(index, action)
|
||||
end
|
||||
|
||||
local function populate_input()
|
||||
return input_menu:populate(input_start_field)
|
||||
end
|
||||
|
||||
|
||||
-- Add/edit menus
|
||||
|
||||
local edit_current_macro
|
||||
local edit_start_selection
|
||||
local edit_start_step
|
||||
local edit_menu_active
|
||||
local edit_insert_position
|
||||
local edit_name_buffer
|
||||
local edit_items
|
||||
local edit_item_delete
|
||||
local edit_item_exit
|
||||
local edit_switch_poller
|
||||
|
||||
local function current_macro_complete()
|
||||
if not edit_current_macro.binding then
|
||||
return false
|
||||
end
|
||||
local laststep = edit_current_macro.steps[#edit_current_macro.steps]
|
||||
if not laststep.inputs[#laststep.inputs].field then
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function handle_edit_items(index, event)
|
||||
if edit_switch_poller then
|
||||
if edit_switch_poller:poll() then
|
||||
if edit_switch_poller.sequence then
|
||||
edit_current_macro.binding = edit_switch_poller.sequence
|
||||
edit_current_macro.bindingcfg = manager.machine.input:seq_to_tokens(edit_switch_poller.sequence)
|
||||
end
|
||||
edit_switch_poller = nil
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local command = edit_items[index]
|
||||
|
||||
local namecancel = false
|
||||
if edit_name_buffer and ((not command) or (command.action ~= 'name')) then
|
||||
edit_name_buffer = nil
|
||||
namecancel = true
|
||||
end
|
||||
|
||||
if not command then
|
||||
return namecancel
|
||||
elseif command.action == 'name' then
|
||||
local function namechar()
|
||||
local ch = tonumber(event)
|
||||
if not ch then
|
||||
return nil
|
||||
elseif (ch >= 0x100) or ((ch & 0x7f) >= 0x20) or (ch == 0x08) then
|
||||
return utf8.char(ch)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
if edit_name_buffer then
|
||||
if event == 'select' then
|
||||
if #edit_name_buffer > 0 then
|
||||
edit_current_macro.name = edit_name_buffer
|
||||
end
|
||||
edit_name_buffer = nil
|
||||
return true
|
||||
elseif event == 'back' then
|
||||
return true -- swallow back while editing text
|
||||
elseif event == 'cancel' then
|
||||
edit_name_buffer = nil
|
||||
return true
|
||||
else
|
||||
local char = namechar()
|
||||
if char == '\b' then
|
||||
edit_name_buffer = edit_name_buffer:gsub('[%z\1-\127\192-\255][\128-\191]*$', '')
|
||||
return true
|
||||
elseif char then
|
||||
edit_name_buffer = edit_name_buffer .. char
|
||||
return true
|
||||
end
|
||||
end
|
||||
elseif event == 'select' then
|
||||
edit_name_buffer = edit_current_macro.name
|
||||
return true
|
||||
else
|
||||
local char = namechar()
|
||||
if char == '\b' then
|
||||
edit_name_buffer = ''
|
||||
return true
|
||||
elseif char then
|
||||
edit_name_buffer = char
|
||||
return true
|
||||
end
|
||||
end
|
||||
elseif command.action == 'binding' then
|
||||
if event == 'select' then
|
||||
if not commonui then
|
||||
commonui = require('commonui')
|
||||
end
|
||||
edit_switch_poller = commonui.switch_polling_helper()
|
||||
return true
|
||||
end
|
||||
elseif command.action == 'releaseaction' then
|
||||
if (event == 'select') or (event == 'left') or (event == 'right') then
|
||||
edit_current_macro.earlycancel = not edit_current_macro.earlycancel
|
||||
return true
|
||||
end
|
||||
elseif command.action == 'holdaction' then
|
||||
if event == 'left' then
|
||||
edit_current_macro.loop = edit_current_macro.loop - 1
|
||||
return true
|
||||
elseif event == 'right' then
|
||||
edit_current_macro.loop = edit_current_macro.loop + 1
|
||||
return true
|
||||
elseif event == 'clear' then
|
||||
edit_current_macro.loop = -1
|
||||
return true
|
||||
end
|
||||
elseif command.action == 'delay' then
|
||||
local step = edit_current_macro.steps[command.step]
|
||||
if event == 'left' then
|
||||
step.delay = step.delay - 1
|
||||
return true
|
||||
elseif event == 'right' then
|
||||
step.delay = step.delay + 1
|
||||
return true
|
||||
elseif event == 'clear' then
|
||||
step.delay = 0
|
||||
return true
|
||||
end
|
||||
elseif command.action == 'duration' then
|
||||
local step = edit_current_macro.steps[command.step]
|
||||
if event == 'left' then
|
||||
step.duration = step.duration - 1
|
||||
return true
|
||||
elseif event == 'right' then
|
||||
step.duration = step.duration + 1
|
||||
return true
|
||||
elseif event == 'clear' then
|
||||
step.duration = 1
|
||||
return true
|
||||
end
|
||||
elseif command.action == 'input' then
|
||||
local inputs = edit_current_macro.steps[command.step].inputs
|
||||
if event == 'select' then
|
||||
local function hanlder(field)
|
||||
inputs[command.input].port = field.port.tag
|
||||
inputs[command.input].mask = field.mask
|
||||
inputs[command.input].type = field.type
|
||||
inputs[command.input].field = field
|
||||
end
|
||||
start_input_menu(hanlder, inputs[command.input].field)
|
||||
edit_start_selection = index
|
||||
return true
|
||||
elseif event == 'clear' then
|
||||
if #inputs > 1 then
|
||||
table.remove(inputs, command.input)
|
||||
return true
|
||||
end
|
||||
end
|
||||
elseif command.action == 'addinput' then
|
||||
if event == 'select' then
|
||||
local inputs = edit_current_macro.steps[command.step].inputs
|
||||
local function handler(field)
|
||||
local newinput = {
|
||||
port = field.port.tag,
|
||||
mask = field.mask,
|
||||
type = field.type,
|
||||
field = field }
|
||||
table.insert(inputs, newinput)
|
||||
end
|
||||
start_input_menu(handler)
|
||||
edit_start_selection = index
|
||||
return true
|
||||
end
|
||||
elseif command.action == 'deletestep' then
|
||||
if event == 'select' then
|
||||
table.remove(edit_current_macro.steps, command.step)
|
||||
if edit_current_macro.loop > #edit_current_macro.steps then
|
||||
edit_current_macro.loop = -1
|
||||
elseif edit_current_macro.loop > command.step then
|
||||
edit_current_macro.loop = edit_current_macro.loop - 1
|
||||
end
|
||||
if edit_insert_position > command.step then
|
||||
edit_insert_position = edit_insert_position - 1
|
||||
end
|
||||
edit_start_step = command.step
|
||||
if edit_start_step > #edit_current_macro.steps then
|
||||
edit_start_step = edit_start_step - 1
|
||||
end
|
||||
return true
|
||||
end
|
||||
elseif command.action == 'addstep' then
|
||||
if event == 'select' then
|
||||
local steps = edit_current_macro.steps
|
||||
local function handler(field)
|
||||
local newstep = {
|
||||
inputs = {
|
||||
{
|
||||
port = field.port.tag,
|
||||
mask = field.mask,
|
||||
type = field.type,
|
||||
field = field } },
|
||||
delay = 0,
|
||||
duration = 1 }
|
||||
table.insert(steps, edit_insert_position, newstep)
|
||||
if edit_current_macro.loop >= edit_insert_position then
|
||||
edit_current_macro.loop = edit_current_macro.loop + 1
|
||||
end
|
||||
edit_start_step = edit_insert_position
|
||||
edit_insert_position = edit_insert_position + 1
|
||||
end
|
||||
start_input_menu(handler)
|
||||
edit_start_selection = index
|
||||
return true
|
||||
elseif event == 'left' then
|
||||
edit_insert_position = edit_insert_position - 1
|
||||
return true
|
||||
elseif event == 'right' then
|
||||
edit_insert_position = edit_insert_position + 1
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local selection
|
||||
if command.step then
|
||||
if event == 'prevgroup' then
|
||||
if command.step > 1 then
|
||||
local found_break = false
|
||||
selection = index - 1
|
||||
while (not edit_items[selection]) or (edit_items[selection].step == command.step) do
|
||||
selection = selection - 1
|
||||
end
|
||||
local step = edit_items[selection].step
|
||||
while edit_items[selection - 1] and (edit_items[selection - 1].step == step) do
|
||||
selection = selection - 1
|
||||
end
|
||||
end
|
||||
elseif event == 'nextgroup' then
|
||||
if command.step < #edit_current_macro.steps then
|
||||
selection = index + 1
|
||||
while (not edit_items[selection]) or (edit_items[selection].step == command.step) do
|
||||
selection = selection + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return namecancel, selection
|
||||
end
|
||||
|
||||
local function add_edit_items(items)
|
||||
edit_items = { }
|
||||
local input = manager.machine.input
|
||||
local arrows
|
||||
|
||||
table.insert(items, { _p('plugin-inputmacro', 'Name'), edit_name_buffer and (edit_name_buffer .. '_') or edit_current_macro.name, '' })
|
||||
edit_items[#items] = { action = 'name' }
|
||||
if not (edit_start_selection or edit_start_step or edit_menu_active) then
|
||||
edit_start_selection = #items
|
||||
end
|
||||
edit_menu_active = true
|
||||
|
||||
local binding = edit_current_macro.binding
|
||||
local activation = binding and input:seq_name(binding) or _p('plugin-inputmacro', '[not set]')
|
||||
table.insert(items, { _p('plugin-inputmacro', 'Activation combination'), activation, edit_switch_poller and 'lr' or '' })
|
||||
edit_items[#items] = { action = 'binding' }
|
||||
|
||||
local releaseaction = edit_current_macro.earlycancel and _p('plugin-inputmacro', 'Stop immediately') or _p('plugin-inputmacro', 'Complete macro')
|
||||
table.insert(items, { _p('plugin-inputmacro', 'On release'), releaseaction, edit_current_macro.earlycancel and 'r' or 'l' })
|
||||
edit_items[#items] = { action = 'releaseaction' }
|
||||
|
||||
local holdaction
|
||||
arrows = 'lr'
|
||||
if edit_current_macro.loop < 0 then
|
||||
holdaction = _p('plugin-inputmacro', 'Release')
|
||||
arrows = 'r'
|
||||
elseif edit_current_macro.loop > 0 then
|
||||
holdaction = string.format(_p('plugin-inputmacro', 'Loop to step %d'), edit_current_macro.loop)
|
||||
if edit_current_macro.loop >= #edit_current_macro.steps then
|
||||
arrows = 'l'
|
||||
end
|
||||
else
|
||||
holdaction = string.format(_p('plugin-inputmacro', 'Prolong step %d'), #edit_current_macro.steps)
|
||||
end
|
||||
table.insert(items, { _p('plugin-inputmacro', 'When held'), holdaction, arrows })
|
||||
edit_items[#items] = { action = 'holdaction' }
|
||||
|
||||
for i, step in ipairs(edit_current_macro.steps) do
|
||||
table.insert(items, { string.format(_p('plugin-inputmacro', 'Step %d'), i), '', 'heading' })
|
||||
table.insert(items, { _p('plugin-inputmacro', 'Delay (frames)'), tostring(step.delay), (step.delay > 0) and 'lr' or 'r' })
|
||||
edit_items[#items] = { action = 'delay', step = i }
|
||||
if edit_start_step == i then
|
||||
edit_start_selection = #items
|
||||
end
|
||||
|
||||
table.insert(items, { _p('plugin-inputmacro', 'Duration (frames)'), tostring(step.duration), (step.duration > 1) and 'lr' or 'r' })
|
||||
edit_items[#items] = { action = 'duration', step = i }
|
||||
|
||||
for j, input in ipairs(step.inputs) do
|
||||
local inputname
|
||||
if input.field then
|
||||
inputname = input.field.name
|
||||
elseif input.port then
|
||||
inputname = _p('plugin-inputmacro', 'n/a')
|
||||
else
|
||||
inputname = _p('plugin-inputmacro', '[not set]')
|
||||
end
|
||||
table.insert(items, { string.format(_p('plugin-inputmacro', 'Input %d'), j), inputname, '' })
|
||||
edit_items[#items] = { action = 'input', step = i, input = j }
|
||||
end
|
||||
|
||||
if step.inputs[#step.inputs].field then
|
||||
table.insert(items, { _p('plugin-inputmacro', 'Add input'), '', '' })
|
||||
edit_items[#items] = { action = 'addinput', step = i }
|
||||
end
|
||||
|
||||
if #edit_current_macro.steps > 1 then
|
||||
table.insert(items, { _p('plugin-inputmacro', 'Delete step'), '', '' })
|
||||
edit_items[#items] = { action = 'deletestep', step = i }
|
||||
end
|
||||
end
|
||||
edit_start_step = nil
|
||||
|
||||
local laststep = edit_current_macro.steps[#edit_current_macro.steps]
|
||||
if laststep.inputs[#laststep.inputs].field then
|
||||
table.insert(items, { '---', '', '' })
|
||||
|
||||
arrows = 'lr'
|
||||
if edit_insert_position > #edit_current_macro.steps then
|
||||
arrows = 'l'
|
||||
elseif edit_insert_position < 2 then
|
||||
arrows = 'r'
|
||||
end
|
||||
table.insert(items, { _p('plugin-inputmacro', 'Add step at position'), tostring(edit_insert_position), arrows })
|
||||
edit_items[#items] = { action = 'addstep', step = i }
|
||||
end
|
||||
end
|
||||
|
||||
local function handle_add(index, event)
|
||||
local handled, selection = handle_edit_items(index, event)
|
||||
if handled then
|
||||
return true, selection
|
||||
elseif event == 'back' then
|
||||
edit_current_macro = nil
|
||||
edit_menu_active = false
|
||||
edit_items = nil
|
||||
table.remove(menu_stack)
|
||||
return true, selection
|
||||
elseif (index == edit_item_exit) and (event == 'select') then
|
||||
if current_macro_complete() then
|
||||
table.insert(macros, edit_current_macro)
|
||||
macros_start_macro = #macros
|
||||
end
|
||||
edit_menu_active = false
|
||||
edit_current_macro = nil
|
||||
edit_items = nil
|
||||
table.remove(menu_stack)
|
||||
return true, selection
|
||||
end
|
||||
return false, selection
|
||||
end
|
||||
|
||||
local function handle_edit(index, event)
|
||||
local handled, selection = handle_edit_items(index, event)
|
||||
if handled then
|
||||
return true, selection
|
||||
elseif (index == edit_item_delete) and (event == 'select') then
|
||||
local macro = macros_selection_save - macros_item_first_macro + 1
|
||||
table.remove(macros, macro)
|
||||
if macro > #macros then
|
||||
macros_selection_save = macros_selection_save - 1
|
||||
end
|
||||
edit_current_macro = nil
|
||||
edit_menu_active = false
|
||||
edit_items = nil
|
||||
table.remove(menu_stack)
|
||||
return true, selection
|
||||
elseif (event == 'back') or ((index == edit_item_exit) and (event == 'select')) then
|
||||
edit_current_macro = nil
|
||||
edit_menu_active = false
|
||||
edit_items = nil
|
||||
table.remove(menu_stack)
|
||||
return true, selection
|
||||
end
|
||||
return false, selection
|
||||
end
|
||||
|
||||
local function populate_add()
|
||||
local items = { }
|
||||
|
||||
table.insert(items, { _p('plugin-inputmacro', 'Add Input Macro'), '', 'off' })
|
||||
table.insert(items, { '---', '', '' })
|
||||
|
||||
add_edit_items(items)
|
||||
|
||||
table.insert(items, { '---', '', '' })
|
||||
if current_macro_complete() then
|
||||
table.insert(items, { _p('plugin-inputmacro', 'Create'), '', '' })
|
||||
else
|
||||
table.insert(items, { _p('plugin-inputmacro', 'Cancel'), '', '' })
|
||||
end
|
||||
edit_item_exit = #items
|
||||
|
||||
local selection = edit_start_selection
|
||||
edit_start_selection = nil
|
||||
if edit_switch_poller then
|
||||
return edit_switch_poller:overlay(items, selection, 'lrrepeat')
|
||||
else
|
||||
return items, selection, 'lrrepeat' .. (edit_name_buffer and ' ignorepause' or '')
|
||||
end
|
||||
end
|
||||
|
||||
local function populate_edit()
|
||||
local items = { }
|
||||
|
||||
table.insert(items, { _p('plugin-inputmacro', 'Edit Input Macro'), '', 'off' })
|
||||
table.insert(items, { '---', '', '' })
|
||||
|
||||
add_edit_items(items)
|
||||
|
||||
table.insert(items, { '---', '', '' })
|
||||
table.insert(items, { _p('plugin-inputmacro', 'Delete macro'), '', '' })
|
||||
edit_item_delete = #items
|
||||
|
||||
table.insert(items, { '---', '', '' })
|
||||
table.insert(items, { _p('plugin-inputmacro', 'Done'), '', '' })
|
||||
edit_item_exit = #items
|
||||
|
||||
local selection = edit_start_selection
|
||||
edit_start_selection = nil
|
||||
if edit_switch_poller then
|
||||
return edit_switch_poller:overlay(items, selection, 'lrrepeat')
|
||||
else
|
||||
return items, selection, 'lrrepeat' .. (edit_name_buffer and ' ignorepause' or '')
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
-- Macros menu
|
||||
|
||||
local macros_item_add
|
||||
|
||||
function handle_macros(index, event)
|
||||
if index == macros_item_add then
|
||||
if event == 'select' then
|
||||
edit_current_macro = new_macro()
|
||||
edit_insert_position = #edit_current_macro.steps + 1
|
||||
macros_selection_save = index
|
||||
table.insert(menu_stack, MENU_TYPES.ADD)
|
||||
return true
|
||||
end
|
||||
elseif index >= macros_item_first_macro then
|
||||
local macro = index - macros_item_first_macro + 1
|
||||
if event == 'select' then
|
||||
edit_current_macro = macros[macro]
|
||||
edit_insert_position = #edit_current_macro.steps + 1
|
||||
macros_selection_save = index
|
||||
table.insert(menu_stack, MENU_TYPES.EDIT)
|
||||
return true
|
||||
elseif event == 'clear' then
|
||||
table.remove(macros, macro)
|
||||
if #macros > 0 then
|
||||
macros_selection_save = index
|
||||
if macro > #macros then
|
||||
macros_selection_save = macros_selection_save - 1
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function populate_macros()
|
||||
local input = manager.machine.input
|
||||
local ioport = manager.machine.ioport
|
||||
local items = { }
|
||||
|
||||
table.insert(items, { _p('plugin-inputmacro', 'Input Macros'), '', 'off' })
|
||||
table.insert(items, { string.format(_p('plugin-inputmacro', 'Press %s to delete'), manager.ui:get_general_input_setting(ioport:token_to_input_type('UI_CLEAR'))), '', 'off' })
|
||||
table.insert(items, { '---', '', '' })
|
||||
|
||||
macros_item_first_macro = #items + 1
|
||||
if #macros > 0 then
|
||||
for index, macro in ipairs(macros) do
|
||||
table.insert(items, { macro.name, input:seq_name(macro.binding), '' })
|
||||
if macros_start_macro == index then
|
||||
macros_selection_save = #items
|
||||
end
|
||||
end
|
||||
else
|
||||
table.insert(items, { _p('plugin-inputmacro', '[no macros]'), '', 'off' })
|
||||
end
|
||||
macros_start_macro = nil
|
||||
|
||||
table.insert(items, { '---', '', '' })
|
||||
table.insert(items, { _p('plugin-inputmacro', 'Add macro'), '', '' })
|
||||
macros_item_add = #items
|
||||
|
||||
local selection = macros_selection_save
|
||||
macros_selection_save = nil
|
||||
return items, selection
|
||||
end
|
||||
|
||||
|
||||
-- Entry points
|
||||
|
||||
local lib = { }
|
||||
|
||||
function lib:init(m)
|
||||
macros = m
|
||||
menu_stack = { MENU_TYPES.MACROS }
|
||||
end
|
||||
|
||||
function lib:handle_event(index, event)
|
||||
local current = menu_stack[#menu_stack]
|
||||
if current == MENU_TYPES.MACROS then
|
||||
return handle_macros(index, event)
|
||||
elseif current == MENU_TYPES.ADD then
|
||||
return handle_add(index, event)
|
||||
elseif current == MENU_TYPES.EDIT then
|
||||
return handle_edit(index, event)
|
||||
elseif current == MENU_TYPES.INPUT then
|
||||
return handle_input(index, event)
|
||||
end
|
||||
end
|
||||
|
||||
function lib:populate()
|
||||
local current = menu_stack[#menu_stack]
|
||||
if current == MENU_TYPES.MACROS then
|
||||
return populate_macros()
|
||||
elseif current == MENU_TYPES.ADD then
|
||||
return populate_add()
|
||||
elseif current == MENU_TYPES.EDIT then
|
||||
return populate_edit()
|
||||
elseif current == MENU_TYPES.INPUT then
|
||||
return populate_input()
|
||||
end
|
||||
end
|
||||
|
||||
return lib
|
||||
155
bios/Arcade/MAME/plugins/inputmacro/inputmacro_persist.lua
Normal file
155
bios/Arcade/MAME/plugins/inputmacro/inputmacro_persist.lua
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
-- license:BSD-3-Clause
|
||||
-- copyright-holders:Vas Crabb
|
||||
|
||||
|
||||
-- Helpers
|
||||
|
||||
local function settings_path()
|
||||
return manager.machine.options.entries.homepath:value():match('([^;]+)') .. '/inputmacro'
|
||||
end
|
||||
|
||||
local function settings_filename()
|
||||
return emu.romname() .. '.cfg'
|
||||
end
|
||||
|
||||
local function make_macro(setting)
|
||||
if (setting.name == nil) or (setting.binding == nil) or (setting.earlycancel == nil) or (setting.loop == nil) or (setting.steps == nil) then
|
||||
return nil
|
||||
end
|
||||
|
||||
local result = {
|
||||
name = setting.name,
|
||||
binding = manager.machine.input:seq_from_tokens(setting.binding),
|
||||
bindingcfg = setting.binding,
|
||||
earlycancel = setting.earlycancel,
|
||||
loop = setting.loop,
|
||||
steps = { } }
|
||||
|
||||
local ioport = manager.machine.ioport
|
||||
for i, step in ipairs(setting.steps) do
|
||||
if step.inputs and step.delay and step.duration then
|
||||
local s = {
|
||||
inputs = { },
|
||||
delay = step.delay,
|
||||
duration = step.duration }
|
||||
for j, input in ipairs(step.inputs) do
|
||||
if input.port and input.mask and input.type then
|
||||
local ipt = {
|
||||
port = input.port,
|
||||
mask = input.mask,
|
||||
type = ioport:token_to_input_type(input.type) }
|
||||
local port = ioport.ports[input.port]
|
||||
if port then
|
||||
local field = port:field(input.mask)
|
||||
if field and (field.type == ipt.type) then
|
||||
ipt.field = field
|
||||
end
|
||||
end
|
||||
table.insert(s.inputs, ipt)
|
||||
end
|
||||
end
|
||||
if #s.inputs > 0 then
|
||||
table.insert(result.steps, s)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if result.loop > #result.steps then
|
||||
result.loop = -1
|
||||
end
|
||||
|
||||
if #result.steps > 0 then
|
||||
return result
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
local function make_settings(macros)
|
||||
local input = manager.machine.input
|
||||
local ioport = manager.machine.ioport
|
||||
local result = { }
|
||||
for i, macro in ipairs(macros) do
|
||||
local m = {
|
||||
name = macro.name,
|
||||
binding = macro.bindingcfg,
|
||||
earlycancel = macro.earlycancel,
|
||||
loop = macro.loop,
|
||||
steps = { } }
|
||||
table.insert(result, m)
|
||||
for j, step in ipairs(macro.steps) do
|
||||
local s = {
|
||||
inputs = { },
|
||||
delay = step.delay,
|
||||
duration = step.duration }
|
||||
table.insert(m.steps, s)
|
||||
for k, input in ipairs(step.inputs) do
|
||||
local b = {
|
||||
port = input.port,
|
||||
mask = input.mask,
|
||||
type = ioport:input_type_to_token(input.type) }
|
||||
table.insert(s.inputs, b)
|
||||
end
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
-- Entry points
|
||||
|
||||
local lib = { }
|
||||
|
||||
function lib:load_settings()
|
||||
local filename = settings_path() .. '/' .. settings_filename()
|
||||
local file = io.open(filename, 'r')
|
||||
if not file then
|
||||
return { }
|
||||
end
|
||||
local json = require('json')
|
||||
local settings = json.parse(file:read('a'))
|
||||
file:close()
|
||||
if not settings then
|
||||
emu.print_error(string.format('Error loading input macros: error parsing file "%s" as JSON', filename))
|
||||
return { }
|
||||
end
|
||||
|
||||
result = { }
|
||||
for index, setting in ipairs(settings) do
|
||||
local macro = make_macro(setting)
|
||||
if macro then
|
||||
table.insert(result, macro)
|
||||
end
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
function lib:save_settings(macros)
|
||||
local path = settings_path()
|
||||
local stat = lfs.attributes(path)
|
||||
if stat and (stat.mode ~= 'directory') then
|
||||
emu.print_error(string.format('Error saving input macros: "%s" is not a directory', path))
|
||||
return
|
||||
end
|
||||
local filename = path .. '/' .. settings_filename()
|
||||
|
||||
if #macros == 0 then
|
||||
os.remove(filename)
|
||||
return
|
||||
elseif not stat then
|
||||
lfs.mkdir(path)
|
||||
end
|
||||
|
||||
local json = require('json')
|
||||
local settings = make_settings(macros)
|
||||
local text = json.stringify(settings, { indent = true })
|
||||
local file = io.open(filename, 'w')
|
||||
if not file then
|
||||
emu.print_error(string.format('Error saving input macros: error opening file "%s" for writing', filename))
|
||||
return
|
||||
end
|
||||
file:write(text)
|
||||
file:close()
|
||||
end
|
||||
|
||||
return lib
|
||||
10
bios/Arcade/MAME/plugins/inputmacro/plugin.json
Normal file
10
bios/Arcade/MAME/plugins/inputmacro/plugin.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"plugin": {
|
||||
"name": "inputmacro",
|
||||
"description": "Input macro plugin",
|
||||
"version": "0.0.1",
|
||||
"author": "Vas Crabb",
|
||||
"type": "plugin",
|
||||
"start": "false"
|
||||
}
|
||||
}
|
||||
22
bios/Arcade/MAME/plugins/json/LICENSE
Normal file
22
bios/Arcade/MAME/plugins/json/LICENSE
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Tim Caswell
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
2
bios/Arcade/MAME/plugins/json/README.md
Normal file
2
bios/Arcade/MAME/plugins/json/README.md
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# luv-json
|
||||
A luv port of luvit's json module
|
||||
735
bios/Arcade/MAME/plugins/json/init.lua
Normal file
735
bios/Arcade/MAME/plugins/json/init.lua
Normal file
|
|
@ -0,0 +1,735 @@
|
|||
-- license:MIT
|
||||
-- copyright-holders:David Kolf
|
||||
|
||||
local exports = {}
|
||||
exports.name = "luvit/json"
|
||||
exports.version = "2.5.0"
|
||||
exports.homepage = "http://dkolf.de/src/dkjson-lua.fsl"
|
||||
exports.description = "David Kolf's JSON library repackaged for lit."
|
||||
exports.tags = {"json", "codec"}
|
||||
exports.license = "MIT"
|
||||
exports.author = {
|
||||
name = "David Kolf",
|
||||
homepage = "http://dkolf.de/",
|
||||
}
|
||||
exports.contributors = {
|
||||
"Tim Caswell",
|
||||
}
|
||||
|
||||
-- Module options:
|
||||
local always_try_using_lpeg = false
|
||||
local register_global_module_table = false
|
||||
local global_module_name = 'json'
|
||||
|
||||
--[==[
|
||||
|
||||
David Kolf's JSON module for Lua 5.1/5.2
|
||||
|
||||
Version 2.5
|
||||
|
||||
|
||||
For the documentation see the corresponding readme.txt or visit
|
||||
<http://dkolf.de/src/dkjson-lua.fsl/>.
|
||||
|
||||
You can contact the author by sending an e-mail to 'david' at the
|
||||
domain 'dkolf.de'.
|
||||
|
||||
|
||||
Copyright (C) 2010-2013 David Heiko Kolf
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
--]==]
|
||||
|
||||
-- global dependencies:
|
||||
local pairs, type, tostring, tonumber, getmetatable, setmetatable =
|
||||
pairs, type, tostring, tonumber, getmetatable, setmetatable
|
||||
local error, require, pcall, select = error, require, pcall, select
|
||||
local floor, huge = math.floor, math.huge
|
||||
local strrep, gsub, strsub, strbyte, strchar, strfind, strlen, strformat =
|
||||
string.rep, string.gsub, string.sub, string.byte, string.char,
|
||||
string.find, string.len, string.format
|
||||
local strmatch = string.match
|
||||
local concat = table.concat
|
||||
|
||||
local json = exports
|
||||
json.original_version = "dkjson 2.5"
|
||||
|
||||
if register_global_module_table then
|
||||
_G[global_module_name] = json
|
||||
end
|
||||
|
||||
_ENV = nil -- blocking globals in Lua 5.2
|
||||
|
||||
pcall (function()
|
||||
-- Enable access to blocked metatables.
|
||||
-- Don't worry, this module doesn't change anything in them.
|
||||
local debmeta = require "debug".getmetatable
|
||||
if debmeta then getmetatable = debmeta end
|
||||
end)
|
||||
|
||||
json.null = setmetatable ({}, {
|
||||
__tojson = function () return "null" end
|
||||
})
|
||||
|
||||
local function isarray (tbl)
|
||||
local max, n, arraylen = 0, 0, 0
|
||||
for k,v in pairs (tbl) do
|
||||
if k == 'n' and type(v) == 'number' then
|
||||
arraylen = v
|
||||
if v > max then
|
||||
max = v
|
||||
end
|
||||
else
|
||||
if type(k) ~= 'number' or k < 1 or floor(k) ~= k then
|
||||
return false
|
||||
end
|
||||
if k > max then
|
||||
max = k
|
||||
end
|
||||
n = n + 1
|
||||
end
|
||||
end
|
||||
if max > 10 and max > arraylen and max > n * 2 then
|
||||
return false -- don't create an array with too many holes
|
||||
end
|
||||
return true, max
|
||||
end
|
||||
|
||||
local escapecodes = {
|
||||
["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f",
|
||||
["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t"
|
||||
}
|
||||
|
||||
local function escapeutf8 (uchar)
|
||||
local value = escapecodes[uchar]
|
||||
if value then
|
||||
return value
|
||||
end
|
||||
local a, b, c, d = strbyte (uchar, 1, 4)
|
||||
a, b, c, d = a or 0, b or 0, c or 0, d or 0
|
||||
if a <= 0x7f then
|
||||
value = a
|
||||
elseif 0xc0 <= a and a <= 0xdf and b >= 0x80 then
|
||||
value = (a - 0xc0) * 0x40 + b - 0x80
|
||||
elseif 0xe0 <= a and a <= 0xef and b >= 0x80 and c >= 0x80 then
|
||||
value = ((a - 0xe0) * 0x40 + b - 0x80) * 0x40 + c - 0x80
|
||||
elseif 0xf0 <= a and a <= 0xf7 and b >= 0x80 and c >= 0x80 and d >= 0x80 then
|
||||
value = (((a - 0xf0) * 0x40 + b - 0x80) * 0x40 + c - 0x80) * 0x40 + d - 0x80
|
||||
else
|
||||
return ""
|
||||
end
|
||||
if value <= 0xffff then
|
||||
return strformat ("\\u%.4x", value)
|
||||
elseif value <= 0x10ffff then
|
||||
-- encode as UTF-16 surrogate pair
|
||||
value = value - 0x10000
|
||||
local highsur, lowsur = 0xD800 + floor (value/0x400), 0xDC00 + (value % 0x400)
|
||||
return strformat ("\\u%.4x\\u%.4x", highsur, lowsur)
|
||||
else
|
||||
return ""
|
||||
end
|
||||
end
|
||||
|
||||
local function fsub (str, pattern, repl)
|
||||
-- gsub always builds a new string in a buffer, even when no match
|
||||
-- exists. First using find should be more efficient when most strings
|
||||
-- don't contain the pattern.
|
||||
if strfind (str, pattern) then
|
||||
return gsub (str, pattern, repl)
|
||||
else
|
||||
return str
|
||||
end
|
||||
end
|
||||
|
||||
local function quotestring (value)
|
||||
-- based on the regexp "escapable" in https://github.com/douglascrockford/JSON-js
|
||||
value = fsub (value, "[%z\1-\31\"\\\127]", escapeutf8)
|
||||
if strfind (value, "[\194\216\220\225\226\239]") then
|
||||
value = fsub (value, "\194[\128-\159\173]", escapeutf8)
|
||||
value = fsub (value, "\216[\128-\132]", escapeutf8)
|
||||
value = fsub (value, "\220\143", escapeutf8)
|
||||
value = fsub (value, "\225\158[\180\181]", escapeutf8)
|
||||
value = fsub (value, "\226\128[\140-\143\168-\175]", escapeutf8)
|
||||
value = fsub (value, "\226\129[\160-\175]", escapeutf8)
|
||||
value = fsub (value, "\239\187\191", escapeutf8)
|
||||
value = fsub (value, "\239\191[\176-\191]", escapeutf8)
|
||||
end
|
||||
return "\"" .. value .. "\""
|
||||
end
|
||||
json.quotestring = quotestring
|
||||
|
||||
local function replace(str, o, n)
|
||||
local i, j = strfind (str, o, 1, true)
|
||||
if i then
|
||||
return strsub(str, 1, i-1) .. n .. strsub(str, j+1, -1)
|
||||
else
|
||||
return str
|
||||
end
|
||||
end
|
||||
|
||||
-- locale independent num2str and str2num functions
|
||||
local decpoint, numfilter
|
||||
|
||||
local function updatedecpoint ()
|
||||
decpoint = strmatch(tostring(0.5), "([^05+])")
|
||||
-- build a filter that can be used to remove group separators
|
||||
numfilter = "[^0-9%-%+eE" .. gsub(decpoint, "[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0") .. "]+"
|
||||
end
|
||||
|
||||
updatedecpoint()
|
||||
|
||||
local function num2str (num)
|
||||
return replace(fsub(tostring(num), numfilter, ""), decpoint, ".")
|
||||
end
|
||||
|
||||
local function str2num (str)
|
||||
local num = tonumber(replace(str, ".", decpoint))
|
||||
if not num then
|
||||
updatedecpoint()
|
||||
num = tonumber(replace(str, ".", decpoint))
|
||||
end
|
||||
return num
|
||||
end
|
||||
|
||||
local function addnewline2 (level, buffer, buflen)
|
||||
buffer[buflen+1] = "\n"
|
||||
buffer[buflen+2] = strrep (" ", level)
|
||||
buflen = buflen + 2
|
||||
return buflen
|
||||
end
|
||||
|
||||
function json.addnewline (state)
|
||||
if state.indent then
|
||||
state.bufferlen = addnewline2 (state.level or 0,
|
||||
state.buffer, state.bufferlen or #(state.buffer))
|
||||
end
|
||||
end
|
||||
|
||||
local encode2 -- forward declaration
|
||||
|
||||
local function addpair (key, value, prev, indent, level, buffer, buflen, tables, globalorder, state)
|
||||
local kt = type (key)
|
||||
if kt ~= 'string' and kt ~= 'number' then
|
||||
return nil, "type '" .. kt .. "' is not supported as a key by JSON."
|
||||
end
|
||||
if prev then
|
||||
buflen = buflen + 1
|
||||
buffer[buflen] = ","
|
||||
end
|
||||
if indent then
|
||||
buflen = addnewline2 (level, buffer, buflen)
|
||||
end
|
||||
buffer[buflen+1] = quotestring (key)
|
||||
buffer[buflen+2] = ":"
|
||||
return encode2 (value, indent, level, buffer, buflen + 2, tables, globalorder, state)
|
||||
end
|
||||
|
||||
local function appendcustom(res, buffer, state)
|
||||
local buflen = state.bufferlen
|
||||
if type (res) == 'string' then
|
||||
buflen = buflen + 1
|
||||
buffer[buflen] = res
|
||||
end
|
||||
return buflen
|
||||
end
|
||||
|
||||
local function exception(reason, value, state, buffer, buflen, defaultmessage)
|
||||
defaultmessage = defaultmessage or reason
|
||||
local handler = state.exception
|
||||
if not handler then
|
||||
return nil, defaultmessage
|
||||
else
|
||||
state.bufferlen = buflen
|
||||
local ret, msg = handler (reason, value, state, defaultmessage)
|
||||
if not ret then return nil, msg or defaultmessage end
|
||||
return appendcustom(ret, buffer, state)
|
||||
end
|
||||
end
|
||||
|
||||
function json.encodeexception(reason, value, state, defaultmessage)
|
||||
return quotestring("<" .. defaultmessage .. ">")
|
||||
end
|
||||
|
||||
encode2 = function (value, indent, level, buffer, buflen, tables, globalorder, state)
|
||||
local valtype = type (value)
|
||||
local valmeta = getmetatable (value)
|
||||
valmeta = type (valmeta) == 'table' and valmeta -- only tables
|
||||
local valtojson = valmeta and valmeta.__tojson
|
||||
if valtojson then
|
||||
if tables[value] then
|
||||
return exception('reference cycle', value, state, buffer, buflen)
|
||||
end
|
||||
tables[value] = true
|
||||
state.bufferlen = buflen
|
||||
local ret, msg = valtojson (value, state)
|
||||
if not ret then return exception('custom encoder failed', value, state, buffer, buflen, msg) end
|
||||
tables[value] = nil
|
||||
buflen = appendcustom(ret, buffer, state)
|
||||
elseif value == nil then
|
||||
buflen = buflen + 1
|
||||
buffer[buflen] = "null"
|
||||
elseif valtype == 'number' then
|
||||
local s
|
||||
if value ~= value or value >= huge or -value >= huge then
|
||||
-- This is the behaviour of the original JSON implementation.
|
||||
s = "null"
|
||||
else
|
||||
s = num2str (value)
|
||||
end
|
||||
buflen = buflen + 1
|
||||
buffer[buflen] = s
|
||||
elseif valtype == 'boolean' then
|
||||
buflen = buflen + 1
|
||||
buffer[buflen] = value and "true" or "false"
|
||||
elseif valtype == 'string' then
|
||||
buflen = buflen + 1
|
||||
buffer[buflen] = quotestring (value)
|
||||
elseif valtype == 'table' then
|
||||
if tables[value] then
|
||||
return exception('reference cycle', value, state, buffer, buflen)
|
||||
end
|
||||
tables[value] = true
|
||||
level = level + 1
|
||||
local isa, n = isarray (value)
|
||||
if n == 0 and valmeta and valmeta.__jsontype == 'object' then
|
||||
isa = false
|
||||
end
|
||||
local msg
|
||||
if isa then -- JSON array
|
||||
buflen = buflen + 1
|
||||
buffer[buflen] = "["
|
||||
for i = 1, n do
|
||||
buflen, msg = encode2 (value[i], indent, level, buffer, buflen, tables, globalorder, state)
|
||||
if not buflen then return nil, msg end
|
||||
if i < n then
|
||||
buflen = buflen + 1
|
||||
buffer[buflen] = ","
|
||||
end
|
||||
end
|
||||
buflen = buflen + 1
|
||||
buffer[buflen] = "]"
|
||||
else -- JSON object
|
||||
local prev = false
|
||||
buflen = buflen + 1
|
||||
buffer[buflen] = "{"
|
||||
local order = valmeta and valmeta.__jsonorder or globalorder
|
||||
if order then
|
||||
local used = {}
|
||||
n = #order
|
||||
for i = 1, n do
|
||||
local k = order[i]
|
||||
local v = value[k]
|
||||
local _
|
||||
if v then
|
||||
used[k] = true
|
||||
buflen, _ = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
|
||||
prev = true -- add a seperator before the next element
|
||||
end
|
||||
end
|
||||
for k,v in pairs (value) do
|
||||
if not used[k] then
|
||||
buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
|
||||
if not buflen then return nil, msg end
|
||||
prev = true -- add a seperator before the next element
|
||||
end
|
||||
end
|
||||
else -- unordered
|
||||
for k,v in pairs (value) do
|
||||
buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
|
||||
if not buflen then return nil, msg end
|
||||
prev = true -- add a seperator before the next element
|
||||
end
|
||||
end
|
||||
if indent then
|
||||
buflen = addnewline2 (level - 1, buffer, buflen)
|
||||
end
|
||||
buflen = buflen + 1
|
||||
buffer[buflen] = "}"
|
||||
end
|
||||
tables[value] = nil
|
||||
else
|
||||
return exception ('unsupported type', value, state, buffer, buflen,
|
||||
"type '" .. valtype .. "' is not supported by JSON.")
|
||||
end
|
||||
return buflen
|
||||
end
|
||||
|
||||
function json.encode (value, state)
|
||||
state = state or {}
|
||||
local oldbuffer = state.buffer
|
||||
local buffer = oldbuffer or {}
|
||||
state.buffer = buffer
|
||||
updatedecpoint()
|
||||
local ret, msg = encode2 (value, state.indent, state.level or 0,
|
||||
buffer, state.bufferlen or 0, state.tables or {}, state.keyorder, state)
|
||||
if not ret then
|
||||
error (msg, 2)
|
||||
elseif oldbuffer == buffer then
|
||||
state.bufferlen = ret
|
||||
return true
|
||||
else
|
||||
state.bufferlen = nil
|
||||
state.buffer = nil
|
||||
return concat (buffer)
|
||||
end
|
||||
end
|
||||
|
||||
local function loc (str, where)
|
||||
local line, pos, linepos = 1, 1, 0
|
||||
while true do
|
||||
pos = strfind (str, "\n", pos, true)
|
||||
if pos and pos < where then
|
||||
line = line + 1
|
||||
linepos = pos
|
||||
pos = pos + 1
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
return "line " .. line .. ", column " .. (where - linepos)
|
||||
end
|
||||
|
||||
local function unterminated (str, what, where)
|
||||
return nil, strlen (str) + 1, "unterminated " .. what .. " at " .. loc (str, where)
|
||||
end
|
||||
|
||||
local function scanwhite (str, pos)
|
||||
while true do
|
||||
pos = strfind (str, "%S", pos)
|
||||
if not pos then return nil end
|
||||
local sub2 = strsub (str, pos, pos + 1)
|
||||
if sub2 == "\239\187" and strsub (str, pos + 2, pos + 2) == "\191" then
|
||||
-- UTF-8 Byte Order Mark
|
||||
pos = pos + 3
|
||||
elseif sub2 == "//" then
|
||||
pos = strfind (str, "[\n\r]", pos + 2)
|
||||
if not pos then return nil end
|
||||
elseif sub2 == "/*" then
|
||||
pos = strfind (str, "*/", pos + 2)
|
||||
if not pos then return nil end
|
||||
pos = pos + 2
|
||||
else
|
||||
return pos
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local escapechars = {
|
||||
["\""] = "\"", ["\\"] = "\\", ["/"] = "/", ["b"] = "\b", ["f"] = "\f",
|
||||
["n"] = "\n", ["r"] = "\r", ["t"] = "\t"
|
||||
}
|
||||
|
||||
local function unichar (value)
|
||||
if value < 0 then
|
||||
return nil
|
||||
elseif value <= 0x007f then
|
||||
return strchar (value)
|
||||
elseif value <= 0x07ff then
|
||||
return strchar (0xc0 + floor(value/0x40),
|
||||
0x80 + (floor(value) % 0x40))
|
||||
elseif value <= 0xffff then
|
||||
return strchar (0xe0 + floor(value/0x1000),
|
||||
0x80 + (floor(value/0x40) % 0x40),
|
||||
0x80 + (floor(value) % 0x40))
|
||||
elseif value <= 0x10ffff then
|
||||
return strchar (0xf0 + floor(value/0x40000),
|
||||
0x80 + (floor(value/0x1000) % 0x40),
|
||||
0x80 + (floor(value/0x40) % 0x40),
|
||||
0x80 + (floor(value) % 0x40))
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
local function scanstring (str, pos)
|
||||
local lastpos = pos + 1
|
||||
local buffer, n = {}, 0
|
||||
while true do
|
||||
local nextpos = strfind (str, "[\"\\]", lastpos)
|
||||
if not nextpos then
|
||||
return unterminated (str, "string", pos)
|
||||
end
|
||||
if nextpos > lastpos then
|
||||
n = n + 1
|
||||
buffer[n] = strsub (str, lastpos, nextpos - 1)
|
||||
end
|
||||
if strsub (str, nextpos, nextpos) == "\"" then
|
||||
lastpos = nextpos + 1
|
||||
break
|
||||
else
|
||||
local escchar = strsub (str, nextpos + 1, nextpos + 1)
|
||||
local value
|
||||
if escchar == "u" then
|
||||
value = tonumber (strsub (str, nextpos + 2, nextpos + 5), 16)
|
||||
if value then
|
||||
local value2
|
||||
if 0xD800 <= value and value <= 0xDBff then
|
||||
-- we have the high surrogate of UTF-16. Check if there is a
|
||||
-- low surrogate escaped nearby to combine them.
|
||||
if strsub (str, nextpos + 6, nextpos + 7) == "\\u" then
|
||||
value2 = tonumber (strsub (str, nextpos + 8, nextpos + 11), 16)
|
||||
if value2 and 0xDC00 <= value2 and value2 <= 0xDFFF then
|
||||
value = (value - 0xD800) * 0x400 + (value2 - 0xDC00) + 0x10000
|
||||
else
|
||||
value2 = nil -- in case it was out of range for a low surrogate
|
||||
end
|
||||
end
|
||||
end
|
||||
value = value and unichar (value)
|
||||
if value then
|
||||
if value2 then
|
||||
lastpos = nextpos + 12
|
||||
else
|
||||
lastpos = nextpos + 6
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if not value then
|
||||
value = escapechars[escchar] or escchar
|
||||
lastpos = nextpos + 2
|
||||
end
|
||||
n = n + 1
|
||||
buffer[n] = value
|
||||
end
|
||||
end
|
||||
if n == 1 then
|
||||
return buffer[1], lastpos
|
||||
elseif n > 1 then
|
||||
return concat (buffer), lastpos
|
||||
else
|
||||
return "", lastpos
|
||||
end
|
||||
end
|
||||
|
||||
local scanvalue -- forward declaration
|
||||
|
||||
local function scantable (what, closechar, str, startpos, nullval, objectmeta, arraymeta)
|
||||
local tbl, n = {}, 0
|
||||
local pos = startpos + 1
|
||||
if what == 'object' then
|
||||
setmetatable (tbl, objectmeta)
|
||||
else
|
||||
setmetatable (tbl, arraymeta)
|
||||
end
|
||||
while true do
|
||||
pos = scanwhite (str, pos)
|
||||
if not pos then return unterminated (str, what, startpos) end
|
||||
local char = strsub (str, pos, pos)
|
||||
if char == closechar then
|
||||
return tbl, pos + 1
|
||||
end
|
||||
local val1, err
|
||||
val1, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta)
|
||||
if err then return nil, pos, err end
|
||||
pos = scanwhite (str, pos)
|
||||
if not pos then return unterminated (str, what, startpos) end
|
||||
char = strsub (str, pos, pos)
|
||||
if char == ":" then
|
||||
if val1 == nil then
|
||||
return nil, pos, "cannot use nil as table index (at " .. loc (str, pos) .. ")"
|
||||
end
|
||||
pos = scanwhite (str, pos + 1)
|
||||
if not pos then return unterminated (str, what, startpos) end
|
||||
local val2
|
||||
val2, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta)
|
||||
if err then return nil, pos, err end
|
||||
tbl[val1] = val2
|
||||
pos = scanwhite (str, pos)
|
||||
if not pos then return unterminated (str, what, startpos) end
|
||||
char = strsub (str, pos, pos)
|
||||
else
|
||||
n = n + 1
|
||||
tbl[n] = val1
|
||||
end
|
||||
if char == "," then
|
||||
pos = pos + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
scanvalue = function (str, pos, nullval, objectmeta, arraymeta)
|
||||
pos = pos or 1
|
||||
pos = scanwhite (str, pos)
|
||||
if not pos then
|
||||
return nil, strlen (str) + 1, "no valid JSON value (reached the end)"
|
||||
end
|
||||
local char = strsub (str, pos, pos)
|
||||
if char == "{" then
|
||||
return scantable ('object', "}", str, pos, nullval, objectmeta, arraymeta)
|
||||
elseif char == "[" then
|
||||
return scantable ('array', "]", str, pos, nullval, objectmeta, arraymeta)
|
||||
elseif char == "\"" then
|
||||
return scanstring (str, pos)
|
||||
else
|
||||
local pstart, pend = strfind (str, "^%-?[%d%.]+[eE]?[%+%-]?%d*", pos)
|
||||
if pstart then
|
||||
local number = str2num (strsub (str, pstart, pend))
|
||||
if number then
|
||||
return number, pend + 1
|
||||
end
|
||||
end
|
||||
pstart, pend = strfind (str, "^%a%w*", pos)
|
||||
if pstart then
|
||||
local name = strsub (str, pstart, pend)
|
||||
if name == "true" then
|
||||
return true, pend + 1
|
||||
elseif name == "false" then
|
||||
return false, pend + 1
|
||||
elseif name == "null" then
|
||||
return nullval, pend + 1
|
||||
end
|
||||
end
|
||||
return nil, pos, "no valid JSON value at " .. loc (str, pos)
|
||||
end
|
||||
end
|
||||
|
||||
local function optionalmetatables(...)
|
||||
if select("#", ...) > 0 then
|
||||
return ...
|
||||
else
|
||||
return {__jsontype = 'object'}, {__jsontype = 'array'}
|
||||
end
|
||||
end
|
||||
|
||||
function json.decode (str, pos, nullval, ...)
|
||||
local objectmeta, arraymeta = optionalmetatables(...)
|
||||
return scanvalue (str, pos, nullval, objectmeta, arraymeta)
|
||||
end
|
||||
|
||||
function json.use_lpeg ()
|
||||
local g = require ("lpeg")
|
||||
|
||||
if g.version() == "0.11" then
|
||||
error "due to a bug in LPeg 0.11, it cannot be used for JSON matching"
|
||||
end
|
||||
|
||||
local pegmatch = g.match
|
||||
local P, S, R = g.P, g.S, g.R
|
||||
|
||||
local function ErrorCall (str, pos, msg, state)
|
||||
if not state.msg then
|
||||
state.msg = msg .. " at " .. loc (str, pos)
|
||||
state.pos = pos
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function Err (msg)
|
||||
return g.Cmt (g.Cc (msg) * g.Carg (2), ErrorCall)
|
||||
end
|
||||
|
||||
local SingleLineComment = P"//" * (1 - S"\n\r")^0
|
||||
local MultiLineComment = P"/*" * (1 - P"*/")^0 * P"*/"
|
||||
local Space = (S" \n\r\t" + P"\239\187\191" + SingleLineComment + MultiLineComment)^0
|
||||
|
||||
local PlainChar = 1 - S"\"\\\n\r"
|
||||
local EscapeSequence = (P"\\" * g.C (S"\"\\/bfnrt" + Err "unsupported escape sequence")) / escapechars
|
||||
local HexDigit = R("09", "af", "AF")
|
||||
local function UTF16Surrogate (match, pos, high, low)
|
||||
high, low = tonumber (high, 16), tonumber (low, 16)
|
||||
if 0xD800 <= high and high <= 0xDBff and 0xDC00 <= low and low <= 0xDFFF then
|
||||
return true, unichar ((high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000)
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
local function UTF16BMP (hex)
|
||||
return unichar (tonumber (hex, 16))
|
||||
end
|
||||
local U16Sequence = (P"\\u" * g.C (HexDigit * HexDigit * HexDigit * HexDigit))
|
||||
local UnicodeEscape = g.Cmt (U16Sequence * U16Sequence, UTF16Surrogate) + U16Sequence/UTF16BMP
|
||||
local Char = UnicodeEscape + EscapeSequence + PlainChar
|
||||
local String = P"\"" * g.Cs (Char ^ 0) * (P"\"" + Err "unterminated string")
|
||||
local Integer = P"-"^(-1) * (P"0" + (R"19" * R"09"^0))
|
||||
local Fractal = P"." * R"09"^0
|
||||
local Exponent = (S"eE") * (S"+-")^(-1) * R"09"^1
|
||||
local Number = (Integer * Fractal^(-1) * Exponent^(-1))/str2num
|
||||
local Constant = P"true" * g.Cc (true) + P"false" * g.Cc (false) + P"null" * g.Carg (1)
|
||||
local SimpleValue = Number + String + Constant
|
||||
local ArrayContent, ObjectContent
|
||||
|
||||
-- The functions parsearray and parseobject parse only a single value/pair
|
||||
-- at a time and store them directly to avoid hitting the LPeg limits.
|
||||
local function parsearray (str, pos, nullval, state)
|
||||
local obj, cont
|
||||
local npos
|
||||
local t, nt = {}, 0
|
||||
repeat
|
||||
obj, cont, npos = pegmatch (ArrayContent, str, pos, nullval, state)
|
||||
if not npos then break end
|
||||
pos = npos
|
||||
nt = nt + 1
|
||||
t[nt] = obj
|
||||
until cont == 'last'
|
||||
return pos, setmetatable (t, state.arraymeta)
|
||||
end
|
||||
|
||||
local function parseobject (str, pos, nullval, state)
|
||||
local obj, key, cont
|
||||
local npos
|
||||
local t = {}
|
||||
repeat
|
||||
key, obj, cont, npos = pegmatch (ObjectContent, str, pos, nullval, state)
|
||||
if not npos then break end
|
||||
pos = npos
|
||||
t[key] = obj
|
||||
until cont == 'last'
|
||||
return pos, setmetatable (t, state.objectmeta)
|
||||
end
|
||||
|
||||
local Array = P"[" * g.Cmt (g.Carg(1) * g.Carg(2), parsearray) * Space * (P"]" + Err "']' expected")
|
||||
local Object = P"{" * g.Cmt (g.Carg(1) * g.Carg(2), parseobject) * Space * (P"}" + Err "'}' expected")
|
||||
local Value = Space * (Array + Object + SimpleValue)
|
||||
local ExpectedValue = Value + Space * Err "value expected"
|
||||
ArrayContent = Value * Space * (P"," * g.Cc'cont' + g.Cc'last') * g.Cp()
|
||||
local Pair = g.Cg (Space * String * Space * (P":" + Err "colon expected") * ExpectedValue)
|
||||
ObjectContent = Pair * Space * (P"," * g.Cc'cont' + g.Cc'last') * g.Cp()
|
||||
local DecodeValue = ExpectedValue * g.Cp ()
|
||||
|
||||
function json.decode (str, pos, nullval, ...)
|
||||
local state = {}
|
||||
state.objectmeta, state.arraymeta = optionalmetatables(...)
|
||||
local obj, retpos = pegmatch (DecodeValue, str, pos, nullval, state)
|
||||
if state.msg then
|
||||
return nil, state.pos, state.msg
|
||||
else
|
||||
return obj, retpos
|
||||
end
|
||||
end
|
||||
|
||||
-- use this function only once:
|
||||
json.use_lpeg = function () return json end
|
||||
|
||||
json.using_lpeg = true
|
||||
|
||||
return json -- so you can get the module using json = require "dkjson".use_lpeg()
|
||||
end
|
||||
|
||||
if always_try_using_lpeg then
|
||||
pcall (json.use_lpeg)
|
||||
end
|
||||
|
||||
json.parse = json.decode
|
||||
json.stringify = json.encode
|
||||
|
||||
return exports
|
||||
9
bios/Arcade/MAME/plugins/json/plugin.json
Normal file
9
bios/Arcade/MAME/plugins/json/plugin.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"plugin": {
|
||||
"name": "json",
|
||||
"description": "json library",
|
||||
"version": "2.5.0",
|
||||
"author": "David Kolf",
|
||||
"type": "library"
|
||||
}
|
||||
}
|
||||
86
bios/Arcade/MAME/plugins/layout/init.lua
Normal file
86
bios/Arcade/MAME/plugins/layout/init.lua
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
-- license:BSD-3-Clause
|
||||
-- copyright-holders:Carl
|
||||
-- Layout scripts should return a table and a string. The table can have two optional keys reset and frame
|
||||
-- which have functions for values called on reset and frame draw respectively and the string is a unique name.
|
||||
local exports = {
|
||||
name = "layout",
|
||||
version = "0.0.1",
|
||||
description = "Layout helper plugin",
|
||||
license = "BSD-3-Clause",
|
||||
author = { name = "Carl" } }
|
||||
|
||||
local layout = exports
|
||||
|
||||
local frame_subscription, stop_subscription
|
||||
|
||||
function layout.startplugin()
|
||||
local scripts = {}
|
||||
local function prepare_layout(file, script)
|
||||
local env = {
|
||||
machine = manager.machine,
|
||||
emu = {
|
||||
device_enumerator = emu.device_enumerator,
|
||||
palette_enumerator = emu.palette_enumerator,
|
||||
screen_enumerator = emu.screen_enumerator,
|
||||
cassette_enumerator = emu.cassette_enumerator,
|
||||
image_enumerator = emu.image_enumerator,
|
||||
slot_enumerator = emu.slot_enumerator,
|
||||
attotime = emu.attotime,
|
||||
render_bounds = emu.render_bounds,
|
||||
render_color = emu.render_color,
|
||||
bitmap_ind8 = emu.bitmap_ind8,
|
||||
bitmap_ind16 = emu.bitmap_ind16,
|
||||
bitmap_ind32 = emu.bitmap_ind32,
|
||||
bitmap_ind64 = emu.bitmap_ind64,
|
||||
bitmap_yuy16 = emu.bitmap_yuy16,
|
||||
bitmap_rgb32 = emu.bitmap_rgb32,
|
||||
bitmap_argb32 = emu.bitmap_argb32,
|
||||
print_verbose = emu.print_verbose,
|
||||
print_error = emu.print_error,
|
||||
print_warning = emu.print_warning,
|
||||
print_info = emu.print_info,
|
||||
print_debug = emu.print_debug },
|
||||
file = file,
|
||||
math = math,
|
||||
print = print,
|
||||
pairs = pairs,
|
||||
ipairs = ipairs,
|
||||
string = string,
|
||||
tonumber = tonumber,
|
||||
tostring = tostring,
|
||||
table = table }
|
||||
local script, err = load(script, script, "t", env)
|
||||
if not script then
|
||||
emu.print_warning("error loading layout script " .. err)
|
||||
return
|
||||
end
|
||||
local hooks = script()
|
||||
if hooks ~= nil then
|
||||
table.insert(scripts, hooks)
|
||||
end
|
||||
end
|
||||
|
||||
emu.register_callback(prepare_layout, "layout")
|
||||
frame_subscription = emu.add_machine_frame_notifier(function ()
|
||||
if manager.machine.paused then
|
||||
return
|
||||
end
|
||||
for num, scr in pairs(scripts) do
|
||||
if scr.frame then
|
||||
scr.frame()
|
||||
end
|
||||
end
|
||||
end)
|
||||
emu.register_prestart(function ()
|
||||
for num, scr in pairs(scripts) do
|
||||
if scr.reset then
|
||||
scr.reset()
|
||||
end
|
||||
end
|
||||
end)
|
||||
stop_subscription = emu.add_machine_stop_notifier(function ()
|
||||
scripts = {}
|
||||
end)
|
||||
end
|
||||
|
||||
return exports
|
||||
10
bios/Arcade/MAME/plugins/layout/plugin.json
Normal file
10
bios/Arcade/MAME/plugins/layout/plugin.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"plugin": {
|
||||
"name": "layout",
|
||||
"description": "Layout helper plugin",
|
||||
"version": "0.0.1",
|
||||
"author": "Carl",
|
||||
"type": "plugin",
|
||||
"start": "true"
|
||||
}
|
||||
}
|
||||
35
bios/Arcade/MAME/plugins/plugin.schema
Normal file
35
bios/Arcade/MAME/plugins/plugin.schema
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"plugin": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z][0-9A-Za-z_]*$"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"author": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"pattern": "^(plugin|library)$"
|
||||
},
|
||||
"start": {
|
||||
"type": "string",
|
||||
"pattern": "^(true|false)$"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [ "name", "description", "version", "author", "type" ]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [ "plugin" ]
|
||||
}
|
||||
175
bios/Arcade/MAME/plugins/portname/init.lua
Normal file
175
bios/Arcade/MAME/plugins/portname/init.lua
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
-- license:BSD-3-Clause
|
||||
-- copyright-holders:Carl
|
||||
-- data files are json files named <romname>.json
|
||||
-- {
|
||||
-- "import":"<import filename>"
|
||||
-- "ports":{
|
||||
-- "<ioport name>":{
|
||||
-- "labels":{
|
||||
-- "<field mask>":{
|
||||
-- "player":<int player number>,
|
||||
-- "name":"<field label>"
|
||||
-- }
|
||||
-- },{
|
||||
-- ...
|
||||
-- }
|
||||
-- }
|
||||
-- }
|
||||
-- any additional metadata can be included for other usage
|
||||
-- and will be ignored
|
||||
local exports = {}
|
||||
exports.name = "portname"
|
||||
exports.version = "0.0.1"
|
||||
exports.description = "IOPort name/translation plugin"
|
||||
exports.license = "BSD-3-Clause"
|
||||
exports.author = { name = "Carl" }
|
||||
|
||||
local portname = exports
|
||||
|
||||
function portname.startplugin()
|
||||
local json = require("json")
|
||||
local ctrlrpath = manager.options.entries.ctrlrpath:value():match("([^;]+)")
|
||||
local function get_filename(nosoft)
|
||||
local filename
|
||||
if emu.softname() ~= "" and not nosoft then
|
||||
local soft = emu.softname():match("([^:]*)$")
|
||||
filename = emu.romname() .. "_" .. soft .. ".json"
|
||||
else
|
||||
filename = emu.romname() .. ".json"
|
||||
end
|
||||
return filename
|
||||
end
|
||||
|
||||
local function parse_names(ctable, depth)
|
||||
if depth >= 5 then
|
||||
emu.print_error("portname: max import depth exceeded")
|
||||
return
|
||||
end
|
||||
if ctable.import then
|
||||
local file = emu.file(ctrlrpath .. "/portname", "r")
|
||||
local ret = file:open(ctable.import)
|
||||
if not ret then
|
||||
parse_names(json.parse(file:read(file:size())), depth + 1)
|
||||
end
|
||||
end
|
||||
if not ctable.ports then
|
||||
return
|
||||
end
|
||||
for pname, port in pairs(ctable.ports) do
|
||||
local ioport = manager.machine.ioport.ports[pname]
|
||||
if ioport then
|
||||
for mask, label in pairs(port.labels) do
|
||||
for num3, field in pairs(ioport.fields) do
|
||||
local nummask = tonumber(mask, 16)
|
||||
if nummask == field.mask and label.player == field.player then
|
||||
field.live.name = label.name
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
emu.register_start(function()
|
||||
local file = emu.file(ctrlrpath .. "/portname", "r")
|
||||
local ret = file:open(get_filename())
|
||||
if ret then
|
||||
if emu.softname() ~= "" then
|
||||
local parent
|
||||
for tag, image in pairs(manager.machine.images) do
|
||||
parent = image.software_parent
|
||||
if parent then
|
||||
break
|
||||
end
|
||||
end
|
||||
if parent then
|
||||
ret = file:open(emu.romname() .. "_" .. parent:match("([^:]*)$") .. ".json")
|
||||
end
|
||||
end
|
||||
if ret then
|
||||
ret = file:open(get_filename(true))
|
||||
if ret then
|
||||
ret = file:open(manager.machine.system.parent .. ".json")
|
||||
if ret then
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
parse_names(json.parse(file:read(file:size())), 0)
|
||||
end)
|
||||
|
||||
local function menu_populate()
|
||||
return {{ _("Save input names to file"), "", 0 }}
|
||||
end
|
||||
|
||||
local function menu_callback(index, event)
|
||||
if event == "select" then
|
||||
local ports = {}
|
||||
for pname, port in pairs(manager.machine.ioport.ports) do
|
||||
local labels = {}
|
||||
local sort = {}
|
||||
for fname, field in pairs(port.fields) do
|
||||
local mask = string.format("%x", field.mask)
|
||||
if not labels[mask] then
|
||||
sort[#sort + 1] = mask
|
||||
labels[mask] = { name = fname, player = field.player }
|
||||
setmetatable(labels[mask], { __tojson = function(v,s)
|
||||
local label = { name = v.name, player = v.player }
|
||||
setmetatable(label, { __jsonorder = { "player", "name" }})
|
||||
return json.stringify(label) end })
|
||||
end
|
||||
end
|
||||
if #sort > 0 then
|
||||
table.sort(sort, function(i, j) return tonumber(i, 16) < tonumber(j, 16) end)
|
||||
setmetatable(labels, { __jsonorder = sort })
|
||||
ports[pname] = { labels = labels }
|
||||
end
|
||||
end
|
||||
local function check_path(path)
|
||||
local attr = lfs.attributes(path)
|
||||
if not attr then
|
||||
lfs.mkdir(path)
|
||||
if not lfs.attributes(path) then
|
||||
manager.machine:popmessage(_("Failed to save input name file"))
|
||||
emu.print_verbose("portname: unable to create path " .. path)
|
||||
return false
|
||||
end
|
||||
elseif attr.mode ~= "directory" then
|
||||
manager.machine:popmessage(_("Failed to save input name file"))
|
||||
emu.print_verbose("portname: path exists but isn't directory " .. path)
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
if not check_path(ctrlrpath) then
|
||||
return false
|
||||
end
|
||||
if not check_path(ctrlrpath .. "/portname") then
|
||||
return false
|
||||
end
|
||||
local filename = get_filename()
|
||||
local file = io.open(ctrlrpath .. "/portname/" .. filename, "r")
|
||||
if file then
|
||||
emu.print_verbose("portname: input name file exists " .. filename)
|
||||
manager.machine:popmessage(_("Failed to save input name file"))
|
||||
file:close()
|
||||
return false
|
||||
end
|
||||
file = io.open(ctrlrpath .. "/portname/" .. filename, "w")
|
||||
local ctable = { romname = emu.romname(), ports = ports }
|
||||
if emu.softname() ~= "" then
|
||||
ctable.softname = emu.softname()
|
||||
end
|
||||
setmetatable(ctable, { __jsonorder = { "romname", "softname", "ports" }})
|
||||
file:write(json.stringify(ctable, { indent = true }))
|
||||
file:close()
|
||||
manager.machine:popmessage(string.format(_("Input port name file saved to %s"), ctrlrpath .. "/portname/" .. filename))
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
emu.register_menu(menu_callback, menu_populate, _("Input ports"))
|
||||
end
|
||||
|
||||
return exports
|
||||
10
bios/Arcade/MAME/plugins/portname/plugin.json
Normal file
10
bios/Arcade/MAME/plugins/portname/plugin.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"plugin": {
|
||||
"name": "portname",
|
||||
"description": "IOPort name/translation plugin",
|
||||
"version": "0.0.1",
|
||||
"author": "Carl",
|
||||
"type": "plugin",
|
||||
"start": "false"
|
||||
}
|
||||
}
|
||||
350
bios/Arcade/MAME/plugins/timecode/init.lua
Normal file
350
bios/Arcade/MAME/plugins/timecode/init.lua
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
-- license:BSD-3-Clause
|
||||
-- copyright-holders:Vas Crabb
|
||||
local exports = {
|
||||
name = 'timecode',
|
||||
version = '0.0.1',
|
||||
description = 'Timecode recorder plugin',
|
||||
license = 'BSD-3-Clause',
|
||||
author = { name = 'Vas Crabb' } }
|
||||
|
||||
|
||||
local timecode = exports
|
||||
|
||||
local frame_subscription, stop_subscription
|
||||
|
||||
function timecode.startplugin()
|
||||
local file -- the timecode log file
|
||||
local write -- whether to record a timecode on the next emulated frame
|
||||
local text -- name of current part
|
||||
local frame_count -- emulated frame counter
|
||||
local start_frame -- start frame count for current part
|
||||
local start_time -- start time for current part
|
||||
local total_time -- total time of parts so far this session
|
||||
local count -- current timecode number
|
||||
local show_counter -- whether to show elapsed time since last timecode
|
||||
local show_total -- whether to show the total time of parts
|
||||
|
||||
local frame_mode -- 0 to count frames, 1 to assume 60 Hz
|
||||
local hotkey_seq -- input sequence to record timecode
|
||||
local hotkey_pressed -- whether the hotkey was pressed on the last frame update
|
||||
local hotkey_cfg -- configuration string for the hotkey
|
||||
|
||||
local item_framemode -- menu index of frame mode item
|
||||
local item_hotkey -- menu index of hotkey item
|
||||
local commonui -- common UI helpers
|
||||
local hotkey_poller -- helper for configuring hotkey
|
||||
|
||||
|
||||
local function get_settings_path()
|
||||
return manager.machine.options.entries.homepath:value():match('([^;]+)') .. '/timecode'
|
||||
end
|
||||
|
||||
|
||||
local function set_default_hotkey()
|
||||
hotkey_seq = manager.machine.input:seq_from_tokens('KEYCODE_F12 NOT KEYCODE_LSHIFT NOT KEYCODE_RSHIFT NOT KEYCODE_LALT NOT KEYCODE_RALT')
|
||||
hotkey_cfg = nil
|
||||
end
|
||||
|
||||
|
||||
local function load_settings()
|
||||
-- set defaults
|
||||
frame_mode = 1
|
||||
set_default_hotkey()
|
||||
|
||||
-- try to open configuration file
|
||||
local cfgname = get_settings_path() .. '/plugin.cfg'
|
||||
local cfgfile = io.open(cfgname, 'r')
|
||||
if not cfgfile then
|
||||
return -- probably harmless, configuration just doesn't exist yet
|
||||
end
|
||||
|
||||
-- parse settings as JSON
|
||||
local json = require('json')
|
||||
local settings = json.parse(cfgfile:read('a'))
|
||||
cfgfile:close()
|
||||
if not settings then
|
||||
emu.print_error(string.format('Error loading timecode recorder settings: error parsing file "%s" as JSON', cfgname))
|
||||
return
|
||||
end
|
||||
|
||||
-- recover frame mode
|
||||
local count_frames = settings.count_frames
|
||||
if count_frames ~= nil then
|
||||
frame_mode = count_frames and 0 or 1
|
||||
end
|
||||
|
||||
-- recover hotkey assignment
|
||||
hotkey_cfg = settings.hotkey
|
||||
if hotkey_cfg then
|
||||
local seq = manager.machine.input:seq_from_tokens(hotkey_cfg)
|
||||
if seq then
|
||||
hotkey_seq = seq
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function save_settings()
|
||||
local path = get_settings_path()
|
||||
local attr = lfs.attributes(path)
|
||||
if not attr then
|
||||
lfs.mkdir(path)
|
||||
elseif attr.mode ~= 'directory' then
|
||||
emu.print_error(string.format('Error saving timecode recorder settings: "%s" is not a directory', path))
|
||||
return
|
||||
end
|
||||
local json = require('json')
|
||||
local settings = { count_frames = frame_mode == 0 }
|
||||
if hotkey_cfg then
|
||||
settings.hotkey = hotkey_cfg
|
||||
end
|
||||
local data = json.stringify(settings, { indent = true })
|
||||
local cfgname = path .. '/plugin.cfg'
|
||||
local cfgfile = io.open(cfgname, 'w')
|
||||
if not cfgfile then
|
||||
emu.print_error(string.format('Error saving timecode recorder settings: error opening file "%s" for writing', cfgname))
|
||||
return
|
||||
end
|
||||
cfgfile:write(data)
|
||||
cfgfile:close()
|
||||
end
|
||||
|
||||
|
||||
local function process_frame()
|
||||
if (not file) or manager.machine.paused then
|
||||
return
|
||||
end
|
||||
if write then
|
||||
write = false
|
||||
count = count + 1
|
||||
show_total = true
|
||||
|
||||
-- time from beginning of playback in milliseconds, HH:MM:SS.fff and frames
|
||||
local curtime = manager.machine.time
|
||||
local sec_start = curtime.seconds
|
||||
local msec_start = (sec_start * 1000) + curtime.msec
|
||||
local msec_start_str = string.format('%015d', msec_start)
|
||||
local curtime_str = string.format(
|
||||
'%02d:%02d:%02d.%03d',
|
||||
sec_start // (60 * 60),
|
||||
(sec_start // 60) % 60,
|
||||
sec_start % 60,
|
||||
msec_start % 1000)
|
||||
local frame_start_str = string.format('%015d', (frame_mode == 0) and frame_count or (msec_start * 60 // 1000))
|
||||
|
||||
-- elapsed from previous timecode in milliseconds, HH:MM:SS.fff and frames
|
||||
local elapsed = curtime - start_time
|
||||
local sec_elapsed = elapsed.seconds
|
||||
local msec_elapsed = (sec_elapsed * 1000) + elapsed.msec
|
||||
local msec_elapsed_str = string.format('%015d', msec_elapsed)
|
||||
local elapsed_str = string.format(
|
||||
'%02d:%02d:%02d.%03d',
|
||||
sec_elapsed // (60 * 60),
|
||||
(sec_elapsed // 60) % 60,
|
||||
sec_elapsed % 60,
|
||||
msec_elapsed % 1000)
|
||||
local frame_elapsed_str = string.format('%015d', (frame_mode == 0) and (frame_count - start_frame) or (msec_elapsed * 60 // 1000))
|
||||
|
||||
-- update start of part
|
||||
start_frame = frame_count
|
||||
start_time = curtime
|
||||
|
||||
local message
|
||||
local key
|
||||
if count == 1 then
|
||||
text = 'INTRO'
|
||||
show_counter = true
|
||||
message = string.format(_p('plugin-timecode', 'TIMECODE: Intro started at %s'), curtime_str)
|
||||
key = 'INTRO_START'
|
||||
elseif count == 2 then
|
||||
total_time = total_time + elapsed
|
||||
show_counter = false
|
||||
message = string.format(_p('plugin-timecode', 'TIMECODE: Intro duration %s'), elapsed_str)
|
||||
key = 'INTRO_STOP'
|
||||
elseif count == 3 then
|
||||
text = 'GAMEPLAY'
|
||||
show_counter = true
|
||||
message = string.format(_p('plugin-timecode', 'TIMECODE: Gameplay started at %s'), curtime_str)
|
||||
key = 'GAMEPLAY_START'
|
||||
elseif count == 4 then
|
||||
total_time = total_time + elapsed
|
||||
show_counter = false
|
||||
message = string.format(_p('plugin-timecode', 'TIMECODE: Gameplay duration %s'), elapsed_str)
|
||||
key = 'GAMEPLAY_STOP'
|
||||
elseif (count % 2) == 1 then
|
||||
local extrano = (count - 3) // 2
|
||||
text = string.format('EXTRA %d', extrano)
|
||||
show_counter = true
|
||||
message = string.format(_p('plugin-timecode', 'TIMECODE: Extra %d started at %s'), extrano, curtime_str)
|
||||
key = string.format('EXTRA_START_%03d', extrano)
|
||||
else
|
||||
local extrano = (count - 4) // 2
|
||||
total_time = total_time + elapsed
|
||||
show_counter = false
|
||||
message = string.format(_p('plugin-timecode', 'TIMECODE: Extra %d duration %s'), extrano, elapsed_str)
|
||||
key = string.format('EXTRA_STOP_%03d', extrano)
|
||||
end
|
||||
|
||||
emu.print_info(message)
|
||||
manager.machine:popmessage(message)
|
||||
|
||||
file:write(
|
||||
string.format(
|
||||
'%-19s %s %s %s %s %s %s\n',
|
||||
key,
|
||||
curtime_str, elapsed_str,
|
||||
msec_start_str, msec_elapsed_str,
|
||||
frame_start_str, frame_elapsed_str))
|
||||
end
|
||||
frame_count = frame_count + 1
|
||||
end
|
||||
|
||||
|
||||
local function process_frame_done()
|
||||
local machine = manager.machine
|
||||
if show_counter then
|
||||
-- show duration of current part
|
||||
local counter = (machine.time - start_time).seconds
|
||||
local counter_str = string.format(
|
||||
machine.paused and _p('plugin-timecode', ' %s%s%02d:%02d [paused] ') or _p('plugin-timecode', ' %s%s%02d:%02d '),
|
||||
text,
|
||||
(#text > 0) and ' ' or '',
|
||||
(counter // 60) % 60,
|
||||
counter % 60)
|
||||
machine.render.ui_container:draw_text('right', 0, counter_str, 0xf0f01010, 0xff000000)
|
||||
end
|
||||
if show_total then
|
||||
-- show total time for all parts so far
|
||||
local total = ((show_counter and (machine.time - start_time) or emu.attotime()) + total_time).seconds
|
||||
total_str = string.format(_p('plugin-timecode', 'TOTAL %02d:%02d '), (total // 60) % 60, total % 60)
|
||||
machine.render.ui_container:draw_text('left', 0, total_str, 0xf010f010, 0xff000000)
|
||||
end
|
||||
if file then
|
||||
local pressed = machine.input:seq_pressed(hotkey_seq)
|
||||
if (not hotkey_pressed) and pressed then
|
||||
write = true
|
||||
end
|
||||
hotkey_pressed = pressed
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function start()
|
||||
file = nil
|
||||
show_counter = false
|
||||
show_total = false
|
||||
load_settings()
|
||||
|
||||
-- only do timecode recording if we're doing input recording
|
||||
local options = manager.machine.options.entries
|
||||
local filename = options.record:value()
|
||||
if #filename > 0 then
|
||||
filename = filename .. '.timecode'
|
||||
emu.print_info(string.format('Record input timecode file: %s', filename))
|
||||
file = emu.file(options.input_directory:value(), 0x0e) -- FIXME: magic number for flags
|
||||
local openerr = file:open(filename)
|
||||
if openerr then
|
||||
-- TODO: this used to throw a fatal error and log the error description
|
||||
emu.print_error('Failed to open file for input timecode recording')
|
||||
file = nil
|
||||
else
|
||||
write = false
|
||||
text = ''
|
||||
frame_count = 0
|
||||
start_frame = 0
|
||||
start_time = emu.attotime()
|
||||
total_time = emu.attotime()
|
||||
count = 0
|
||||
show_counter = false
|
||||
show_total = false
|
||||
hotkey_pressed = false
|
||||
|
||||
file:write('# ==========================================\n')
|
||||
file:write('# TIMECODE FILE FOR VIDEO PREVIEW GENERATION\n')
|
||||
file:write('# ==========================================\n')
|
||||
file:write('#\n')
|
||||
file:write('# VIDEO_PART: code of video timecode\n')
|
||||
file:write('# START: start time (hh:mm:ss.mmm)\n')
|
||||
file:write('# ELAPSED: elapsed time (hh:mm:ss.mmm)\n')
|
||||
file:write('# MSEC_START: start time (milliseconds)\n')
|
||||
file:write('# MSEC_ELAPSED: elapsed time (milliseconds)\n')
|
||||
file:write('# FRAME_START: start time (frames)\n')
|
||||
file:write('# FRAME_ELAPSED: elapsed time (frames)\n')
|
||||
file:write('#\n')
|
||||
file:write('# VIDEO_PART======= START======= ELAPSED===== MSEC_START===== MSEC_ELAPSED=== FRAME_START==== FRAME_ELAPSED==\n')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function stop()
|
||||
-- close the file if we're recording
|
||||
if file then
|
||||
file:close()
|
||||
file = nil
|
||||
end
|
||||
|
||||
-- try to save settings
|
||||
save_settings()
|
||||
end
|
||||
|
||||
|
||||
local function menu_callback(index, event)
|
||||
if hotkey_poller then
|
||||
if hotkey_poller:poll() then
|
||||
if hotkey_poller.sequence then
|
||||
hotkey_seq = hotkey_poller.sequence
|
||||
hotkey_cfg = manager.machine.input:seq_to_tokens(hotkey_seq)
|
||||
end
|
||||
hotkey_poller = nil
|
||||
return true
|
||||
end
|
||||
elseif index == item_framemode then
|
||||
if (event == 'select') or (event == 'left') or (event == 'right') then
|
||||
frame_mode = (frame_mode ~= 0) and 0 or 1
|
||||
return true
|
||||
end
|
||||
elseif index == item_hotkey then
|
||||
if event == 'select' then
|
||||
if not commonui then
|
||||
commonui = require('commonui')
|
||||
end
|
||||
hotkey_poller = commonui.switch_polling_helper()
|
||||
return true
|
||||
elseif event == 'clear' then
|
||||
set_default_hotkey()
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
|
||||
local function menu_populate()
|
||||
local result = { }
|
||||
table.insert(result, { _p('plugin-timecode', 'Timecode Recorder'), '', 'off' })
|
||||
table.insert(result, { '---', '', '' })
|
||||
|
||||
local frame_mode_val = (frame_mode > 0) and _p('plugin-timecode', 'Assume 60 Hz') or _p('plugins-timecode', 'Count emulated frames')
|
||||
table.insert(result, { _p('plugin-timecode', 'Frame numbers'), frame_mode_val, (frame_mode > 0) and 'l' or 'r' })
|
||||
item_framemode = #result
|
||||
|
||||
table.insert(result, { _p('plugin-timecode', 'Hotkey'), manager.machine.input:seq_name(hotkey_seq), hotkey_poller and 'lr' or '' })
|
||||
item_hotkey = #result
|
||||
|
||||
if hotkey_poller then
|
||||
return hotkey_poller:overlay(result)
|
||||
else
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
frame_subscription = emu.add_machine_frame_notifier(process_frame)
|
||||
emu.register_frame_done(process_frame_done)
|
||||
emu.register_prestart(start)
|
||||
stop_subscription = emu.add_machine_stop_notifier(stop)
|
||||
emu.register_menu(menu_callback, menu_populate, _p('plugin-timecode', 'Timecode Recorder'))
|
||||
end
|
||||
|
||||
return exports
|
||||
10
bios/Arcade/MAME/plugins/timecode/plugin.json
Normal file
10
bios/Arcade/MAME/plugins/timecode/plugin.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"plugin": {
|
||||
"name": "timecode",
|
||||
"description": "Timecode recorder plugin",
|
||||
"version": "0.0.1",
|
||||
"author": "Vas Crabb",
|
||||
"type": "plugin",
|
||||
"start": "false"
|
||||
}
|
||||
}
|
||||
79
bios/Arcade/MAME/plugins/timer/init.lua
Normal file
79
bios/Arcade/MAME/plugins/timer/init.lua
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
-- license:BSD-3-Clause
|
||||
-- copyright-holders:Vas Crabb
|
||||
-- TODO: track time properly across soft reset and state load
|
||||
local exports = {
|
||||
name = 'timer',
|
||||
version = '0.0.3',
|
||||
description = 'Game play timer',
|
||||
license = 'BSD-3-Clause',
|
||||
author = { name = 'Vas Crabb' } }
|
||||
|
||||
local timer = exports
|
||||
|
||||
local reset_subscription, stop_subscription
|
||||
|
||||
function timer.startplugin()
|
||||
local total_time = 0
|
||||
local start_time = 0
|
||||
local play_count = 0
|
||||
local emu_total = emu.attotime()
|
||||
|
||||
local reference = 0
|
||||
local lastupdate
|
||||
local highlight -- hacky - workaround for the menu not remembering the selected item if its ref is nullptr
|
||||
|
||||
|
||||
local function sectohms(time)
|
||||
local hrs = time // 3600
|
||||
local min = (time % 3600) // 60
|
||||
local sec = time % 60
|
||||
return string.format(_p('plugin-timer', '%03d:%02d:%02d'), hrs, min, sec)
|
||||
end
|
||||
|
||||
local function menu_populate()
|
||||
lastupdate = os.time()
|
||||
local refname = (reference == 0) and _p('plugin-timer', 'Wall clock') or _p('plugin-timer', 'Emulated time')
|
||||
local time = (reference == 0) and (lastupdate - start_time) or manager.machine.time.seconds
|
||||
local total = (reference == 0) and (total_time + time) or (manager.machine.time + emu_total).seconds
|
||||
return
|
||||
{
|
||||
{ _p("plugin-timer", "Reference"), refname, (reference == 0) and 'r' or 'l' },
|
||||
{ '---', '', '' },
|
||||
{ _p("plugin-timer", "Current time"), sectohms(time), "off" },
|
||||
{ _p("plugin-timer", "Total time"), sectohms(total), "off" },
|
||||
{ _p("plugin-timer", "Play Count"), tostring(play_count), "off" } },
|
||||
highlight,
|
||||
"idle"
|
||||
end
|
||||
|
||||
local function menu_callback(index, event)
|
||||
if (index == 1) and ((event == 'left') or (event == 'right') or (event == 'select')) then
|
||||
reference = reference ~ 1
|
||||
return true
|
||||
end
|
||||
highlight = index
|
||||
return os.time() > lastupdate
|
||||
end
|
||||
|
||||
|
||||
reset_subscription = emu.add_machine_reset_notifier(
|
||||
function ()
|
||||
if emu.romname() ~= '___empty' then
|
||||
start_time = os.time()
|
||||
local persister = require('timer/timer_persist')
|
||||
total_time, play_count, emu_total = persister:start_session()
|
||||
end
|
||||
end)
|
||||
|
||||
stop_subscription = emu.add_machine_stop_notifier(
|
||||
function ()
|
||||
if emu.romname() ~= '___empty' then
|
||||
local persister = require('timer/timer_persist')
|
||||
persister:update_totals(start_time)
|
||||
end
|
||||
end)
|
||||
|
||||
emu.register_menu(menu_callback, menu_populate, _p("plugin-timer", "Timer"))
|
||||
end
|
||||
|
||||
return exports
|
||||
10
bios/Arcade/MAME/plugins/timer/plugin.json
Normal file
10
bios/Arcade/MAME/plugins/timer/plugin.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"plugin": {
|
||||
"name": "timer",
|
||||
"description": "Game play timer",
|
||||
"version": "0.0.3",
|
||||
"author": "Vas Crabb",
|
||||
"type": "plugin",
|
||||
"start": "false"
|
||||
}
|
||||
}
|
||||
249
bios/Arcade/MAME/plugins/timer/timer_persist.lua
Normal file
249
bios/Arcade/MAME/plugins/timer/timer_persist.lua
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
-- license:BSD-3-Clause
|
||||
-- copyright-holders:Vas Crabb
|
||||
|
||||
local sqlite3 = require('lsqlite3')
|
||||
|
||||
|
||||
local function check_schema(db)
|
||||
local create_statement =
|
||||
[[CREATE TABLE timer (
|
||||
driver VARCHAR(32) NOT NULL,
|
||||
softlist VARCHAR(24) NOT NULL DEFAULT '',
|
||||
software VARCHAR(16) NOT NULL DEFAULT '',
|
||||
total_time INTEGER NOT NULL DEFAULT 0,
|
||||
play_count INTEGER NOT NULL DEFAULT 0,
|
||||
emu_sec INTEGER NOT NULL DEFAULT 0,
|
||||
emu_nsec INTEGER NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (driver, softlist, software));]]
|
||||
|
||||
-- create table if it doesn't exist yet
|
||||
local table_found = false
|
||||
db:exec(
|
||||
[[SELECT * FROM sqlite_master WHERE type = 'table' AND name='timer';]],
|
||||
function()
|
||||
table_found = true
|
||||
end)
|
||||
if not table_found then
|
||||
emu.print_verbose('Creating timer database table')
|
||||
db:exec(create_statement)
|
||||
return
|
||||
end
|
||||
|
||||
-- check recently added columns
|
||||
local have_softlist = false
|
||||
local have_emu_sec = false
|
||||
local have_emu_nsec = false
|
||||
db:exec(
|
||||
[[PRAGMA table_info(timer);]],
|
||||
function(udata, n, vals, cols)
|
||||
for i, name in ipairs(cols) do
|
||||
if name == 'name' then
|
||||
if vals[i] == 'softlist' then
|
||||
have_softlist = true
|
||||
elseif vals[i] == 'emu_sec' then
|
||||
have_emu_sec = true
|
||||
elseif vals[i] == 'emu_nsec' then
|
||||
have_emu_nsec = true
|
||||
end
|
||||
return 0
|
||||
end
|
||||
end
|
||||
return 0
|
||||
end)
|
||||
if not have_softlist then
|
||||
emu.print_verbose('Adding softlist column to timer database')
|
||||
db:exec([[ALTER TABLE timer ADD COLUMN softlist VARCHAR(24) NOT NULL DEFAULT '';]])
|
||||
local to_split = { }
|
||||
db:exec(
|
||||
[[SELECT DISTINCT software FROM timer WHERE software LIKE '%:%';]],
|
||||
function(udata, n, vals)
|
||||
table.insert(to_split, vals[1])
|
||||
return 0
|
||||
end)
|
||||
if #to_split > 0 then
|
||||
local update = db:prepare([[UPDATE timer SET softlist = ?, software = ? WHERE software = ?;]])
|
||||
for i, softname in ipairs(to_split) do
|
||||
local x, y = softname:find(':')
|
||||
local softlist = softname:sub(1, x - 1)
|
||||
local software = softname:sub(y + 1)
|
||||
update:bind_values(softlist, software, softname)
|
||||
update:step()
|
||||
update:reset()
|
||||
end
|
||||
end
|
||||
end
|
||||
if not have_emu_sec then
|
||||
emu.print_verbose('Adding emu_sec column to timer database')
|
||||
db:exec([[ALTER TABLE timer ADD COLUMN emu_sec INTEGER NOT NULL DEFAULT 0;]])
|
||||
end
|
||||
if not have_emu_nsec then
|
||||
emu.print_verbose('Adding emu_nsec column to timer database')
|
||||
db:exec([[ALTER TABLE timer ADD COLUMN emu_nsec INTEGER NOT NULL DEFAULT 0;]])
|
||||
end
|
||||
|
||||
-- check the required columns are in the primary key
|
||||
local index_name
|
||||
db:exec(
|
||||
[[SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = 'timer';]],
|
||||
function(udata, n, vals)
|
||||
index_name = vals[1]
|
||||
end)
|
||||
local index_good
|
||||
if index_name then
|
||||
local driver_indexed = false
|
||||
local softlist_indexed = false
|
||||
local software_indexed = false
|
||||
db:exec(
|
||||
string.format([[PRAGMA index_info('%s');]], index_name), -- can't use prepared statement for PRAGMA
|
||||
function(udata, n, vals, cols)
|
||||
for i, name in ipairs(cols) do
|
||||
if name == 'name' then
|
||||
if vals[i] == 'driver' then
|
||||
driver_indexed = true
|
||||
elseif vals[i] == 'softlist' then
|
||||
softlist_indexed = true
|
||||
elseif vals[i] == 'software' then
|
||||
software_indexed = true
|
||||
end
|
||||
return 0
|
||||
end
|
||||
end
|
||||
return 0
|
||||
end)
|
||||
index_good = driver_indexed and softlist_indexed and software_indexed
|
||||
end
|
||||
|
||||
-- if the required columns are not indexed, migrate to a new table with desired primary key
|
||||
if not index_good then
|
||||
emu.print_verbose('Re-indexing timer database table')
|
||||
db:exec([[DROP TABLE IF EXISTS timer_old;]])
|
||||
db:exec([[ALTER TABLE timer RENAME TO timer_old;]])
|
||||
db:exec(create_statement)
|
||||
db:exec(
|
||||
[[INSERT
|
||||
INTO timer (driver, softlist, software, total_time, play_count, emu_sec, emu_nsec)
|
||||
SELECT driver, softlist, software, total_time, play_count, emu_sec, emu_nsec FROM timer_old;]])
|
||||
db:exec([[DROP TABLE timer_old;]])
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function open_database()
|
||||
-- make sure settings directory exists
|
||||
local dir = manager.machine.options.entries.homepath:value():match('([^;]+)') .. '/timer'
|
||||
local attr = lfs.attributes(dir)
|
||||
if not attr then
|
||||
lfs.mkdir(dir)
|
||||
elseif attr.mode ~= 'directory' then
|
||||
emu.print_error(string.format('Error opening timer database: "%s" is not a directory', dir))
|
||||
return nil
|
||||
end
|
||||
|
||||
-- open database
|
||||
local filename = dir .. '/timer.db'
|
||||
local db = sqlite3.open(filename)
|
||||
if not db then
|
||||
emu.print_error(string.format('Error opening timer database file "%s"', filename))
|
||||
return nil
|
||||
end
|
||||
|
||||
-- make sure schema is up-to-date
|
||||
check_schema(db)
|
||||
return db
|
||||
end
|
||||
|
||||
|
||||
local function get_software()
|
||||
local softname = emu.softname()
|
||||
local i, j = softname:find(':')
|
||||
if i then
|
||||
return softname:sub(1, i - 1), softname:sub(j + 1)
|
||||
else
|
||||
-- FIXME: need a way to get the implicit software list when no colon in the option value
|
||||
return '', softname
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function get_current(db)
|
||||
local statement = db:prepare(
|
||||
[[SELECT
|
||||
total_time, play_count, emu_sec, emu_nsec
|
||||
FROM timer
|
||||
WHERE driver = ? AND softlist = ? AND software = ?;]])
|
||||
statement:bind_values(emu.romname(), get_software())
|
||||
local result
|
||||
if statement:step() == sqlite3.ROW then
|
||||
result = statement:get_named_values()
|
||||
end
|
||||
statement:reset()
|
||||
return result
|
||||
end
|
||||
|
||||
|
||||
local lib = { }
|
||||
|
||||
function lib:start_session()
|
||||
-- open database
|
||||
local db = open_database()
|
||||
if not db then
|
||||
return 0, 0, emu.attotime()
|
||||
end
|
||||
|
||||
-- get existing values
|
||||
local row = get_current(db)
|
||||
local update
|
||||
if row then
|
||||
update = db:prepare(
|
||||
[[UPDATE timer
|
||||
SET play_count = play_count + 1
|
||||
WHERE driver = ? AND softlist = ? AND software = ?;]])
|
||||
else
|
||||
row = { total_time = 0, play_count = 0, emu_sec = 0, emu_nsec = 0 }
|
||||
update = db:prepare(
|
||||
[[INSERT
|
||||
INTO timer (driver, softlist, software, total_time, play_count, emu_sec, emu_nsec)
|
||||
VALUES (?, ?, ?, 0, 1, 0, 0);]])
|
||||
end
|
||||
update:bind_values(emu.romname(), get_software())
|
||||
update:step()
|
||||
update:reset()
|
||||
return row.total_time, row.play_count + 1, emu.attotime.from_seconds(row.emu_sec) + emu.attotime.from_nsec(row.emu_nsec)
|
||||
end
|
||||
|
||||
function lib:update_totals(start)
|
||||
-- open database
|
||||
local db = open_database()
|
||||
if not db then
|
||||
return
|
||||
end
|
||||
|
||||
-- get existing values
|
||||
local row = get_current(db)
|
||||
if not row then
|
||||
row = { total_time = 0, play_count = 1, emu_sec = 0, emu_nsec = 0 }
|
||||
end
|
||||
|
||||
-- calculate new totals
|
||||
local emu_total = emu.attotime.from_seconds(row.emu_sec) + emu.attotime.from_nsec(row.emu_nsec) + manager.machine.time
|
||||
row.total_time = os.time() - start + row.total_time
|
||||
row.emu_sec = emu_total.seconds
|
||||
row.emu_nsec = emu_total.nsec
|
||||
|
||||
-- update database
|
||||
local update = db:prepare(
|
||||
[[INSERT OR REPLACE
|
||||
INTO timer (driver, softlist, software, total_time, play_count, emu_sec, emu_nsec)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?);]])
|
||||
local softlist, software = get_software()
|
||||
update:bind_values(emu.romname(), softlist, software, row.total_time, row.play_count, row.emu_sec, row.emu_nsec)
|
||||
update:step()
|
||||
update:reset()
|
||||
|
||||
-- close database
|
||||
if db:close() ~= sqlite3.OK then
|
||||
emu.print_error('Error closing timer database')
|
||||
end
|
||||
end
|
||||
|
||||
return lib
|
||||
401
bios/Arcade/MAME/plugins/xml/init.lua
Normal file
401
bios/Arcade/MAME/plugins/xml/init.lua
Normal file
|
|
@ -0,0 +1,401 @@
|
|||
-- license:MIT
|
||||
-- copyright-holders:Gavin Kistner
|
||||
|
||||
local exports = {}
|
||||
exports.name = "SLAXML"
|
||||
exports.version = "0.8"
|
||||
exports.homepage = "http://github.com/Phrogz/SLAXML"
|
||||
exports.description = "Lua SLAX XML parser"
|
||||
exports.tags = {"xml"}
|
||||
exports.license = "MIT"
|
||||
exports.author = {
|
||||
name = "Gavin Kistner",
|
||||
}
|
||||
|
||||
local SLAXML = exports
|
||||
|
||||
--[=====================================================================[
|
||||
v0.8 Copyright © 2013-2018 Gavin Kistner <!@phrogz.net>; MIT Licensed
|
||||
See http://github.com/Phrogz/SLAXML for details.
|
||||
--]=====================================================================]
|
||||
SLAXML.VERSION = "0.8"
|
||||
SLAXML._call = {
|
||||
pi = function(target,content)
|
||||
print(string.format("<?%s %s?>",target,content))
|
||||
end,
|
||||
comment = function(content)
|
||||
print(string.format("<!-- %s -->",content))
|
||||
end,
|
||||
startElement = function(name,nsURI,nsPrefix)
|
||||
io.write("<")
|
||||
if nsPrefix then io.write(nsPrefix,":") end
|
||||
io.write(name)
|
||||
if nsURI then io.write(" (ns='",nsURI,"')") end
|
||||
print(">")
|
||||
end,
|
||||
attribute = function(name,value,nsURI,nsPrefix)
|
||||
io.write(' ')
|
||||
if nsPrefix then io.write(nsPrefix,":") end
|
||||
io.write(name,'=',string.format('%q',value))
|
||||
if nsURI then io.write(" (ns='",nsURI,"')") end
|
||||
io.write("\n")
|
||||
end,
|
||||
text = function(text,cdata)
|
||||
print(string.format(" %s: %q",cdata and 'cdata' or 'text',text))
|
||||
end,
|
||||
closeElement = function(name,nsURI,nsPrefix)
|
||||
io.write("</")
|
||||
if nsPrefix then io.write(nsPrefix,":") end
|
||||
print(name..">")
|
||||
end,
|
||||
}
|
||||
|
||||
function SLAXML:parser(callbacks)
|
||||
return { _call=callbacks or self._call, parse=SLAXML.parse }
|
||||
end
|
||||
|
||||
function SLAXML:parse(xml,options)
|
||||
if not options then options = { stripWhitespace=false } end
|
||||
|
||||
-- Cache references for maximum speed
|
||||
local find, sub, gsub, char, push, pop, concat = string.find, string.sub, string.gsub, string.char, table.insert, table.remove, table.concat
|
||||
local first, last, match1, match2, match3, pos2, nsURI
|
||||
local unpack = unpack or table.unpack
|
||||
local pos = 1
|
||||
local state = "text"
|
||||
local textStart = 1
|
||||
local currentElement={}
|
||||
local currentAttributes={}
|
||||
local currentAttributeCt -- manually track length since the table is re-used
|
||||
local nsStack = {}
|
||||
local anyElement = false
|
||||
|
||||
local utf8markers = { {0x7FF,192}, {0xFFFF,224}, {0x1FFFFF,240} }
|
||||
local function utf8(decimal) -- convert unicode code point to utf-8 encoded character string
|
||||
if decimal<128 then return char(decimal) end
|
||||
local charbytes = {}
|
||||
for bytes,vals in ipairs(utf8markers) do
|
||||
if decimal<=vals[1] then
|
||||
for b=bytes+1,2,-1 do
|
||||
local mod = decimal%64
|
||||
decimal = (decimal-mod)/64
|
||||
charbytes[b] = char(128+mod)
|
||||
end
|
||||
charbytes[1] = char(vals[2]+decimal)
|
||||
return concat(charbytes)
|
||||
end
|
||||
end
|
||||
end
|
||||
local entityMap = { ["lt"]="<", ["gt"]=">", ["amp"]="&", ["quot"]='"', ["apos"]="'" }
|
||||
local entitySwap = function(orig,n,s) return entityMap[s] or n=="#" and utf8(tonumber('0'..s)) or orig end
|
||||
local function unescape(str) return gsub( str, '(&(#?)([%d%a]+);)', entitySwap ) end
|
||||
|
||||
local function finishText()
|
||||
if first>textStart and self._call.text then
|
||||
local text = sub(xml,textStart,first-1)
|
||||
if options.stripWhitespace then
|
||||
text = gsub(text,'^%s+','')
|
||||
text = gsub(text,'%s+$','')
|
||||
if #text==0 then text=nil end
|
||||
end
|
||||
if text then self._call.text(unescape(text),false) end
|
||||
end
|
||||
end
|
||||
|
||||
local function findPI()
|
||||
first, last, match1, match2 = find( xml, '^<%?([:%a_][:%w_.-]*) ?(.-)%?>', pos )
|
||||
if first then
|
||||
finishText()
|
||||
if self._call.pi then self._call.pi(match1,match2) end
|
||||
pos = last+1
|
||||
textStart = pos
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local function findComment()
|
||||
first, last, match1 = find( xml, '^<!%-%-(.-)%-%->', pos )
|
||||
if first then
|
||||
finishText()
|
||||
if self._call.comment then self._call.comment(match1) end
|
||||
pos = last+1
|
||||
textStart = pos
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local function nsForPrefix(prefix)
|
||||
if prefix=='xml' then return 'http://www.w3.org/XML/1998/namespace' end -- http://www.w3.org/TR/xml-names/#ns-decl
|
||||
for i=#nsStack,1,-1 do if nsStack[i][prefix] then return nsStack[i][prefix] end end
|
||||
error(("Cannot find namespace for prefix %s"):format(prefix))
|
||||
end
|
||||
|
||||
local function startElement()
|
||||
anyElement = true
|
||||
first, last, match1 = find( xml, '^<([%a_][%w_.-]*)', pos )
|
||||
if first then
|
||||
currentElement[2] = nil -- reset the nsURI, since this table is re-used
|
||||
currentElement[3] = nil -- reset the nsPrefix, since this table is re-used
|
||||
finishText()
|
||||
pos = last+1
|
||||
first,last,match2 = find(xml, '^:([%a_][%w_.-]*)', pos )
|
||||
if first then
|
||||
currentElement[1] = match2
|
||||
currentElement[3] = match1 -- Save the prefix for later resolution
|
||||
match1 = match2
|
||||
pos = last+1
|
||||
else
|
||||
currentElement[1] = match1
|
||||
for i=#nsStack,1,-1 do if nsStack[i]['!'] then currentElement[2] = nsStack[i]['!']; break end end
|
||||
end
|
||||
currentAttributeCt = 0
|
||||
push(nsStack,{})
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local function findAttribute()
|
||||
first, last, match1 = find( xml, '^%s+([:%a_][:%w_.-]*)%s*=%s*', pos )
|
||||
if first then
|
||||
pos2 = last+1
|
||||
first, last, match2 = find( xml, '^"([^<"]*)"', pos2 ) -- FIXME: disallow non-entity ampersands
|
||||
if first then
|
||||
pos = last+1
|
||||
match2 = unescape(match2)
|
||||
else
|
||||
first, last, match2 = find( xml, "^'([^<']*)'", pos2 ) -- FIXME: disallow non-entity ampersands
|
||||
if first then
|
||||
pos = last+1
|
||||
match2 = unescape(match2)
|
||||
end
|
||||
end
|
||||
end
|
||||
if match1 and match2 then
|
||||
local currentAttribute = {match1,match2}
|
||||
local prefix,name = string.match(match1,'^([^:]+):([^:]+)$')
|
||||
if prefix then
|
||||
if prefix=='xmlns' then
|
||||
nsStack[#nsStack][name] = match2
|
||||
else
|
||||
currentAttribute[1] = name
|
||||
currentAttribute[4] = prefix
|
||||
end
|
||||
else
|
||||
if match1=='xmlns' then
|
||||
nsStack[#nsStack]['!'] = match2
|
||||
currentElement[2] = match2
|
||||
end
|
||||
end
|
||||
currentAttributeCt = currentAttributeCt + 1
|
||||
currentAttributes[currentAttributeCt] = currentAttribute
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local function findCDATA()
|
||||
first, last, match1 = find( xml, '^<!%[CDATA%[(.-)%]%]>', pos )
|
||||
if first then
|
||||
finishText()
|
||||
if self._call.text then self._call.text(match1,true) end
|
||||
pos = last+1
|
||||
textStart = pos
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local function closeElement()
|
||||
first, last, match1 = find( xml, '^%s*(/?)>', pos )
|
||||
if first then
|
||||
state = "text"
|
||||
pos = last+1
|
||||
textStart = pos
|
||||
|
||||
-- Resolve namespace prefixes AFTER all new/redefined prefixes have been parsed
|
||||
if currentElement[3] then currentElement[2] = nsForPrefix(currentElement[3]) end
|
||||
if self._call.startElement then self._call.startElement(unpack(currentElement)) end
|
||||
if self._call.attribute then
|
||||
for i=1,currentAttributeCt do
|
||||
if currentAttributes[i][4] then currentAttributes[i][3] = nsForPrefix(currentAttributes[i][4]) end
|
||||
self._call.attribute(unpack(currentAttributes[i]))
|
||||
end
|
||||
end
|
||||
|
||||
if match1=="/" then
|
||||
pop(nsStack)
|
||||
if self._call.closeElement then self._call.closeElement(unpack(currentElement)) end
|
||||
end
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local function findElementClose()
|
||||
first, last, match1, match2 = find( xml, '^</([%a_][%w_.-]*)%s*>', pos )
|
||||
if first then
|
||||
nsURI = nil
|
||||
for i=#nsStack,1,-1 do if nsStack[i]['!'] then nsURI = nsStack[i]['!']; break end end
|
||||
else
|
||||
first, last, match2, match1 = find( xml, '^</([%a_][%w_.-]*):([%a_][%w_.-]*)%s*>', pos )
|
||||
if first then nsURI = nsForPrefix(match2) end
|
||||
end
|
||||
if first then
|
||||
finishText()
|
||||
if self._call.closeElement then self._call.closeElement(match1,nsURI) end
|
||||
pos = last+1
|
||||
textStart = pos
|
||||
pop(nsStack)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
while pos<#xml do
|
||||
if state=="text" then
|
||||
if not (findPI() or findComment() or findCDATA() or findElementClose()) then
|
||||
if startElement() then
|
||||
state = "attributes"
|
||||
else
|
||||
first, last = find( xml, '^[^<]+', pos )
|
||||
pos = (first and last or pos) + 1
|
||||
end
|
||||
end
|
||||
elseif state=="attributes" then
|
||||
if not findAttribute() then
|
||||
if not closeElement() then
|
||||
error("Was in an element and couldn't find attributes or the close.")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not anyElement then error("Parsing did not discover any elements") end
|
||||
if #nsStack > 0 then error("Parsing ended with unclosed elements") end
|
||||
end
|
||||
|
||||
-- Optional parser that creates a flat DOM from parsing
|
||||
function SLAXML:dom(xml,opts)
|
||||
if not opts then opts={} end
|
||||
local rich = not opts.simple
|
||||
local push, pop = table.insert, table.remove
|
||||
local doc = {type="document", name="#doc", kids={}}
|
||||
local current,stack = doc, {doc}
|
||||
local builder = SLAXML:parser{
|
||||
startElement = function(name,nsURI,nsPrefix)
|
||||
local el = { type="element", name=name, kids={}, el=rich and {} or nil, attr={}, nsURI=nsURI, nsPrefix=nsPrefix, parent=rich and current or nil }
|
||||
if current==doc then
|
||||
if doc.root then error(("Encountered element '%s' when the document already has a root '%s' element"):format(name,doc.root.name)) end
|
||||
doc.root = rich and el or nil
|
||||
end
|
||||
push(current.kids,el)
|
||||
if current.el then push(current.el,el) end
|
||||
current = el
|
||||
push(stack,el)
|
||||
end,
|
||||
attribute = function(name,value,nsURI,nsPrefix)
|
||||
if not current or current.type~="element" then error(("Encountered an attribute %s=%s but I wasn't inside an element"):format(name,value)) end
|
||||
local attr = {type='attribute',name=name,nsURI=nsURI,nsPrefix=nsPrefix,value=value,parent=rich and current or nil}
|
||||
if rich then current.attr[name] = value end
|
||||
push(current.attr,attr)
|
||||
end,
|
||||
closeElement = function(name)
|
||||
if current.name~=name or current.type~="element" then error(("Received a close element notification for '%s' but was inside a '%s' %s"):format(name,current.name,current.type)) end
|
||||
pop(stack)
|
||||
current = stack[#stack]
|
||||
end,
|
||||
text = function(value,cdata)
|
||||
-- documents may only have text node children that are whitespace: https://www.w3.org/TR/xml/#NT-Misc
|
||||
if current.type=='document' and not value:find('^%s+$') then error(("Document has non-whitespace text at root: '%s'"):format(value:gsub('[\r\n\t]',{['\r']='\\r', ['\n']='\\n', ['\t']='\\t'}))) end
|
||||
push(current.kids,{type='text',name='#text',cdata=cdata and true or nil,value=value,parent=rich and current or nil})
|
||||
end,
|
||||
comment = function(value)
|
||||
push(current.kids,{type='comment',name='#comment',value=value,parent=rich and current or nil})
|
||||
end,
|
||||
pi = function(name,value)
|
||||
push(current.kids,{type='pi',name=name,value=value,parent=rich and current or nil})
|
||||
end
|
||||
}
|
||||
builder:parse(xml,opts)
|
||||
return doc
|
||||
end
|
||||
|
||||
local escmap = {["<"]="<", [">"]=">", ["&"]="&", ['"']=""", ["'"]="'"}
|
||||
local function esc(s) return s:gsub('[<>&"]', escmap) end
|
||||
|
||||
-- opts.indent: number of spaces, or string
|
||||
function SLAXML:xml(n,opts)
|
||||
opts = opts or {}
|
||||
local out = {}
|
||||
local tab = opts.indent and (type(opts.indent)=="number" and string.rep(" ",opts.indent) or opts.indent) or ""
|
||||
local ser = {}
|
||||
local omit = {}
|
||||
if opts.omit then for _,s in ipairs(opts.omit) do omit[s]=true end end
|
||||
|
||||
function ser.document(n)
|
||||
for _,kid in ipairs(n.kids) do
|
||||
if ser[kid.type] then ser[kid.type](kid,0) end
|
||||
end
|
||||
end
|
||||
|
||||
function ser.pi(n,depth)
|
||||
depth = depth or 0
|
||||
table.insert(out, tab:rep(depth)..'<?'..n.name..' '..n.value..'?>')
|
||||
end
|
||||
|
||||
function ser.element(n,depth)
|
||||
if n.nsURI and omit[n.nsURI] then return end
|
||||
depth = depth or 0
|
||||
local indent = tab:rep(depth)
|
||||
local name = n.nsPrefix and n.nsPrefix..':'..n.name or n.name
|
||||
local result = indent..'<'..name
|
||||
if n.attr and n.attr[1] then
|
||||
local sorted = n.attr
|
||||
if opts.sort then
|
||||
sorted = {}
|
||||
for i,a in ipairs(n.attr) do sorted[i]=a end
|
||||
table.sort(sorted,function(a,b)
|
||||
if a.nsPrefix and b.nsPrefix then
|
||||
return a.nsPrefix==b.nsPrefix and a.name<b.name or a.nsPrefix<b.nsPrefix
|
||||
elseif not (a.nsPrefix or b.nsPrefix) then
|
||||
return a.name<b.name
|
||||
elseif b.nsPrefix then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local attrs = {}
|
||||
for _,a in ipairs(sorted) do
|
||||
if (not a.nsURI or not omit[a.nsURI]) and not (omit[a.value] and a.name:find('^xmlns:')) then
|
||||
attrs[#attrs+1] = ' '..(a.nsPrefix and (a.nsPrefix..':') or '')..a.name..'="'..esc(a.value)..'"'
|
||||
end
|
||||
end
|
||||
result = result..table.concat(attrs,'')
|
||||
end
|
||||
result = result .. (n.kids and n.kids[1] and '>' or '/>')
|
||||
table.insert(out, result)
|
||||
if n.kids and n.kids[1] then
|
||||
for _,kid in ipairs(n.kids) do
|
||||
if ser[kid.type] then ser[kid.type](kid,depth+1) end
|
||||
end
|
||||
table.insert(out, indent..'</'..name..'>')
|
||||
end
|
||||
end
|
||||
|
||||
function ser.text(n,depth)
|
||||
if n.cdata then
|
||||
table.insert(out, tab:rep(depth)..'<![CDATA['..n.value..']]>')
|
||||
else
|
||||
table.insert(out, tab:rep(depth)..esc(n.value))
|
||||
end
|
||||
end
|
||||
|
||||
function ser.comment(n,depth)
|
||||
table.insert(out, tab:rep(depth)..'<!--'..n.value..'-->')
|
||||
end
|
||||
|
||||
ser[n.type](n,0)
|
||||
|
||||
return table.concat(out, opts.indent and '\n' or '')
|
||||
end
|
||||
|
||||
return SLAXML
|
||||
9
bios/Arcade/MAME/plugins/xml/plugin.json
Normal file
9
bios/Arcade/MAME/plugins/xml/plugin.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"plugin": {
|
||||
"name": "SLAXML",
|
||||
"description": "Lua SLAX XML parser",
|
||||
"version": "0.8",
|
||||
"author": "Gavin Kistner",
|
||||
"type": "library"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue