summaryrefslogtreecommitdiffstatshomepage
path: root/plugins
diff options
context:
space:
mode:
author Jack Li <jackrjli@berkeley.edu>2019-05-14 07:26:27 -0700
committer ajrhacker <ajrhacker@users.noreply.github.com>2019-05-14 10:26:27 -0400
commit0040650300e4d60406e3ad27d7aee914195dd69e (patch)
tree4a91198dd84e2252a01211830e80d47fcb51f7be /plugins
parente24a0a19308457e5a23cd84a35fffea53701eea5 (diff)
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"
Diffstat (limited to 'plugins')
-rw-r--r--plugins/autofire/autofire_menu.lua295
-rw-r--r--plugins/autofire/autofire_save.lua86
-rw-r--r--plugins/autofire/init.lua95
-rw-r--r--plugins/autofire/plugin.json10
4 files changed, 486 insertions, 0 deletions
diff --git a/plugins/autofire/autofire_menu.lua b/plugins/autofire/autofire_menu.lua
new file mode 100644
index 00000000000..f5f46a96d1e
--- /dev/null
+++ b/plugins/autofire/autofire_menu.lua
@@ -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
diff --git a/plugins/autofire/autofire_save.lua b/plugins/autofire/autofire_save.lua
new file mode 100644
index 00000000000..e7d28029cd5
--- /dev/null
+++ b/plugins/autofire/autofire_save.lua
@@ -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
diff --git a/plugins/autofire/init.lua b/plugins/autofire/init.lua
new file mode 100644
index 00000000000..fd268b65ca3
--- /dev/null
+++ b/plugins/autofire/init.lua
@@ -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
diff --git a/plugins/autofire/plugin.json b/plugins/autofire/plugin.json
new file mode 100644
index 00000000000..ccff6111c51
--- /dev/null
+++ b/plugins/autofire/plugin.json
@@ -0,0 +1,10 @@
+{
+ "plugin": {
+ "name": "autofire",
+ "description": "Autofire plugin",
+ "version": "0.0.1",
+ "author": "Jack Li",
+ "type": "plugin",
+ "start": "false"
+ }
+}