script_name('Znakomec ID Scanner') script_author('Codex') script_version('3.0.0') script_properties('work-in-pause') require 'lib.moonloader' local sampev = require 'lib.samp.events' local vkeys = require 'vkeys' -- Controls and scan settings. local PANEL_KEY = vkeys.VK_F10 local SCAN_KEY = vkeys.VK_F11 local REQUEST_DELAY_MS = 1400 local MAX_PLAYER_ID = 1004 local RPC_SET_PLAYER_NAME = 11 local scanning = false local panelVisible = false local pendingId = -1 local realNames = {} local scanChecked = 0 local scanLearned = 0 local lastId = -1 local panelFont = nil local lastRefreshAt = 0 local function chat(message, color) sampAddChatMessage('[Znakomec] ' .. message, color or 0x66CCFFFF) end local function countKnownNames() local count = 0 for _ in pairs(realNames) do count = count + 1 end return count end -- Locally feed SA-MP the same SetPlayerName RPC it normally receives from a -- server. Nothing is sent to Gambit and FakePlayer.sf is not required. local function setPlayerNameLocal(playerId, desiredNick) if not sampIsPlayerConnected(playerId) then return false end if sampGetPlayerNickname(playerId) == desiredNick then return true end local bs = raknetNewBitStream() raknetBitStreamWriteInt16(bs, playerId) raknetBitStreamWriteInt8(bs, #desiredNick) raknetBitStreamWriteString(bs, desiredNick) raknetBitStreamWriteInt8(bs, 1) raknetEmulRpcReceiveBitStream(RPC_SET_PLAYER_NAME, bs) raknetDeleteBitStream(bs) return sampGetPlayerNickname(playerId) == desiredNick end local function applyKnownName(playerId, realName) local firstName, lastName = realName:match('^(%S+)%s+(%S+)$') if not firstName then return false end return setPlayerNameLocal(playerId, firstName .. '_' .. lastName) end local function refreshKnownNicknames() for playerId, realName in pairs(realNames) do if sampIsPlayerConnected(playerId) then applyKnownName(playerId, realName) else -- An ID may later be occupied by somebody else. Never retain a -- learned name after that slot disconnects. realNames[playerId] = nil end end end -- Gambit /id response example: Malcolm Harris [21] - ... function sampev.onServerMessage(color, text) if pendingId < 0 then return end local cleanText = text:gsub('{%x+}', '') local firstName, lastName, idText = cleanText:match( '^%s*([%a][%w%-]*)%s+([%a][%w%-]*)%s+%[(%d+)%]' ) local responseId = tonumber(idText) -- Exact pending ID comparison prevents Player_123 impostors or another -- player's number elsewhere in the line from being accepted. if firstName and lastName and responseId == pendingId then if firstName:lower() ~= 'player' and firstName:lower() ~= 'mask' then local wasKnown = realNames[responseId] ~= nil local realName = firstName .. ' ' .. lastName realNames[responseId] = realName applyKnownName(responseId, realName) if not wasKnown then scanLearned = scanLearned + 1 end end end end local function findLastConnectedId() local result = -1 for playerId = 0, MAX_PLAYER_ID do if sampIsPlayerConnected(playerId) then result = playerId end end return result end local function toggleScan() if scanning then scanning = false pendingId = -1 chat('Stopping after the current request.', 0xFFCC66FF) return end lastId = findLastConnectedId() if lastId < 0 then chat('No connected players found.', 0xFF6666FF) return end scanning = true scanChecked = 0 scanLearned = 0 lua_thread.create(function() chat(string.format('Scanning connected IDs 0-%d. F11 or /znakomec stops it.', lastId)) for playerId = 0, lastId do if not scanning then break end if sampIsPlayerConnected(playerId) then pendingId = playerId sampSendChat(string.format('/id %d', playerId)) scanChecked = scanChecked + 1 wait(REQUEST_DELAY_MS) pendingId = -1 end end local completed = scanning scanning = false pendingId = -1 if completed then chat(string.format('Done. Checked: %d, names found: %d.', scanChecked, scanLearned), 0x66FF99FF) else chat(string.format('Stopped. Checked: %d.', scanChecked), 0xFFCC66FF) end end) end local function drawPanel() if not panelVisible or not panelFont then return end local x, y, width, height = 25, 190, 330, 112 renderDrawBox(x, y, width, height, 0xD0181B22) renderDrawBox(x, y, width, 3, scanning and 0xFF55DD88 or 0xFF58A6FF) local status = scanning and 'SCANNING' or 'READY' local progress = scanning and string.format('%d / ID %d', scanChecked, lastId) or '-' local text = string.format( 'ZNAKOMEC 3.0 [%s]\nF10 panel on/off\nF11 scan start/stop\nKnown: %d Progress: %s', status, countKnownNames(), progress ) renderFontDrawText(panelFont, text, x + 12, y + 10, 0xFFFFFFFF) end function main() repeat wait(100) until isSampAvailable() panelFont = renderCreateFont('Arial', 10, 5) sampRegisterChatCommand('znakomec', toggleScan) chat('Loaded. F10: panel, F11: scan, /znakomec: scan.') while true do wait(0) if not sampIsChatInputActive() and not sampIsDialogActive() then if isKeyJustPressed(PANEL_KEY) then panelVisible = not panelVisible end if isKeyJustPressed(SCAN_KEY) then toggleScan() end end drawPanel() -- Keep learned names applied if Gambit refreshes a player slot. local now = os.clock() if now - lastRefreshAt >= 1 then lastRefreshAt = now refreshKnownNicknames() end end end