diff options
Diffstat (limited to 'plugins/inputmacro')
-rw-r--r-- | plugins/inputmacro/init.lua | 140 | ||||
-rw-r--r-- | plugins/inputmacro/inputmacro_menu.lua | 624 | ||||
-rw-r--r-- | plugins/inputmacro/inputmacro_persist.lua | 155 | ||||
-rw-r--r-- | plugins/inputmacro/plugin.json | 10 |
4 files changed, 929 insertions, 0 deletions
diff --git a/plugins/inputmacro/init.lua b/plugins/inputmacro/init.lua new file mode 100644 index 00000000000..9c520f3a503 --- /dev/null +++ b/plugins/inputmacro/init.lua @@ -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 diff --git a/plugins/inputmacro/inputmacro_menu.lua b/plugins/inputmacro/inputmacro_menu.lua new file mode 100644 index 00000000000..48fef405d0f --- /dev/null +++ b/plugins/inputmacro/inputmacro_menu.lua @@ -0,0 +1,624 @@ +-- 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 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, + 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_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 (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', '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_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 = { } + + 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 diff --git a/plugins/inputmacro/inputmacro_persist.lua b/plugins/inputmacro/inputmacro_persist.lua new file mode 100644 index 00000000000..b8670800fa4 --- /dev/null +++ b/plugins/inputmacro/inputmacro_persist.lua @@ -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 diff --git a/plugins/inputmacro/plugin.json b/plugins/inputmacro/plugin.json new file mode 100644 index 00000000000..2d209fca116 --- /dev/null +++ b/plugins/inputmacro/plugin.json @@ -0,0 +1,10 @@ +{ + "plugin": { + "name": "inputmacro", + "description": "Input macro plugin", + "version": "0.0.1", + "author": "Vas Crabb", + "type": "plugin", + "start": "false" + } +} |