Show Ultimate Menu information in companion panel

This commit is contained in:
Debugger
2026-09-14 19:06:36 -04:00
parent a7120f68e7
commit 624c607185
7 changed files with 285 additions and 54 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Ultimate Menu for Nenyoo (GTA V Legacy)
This edition runs the YimMenu Legacy script through a Nenyoo API adapter. The bundled `lib/Ultimate_Menu_Legacy.lua` is an unchanged copy of `YimMenu/Ultimate_Menu_Legacy.lua`; the original menu hierarchy and action callbacks remain in one source file. YimMenu tabs, ImGui groups, controls, and popups appear as nested Nenyoo menu lists. Controls that depend on game state are added when their conditions become true.
This edition runs the YimMenu Legacy script through a Nenyoo API adapter. The bundled `lib/Ultimate_Menu_Legacy.lua` is an unchanged copy of `YimMenu/Ultimate_Menu_Legacy.lua`; the original menu hierarchy and action callbacks remain in one source file. YimMenu tabs, controls, and popups appear as nested Nenyoo menu lists. Informational text and live status appear in a companion panel only while their submenu is open; hover the panel and use the mouse wheel for longer instructions. Controls that depend on game state are added when their conditions become true.
Copy the contents of this folder to `%LOCALAPPDATA%\Nenyoo\Plus\Scripts\User\UltimateMenu\`, then select `Ultimate_Menu.lua` from Nenyoo's Scripts menu to load its page. Keep the `lib` folder beside the entry script. The script targets GTA V Legacy build 3889 and uses the online menus only after joining GTA Online.
+2 -2
View File
@@ -4,8 +4,8 @@ Use GTA V **Legacy build 3889** and the Nenyoo DLL built with the `__ny_execute_
## First run
1. In Story Mode, open the page. Confirm `Ultimate Menu 1.73 By L7NEG` appears with `Story Mode`, `Self Menu`, `Recovery Menu`, `Online Services Menu`, `Collectibles`, `Events Menu`, `Gun Van Menu`, `YimResupplier`, and `Heists Data Editor Menu`. Open and back out of several nested lists. Record any missing row, empty page, freeze, or Lua error.
2. In `Story Mode`, record the current character's cash, use `Current Character Cash` to set a distinct value, and confirm the game shows it. Restore the original value. Online-only pages should show a single, non-selectable `Unavailable in Single Player.` notice rather than actionable controls.
1. In Story Mode, open the page. Confirm `Ultimate Menu 1.73 By L7NEG` appears with `Story Mode`, `Self Menu`, `Recovery Menu`, `Online Services Menu`, `Collectibles`, `Events Menu`, `Gun Van Menu`, `YimResupplier`, and `Heists Data Editor Menu`. Open and back out of several nested lists. Pages with explanations should show a companion information panel; the panel should close when leaving the page. Hover and scroll the panel to read long text. Record any missing row, empty page, freeze, or Lua error.
2. In `Story Mode`, record the current character's cash, use `Current Character Cash` to set a distinct value, and confirm the game shows it. Restore the original value. Online-only pages should show `Unavailable in Single Player.` in the information panel rather than actionable controls.
3. Join a GTA Online session and reopen the menu. Verify changing text, toggle, slider, combo, and button controls update the displayed value or game state. Leave the session and confirm Story Mode controls still work.
4. In `Gun Van Menu > Weapons > Edit Weapons`, search for `Pistol`, select its row, and use `Set Weapon` on a recorded slot. Check that the displayed slot and the in-game Gun Van show Pistol. Restore the original weapon. Test `Remove Weapon` and `Close`, then reopen the editor.
5. For a business you own, open `YimResupplier` and verify its controls appear after ownership is detected. Test a teleport to a known property and confirm the landing position. Repeat after changing sessions.
+1
View File
@@ -36,5 +36,6 @@ end
local compatibility = run(directory .. "lib\\yim_compat.lua")
run(directory .. "lib\\Ultimate_Menu_Legacy.lua")
run(directory .. "lib\\info_panel.lua")(compatibility)
compatibility.materialize_imgui()
return compatibility
+138
View File
@@ -0,0 +1,138 @@
-- Companion information panel for the Nenyoo edition of Ultimate Menu.
-- The menu adapter publishes text on the script thread. Draw only the
-- published strings here; overlay.on_draw runs on Nenyoo's render thread.
return function(compat)
local scroll_by_page = {}
local last_page
local function clamp(value, low, high)
return math.max(low, math.min(high, value))
end
local function wrap_paragraph(value, max_width, output, heading, color)
local line = ""
for word in value:gmatch("%S+") do
local candidate = line == "" and word or line .. " " .. word
if line ~= "" and text.width(font.small, candidate) > max_width then
output[#output + 1] = {text = line, heading = heading, color = color}
line = word
else
line = candidate
end
end
if line ~= "" then output[#output + 1] = {text = line, heading = heading, color = color} end
end
local function make_lines(rows, max_width)
local lines = {}
for _, row in ipairs(rows) do
local value = tostring(row.text or ""):gsub("\r", "")
if value == "" then
lines[#lines + 1] = {gap = 8}
else
local compact = value:gsub("%s+", " "):match("^%s*(.-)%s*$")
local heading = #compact <= 38 and (compact:sub(-1) == ":" or compact == "Preps")
for paragraph in (value .. "\n"):gmatch("(.-)\n") do
if paragraph:match("%S") then wrap_paragraph(paragraph, max_width, lines, heading, row.color)
else lines[#lines + 1] = {gap = 8} end
end
lines[#lines + 1] = {gap = heading and 7 or 11}
end
if row.tooltip then
wrap_paragraph(row.tooltip:gsub("%s+", " "), max_width, lines, false, nil)
lines[#lines + 1] = {gap = 11}
end
end
while #lines > 0 and lines[#lines].gap do lines[#lines] = nil end
return lines
end
overlay.on_draw("ultimate_menu_information", function()
if not menu.is_visible() then return end
local id = menu.page_id()
local info = compat.info_for_page(id)
if not info or #info.rows == 0 then return end
local sw, sh = ctx.screen_w(), ctx.screen_h()
local bx, by, bw = menu.bounds()
if not bw or bw <= 0 then return end
local gap, pad, header_h = 14, 14, 36
local width = math.min(420, math.max(280, sw * 0.30))
local side = bx + bw + gap + width <= sw - 8 or bx - gap - width >= 8
if not side then width = math.min(600, sw - 16) end
local x
if side and bx + bw + gap + width <= sw - 8 then x = bx + bw + gap
elseif side then x = bx - gap - width
else x = clamp(bx + (bw - width) * 0.5, 8, sw - width - 8) end
local lines = make_lines(info.rows, width - pad * 2)
if #lines == 0 then return end
local line_h = text.height(font.small) + 4
local content_h = 0
for _, line in ipairs(lines) do content_h = content_h + (line.gap or line_h) end
local desired_h = header_h + pad * 2 + content_h
local height = math.min(desired_h, sh - 16, 650)
local y = clamp(by, 8, sh - height - 8)
if not side then
local _, content_y, _, content_height = menu.content_rect()
local below = content_y + content_height + 56
local space_below = sh - below - 8
if content_height > 0 and space_below >= 170 then
y = below
height = math.min(height, space_below)
elseif by >= 180 then
height = math.min(height, by - 16)
y = by - height - 8
end
end
local viewport_h = height - header_h - pad * 2
if viewport_h <= line_h then return end
if last_page ~= id then scroll_by_page[id] = 0; last_page = id end
local max_scroll = math.max(0, content_h - viewport_h)
local scroll = clamp(scroll_by_page[id] or 0, 0, max_scroll)
local mx, my = input.mouse_x(), input.mouse_y()
if mx >= x and mx <= x + width and my >= y and my <= y + height then
scroll = clamp(scroll - input.mouse_wheel() * line_h * 3, 0, max_scroll)
end
scroll_by_page[id] = scroll
local ar, ag, ab = theme.accent()
draw.rect(x, y, x + width, y + height, 12, 15, 22, 232, 7)
draw.rect(x, y, x + width, y + header_h, 24, 32, 44, 245, 7)
draw.rect(x, y + header_h - 2, x + width, y + header_h, ar, ag, ab, 255)
draw.rect_outline(x, y, x + width, y + height, ar, ag, ab, 125, 7)
text.draw_ellipsis(font.item, x + pad, y + (header_h - text.height(font.item)) * 0.5,
240, 245, 250, 255, info.title, width - pad * 2)
local top = y + header_h + pad
draw.push_clip(x + pad, top, x + width - pad, top + viewport_h)
local cursor = top - scroll
for _, line in ipairs(lines) do
if line.gap then cursor = cursor + line.gap
else
if cursor + line_h >= top and cursor <= top + viewport_h then
local r, g, b = 220, 228, 238
if line.heading then r, g, b = ar, ag, ab end
if line.color then
local function channel(value)
value = value or 1
return clamp(math.floor((value <= 1 and value * 255 or value) + 0.5), 0, 255)
end
r, g, b = channel(line.color[1]), channel(line.color[2]), channel(line.color[3])
end
text.draw(font.small, x + pad, cursor, r, g, b, 245, line.text)
end
cursor = cursor + line_h
end
end
draw.pop_clip()
if max_scroll > 0 then
local track_top, track_height = top, viewport_h
local thumb_h = math.max(22, track_height * viewport_h / content_h)
local thumb_y = track_top + (track_height - thumb_h) * scroll / max_scroll
draw.rect(x + width - 5, thumb_y, x + width - 2, thumb_y + thumb_h,
ar, ag, ab, 190, 2)
end
end)
end
+72 -16
View File
@@ -5,7 +5,7 @@ local threads_created = 0
local thread_callbacks = {}
local thread_origins = {}
local menu_back_calls = 0
local notices, deleted_notices = {}, {}
local list_refs, current_page_id, panel_draw, drawn_text, menu_visible = {}, 0, nil, {}, true
local memory_values = {}
local last_pointer
local function ref()
@@ -17,7 +17,9 @@ menu = {
list = function(_, label)
counters.list = counters.list + 1
list_names[label] = true
return ref()
local result = ref()
list_refs[label] = result
return result
end,
action = function(_, label, _, _, callback)
counters.action = counters.action + 1
@@ -34,14 +36,11 @@ menu = {
return ref()
end,
readonly = function() counters.text = counters.text + 1; return ref() end,
divider = function(_, label)
local result = ref()
if label == "Unavailable in Single Player." or label == "Waiting for game..." then
notices[result.id] = label
end
return result
end,
delete = function(value) deleted_notices[value.id] = true end,
divider = function() return ref() end,
is_visible = function() return menu_visible end,
page_id = function() return current_page_id end,
bounds = function() return 100, 80, 420 end,
content_rect = function() return 100, 250, 420, 440 end,
get_value = function() return false end,
set_value = function() end,
get_menu_name = function() return "" end,
@@ -57,7 +56,10 @@ __smenu = {
node_in_viewport = function() return true end,
}
util = {
joaat = function(name) return #tostring(name) end,
joaat = function(name)
local page = tostring(name):match("^Stand#(%d+)$")
return page and (100000 + tonumber(page)) or #tostring(name)
end,
toast = function() end,
yield = function()
local _, is_main = coroutine.running()
@@ -70,6 +72,23 @@ util = {
return {}
end,
}
overlay = {on_draw = function(name, callback)
assert(name == "ultimate_menu_information", "Unexpected overlay registration")
panel_draw = callback
end}
font = {small = 1, item = 2}
ctx = {screen_w = function() return 1280 end, screen_h = function() return 900 end}
input = {mouse_x = function() return 0 end, mouse_y = function() return 0 end,
mouse_wheel = function() return 0 end}
theme = {accent = function() return 55, 145, 245 end}
text = {
width = function(_, value) return #tostring(value) * 8 end,
height = function() return 16 end,
draw = function(_, _, _, _, _, _, _, value) drawn_text[#drawn_text + 1] = value end,
draw_ellipsis = function(_, _, _, _, _, _, _, value) drawn_text[#drawn_text + 1] = value end,
}
draw = {rect = function() end, rect_outline = function() end,
push_clip = function() end, pop_clip = function() end}
__ny_execute_as_script = function(_, callback) callback(); return true end
__ny_request_script_host = function() return true end
memory = {
@@ -187,22 +206,59 @@ if not os.getenv("UM_START_OFFLINE") then
assert(counters.action >= 940, "Missing initial actions: " .. counters.action)
end
assert(#compat.imgui_sections >= 43, "Missing ImGui sections: " .. #compat.imgui_sections)
assert(panel_draw, "Information overlay did not register")
if os.getenv("UM_START_OFFLINE") then
local viewport_thread = coroutine.create(thread_callbacks[#thread_callbacks])
local ok, message = coroutine.resume(viewport_thread)
assert(ok, message)
assert(next(notices), "Single Player warning did not appear as a non-selectable row")
for _, label in pairs(notices) do
assert(not label:find("\n", 1, true), "Single Player warning spans multiple menu rows")
local recovery_id = util.joaat("Stand#" .. list_refs["Set Rank menu"].id)
local offline_info = compat.info_for_page(recovery_id)
local warning = false
for _, row in ipairs(offline_info.rows) do
if row.text:find("Unavailable in Single Player", 1, true) then warning = true end
end
assert(warning, "Single Player warning did not reach the information panel")
session_started = true
ok, message = coroutine.resume(viewport_thread)
assert(ok, message)
for id in pairs(notices) do
assert(deleted_notices[id], "Single Player warning remained after joining online")
for _, row in ipairs(compat.info_for_page(recovery_id).rows) do
assert(not row.text:find("Unavailable in Single Player", 1, true),
"Single Player warning remained after joining online")
end
assert(counters.action >= 940, "Online controls did not appear after joining: " .. counters.action)
end
local apartment_id = util.joaat("Stand#" .. list_refs["Apartment Heist "].id)
local apartment_info = compat.info_for_page(apartment_id)
assert(apartment_info and #apartment_info.rows >= 5, "Apartment Heist instructions did not reach the panel")
assert(counters.text == 0, "Informational text still occupies selectable menu rows")
local kortz_id = util.joaat("Stand#" .. list_refs["Kortz Center Heist"].id)
local kortz_info = compat.info_for_page(kortz_id)
local colored_status, expanded_help = false, false
for _, row in ipairs(kortz_info.rows) do
if row.color then colored_status = true end
if row.text:find("Select the main painting", 1, true) then expanded_help = true end
end
assert(colored_status, "Colored YimMenu status text lost its color")
assert(expanded_help, "YimMenu help marker was not expanded in the panel")
current_page_id = apartment_id
panel_draw()
local found_instruction = false
local found_wrapped_instruction = false
for _, value in ipairs(drawn_text) do
if value:find("Pay for the preparation", 1, true) then found_instruction = true end
if value:find("change the session", 1, true) then found_wrapped_instruction = true end
end
assert(found_instruction, "Long heist instructions were not drawn in the information panel")
assert(found_wrapped_instruction, "Long heist instructions were cut off instead of wrapped")
local drawn_count = #drawn_text
current_page_id = 0
panel_draw()
assert(#drawn_text == drawn_count, "Information panel appeared outside its submenu")
current_page_id = apartment_id
menu_visible = false
panel_draw()
assert(#drawn_text == drawn_count, "Information panel remained after the menu closed")
menu_visible = true
for _, name in ipairs({"Self Menu", "Story Mode", "Recovery Menu", "Online Services Menu",
"Collectibles", "Events Menu", "YimResupplier", "Heists Data Editor Menu", "Kortz Center Heist"}) do
assert(list_names[name], "Missing original menu section: " .. name)
+70 -34
View File
@@ -254,7 +254,16 @@ end
local tab_methods = {}
local imgui_sections = {}
local info_pages = {}
local tab_mt = {__index = tab_methods}
local function page_id(ref)
return util.joaat("Stand#" .. tostring(type(ref) == "table" and ref.id or ref))
end
local function info_page(ref, title)
local id = page_id(ref)
if not info_pages[id] then info_pages[id] = {title = title or "Information", static = {}} end
return info_pages[id], id
end
local function text_input(parent, label, help, callback, value)
if menu.text_input then
return menu.text_input(parent, label, {}, help, callback, value)
@@ -262,6 +271,7 @@ local function text_input(parent, label, help, callback, value)
return __smenu.text_input(type(parent) == "table" and parent.id or parent, label, {}, help, callback, value)
end
local function tab(ref, path)
info_page(ref, path:match("([^/]+)$") or path)
return setmetatable({ref = ref, path = path}, tab_mt)
end
@@ -297,7 +307,10 @@ function tab_methods:add_input_int(label, default_value)
function obj:set_value(value) self.value = value end
return obj
end
function tab_methods:add_text(label) return menu.readonly(self.ref, label, "") end
function tab_methods:add_text(label)
local page = info_page(self.ref)
page.static[#page.static + 1] = {text = tostring(label or "")}
end
function tab_methods:add_separator() return menu.divider(self.ref, "") end
function tab_methods:add_sameline() end
function tab_methods:add_imgui(callback)
@@ -327,7 +340,7 @@ function script.execute_as_script(name, callback)
return __ny_execute_as_script(name, callback)
end
local current_section, current_event, building, widget_index, widget_occurrences, text_occurrences, separator_occurrences, parent_stack
local current_section, current_event, building, widget_index, widget_occurrences, separator_occurrences, parent_stack
local function current_parent() return parent_stack[#parent_stack] end
local function push_parent(ref) parent_stack[#parent_stack + 1] = ref end
local function pop_parent() if #parent_stack > 1 then parent_stack[#parent_stack] = nil end end
@@ -364,13 +377,9 @@ local function label_for_menu(label)
end
local function replay(section, event)
current_section, current_event = section, event
building, widget_index, widget_occurrences, text_occurrences, separator_occurrences, parent_stack = false, 0, {}, {}, {}, {section.parent}
section.notice_seen = false
building, widget_index, widget_occurrences, separator_occurrences, parent_stack = false, 0, {}, {}, {section.parent}
section.info_rows = {}
section.callback()
if section.notice_ref and not section.notice_seen then
menu.delete(section.notice_ref)
section.notice_ref = nil
end
current_section, current_event, parent_stack = nil, nil, nil
end
local function event_for(id, value)
@@ -386,6 +395,7 @@ local function register_action(label, value)
end)
end
section.last_ref = section.widget_refs[id]
section.last_info_row = nil
return event_for(id, value)
end
@@ -402,6 +412,7 @@ function ImGui.Checkbox(label, value)
end, value and true or false)
end
section.last_ref = section.widget_refs[id]
section.last_info_row = nil
if current_event and current_event.id == id then return current_event.value, true end
return value, false
end
@@ -419,6 +430,7 @@ local function numeric_input(label, value, as_float)
end, tostring(value or 0))
end
section.last_ref = section.widget_refs[id]
section.last_info_row = nil
if current_event and current_event.id == id then return current_event.value, true end
return value, false
end
@@ -435,6 +447,7 @@ function ImGui.SliderInt(label, value, min_value, max_value)
end)
end
section.last_ref = section.widget_refs[id]
section.last_info_row = nil
if current_event and current_event.id == id then return current_event.value, true end
return value, false
end
@@ -448,6 +461,7 @@ function ImGui.InputText(label, value)
end, value or "")
end
section.last_ref = section.widget_refs[id]
section.last_info_row = nil
if current_event and current_event.id == id then return current_event.value, true end
return value, false
end
@@ -469,6 +483,7 @@ function ImGui.Combo(label, selected, choices, count)
end
end
section.last_ref = section.containers_by_id[id]
section.last_info_row = nil
local current_choice = choices[(selected or 0) + 1]
if current_choice then
local display = menu_label .. ": " .. tostring(current_choice)
@@ -477,7 +492,7 @@ function ImGui.Combo(label, selected, choices, count)
if current_event and current_event.id == id then return current_event.value, true end
return selected, false
end
function ImGui.Text(value)
local function record_text(value, color)
if value == nil or value == "" then return end
local label = tostring(value)
if current_section.tooltip then
@@ -487,29 +502,21 @@ function ImGui.Text(value)
return
end
local compact = label:gsub("%s+", " "):match("^%s*(.-)%s*$")
if compact == "Unavailable in Single Player." or compact == "Waiting for game..." then
current_section.notice_seen = true
if not current_section.notice_ref then
current_section.notice_ref = menu.divider(current_parent(), compact)
elseif menu.get_menu_name(current_section.notice_ref) ~= compact then
menu.set_menu_name(current_section.notice_ref, compact)
end
return
end
label = compact
local text_id = source_key(text_occurrences, "text:" .. tostring(#current_section.text_refs + 1))
if not current_section.text_refs[text_id] then
current_section.text_refs[text_id] = menu.readonly(current_parent(), label, "")
else
local ref = current_section.text_refs[text_id]
if menu.get_menu_name(ref) ~= label then menu.set_menu_name(ref, label) end
end
current_section.last_ref = current_section.text_refs[text_id]
if #label <= 60 then current_section.last_text = label:gsub(":$", "") end
local _, id = info_page(current_parent())
local rows = current_section.info_rows[id]
if not rows then rows = {}; current_section.info_rows[id] = rows end
local row = {text = label, color = color}
rows[#rows + 1] = row
current_section.last_info_row = row
current_section.last_ref = nil
if #compact <= 60 then current_section.last_text = compact:gsub(":$", "") end
end
ImGui.Text = record_text
ImGui.TextUnformatted = ImGui.Text
ImGui.TextWrapped = ImGui.Text
function ImGui.TextColored(_, _, _, _, value) ImGui.Text(value) end
function ImGui.TextColored(r, g, b, a, value)
record_text(value, {r, g, b, a})
end
function ImGui.Separator()
local id = source_key(separator_occurrences, "separator:" .. tostring(widget_index))
if not current_section.separator_refs[id] then
@@ -523,7 +530,13 @@ function ImGui.ProgressBar(fraction, _, _, overlay)
ImGui.Text(overlay or string.format("%d%%", math.floor(math.max(0, math.min(1, fraction or 0)) * 100)))
end
function ImGui.SetTooltip(message)
if current_section.last_ref then menu.set_help_text(current_section.last_ref, tostring(message)) end
if current_section.last_info_row then
local row = current_section.last_info_row
if row.text == "[?]" then row.text = tostring(message)
else row.tooltip = tostring(message) end
elseif current_section.last_ref then
menu.set_help_text(current_section.last_ref, tostring(message))
end
end
function ImGui.BeginTooltip() current_section.tooltip = true; return true end
function ImGui.EndTooltip() current_section.tooltip = false end
@@ -550,11 +563,13 @@ function ImGui.BeginTabItem(label)
local ref = current_section.containers_by_id[id]
if not ref then
ref = menu.list(current_parent(), label, {}, "")
info_page(ref, label)
current_section.containers[#current_section.containers + 1] = ref
current_section.containers_by_id[id] = ref
end
if not ref then return false end
if current_event and not current_event.ancestors[ref.id] then return false end
current_section.last_info_row = nil
push_parent(ref)
return true
end
@@ -577,15 +592,36 @@ ImGuiCol = {Text = 0}
ImGuiTabBarFlags = {None = 0}
ImGuiWindowFlags = {Modal = 0, NoCollapse = 0, NoMove = 0, NoResize = 0}
local compat = {imgui_sections = imgui_sections}
local compat = {imgui_sections = imgui_sections, info_pages = info_pages}
function compat.info_for_page(id)
local page = info_pages[id]
if not page then return nil end
local rows, last_label = {}, nil
local function append(row)
local label = row.text:gsub("%s+", " "):match("^%s*(.-)%s*$")
if label == "" then
if #rows > 0 and rows[#rows].text ~= "" then rows[#rows + 1] = {text = ""} end
last_label = nil
elseif label ~= last_label then
rows[#rows + 1] = row
last_label = label
end
end
for _, row in ipairs(page.static) do append(row) end
for _, section in ipairs(imgui_sections) do
local live = section.info_rows and section.info_rows[id]
if live then for _, row in ipairs(live) do append(row) end end
end
return {title = page.title, rows = rows}
end
function compat.materialize_imgui()
for _, section in ipairs(imgui_sections) do
section.text_refs, section.widget_refs, section.separator_refs = {}, {}, {}
section.widget_refs, section.separator_refs = {}, {}
section.info_rows = {}
section.containers, section.containers_by_id = {}, {}
section.tooltip, section.last_ref, section.last_text = false, nil, nil
section.notice_ref, section.notice_seen = nil, false
current_section, current_event = section, nil
building, widget_index, widget_occurrences, text_occurrences, separator_occurrences, parent_stack = true, 0, {}, {}, {}, {section.parent}
building, widget_index, widget_occurrences, separator_occurrences, parent_stack = true, 0, {}, {}, {section.parent}
scan_context = section.path:find("Story Mode", 1, true) and "story" or "online"
section.callback()
scan_context = nil
+1 -1
View File
@@ -43,7 +43,7 @@
- For **Nenyoo GTA V Legacy**:
Copy the contents of [`Nenyoo`](Nenyoo) to `%LOCALAPPDATA%\Nenyoo\Plus\Scripts\User\UltimateMenu\`. In Nenyoo, open `Scripts` and select `Ultimate_Menu.lua` to load its menu page. The `lib` folder must stay beside the entry script. Join GTA Online before using online actions.
The Nenyoo edition translates the YimMenu Legacy menu hierarchy and controls. It requires a Nenyoo build with the named-script execution and host-request Lua bridges. GTA V Enhanced has not been validated.
The Nenyoo edition translates the YimMenu Legacy menu hierarchy and controls, with instructions and live status in a companion information panel beside the active submenu. It requires a Nenyoo build with the named-script execution and host-request Lua bridges. GTA V Enhanced has not been validated.
</details>
---