diff --git a/ATGComkub.lua b/ATGComkub.lua index 0815f86..430da0b 100644 --- a/ATGComkub.lua +++ b/ATGComkub.lua @@ -1,25 +1,73 @@ --- Improved place-based script loader --- Features: --- 1) allowlist as table with name + url (easier to maintain) --- 2) HttpService check + basic URL validation --- 3) retry mechanism + non-blocking coroutine --- 4) clear logging (print/warn) --- 5) fallback suggestion to use ModuleScript (see notes below) - local HttpService = game:GetService("HttpService") -local RunService = game:GetService("RunService") +local RunService = game:GetService("RunService") local ReplicatedStorage = game:GetService("ReplicatedStorage") - +local allowedToRun = true -- ----------------------- -- Allowed Place configuration -- ----------------------- -- เพิ่มรายการได้ง่าย: ใส่ placeId => { name = "...", url = "https://.../file.lua" } local allowedPlaces = { - [8069117419] = { name = "demon", url = "https://raw.githubusercontent.com/ATGFAIL/ATGHub/main/demon.lua" }, - [127742093697776] = { name = "Plants-Vs-Brainrots", url = "https://raw.githubusercontent.com/ATGFAIL/ATGHub/main/Plants-Vs-Brainrots.lua" }, - [96114180925459] = { name = "Lasso-Animals", url = "https://raw.githubusercontent.com/ATGFAIL/ATGHub/main/Lasso-Animals.lua" }, - [135880624242201] = { name = "Cut-Tree", url = "https://raw.githubusercontent.com/ATGFAIL/ATGHub/main/cut-tree.lua" }, - [142823291] = { name = "Murder-Mystery-2", url = "https://raw.githubusercontent.com/ATGFAIL/ATGHub/main/Murder-Mystery-2.lua" }, + [127742093697776] = { + name = "Plants-Vs-Brainrots", + url = "https://api.luarmor.net/files/v3/loaders/059cb863ce855658c5a1b050dab6fbaf.lua" + }, + [96114180925459] = { + name = "Lasso-Animals", + url = "https://api.luarmor.net/files/v3/loaders/49ef22e94528a49b6f1f7b0de2a98367.lua" + }, + [135880624242201] = {name = "Cut-Tree", url = "https://raw.githubusercontent.com/ATGFAIL/ATGHub/main/cut-tree.lua"}, + [126509999114328] = { + name = "99 Nights in the Forest", + url = "https://api.luarmor.net/files/v3/loaders/3be199e8307561dc3cfb7855a31269dd.lua" + }, + [79546208627805] = { + name = "99 Nights in the Forest", + url = "https://api.luarmor.net/files/v3/loaders/3be199e8307561dc3cfb7855a31269dd.lua" + }, + [102181577519757] = { + name = "DARK-DECEPTION-HUNTED", + url = "https://raw.githubusercontent.com/ATGFAIL/ATGHub/main/Dark-Deception-Hunted.lua" + }, + [136431686349723] = { + name = "DARK-DECEPTION-HUNTED", + url = "https://raw.githubusercontent.com/ATGFAIL/ATGHub/main/Dark-Deception-Hunted.lua" + }, + [125591428878906] = { + name = "DARK-DECEPTION-HUNTED", + url = "https://raw.githubusercontent.com/ATGFAIL/ATGHub/main/Dark-Deception-Hunted.lua" + }, + [142823291] = { + name = "Murder-Mystery-2", + url = "https://api.luarmor.net/files/v3/loaders/d48b61ec237a114790c3a9346aa4bedf.lua" + }, + [126884695634066] = { + name = "Grow-a-Garden", + url = "https://api.luarmor.net/files/v3/loaders/30c274d8989e8c01a8c8fa3511756d0b.lua" + }, + [124977557560410] = { + name = "Grow-a-Garden", + url = "https://api.luarmor.net/files/v3/loaders/30c274d8989e8c01a8c8fa3511756d0b.lua" + }, + [122826953758426] = { + name = "Raise-Animals", + url = "https://raw.githubusercontent.com/ATGFAIL/ATGHub/main/Raise-Animals.lua" + }, + [115317601829407] = { + name = "Arise-Shadow-Hunt", + url = "https://api.luarmor.net/files/v3/loaders/595828b9a7e9744b44904048c7337210.lua" + }, + [93978595733734] = { + name = "Violence-District", + url = "https://raw.githubusercontent.com/ATGFAIL/ATGHub/main/Violence-District.lua" + }, + [118915549367482] = { + name = "Dont-Wake-the-Brainrots", + url = "https://raw.githubusercontent.com/ATGFAIL/ATGHub/main/Dont-Wake-the-Brainrots.lua" + }, + [10449761463] = { + name = "The-Strongest-Battlegrounds", + url = "https://raw.githubusercontent.com/ATGFAIL/ATGHub/main/The-Strongest-Battlegrounds.lua" + } } -- ----------------------- @@ -38,10 +86,16 @@ local function logError(...) end local function isValidLuaUrl(url) - if type(url) ~= "string" then return false end + if type(url) ~= "string" then + return false + end -- basic checks: http/https and ends with .lua (case-insensitive) - if not url:match("^https?://") then return false end - if not url:lower():match("%.lua$") then return false end + if not url:match("^https?://") then + return false + end + if not url:lower():match("%.lua$") then + return false + end return true end @@ -50,7 +104,7 @@ end -- ----------------------- local placeConfig = allowedPlaces[game.PlaceId] if not placeConfig then - logWarn("Script ไม่ทำงานในแมพนี้:", tostring(game.PlaceId)) + game.Players.LocalPlayer:Kick("[ ATG ] NOT SUPPORT") return end @@ -59,18 +113,21 @@ logInfo(("Script loaded for PlaceId %s (%s)"):format(tostring(game.PlaceId), tos -- Check HttpService availability early if not HttpService.HttpEnabled then logError("HttpService.HttpEnabled = false. ไม่สามารถโหลดสคริปต์จาก URL ได้.") - -- ถ้าต้องการให้ทำงานต่อแม้ Http ปิด ให้ใส่ fallback (เช่น require ModuleScript) ด้านล่าง - -- return +-- ถ้าต้องการให้ทำงานต่อแม้ Http ปิด ให้ใส่ fallback (เช่น require ModuleScript) ด้านล่าง +-- return end -- ----------------------- -- Script loader (with retries) -- ----------------------- local function fetchScript(url) - local ok, result = pcall(function() - -- second arg true = skip cache; บาง executor อาจรองรับ - return game:HttpGet(url, true) - end) + local ok, result = + pcall( + function() + -- second arg true = skip cache; บาง executor อาจรองรับ + return game:HttpGet(url, true) + end + ) return ok, result end @@ -88,14 +145,17 @@ local function loadExtraScript(url, options) local ok, res = fetchScript(url) if ok and type(res) == "string" and #res > 0 then -- attempt to execute safely - local execOk, execRes = pcall(function() - -- loadstring may not exist in some environments; pcall + loadstring used here - local f, loadErr = loadstring(res) - if not f then - error(("loadstring error: %s"):format(tostring(loadErr))) + local execOk, execRes = + pcall( + function() + -- loadstring may not exist in some environments; pcall + loadstring used here + local f, loadErr = loadstring(res) + if not f then + error(("loadstring error: %s"):format(tostring(loadErr))) + end + return f() end - return f() - end) + ) if execOk then return true, execRes @@ -117,23 +177,25 @@ local function loadExtraScript(url, options) end -- Run loader inside coroutine so main thread isn't blocked by network retries -coroutine.wrap(function() - logInfo("เริ่มโหลดสคริปต์สำหรับแมพ:", placeConfig.name, placeConfig.url) - local ok, result = loadExtraScript(placeConfig.url, { retries = 3, retryDelay = 1 }) - - if ok then - logInfo("✅ Extra script loaded successfully for", placeConfig.name) - else - logError("❌ ไม่สามารถโหลดสคริปต์เพิ่มเติมได้:", result) - -- ตัวอย่าง fallback: ถ้ามี ModuleScript เก็บไว้ใน ReplicatedStorage ให้ require แทน - -- local mod = ReplicatedStorage:FindFirstChild("Fallback_" .. placeConfig.name) - -- if mod and mod:IsA("ModuleScript") then - -- local success, modRes = pcall(require, mod) - -- if success then - -- logInfo("✅ Loaded fallback ModuleScript for", placeConfig.name) - -- else - -- logError("Fallback ModuleScript error:", modRes) - -- end - -- end +coroutine.wrap( + function() + logInfo("เริ่มโหลดสคริปต์สำหรับแมพ:", placeConfig.name, placeConfig.url) + local ok, result = loadExtraScript(placeConfig.url, {retries = 3, retryDelay = 1}) + + if ok then + logInfo("✅ Extra script loaded successfully for", placeConfig.name) + else + -- ตัวอย่าง fallback: ถ้ามี ModuleScript เก็บไว้ใน ReplicatedStorage ให้ require แทน + -- local mod = ReplicatedStorage:FindFirstChild("Fallback_" .. placeConfig.name) + -- if mod and mod:IsA("ModuleScript") then + -- local success, modRes = pcall(require, mod) + -- if success then + -- logInfo("✅ Loaded fallback ModuleScript for", placeConfig.name) + -- else + -- logError("Fallback ModuleScript error:", modRes) + -- end + -- end + logError("❌ ไม่สามารถโหลดสคริปต์เพิ่มเติมได้:", result) + end end -end)() +)() diff --git a/ESP.lua b/ESP.lua new file mode 100644 index 0000000..5bdb8f4 --- /dev/null +++ b/ESP.lua @@ -0,0 +1,512 @@ +-- ATG ESP (optimized) — patched version (UI sync + Reset fixes) +local Players = game:GetService("Players") +local RunService = game:GetService("RunService") +local LocalPlayer = Players.LocalPlayer +local Camera = workspace.CurrentCamera + +-- state + config (เก็บค่า default ที่เหมาะสม) +local state = state or {} +state.espTable = state.espTable or {} +state.pools = state.pools or {billboards = {}, highlights = {}} +state.config = state.config or { + enabled = false, + updateRate = 10, + maxDistance = 250, + maxVisibleCount = 60, + showName = true, + showHealth = true, + showDistance = true, + espColor = Color3.fromRGB(255, 50, 50), + labelScale = 1, + alwaysOnTop = false, + smartHideCenter = true, + centerHideRadius = 0.12, + raycastOcclusion = false, + raycastInterval = 0.6, + highlightEnabled = false, + highlightFillTransparency = 0.6, + highlightOutlineTransparency = 0.6, + teamCheck = true, + ignoreLocalPlayer = true, +} + +-- Helpers: pooling... +local function borrowBillboard() + local pool = state.pools.billboards + if #pool > 0 then + return table.remove(pool) + else + local billboard = Instance.new("BillboardGui") + billboard.Name = "ATG_ESP" + billboard.Size = UDim2.new(0, 150, 0, 30) + billboard.StudsOffset = Vector3.new(0, 2.6, 0) + billboard.Adornee = nil + billboard.AlwaysOnTop = state.config.alwaysOnTop + billboard.ResetOnSpawn = false + + local label = Instance.new("TextLabel") + label.Name = "ATG_ESP_Label" + label.Size = UDim2.fromScale(1, 1) + label.BackgroundTransparency = 1 + label.BorderSizePixel = 0 + label.Text = "" + label.TextScaled = true + label.Font = Enum.Font.GothamBold + label.TextStrokeTransparency = 0.4 + label.TextStrokeColor3 = Color3.new(0,0,0) + label.TextWrapped = true + label.Parent = billboard + + return {billboard = billboard, label = label} + end +end + +local function returnBillboard(obj) + if not obj or not obj.billboard then return end + pcall(function() + obj.label.Text = "" + obj.billboard.Parent = nil + obj.billboard.Adornee = nil + end) + table.insert(state.pools.billboards, obj) +end + +local function borrowHighlight() + local pool = state.pools.highlights + if #pool > 0 then + return table.remove(pool) + else + local hl = Instance.new("Highlight") + hl.Name = "ATG_ESP_Highlight" + hl.Enabled = false + return hl + end +end + +local function returnHighlight(hl) + if not hl then return end + pcall(function() + hl.Enabled = false + hl.Adornee = nil + hl.Parent = nil + end) + table.insert(state.pools.highlights, hl) +end + +-- util +local function getHRP(player) + if not player or not player.Character then return nil end + return player.Character:FindFirstChild("HumanoidRootPart") +end +local function getHumanoid(char) + if not char then return nil end + return char:FindFirstChildOfClass("Humanoid") +end +local function isSameTeam(a,b) + if not a or not b then return false end + if a.Team and b.Team and a.Team == b.Team then return true end + return false +end + +-- entries +local function ensureEntryForPlayer(p) + if not p then return end + local uid = p.UserId + if state.espTable[uid] then return state.espTable[uid] end + + local info = { + player = p, + billboardObj = nil, + highlightObj = nil, + lastVisible = false, + lastScreenPos = Vector2.new(0,0), + lastDistance = math.huge, + lastRaycast = -999, + connected = true, + charConn = nil + } + + info.charConn = p.CharacterAdded:Connect(function(char) + task.wait(0.05) + if info.billboardObj and info.billboardObj.billboard then + local head = char:FindFirstChild("Head") or char:FindFirstChild("UpperTorso") or char:FindFirstChild("HumanoidRootPart") + if head then info.billboardObj.billboard.Adornee = head end + end + if info.highlightObj then info.highlightObj.Adornee = char end + end) + + state.espTable[uid] = info + return info +end + +local function cleanupEntry(uid) + local info = state.espTable[uid] + if not info then return end + pcall(function() + if info.charConn then info.charConn:Disconnect() info.charConn = nil end + if info.billboardObj then returnBillboard(info.billboardObj) info.billboardObj = nil end + if info.highlightObj then returnHighlight(info.highlightObj) info.highlightObj = nil end + end) + state.espTable[uid] = nil +end + +-- visibility +local function shouldShowFor(info) + if not info or not info.player then return false end + local p = info.player + if not p.Character or not p.Character.Parent then return false end + if state.config.ignoreLocalPlayer and p == LocalPlayer then return false end + if state.config.teamCheck and isSameTeam(p, LocalPlayer) then return false end + + local myHRP = getHRP(LocalPlayer) + local theirHRP = getHRP(p) + if not myHRP or not theirHRP then return false end + + local dist = (myHRP.Position - theirHRP.Position).Magnitude + if dist > state.config.maxDistance then return false end + + local head = p.Character:FindFirstChild("Head") or p.Character:FindFirstChild("UpperTorso") or theirHRP + if not head then return false end + local screenPos, onScreen = Camera:WorldToViewportPoint(head.Position) + if not onScreen then return false end + + if state.config.smartHideCenter then + local sx = screenPos.X / Camera.ViewportSize.X + local sy = screenPos.Y / Camera.ViewportSize.Y + local dx = sx - 0.5 + local dy = sy - 0.5 + local d = math.sqrt(dx*dx + dy*dy) + if d < state.config.centerHideRadius then return false end + end + + if state.config.raycastOcclusion then + local now = tick() + if now - info.lastRaycast >= state.config.raycastInterval then + info.lastRaycast = now + local origin = Camera.CFrame.Position + local direction = (head.Position - origin) + local rayParams = RaycastParams.new() + rayParams.FilterDescendantsInstances = {LocalPlayer.Character} + rayParams.FilterType = Enum.RaycastFilterType.Blacklist + local r = workspace:Raycast(origin, direction, rayParams) + if r and r.Instance and not r.Instance:IsDescendantOf(p.Character) then return false end + end + end + + return true +end + +-- label text +local function buildLabelText(p) + local parts = {} + if state.config.showName then table.insert(parts, p.DisplayName or p.Name) end + local hum = getHumanoid(p.Character) + if state.config.showHealth and hum then table.insert(parts, "HP:" .. math.floor(hum.Health)) end + if state.config.showDistance then + local myHRP = getHRP(LocalPlayer) + local theirHRP = getHRP(p) + if myHRP and theirHRP then + local d = math.floor((myHRP.Position - theirHRP.Position).Magnitude) + table.insert(parts, "[" .. d .. "m]") + end + end + return table.concat(parts, " | ") +end + +-- centralized updater +local accumulator = 0 +local updateInterval = 1 / math.max(1, state.config.updateRate) +local lastVisibleCount = 0 + +local function performUpdate(dt) + accumulator = accumulator + dt + updateInterval = 1 / math.max(1, state.config.updateRate) + if accumulator < updateInterval then return end + accumulator = accumulator - updateInterval + + local visibleCount = 0 + local players = Players:GetPlayers() + for _, p in ipairs(players) do + if p ~= LocalPlayer or not state.config.ignoreLocalPlayer then ensureEntryForPlayer(p) end + end + + for uid, info in pairs(state.espTable) do + local p = info.player + if not p or not p.Parent then + cleanupEntry(uid) + else + local canShow = state.config.enabled and shouldShowFor(info) + if canShow and visibleCount >= state.config.maxVisibleCount then canShow = false end + + if canShow then + visibleCount = visibleCount + 1 + if not info.billboardObj then + local obj = borrowBillboard() + local head = p.Character and (p.Character:FindFirstChild("Head") or p.Character:FindFirstChild("UpperTorso") or getHRP(p)) + if head then + obj.billboard.Parent = head + obj.billboard.Adornee = head + obj.billboard.AlwaysOnTop = state.config.alwaysOnTop + info.billboardObj = obj + else + returnBillboard(obj) + info.billboardObj = nil + end + end + + if state.config.highlightEnabled and not info.highlightObj then + local hl = borrowHighlight() + hl.Adornee = p.Character + hl.Parent = p.Character + hl.Enabled = true + hl.FillColor = state.config.espColor + hl.FillTransparency = state.config.highlightFillTransparency + hl.OutlineColor = state.config.espColor + hl.OutlineTransparency = state.config.highlightOutlineTransparency + info.highlightObj = hl + elseif (not state.config.highlightEnabled) and info.highlightObj then + returnHighlight(info.highlightObj) + info.highlightObj = nil + end + + if info.billboardObj and info.billboardObj.label then + local txt = buildLabelText(p) + if info.billboardObj.label.Text ~= txt then info.billboardObj.label.Text = txt end + if info.billboardObj.label.TextColor3 ~= state.config.espColor then info.billboardObj.label.TextColor3 = state.config.espColor end + info.billboardObj.billboard.Size = UDim2.new(0, math.clamp(120 + (#txt * 4), 100, 280), 0, math.clamp(16 * state.config.labelScale, 12, 48)) + info.billboardObj.billboard.AlwaysOnTop = state.config.alwaysOnTop + end + else + if info.billboardObj then returnBillboard(info.billboardObj) info.billboardObj = nil end + if info.highlightObj then returnHighlight(info.highlightObj) info.highlightObj = nil end + end + end + end + + lastVisibleCount = visibleCount +end + +-- connect heartbeat +if state._espHeartbeatConn then pcall(function() state._espHeartbeatConn:Disconnect() end) state._espHeartbeatConn = nil end +state._espHeartbeatConn = RunService.Heartbeat:Connect(performUpdate) + +-- cleanup on leave +Players.PlayerRemoving:Connect(function(p) if not p then return end cleanupEntry(p.UserId) end) + +-- UI sync helpers (รองรับหลายไลบรารี) +local uiRefs = {} -- store widgets by key + +local function trySetWidgetValue(widget, val) + if not widget then return end + pcall(function() + -- common APIs across various UI libs + if widget.SetValue then widget:SetValue(val) end + if widget.Set then widget:Set(val) end + if widget.SetState then widget:SetState(val) end + if widget.SetValueNoCallback then widget:SetValueNoCallback(val) end + -- some libs store .Value property + if widget.Value ~= nil then widget.Value = val end + end) +end + +-- function to apply config to runtime (and update visuals immediately) +local function applyConfigToState(cfg) + -- shallow copy allowed (cfg is a table) + for k, v in pairs(cfg) do state.config[k] = v end + + -- immediate side-effects + updateInterval = 1 / math.max(1, state.config.updateRate) + + -- update existing billboards / highlights + for uid, info in pairs(state.espTable) do + if info.billboardObj and info.billboardObj.billboard then + local bb = info.billboardObj.billboard + local label = info.billboardObj.label + label.TextColor3 = state.config.espColor + bb.AlwaysOnTop = state.config.alwaysOnTop + bb.Size = UDim2.new(0, math.clamp(120 + (#label.Text * 4), 100, 280), 0, math.clamp(16 * state.config.labelScale, 12, 48)) + end + if info.highlightObj then + local hl = info.highlightObj + hl.FillColor = state.config.espColor + hl.FillTransparency = state.config.highlightFillTransparency + hl.OutlineColor = state.config.espColor + hl.OutlineTransparency = state.config.highlightOutlineTransparency + hl.Enabled = state.config.highlightEnabled + if not state.config.highlightEnabled then + returnHighlight(hl) + info.highlightObj = nil + end + end + end +end + +-- Exposed API +local ESP_API = {} + +function ESP_API.ToggleEnabled(v) + state.config.enabled = v + if not v then + for uid,info in pairs(state.espTable) do + if info.billboardObj then returnBillboard(info.billboardObj) info.billboardObj = nil end + if info.highlightObj then returnHighlight(info.highlightObj) info.highlightObj = nil end + end + end +end + +function ESP_API.SetColor(c) + state.config.espColor = c + -- live update + for _,info in pairs(state.espTable) do + if info.billboardObj and info.billboardObj.label then info.billboardObj.label.TextColor3 = c end + if info.highlightObj then + info.highlightObj.FillColor = c + info.highlightObj.OutlineColor = c + end + end +end + +function ESP_API.SetShowName(v) state.config.showName = v end +function ESP_API.SetShowHealth(v) state.config.showHealth = v end +function ESP_API.SetShowDistance(v) state.config.showDistance = v end +function ESP_API.SetUpdateRate(v) + state.config.updateRate = math.clamp(math.floor(v), 1, 60) + updateInterval = 1 / state.config.updateRate +end +function ESP_API.SetMaxDistance(v) state.config.maxDistance = math.max(20, v) end +function ESP_API.SetLabelScale(v) + state.config.labelScale = math.clamp(v, 0.5, 3) + -- apply to existing labels + for _,info in pairs(state.espTable) do + if info.billboardObj and info.billboardObj.label and info.billboardObj.billboard then + local label = info.billboardObj.label + info.billboardObj.billboard.Size = UDim2.new(0, math.clamp(120 + (#label.Text * 4), 100, 280), 0, math.clamp(16 * state.config.labelScale, 12, 48)) + end + end +end +function ESP_API.SetAlwaysOnTop(v) + state.config.alwaysOnTop = v + for _,info in pairs(state.espTable) do + if info.billboardObj and info.billboardObj.billboard then info.billboardObj.billboard.AlwaysOnTop = v end + end +end +function ESP_API.SetHighlightEnabled(v) + state.config.highlightEnabled = v + -- toggle highlights on existing entries + for _,info in pairs(state.espTable) do + if v and not info.highlightObj and info.player and info.player.Character then + local hl = borrowHighlight() + hl.Adornee = info.player.Character + hl.Parent = info.player.Character + hl.Enabled = true + hl.FillColor = state.config.espColor + hl.FillTransparency = state.config.highlightFillTransparency + hl.OutlineColor = state.config.espColor + hl.OutlineTransparency = state.config.highlightOutlineTransparency + info.highlightObj = hl + elseif (not v) and info.highlightObj then + returnHighlight(info.highlightObj) + info.highlightObj = nil + end + end +end +function ESP_API.SetHighlightFillTrans(v) state.config.highlightFillTransparency = math.clamp(v, 0, 1) end +function ESP_API.SetHighlightOutlineTrans(v) state.config.highlightOutlineTransparency = math.clamp(v, 0, 1) end + +function ESP_API.ResetConfig() + local defaults = { + enabled = false, + updateRate = 10, + maxDistance = 250, + maxVisibleCount = 60, + showName = true, + showHealth = true, + showDistance = true, + espColor = Color3.fromRGB(255, 50, 50), + labelScale = 1, + alwaysOnTop = false, + smartHideCenter = true, + centerHideRadius = 0.12, + raycastOcclusion = false, + raycastInterval = 0.6, + highlightEnabled = false, + highlightFillTransparency = 0.6, + highlightOutlineTransparency = 0.6, + teamCheck = true, + ignoreLocalPlayer = true, + } + -- replace state.config + state.config = defaults + -- apply runtime changes + applyConfigToState(state.config) + -- update UI elements if present + -- try to update known widgets in uiRefs + pcall(function() + trySetWidgetValue(uiRefs.ESPToggle, state.config.enabled) + trySetWidgetValue(uiRefs.ESPColor, state.config.espColor) + trySetWidgetValue(uiRefs.ESP_Name, state.config.showName) + trySetWidgetValue(uiRefs.ESP_Health, state.config.showHealth) + trySetWidgetValue(uiRefs.ESP_Distance, state.config.showDistance) + trySetWidgetValue(uiRefs.ESP_Highlight, state.config.highlightEnabled) + trySetWidgetValue(uiRefs.ESP_HighlightFill, state.config.highlightFillTransparency) + trySetWidgetValue(uiRefs.ESP_HighlightOutline, state.config.highlightOutlineTransparency) + trySetWidgetValue(uiRefs.ESP_Rate, state.config.updateRate) + trySetWidgetValue(uiRefs.ESP_MaxDist, state.config.maxDistance) + trySetWidgetValue(uiRefs.ESP_LabelScale, state.config.labelScale) + trySetWidgetValue(uiRefs.ESP_AlwaysOnTop, state.config.alwaysOnTop) + end) +end + +-- expose API +_G.ATG_ESP_API = ESP_API + +-- UI hookup (store refs and ensure OnChanged hooks update state) +if Tabs and Tabs.ESP then + -- store ref for safe-set on Reset + uiRefs.ESPToggle = Tabs.ESP:AddToggle("ESPToggle", { Title = "ESP", Default = state.config.enabled }) + if uiRefs.ESPToggle then uiRefs.ESPToggle:OnChanged(function(v) ESP_API.ToggleEnabled(v) end) end + + uiRefs.ESPColor = Tabs.ESP:AddColorpicker("ESPColor", { Title = "ESP Color", Default = state.config.espColor }) + if uiRefs.ESPColor then uiRefs.ESPColor:OnChanged(function(c) ESP_API.SetColor(c) end) end + + uiRefs.ESP_Name = Tabs.ESP:AddToggle("ESP_Name", { Title = "Show Name", Default = state.config.showName }) + if uiRefs.ESP_Name then uiRefs.ESP_Name:OnChanged(function(v) ESP_API.SetShowName(v) end) end + + uiRefs.ESP_Health = Tabs.ESP:AddToggle("ESP_Health", { Title = "Show Health", Default = state.config.showHealth }) + if uiRefs.ESP_Health then uiRefs.ESP_Health:OnChanged(function(v) ESP_API.SetShowHealth(v) end) end + + uiRefs.ESP_Distance = Tabs.ESP:AddToggle("ESP_Distance", { Title = "Show Distance", Default = state.config.showDistance }) + if uiRefs.ESP_Distance then uiRefs.ESP_Distance:OnChanged(function(v) ESP_API.SetShowDistance(v) end) end + + uiRefs.ESP_Highlight = Tabs.ESP:AddToggle("ESP_Highlight", { Title = "Highlight", Default = state.config.highlightEnabled }) + if uiRefs.ESP_Highlight then uiRefs.ESP_Highlight:OnChanged(function(v) ESP_API.SetHighlightEnabled(v) end) end + + uiRefs.ESP_HighlightFill = Tabs.ESP:AddSlider("ESP_HighlightFill", { Title = "Highlight Fill Transparency", Default = state.config.highlightFillTransparency, Min = 0, Max = 1, Rounding = 0.01 }) + if uiRefs.ESP_HighlightFill then uiRefs.ESP_HighlightFill:OnChanged(function(v) ESP_API.SetHighlightFillTrans(v) end) end + + uiRefs.ESP_HighlightOutline = Tabs.ESP:AddSlider("ESP_HighlightOutline", { Title = "Highlight Outline Transparency", Default = state.config.highlightOutlineTransparency, Min = 0, Max = 1, Rounding = 0.01 }) + if uiRefs.ESP_HighlightOutline then uiRefs.ESP_HighlightOutline:OnChanged(function(v) ESP_API.SetHighlightOutlineTrans(v) end) end + + uiRefs.ESP_Rate = Tabs.ESP:AddSlider("ESP_Rate", { Title = "Update Rate (per sec)", Default = state.config.updateRate, Min = 1, Max = 60, Rounding = 1 }) + if uiRefs.ESP_Rate then uiRefs.ESP_Rate:OnChanged(function(v) ESP_API.SetUpdateRate(v) end) end + + uiRefs.ESP_MaxDist = Tabs.ESP:AddSlider("ESP_MaxDist", { Title = "Max Distance", Default = state.config.maxDistance, Min = 50, Max = 1000, Rounding = 1 }) + if uiRefs.ESP_MaxDist then uiRefs.ESP_MaxDist:OnChanged(function(v) ESP_API.SetMaxDistance(v) end) end + + uiRefs.ESP_LabelScale = Tabs.ESP:AddSlider("ESP_LabelScale", { Title = "Label Scale", Default = state.config.labelScale, Min = 0.5, Max = 3, Rounding = 0.1 }) + if uiRefs.ESP_LabelScale then uiRefs.ESP_LabelScale:OnChanged(function(v) ESP_API.SetLabelScale(v) end) end + + uiRefs.ESP_AlwaysOnTop = Tabs.ESP:AddToggle("ESP_AlwaysOnTop", { Title = "AlwaysOnTop", Default = state.config.alwaysOnTop }) + if uiRefs.ESP_AlwaysOnTop then uiRefs.ESP_AlwaysOnTop:OnChanged(function(v) ESP_API.SetAlwaysOnTop(v) end) end + + Tabs.ESP:AddButton({ + Title = "Reset ESP Config", + Description = "Reset to sane defaults", + Callback = function() + ESP_API.ResetConfig() + print("ESP config reset. UI should be synced.") + end + }) +end + +-- end diff --git a/InterfaceManager.lua b/InterfaceManager.lua new file mode 100644 index 0000000..0d85885 --- /dev/null +++ b/InterfaceManager.lua @@ -0,0 +1,137 @@ +local httpService = game:GetService("HttpService") + +local InterfaceManager = {} do + InterfaceManager.Folder = "FluentSettings" + InterfaceManager.Settings = { + Theme = "Dark", + Acrylic = true, + Transparency = true, + MenuKeybind = "LeftControl", + Language = "en" + } + + function InterfaceManager:SetFolder(folder) + self.Folder = folder; + self:BuildFolderTree() + end + + function InterfaceManager:SetLibrary(library) + self.Library = library + end + + function InterfaceManager:BuildFolderTree() + local paths = {} + + local parts = self.Folder:split("/") + for idx = 1, #parts do + paths[#paths + 1] = table.concat(parts, "/", 1, idx) + end + + table.insert(paths, self.Folder) + table.insert(paths, self.Folder .. "/settings") + + for i = 1, #paths do + local str = paths[i] + if not isfolder(str) then + makefolder(str) + end + end + end + + function InterfaceManager:SaveSettings() + writefile(self.Folder .. "/options.json", httpService:JSONEncode(InterfaceManager.Settings)) + end + + function InterfaceManager:LoadSettings() + local path = self.Folder .. "/options.json" + if isfile(path) then + local data = readfile(path) + local success, decoded = pcall(httpService.JSONDecode, httpService, data) + + if success then + for i, v in next, decoded do + InterfaceManager.Settings[i] = v + end + end + end + end + + function InterfaceManager:BuildInterfaceSection(tab) + assert(self.Library, "Must set InterfaceManager.Library") + local Library = self.Library + local Settings = InterfaceManager.Settings + + InterfaceManager:LoadSettings() + + local section = tab:AddSection("Interface") + + local InterfaceTheme = section:AddDropdown("InterfaceTheme", { + Title = "Theme", + Description = "Changes the interface theme.", + Values = Library.Themes, + Default = Settings.Theme, + Callback = function(Value) + Library:SetTheme(Value) + Settings.Theme = Value + InterfaceManager:SaveSettings() + end + }) + + InterfaceTheme:SetValue(Settings.Theme) + + -- Language Dropdown (only if Translation System exists) + if Library.Translation then + local LanguageDropdown = section:AddDropdown("InterfaceLanguage", { + Title = "🌐 Language", + Description = "Changes the interface language (Auto-Translate)", + Values = Library.Translation:GetLanguageOptions(), + Default = Library.Translation:GetLanguageIndex(), + Callback = function(Value) + local langCode = Library.Translation:GetLanguageCode(Value) + Library.Translation:SetLanguage(langCode) + Settings.Language = langCode + InterfaceManager:SaveSettings() + end + }) + + -- Load saved language + if Settings.Language and Settings.Language ~= "en" then + Library.Translation:SetLanguage(Settings.Language) + LanguageDropdown:SetValue(Library.Translation:GetLanguageIndex()) + end + end + + if Library.UseAcrylic then + section:AddToggle("AcrylicToggle", { + Title = "Acrylic", + Description = "The blurred background requires graphic quality 8+", + Default = Settings.Acrylic, + Callback = function(Value) + Library:ToggleAcrylic(Value) + Settings.Acrylic = Value + InterfaceManager:SaveSettings() + end + }) + end + + section:AddToggle("TransparentToggle", { + Title = "Transparency", + Description = "Makes the interface transparent.", + Default = Settings.Transparency, + Callback = function(Value) + Library:ToggleTransparency(Value) + Settings.Transparency = Value + InterfaceManager:SaveSettings() + end + }) + + local MenuKeybind = section:AddKeybind("MenuKeybind", { Title = "Minimize Bind", Default = Settings.MenuKeybind }) + MenuKeybind:OnChanged(function() + Settings.MenuKeybind = MenuKeybind.Value + InterfaceManager:SaveSettings() + end) + Library.MinimizeKeybind = MenuKeybind + end +end + +return InterfaceManager diff --git a/autosave.lua b/autosave.lua new file mode 100644 index 0000000..3fd8e88 --- /dev/null +++ b/autosave.lua @@ -0,0 +1,487 @@ +---@diagnostic disable: undefined-global +local httpService = game:GetService("HttpService") +local Workspace = game:GetService("Workspace") + +local SaveManager = {} do + SaveManager.FolderRoot = "ATGSettings" + SaveManager.SubFolder = nil -- ถ้าตั้งไว้จะใช้แทน PlaceId (share save ข้าม PlaceId) + SaveManager.Ignore = {} + SaveManager.Options = {} + SaveManager.AutoSaveEnabled = false + SaveManager.AutoSaveConfig = nil + SaveManager.AutoSaveDebounce = false + SaveManager.OriginalCallbacks = {} + + SaveManager.Parser = { + Toggle = { + Save = function(idx, object) + return { type = "Toggle", idx = idx, value = object.Value } + end, + Load = function(idx, data) + if SaveManager.Options[idx] then + SaveManager.Options[idx]:SetValue(data.value) + end + end, + }, + Slider = { + Save = function(idx, object) + return { type = "Slider", idx = idx, value = tostring(object.Value) } + end, + Load = function(idx, data) + if SaveManager.Options[idx] then + SaveManager.Options[idx]:SetValue(data.value) + end + end, + }, + Dropdown = { + Save = function(idx, object) + return { type = "Dropdown", idx = idx, value = object.Value, multi = object.Multi } + end, + Load = function(idx, data) + if SaveManager.Options[idx] then + SaveManager.Options[idx]:SetValue(data.value) + end + end, + }, + Colorpicker = { + Save = function(idx, object) + return { type = "Colorpicker", idx = idx, value = object.Value:ToHex(), transparency = object.Transparency } + end, + Load = function(idx, data) + if SaveManager.Options[idx] then + SaveManager.Options[idx]:SetValueRGB(Color3.fromHex(data.value), data.transparency) + end + end, + }, + Keybind = { + Save = function(idx, object) + return { type = "Keybind", idx = idx, mode = object.Mode, key = object.Value } + end, + Load = function(idx, data) + if SaveManager.Options[idx] then + SaveManager.Options[idx]:SetValue(data.key, data.mode) + end + end, + }, + Input = { + Save = function(idx, object) + return { type = "Input", idx = idx, text = object.Value } + end, + Load = function(idx, data) + if SaveManager.Options[idx] and type(data.text) == "string" then + SaveManager.Options[idx]:SetValue(data.text) + end + end, + }, + } + + -- helpers + local function sanitizeFilename(name) + name = tostring(name or "") + name = name:gsub("%s+", "_") + name = name:gsub("[^%w%-%_]", "") + if name == "" then return "Unknown" end + return name + end + + local function getPlaceId() + local ok, id = pcall(function() return tostring(game.PlaceId) end) + if ok and id then return id end + return "UnknownPlace" + end + + local function ensureFolder(path) + if not isfolder(path) then + makefolder(path) + end + end + + local function getConfigsFolder(self) + local root = self.FolderRoot + local sub = self.SubFolder or getPlaceId() + return root .. "/" .. sub + end + + local function getConfigFilePath(self, name) + local folder = getConfigsFolder(self) + return folder .. "/" .. name .. ".json" + end + + local function getSaveManagerUIPath(self) + local folder = getConfigsFolder(self) + return folder .. "/savemanager_ui.json" + end + + function SaveManager:BuildFolderTree() + local root = self.FolderRoot + ensureFolder(root) + + local sub = self.SubFolder or getPlaceId() + local placeFolder = root .. "/" .. sub + ensureFolder(placeFolder) + + -- Migrate legacy configs (ทำแบบ sync ครั้งเดียว) + local legacySettingsFolder = root .. "/settings" + if isfolder(legacySettingsFolder) then + local files = listfiles(legacySettingsFolder) + for i = 1, #files do + local f = files[i] + if f:sub(-5) == ".json" then + local base = f:match("([^/\\]+)%.json$") + if base and base ~= "options" then + local dest = placeFolder .. "/" .. base .. ".json" + if not isfile(dest) then + local ok, data = pcall(readfile, f) + if ok and data then + pcall(writefile, dest, data) + end + end + end + end + end + + local autopath = legacySettingsFolder .. "/autoload.txt" + if isfile(autopath) then + local autodata = readfile(autopath) + local destAuto = placeFolder .. "/autoload.txt" + if not isfile(destAuto) then + pcall(writefile, destAuto, autodata) + end + end + end + end + + function SaveManager:SetIgnoreIndexes(list) + for _, key in next, list do + self.Ignore[key] = true + end + end + + function SaveManager:SetFolder(folder) + self.FolderRoot = tostring(folder or "ATGSettings") + self:BuildFolderTree() + end + + function SaveManager:SetSubFolder(name) + self.SubFolder = name and tostring(name) or nil + self:BuildFolderTree() + end + + function SaveManager:SetLibrary(library) + self.Library = library + self.Options = library.Options + end + + function SaveManager:Save(name) + if (not name) then + return false, "no config file is selected" + end + + local fullPath = getConfigFilePath(self, name) + local data = { objects = {} } + + for idx, option in next, SaveManager.Options do + if not self.Parser[option.Type] then continue end + if self.Ignore[idx] then continue end + table.insert(data.objects, self.Parser[option.Type].Save(idx, option)) + end + + local success, encoded = pcall(httpService.JSONEncode, httpService, data) + if not success then + return false, "failed to encode data" + end + + local folder = fullPath:match("^(.*)/[^/]+$") + if folder then ensureFolder(folder) end + + writefile(fullPath, encoded) + return true + end + + function SaveManager:SaveUI() + local uiPath = getSaveManagerUIPath(self) + local uiData = { + autoload_enabled = (self:GetAutoloadConfig() ~= nil), + autoload_config = (self:GetAutoloadConfig() or nil), + autosave_enabled = self.AutoSaveEnabled, + autosave_config = self.AutoSaveConfig + } + + local success, encoded = pcall(httpService.JSONEncode, httpService, uiData) + if success then + local folder = uiPath:match("^(.*)/[^/]+$") + if folder then ensureFolder(folder) end + writefile(uiPath, encoded) + end + end + + function SaveManager:LoadUI() + local uiPath = getSaveManagerUIPath(self) + if not isfile(uiPath) then return nil end + + local success, decoded = pcall(httpService.JSONDecode, httpService, readfile(uiPath)) + if success then + return decoded + end + return nil + end + + -- โหลด config: ของเบาโหลดตรง, Toggle stagger เพื่อกระจาย heavy callback + function SaveManager:Load(name) + if (not name) then + return false, "no config file is selected" + end + + local file = getConfigFilePath(self, name) + if not isfile(file) then return false, "invalid file" end + + local success, decoded = pcall(httpService.JSONDecode, httpService, readfile(file)) + if not success then return false, "decode error" end + + local objects = decoded.objects + local deferredToggles = {} + + -- โหลดของเบาทันที (Slider/Input/Dropdown/Colorpicker/Keybind) + for i = 1, #objects do + local option = objects[i] + local parser = self.Parser[option.type] + if parser then + if option.type == "Toggle" then + deferredToggles[#deferredToggles + 1] = option + else + pcall(parser.Load, option.idx, option) + end + end + end + + -- โหลด Toggle แบบ stagger (yield ทุก 5 ตัว ให้เกมหายใจ) + if #deferredToggles > 0 then + task.spawn(function() + for i = 1, #deferredToggles do + local option = deferredToggles[i] + pcall(self.Parser.Toggle.Load, option.idx, option) + if i % 5 == 0 then + task.wait() + end + end + end) + end + + return true + end + + function SaveManager:Delete(name) + if not name then + return false, "no config file is selected" + end + + local file = getConfigFilePath(self, name) + if not isfile(file) then + return false, "config does not exist" + end + + delfile(file) + + local autopath = getConfigsFolder(self) .. "/autoload.txt" + if isfile(autopath) then + local currentAutoload = readfile(autopath) + if currentAutoload == name then + delfile(autopath) + end + end + + return true + end + + function SaveManager:GetAutoloadConfig() + local autopath = getConfigsFolder(self) .. "/autoload.txt" + if isfile(autopath) then + return readfile(autopath) + end + return nil + end + + function SaveManager:SetAutoloadConfig(name) + if not name then + return false, "no config name provided" + end + + local file = getConfigFilePath(self, name) + if not isfile(file) then + return false, "config does not exist" + end + + local autopath = getConfigsFolder(self) .. "/autoload.txt" + writefile(autopath, name) + self:SaveUI() + return true + end + + function SaveManager:DisableAutoload() + local autopath = getConfigsFolder(self) .. "/autoload.txt" + if isfile(autopath) then + delfile(autopath) + self:SaveUI() + return true + end + return false, "no autoload config set" + end + + function SaveManager:IgnoreThemeSettings() + self:SetIgnoreIndexes({ + "InterfaceTheme", "AcrylicToggle", "TransparentToggle", "MenuKeybind" + }) + end + + function SaveManager:RefreshConfigList() + local folder = getConfigsFolder(self) + if not isfolder(folder) then + return {} + end + + local list = listfiles(folder) + local out = {} + for i = 1, #list do + local file = list[i] + if file:sub(-5) == ".json" then + local name = file:match("([^/\\]+)%.json$") + if name and name ~= "options" and name ~= "autoload" and name ~= "savemanager_ui" then + table.insert(out, name) + end + end + end + + return out + end + + function SaveManager:LoadAutoloadConfig() + local name = self:GetAutoloadConfig() + if name then + -- ใช้ LoadAsync เพื่อป้องกัน lag + self:Load(name) -- หรือใช้ LoadAsync(name) ถ้าต้องการ + end + end + + function SaveManager:EnableAutoSave(configName) + self.AutoSaveEnabled = true + self.AutoSaveConfig = configName + self:SaveUI() + + for idx, option in next, self.Options do + if not self.Ignore[idx] and self.Parser[option.Type] then + if not self.OriginalCallbacks[idx] then + self.OriginalCallbacks[idx] = option.Callback + end + + local originalCallback = self.OriginalCallbacks[idx] + option.Callback = function(...) + if originalCallback then + originalCallback(...) + end + + -- Debounce: รวม save หลายๆ การเปลี่ยนแปลงเป็นครั้งเดียว + if self.AutoSaveEnabled and self.AutoSaveConfig and not self.AutoSaveDebounce then + self.AutoSaveDebounce = true + task.delay(3, function() + self.AutoSaveDebounce = false + if self.AutoSaveEnabled and self.AutoSaveConfig then + self:Save(self.AutoSaveConfig) + end + end) + end + end + end + end + end + + function SaveManager:DisableAutoSave() + self.AutoSaveEnabled = false + self.AutoSaveConfig = nil + self:SaveUI() + + for idx, option in next, self.Options do + if self.OriginalCallbacks[idx] then + option.Callback = self.OriginalCallbacks[idx] + end + end + end + + function SaveManager:BuildConfigSection(tab) + assert(self.Library, "Must set SaveManager.Library") + + local section = tab:AddSection("[ 📁 ] Configuration Manager") + + local uiSettings = self:LoadUI() + + local fixedConfigName = "AutoSave" + if not isfile(getConfigFilePath(self, fixedConfigName)) then + pcall(function() self:Save(fixedConfigName) end) + end + + -- โหลด config ตอนเริ่มต้น + if uiSettings and uiSettings.autoload_enabled then + if isfile(getConfigFilePath(self, fixedConfigName)) then + task.defer(function() + pcall(function() self:Load(fixedConfigName) end) + end) + end + end + + section:AddToggle("SaveManager_AutoloadToggle", { + Title = "Auto Load", + Description = "Auto Load Save", + Default = (uiSettings and uiSettings.autoload_enabled) or false, + Callback = function(value) + if value then + if not isfile(getConfigFilePath(self, fixedConfigName)) then + self:Save(fixedConfigName) + end + + local ok = self:SetAutoloadConfig(fixedConfigName) + if not ok then + if SaveManager.Options and SaveManager.Options.SaveManager_AutoloadToggle then + SaveManager.Options.SaveManager_AutoloadToggle:SetValue(false) + end + end + else + self:DisableAutoload() + end + end + }) + + section:AddToggle("SaveManager_AutoSaveToggle", { + Title = "Auto Save", + Description = "Auto Save When You Settings", + Default = (uiSettings and uiSettings.autosave_enabled) or false, + Callback = function(value) + if value then + if not isfile(getConfigFilePath(self, fixedConfigName)) then + self:Save(fixedConfigName) + end + + self:EnableAutoSave(fixedConfigName) + else + self:DisableAutoSave() + end + end + }) + + SaveManager:SetIgnoreIndexes({ + "SaveManager_AutoloadToggle", + "SaveManager_AutoSaveToggle" + }) + + -- เปิดใช้ Auto Save + if uiSettings and uiSettings.autosave_enabled then + if isfile(getConfigFilePath(self, fixedConfigName)) then + task.defer(function() + pcall(function() self:EnableAutoSave(fixedConfigName) end) + end) + end + end + end + + SaveManager:BuildFolderTree() +end + +return SaveManager diff --git a/config.lua b/config.lua new file mode 100644 index 0000000..9c83ed2 --- /dev/null +++ b/config.lua @@ -0,0 +1,582 @@ +local httpService = game:GetService("HttpService") +local Workspace = game:GetService("Workspace") + +local SaveManager = {} do + SaveManager.FolderRoot = "ATGSettings" + SaveManager.Ignore = {} + SaveManager.Options = {} + SaveManager.Parser = { + Toggle = { + Save = function(idx, object) + return { type = "Toggle", idx = idx, value = object.Value } + end, + Load = function(idx, data) + if SaveManager.Options[idx] then + SaveManager.Options[idx]:SetValue(data.value) + end + end, + }, + Slider = { + Save = function(idx, object) + return { type = "Slider", idx = idx, value = tostring(object.Value) } + end, + Load = function(idx, data) + if SaveManager.Options[idx] then + SaveManager.Options[idx]:SetValue(data.value) + end + end, + }, + Dropdown = { + Save = function(idx, object) + return { type = "Dropdown", idx = idx, value = object.Value, mutli = object.Multi } + end, + Load = function(idx, data) + if SaveManager.Options[idx] then + SaveManager.Options[idx]:SetValue(data.value) + end + end, + }, + Colorpicker = { + Save = function(idx, object) + return { type = "Colorpicker", idx = idx, value = object.Value:ToHex(), transparency = object.Transparency } + end, + Load = function(idx, data) + if SaveManager.Options[idx] then + SaveManager.Options[idx]:SetValueRGB(Color3.fromHex(data.value), data.transparency) + end + end, + }, + Keybind = { + Save = function(idx, object) + return { type = "Keybind", idx = idx, mode = object.Mode, key = object.Value } + end, + Load = function(idx, data) + if SaveManager.Options[idx] then + SaveManager.Options[idx]:SetValue(data.key, data.mode) + end + end, + }, + + Input = { + Save = function(idx, object) + return { type = "Input", idx = idx, text = object.Value } + end, + Load = function(idx, data) + if SaveManager.Options[idx] and type(data.text) == "string" then + SaveManager.Options[idx]:SetValue(data.text) + end + end, + }, + } + + -- helpers + local function sanitizeFilename(name) + name = tostring(name or "") + name = name:gsub("%s+", "_") + name = name:gsub("[^%w%-%_]", "") + if name == "" then return "Unknown" end + return name + end + + local function getPlaceId() + local ok, id = pcall(function() return tostring(game.PlaceId) end) + if ok and id then return id end + return "UnknownPlace" + end + + local function ensureFolder(path) + if not isfolder(path) then + makefolder(path) + end + end + + -- เปลี่ยนโครงสร้าง: ATGSettings/PlaceId/ + local function getConfigsFolder(self) + local root = self.FolderRoot + local placeId = getPlaceId() + return root .. "/" .. placeId + end + + local function getConfigFilePath(self, name) + local folder = getConfigsFolder(self) + return folder .. "/" .. name .. ".json" + end + + function SaveManager:BuildFolderTree() + local root = self.FolderRoot + ensureFolder(root) + + local placeId = getPlaceId() + local placeFolder = root .. "/" .. placeId + ensureFolder(placeFolder) + + -- Migrate legacy configs (จากโครงสร้างเก่า) + local legacySettingsFolder = root .. "/settings" + if isfolder(legacySettingsFolder) then + local files = listfiles(legacySettingsFolder) + for i = 1, #files do + local f = files[i] + if f:sub(-5) == ".json" then + local base = f:match("([^/\\]+)%.json$") + if base and base ~= "options" then + local dest = placeFolder .. "/" .. base .. ".json" + if not isfile(dest) then + local ok, data = pcall(readfile, f) + if ok and data then + pcall(writefile, dest, data) + end + end + end + end + end + + local autopath = legacySettingsFolder .. "/autoload.txt" + if isfile(autopath) then + local autodata = readfile(autopath) + local destAuto = placeFolder .. "/autoload.txt" + if not isfile(destAuto) then + pcall(writefile, destAuto, autodata) + end + end + end + end + + function SaveManager:SetIgnoreIndexes(list) + for _, key in next, list do + self.Ignore[key] = true + end + end + + function SaveManager:SetFolder(folder) + self.FolderRoot = tostring(folder or "ATGSettings") + self:BuildFolderTree() + end + + function SaveManager:SetLibrary(library) + self.Library = library + self.Options = library.Options + end + + function SaveManager:Save(name) + if (not name) then + return false, "no config file is selected" + end + + local fullPath = getConfigFilePath(self, name) + local data = { objects = {} } + + for idx, option in next, SaveManager.Options do + if not self.Parser[option.Type] then continue end + if self.Ignore[idx] then continue end + table.insert(data.objects, self.Parser[option.Type].Save(idx, option)) + end + + local success, encoded = pcall(httpService.JSONEncode, httpService, data) + if not success then + return false, "failed to encode data" + end + + local folder = fullPath:match("^(.*)/[^/]+$") + if folder then ensureFolder(folder) end + + writefile(fullPath, encoded) + return true + end + + function SaveManager:Load(name) + if (not name) then + return false, "no config file is selected" + end + + local file = getConfigFilePath(self, name) + if not isfile(file) then return false, "invalid file" end + + local success, decoded = pcall(httpService.JSONDecode, httpService, readfile(file)) + if not success then return false, "decode error" end + + for _, option in next, decoded.objects do + if self.Parser[option.type] then + task.spawn(function() self.Parser[option.type].Load(option.idx, option) end) + end + end + + return true + end + + function SaveManager:Delete(name) + if not name then + return false, "no config file is selected" + end + + local file = getConfigFilePath(self, name) + if not isfile(file) then + return false, "config does not exist" + end + + delfile(file) + + -- ถ้าเป็น autoload ให้ลบ autoload ด้วย + local autopath = getConfigsFolder(self) .. "/autoload.txt" + if isfile(autopath) then + local currentAutoload = readfile(autopath) + if currentAutoload == name then + delfile(autopath) + end + end + + return true + end + + function SaveManager:GetAutoloadConfig() + local autopath = getConfigsFolder(self) .. "/autoload.txt" + if isfile(autopath) then + return readfile(autopath) + end + return nil + end + + function SaveManager:SetAutoloadConfig(name) + if not name then + return false, "no config name provided" + end + + local file = getConfigFilePath(self, name) + if not isfile(file) then + return false, "config does not exist" + end + + local autopath = getConfigsFolder(self) .. "/autoload.txt" + writefile(autopath, name) + return true + end + + function SaveManager:DisableAutoload() + local autopath = getConfigsFolder(self) .. "/autoload.txt" + if isfile(autopath) then + delfile(autopath) + return true + end + return false, "no autoload config set" + end + + function SaveManager:IgnoreThemeSettings() + self:SetIgnoreIndexes({ + "InterfaceTheme", "AcrylicToggle", "TransparentToggle", "MenuKeybind" + }) + end + + function SaveManager:RefreshConfigList() + local folder = getConfigsFolder(self) + if not isfolder(folder) then + return {} + end + local list = listfiles(folder) + local out = {} + for i = 1, #list do + local file = list[i] + if file:sub(-5) == ".json" then + local name = file:match("([^/\\]+)%.json$") + if name and name ~= "options" and name ~= "autoload" then + table.insert(out, name) + end + end + end + return out + end + + function SaveManager:LoadAutoloadConfig() + local name = self:GetAutoloadConfig() + if name then + local success, err = self:Load(name) + if not success then + return self.Library:Notify({ + Title = "Config Loader", + Content = "Failed to load autoload config", + SubContent = err, + Duration = 5 + }) + end + + self.Library:Notify({ + Title = "Config Loader", + Content = "Autoload Success", + SubContent = string.format('Loaded "%s"', name), + Duration = 3 + }) + end + end + + function SaveManager:BuildConfigSection(tab) + assert(self.Library, "Must set SaveManager.Library") + + local section = tab:AddSection("📁 Configuration Manager") + + -- Config Name Input + section:AddInput("SaveManager_ConfigName", { + Title = "💾 Config Name", + Placeholder = "Enter config name...", + Description = "Type a name for your new config" + }) + + -- Config List Dropdown (เก็บค่าที่เลือกไว้) + local ConfigListDropdown = section:AddDropdown("SaveManager_ConfigList", { + Title = "📋 Available Configs", + Values = self:RefreshConfigList(), + AllowNull = true, + Description = "Select a config to manage" + }) + + -- Autoload Status Display + local currentAutoload = self:GetAutoloadConfig() + local AutoloadToggle = section:AddToggle("SaveManager_AutoloadToggle", { + Title = "🔄 Auto Load", + Description = currentAutoload and ('Current: "' .. currentAutoload .. '"') or "No autoload config set", + Default = currentAutoload ~= nil, + Callback = function(value) + local selectedConfig = SaveManager.Options.SaveManager_ConfigList.Value + + if value then + -- เปิด Autoload + if not selectedConfig then + -- แก้ไข: ใช้ Options แทน Toggle object โดยตรง + SaveManager.Options.SaveManager_AutoloadToggle:SetValue(false) + return self.Library:Notify({ + Title = "Config Loader", + Content = "Error", + SubContent = "Please select a config first", + Duration = 3 + }) + end + + local success, err = self:SetAutoloadConfig(selectedConfig) + if success then + -- แก้ไข: อัพเดท Description + AutoloadToggle.Description = 'Current: "' .. selectedConfig .. '"' + self.Library:Notify({ + Title = "Config Loader", + Content = "Autoload Enabled", + SubContent = string.format('"%s" will load automatically', selectedConfig), + Duration = 3 + }) + else + SaveManager.Options.SaveManager_AutoloadToggle:SetValue(false) + self.Library:Notify({ + Title = "Config Loader", + Content = "Error", + SubContent = err or "Failed to set autoload", + Duration = 3 + }) + end + else + -- ปิด Autoload + local success, err = self:DisableAutoload() + if success then + AutoloadToggle.Description = "No autoload config set" + self.Library:Notify({ + Title = "Config Loader", + Content = "Autoload Disabled", + SubContent = "Configs will no longer auto-load", + Duration = 3 + }) + end + end + end + }) + + section:AddButton({ + Title = "💾 Save New Config", + Description = "Create a new configuration file", + Callback = function() + local name = SaveManager.Options.SaveManager_ConfigName.Value + + if name:gsub(" ", "") == "" then + return self.Library:Notify({ + Title = "Config Loader", + Content = "Invalid Name", + SubContent = "Config name cannot be empty", + Duration = 3 + }) + end + + local success, err = self:Save(name) + if not success then + return self.Library:Notify({ + Title = "Config Loader", + Content = "Save Failed", + SubContent = err or "Unknown error", + Duration = 5 + }) + end + + self.Library:Notify({ + Title = "Config Loader", + Content = "Config Saved", + SubContent = string.format('Created "%s"', name), + Duration = 3 + }) + + -- Refresh dropdown + ConfigListDropdown:SetValues(self:RefreshConfigList()) + ConfigListDropdown:SetValue(name) + end + }) + + section:AddButton({ + Title = "📂 Load Config", + Description = "Load selected configuration", + Callback = function() + local name = SaveManager.Options.SaveManager_ConfigList.Value + + if not name then + return self.Library:Notify({ + Title = "Config Loader", + Content = "No Config Selected", + SubContent = "Please select a config to load", + Duration = 3 + }) + end + + local success, err = self:Load(name) + if not success then + return self.Library:Notify({ + Title = "Config Loader", + Content = "Load Failed", + SubContent = err or "Unknown error", + Duration = 5 + }) + end + + self.Library:Notify({ + Title = "Config Loader", + Content = "Config Loaded", + SubContent = string.format('Loaded "%s"', name), + Duration = 3 + }) + end + }) + + section:AddButton({ + Title = "✏️ Overwrite Config", + Description = "Save current settings to selected config", + Callback = function() + local name = SaveManager.Options.SaveManager_ConfigList.Value + + if not name then + return self.Library:Notify({ + Title = "Config Loader", + Content = "No Config Selected", + SubContent = "Please select a config to overwrite", + Duration = 3 + }) + end + + local success, err = self:Save(name) + if not success then + return self.Library:Notify({ + Title = "Config Loader", + Content = "Save Failed", + SubContent = err or "Unknown error", + Duration = 5 + }) + end + + self.Library:Notify({ + Title = "Config Loader", + Content = "Config Updated", + SubContent = string.format('Overwrote "%s"', name), + Duration = 3 + }) + end + }) + + section:AddButton({ + Title = "🗑️ Delete Config", + Description = "Permanently delete selected config", + Callback = function() + local name = SaveManager.Options.SaveManager_ConfigList.Value + + if not name then + return self.Library:Notify({ + Title = "Config Loader", + Content = "No Config Selected", + SubContent = "Please select a config to delete", + Duration = 3 + }) + end + + -- Confirmation dialog + self.Library:Dialog({ + Title = "Delete Config", + Content = string.format('Are you sure you want to delete "%s"?', name), + Buttons = { + { + Title = "Delete", + Callback = function() + local success, err = self:Delete(name) + if not success then + return self.Library:Notify({ + Title = "Config Loader", + Content = "Delete Failed", + SubContent = err or "Unknown error", + Duration = 5 + }) + end + + self.Library:Notify({ + Title = "Config Loader", + Content = "Config Deleted", + SubContent = string.format('Deleted "%s"', name), + Duration = 3 + }) + + -- Update UI + ConfigListDropdown:SetValues(self:RefreshConfigList()) + ConfigListDropdown:SetValue(nil) + + -- Update autoload toggle if deleted config was autoload + local currentAutoload = self:GetAutoloadConfig() + if currentAutoload then + SaveManager.Options.SaveManager_AutoloadToggle:SetValue(true) + AutoloadToggle.Description = 'Current: "' .. currentAutoload .. '"' + else + SaveManager.Options.SaveManager_AutoloadToggle:SetValue(false) + AutoloadToggle.Description = "No autoload config set" + end + end + }, + { + Title = "Cancel" + } + } + }) + end + }) + + section:AddButton({ + Title = "🔄 Refresh List", + Description = "Update available configs list", + Callback = function() + local configs = self:RefreshConfigList() + ConfigListDropdown:SetValues(configs) + ConfigListDropdown:SetValue(nil) + + self.Library:Notify({ + Title = "Config Loader", + Content = "List Refreshed", + SubContent = string.format("Found %d config(s)", #configs), + Duration = 2 + }) + end + }) + + -- อย่าลืม Ignore UI controls ของ SaveManager เอง + SaveManager:SetIgnoreIndexes({ + "SaveManager_ConfigName", + "SaveManager_AutoloadToggle" + }) + end + + SaveManager:BuildFolderTree() +end + +return SaveManager diff --git a/walkjump.lua b/walkjump.lua new file mode 100644 index 0000000..8e5e479 --- /dev/null +++ b/walkjump.lua @@ -0,0 +1,272 @@ +local Section = Tabs.Players:AddSection("Speed & Jump") +-- wait for LocalPlayer if not ready (safe in LocalScript) +if not LocalPlayer or typeof(LocalPlayer) == "Instance" and LocalPlayer.ClassName == "" then + LocalPlayer = Players.LocalPlayer +end + +do + -- config + local enforcementRate = 0.1 -- วินาที (0.1 = 10 ครั้ง/วินาที) -> ตอบสนองดีขึ้น แต่ไม่เกินไป + local WalkMin, WalkMax = 8, 200 + local JumpMin, JumpMax = 10, 300 + + local DesiredWalkSpeed = 16 + local DesiredJumpPower = 50 + + local WalkEnabled = true + local JumpEnabled = true + + -- เก็บค่าเดิมของ humanoid (weak table ตาม instance) + local originalValues = setmetatable({}, { __mode = "k" }) + + local currentHumanoid = nil + local heartbeatConn = nil + local lastApplyTick = 0 + + local function clamp(v, a, b) + if v < a then return a end + if v > b then return b end + return v + end + + local function findHumanoid() + if not Players.LocalPlayer then return nil end + local char = Players.LocalPlayer.Character + if not char then return nil end + return char:FindFirstChildWhichIsA("Humanoid") + end + + local function saveOriginal(hum) + if not hum then return end + if not originalValues[hum] then + local ok, ws, jp, usejp = pcall(function() + return hum.WalkSpeed, hum.JumpPower, hum.UseJumpPower + end) + if ok then + originalValues[hum] = { WalkSpeed = ws or 16, JumpPower = jp or 50, UseJumpPower = usejp } + else + originalValues[hum] = { WalkSpeed = 16, JumpPower = 50, UseJumpPower = true } + end + end + end + + local function restoreOriginal(hum) + if not hum then return end + local orig = originalValues[hum] + if orig then + pcall(function() + if orig.UseJumpPower ~= nil then + hum.UseJumpPower = orig.UseJumpPower + end + hum.WalkSpeed = orig.WalkSpeed or 16 + hum.JumpPower = orig.JumpPower or 50 + end) + originalValues[hum] = nil + end + end + + local function applyToHumanoid(hum) + if not hum then return end + saveOriginal(hum) + + -- Walk + if WalkEnabled then + local desired = clamp(math.floor(DesiredWalkSpeed + 0.5), WalkMin, WalkMax) + if hum.WalkSpeed ~= desired then + pcall(function() hum.WalkSpeed = desired end) + end + end + + -- Jump: ensure UseJumpPower true, then set JumpPower + if JumpEnabled then + pcall(function() + -- set UseJumpPower true to ensure JumpPower is respected + if hum.UseJumpPower ~= true then + hum.UseJumpPower = true + end + end) + + local desiredJ = clamp(math.floor(DesiredJumpPower + 0.5), JumpMin, JumpMax) + if hum.JumpPower ~= desiredJ then + pcall(function() hum.JumpPower = desiredJ end) + end + end + end + + local function startEnforcement() + if heartbeatConn then return end + local acc = 0 + heartbeatConn = RunService.Heartbeat:Connect(function(dt) + acc = acc + dt + if acc < enforcementRate then return end + acc = 0 + + local hum = findHumanoid() + if hum then + currentHumanoid = hum + -- apply only when enabled; if both disabled, avoid applying + if WalkEnabled or JumpEnabled then + applyToHumanoid(hum) + end + else + -- no humanoid: clear currentHumanoid + currentHumanoid = nil + end + end) + end + + local function stopEnforcement() + if heartbeatConn then + heartbeatConn:Disconnect() + heartbeatConn = nil + end + end + + -- Toggle handlers + local function setWalkEnabled(v) + WalkEnabled = not not v + if WalkEnabled then + -- immediately apply + local hum = findHumanoid() + if hum then + applyToHumanoid(hum) + end + startEnforcement() + else + -- restore walk value on current humanoid if we recorded it + if currentHumanoid then + -- only restore WalkSpeed (not touching Jump here) + local orig = originalValues[currentHumanoid] + if orig and orig.WalkSpeed ~= nil then + pcall(function() currentHumanoid.WalkSpeed = orig.WalkSpeed end) + end + end + + -- if both disabled, we can stop enforcement and restore jump if needed + if not JumpEnabled then + if currentHumanoid then + restoreOriginal(currentHumanoid) + end + stopEnforcement() + end + end + end + + local function setJumpEnabled(v) + JumpEnabled = not not v + if JumpEnabled then + local hum = findHumanoid() + if hum then + applyToHumanoid(hum) + end + startEnforcement() + else + if currentHumanoid then + -- restore JumpPower and UseJumpPower + local orig = originalValues[currentHumanoid] + if orig and (orig.JumpPower ~= nil or orig.UseJumpPower ~= nil) then + pcall(function() + if orig.UseJumpPower ~= nil then + currentHumanoid.UseJumpPower = orig.UseJumpPower + end + if orig.JumpPower ~= nil then + currentHumanoid.JumpPower = orig.JumpPower + end + end) + end + end + + if not WalkEnabled then + if currentHumanoid then + restoreOriginal(currentHumanoid) + end + stopEnforcement() + end + end + end + + -- sliders callbacks + local function setWalkSpeed(v) + DesiredWalkSpeed = clamp(v, WalkMin, WalkMax) + if WalkEnabled then + local hum = findHumanoid() + if hum then applyToHumanoid(hum) end + startEnforcement() + end + end + + local function setJumpPower(v) + DesiredJumpPower = clamp(v, JumpMin, JumpMax) + if JumpEnabled then + local hum = findHumanoid() + if hum then applyToHumanoid(hum) end + startEnforcement() + end + end + + -- CharacterAdded handling to apply as soon as possible + if Players.LocalPlayer then + Players.LocalPlayer.CharacterAdded:Connect(function(char) + -- small wait for humanoid to exist + local hum = nil + for i = 1, 20 do + hum = char:FindFirstChildWhichIsA("Humanoid") + if hum then break end + task.wait(0.05) + end + if hum and (WalkEnabled or JumpEnabled) then + applyToHumanoid(hum) + startEnforcement() + end + end) + end + + -- UI + local speedSlider = Section:AddSlider("WalkSpeedSlider", { + Title = "WalkSpeed", + Default = DesiredWalkSpeed, Min = WalkMin, Max = WalkMax, Rounding = 0, + Callback = function(Value) setWalkSpeed(Value) end + }) + speedSlider:OnChanged(setWalkSpeed) + + local jumpSlider = Section:AddSlider("JumpPowerSlider", { + Title = "JumpPower", + Default = DesiredJumpPower, Min = JumpMin, Max = JumpMax, Rounding = 0, + Callback = function(Value) setJumpPower(Value) end + }) + jumpSlider:OnChanged(setJumpPower) + + local walkToggle = Section:AddToggle("EnableWalkToggle", { + Title = "Enable Walk", + Description = "เปิด/ปิดการบังคับ WalkSpeed", + Default = WalkEnabled, + Callback = function(value) setWalkEnabled(value) end + }) + walkToggle:OnChanged(setWalkEnabled) + + local jumpToggle = Section:AddToggle("EnableJumpToggle", { + Title = "Enable Jump", + Description = "เปิด/ปิดการบังคับ JumpPower", + Default = JumpEnabled, + Callback = function(value) setJumpEnabled(value) end + }) + jumpToggle:OnChanged(setJumpEnabled) + + Section:AddButton({ + Title = "Reset to defaults", + Description = "คืนค่า Walk/Jump ไปค่าเริ่มต้น (16, 50)", + Callback = function() + DesiredWalkSpeed = 16 + DesiredJumpPower = 50 + speedSlider:SetValue(DesiredWalkSpeed) + jumpSlider:SetValue(DesiredJumpPower) + if WalkEnabled or JumpEnabled then + local hum = findHumanoid() + if hum then applyToHumanoid(hum) end + end + end + }) + + -- start enforcement if either is enabled initially + if WalkEnabled or JumpEnabled then startEnforcement() end +end