-- swim> Pause Menu (online script) -- -- Replaces AC's ESC pause UI on the swim server with a custom Obsidian-Glass -- card and a built-in server browser. Drop this file into the server's -- online-script slot; the cutup script (test-cutup.lua) runs alongside it -- and shares settings via the `ac.connect` block below. -- -- Differences from the original app version: -- 1. No windowMain() — online scripts don't use sidebar windows. -- 2. No ac.storage — the cutup script is the sole disk owner; this file -- reads/writes the live shared struct only. -- The menu is triggered by ESC at any time (driving, paused, replay etc.) -- and the game keeps running underneath. AC's pause is reserved for the -- explicit "AC pause menu" button. local menuOpen = false -- when true, our card overlay is drawn local useNativeMenu = false -- when true, AC draws its default pause UI instead local sim = ac.getSim() local steam = require('shared/utils/steam') -- Player identity (looked up once at script load — Steam ID won't change) local localSteamId do local ok, id = pcall(function() return steam.user.getSteamID() end) localSteamId = ok and id or nil end -------------- -- SETTINGS — shared with the cutup script via ac.connect. -- -- The cutup script (test-cutup.lua) owns disk persistence: it hydrates this -- shared struct from its ac.storage on startup, and its own setInterval -- watcher catches our writes here and persists them. So writes from the -- pills below are durable; we just don't touch disk ourselves. -- -- IMPORTANT: the layout below must match the cutup script byte-for-byte. local Settings = ac.connect({ ac.StructItem.key('swim.settings.v1'), hudEnabled = ac.StructItem.boolean(), hudPosition = ac.StructItem.int32(), compactMode = ac.StructItem.boolean(), }, true) -- keepLive=true so the struct outlives any single script unload local POSITION_NAMES = { 'TL', 'TR', 'BL', 'BR' } -- For display only: if we render before the cutup script has hydrated the -- shared struct, hudPosition will be 0 (C int default) which isn't a valid -- value. Coerce it to a sane default for the position-pill highlight, but -- don't write — let the cutup script's hydration win. local function safeHudPosition() local p = Settings.hudPosition if p < 1 or p > 4 then return 1 end return p end -------------- -- THEME (Obsidian Glass — palette mirrors swim>) local Theme = { glass = rgbm(0.047, 0.047, 0.059, 0.78), glassSheen = rgbm(1, 1, 1, 0.06), glassSheenLo = rgbm(1, 1, 1, 0.0), glassEdge = rgbm(1, 1, 1, 0.10), glassSurface = rgbm(0.094, 0.098, 0.118, 0.78), glassSurfaceHi= rgbm(0.149, 0.153, 0.180, 0.92), glassDisabled = rgbm(0.063, 0.063, 0.075, 0.55), textHero = rgbm(1, 1, 1, 1), textPrimary = rgbm(0.941, 0.941, 0.949, 1), textSecondary = rgbm(0.659, 0.659, 0.690, 1), textMuted = rgbm(0.376, 0.376, 0.408, 1), textGhost = rgbm(0.518, 0.518, 0.553, 1), textDisabled = rgbm(0.310, 0.310, 0.345, 1), accent = rgbm(0.647, 0.769, 0.831, 1), accentBright = rgbm(0.761, 0.855, 0.902, 1), accentGlow = rgbm(0.647, 0.769, 0.831, 0.22), accentSubtle = rgbm(0.647, 0.769, 0.831, 0.10), success = rgbm(0.639, 0.902, 0.208, 1), error = rgbm(0.973, 0.443, 0.443, 1), backdrop = rgbm(0.012, 0.012, 0.024, 0.42), } local UI = { cardW = 420, cardH = 550, userCardW = 420, userCardH = 170, serverCardW = 460, serverCardH = 736, lbCardW = 460, lbCardH = 736, cardGap = 16, pad = 22, radius = 18, innerRadius = 11, btnH = 44, btnGap = 8, pulsePhase = 0, } -------------- -- Smoothing helper (frame-rate independent) local function smooth(current, target, speed, dt) local k = 1 - math.exp(-speed * dt) return current + (target - current) * k end local function lerpColor(a, b, t) return rgbm( a.r + (b.r - a.r) * t, a.g + (b.g - a.g) * t, a.b + (b.b - a.b) * t, a.mult + (b.mult - a.mult) * t ) end -------------- -- Drawing primitives local function drawPanel(x, y, w, h, radius, fillColor, borderColor, borderWidth) local p1, p2 = vec2(x, y), vec2(x + w, y + h) if fillColor then ui.drawRectFilled(p1, p2, fillColor, radius) end if borderColor then ui.drawRect(p1, p2, borderColor, radius, ui.CornerFlags.All, borderWidth or 1) end end local function drawVerticalGradient(x, y, w, h, top, bottom, rounding, cornerFlags) ui.beginGradientShade() ui.drawRectFilled(vec2(x, y), vec2(x + w, y + h), rgbm.colors.white, rounding or 0, cornerFlags or ui.CornerFlags.All) ui.endGradientShade(vec2(x, y), vec2(x, y + h), top, bottom, true) end local function drawGlowStrip(x, y, width, color) local cx = x + width / 2 local sw = width * 0.6 ui.drawRectFilled(vec2(cx - sw / 2, y), vec2(cx + sw / 2, y + 2), color) end -- Shared scaffolding for any scrollable list inside a card. local SCROLLBAR_W = 8 local function drawScrollableList(id, x, y, w, h, accent, contentFn) drawPanel(x, y, w, h, UI.innerRadius, Theme.glassSurface, Theme.glassEdge, 1) ui.setCursor(vec2(x + 6, y + 6)) local innerW = w - 12 local innerH = h - 12 ui.pushStyleVar(ui.StyleVar.ScrollbarSize, SCROLLBAR_W) ui.pushStyleVar(ui.StyleVar.ScrollbarRounding, 4) ui.pushStyleColor(ui.StyleColor.ScrollbarBg, rgbm(0, 0, 0, 0)) ui.pushStyleColor(ui.StyleColor.ScrollbarGrab, rgbm(1, 1, 1, 0.10)) ui.pushStyleColor(ui.StyleColor.ScrollbarGrabHovered, rgbm(1, 1, 1, 0.20)) ui.pushStyleColor(ui.StyleColor.ScrollbarGrabActive, rgbm(accent.r, accent.g, accent.b, 0.65)) if ui.beginChild(id, vec2(innerW, innerH), false, ui.WindowFlags.NoBackground) then contentFn(innerW, innerH, SCROLLBAR_W) end ui.endChild() ui.popStyleColor(4) ui.popStyleVar(2) end -------------- -- Per-button hover animation state local hover = setmetatable({}, { __index = function() return 0 end }) -- Compact toggle/selector pill. local function togglePill(id, label, x, y, w, h, active, accent) local bg = active and Theme.accentSubtle or Theme.glassSurface local edge = active and Theme.accent or Theme.glassEdge drawPanel(x, y, w, h, UI.innerRadius, bg, edge, 1) ui.setCursor(vec2(x, y)) local clicked = ui.invisibleButton(id, vec2(w, h)) local hov = ui.itemHovered() local col = active and accent or (hov and Theme.textHero or Theme.textPrimary) ui.pushFont(ui.Font.Small) local lw = ui.measureText(label).x ui.setCursor(vec2(x + (w - lw) / 2, y + (h - 14) / 2)) ui.textColored(label, col) ui.popFont() return clicked end -- Animated row button with optional icon and disabled state. local function rowButton(opts) local id, label, icon = opts.id, opts.label, opts.icon local x, y, w, h = opts.x, opts.y, opts.w, opts.h local primary = opts.primary local enabled = opts.enabled ~= false local dt = opts.dt or 0 local hint = opts.hint local clicked = false ui.setCursor(vec2(x, y)) if enabled then clicked = ui.invisibleButton(id, vec2(w, h)) local h_target = ui.itemHovered() and 1 or 0 hover[id] = smooth(hover[id] or 0, h_target, 14, dt) else hover[id] = 0 end local hv = hover[id] or 0 local bgBase = enabled and Theme.glassSurface or Theme.glassDisabled local bgHover = enabled and Theme.glassSurfaceHi or Theme.glassDisabled local bg = lerpColor(bgBase, bgHover, hv) local edgeBase = Theme.glassEdge local edgeHover = primary and Theme.accent or Theme.accentSubtle local edge = lerpColor(edgeBase, edgeHover, hv * (enabled and 1 or 0)) drawPanel(x, y, w, h, UI.innerRadius, bg, edge, 1) if primary then local a = enabled and 1 or 0.3 local barCol = rgbm(Theme.accent.r, Theme.accent.g, Theme.accent.b, a) ui.drawRectFilled(vec2(x, y + 8), vec2(x + 3, y + h - 8), barCol) end local textX = x + 16 if icon then local iconCol if not enabled then iconCol = Theme.textDisabled elseif primary then iconCol = lerpColor(Theme.accent, Theme.accentBright, hv) else iconCol = lerpColor(Theme.textSecondary, Theme.textHero, hv) end ui.setCursor(vec2(x + 14, y + (h - 18) / 2)) ui.icon(icon, 18, iconCol) textX = x + 14 + 18 + 12 end local labelCol if not enabled then labelCol = Theme.textDisabled else labelCol = lerpColor(Theme.textPrimary, Theme.textHero, hv) end ui.pushFont(ui.Font.Main) local lh = ui.measureText(label).y ui.setCursor(vec2(textX, y + (h - lh) / 2)) ui.textColored(label, labelCol) ui.popFont() if hint then ui.pushFont(ui.Font.Small) local hw = ui.measureText(hint).x ui.setCursor(vec2(x + w - hw - 14, y + (h - 14) / 2)) ui.textColored(hint, enabled and Theme.textGhost or Theme.textDisabled) ui.popFont() end return enabled and clicked or false end -------------- -- Server browser state + networking local SWIM_API = 'https://api.swimserver.com/servers/assetto' local Browser = { state = 'idle', servers = {}, error = nil, lastFetch = 0, spinPhase = 0, } local function parseMaxFromFormat(fmt) if type(fmt) ~= 'string' then return 0 end local m = fmt:match('/(%d+)') return tonumber(m) or 0 end local function truncate(s, n) if type(s) ~= 'string' then return '' end if #s > n then return s:sub(1, n - 1) .. '…' end return s end local function parseServers(raw) local data = JSON.parse(raw) if type(data) ~= 'table' then return nil, 'malformed JSON' end local list = data.servers or data.data or data if type(list) ~= 'table' then return nil, 'no server list in response' end local out = {} for _, s in ipairs(list) do if type(s) == 'table' and (s.status == nil or s.status == 'running') then out[#out + 1] = { id = tostring(s.id or s.name or ''), name = tostring(s.display_name or s.name or s.serverName or 'Unnamed'), group = tostring(s.group or ''), description = tostring(s.description or ''), connectUrl = tostring(s.connect_url or s.connectUrl or ''), ip = tostring(s.ip or s.address or ''), port = tonumber(s.port or s.udp_port or s.udpPort) or 0, players = tonumber(s.players) or 0, maxPlayers = parseMaxFromFormat(s.player_format), passworded = s.password == true or s.passworded == true, } end end table.sort(out, function(a, b) if a.players ~= b.players then return a.players > b.players end return a.name:lower() < b.name:lower() end) return out end local function fetchServers() if Browser.state == 'loading' then return end Browser.state = 'loading' Browser.error = nil ac.log('swim> fetching server list...') web.get(SWIM_API, function(err, response) if err then Browser.state = 'error' Browser.error = tostring(err) ac.log('swim> server fetch failed: ' .. Browser.error) return end if response.status and response.status >= 400 then Browser.state = 'error' Browser.error = 'HTTP ' .. tostring(response.status) return end local body = response.body or '' local servers, perr = parseServers(body) if not servers then Browser.state = 'error' Browser.error = perr or 'parse error' return end Browser.servers = servers Browser.state = 'ready' Browser.lastFetch = tonumber(sim.systemTime) or 0 ac.log(string.format('swim> %d servers loaded', #servers)) end) end -------------- -- Join flow with confirm modal local Confirm = nil local Joining = nil local function dispatchAndShutdown(url) ac.log('swim> os.openURL: ' .. url) os.openURL(url) setTimeout(function() ac.shutdownAssettoCorsa() end, 0.5) end local function htmlDecode(s) return (s:gsub('&', '&'):gsub('<', '<'):gsub('>', '>') :gsub('"', '"'):gsub(''', "'")) end local function extractCmLink(text) if not text then return nil end local m = text:match('ac[mst][a-z]+://[^"\'<>%s]+') return m and htmlDecode(m) or nil end local function performConnect(s) ac.log('swim> resolving CM link from ' .. s.connectUrl) web.get(s.connectUrl, function(err, response) local link if response and response.headers then local loc = response.headers['Location'] or response.headers['location'] if loc then link = extractCmLink(loc) or (loc:match('^ac') and loc or nil) end end if not link and response and response.body then link = extractCmLink(response.body) end if not link and err then link = extractCmLink(tostring(err)) end if link then dispatchAndShutdown(link) else local status = response and response.status or 'no-response' local preview = (response and response.body or ''):sub(1, 400) ac.log(string.format('swim> no CM link found (status=%s)', tostring(status))) ac.log('swim> body preview: ' .. preview) ac.log('swim> falling back to opening the http URL (browser will flash)') dispatchAndShutdown(s.connectUrl) end end) end local function resolveAndConnect(s) if not s or s.connectUrl == '' then Joining = { server = s, state = 'error', error = 'no connect_url' } return end Joining = { server = s, state = 'connecting' } performConnect(s) end local function requestJoin(s) if Joining and (Joining.state == 'resolving' or Joining.state == 'connecting') then return end Confirm = { server = s } end local function confirmJoin() if not Confirm then return end local s = Confirm.server Confirm = nil resolveAndConnect(s) end local function cancelJoin() Confirm = nil end -- Kick off an initial fetch so the list is warm by the first ESC press. fetchServers() -------------- -- Leaderboard state + networking -- registered_only=true makes the server return only entries where the player -- has linked their Steam to a swimserver.com account. local LEADERBOARD_API = 'https://api.swimserver.com/identity/cutup/leaderboard?registered_only=true' local Leaderboard = { state = 'idle', entries = {}, error = nil, lastFetch = 0, } local lbFilter = 'all' -- 'all' | 'map' | 'car' | 'both' local function fmtScore(n) local s = tostring(math.floor(tonumber(n) or 0)) while true do local s2, c = s:gsub('^(-?%d+)(%d%d%d)', '%1,%2') s = s2 if c == 0 then break end end return s end local function steamIdEqual(a, b) if a == nil or b == nil then return false end return tostring(a) == tostring(b) end local function applyLbFilter(entries) if lbFilter == 'all' then return entries end local track = ac.getTrackID() local car = ac.getCarID(0) local out = {} for _, e in ipairs(entries) do local trackOk = (lbFilter == 'car') or (e.track == track) local carOk = (lbFilter == 'map') or (e.car == car) if trackOk and carOk then out[#out + 1] = e end end return out end local function findUserEntry(entries) if not localSteamId then return nil, nil end for i, e in ipairs(entries) do if steamIdEqual(e.steamid, localSteamId) then return i, e end end return nil, nil end local function fetchLeaderboard() if Leaderboard.state == 'loading' then return end Leaderboard.state = 'loading' Leaderboard.error = nil ac.log('swim> fetching leaderboard...') web.get(LEADERBOARD_API, function(err, response) if err then Leaderboard.state = 'error' Leaderboard.error = tostring(err) return end if response.status and response.status >= 400 then Leaderboard.state = 'error' Leaderboard.error = 'HTTP ' .. tostring(response.status) return end local data = JSON.parse(response.body or '') if type(data) ~= 'table' then Leaderboard.state = 'error' Leaderboard.error = 'malformed JSON' return end table.sort(data, function(a, b) return (tonumber(a.score) or 0) > (tonumber(b.score) or 0) end) Leaderboard.entries = data Leaderboard.state = 'ready' Leaderboard.lastFetch = tonumber(sim.systemTime) or 0 ac.log(string.format('swim> %d leaderboard entries loaded', #data)) end) end fetchLeaderboard() -------------- -- Pause menu card local function drawCustomPauseMenu(dt, cardX, cardY) local pulse = 0.92 + math.sin(UI.pulsePhase) * 0.08 local accent = rgbm(Theme.accent.r * pulse, Theme.accent.g * pulse, Theme.accent.b * pulse, 1) local cardW, cardH = UI.cardW, UI.cardH ui.beginTransparentWindow('swimPauseMenu', vec2(cardX, cardY), vec2(cardW, cardH), true, true) local pad = UI.pad drawPanel(-2, -2, cardW + 4, cardH + 4, UI.radius + 2, rgbm(0,0,0,0), Theme.accentGlow, 1) drawPanel(0, 0, cardW, cardH, UI.radius, Theme.glass, Theme.glassEdge, 1) ui.pushFont(ui.Font.Huge) local brandSize = ui.measureText('swim>') ui.popFont() local brandY = 16 local headerBandH = brandY + brandSize.y + 14 drawVerticalGradient(0, 0, cardW, headerBandH, Theme.glassSheen, Theme.glassSheenLo, UI.radius, ui.CornerFlags.Top) drawGlowStrip(0, 0, cardW, accent) ui.pushFont(ui.Font.Huge) ui.setCursor(vec2(pad, brandY)) ui.textColored('swim', accent) ui.sameLine(0, 0) ui.textColored('>', Theme.textMuted) ui.popFont() -- Close (×) button top-right do local sz = 28 local cx = cardW - sz - 12 local cyClose = brandY + (brandSize.y - sz) / 2 ui.setCursor(vec2(cx, cyClose)) if ui.invisibleButton('##swimPauseClose', vec2(sz, sz)) then menuOpen = false end local hov = ui.itemHovered() local col = hov and Theme.textHero or Theme.textSecondary local p = 8 ui.drawLine(vec2(cx + p, cyClose + p), vec2(cx + sz - p, cyClose + sz - p), col, 2) ui.drawLine(vec2(cx + sz - p, cyClose + p), vec2(cx + p, cyClose + sz - p), col, 2) end local cy = headerBandH ui.drawRectFilled(vec2(pad, cy), vec2(cardW - pad, cy + 1), Theme.glassEdge) cy = cy + 18 -- ── SESSION ───────────────────────── ui.pushFont(ui.Font.Small) ui.setCursor(vec2(pad, cy)) ui.textColored('SESSION', Theme.textGhost) ui.popFont() cy = cy + 18 local btnW = cardW - pad * 2 if rowButton{ id = '##resume', label = 'Resume', icon = ui.Icons.Resume, x = pad, y = cy, w = btnW, h = UI.btnH, primary = true, dt = dt, hint = 'ESC', } then menuOpen = false end cy = cy + UI.btnH + UI.btnGap if rowButton{ id = '##teleport', label = 'Teleport to pits', icon = ui.Icons.PitStopAlt, x = pad, y = cy, w = btnW, h = UI.btnH, dt = dt, } then ac.tryToTeleportToPits() menuOpen = false end cy = cy + UI.btnH + UI.btnGap if rowButton{ id = '##revert', label = 'Revert to pits', icon = ui.Icons.Pitlane, x = pad, y = cy, w = btnW, h = UI.btnH, dt = dt, } then ac.tryToTeleportToPits() menuOpen = false setTimeout(function() ac.tryToOpenRaceMenu() end, 0.1) end cy = cy + UI.btnH + UI.btnGap if rowButton{ id = '##nativeMenu', label = 'AC pause menu', icon = ui.Icons.Settings, x = pad, y = cy, w = btnW, h = UI.btnH, dt = dt, } then useNativeMenu = true menuOpen = false ac.tryToPause(true) end cy = cy + UI.btnH + UI.btnGap if rowButton{ id = '##exit', label = 'Exit game', icon = ui.Icons.Exit, x = pad, y = cy, w = btnW, h = UI.btnH, dt = dt, } then ac.shutdownAssettoCorsa() end cy = cy + UI.btnH + UI.btnGap + 8 -- ── SETTINGS ───────────────────────── ui.pushFont(ui.Font.Small) ui.setCursor(vec2(pad, cy)) ui.textColored('SETTINGS', Theme.textGhost) ui.popFont() cy = cy + 18 local toggleW = (btnW - 8) / 2 local toggleH = 32 if togglePill('##setHud', Settings.hudEnabled and 'HUD: ON' or 'HUD: off', pad, cy, toggleW, toggleH, Settings.hudEnabled, accent) then Settings.hudEnabled = not Settings.hudEnabled end if togglePill('##setCompact', Settings.compactMode and 'Compact: ON' or 'Compact: off', pad + toggleW + 8, cy, toggleW, toggleH, Settings.compactMode, accent) then Settings.compactMode = not Settings.compactMode end cy = cy + toggleH + 10 local pillGap = 6 local pillW = (btnW - pillGap * 3) / 4 local pillH = 30 local activePos = safeHudPosition() for i, name in ipairs(POSITION_NAMES) do local px = pad + (i - 1) * (pillW + pillGap) if togglePill('##pos' .. i, name, px, cy, pillW, pillH, activePos == i, accent) then Settings.hudPosition = i end end cy = cy + pillH -- ── Footer ───────────────────────── drawGlowStrip(0, cardH - 2, cardW, rgbm(accent.r, accent.g, accent.b, 0.5)) ui.pushFont(ui.Font.Small) ui.setCursor(vec2(pad, cardH - 26)) ui.textColored('swim>', Theme.textMuted) local stateText = 'online' local sw = ui.measureText(stateText).x ui.setCursor(vec2(cardW - pad - sw - 16, cardH - 26)) ui.textColored('●', Theme.success) ui.sameLine(0, 4) ui.textColored(stateText, Theme.textMuted) ui.popFont() ui.endTransparentWindow() end -------------- -- Server browser card local function drawServerRow(s, x, y, w, h, dt, accent) local id = '##srv_' .. (s.id ~= '' and s.id or s.name) ui.setCursor(vec2(x, y)) ui.invisibleButton(id, vec2(w - 80, h)) local hov = ui.itemHovered() hover[id] = smooth(hover[id] or 0, hov and 1 or 0, 14, dt) local hv = hover[id] local bg = lerpColor(Theme.glassSurface, Theme.glassSurfaceHi, hv) local edge = lerpColor(Theme.glassEdge, Theme.accentSubtle, hv) drawPanel(x, y, w, h, UI.innerRadius, bg, edge, 1) if hv > 0.01 then local barCol = rgbm(Theme.accent.r, Theme.accent.g, Theme.accent.b, hv) ui.drawRectFilled(vec2(x, y + 8), vec2(x + 3, y + h - 8), barCol) end ui.pushFont(ui.Font.Main) ui.setCursor(vec2(x + 14, y + 6)) ui.textColored(s.name, hv > 0.01 and Theme.textHero or Theme.textPrimary) ui.popFont() ui.pushFont(ui.Font.Small) local parts = {} if s.group ~= '' then parts[#parts + 1] = s.group end if s.description ~= '' then parts[#parts + 1] = s.description end if s.passworded then parts[#parts + 1] = 'password' end local subtitle = #parts > 0 and table.concat(parts, ' · ') or 'no info' subtitle = truncate(subtitle, 48) ui.setCursor(vec2(x + 14, y + h - 18)) ui.textColored(subtitle, Theme.textMuted) ui.popFont() ui.pushFont(ui.Font.Small) local pc = string.format('%d/%d', s.players, s.maxPlayers) local pcW = ui.measureText(pc).x local rightX = x + w - 14 - 72 local pcCol = s.players > 0 and Theme.accent or Theme.textSecondary ui.setCursor(vec2(rightX - pcW - 8, y + 8)) ui.textColored('●', pcCol) ui.sameLine(0, 4) ui.textColored(pc, pcCol) ui.popFont() local btnW, btnH = 64, 28 local btnX = x + w - btnW - 10 local btnY = y + (h - btnH) / 2 local btnId = id .. '_join' ui.setCursor(vec2(btnX, btnY)) local joinClicked = ui.invisibleButton(btnId, vec2(btnW, btnH)) local jhov = ui.itemHovered() hover[btnId] = smooth(hover[btnId] or 0, jhov and 1 or 0, 16, dt) local jhv = hover[btnId] local joinBg = lerpColor(Theme.accentSubtle, Theme.accent, jhv) local joinEdge = lerpColor(Theme.accent, Theme.accentBright, jhv) drawPanel(btnX, btnY, btnW, btnH, 8, joinBg, joinEdge, 1) ui.pushFont(ui.Font.Small) local joinLabel = 'JOIN' local jw = ui.measureText(joinLabel).x local jcol = jhv > 0.5 and Theme.textHero or Theme.accent ui.setCursor(vec2(btnX + (btnW - jw) / 2, btnY + (btnH - 14) / 2)) ui.textColored(joinLabel, jcol) ui.popFont() return joinClicked end local function drawSpinner(x, y, size, color, t) local cx, cy = x + size / 2, y + size / 2 local r = size / 2 - 2 for i = 0, 7 do local a = (i / 8) * math.pi * 2 local fade = 0.2 + 0.8 * ((math.sin(t * 4 - i * 0.6) + 1) / 2) ui.drawCircleFilled( vec2(cx + math.cos(a) * r, cy + math.sin(a) * r), 2, rgbm(color.r, color.g, color.b, color.mult * fade), 8) end end local function drawServerBrowser(dt, cardX, cardY) Browser.spinPhase = Browser.spinPhase + dt local pulse = 0.92 + math.sin(UI.pulsePhase) * 0.08 local accent = rgbm(Theme.accent.r * pulse, Theme.accent.g * pulse, Theme.accent.b * pulse, 1) local cardW, cardH = UI.serverCardW, UI.serverCardH ui.beginTransparentWindow('swimServerBrowser', vec2(cardX, cardY), vec2(cardW, cardH), true, true) local pad = UI.pad drawPanel(-2, -2, cardW + 4, cardH + 4, UI.radius + 2, nil, Theme.accentGlow, 1) drawPanel(0, 0, cardW, cardH, UI.radius, Theme.glass, Theme.glassEdge, 1) ui.pushFont(ui.Font.Huge) local brandSize = ui.measureText('Servers') ui.popFont() local brandY = 16 local headerBandH = brandY + brandSize.y + 14 drawVerticalGradient(0, 0, cardW, headerBandH, Theme.glassSheen, Theme.glassSheenLo, UI.radius, ui.CornerFlags.Top) drawGlowStrip(0, 0, cardW, accent) ui.pushFont(ui.Font.Huge) ui.setCursor(vec2(pad, brandY)) ui.textColored('Servers', Theme.textHero) ui.popFont() -- Refresh button (Reset icon) do local sz = 28 local iconSz = 18 local cx = cardW - sz - 12 local cyBtn = brandY + (brandSize.y - sz) / 2 ui.setCursor(vec2(cx, cyBtn)) if ui.invisibleButton('##swimSrvRefresh', vec2(sz, sz)) then fetchServers() end local hov = ui.itemHovered() local col = hov and Theme.textHero or Theme.textSecondary ui.setCursor(vec2(cx + (sz - iconSz) / 2, cyBtn + (sz - iconSz) / 2)) ui.icon(ui.Icons.Reset, iconSz, col) end local cy = headerBandH ui.drawRectFilled(vec2(pad, cy), vec2(cardW - pad, cy + 1), Theme.glassEdge) cy = cy + 16 ui.pushFont(ui.Font.Small) ui.setCursor(vec2(pad, cy)) if Browser.state == 'ready' then ui.textColored(string.format('SERVERS · %d', #Browser.servers), Theme.textGhost) else ui.textColored('SERVERS', Theme.textGhost) end ui.popFont() cy = cy + 18 local listX = pad local listY = cy local listW = cardW - pad * 2 local listH = cardH - listY - 40 drawScrollableList('##swimSrvList', listX, listY, listW, listH, accent, function(innerW, innerH, scrollW) if Browser.state == 'loading' or Browser.state == 'idle' then local label = 'fetching servers' ui.pushFont(ui.Font.Small) local lw = ui.measureText(label).x ui.popFont() local cx0 = (innerW - (16 + 8 + lw)) / 2 local cy0 = innerH / 2 - 12 drawSpinner(cx0, cy0, 16, accent, Browser.spinPhase) ui.pushFont(ui.Font.Small) ui.setCursor(vec2(cx0 + 24, cy0 + 1)) ui.textColored(label, Theme.textSecondary) ui.popFont() elseif Browser.state == 'error' then ui.pushFont(ui.Font.Small) ui.setCursor(vec2(8, 12)) ui.textColored('Failed to load servers', Theme.error) ui.setCursor(vec2(8, 30)) ui.textColored(Browser.error or '', Theme.textMuted) ui.popFont() elseif #Browser.servers == 0 then ui.pushFont(ui.Font.Small) ui.setCursor(vec2(8, 12)) ui.textColored('No active servers right now.', Theme.textMuted) ui.popFont() else local rowH = 56 local rowGap = 6 local rowY = 0 local rowW = innerW - scrollW - 6 for _, s in ipairs(Browser.servers) do if drawServerRow(s, 0, rowY, rowW, rowH, dt, accent) then requestJoin(s) end rowY = rowY + rowH + rowGap end ui.setCursor(vec2(0, rowY)) ui.invisibleButton('##swimSrvBottom', vec2(1, 1)) end end) -- Footer drawGlowStrip(0, cardH - 2, cardW, rgbm(accent.r, accent.g, accent.b, 0.5)) ui.pushFont(ui.Font.Small) ui.setCursor(vec2(pad, cardH - 26)) local footL if Browser.state == 'ready' then footL = string.format('swim> · %d servers', #Browser.servers) elseif Browser.state == 'loading' then footL = 'swim> · loading...' else footL = 'swim> · ' .. (Browser.error and 'error' or 'idle') end ui.textColored(footL, Theme.textMuted) if Browser.lastFetch > 0 then -- tonumber forces the int64 cdata that sim.systemTime returns down to -- a plain Lua number so string.format('%d', ...) doesn't choke. local age = math.max(0, tonumber(sim.systemTime - Browser.lastFetch) or 0) local ageStr if age < 60 then ageStr = string.format('%ds ago', age) elseif age < 3600 then ageStr = string.format('%dm ago', math.floor(age / 60)) else ageStr = string.format('%dh ago', math.floor(age / 3600)) end local aw = ui.measureText(ageStr).x ui.setCursor(vec2(cardW - pad - aw, cardH - 26)) ui.textColored(ageStr, Theme.textMuted) end ui.popFont() ui.endTransparentWindow() end -------------- -- User score card local function drawUserScoreCard(dt, cardX, cardY) local pulse = 0.92 + math.sin(UI.pulsePhase) * 0.08 local accent = rgbm(Theme.accent.r * pulse, Theme.accent.g * pulse, Theme.accent.b * pulse, 1) local cardW, cardH = UI.userCardW, UI.userCardH ui.beginTransparentWindow('swimUserScore', vec2(cardX, cardY), vec2(cardW, cardH), true, true) local pad = UI.pad drawPanel(-2, -2, cardW + 4, cardH + 4, UI.radius + 2, nil, Theme.accentGlow, 1) drawPanel(0, 0, cardW, cardH, UI.radius, Theme.glass, Theme.glassEdge, 1) ui.pushFont(ui.Font.Main) local titleSize = ui.measureText('Your score') ui.popFont() local headerBandH = 12 + titleSize.y + 10 drawVerticalGradient(0, 0, cardW, headerBandH, Theme.glassSheen, Theme.glassSheenLo, UI.radius, ui.CornerFlags.Top) drawGlowStrip(0, 0, cardW, accent) ui.pushFont(ui.Font.Main) ui.setCursor(vec2(pad, 12)) ui.textColored('Your score', Theme.textPrimary) ui.popFont() do local labels = { all = 'overall', map = 'on map', car = 'this car', both = 'combo' } local label = labels[lbFilter] or lbFilter ui.pushFont(ui.Font.Small) local lw = ui.measureText(label).x ui.setCursor(vec2(cardW - pad - lw, 16)) ui.textColored(label, Theme.textGhost) ui.popFont() end local cy = headerBandH + 12 local filtered = applyLbFilter(Leaderboard.entries) local rank, entry = findUserEntry(filtered) if Leaderboard.state == 'loading' or Leaderboard.state == 'idle' then ui.pushFont(ui.Font.Small) ui.setCursor(vec2(pad, cy)) ui.textColored('loading...', Theme.textMuted) ui.popFont() elseif Leaderboard.state == 'error' then ui.pushFont(ui.Font.Small) ui.setCursor(vec2(pad, cy)) ui.textColored('failed to load: ' .. (Leaderboard.error or ''), Theme.error) ui.popFont() elseif not entry then ui.pushFont(ui.Font.Main) ui.setCursor(vec2(pad, cy)) ui.textColored('No score yet', Theme.textMuted) ui.popFont() ui.pushFont(ui.Font.Small) ui.setCursor(vec2(pad, cy + 26)) ui.textColored('Score points to appear on the board', Theme.textGhost) ui.popFont() else local rankStr = '#' .. tostring(rank) ui.pushFont(ui.Font.Huge) local rw = ui.measureText(rankStr).x ui.setCursor(vec2(cardW - pad - rw, cy - 4)) ui.textColored(rankStr, accent) ui.popFont() local scoreStr = fmtScore(entry.score) ui.pushFont(ui.Font.Huge) ui.setCursor(vec2(pad, cy - 4)) ui.textColored(scoreStr, Theme.textHero) local sw = ui.measureText(scoreStr).x ui.popFont() ui.pushFont(ui.Font.Main) ui.setCursor(vec2(pad + sw + 6, cy + 8)) ui.textColored('pts', Theme.textMuted) ui.popFont() ui.pushFont(ui.Font.Small) ui.setCursor(vec2(pad, cy + 44)) local subtitle = truncate( (entry.car_name ~= '' and entry.car_name or entry.car) .. ' · ' .. (entry.track_name ~= '' and entry.track_name or entry.track), 54) ui.textColored(subtitle, Theme.textSecondary) ui.popFont() end ui.endTransparentWindow() end -------------- -- Leaderboard card local function drawLbRow(e, rank, x, y, w, h, accent) if type(e) ~= 'table' then return end local isYou = steamIdEqual(e.steamid, localSteamId) if isYou then local bg = rgbm(Theme.accent.r, Theme.accent.g, Theme.accent.b, 0.10) drawPanel(x, y, w, h, 6, bg, nil) end ui.pushFont(ui.Font.Main) local rankStr = '#' .. tostring(rank) local rankCol = isYou and accent or (rank <= 3 and Theme.textHero or Theme.textMuted) ui.setCursor(vec2(x + 8, y + (h - 18) / 2)) ui.textColored(rankStr, rankCol) ui.popFont() local name = e.username if type(name) ~= 'string' or name == '' then name = e.player_name end if type(name) ~= 'string' or name == '' then name = 'Unknown' end name = truncate(name, 16) ui.pushFont(ui.Font.Main) ui.setCursor(vec2(x + 50, y + (h - 18) / 2)) ui.textColored(name, isYou and Theme.textHero or Theme.textPrimary) ui.popFont() local scoreStr = fmtScore(e.score) ui.pushFont(ui.Font.Main) local sw = ui.measureText(scoreStr).x ui.setCursor(vec2(x + w - sw - 10, y + (h - 18) / 2)) ui.textColored(scoreStr, isYou and accent or Theme.textPrimary) ui.popFont() end local function drawLeaderboardCard(dt, cardX, cardY) local pulse = 0.92 + math.sin(UI.pulsePhase) * 0.08 local accent = rgbm(Theme.accent.r * pulse, Theme.accent.g * pulse, Theme.accent.b * pulse, 1) local cardW, cardH = UI.lbCardW, UI.lbCardH ui.beginTransparentWindow('swimLeaderboard', vec2(cardX, cardY), vec2(cardW, cardH), true, true) local pad = UI.pad drawPanel(-2, -2, cardW + 4, cardH + 4, UI.radius + 2, nil, Theme.accentGlow, 1) drawPanel(0, 0, cardW, cardH, UI.radius, Theme.glass, Theme.glassEdge, 1) ui.pushFont(ui.Font.Huge) local brandSize = ui.measureText('Leaderboard') ui.popFont() local brandY = 16 local headerBandH = brandY + brandSize.y + 14 drawVerticalGradient(0, 0, cardW, headerBandH, Theme.glassSheen, Theme.glassSheenLo, UI.radius, ui.CornerFlags.Top) drawGlowStrip(0, 0, cardW, accent) ui.pushFont(ui.Font.Huge) ui.setCursor(vec2(pad, brandY)) ui.textColored('Leaderboard', Theme.textHero) ui.popFont() do local sz = 28 local iconSz = 18 local cx = cardW - sz - 12 local cyBtn = brandY + (brandSize.y - sz) / 2 ui.setCursor(vec2(cx, cyBtn)) if ui.invisibleButton('##swimLbRefresh', vec2(sz, sz)) then fetchLeaderboard() end local hov = ui.itemHovered() local col = hov and Theme.textHero or Theme.textSecondary ui.setCursor(vec2(cx + (sz - iconSz) / 2, cyBtn + (sz - iconSz) / 2)) ui.icon(ui.Icons.Reset, iconSz, col) end local cy = headerBandH ui.drawRectFilled(vec2(pad, cy), vec2(cardW - pad, cy + 1), Theme.glassEdge) cy = cy + 14 local filterDefs = { { key = 'all', label = 'All' }, { key = 'map', label = 'Map' }, { key = 'car', label = 'Car' }, { key = 'both', label = 'Both' }, } local filterGap = 6 local filterW = (cardW - pad * 2 - filterGap * 3) / 4 local filterH = 30 for i, f in ipairs(filterDefs) do local fx = pad + (i - 1) * (filterW + filterGap) if togglePill('##lbFilter_' .. f.key, f.label, fx, cy, filterW, filterH, lbFilter == f.key, accent) then lbFilter = f.key end end cy = cy + filterH + 12 ui.pushFont(ui.Font.Small) ui.setCursor(vec2(pad, cy)) local filtered = applyLbFilter(Leaderboard.entries) ui.textColored(string.format('TOP 10 · %d total', #filtered), Theme.textGhost) ui.popFont() cy = cy + 18 local listX = pad local listY = cy local listW = cardW - pad * 2 local listH = cardH - listY - 56 -- reserve room for the swimserver.com note drawScrollableList('##swimLbList', listX, listY, listW, listH, accent, function(innerW, innerH, scrollW) if Leaderboard.state == 'loading' or Leaderboard.state == 'idle' then local label = 'loading leaderboard' ui.pushFont(ui.Font.Small) local lw = ui.measureText(label).x ui.popFont() local cx0 = (innerW - (16 + 8 + lw)) / 2 local cy0 = innerH / 2 - 12 drawSpinner(cx0, cy0, 16, accent, Browser.spinPhase) ui.pushFont(ui.Font.Small) ui.setCursor(vec2(cx0 + 24, cy0 + 1)) ui.textColored(label, Theme.textSecondary) ui.popFont() elseif Leaderboard.state == 'error' then ui.pushFont(ui.Font.Small) ui.setCursor(vec2(8, 12)) ui.textColored('Failed to load', Theme.error) ui.setCursor(vec2(8, 30)) ui.textColored(Leaderboard.error or '', Theme.textMuted) ui.popFont() elseif #filtered == 0 then ui.pushFont(ui.Font.Small) ui.setCursor(vec2(8, 12)) ui.textColored('No entries for this filter', Theme.textMuted) ui.popFont() else local rowH = 32 local rowGap = 4 local rowW = innerW - scrollW - 6 local rowY = 0 for i = 1, 10 do local e = filtered[i] if e ~= nil then pcall(drawLbRow, e, i, 0, rowY, rowW, rowH, accent) end rowY = rowY + rowH + rowGap end ui.setCursor(vec2(0, rowY)) ui.invisibleButton('##swimLbBottom', vec2(1, 1)) end end) ui.pushFont(ui.Font.Small) ui.setCursor(vec2(pad, cardH - 42)) ui.textColored('Create an account at swimserver.com', Theme.textSecondary) ui.setCursor(vec2(pad, cardH - 26)) ui.textColored('and link your Steam to appear on the board.', Theme.textMuted) ui.popFont() ui.endTransparentWindow() end -------------- -- Confirm-join modal local function drawConfirmModal(dt) if not Confirm then return end local s = Confirm.server local uiState = ac.getUI() local sw, sh = uiState.windowSize.x, uiState.windowSize.y ui.drawRectFilled(vec2(0, 0), vec2(sw, sh), rgbm(0, 0, 0, 0.45)) local cardW, cardH = 460, 260 local cardX = math.floor((sw - cardW) / 2) local cardY = math.floor((sh - cardH) / 2) ui.beginTransparentWindow('swimConfirmJoin', vec2(cardX, cardY), vec2(cardW, cardH), true, true) ui.bringWindowToFront() local pad = UI.pad local pulse = 0.92 + math.sin(UI.pulsePhase) * 0.08 local accent = rgbm(Theme.accent.r * pulse, Theme.accent.g * pulse, Theme.accent.b * pulse, 1) drawPanel(-2, -2, cardW + 4, cardH + 4, UI.radius + 2, nil, Theme.accentGlow, 1) drawPanel(0, 0, cardW, cardH, UI.radius, Theme.glass, Theme.glassEdge, 1) ui.pushFont(ui.Font.Main) local titleSize = ui.measureText('Confirm join') ui.popFont() local headerBandH = 16 + titleSize.y + 14 drawVerticalGradient(0, 0, cardW, headerBandH, Theme.glassSheen, Theme.glassSheenLo, UI.radius, ui.CornerFlags.Top) drawGlowStrip(0, 0, cardW, accent) ui.pushFont(ui.Font.Main) ui.setCursor(vec2(pad, 16)) ui.textColored('Confirm join', Theme.textPrimary) ui.popFont() local cy = headerBandH ui.drawRectFilled(vec2(pad, cy), vec2(cardW - pad, cy + 1), Theme.glassEdge) cy = cy + 18 ui.pushFont(ui.Font.Huge) ui.setCursor(vec2(pad, cy)) ui.textColored(truncate(s.name, 28), accent) local nameH = ui.measureText('A').y ui.popFont() cy = cy + nameH + 4 ui.pushFont(ui.Font.Small) ui.setCursor(vec2(pad, cy)) local meta = string.format('%s · %d/%d', s.group ~= '' and s.group or 'unknown', s.players, s.maxPlayers) ui.textColored(meta, Theme.textSecondary) ui.popFont() cy = cy + 18 if s.description ~= '' then ui.pushFont(ui.Font.Small) ui.setCursor(vec2(pad, cy)) ui.textColored(truncate(s.description, 64), Theme.textMuted) ui.popFont() cy = cy + 18 end ui.pushFont(ui.Font.Small) ui.setCursor(vec2(pad, cy + 6)) ui.textColored('Assetto Corsa will restart to connect.', Theme.textGhost) ui.popFont() local btnH = 40 local gap = 10 local btnW = (cardW - pad * 2 - gap) / 2 local btnY = cardH - btnH - pad do ui.setCursor(vec2(pad, btnY)) local clicked = ui.invisibleButton('##swimConfirmCancel', vec2(btnW, btnH)) local hov = ui.itemHovered() hover['##swimConfirmCancel'] = smooth(hover['##swimConfirmCancel'] or 0, hov and 1 or 0, 14, dt) local hv = hover['##swimConfirmCancel'] local bg = lerpColor(Theme.glassSurface, Theme.glassSurfaceHi, hv) drawPanel(pad, btnY, btnW, btnH, UI.innerRadius, bg, Theme.glassEdge, 1) ui.pushFont(ui.Font.Main) local lw = ui.measureText('Cancel').x ui.setCursor(vec2(pad + (btnW - lw) / 2, btnY + (btnH - 18) / 2)) ui.textColored('Cancel', hv > 0.5 and Theme.textHero or Theme.textPrimary) ui.popFont() if clicked then cancelJoin() end end do local jx = pad + btnW + gap ui.setCursor(vec2(jx, btnY)) local clicked = ui.invisibleButton('##swimConfirmOk', vec2(btnW, btnH)) local hov = ui.itemHovered() hover['##swimConfirmOk'] = smooth(hover['##swimConfirmOk'] or 0, hov and 1 or 0, 16, dt) local hv = hover['##swimConfirmOk'] local bg = lerpColor(Theme.accentSubtle, Theme.accent, hv) local edge = lerpColor(Theme.accent, Theme.accentBright, hv) drawPanel(jx, btnY, btnW, btnH, UI.innerRadius, bg, edge, 1) ui.pushFont(ui.Font.Main) local lw = ui.measureText('Join').x ui.setCursor(vec2(jx + (btnW - lw) / 2, btnY + (btnH - 18) / 2)) ui.textColored('Join', hv > 0.5 and Theme.textHero or accent) ui.popFont() if clicked then confirmJoin() end end ui.endTransparentWindow() end -------------- -- Connecting overlay local function drawConnectingOverlay(dt) if not Joining then return end if Joining.state ~= 'resolving' and Joining.state ~= 'connecting' and Joining.state ~= 'error' then return end local uiState = ac.getUI() local sw, sh = uiState.windowSize.x, uiState.windowSize.y ui.drawRectFilled(vec2(0, 0), vec2(sw, sh), rgbm(0, 0, 0, 0.55)) local cardW, cardH = 360, 140 local cardX = math.floor((sw - cardW) / 2) local cardY = math.floor((sh - cardH) / 2) ui.beginTransparentWindow('swimConnecting', vec2(cardX, cardY), vec2(cardW, cardH), true, true) ui.bringWindowToFront() local pulse = 0.92 + math.sin(UI.pulsePhase) * 0.08 local accent = rgbm(Theme.accent.r * pulse, Theme.accent.g * pulse, Theme.accent.b * pulse, 1) drawPanel(-2, -2, cardW + 4, cardH + 4, UI.radius + 2, nil, Theme.accentGlow, 1) drawPanel(0, 0, cardW, cardH, UI.radius, Theme.glass, Theme.glassEdge, 1) Browser.spinPhase = Browser.spinPhase + dt drawSpinner(28, 28, 18, accent, Browser.spinPhase) ui.pushFont(ui.Font.Main) ui.setCursor(vec2(60, 24)) if Joining.state == 'connecting' then ui.textColored('Launching Content Manager...', Theme.textPrimary) else ui.textColored('Connection failed', Theme.error) end ui.popFont() ui.pushFont(ui.Font.Small) ui.setCursor(vec2(60, 52)) ui.textColored(truncate(Joining.server.name, 32), Theme.textSecondary) ui.popFont() if Joining.state == 'error' then ui.pushFont(ui.Font.Small) ui.setCursor(vec2(28, 80)) ui.textColored(Joining.error or '', Theme.textMuted) ui.popFont() local btnW, btnH = 80, 28 local btnX = cardW - btnW - 16 local btnY = cardH - btnH - 12 ui.setCursor(vec2(btnX, btnY)) if ui.invisibleButton('##swimJoinDismiss', vec2(btnW, btnH)) then Joining = nil end local hov = ui.itemHovered() drawPanel(btnX, btnY, btnW, btnH, 8, hov and Theme.glassSurfaceHi or Theme.glassSurface, Theme.glassEdge, 1) ui.pushFont(ui.Font.Small) local lw = ui.measureText('Dismiss').x ui.setCursor(vec2(btnX + (btnW - lw) / 2, btnY + (btnH - 14) / 2)) ui.textColored('Dismiss', hov and Theme.textHero or Theme.textPrimary) ui.popFont() end ui.endTransparentWindow() end -------------- -- Hook: replace AC's pause UI local lastFrameTime = ui.time() ui.onExclusiveHUD(function(mode) -- Always block AC from consuming ESC. We handle ESC ourselves so the user -- can summon the overlay while driving. ac.blockEscapeButton() if ui.keyboardButtonPressed(ui.KeyIndex.Escape, false) then if useNativeMenu then useNativeMenu = false ac.tryToPause(false) else menuOpen = not menuOpen end end if useNativeMenu then if mode ~= 'pause' then useNativeMenu = false end return end if not menuOpen then return end local now = ui.time() local dt = math.max(0, math.min(0.1, now - lastFrameTime)) lastFrameTime = now UI.pulsePhase = (UI.pulsePhase + dt * 1.5) % (math.pi * 2) local nowSec = tonumber(sim.systemTime) or 0 if Browser.state == 'idle' or (Browser.state == 'ready' and Browser.lastFetch > 0 and nowSec - Browser.lastFetch > 120) then fetchServers() end if Leaderboard.state == 'idle' or (Leaderboard.state == 'ready' and Leaderboard.lastFetch > 0 and nowSec - Leaderboard.lastFetch > 120) then fetchLeaderboard() end local uiState = ac.getUI() local sw, sh = uiState.windowSize.x, uiState.windowSize.y ui.drawRectFilled(vec2(0, 0), vec2(sw, sh), Theme.backdrop) -- 3-column layout: pause+user / server / leaderboard local col1H = UI.cardH + UI.cardGap + UI.userCardH local groupW = UI.cardW + UI.cardGap + UI.serverCardW + UI.cardGap + UI.lbCardW local groupH = math.max(col1H, UI.serverCardH, UI.lbCardH) local groupX = math.floor((sw - groupW) / 2) local groupY = math.floor((sh - groupH) / 2) local col1X = groupX drawCustomPauseMenu(dt, col1X, groupY) drawUserScoreCard(dt, col1X, groupY + UI.cardH + UI.cardGap) local col2X = groupX + UI.cardW + UI.cardGap drawServerBrowser(dt, col2X, groupY) local col3X = col2X + UI.serverCardW + UI.cardGap drawLeaderboardCard(dt, col3X, groupY) drawConfirmModal(dt) drawConnectingOverlay(dt) return true end) ac.log('swim> Pause Menu (online) loaded')