diff options
-rw-r--r-- | plugins/autofire/autofire_menu.lua | 295 | ||||
-rw-r--r-- | plugins/autofire/autofire_save.lua | 86 | ||||
-rw-r--r-- | plugins/autofire/init.lua | 95 | ||||
-rw-r--r-- | plugins/autofire/plugin.json | 10 |
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" + } +} |