mirror of
https://github.com/holub/mame
synced 2025-05-31 10:01:51 +03:00
Add plugin for autofire (#5050)
* Hardcoded autofire plugin * Changed register_frame to register_frame_done, removed pause check * Added support for multiple buttons loaded from a file * Implemented saving settings on quit * Fixed multiple keybindings for same button from overwriting each other * Replaced double-quotes with single-quotes * Refactored saving/loading into a separate module * Changed button format to use input_code rather than string token Settings format is unchanged (still saved as string token). * Rewrote table initialization in save/load * Implemented menus * Added helper messages for "on frames"/"off frames"
This commit is contained in:
parent
e24a0a1930
commit
0040650300
295
plugins/autofire/autofire_menu.lua
Normal file
295
plugins/autofire/autofire_menu.lua
Normal file
@ -0,0 +1,295 @@
|
||||
local lib = {}
|
||||
|
||||
-- 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 being created/edited
|
||||
local current_button = {}
|
||||
|
||||
-- Inputs that can be autofired (to list in BUTTON menu)
|
||||
local inputs = {}
|
||||
|
||||
-- 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.field and button.key and button.on_frames and button.off_frames and button.button and button.counter
|
||||
end
|
||||
|
||||
local function is_supported_input(ioport_field)
|
||||
-- IPT_BUTTON1 through IPT_BUTTON16 in ioport_type enum (ioport.h)
|
||||
return ioport_field.type >= 64 and ioport_field.type <= 79
|
||||
end
|
||||
|
||||
-- Main menu
|
||||
|
||||
local function populate_main_menu(buttons)
|
||||
local menu = {}
|
||||
menu[#menu + 1] = {_('Autofire buttons'), '', 'off'}
|
||||
menu[#menu + 1] = {'---', '', ''}
|
||||
header_height = #menu
|
||||
|
||||
for index, button in ipairs(buttons) do
|
||||
-- Assume refresh rate of 60 Hz; maybe better to use screen_device refresh()?
|
||||
local rate = 60 / (button.on_frames + button.off_frames)
|
||||
-- Round to two decimal places
|
||||
rate = math.floor(rate * 100) / 100
|
||||
local text = button.button.name .. ' [' .. rate .. ' Hz]'
|
||||
local subtext = manager:machine():input():code_name(button.key)
|
||||
menu[#menu + 1] = {text, subtext, ''}
|
||||
end
|
||||
content_height = #menu
|
||||
|
||||
menu[#menu + 1] = {'---', '', ''}
|
||||
menu[#menu + 1] = {_('Add autofire button'), '', ''}
|
||||
return menu
|
||||
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
|
||||
current_button = buttons[adjusted_index]
|
||||
table.insert(menu_stack, MENU_TYPES.EDIT)
|
||||
return true
|
||||
elseif event == 'clear' then
|
||||
table.remove(buttons, adjusted_index)
|
||||
return true
|
||||
end
|
||||
elseif section == MENU_SECTIONS.FOOTER then
|
||||
if event == 'select' then
|
||||
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 = current_button.button and current_button.button.name or _('NOT SET')
|
||||
local key_name = current_button.key and manager:machine():input():code_name(current_button.key) or _('NOT SET')
|
||||
menu[#menu + 1] = {_('Input'), button_name, ''}
|
||||
menu[#menu + 1] = {_('Hotkey'), key_name, ''}
|
||||
menu[#menu + 1] = {_('On frames'), current_button.on_frames, current_button.on_frames > 1 and 'lr' or 'r'}
|
||||
menu[#menu + 1] = {_('Off frames'), current_button.off_frames, current_button.off_frames > 1 and 'lr' or 'r'}
|
||||
end
|
||||
|
||||
-- Borrowed from the cheat plugin
|
||||
local function poll_for_hotkey()
|
||||
local input = manager:machine():input()
|
||||
manager:machine():popmessage(_('Press button for hotkey or wait to leave unchanged'))
|
||||
manager:machine():video():frame_update(true)
|
||||
input:seq_poll_start('switch')
|
||||
local time = os.clock()
|
||||
while (not input:seq_poll()) and (os.clock() < time + 1) do end
|
||||
local tokens = input:seq_to_tokens(input:seq_poll_final())
|
||||
manager:machine():popmessage()
|
||||
manager:machine():video():frame_update(true)
|
||||
|
||||
local final_token = nil
|
||||
for token in tokens:gmatch('%S+') do
|
||||
final_token = token
|
||||
end
|
||||
return final_token and input:code_from_token(final_token) or nil
|
||||
end
|
||||
|
||||
local function handle_configure_menu(index, event)
|
||||
-- Input
|
||||
if index == 1 then
|
||||
if event == 'select' then
|
||||
table.insert(menu_stack, MENU_TYPES.BUTTON)
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
-- Hotkey
|
||||
elseif index == 2 then
|
||||
if event == 'select' then
|
||||
local keycode = poll_for_hotkey()
|
||||
if keycode then
|
||||
current_button.key = keycode
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
else
|
||||
return false
|
||||
end
|
||||
-- On frames
|
||||
elseif index == 3 then
|
||||
manager:machine():popmessage(_('Number of frames button will be pressed'))
|
||||
if event == 'left' then
|
||||
current_button.on_frames = current_button.on_frames - 1
|
||||
elseif event == 'right' then
|
||||
current_button.on_frames = current_button.on_frames + 1
|
||||
end
|
||||
-- Off frames
|
||||
elseif index == 4 then
|
||||
manager:machine():popmessage(_('Number of frames button will be released'))
|
||||
if event == 'left' then
|
||||
current_button.off_frames = current_button.off_frames - 1
|
||||
elseif event == 'right' then
|
||||
current_button.off_frames = current_button.off_frames + 1
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function populate_edit_menu()
|
||||
local menu = {}
|
||||
menu[#menu + 1] = {_('Edit autofire button'), '', 'off'}
|
||||
menu[#menu + 1] = {'---', '', ''}
|
||||
header_height = #menu
|
||||
|
||||
populate_configure_menu(menu)
|
||||
content_height = #menu
|
||||
|
||||
menu[#menu + 1] = {'---', '', ''}
|
||||
menu[#menu + 1] = {_('Done'), '', ''}
|
||||
return menu
|
||||
end
|
||||
|
||||
local function handle_edit_menu(index, event, buttons)
|
||||
local section, adjusted_index = menu_section(index)
|
||||
if section == MENU_SECTIONS.CONTENT then
|
||||
return handle_configure_menu(adjusted_index, event)
|
||||
elseif section == MENU_SECTIONS.FOOTER then
|
||||
if event == 'select' then
|
||||
table.remove(menu_stack)
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function populate_add_menu()
|
||||
local menu = {}
|
||||
menu[#menu + 1] = {_('Add autofire button'), '', 'off'}
|
||||
menu[#menu + 1] = {'---', '', ''}
|
||||
header_height = #menu
|
||||
|
||||
populate_configure_menu(menu)
|
||||
content_height = #menu
|
||||
|
||||
menu[#menu + 1] = {'---', '', ''}
|
||||
if is_button_complete(current_button) then
|
||||
menu[#menu + 1] = {_('Create'), '', ''}
|
||||
else
|
||||
menu[#menu + 1] = {_('Cancel'), '', ''}
|
||||
end
|
||||
return menu
|
||||
end
|
||||
|
||||
local function handle_add_menu(index, event, buttons)
|
||||
local section, adjusted_index = menu_section(index)
|
||||
if section == MENU_SECTIONS.CONTENT then
|
||||
return handle_configure_menu(adjusted_index, event)
|
||||
elseif section == MENU_SECTIONS.FOOTER then
|
||||
if event == 'select' then
|
||||
table.remove(menu_stack)
|
||||
if is_button_complete(current_button) then
|
||||
buttons[#buttons + 1] = current_button
|
||||
end
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Button selection menu
|
||||
|
||||
local function populate_button_menu()
|
||||
menu = {}
|
||||
inputs = {}
|
||||
menu[#menu + 1] = {_('Select an input for autofire'), '', 'off'}
|
||||
menu[#menu + 1] = {'---', '', ''}
|
||||
header_height = #menu
|
||||
|
||||
for port_key, port in pairs(manager:machine():ioport().ports) do
|
||||
for field_key, field in pairs(port.fields) do
|
||||
if is_supported_input(field) then
|
||||
menu[#menu + 1] = {field.name, '', ''}
|
||||
inputs[#inputs + 1] = {
|
||||
port_name = port_key,
|
||||
field_name = field_key,
|
||||
ioport_field = field
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
content_height = #menu
|
||||
return menu
|
||||
end
|
||||
|
||||
local function handle_button_menu(index, event)
|
||||
local section, adjusted_index = menu_section(index)
|
||||
if section == MENU_SECTIONS.CONTENT and event == 'select' then
|
||||
local selected_input = inputs[adjusted_index]
|
||||
current_button.port = selected_input.port_name
|
||||
current_button.field = selected_input.field_name
|
||||
current_button.button = selected_input.ioport_field
|
||||
table.remove(menu_stack)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
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
|
86
plugins/autofire/autofire_save.lua
Normal file
86
plugins/autofire/autofire_save.lua
Normal file
@ -0,0 +1,86 @@
|
||||
local lib = {}
|
||||
|
||||
local function get_settings_path()
|
||||
return lfs.env_replace(manager:machine():options().entries.pluginspath:value():match('([^;]+)')) .. '/autofire/cfg/'
|
||||
end
|
||||
|
||||
local function get_settings_filename()
|
||||
return emu.romname() .. '.cfg'
|
||||
end
|
||||
|
||||
local function initialize_button(settings)
|
||||
if settings.port and settings.field and settings.key and settings.on_frames and settings.off_frames then
|
||||
local new_button = {
|
||||
port = settings.port,
|
||||
field = settings.field,
|
||||
key = manager:machine():input():code_from_token(settings.key),
|
||||
on_frames = settings.on_frames,
|
||||
off_frames = settings.off_frames,
|
||||
counter = 0
|
||||
}
|
||||
local port = manager:machine():ioport().ports[settings.port]
|
||||
if port then
|
||||
local field = port.fields[settings.field]
|
||||
if field then
|
||||
new_button.button = field
|
||||
return new_button
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function serialize_settings(button_list)
|
||||
local settings = {}
|
||||
for index, button in ipairs(button_list) do
|
||||
setting = {
|
||||
port = button.port,
|
||||
field = button.field,
|
||||
key = manager:machine():input():code_to_token(button.key),
|
||||
on_frames = button.on_frames,
|
||||
off_frames = button.off_frames
|
||||
}
|
||||
settings[#settings + 1] = setting
|
||||
end
|
||||
return settings
|
||||
end
|
||||
|
||||
function lib:load_settings()
|
||||
local buttons = {}
|
||||
local json = require('json')
|
||||
local file = io.open(get_settings_path() .. get_settings_filename(), 'r')
|
||||
if not file then
|
||||
return buttons
|
||||
end
|
||||
local loaded_settings = json.parse(file:read('a'))
|
||||
file:close()
|
||||
if not loaded_settings then
|
||||
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 not attr then
|
||||
lfs.mkdir(path)
|
||||
elseif attr.mode ~= 'directory' then
|
||||
return
|
||||
end
|
||||
local json = require('json')
|
||||
local settings = serialize_settings(buttons)
|
||||
local file = io.open(path .. get_settings_filename(), 'w')
|
||||
if file then
|
||||
file:write(json.stringify(settings, {indent = true}))
|
||||
file:close()
|
||||
end
|
||||
end
|
||||
|
||||
return lib
|
95
plugins/autofire/init.lua
Normal file
95
plugins/autofire/init.lua
Normal file
@ -0,0 +1,95 @@
|
||||
-- license:BSD-3-Clause
|
||||
-- copyright-holders:Jack Li
|
||||
local exports = {}
|
||||
exports.name = 'autofire'
|
||||
exports.version = '0.0.1'
|
||||
exports.description = 'Autofire plugin'
|
||||
exports.license = 'The BSD 3-Clause License'
|
||||
exports.author = { name = 'Jack Li' }
|
||||
|
||||
local autofire = exports
|
||||
|
||||
function autofire.startplugin()
|
||||
|
||||
-- List of autofire buttons, each being a table with keys:
|
||||
-- 'port' - port name of the button being autofired
|
||||
-- 'field' - field name of the button being autofired
|
||||
-- 'key' - input_code of 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 function process_button(button)
|
||||
local pressed = manager:machine():input():code_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
|
||||
|
||||
local function button_states_key(button)
|
||||
return button.port .. '\0' .. button.field
|
||||
end
|
||||
|
||||
local function process_frame()
|
||||
-- Resolves conflicts between multiple autofire keybindings for the same button.
|
||||
local button_states = {}
|
||||
|
||||
for i, button in ipairs(buttons) do
|
||||
local state = button_states[button_states_key(button)]
|
||||
if not state then
|
||||
state = 0
|
||||
end
|
||||
state = process_button(button) | state
|
||||
button_states[button_states_key(button)] = state
|
||||
end
|
||||
for i, button in ipairs(buttons) do
|
||||
button.button:set_value(button_states[button_states_key(button)])
|
||||
end
|
||||
end
|
||||
|
||||
local function load_settings()
|
||||
local loader = require('autofire/autofire_save')
|
||||
if loader then
|
||||
buttons = loader:load_settings()
|
||||
end
|
||||
end
|
||||
|
||||
local function save_settings()
|
||||
local saver = require('autofire/autofire_save')
|
||||
if saver then
|
||||
saver:save_settings(buttons)
|
||||
end
|
||||
end
|
||||
|
||||
local function menu_callback(index, event)
|
||||
local menu_handler = require('autofire/autofire_menu')
|
||||
if menu_handler then
|
||||
return menu_handler:handle_menu_event(index, event, buttons)
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
local function menu_populate()
|
||||
local menu_handler = require('autofire/autofire_menu')
|
||||
if menu_handler then
|
||||
return menu_handler:populate_menu(buttons)
|
||||
else
|
||||
return {{_('Failed to load autofire menu'), '', ''}}
|
||||
end
|
||||
end
|
||||
|
||||
emu.register_frame_done(process_frame)
|
||||
emu.register_start(load_settings)
|
||||
emu.register_stop(save_settings)
|
||||
emu.register_menu(menu_callback, menu_populate, _('Autofire'))
|
||||
end
|
||||
|
||||
return exports
|
10
plugins/autofire/plugin.json
Normal file
10
plugins/autofire/plugin.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"plugin": {
|
||||
"name": "autofire",
|
||||
"description": "Autofire plugin",
|
||||
"version": "0.0.1",
|
||||
"author": "Jack Li",
|
||||
"type": "plugin",
|
||||
"start": "false"
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user