mirror of
https://github.com/holub/mame
synced 2025-06-10 23:02:38 +03:00

Changed the hiscore plugin to save inside the "hiscore" folder in the plugin data directory. The old hiscore path setting has already been removed from MAME for some time. This means the plugin was always saving in the "hi" folder in the working directory with no way to change it, which is problematic on Linux and macOS, particularly for distro packagers. There are lots of plugin and UI changes in this release, so we may as well get this out of the way now. Also made it possible to change the "timed save" option from the Plugin Options menu, and save that in the data folder, too. Documented the input macro plugin. The only undocumented plugins now are the cheat plugin, the cheat finder plugin, and the port name plugin.
662 lines
18 KiB
Lua
662 lines
18 KiB
Lua
-- Constants
|
|
|
|
local MENU_TYPES = { MACROS = 0, ADD = 1, EDIT = 2, INPUT = 3 }
|
|
|
|
|
|
-- Globals
|
|
|
|
local macros
|
|
local menu_stack
|
|
|
|
local macros_start_macro -- really for the macros menu, but has to be declared local before edit menu functions
|
|
|
|
|
|
-- 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,
|
|
earlycancel = true,
|
|
loop = -1,
|
|
steps = {
|
|
{
|
|
inputs = {
|
|
{
|
|
port = nil,
|
|
field = nil } },
|
|
delay = 0,
|
|
duration = 1 } } }
|
|
end
|
|
|
|
|
|
-- Input menu
|
|
|
|
local input_action
|
|
local input_start_field
|
|
local input_choices
|
|
local input_item_first_choice
|
|
local input_item_cancel
|
|
|
|
function handle_input(index, action)
|
|
if (action == 'cancel') or ((index == input_item_cancel) and (action == 'select')) then
|
|
table.remove(menu_stack)
|
|
input_action = nil
|
|
return true
|
|
elseif action == 'select' then
|
|
field = input_choices[index - input_item_first_choice + 1]
|
|
if field then
|
|
table.remove(menu_stack)
|
|
input_action(field)
|
|
input_action = nil
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function populate_input()
|
|
local items = { }
|
|
|
|
items[#items + 1] = { _p('plugin-inputmacro', 'Set Input'), '', 'off' }
|
|
items[#items + 1] = { '---', '', '' }
|
|
|
|
if not input_choices then
|
|
local ioport = manager.machine.ioport
|
|
|
|
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 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
|
|
|
|
input_choices = { }
|
|
for tag, port in pairs(manager.machine.ioport.ports) do
|
|
for name, field in pairs(port.fields) do
|
|
if supported(field) then
|
|
table.insert(input_choices, field)
|
|
end
|
|
end
|
|
end
|
|
table.sort(input_choices, compare)
|
|
|
|
local index = 1
|
|
local prev
|
|
while index <= #input_choices do
|
|
local current = input_choices[index]
|
|
if (not prev) or (prev.device.tag ~= current.device.tag) then
|
|
table.insert(input_choices, index, false)
|
|
index = index + 2
|
|
else
|
|
index = index + 1
|
|
end
|
|
prev = current
|
|
end
|
|
end
|
|
|
|
input_item_first_choice = #items + 1
|
|
local selection = input_item_first_choice
|
|
for index, field in ipairs(input_choices) do
|
|
if field then
|
|
items[#items + 1] = { _p('input-name', field.name), '', '' }
|
|
if input_start_field and (field.port.tag == input_start_field.port.tag) and (field.mask == input_start_field.mask) and (field.type == input_start_field.type) then
|
|
selection = #items
|
|
input_start_field = nil
|
|
end
|
|
else
|
|
local device = input_choices[index + 1].device
|
|
if device.owner then
|
|
items[#items + 1] = { string.format(_('plugin-inputmacro', '%s [root%s]'), device.name, device.tag), '', 'heading' }
|
|
else
|
|
items[#items + 1] = { string.format(_('plugin-inputmacro', '[root%s]'), device.tag), '', 'heading' }
|
|
end
|
|
end
|
|
end
|
|
input_start_field = nil
|
|
|
|
items[#items + 1] = { '---', '', '' }
|
|
items[#items + 1] = { _p('plugin-inputmacro', 'Cancel'), '', '' }
|
|
input_item_cancel = #items
|
|
|
|
return items, selection
|
|
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_exit
|
|
|
|
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 set_binding()
|
|
local input = manager.machine.input
|
|
local poller = input:switch_sequence_poller()
|
|
manager.machine:popmessage(_p('plugin-inputmacro', 'Enter activation sequence or wait to leave unchanged'))
|
|
manager.machine.video:frame_update()
|
|
poller:start()
|
|
local time = os.clock()
|
|
local clearmsg = true
|
|
while (not poller:poll()) and (poller.modified or (os.clock() < (time + 1))) do
|
|
if poller.modified then
|
|
if not poller.valid then
|
|
manager.machine:popmessage(_p('plugin-inputmacro', 'Invalid sequence entered'))
|
|
clearmsg = false
|
|
break
|
|
end
|
|
manager.machine:popmessage(input:seq_name(poller.sequence))
|
|
manager.machine.video:frame_update()
|
|
end
|
|
end
|
|
if clearmsg then
|
|
manager.machine:popmessage()
|
|
end
|
|
if poller.valid then
|
|
edit_current_macro.binding = poller.sequence
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function handle_edit_items(index, event)
|
|
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 == '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
|
|
return set_binding()
|
|
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
|
|
input_action =
|
|
function(field)
|
|
inputs[command.input].port = field.port
|
|
inputs[command.input].field = field
|
|
end
|
|
input_start_field = inputs[command.input].field
|
|
edit_start_selection = index
|
|
table.insert(menu_stack, MENU_TYPES.INPUT)
|
|
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
|
|
input_action =
|
|
function(field)
|
|
inputs[#inputs + 1] = {
|
|
port = field.port,
|
|
field = field }
|
|
end
|
|
edit_start_selection = index
|
|
table.insert(menu_stack, MENU_TYPES.INPUT)
|
|
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
|
|
input_action =
|
|
function(field)
|
|
local newstep = {
|
|
inputs = {
|
|
{
|
|
port = field.port,
|
|
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
|
|
edit_start_selection = index
|
|
table.insert(menu_stack, MENU_TYPES.INPUT)
|
|
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
|
|
|
|
return namecancel
|
|
end
|
|
|
|
local function add_edit_items(items)
|
|
edit_items = { }
|
|
local input = manager.machine.input
|
|
local arrows
|
|
|
|
items[#items + 1] = { _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]')
|
|
items[#items + 1] = { _p('plugin-inputmacro', 'Activation sequence'), activation, '' }
|
|
edit_items[#items] = { action = 'binding' }
|
|
|
|
local releaseaction = edit_current_macro.earlycancel and _p('plugin-inputmacro', 'Stop immediately') or _p('plugin-inputmacro', 'Complete macro')
|
|
items[#items + 1] = { _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
|
|
items[#items + 1] = { _p('plugin-inputmacro', 'When held'), holdaction, arrows }
|
|
edit_items[#items] = { action = 'holdaction' }
|
|
|
|
for i, step in ipairs(edit_current_macro.steps) do
|
|
items[#items + 1] = { string.format(_p('plugin-inputmacro', 'Step %d'), i), '', 'heading' }
|
|
items[#items + 1] = { _p('plugin-inputmacro', 'Delay (frames)'), 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
|
|
|
|
items[#items + 1] = { _p('plugin-inputmacro', 'Duration (frames)'), 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 = input.field and _p('input-name', input.field.name) or _p('plugin-inputmacro', '[not set]')
|
|
items[#items + 1] = { 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
|
|
items[#items + 1] = { _p('plugin-inputmacro', 'Add input'), '', '' }
|
|
edit_items[#items] = { action = 'addinput', step = i }
|
|
end
|
|
|
|
if #edit_current_macro.steps > 1 then
|
|
items[#items + 1] = { _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
|
|
items[#items + 1] = { '---', '', '' }
|
|
|
|
arrows = 'lr'
|
|
if edit_insert_position > #edit_current_macro.steps then
|
|
arrows = 'l'
|
|
elseif edit_insert_position < 2 then
|
|
arrows = 'r'
|
|
end
|
|
items[#items + 1] = { _p('plugin-inputmacro', 'Add step at position'), edit_insert_position, arrows }
|
|
edit_items[#items] = { action = 'addstep', step = i }
|
|
end
|
|
end
|
|
|
|
local function handle_add(index, event)
|
|
if handle_edit_items(index, event) then
|
|
return true
|
|
elseif event == 'cancel' then
|
|
input_choices = nil
|
|
edit_current_macro = nil
|
|
edit_menu_active = false
|
|
edit_items = nil
|
|
table.remove(menu_stack)
|
|
return true
|
|
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
|
|
input_choices = nil
|
|
edit_menu_active = false
|
|
edit_current_macro = nil
|
|
edit_items = nil
|
|
table.remove(menu_stack)
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function handle_edit(index, event)
|
|
if handle_edit_items(index, event) then
|
|
return true
|
|
elseif (event == 'cancel') or ((index == edit_item_exit) and (event == 'select')) then
|
|
input_choices = nil
|
|
edit_current_macro = nil
|
|
edit_menu_active = false
|
|
edit_items = nil
|
|
table.remove(menu_stack)
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function populate_add()
|
|
local items = { }
|
|
|
|
items[#items + 1] = { _p('plugin-inputmacro', 'Add Input Macro'), '', 'off' }
|
|
items[#items + 1] = { '---', '', '' }
|
|
|
|
add_edit_items(items)
|
|
|
|
items[#items + 1] = { '---', '', '' }
|
|
if current_macro_complete() then
|
|
items[#items + 1] = { _p('plugin-inputmacro', 'Create'), '', '' }
|
|
else
|
|
items[#items + 1] = { _p('plugin-inputmacro', 'Cancel'), '', '' }
|
|
end
|
|
edit_item_exit = #items
|
|
|
|
local selection = edit_start_selection
|
|
edit_start_selection = nil
|
|
return items, selection, 'lrrepeat'
|
|
end
|
|
|
|
local function populate_edit()
|
|
local items = { }
|
|
|
|
items[#items + 1] = { _p('plugin-inputmacro', 'Edit Input Macro'), '', 'off' }
|
|
items[#items + 1] = { '---', '', '' }
|
|
|
|
add_edit_items(items)
|
|
|
|
items[#items + 1] = { '---', '', '' }
|
|
items[#items + 1] = { _p('plugin-inputmacro', 'Done'), '', '' }
|
|
edit_item_exit = #items
|
|
|
|
local selection = edit_start_selection
|
|
edit_start_selection = nil
|
|
return items, selection, 'lrrepeat'
|
|
end
|
|
|
|
|
|
-- Macros menu
|
|
|
|
local macros_item_first_macro
|
|
local macros_selection_save
|
|
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
|
|
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 = { }
|
|
|
|
items[#items + 1] = { _p('plugin-inputmacro', 'Input Macros'), '', 'off' }
|
|
items[#items + 1] = { string.format(_p('plugin-inputmacro', 'Press %s to delete'), input:seq_name(ioport:type_seq(ioport:token_to_input_type('UI_CLEAR')))), '', 'off' }
|
|
items[#items + 1] = { '---', '', '' }
|
|
|
|
macros_item_first_macro = #items + 1
|
|
if #macros > 0 then
|
|
for index, macro in ipairs(macros) do
|
|
items[#items + 1] = { macro.name, input:seq_name(macro.binding), '' }
|
|
if macros_start_macro == index then
|
|
macros_selection_save = #items
|
|
end
|
|
end
|
|
else
|
|
items[#items + 1] = { _p('plugin-inputmacro', '[no macros]'), '', 'off' }
|
|
end
|
|
macros_start_macro = nil
|
|
|
|
items[#items + 1] = { '---', '', '' }
|
|
items[#items + 1] = { _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
|