-- ============================================================ -- LOBBY GUARD · DUNGEON QUEST REBORN By Baltazar_exe -- ============================================================ local Config = { -- ── 1. YOUR ACCOUNTS (the important part) ──────────── -- -- USERNAMES of every account you play on (main + alts). Anyone -- who joins and ISN'T on this list is treated as an intruder. -- -- Must be the USERNAME (the @name on the profile), NOT the -- display name. If your profile shows "Cool Name" but the @ is -- "coolname_2010", put "coolname_2010". -- -- If a name is missing, that account joining your party closes -- your main. Case doesn't matter. Allow = { "PUT_YOUR_MAIN_USERNAME_HERE", "PUT_YOUR_ALT_USERNAME_HERE", -- add or remove lines to match how many accounts you have }, -- ── 2. DISCORD ALERT (optional) ───────────────────── -- -- Leave "" if you only want the game to close, no alert. -- To get a URL: your server > channel settings > Integrations > -- Webhooks > New Webhook > Copy URL. WebhookURL = "", -- Ping you in the message. Discord > Settings > Advanced > -- Developer Mode on > right-click your name > Copy ID. Numbers -- only. "" = no ping. PingRoleId pings a role instead. PingUserId = "", PingRoleId = "", -- ── 3. BEHAVIOUR ───────────────────────────────────── -- true = close the game on an intruder -- false = only send the alert, keep playing (for testing) CloseGame = true, GuardInLobby = false, -- only turn on if you're in your own private/VIP server GuardInDungeon = true, -- When detection isn't sure where you are (happens during load / -- mid-teleport). Off, because that moment usually means you're -- entering the full lobby, and closing then would be a mistake. GuardWhenUnknown = false, CloseDelay = 1.0, -- seconds after the alert before closing (lets the webhook actually send) RescanEvery = 1, -- seconds between player-list scans } -- ============================================================ -- Logic below. Nothing to edit. -- ============================================================ local Players = game:GetService("Players") local HttpService = game:GetService("HttpService") -- Can be nil on auto-exec (script runs before the client builds the -- player). Reassigned in waitReady() once it appears - no "local" -- there, it's this same variable. With it nil, "not you" would be -- true for YOU and the script would close the game every load. local LocalPlayer = Players.LocalPlayer if getgenv and getgenv().__LobbyGuardUnload then pcall(getgenv().__LobbyGuardUnload) end local conns = {} local fired = false -- already alerted local running = true -- script alive (kept separate so the scan doesn't stop after the first alert) local lastWhere = nil local function allowed(name) local want = string.lower(tostring(name)) for _, n in ipairs(Config.Allow) do if string.lower(tostring(n)) == want then return true end end return false end -- ── lobby or dungeon? ──────────────────────────────────── -- dungeon = the "dungeon" folder exists AND has "room 1"/"room 2"... -- children (the folder alone also shows as a lobby preview). -- lobby = the "games"/"vipServer"/"bossLobbies" hub folders. -- The folder blinks out for a moment on room transitions, so a 2s -- sticky window keeps it reading "dungeon" through the gap. local DUNGEON_STICKY = 2 local lastDungeonAt = 0 local function getDungeonFolder() local d = workspace:FindFirstChild("dungeon") or workspace:FindFirstChild("Dungeon") if d then return d end for _, ch in ipairs(workspace:GetChildren()) do if string.lower(ch.Name) == "dungeon" then return ch end end return nil end local function folderLooksLikeDungeon(d) if not (d and d.Parent) then return false end if d:FindFirstChild("room 1") or d:FindFirstChild("Room 1") or d:FindFirstChild("room1") then return true end for _, ch in ipairs(d:GetChildren()) do local n = string.lower(ch.Name) if n:match("^room%s*%d+") or n:match("^room%d+") then return true end end return false end local function lobbyHub() return workspace:FindFirstChild("games") ~= nil or workspace:FindFirstChild("vipServer") ~= nil or workspace:FindFirstChild("bossLobbies") ~= nil end -- "dungeon" | "lobby" | "?" local function whereAmI() local ok, res = pcall(function() if folderLooksLikeDungeon(getDungeonFolder()) then lastDungeonAt = os.clock() return "dungeon" end if lastDungeonAt > 0 and os.clock() - lastDungeonAt < DUNGEON_STICKY then return "dungeon" end if lobbyHub() then return "lobby" end return "?" end) return ok and res or "?" end local function guardActive() local w = whereAmI() if w == "dungeon" then return Config.GuardInDungeon ~= false, w end if w == "lobby" then return Config.GuardInLobby ~= false, w end return Config.GuardWhenUnknown ~= false, w end local function intruders() local list = {} if not LocalPlayer then return list end for _, p in ipairs(Players:GetPlayers()) do if p ~= LocalPlayer and not allowed(p.Name) then list[#list + 1] = p end end return list end -- Executor's request/http_request first: HttpService:PostAsync is -- blocked for off-game domains in most executors. HttpService is the -- fallback. local function sendWebhook(payload) local url = Config.WebhookURL if type(url) ~= "string" or url == "" then return false, "no URL" end local body = HttpService:JSONEncode(payload) local reqFn = (syn and syn.request) or (http and http.request) or http_request or request or (fluxus and fluxus.request) if reqFn then local ok, res = pcall(reqFn, { Url = url, Method = "POST", Headers = { ["Content-Type"] = "application/json" }, Body = body, }) if ok then return true end return false, tostring(res) end local ok, err = pcall(function() HttpService:PostAsync(url, body, Enum.HttpContentType.ApplicationJson) end) return ok, (not ok) and tostring(err) or nil end local function mentionText() if Config.PingRoleId ~= "" then return "<@&" .. Config.PingRoleId .. ">" end if Config.PingUserId ~= "" then return "<@" .. Config.PingUserId .. ">" end return "" end -- Webhook is sent BEFORE closing, and the call is blocking on -- purpose: closing first would kill the process with the request -- still queued. Closing is the last line. local function alert(who) if fired then return end fired = true local names = {} for _, p in ipairs(who) do names[#names + 1] = ("%s (%s · ID %d)"):format(p.DisplayName, p.Name, p.UserId) end if #names == 0 then names[1] = "?" end local jobId = tostring(game.JobId) if jobId == "" then jobId = "(local / studio)" end local content = mentionText() if content ~= "" then content = content .. " player joined your server" end local ok, err = sendWebhook({ content = content ~= "" and content or nil, embeds = { { title = "Player detected in the server", description = ("**%d** player(s) besides you:\n%s"):format(#who, table.concat(names, "\n")), color = 0xF0546C, fields = { { name = "Your account", value = LocalPlayer and ("%s (%s)"):format(LocalPlayer.DisplayName, LocalPlayer.Name) or "(unknown)", inline = true }, { name = "Players in server", value = tostring(#Players:GetPlayers()), inline = true }, { name = "Where", value = whereAmI(), inline = true }, { name = "PlaceId", value = tostring(game.PlaceId), inline = true }, { name = "JobId", value = jobId, inline = false }, { name = "Action", value = Config.CloseGame and "closing the game" or "alert only (CloseGame = false)", inline = false }, }, } }, }) if Config.WebhookURL == "" then print("[LobbyGuard] intruder:", table.concat(names, ", "), "(no webhook configured)") elseif ok then print("[LobbyGuard] webhook sent:", table.concat(names, ", ")) else warn("[LobbyGuard] webhook FAILED:", err, "- continuing anyway") end if not Config.CloseGame then task.delay(30, function() fired = false end) -- re-arm so it alerts again for the next one return end task.wait(Config.CloseDelay or 1) -- 1) most executors implement this as "kill the client" local shut = pcall(function() game:Shutdown() end) if shut then return end -- 2) always exists, but DISCONNECTS instead of closing the window pcall(function() if LocalPlayer then LocalPlayer:Kick("LobbyGuard: another player joined the server.") end end) end local function check() if fired then return end local on, w = guardActive() -- checked every scan: you move in and out of dungeons all run if not on then if lastWhere ~= w then lastWhere = w print("[LobbyGuard] in " .. w .. " - guard off here") end return end if lastWhere ~= w then lastWhere = w print("[LobbyGuard] in " .. w .. " - watching") end local who = intruders() if #who > 0 then alert(who) end end -- Nothing can arm before LocalPlayer exists - not even PlayerAdded: -- with it nil, "p == LocalPlayer" never matches and YOUR own join -- would fire the alert. local function waitReady() local t0 = os.clock() while not Players.LocalPlayer and os.clock() - t0 < 60 do task.wait(0.1) end LocalPlayer = Players.LocalPlayer if not LocalPlayer then warn("[LobbyGuard] Players.LocalPlayer never appeared - guard NOT armed") return false end t0 = os.clock() while not game:IsLoaded() and os.clock() - t0 < 60 do task.wait(0.1) end return true end print(("[LobbyGuard] loaded · close=%s · accounts=%d · scan every %ss"):format( tostring(Config.CloseGame), #Config.Allow, tostring(Config.RescanEvery))) print(("[LobbyGuard] guard: lobby=%s · dungeon=%s · unknown=%s"):format( tostring(Config.GuardInLobby ~= false), tostring(Config.GuardInDungeon ~= false), tostring(Config.GuardWhenUnknown ~= false))) if Config.WebhookURL == "" then warn("[LobbyGuard] WebhookURL empty - it will close the game with NO Discord alert") end if Config.CloseGame and #Config.Allow == 0 then warn("[LobbyGuard] account list EMPTY and CloseGame on - any join (incl. your alt) closes this account") end do local placeholders = 0 for _, n in ipairs(Config.Allow) do if string.find(string.upper(tostring(n)), "PUT_YOUR") == 1 then placeholders = placeholders + 1 end end if placeholders > 0 then warn(("[LobbyGuard] you left %d example name(s) (\"PUT_YOUR_...\") in Config.Allow - replace with your real USERNAMES"):format(placeholders)) end end task.spawn(function() if not Players.LocalPlayer then print("[LobbyGuard] waiting for the player to arm...") end if not waitReady() then return end conns[#conns + 1] = Players.PlayerAdded:Connect(function(p) if p == LocalPlayer then return end if allowed(p.Name) then print("[LobbyGuard] " .. p.Name .. " is on the list - ignored") return end local on, w = guardActive() if not on then print(("[LobbyGuard] %s joined, but I'm in %s - guard off here"):format(p.Name, w)) return end alert({ p }) end) print(("[LobbyGuard] armed · %d player(s) in server · you = %s"):format( #Players:GetPlayers(), tostring(LocalPlayer.Name))) -- Periodic scan, forever. PlayerAdded only catches joins from now -- on; this catches whoever was already there or still replicating -- when the script loaded. Only the unload stops it. while running do check() task.wait(math.max(tonumber(Config.RescanEvery) or 3, 1)) end end) if getgenv then getgenv().__LobbyGuardUnload = function() for _, c in ipairs(conns) do pcall(function() c:Disconnect() end) end conns = {} fired = true running = false getgenv().__LobbyGuardUnload = nil print("[LobbyGuard] unloaded") end end