>_ NRL Script
Universal Volt · · 3 menit baca

Remote Spy Ringan dengan hookmetamethod (Tanpa Lag)

Bikin remote spy sendiri pakai hookmetamethod di __namecall. Ringan, nggak bikin FPS drop, dan bisa filter remote yang spam supaya log-nya kebaca.

Remote spy bawaan executor biasanya nge-log semua FireServer dan InvokeServer tanpa filter. Di game yang rame, satu remote heartbeat bisa nembak 60x per detik dan log kamu langsung nggak kebaca. Yang di bawah ini nge-hook __namecall sekali, punya rate limit per remote, dan bisa dimatiin bersih.

Kenapa __namecall, bukan hook per-remote#

FireServer dipanggil lewat titik dua (remote:FireServer(...)). Di balik layar Luau nerjemahin itu jadi lookup __namecall di metatable game. Artinya satu hook di situ nangkep semua remote sekaligus — termasuk remote yang dibikin setelah script kamu jalan.

Kalau kamu hook FireServer per-instance pakai hookfunction, kamu harus nge-loop semua remote yang ada sekarang, dan remote baru bakal lolos.

Skripnya#

```lua showLineNumbers --!strict -- Remote Spy — hookmetamethod pada __namecall -- Cleanup: panggil RemoteSpy.stop()

local RemoteSpy = {}

local THROTTLE_SECONDS = 1.0 -- maksimal 1 log per remote per detik local MAX_ARG_DEPTH = 2 -- kedalaman table yang di-serialize

local lastLogged: { [Instance]: number } = {} local ignored: { [string]: boolean } = {} local originalNamecall local active = false

-- Serialize argumen jadi string yang kebaca. Table dalam dibatasi kedalamannya -- supaya payload gede nggak nge-freeze thread waktu di-format. local function serialize(value: any, depth: number): string local t = typeof(value)

if t == "string" then return string.format("%q", #value > 120 and value:sub(1, 120) .. "..." or value) elseif t == "Instance" then return value:GetFullName() elseif t == "table" then if depth >= MAX_ARG_DEPTH then return "{...}" end local parts = {} for k, v in pairs(value) do table.insert(parts, string.format("[%s] = %s", tostring(k), serialize(v, depth + 1))) end return "{ " .. table.concat(parts, ", ") .. " }" end

return tostring(value) end

local function formatArgs(args: { any }, count: number): string local parts = table.create(count) for i = 1, count do parts[i] = serialize(args[i], 0) end return table.concat(parts, ", ") end

function RemoteSpy.ignore(name: string) ignored[name] = true end

function RemoteSpy.start() if active then rconsolewarn("RemoteSpy: sudah aktif, start() diabaikan") return end

originalNamecall = hookmetamethod(game, "__namecall", newcclosure(function(self, ...) local method = getnamecallmethod()

-- checkcaller: kalau panggilannya dari script kita sendiri, jangan di-log. -- Tanpa ini, kode kamu sendiri bakal muncul di log dan bisa rekursi. if not checkcaller() and (method == "FireServer" or method == "InvokeServer") then local remote = self :: Instance

if not ignored[remote.Name] then local now = os.clock() local last = lastLogged[remote] or 0

if now - last >= THROTTLE_SECONDS then lastLogged[remote] = now

local args = table.pack(...) rconsoleprint(string.format( "[%s] %s :%s(%s)\n", os.date("%H:%M:%S"), remote:GetFullName(), method, formatArgs(args, args.n) )) end end end

return originalNamecall(self, ...) end))

active = true rconsoleprint("RemoteSpy: aktif\n") end

function RemoteSpy.stop() if not active then return end

-- Kembalikan metamethod aslinya. Kalau ini dilewat, hook-nya nyangkut -- sampai kamu rejoin — dan hook kedua bakal numpuk di atasnya. hookmetamethod(game, "__namecall", originalNamecall)

table.clear(lastLogged) active = false rconsoleprint("RemoteSpy: berhenti, metamethod dikembalikan\n") end

rconsoleshow() RemoteSpy.start()

return RemoteSpy BLOCK0

Hasil#

Log-nya jadi sekitar 5–15 baris per menit di game rata-rata, bukan ribuan. Cukup buat mapping remote mana yang sebenarnya penting sebelum kamu mulai bedah payload-nya.