Skip to main content

Scripted Menu

Overview of the Scripted Menu

A scripted menu system was made available for the first time with game updates released for Armored Core Master of Arena and Armored Core Project Phantasma in July 2025.

It enables the creation of an additional option directly on the main emulator menu, with a number of sub-options available for the user to select. In the case of the Armored Core games, this allowed the user to toggle features within the game that a user may - or may not - want to have enabled.

The Scripted Menu implementation in Armored Core Project Phantasma v1.03

This was not the first game to include custom menu options. The Resistance Retribution PSPHD release (of which a common UI is shared between all of Sony's emulators) re-purposed the Change Disc menu to create a menu to allow you to switch to the "Infected Mode" which the game included.

At a surface level this would also appear to be a fairly similar implementation - possibly even using the same trick of repurposing existing menu infrastructure.

However, once you dig a bit deeper it becomes apparent that unlike the approach seen with Resistance Retribution, this appears to be a fully fledged dynamic menu system which the developers of future releases could now utilise to add per-game user selectable functionality as desired. From a homebrew perspective, this could be used to build cheat or patch selection menus directly into the emulator interface, instead of having to rely on button combination presses to trigger them, or as a means of easily triggering any other LUA based customisations you may want to make to the game.

How the Menu Works

Based on the implementation in Armored Core Project Phantasma v1.03

The scripted menu is created directly from LUA using two new C functions which have been added:-
uiAddScriptedMenuLabel("name")
uiAddScriptedMenuItem("name", {get=function(), set=function(bool)})

In the case of Armored Core, these are called from:-
/app0/scripts/features.lua

The menu is first created by calling uiAddScriptedMenuLabel with the name you want the menu option to have. This can be a string defined directly in code, or it can pull the string in from the localisation files (eg "i18nHumanPlus" in /app0/assets/common/localization/en-US.json).

note

Each time that you call uiAddScriptedMenuLabel it seems to reinstantiate the menu

/app0/scripts/features.lua
if uiAddScriptedMenuLabel ~= nil then
uiAddScriptedMenuLabel("i18nHumanPlus")
--if titleIsSIEA then
--uiAddScriptedMenuLabel("Human Plus")
--elseif titleIsSIEJ then
--uiAddScriptedMenuLabel("強化にん間") -- 強化人間
--end
end

Menu items are then added by calling uiAddScriptedMenuItem. This function takes a string for the name of the item (which, again, can be a direct string or a reference to a string in the localisation files), and a table containing a get and set function.

/app0/scripts/features.lua
if uiAddScriptedMenuItem ~= nil then
print("====\nAdding Human Plus menu\n====\n")
uiAddScriptedMenuItem("0", {
get = function()
local human = R3K_ReadMem8(HUMAN_PLUS_ADDRESS)
if human == 0 then
return true
end
return false
end,
set = function(value)
R3K_WriteMem8(HUMAN_PLUS_ADDRESS, 0)
R3K_WriteMem8(HUMAN_PLUS_STORAGE, 0)
end,
})
uiAddScriptedMenuItem("1", {
get = function()
local human = R3K_ReadMem8(HUMAN_PLUS_ADDRESS)
if human >= 1 and human <= 3 then
return true
end
return false
end,
set = function(value)
R3K_WriteMem8(HUMAN_PLUS_ADDRESS, 1)
R3K_WriteMem8(HUMAN_PLUS_STORAGE, 1)
end,
})
-- And so on
end

The get function appears to be called repeatedly whenever the emulator menu is open and the scripted menu displayed, and seems to determine whether or not the option has a check mark next to it (check mark shown if the function returns true, unchecked if the function returns false).

The set function appears to be called every time the user selects the option. set receives a bool parameter (value in the above example) that appears to indicate the current state of the option at the point of the user selecting it (is it currently checked or unchecked?). This is true if the option is currently unchecked and false if the option is checked.

How scope works needs further investigation, but from the above example they are referring to variables within the get/set functions which are declared outside of their scope in the wider features.lua file. It may be a case that it depends on how and where the emulator is loading your script file from as to whether or not it's variables remain in scope during normal runtime.

From these basic fundamentals, you can see that it is now very easy to start building out a custom menu which allows the user to execute any LUA functionality you might want. You could, for example, have individual options for everything you want to be available - or you could combine them into single options which toggle the state.

An example of a custom implementation of the Scripted Menu

Pushing the Limits with Nested Menus

As the menu and the options are defined at runtime, and since it's possible to reinstatiate the entire menu by simply calling uiAddScriptedMenuLabel, it's possible to build out a system which allows you to navigate through multiple sub-menus - really as many as you want to manage.

An example of Nested Menus with the Scripted Menu

There may be proper built in functionality which allows you to do this, but in leui of that knowledge this method could be used.

The idea behind this involves moving each menus creation logic into separate functions. These functions can then be called as required (via require) by the menu navigation buttons to rebuild the available menu options, without having to worry about things going out of scope and without having to duplicate excessive amounts of the menus definition.

note

As mentioned above, how scope works needs further investigation so for the purposes of this example we err on the side of caution and just load everything back in when wanting to load a new page of the menu. If this isn't required, then it simplifies menu creation even further.

The menu code is created in a location that LUA is configured to look for files when calling require, for example:
/app0/global/scripts/lua_include/scripted_menu.lua

note

The search path is defined in package.path. As an alternative to placing it in one of the preconfigured search locations, you could temporarily override this before calling require, and then set it back.

-- If your menu script were stored at:
-- /data/ps1hd/scripted_menu.lua
local original_path = package.path
-- Override the search path
package.path = "/data/ps1hd/?.lua"
if pcall(require, "scripted_menu") then
if scripted_menu_toplevel ~= nil then
scripted_menu_toplevel()
end
end
-- Restore the search path
package.path = original_path

The definition for each menu is placed into seperate functions within this file:

scripted_menu_toplevel = function()
uiAddScriptedMenuLabel("Additional Options")
uiAddScriptedMenuItem("Unselected Item", {
get = function()
-- Logic to determine if option has a check mark
return false -- No check mark
end,
set = function(value)
-- Logic to execute when user selects option
-- value is true when the option currently has no check mark
end,
})
uiAddScriptedMenuItem("Selected Item", {
get = function()
-- Logic to determine if option has a check mark
return true -- Check mark
end,
set = function(value)
-- Logic to execute when user selects option
-- value is true when the option currently has no check mark
end,
})
-- And so on
--
end

When it comes to navigating to a new menu, then the set function could be configured like this:

uiAddScriptedMenuItem("--> Emulation Options", {
get = function()
return false
end,
set = function(value)
-- Attempt to load scripted_menu.lua
if pcall(require, "scripted_menu") then
if scripted_menu_emu ~= nil then
-- Call the function to the menu that should
-- be loaded
scripted_menu_emu()
end
end
end,
})

The menu interface will immediately be redrawn based on the definition defined within scripted_menu_emu().

To trigger the initial creation of the menu, then a second script is created in a location that automatically executes when the emulator loads, for example:
/app0/scripts/create_initial_menu.lua

Similar to the above, all this script would need to contain is a call to the function to create whatever you want your first level of menu to be.

if pcall(require, "scripted_menu") then
if scripted_menu_toplevel ~= nil then
scripted_menu_toplevel()
end
end

Full Example

This example is the code that builds the menu shown in the following clip:
An example of Nested Menus with the Scripted Menu

/app0/scripts/create_initial_menu.lua
-- Load the top level menu on emulator start-up
if pcall(require, "scripted_menu") then
if scripted_menu_toplevel ~= nil then
scripted_menu_toplevel()
end
end
/app0/global/scripts/lua_include/scripted_menu.lua
-- Definitions for the various menus
scripted_menu_toplevel = function()
-- The Top Level menu
-- This is the menu which should be loaded on start-up
uiAddScriptedMenuLabel("Additional Options")
uiAddScriptedMenuItem("Unselected Item", {
get = function()
return false
end,
set = function(value)
end,
})
uiAddScriptedMenuItem("Selected Item", {
get = function()
return true
end,
set = function(value)
end,
})
uiAddScriptedMenuItem("--> Emulation Options", {
get = function()
return false
end,
set = function(value)
if pcall(require, "scripted_menu") then
if scripted_menu_emu ~= nil then
scripted_menu_emu()
end
end
end,
})
uiAddScriptedMenuItem("--> Cheat Options", {
get = function()
return false
end,
set = function(value)
if pcall(require, "scripted_menu") then
if scripted_menu_cheats ~= nil then
scripted_menu_cheats()
end
end
end,
})
end

scripted_menu_cheats = function()
-- The Cheat Options menu
uiAddScriptedMenuLabel("Additional Options")
uiAddScriptedMenuItem("Max Lives", {
get = function()
return false
end,
set = function(value)
end,
})
uiAddScriptedMenuItem("Unlock All Levels", {
get = function()
return false
end,
set = function(value)
end,
})
uiAddScriptedMenuItem("Unlock All Characters", {
get = function()
return false
end,
set = function(value)
end,
})
uiAddScriptedMenuItem("<-- Return to Top Menu", {
get = function()
return false
end,
set = function(value)
if pcall(require, "scripted_menu") then
if scripted_menu_toplevel ~= nil then
scripted_menu_toplevel()
end
end
end,
})
end

scripted_menu_emu = function()
-- The Emulation Options menu
uiAddScriptedMenuLabel("Additional Options")
uiAddScriptedMenuItem("Graphics Settings On", {
get = function()
if EM_GetSettingCli("--userui-settings-graphics") == "1" then
return true
end
return false
end,
set = function(value)
EM_SetSettingCli("--userui-settings-graphics=1")
end,
})
uiAddScriptedMenuItem("Graphics Settings Off", {
get = function()
if EM_GetSettingCli("--userui-settings-graphics") == "0" then
return true
end
return false
end,
set = function(value)
EM_SetSettingCli("--userui-settings-graphics=0")
end,
})
uiAddScriptedMenuItem("Show Graphics Settings", {
get = function()
if EM_GetSettingCli("--userui-settings-graphics") == "1" then
return true
end
return false
end,
set = function(value)
if value then -- Unchecked
EM_SetSettingCli("--userui-settings-graphics=1")
else
EM_SetSettingCli("--userui-settings-graphics=0")
end
end,
})
uiAddScriptedMenuItem("Analog On", {
get = function()
if EM_GetSettingCli("--legacy-force-analog-value") == "true" or nil then
return true
end
return false
end,
set = function(value)
if value then -- Unchecked
EM_SetSettingCli("--legacy-force-analog-value=true")
EM_SetSettingCli("--legacy-force-analog=true")
EM_SetSettingCli("--remap-lstick-to-digital=false")
EM_SaveState(0)
end
end,
})
uiAddScriptedMenuItem("Analog Off", {
get = function()
if EM_GetSettingCli("--legacy-force-analog-value") == "false" then
return true
end
return false
end,
set = function(value)
if value then -- Unchecked
EM_SetSettingCli("--legacy-force-analog-value=false")
EM_SetSettingCli("--legacy-force-analog=true")
EM_SetSettingCli("--remap-lstick-to-digital=true")
EM_SaveState(0)
end
end,
})
uiAddScriptedMenuItem("Analog Mode", {
get = function()
if EM_GetSettingCli("--legacy-force-analog-value") == "true" or nil then
return true
end
return false
end,
set = function(value)
if value then -- Unchecked
-- Turn on Analog
EM_SetSettingCli("--legacy-force-analog-value=true")
EM_SetSettingCli("--legacy-force-analog=true")
EM_SetSettingCli("--remap-lstick-to-digital=false")
EM_SaveState(0)
else
-- Turn off Analog
EM_SetSettingCli("--legacy-force-analog-value=false")
EM_SetSettingCli("--legacy-force-analog=true")
EM_SetSettingCli("--remap-lstick-to-digital=true")
EM_SaveState(0)
end
end,
})
uiAddScriptedMenuItem("Update Analog State", {
get = function()
return false
end,
set = function(value)
-- Workaround for changing analog mode
-- The above CLI commands only appear to apply at emulator start-up
-- By creating and loading a save state this seems to trigger these
-- values being re-evaluated.
-- Calling EM_SaveState and EM_LoadState back to back doesn't seem to
-- work well (ie. it tries to load before the state is saved), so putting
-- the load in a seperate option gives it time to create the state
-- Note: these are temporary states outside of the UI save states functionality
EM_LoadState(0)
end,
})
uiAddScriptedMenuItem("--> More Options", {
get = function()
return false
end,
set = function(value)
if pcall(require, "scripted_menu") then
if scripted_menu_emu_more ~= nil then
scripted_menu_emu_more()
end
end
end,
})
uiAddScriptedMenuItem("<-- Return to Top Menu", {
get = function()
return false
end,
set = function(value)
if pcall(require, "scripted_menu") then
if scripted_menu_toplevel ~= nil then
scripted_menu_toplevel()
end
end
end,
})
end

scripted_menu_emu_more = function()
-- The Emulator Options -> More Options menu
uiAddScriptedMenuLabel("Additional Options")
uiAddScriptedMenuItem("More Options", {
get = function()
return false
end,
set = function(value)
end,
})
uiAddScriptedMenuItem("Oh No! More Options", {
get = function()
return true
end,
set = function(value)
end,
})
uiAddScriptedMenuItem("<-- Return to Emulation Options", {
get = function()
return false
end,
set = function(value)
if pcall(require, "scripted_menu") then
if scripted_menu_emu ~= nil then
scripted_menu_emu()
end
end
end,
})
uiAddScriptedMenuItem("<-- Return to Top Menu", {
get = function()
return false
end,
set = function(value)
if pcall(require, "scripted_menu") then
if scripted_menu_toplevel ~= nil then
scripted_menu_toplevel()
end
end
end,
})
end