All notable changes to LastMenu are documented here.
Format follows Keep a Changelog.
Version scheme: MAJOR.MINOR.PATCH — breaking changes bump MAJOR.
1.0.2 — 2026-05-06
UI_ConfirmAsync(message, opts?)(alert.lua,exports.lua) — one-liner async confirm shortcut. WrapsUI_AlertAsyncwith pre-filled confirm/cancel buttons. Accepts{ type, title, confirm_label, cancel_label }. Exported viaexports('confirm_async', ...)./lm_overlaycommand +debug_overlayexport (exports.lua) — real-time watcher stats overlay viaDrawText. Displays field, status (ok/DIS), interval and cb_id suffix for all active watchers. ACE permission:lastmenu.dev.- Notification group counter (
Notify.svelte,Notify.css) — when a toast withgrouparrives while an identical one is already shown, the×Ncounter increments instead of silently replacing it. - NUI crash safety net (
App.svelte) —window.onerror+window.onunhandledrejectioninonMount. If a JS error occurs while the stack is non-empty, fires/escapeto release NUI focus. useTarget.svelte.ts(TargetComponents/) — missing file created. Implements reactive cooldown (100ms timer), stateful items (toggle/checkbox/slider), accordions, hold-to-confirm for target menus.- Reset all settings — footer button (red-tinted) resets every field to
SETTINGS_DEFAULTSin the live local state; live preview reflects it instantly; Cancel still discards without saving. - Live preview — settings panel now applies theme changes in real-time while open; cancelling or closing reverts to the saved state without touching
localStorage. Implemented via a split$effectpair inApp.svelteand anonPreviewcallback chain throughUserSettings→useUserSettings. Hold duration (Navigation section)— removed from user settings panel;_normalizeHoldstill resolvesuser > dev > configbut the user override is no longer exposed in the UI.- Page size (Context section) — user override for items-per-page;
null= Auto (uses devb:page_size()or default). Applied inApp.svelteby spreadingpage_sizeontomenuItem.datawhen non-null — zero changes toContext.svelte. - Radial size (Appearance section) — Compact / Normal / Large / Auto; sets
--ui-radial-sizeCSS variable viaapplyTheme. - Target menu size (Appearance section) — Compact / Normal / Large / Auto; sets
--ui-target-widthCSS variable viaapplyTheme. - RegisterKeyMapping for target hold key — replaced
IsControlPressedpolling with+lastmenu_target/-lastmenu_targetcommand pair;LastMenu._targetHeldboolean. Dev-configurable default viaConfig.target_key. 8-language auto-translated descriptions usinglm_languageKVP read at resource start.
selectedIndexout of bounds on radial (useRadial.svelte.ts) —kbIndex >= 0did not checkkbIndex < n. If a button became invisible during keyboard navigation, an invalid index was returned. Added guardkbIndex < n.rgb()accent missing--ui-accent-dim(theme.ts) — dark shade calculation only handled#rrggbb/#rgb.rgb(r,g,b)colors did not generate a dim shade. Addedrgb()/rgba()parsing.- Dead code removed (
stack.lua) — commented-outDisableControlAction(199/200)block and undocumented AFK camera code removed. - Radial menu coordinates broken at >1080p —
handleMousemovecomputed mouse position relative togetBoundingClientRect()without accounting for the CSSzoomapplied byapplyViewportScale. At resolutions > 1920×1080 (e.g. 1440p, 4K) the zoom factor caused the mouse-to-arc mapping to be off proportionally to the scale, requiring the mouse to be far outside the visual menu to register a sector. Fix: divide(clientX - rect.left)and(clientY - rect.top)byparseFloat(document.documentElement.style.zoom) || 1. - Target hold key not detected on resource restart —
RegisterKeyMappingbinding propagation in FiveM is asynchronous; after a resource restart the+lastmenu_targetcommand may not fire until the GTA key-binding system refreshes (triggered by another resource starting). AddedIsControlPressed(0, Config.target_hold_key)as a direct fallback alongsideLastMenu._targetHeldin the polling condition. The fallback activates the reticle on the default key immediately after restart; once the key mapping propagates, the command path takes over. Players who rebind the key in GTA settings are unaffected (the command fires on their custom key; the fallback only covers the default). - Reset All button overflows footer at narrow widths / long translations — changed from
[icon + text]to icon-only (RotateCcw) withtitletooltip, reducing the footer IO row to three compact icon buttons that never push Cancel / Save off-screen.
- Upvalue localization in
reactive.lua—json.encode,math.floor,math.min,math.max,math.randomcached as module-level upvalues. Avoids_ENVtable traversal on every hot-path call inside the tick loop and_jitter. Measurable gain when multiple watchers tick at high frequency (≥8 watchers at ≤200 ms intervals: ~5–8 % tick time reduction). Bridge.sendpayload copy removed — eliminated thepairs()shallow-copy loop that duplicated the payload table on every send. All callers pass fresh table literals;payload.type = msgTypeis now set in-place. Saves one table allocation + iteration per enqueued message.- Size guard moved to debug-only in
Bridge.send— thejson.encode(outMsg)call used solely to measure payload size beforeSendNUIMessage(which re-encodes internally) is now gated behindConfig.debug. Eliminates one full encode per tick in production. _hasDebouncepre-computed flag in reactive watchers —w.debounce_msevaluation moved from per-tick (inside_processWatcher) to attach-time. The hot-path debounce flush check now reads a boolean flag instead of doing a type check + numeric comparison on every watcher tick, regardless of whether debounce is configured.intervalCurrentreset guard —w.intervalCurrent = w.intervalon every successful watcher evaluation is now guarded byw.intervalCurrent ~= w.interval. Avoids the unconditional write in the common steady-state (no backoff active), reducing memory writes in the hot loop.math.*upvalues instartTicking—tickMscomputation now uses the module-levelmath_max/math_min/math_floorupvalues.- Target thread:
Stack.peek()single call per iteration — in the polling thread,Stack.peek()was called twice per iteration to check~= nilthen.type. Now called once at the top of the loop body into_stackTop, reused across both checks. - Target thread:
TR.getTargetDist()cached per tick —getTargetDist()(which performs a type-check + conditional log onConfig.target_max_distance) was called twice per active tick: once forgetRaycastHitand once internally viagetSpatialRadiusinsidefindMatchingRegs. The explicit call is now cached intomaxDistand passed directly togetRaycastHit, eliminating the redundant outer call. - Target thread:
PlayerPedId()re-call eliminated —GetEntitySpeed(PlayerPedId())in the idle-moving check re-invokedPlayerPedId()unnecessarily. Now uses theplayerPedlocal already declared at the top of the same tick branch. - Target thread:
table.sortfast-path for single match —table.sort+table.concatwere called unconditionally to buildmatchKey. For the common single-registration case (1 match), sort and concat are now bypassed;matchKey = parts[1]is assigned directly. The empty-match case (matchKey = '') is also short-circuited before any table operation. json_encodeupvalue inReactive.attach—json.encodeused at attach-time to seedw._lastJsonfor table watchers was not yet using the module-level upvalue. Now consistent with the rest of the file.- EasySwitch
{ safe = true }on_zoneDispatch(target/raycast.lua) — the zone dispatch loop ran without error protection around:execute(). A malformed registration (e.g. nilradiuson a sphere) could throw and permanently crash the target polling thread. Thesafeoption wraps dispatch in a pcall internally, isolating faults to the offending registration only. - EasySwitch
_Gglobal export (easyswitch.lua) — the updated library usedlocal EasySwitch … return EasySwitch(module pattern). In FiveM'sclient_scriptscontext thereturnvalue is discarded, leaving_G.EasySwitchnil and breaking every consumer (stack.lua,raycast.lua). Added_G.EasySwitch = EasySwitchbefore thereturnto support both load contexts.
- Settings panel sections restructured
- Sections
modal+progressmerged into Components (idcomponents). perfModetoggle moved into the Appearance section (after blur effects).perfsection removed.target_keyblock andnav_hintdeveloper note removed from Navigation section.- Components tab expanded —
contextandnotifysections removed as standalone sidebar entries; their settings merged into Components under labelled sub-group separators (──── CONTEXT ────,──── NOTIFICATIONS ────, etc.). Radial and Target size settings also moved from Appearance into Components. Sidebar reduced from 7 to 5 tabs. - Settings panel opens on Appearance by default (was Navigation).
- Hint copy cleaned — developer-facing notes removed from
hold_duration_hint,page_size_hint, andlang_hintacross all 8 languages; hints now convey only what is actionable for the player.
- Sections
SETTINGS_VERSIONbumped 3 → 4 — new fields (holdDuration,pageSize,radialSize,targetSize) added toUserSettingsinterface andSETTINGS_DEFAULTS(all defaultnull).targetKeyremoved fromUserSettingsinterface and defaults (superseded byRegisterKeyMapping).- Translations —
sec_modal,sec_progress,sec_perfremoved fromTranslationKeys;sec_componentsadded. New keys:hold_duration,hold_duration_hint,page_size,page_size_hint,radial_size,radial_size_hint,target_size,target_size_hint,auto_label,size_compact,size_normal,size_large,comp_context,comp_notify,comp_radial,comp_target,comp_modal,comp_progress— all 8 languages. $effectsplit inApp.svelte— theme application andlocalStoragepersistence are now separate effects so live preview doesn't write stale values to storage.
1.0.1 — 2026-05-04
- Removed redundant
Reactive.evaluateafterReactive.attachin context and radial builders.attachalready initialisesnextAt = 0on all watchers; the subsequentevaluatecall was iterating the full watcher list a second time for no effect. Eliminates one full watcher loop on every menu open (context,context_build,context_update,radial,radial_build). _stableIdfast path — labels containing only alphanumeric characters and spaces (the vast majority of real labels) now execute a singlegsubinstead of five chained passes, reducing temporary string allocations by ~80 % per item during menu build.isCallablehoisted to module level incore.lua. Previously re-created as a closure on each_makeResolvercall (once per menu build); now a shared module-level function with zero per-build allocation cost.
- Watcher thread crash on un-encodable table —
json.encode(value)insideReactive._processWatcherwas not protected bypcall. A watcher returning a table that cannot be JSON-encoded (circular reference, unserializable userdata, etc.) would throw an unhandled error that propagated out of the tick loop and killed the entire reactive thread, stopping all watchers for that menu permanently. The encode call is now wrapped inpcall; failures are treated as watcher errors and enter the existing backoff/DEAD cycle, isolating the fault to the offending field only. - Tick loop safety net — each
_processWatchercall inReactive.startTickingis now individually guarded by an outerpcall. Any unhandled error path in future watcher logic will be caught and logged per-watcher rather than crashing the thread.
1.0.0 — 2026-05-03
First public release of LastMenu.
- Global namespace
LastMenucreated inconfig.lua, locked via__newindexafter load (exports.lua) — any external write raises an error - Strict load order:
config → bridge → stack → reactive → core → builders → exports → settings - Bridge layer with automatic NUI batching (
Citizen.SetTimeout(0)) — single message sent directly, multiple messages grouped into abatch; payload > 1 MB guard - Stack engine: navigation pile with NUI focus management per mode (
both/mouse/keyboard), Map key suppression (199/200), configurable distance-based auto-close - Reactive engine: adaptive polling with exponential backoff (
×1.5, max×8), table change detection viajson.encode, ±10% jitter on all retry delays, automatic recovery after errors (MAX_ERRORS = 5,RETRY_DELAY_MS = 30 000) - Events:
LastMenu:menuOpened(id, type)/LastMenu:menuClosed(id, type) - Automatic cleanup on
onResourceStop: NUI focus released, stack cleared, dismiss callbacks purged
- Full fluent builder:
title,banner,description,nav,animation,search,page_size,cancelable,scroll - Items:
button,slider,stepper,checkbox,toggle,list,stat,input_inline,color_picker,date_picker,separator,header,accordion,tab,submenu,back - Reactive fields per item:
label,visible,disabled,color,badge,preview(button);value,max,color(stat) opts.confirm_hold— per-item hold-to-confirm (ms ortruefor global value)opts.cooldown+opts.persist_key— inter-click cooldown with KVP persistence (survives NUI reload)opts.preview— hover preview card{ image, title, desc, stats[] }on buttonsopts.timeout— auto-click after delay- Stable slugified IDs (
_stableId) with automatic deduplication (_2,_3) for conditional items UI_Context(fn)— immediate openUI_Context_Build(fn)→ handle (open,close,update) — persistent reloadable menu, double-open guardUI_Context_Update(handle, fn)— alias for update- O(m) NUI patch via
liveOverrides— only modified fields are updated, no full list recompute
- Builder:
title,message,type(info/confirm/warn/error),confirm(label, cb),cancel(label, cb) UI_Alert(fn)— standard openUI_Alert_Build(fn)→ handleUI_AlertAsync(fn)→bool— blocking coroutine, returnstrue(confirm) orfalse(cancel / Escape)
- Builder:
title,field(label, opts)—type='text'|'number',default,placeholder,maxlen,min,max,pattern,pattern_error confirm(label, cb)—cb(values[]),cancel(label, cb)UI_Input(fn)— standard openUI_Input_Build(fn)→ handleUI_InputAsync(fn)→values[]|false— blocking coroutine
- Builder:
message,type(info/success/warn/error),duration,persistent(duration=0),icon,title,group,on_dismiss(cb) group— deduplication: replaces the active toast with the same group keyon_dismiss— callback on × click (persistent only), purged onnotify_expiredandonResourceStop- Direct Bridge send, no Stack push
- Builder:
label,duration,anim(dict, clip, flag?),prop(model, bone, pos, rot?),cancelable - Deduplication: only one active bar at a time (
_openProgressInstance) UI_Progress(fn)— standard openUI_Progress_Build(fn)→ handle
- Builder:
center_label,button(label, opts)—icon,visible (fn),disabled (fn),refresh,confirm_hold,keep_open,submenu(fn)(pushes nested radial) - Gamepad thread: reads left stick → NUI messages when radial is active (magnitude > 0)
UI_Radial(fn)— standard openUI_Radial_Build(fn)→ handle
UI_Target_AddEntity(entity, fn)UI_Target_AddModel(model_hash, fn)UI_Target_AddSphere(coords, radius, fn)UI_Target_AddBox(coords, size, fn)UI_Target_AddPoly(vertices[], fn)UI_Target_Update(target_id, fn)/UI_Target_Remove(target_id)/UI_Target_Clear()- Zone builder:
label,icon,distance,banner,on_enter(fn),on_leave(fn) - Items:
button,toggle,checkbox,slider,separator,group(label, opts, fn) - Adaptive raycast with
target_max_distanceand configurable hold key (target_hold_key, default Left Ctrl) - Debug zone drawing with
Config.debugTarget = true
- Key format:
lm_cd_<resName>_<key>— capped at 1 year, survives NUI reload and reconnect - NUI → Lua via
cooldown:set/cooldown:clearcallbacks - Validation: key ≤ 64 chars, pattern
^[%w%-_]+$
- Command
lastmenu_settings(F12 keybind by default viaRegisterKeyMapping) - Preferences: navigation mode (
both/mouse/keyboard), target key - localStorage persistence → restored on
Bridge.onReadyat NUI startup - Exports:
settings_open(),settings_close()
- Svelte 5 Runes app — NUI stack mirroring the Lua stack
- Menus:
context,alert,input,notify,progress,radial,target - Incoming messages:
open,close,patch,batch,reset,settings,cooldown:set - Enter/exit animations (200 ms exit before remove)
- Theming:
default.csswith 50+ CSS variables; preset themes (Military, Luxury, Neon/Cyberpunk) - Dynamic Lucide icons
context, context_build, context_update
alert, alert_build, alert_async
notify
progress, progress_build
radial, radial_build
input, input_build, input_async
target_add_entity, target_add_model, target_add_sphere, target_add_box, target_add_poly
target_update, target_remove, target_clear
lastmenu_back, debug_stats, version
settings_open, settings_close
/lm_test(ACElastmenu.dev) — runs the test/showcase suite/lm_debug(ACElastmenu.dev) — JSON snapshot of the Reactive watcher state