-- Custom Rectangular Nameplate for Assetto Corsa
-- Shows driver name, country flag, ping, and SERVER-BASED input mode
local driverData = {}
local serverInputModes = {} -- Store server input mode data
local steamAvatarUrls = {} -- Store Steam avatar URLs
local lastBroadcastReceived = 0
-- Online event to receive input mode broadcasts from server
-- CRITICAL: Must specify the exact key that matches C# packet
local inputModeBroadcastEvent = ac.OnlineEvent({
ac.StructItem.key("input_modes_broadcast"), -- MUST match C# packet key exactly
data = ac.StructItem.string(512)
}, function(sender, message)
if message and message.data and message.data ~= "" then
ac.log("Nameplate: Received server input data: " .. message.data)
lastBroadcastReceived = os.clock()
-- Clear and parse new data
serverInputModes = {}
-- Parse format: "sessionId:inputMode;sessionId:inputMode;..."
for sessionData in string.gmatch(message.data, "([^;]+)") do
local sessionId, inputMode = string.match(sessionData, "(%d+):(%w+)")
if sessionId and inputMode then
local sessionIdNum = tonumber(sessionId)
serverInputModes[sessionIdNum] = inputMode
ac.log("Nameplate: Server data - Session " .. sessionId .. " = " .. inputMode)
end
end
end
end)
-- Online event to receive Steam avatar URLs from server
local steamAvatarBroadcastEvent = ac.OnlineEvent({
ac.StructItem.key("steam_avatars_broadcast"), -- MUST match C# packet key exactly
data = ac.StructItem.string(1024) -- Increased size for URLs
}, function(sender, message)
if message and message.data and message.data ~= "" then
ac.log("Nameplate: Received Steam avatar URLs: " .. message.data)
-- Clear and parse new data
steamAvatarUrls = {}
-- Parse format: "sessionId:avatarUrl;sessionId:avatarUrl;..."
for sessionData in string.gmatch(message.data, "([^;]+)") do
local sessionId, avatarUrl = string.match(sessionData, "(%d+):(.+)")
if sessionId and avatarUrl then
local sessionIdNum = tonumber(sessionId)
steamAvatarUrls[sessionIdNum] = avatarUrl
ac.log("Nameplate: Steam avatar - Session " .. sessionId .. " = " .. avatarUrl)
end
end
end
end)
-- Get input type from server data (preferred) or fallback to local detection
function getInputType(car)
if not car then return "unknown" end
-- Try to get session ID and use server data
local sessionId = car.sessionID
if sessionId and serverInputModes[sessionId] then
local serverInputMode = serverInputModes[sessionId]
ac.log("Nameplate: Using server data for session " .. sessionId .. ": " .. serverInputMode)
return serverInputMode
end
-- Fallback to local detection if no server data available
local index = car.index or car.carIndex or 0
local steer = car.steer or 0
local gas = car.gas or 0
local brake = car.brake or 0
local currentType = "gamepad"
local steerAbs = math.abs(steer)
if (steer == 0 or steerAbs > 0.95) and (gas == 0 or gas > 0.95) and (brake == 0 or brake > 0.95) then
currentType = "keyboard"
end
if steerAbs > 0.05 and steerAbs < 0.9 then
currentType = "wheel"
end
-- Store last 5 readings for each player
driverData[index] = driverData[index] or {}
driverData[index].inputTypeHistory = driverData[index].inputTypeHistory or {}
table.insert(driverData[index].inputTypeHistory, currentType)
if #driverData[index].inputTypeHistory > 5 then
table.remove(driverData[index].inputTypeHistory, 1)
end
-- Count most frequent
local count = { keyboard = 0, wheel = 0, gamepad = 0 }
for _, t in ipairs(driverData[index].inputTypeHistory) do
count[t] = (count[t] or 0) + 1
end
local detected = "gamepad"
local maxCount = 0
for k, v in pairs(count) do
if v > maxCount then
maxCount = v
detected = k
end
end
driverData[index].lastInputType = detected
return detected
end
function drawInputIcon(inputType, x, y, alpha)
if inputType == "wheel" then
-- Use steering wheel image - bigger size (36x36) and white color
local success = pcall(function()
ui.drawImage(
"https://i.imgur.com/OEENH81.png",
vec2(x, y),
vec2(x + 25, y + 25),
rgbm(1.0, 1.0, 1.0, alpha)
)
end)
if not success then
ac.log("Nameplate: Failed to draw wheel icon from URL.")
ui.dwriteDrawText("🏎", 26, vec2(x, y), rgbm(0.2, 1.0, 0.2, alpha), 1, ui.Alignment.Left, ui.Alignment.Top)
end
elseif inputType == "keyboard" then
ui.dwriteDrawText("⌨", 26, vec2(x, y), rgbm(0.2, 0.8, 1.0, alpha), 1, ui.Alignment.Left, ui.Alignment.Top)
elseif inputType == "gamepad" then
-- Use controller image - bigger size (36x36) and white color
local success = pcall(function()
ui.drawImage(
"https://i.ibb.co/3m56fyZy/gamepad.png",
vec2(x, y),
vec2(x + 36, y + 36),
rgbm(1.0, 1.0, 1.0, alpha)
)
end)
if not success then
ac.log("Nameplate: Failed to draw gamepad icon from URL.")
ui.dwriteDrawText("🎮", 26, vec2(x, y), rgbm(1.0, 0.8, 0.2, alpha), 1, ui.Alignment.Left, ui.Alignment.Top)
end
else
-- Unknown - show question mark
ui.dwriteDrawText("?", 26, vec2(x, y), rgbm(0.7, 0.7, 0.7, alpha), 1, ui.Alignment.Left, ui.Alignment.Top)
end
end
function drawCountryFlag(nationCode, x, y)
if nationCode and nationCode ~= "" then
-- Try to draw flag image
local success = pcall(function()
ui.drawImage(
"/content/gui/flags/" .. nationCode .. ".png",
vec2(x, y),
vec2(x + 24, y + 18),
rgbm(1, 1, 1, 1)
)
end)
-- If flag fails, show country code
if not success then
ui.dwriteDrawText(nationCode, 16, vec2(x, y + 2), rgbm(0.8, 0.8, 1.0, 1), 1, ui.Alignment.Left, ui.Alignment.Top)
end
end
end
function drawPingBars(x, y, ping, pingColor)
local barWidth = 6
local barHeight = 16
local barSpacing = 3
local maxBars = 4
local bars = 4
if ping > 200 then
bars = 1
elseif ping > 150 then
bars = 2
elseif ping > 100 then
bars = 3
end
for i = 1, maxBars do
local barX = x + (i - 1) * (barWidth + barSpacing)
local currentBarHeight = barHeight * (i / maxBars)
local currentY = y + barHeight - currentBarHeight
local alpha = (i <= bars) and 1.0 or 0.3
local color = rgbm(pingColor.r, pingColor.g, pingColor.b, alpha)
ui.drawRectFilled(vec2(barX, currentY), vec2(barX + barWidth, y + barHeight), color)
end
end
ui.onDriverNameTag(function(car)
-- Clean the driver name properly
local rawName = tostring(car.driverName or "unknown")
local name = rawName:gsub("0x[%x]+", ""):gsub("[^%w%s*%-%.%@]", "")
name = name:gsub("^%s*(.-)%s*$", "%1")
-- Skip AI traffic cars - only show input modes for real players
local isRealPlayer = car.isConnected and not car.isAIControlled
if not isRealPlayer then
-- For AI cars, just show name without input mode
ui.pushDWriteFont("Berlin Sans FB")
ui.drawRectFilled(vec2(-90, 0), vec2(470, 36), rgbm(0, 0, 0, 0.0), 8)
ui.dwriteDrawText(name, 21, vec2(-80, 6), rgbm(0.8, 0.8, 0.8, 1), 1, ui.Alignment.Left, ui.Alignment.Top)
ui.popDWriteFont()
return
end
local ping = car.ping or car.pingMs or 0
local pingColor = rgbm(0.2, 1.0, 0.2, 1.0)
if ping > 250 then
pingColor = rgbm(1.0, 0.2, 0.2, 1.0)
elseif ping > 120 then
pingColor = rgbm(1.0, 1.0, 0.2, 1.0)
end
-- Get nation code
local nationCode = ""
if car.driverNationCode and car.driverNationCode ~= "" then
nationCode = string.upper(tostring(car.driverNationCode))
elseif car.nationality and car.nationality ~= "" then
nationCode = string.upper(tostring(car.nationality))
else
nationCode = "ITA"
end
local speed = (car.speedKmh or 0)
local speedText = string.format("%d KM/H", speed)
ui.pushDWriteFont("Berlin Sans FB")
-- Main background - MADE TRANSPARENT to avoid overlap
ui.drawRectFilled(vec2(-90, 0), vec2(470, 36), rgbm(0, 0, 0, 0.0), 8)
-- Driver name (leftmost)
ui.dwriteDrawText(name, 21, vec2(-80, 6), rgbm(1,1,1,1), 1, ui.Alignment.Left, ui.Alignment.Top)
-- Layout: Name | Input | Flag | Ping Bars
local inputX = 322 -- Input icon position (moved further right to avoid name overlap)
local flagX = 320 -- Flag position (moved right to make room for input)
local barsX = 320 -- Ping bars position (moved right)
local barsY = 18
-- Input icon (moved further right and better vertical centering) - only for real players
local inputType = getInputType(car)
drawInputIcon(inputType, inputX, 35, 1.0) -- Lower vertical position for better centering
-- Country flag
drawCountryFlag(nationCode, flagX, 9)
-- Ping bars (rightmost)
drawPingBars(barsX, barsY, ping, pingColor)
-- رقم البنق يمين الشارة (نفس فكرة السكربت الثاني)
ui.dwriteDrawText(tostring(ping).."ms", 26, vec2(barsX + 40, 8), pingColor, 1, ui.Alignment.Left, ui.Alignment.Top)
-- Steam Avatar (right of ping)
local avatarUrl = steamAvatarUrls[car.sessionID]
if avatarUrl then
local avatarX = barsX + -321 -- Adjust X position relative to ping bars
local avatarY = 2 -- Adjust Y position to align with ping text
local avatarSize = 60 -- Adjust size as needed (e.g., 24x24)
local success = pcall(function()
ui.drawImage(
avatarUrl,
vec2(avatarX, avatarY),
vec2(avatarX + avatarSize, avatarY + avatarSize),
rgbm(1, 1, 1, 1)
)
end)
if not success then
ac.log("Nameplate: Failed to draw avatar from URL: " .. avatarUrl)
end
end
-- الشريط الأزرق للسرعة
local minSpeed, maxSpeed = 0, 260
local speedBoxX = -90
local speedBoxWMax = 560
local speedBoxH = 17
local speedBoxY = 46
local t = math.max(0, math.min(1, (speed-minSpeed)/(maxSpeed-minSpeed)))
local minWidth = 15
local speedBoxW = minWidth + (speedBoxWMax - minWidth) * t
ui.drawRectFilled(vec2(speedBoxX, speedBoxY), vec2(speedBoxX + speedBoxW, speedBoxY + speedBoxH), rgbm(0.1, 0.45, 0.95, 0.89), 7)
ui.dwriteDrawText(speedText, 18, vec2(speedBoxX + speedBoxWMax/2, speedBoxY - 1), rgbm(1,1,1,1), 1, ui.Alignment.Center, ui.Alignment.Top)
ui.popDWriteFont()
-- Debug info in console every few seconds - only for real players
if math.fmod(os.clock(), 5) < 0.1 then
local sessionId = car.sessionID or "unknown"
ac.log("Nameplate Debug: Player " .. name .. " - Session: " .. sessionId .. " - Input: " .. inputType .. " - Server data available: " .. tostring(serverInputModes[sessionId] ~= nil))
end
end)
-- Debug logging
ac.log("Nameplate: Enhanced script loaded with server input mode integration!")
-- Periodic debug info
local lastDebugTime = 0
function script.update(dt)
local currentTime = os.clock()
if currentTime - lastDebugTime > 10 then -- Every 10 seconds
lastDebugTime = currentTime
local serverDataCount = 0
for _ in pairs(serverInputModes) do
serverDataCount = serverDataCount + 1
end
ac.log("Nameplate: Server input modes tracked: " .. serverDataCount .. " | Last broadcast: " ..
(lastBroadcastReceived > 0 and string.format("%.1fs ago", currentTime - lastDebugTime) or "never"))
end
end