From ca6d06aad840a9dd395a74dae103af072c6cd38f Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Wed, 8 Oct 2025 20:28:23 +0700 Subject: [PATCH 01/76] Create EPS.lua --- EPS.lua | 495 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 495 insertions(+) create mode 100644 EPS.lua diff --git a/EPS.lua b/EPS.lua new file mode 100644 index 0000000..b664039 --- /dev/null +++ b/EPS.lua @@ -0,0 +1,495 @@ +-- Optimized ESP (Single update loop, pooling, UI hooks, save/load) +local Players = game:GetService("Players") +local RunService = game:GetService("RunService") +local HttpService = game:GetService("HttpService") +local Workspace = game:GetService("Workspace") +local LocalPlayer = Players.LocalPlayer +local Camera = Workspace.CurrentCamera + +-- Detect Drawing API if available (exploit env). If your safeNewDrawing exists, use it. +local hasDrawing = (typeof(Drawing) == "table") or (type(safeNewDrawing) == "function") +local useSafeNewDrawing = (type(safeNewDrawing) == "function") + +-- Defaults +local DEFAULT_CONFIG = { + enabled = false, + renderMode = "2D", -- "2D" (Drawing) or "Billboard" + updateRate = 10, -- updates per second + maxDistance = 250, -- meters + showName = true, + showHealth = true, + showDistance = true, + textSize = 16, + espColor = Color3.fromRGB(255, 50, 50), + boxThickness = 1, + labelOffsetY = 2.6, -- studs (for billboard) + highlightEnabled = true, + highlightFillTransparency = 0.7, + highlightOutlineTransparency = 0.4, + occlusionCheck = false, -- raycast check (may be expensive if enabled) + hideIfCentered = false, -- if true, hide ESP when near screen center (reduce obstruction) + hideCenterRadius = 0.12, -- fraction of screen (0..0.5) +} + +-- Config persistence file name (for exploit runners) +local CONFIG_FILENAME = "atg_esp_config.json" + +-- state +local state = { + config = DEFAULT_CONFIG, + espTable = {}, -- [player] = info + running = false, +} + +-- helper: deep copy table +local function deepCopy(t) + local nt = {} + for k,v in pairs(t) do + if type(v) == "table" then nt[k] = deepCopy(v) else nt[k] = v end + end + return nt +end + +-- Load config (supports writefile/readfile in exploit env; falls back to _G) +local function SaveConfig() + local ok, err = pcall(function() + local json = HttpService:JSONEncode(state.config) + if writefile then + writefile(CONFIG_FILENAME, json) + else + _G.ATG_ESP_SAVED_CONFIG = json + end + end) + if not ok then warn("ESP: SaveConfig failed:", err) end +end + +local function LoadConfig() + local ok, content = pcall(function() + if readfile and isfile and isfile(CONFIG_FILENAME) then + return readfile(CONFIG_FILENAME) + else + return _G and _G.ATG_ESP_SAVED_CONFIG + end + end) + if ok and content then + local suc, decoded = pcall(function() return HttpService:JSONDecode(content) end) + if suc and type(decoded) == "table" then + -- merge with defaults + local merged = deepCopy(DEFAULT_CONFIG) + for k,v in pairs(decoded) do merged[k] = v end + state.config = merged + return + end + end + -- fallback default + state.config = deepCopy(DEFAULT_CONFIG) +end + +-- Reset config to defaults +local function ResetConfig() + state.config = deepCopy(DEFAULT_CONFIG) + SaveConfig() +end + +-- Utility: safe property set to avoid expensive updates if not necessary +local function safeSet(obj, prop, value) + if not obj then return end + if obj[prop] ~= value then + obj[prop] = value + end +end + +-- Create or recycle ESP visuals for a player +local function createESPForPlayer(player) + if not player or player == LocalPlayer then return end + if state.espTable[player] then return end + + local info = { + player = player, + billboard = nil, -- BillboardGui (if used) + label = nil, + drawings = nil, -- {box, text, line} + highlight = nil, + lastVisible = false, + lastText = "", + lastColor = nil, + charConn = nil, + } + state.espTable[player] = info + + -- cleanup function + local function cleanup() + pcall(function() + if info.billboard then info.billboard:Destroy() info.billboard = nil end + if info.label then info.label:Destroy() info.label = nil end + if info.drawings then + for _,d in pairs(info.drawings) do + if d and type(d.Destroy) == "function" then + d:Remove() -- Drawing API uses Remove() + elseif d and type(d) == "userdata" then + -- fallback + pcall(function() d:Remove() end) + end + end + info.drawings = nil + end + if info.highlight then + info.highlight:Destroy() + info.highlight = nil + end + if info.charConn then info.charConn:Disconnect() info.charConn = nil end + end) + state.espTable[player] = nil + end + + -- create highlight object (but don't enable Adornee until char exists) + if state.config.highlightEnabled then + local ok, h = pcall(function() + local hl = Instance.new("Highlight") + hl.Name = "ATG_ESP_Highlight" + hl.FillColor = state.config.espColor + hl.FillTransparency = state.config.highlightFillTransparency + hl.OutlineTransparency = state.config.highlightOutlineTransparency + hl.Enabled = false + hl.Parent = LocalPlayer:FindFirstChildOfClass("PlayerGui") or game:GetService("CoreGui") -- safe parent + return hl + end) + if ok and h then info.highlight = h end + end + + -- create billboard if mode is Billboard + if state.config.renderMode == "Billboard" then + local function createBillboard(char) + if not char or not char.Parent then return end + local head = char:FindFirstChild("Head") + if not head then return end + + pcall(function() + if info.billboard then info.billboard:Destroy() info.billboard = nil end + local bb = Instance.new("BillboardGui") + bb.Name = "ATG_ESP_BB" + bb.Size = UDim2.new(0, 200, 0, 36) + bb.AlwaysOnTop = true + bb.StudsOffset = Vector3.new(0, state.config.labelOffsetY, 0) + bb.Adornee = head + bb.Parent = head + + local label = Instance.new("TextLabel") + label.Name = "ATG_ESP_LABEL" + label.Size = UDim2.fromScale(1,1) + label.BackgroundTransparency = 1 + label.TextScaled = true + label.Font = Enum.Font.GothamBold + label.Text = "" + label.TextColor3 = state.config.espColor + label.TextStrokeTransparency = 0.4 + label.Parent = bb + + info.billboard = bb + info.label = label + end) + end + + -- attach to existing character + if player.Character and player.Character.Parent then + createBillboard(player.Character) + end + -- reconnect on respawn + info.charConn = player.CharacterAdded:Connect(function(char) + task.wait(0.05) + if state.config.renderMode == "Billboard" then createBillboard(char) end + if info.highlight and state.config.highlightEnabled then + pcall(function() info.highlight.Adornee = char end) + info.highlight.Enabled = state.config.enabled + end + end) + else + -- Drawing mode (2D). Create drawing objects now (will be positioned each update) + if hasDrawing then + local ok, d = pcall(function() + local box, nameText + if useSafeNewDrawing then + box = safeNewDrawing("Square", {Thickness = state.config.boxThickness, Filled = false, Visible = false}) + nameText = safeNewDrawing("Text", {Size = state.config.textSize, Center = true, Outline = true, Visible = false, Text = player.Name}) + else + box = Drawing.new("Square") + box.Thickness = state.config.boxThickness + box.Filled = false + box.Visible = false + nameText = Drawing.new("Text") + nameText.Size = state.config.textSize + nameText.Center = true + nameText.Outline = true + nameText.Visible = false + nameText.Text = player.Name + end + return {box = box, text = nameText} + end) + if ok and d then info.drawings = d end + end + -- connect charAdded to set highlight adornee + info.charConn = player.CharacterAdded:Connect(function(char) + task.wait(0.05) + if info.highlight and state.config.highlightEnabled then + pcall(function() info.highlight.Adornee = char end) + info.highlight.Enabled = state.config.enabled + end + end) + end + + -- PlayerRemoving cleanup hook (in case this function called before global hook) + player.AncestryChanged:Connect(function() + if not player.Parent then + cleanup() + end + end) + + -- expose cleanup in info for external use + info.cleanup = cleanup +end + +local function removeESPForPlayer(player) + local info = state.espTable[player] + if not info then return end + pcall(function() + if info.cleanup then info.cleanup() end + end) +end + +-- Global players hooks +Players.PlayerAdded:Connect(function(p) + -- Small delay to allow Player object to fully init + task.defer(function() + if state.config.enabled and p ~= LocalPlayer then + createESPForPlayer(p) + end + end) +end) +Players.PlayerRemoving:Connect(function(p) + removeESPForPlayer(p) +end) + +-- initial spawn: create entries for existing players +local function initPlayers() + for _, p in ipairs(Players:GetPlayers()) do + if p ~= LocalPlayer and state.config.enabled then + createESPForPlayer(p) + end + end +end + +-- Utility: check if part is visible (occlusion) - optional and may be expensive if many checks +local function isVisibleFromCamera(worldPos) + if not state.config.occlusionCheck then return true end + local origin = Camera.CFrame.Position + local direction = (worldPos - origin) + local rayParams = RaycastParams.new() + rayParams.FilterDescendantsInstances = {LocalPlayer.Character or Workspace} + rayParams.FilterType = Enum.RaycastFilterType.Blacklist + rayParams.IgnoreWater = true + local result = Workspace:Raycast(origin, direction.Unit * math.clamp(direction.Magnitude, 0, 1000), rayParams) + if not result then return true end + -- if hit point is very close to worldPos, consider visible + local hitPos = result.Position + return (hitPos - worldPos).Magnitude < 0.5 +end + +-- Build label text from toggles +local function buildLabelText(player) + local parts = {} + if state.config.showName then table.insert(parts, player.DisplayName or player.Name) end + if state.config.showHealth then + local hum = player.Character and player.Character:FindFirstChildOfClass("Humanoid") + if hum then table.insert(parts, "HP:" .. tostring(math.floor(hum.Health))) end + end + if state.config.showDistance then + local myHRP = LocalPlayer.Character and LocalPlayer.Character:FindFirstChild("HumanoidRootPart") + local theirHRP = player.Character and player.Character:FindFirstChild("HumanoidRootPart") + 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 + +-- Single update loop (throttled by config.updateRate) +local accum = 0 +local function startLoop() + if state.running then return end + state.running = true + accum = 0 + RunService.Heartbeat:Connect(function(dt) + if not state.running then return end + accum = accum + dt + local rate = math.clamp(tonumber(state.config.updateRate) or DEFAULT_CONFIG.updateRate, 1, 60) + local interval = 1 / rate + if accum < interval then return end + accum = 0 + + -- iterate players + local camCFrame = Camera.CFrame + local viewportSize = Camera.ViewportSize + for player, info in pairs(state.espTable) do + local visible = false + local labelText = "" + if not player or not player.Character or not player.Character.Parent then + -- ensure visuals hidden + if info.label then safeSet(info.label, "Text", "") end + if info.drawings then + if info.drawings.box then info.drawings.box.Visible = false end + if info.drawings.text then info.drawings.text.Visible = false end + end + if info.billboard then info.billboard.Enabled = false end + if info.highlight then info.highlight.Enabled = false end + else + local hrp = player.Character:FindFirstChild("HumanoidRootPart") or player.Character:FindFirstChild("Head") + if hrp then + local dist = (Camera.CFrame.Position - hrp.Position).Magnitude + if dist <= state.config.maxDistance then + local screenPos, onScreen = Camera:WorldToViewportPoint(hrp.Position + Vector3.new(0, state.config.labelOffsetY, 0)) + -- hide if offscreen or behind camera + if onScreen then + -- optional: hide when it's near center to avoid obstruction + local hideCenter = false + if state.config.hideIfCentered then + local cx = screenPos.X / viewportSize.X - 0.5 + local cy = screenPos.Y / viewportSize.Y - 0.5 + local radius = state.config.hideCenterRadius + hideCenter = (math.abs(cx) < radius and math.abs(cy) < radius) + end + + if not hideCenter then + -- occlusion check (optional) + if isVisibleFromCamera(hrp.Position) then + visible = true + end + end + end + end + end + labelText = buildLabelText(player) + end + + -- apply visuals based on renderMode + if state.config.renderMode == "Billboard" then + if info.billboard and info.label then + info.billboard.Enabled = visible and state.config.enabled + if visible and state.config.enabled then + if info.label.Text ~= labelText then info.label.Text = labelText end + if info.label.TextColor3 ~= state.config.espColor then info.label.TextColor3 = state.config.espColor end + end + end + else + if info.drawings then + -- Drawing objects expect 2D coords + if visible and state.config.enabled then + local hrp = player.Character and (player.Character:FindFirstChild("Head") or player.Character:FindFirstChild("HumanoidRootPart")) + if hrp then + local pos3 = hrp.Position + Vector3.new(0, state.config.labelOffsetY, 0) + local screenPos, onScreen = Camera:WorldToViewportPoint(pos3) + if onScreen then + local x = screenPos.X + local y = screenPos.Y + local t = info.drawings.text + local b = info.drawings.box + if t then + t.Text = labelText + t.Position = Vector2.new(x, y - (state.config.textSize)) + t.Color = state.config.espColor + t.Size = state.config.textSize + t.Visible = true + end + if b then + -- small box around head (approx) + local size = state.config.boxThickness * 20 + b.Position = Vector2.new(x - size/2, y - size/2) + b.Size = Vector2.new(size, size) + b.Color = state.config.espColor + b.Visible = true + end + else + -- offscreen + if info.drawings.text then info.drawings.text.Visible = false end + if info.drawings.box then info.drawings.box.Visible = false end + end + end + else + if info.drawings.text then info.drawings.text.Visible = false end + if info.drawings.box then info.drawings.box.Visible = false end + end + end + end + + -- highlight enable/disable + if info.highlight then + info.highlight.Enabled = visible and state.config.highlightEnabled and state.config.enabled + -- update color/transparency if changed + safeSet(info.highlight, "FillColor", state.config.espColor) + safeSet(info.highlight, "FillTransparency", state.config.highlightFillTransparency) + safeSet(info.highlight, "OutlineTransparency", state.config.highlightOutlineTransparency) + end + end + end) +end + +-- Start/Stop ESP +local function enableESP(val) + state.config.enabled = val and true or false + if state.config.enabled then + -- create for existing players + initPlayers() + -- enable highlights adornee for those with characters + for p, info in pairs(state.espTable) do + if info.highlight and p.Character then + pcall(function() info.highlight.Adornee = p.Character end) + end + end + startLoop() + else + -- disable visuals but keep objects (so re-enabling is fast) + for p, info in pairs(state.espTable) do + if info.billboard then info.billboard.Enabled = false end + if info.drawings then + if info.drawings.text then info.drawings.text.Visible = false end + if info.drawings.box then info.drawings.box.Visible = false end + end + if info.highlight then info.highlight.Enabled = false end + end + end +end + +-- Expose Save/Load/Reset functions and UI binding examples +-- UI integration (example): hook your Tabs.ESP UI elements to these functions +-- Example usage with provided UI API: +-- local espToggle = Tabs.ESP:AddToggle("ESPToggle", { Title = "ESP", Default = state.config.enabled }) +-- espToggle:OnChanged(function(v) enableESP(v) SaveConfig() end) +-- local rateSlider = Tabs.ESP:AddSlider("ESP_UpdateRate", { Title="Update Rate", Default = state.config.updateRate, Min = 1, Max = 60, Rounding = 1 }) +-- rateSlider:OnChanged(function(v) state.config.updateRate = v SaveConfig() end) +-- local distSlider = Tabs.ESP:AddSlider("ESP_MaxDist", { Title="Max Distance", Default = state.config.maxDistance, Min = 50, Max = 1000, Rounding = 1 }) +-- distSlider:OnChanged(function(v) state.config.maxDistance = v SaveConfig() end) +-- local colorPicker = Tabs.ESP:AddColorpicker("ESP_Color", { Title = "ESP Color", Default = state.config.espColor }) +-- colorPicker:OnChanged(function(c) state.config.espColor = c SaveConfig() end) +-- Tabs.ESP:AddToggle("ESP_Highlight", { Title = "Highlight", Default = state.config.highlightEnabled }):OnChanged(function(v) state.config.highlightEnabled = v SaveConfig() end) +-- Tabs.ESP:AddButton({ Title = "Reset ESP Config", Description = "Reset to defaults", Callback = function() ResetConfig() end }) +-- Tabs.ESP:AddButton({ Title = "Save Config", Description = "Save current config", Callback = function() SaveConfig() end }) +-- Tabs.ESP:AddButton({ Title = "Load Config", Description = "Load saved config", Callback = function() LoadConfig() enableESP(state.config.enabled) end }) + +-- Load saved config then start with current value +LoadConfig() +-- (example) If you want ESP enabled by default from saved config: +if state.config.enabled then + enableESP(true) +end + +-- Provide API for external scripts to control quickly: +return { + Enable = enableESP, + CreateForPlayer = createESPForPlayer, + RemoveForPlayer = removeESPForPlayer, + SaveConfig = SaveConfig, + LoadConfig = LoadConfig, + ResetConfig = ResetConfig, + GetConfig = function() return deepCopy(state.config) end, +} + From 94025b2a42b0d8e3f8f94135989c90e315742d0b Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Wed, 8 Oct 2025 20:28:41 +0700 Subject: [PATCH 02/76] Rename EPS.lua to ESP.lua --- EPS.lua => ESP.lua | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename EPS.lua => ESP.lua (100%) diff --git a/EPS.lua b/ESP.lua similarity index 100% rename from EPS.lua rename to ESP.lua From 2eb1f680541ebda22cedfeb3abe315189cf5a859 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Wed, 8 Oct 2025 20:49:42 +0700 Subject: [PATCH 03/76] Update ESP.lua --- ESP.lua | 806 +++++++++++++++++++++++++++----------------------------- 1 file changed, 391 insertions(+), 415 deletions(-) diff --git a/ESP.lua b/ESP.lua index b664039..5e1f330 100644 --- a/ESP.lua +++ b/ESP.lua @@ -1,310 +1,252 @@ --- Optimized ESP (Single update loop, pooling, UI hooks, save/load) +-- ATG ESP (optimized) +-- หลักการสำคัญ: +-- 1) ใช้ centralized update loop (Heartbeat) ที่ throttle ด้วย updateInterval (ไม่สร้าง RenderStepped per-player) +-- 2) Culling: ระยะ, อยู่ในหน้าจอ, (optionally) raycast สำหรับ visibility แบบ throttled +-- 3) Object pooling สำหรับ BillboardGui และ Highlight เพื่อลดการสร้าง/ทำลายบ่อย ๆ +-- 4) เก็บ state/config ที่ UI สามารถแก้ได้ และมี Reset/Presets +-- 5) เช็ค cleanup เมื่อ PlayerLeft หรือ Unload + local Players = game:GetService("Players") local RunService = game:GetService("RunService") -local HttpService = game:GetService("HttpService") -local Workspace = game:GetService("Workspace") +local UIS = game:GetService("UserInputService") local LocalPlayer = Players.LocalPlayer -local Camera = Workspace.CurrentCamera - --- Detect Drawing API if available (exploit env). If your safeNewDrawing exists, use it. -local hasDrawing = (typeof(Drawing) == "table") or (type(safeNewDrawing) == "function") -local useSafeNewDrawing = (type(safeNewDrawing) == "function") +local Camera = workspace.CurrentCamera --- Defaults -local DEFAULT_CONFIG = { +-- state + config (เก็บค่า default ที่เหมาะสม) +local state = state or {} -- บันทึกข้ามสคริปต์ถ้ามี +state.espTable = state.espTable or {} -- map userId -> info +state.pools = state.pools or {billboards = {}, highlights = {}} +state.config = state.config or { enabled = false, - renderMode = "2D", -- "2D" (Drawing) or "Billboard" - updateRate = 10, -- updates per second - maxDistance = 250, -- meters + updateRate = 10, -- updates per second (10 => 0.1s interval) + maxDistance = 250, -- เมตร ในเกม units + maxVisibleCount = 60, -- limit จำนวน ESP แสดงพร้อมกัน (ป้องกัน overload) showName = true, showHealth = true, showDistance = true, - textSize = 16, espColor = Color3.fromRGB(255, 50, 50), - boxThickness = 1, - labelOffsetY = 2.6, -- studs (for billboard) - highlightEnabled = true, - highlightFillTransparency = 0.7, - highlightOutlineTransparency = 0.4, - occlusionCheck = false, -- raycast check (may be expensive if enabled) - hideIfCentered = false, -- if true, hide ESP when near screen center (reduce obstruction) - hideCenterRadius = 0.12, -- fraction of screen (0..0.5) -} - --- Config persistence file name (for exploit runners) -local CONFIG_FILENAME = "atg_esp_config.json" - --- state -local state = { - config = DEFAULT_CONFIG, - espTable = {}, -- [player] = info - running = false, + labelScale = 1, -- scale ของข้อความ (TextScaled true จะอิงขนาด parent) + alwaysOnTop = false, -- BillboardGui.AlwaysOnTop (ถ้า true จะไม่ถูกบัง) + smartHideCenter = true, -- ซ่อน label ถ้ามันบังหน้าจอกลาง (ปรับได้) + centerHideRadius = 0.12, -- % screen radius จาก center ที่จะซ่อน (0.12 => 12%) + raycastOcclusion = false, -- ถ้าต้องการเพิ่ม check line-of-sight (ถ้าเปิดจะทำงานแบบ throttled) + raycastInterval = 0.6, -- วินาทีต่อการ raycast ต่อตัว (ถ้าเปิด) + highlightEnabled = false, + highlightFillTransparency = 0.6, + highlightOutlineTransparency = 0.6, + teamCheck = true, -- ไม่แสดง ESP ของเพื่อนร่วมทีม (ถ้าเกมมีทีม) + ignoreLocalPlayer = true, -- ไม่แสดงตัวเอง } --- helper: deep copy table -local function deepCopy(t) - local nt = {} - for k,v in pairs(t) do - if type(v) == "table" then nt[k] = deepCopy(v) else nt[k] = v end +-- pooling helpers +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 - return nt end --- Load config (supports writefile/readfile in exploit env; falls back to _G) -local function SaveConfig() - local ok, err = pcall(function() - local json = HttpService:JSONEncode(state.config) - if writefile then - writefile(CONFIG_FILENAME, json) - else - _G.ATG_ESP_SAVED_CONFIG = json - 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) - if not ok then warn("ESP: SaveConfig failed:", err) end + table.insert(state.pools.billboards, obj) end -local function LoadConfig() - local ok, content = pcall(function() - if readfile and isfile and isfile(CONFIG_FILENAME) then - return readfile(CONFIG_FILENAME) - else - return _G and _G.ATG_ESP_SAVED_CONFIG - end - end) - if ok and content then - local suc, decoded = pcall(function() return HttpService:JSONDecode(content) end) - if suc and type(decoded) == "table" then - -- merge with defaults - local merged = deepCopy(DEFAULT_CONFIG) - for k,v in pairs(decoded) do merged[k] = v end - state.config = merged - return - 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 - -- fallback default - state.config = deepCopy(DEFAULT_CONFIG) end --- Reset config to defaults -local function ResetConfig() - state.config = deepCopy(DEFAULT_CONFIG) - SaveConfig() +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 --- Utility: safe property set to avoid expensive updates if not necessary -local function safeSet(obj, prop, value) - if not obj then return end - if obj[prop] ~= value then - obj[prop] = value +-- helper utilities +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) + -- best-effort: ถ้าตัวเกมมี Team, compare Team property + 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 --- Create or recycle ESP visuals for a player -local function createESPForPlayer(player) - if not player or player == LocalPlayer then return end - if state.espTable[player] then return end +-- create / remove logic (doesn't connect RenderStepped per-player) +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 = player, - billboard = nil, -- BillboardGui (if used) - label = nil, - drawings = nil, -- {box, text, line} - highlight = nil, + player = p, + billboardObj = nil, -- {billboard,label} + highlightObj = nil, -- Highlight instance lastVisible = false, - lastText = "", - lastColor = nil, - charConn = nil, + lastScreenPos = Vector2.new(0,0), + lastDistance = math.huge, + lastRaycast = -999, + connected = true, -- if we have CharacterAdded connection + charConn = nil } - state.espTable[player] = info - - -- cleanup function - local function cleanup() - pcall(function() - if info.billboard then info.billboard:Destroy() info.billboard = nil end - if info.label then info.label:Destroy() info.label = nil end - if info.drawings then - for _,d in pairs(info.drawings) do - if d and type(d.Destroy) == "function" then - d:Remove() -- Drawing API uses Remove() - elseif d and type(d) == "userdata" then - -- fallback - pcall(function() d:Remove() end) - end - end - info.drawings = nil - end - if info.highlight then - info.highlight:Destroy() - info.highlight = nil - end - if info.charConn then info.charConn:Disconnect() info.charConn = nil end - end) - state.espTable[player] = nil - end - - -- create highlight object (but don't enable Adornee until char exists) - if state.config.highlightEnabled then - local ok, h = pcall(function() - local hl = Instance.new("Highlight") - hl.Name = "ATG_ESP_Highlight" - hl.FillColor = state.config.espColor - hl.FillTransparency = state.config.highlightFillTransparency - hl.OutlineTransparency = state.config.highlightOutlineTransparency - hl.Enabled = false - hl.Parent = LocalPlayer:FindFirstChildOfClass("PlayerGui") or game:GetService("CoreGui") -- safe parent - return hl - end) - if ok and h then info.highlight = h end - end - - -- create billboard if mode is Billboard - if state.config.renderMode == "Billboard" then - local function createBillboard(char) - if not char or not char.Parent then return end - local head = char:FindFirstChild("Head") - if not head then return end - - pcall(function() - if info.billboard then info.billboard:Destroy() info.billboard = nil end - local bb = Instance.new("BillboardGui") - bb.Name = "ATG_ESP_BB" - bb.Size = UDim2.new(0, 200, 0, 36) - bb.AlwaysOnTop = true - bb.StudsOffset = Vector3.new(0, state.config.labelOffsetY, 0) - bb.Adornee = head - bb.Parent = head - - local label = Instance.new("TextLabel") - label.Name = "ATG_ESP_LABEL" - label.Size = UDim2.fromScale(1,1) - label.BackgroundTransparency = 1 - label.TextScaled = true - label.Font = Enum.Font.GothamBold - label.Text = "" - label.TextColor3 = state.config.espColor - label.TextStrokeTransparency = 0.4 - label.Parent = bb - - info.billboard = bb - info.label = label - end) - end - -- attach to existing character - if player.Character and player.Character.Parent then - createBillboard(player.Character) - end - -- reconnect on respawn - info.charConn = player.CharacterAdded:Connect(function(char) - task.wait(0.05) - if state.config.renderMode == "Billboard" then createBillboard(char) end - if info.highlight and state.config.highlightEnabled then - pcall(function() info.highlight.Adornee = char end) - info.highlight.Enabled = state.config.enabled + -- connect character added to reattach Adornee when respawn + info.charConn = p.CharacterAdded:Connect(function(char) + -- small delay ให้เวลา Head สร้าง + 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) - else - -- Drawing mode (2D). Create drawing objects now (will be positioned each update) - if hasDrawing then - local ok, d = pcall(function() - local box, nameText - if useSafeNewDrawing then - box = safeNewDrawing("Square", {Thickness = state.config.boxThickness, Filled = false, Visible = false}) - nameText = safeNewDrawing("Text", {Size = state.config.textSize, Center = true, Outline = true, Visible = false, Text = player.Name}) - else - box = Drawing.new("Square") - box.Thickness = state.config.boxThickness - box.Filled = false - box.Visible = false - nameText = Drawing.new("Text") - nameText.Size = state.config.textSize - nameText.Center = true - nameText.Outline = true - nameText.Visible = false - nameText.Text = player.Name - end - return {box = box, text = nameText} - end) - if ok and d then info.drawings = d end end - -- connect charAdded to set highlight adornee - info.charConn = player.CharacterAdded:Connect(function(char) - task.wait(0.05) - if info.highlight and state.config.highlightEnabled then - pcall(function() info.highlight.Adornee = char end) - info.highlight.Enabled = state.config.enabled - end - end) - end - - -- PlayerRemoving cleanup hook (in case this function called before global hook) - player.AncestryChanged:Connect(function() - if not player.Parent then - cleanup() + if info.highlightObj then + info.highlightObj.Adornee = char end end) - -- expose cleanup in info for external use - info.cleanup = cleanup + state.espTable[uid] = info + return info end -local function removeESPForPlayer(player) - local info = state.espTable[player] +local function cleanupEntry(uid) + local info = state.espTable[uid] if not info then return end pcall(function() - if info.cleanup then info.cleanup() end + 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 --- Global players hooks -Players.PlayerAdded:Connect(function(p) - -- Small delay to allow Player object to fully init - task.defer(function() - if state.config.enabled and p ~= LocalPlayer then - createESPForPlayer(p) +-- visibility check (distance + on-screen + optional raycast) +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 + + -- screen check + 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 + + -- smartCenter hide: ถ้า label อยู่ใกล้หน้าจอกลางมากเกินไป + if state.config.smartHideCenter then + local sx = screenPos.X / Camera.ViewportSize.X + local sy = screenPos.Y / Camera.ViewportSize.Y + local cx = 0.5 + local cy = 0.5 + local dx = sx - cx + local dy = sy - cy + local d = math.sqrt(dx*dx + dy*dy) + if d < state.config.centerHideRadius then + return false end - end) -end) -Players.PlayerRemoving:Connect(function(p) - removeESPForPlayer(p) -end) + end --- initial spawn: create entries for existing players -local function initPlayers() - for _, p in ipairs(Players:GetPlayers()) do - if p ~= LocalPlayer and state.config.enabled then - createESPForPlayer(p) + -- optional throttled raycast occlusion (ถ้าเปิด) + 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) + -- ถ้าวัตถุติดกันและไม่ใช่ตัวเป้าหมาย แปลว่าไม่ visible + if r and r.Instance and not r.Instance:IsDescendantOf(p.Character) then + return false + end + else + -- ใช้ last known result (ไม่ทำ raycast ทุก frame) + -- ถ้าไม่มี last result ให้อนุรักษ์ default true end end -end --- Utility: check if part is visible (occlusion) - optional and may be expensive if many checks -local function isVisibleFromCamera(worldPos) - if not state.config.occlusionCheck then return true end - local origin = Camera.CFrame.Position - local direction = (worldPos - origin) - local rayParams = RaycastParams.new() - rayParams.FilterDescendantsInstances = {LocalPlayer.Character or Workspace} - rayParams.FilterType = Enum.RaycastFilterType.Blacklist - rayParams.IgnoreWater = true - local result = Workspace:Raycast(origin, direction.Unit * math.clamp(direction.Magnitude, 0, 1000), rayParams) - if not result then return true end - -- if hit point is very close to worldPos, consider visible - local hitPos = result.Position - return (hitPos - worldPos).Magnitude < 0.5 + return true end --- Build label text from toggles -local function buildLabelText(player) +-- update label content (only when changed) +local function buildLabelText(p) local parts = {} - if state.config.showName then table.insert(parts, player.DisplayName or player.Name) end - if state.config.showHealth then - local hum = player.Character and player.Character:FindFirstChildOfClass("Humanoid") - if hum then table.insert(parts, "HP:" .. tostring(math.floor(hum.Health))) end + 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 = LocalPlayer.Character and LocalPlayer.Character:FindFirstChild("HumanoidRootPart") - local theirHRP = player.Character and player.Character:FindFirstChild("HumanoidRootPart") + 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]") @@ -313,183 +255,217 @@ local function buildLabelText(player) return table.concat(parts, " | ") end --- Single update loop (throttled by config.updateRate) -local accum = 0 -local function startLoop() - if state.running then return end - state.running = true - accum = 0 - RunService.Heartbeat:Connect(function(dt) - if not state.running then return end - accum = accum + dt - local rate = math.clamp(tonumber(state.config.updateRate) or DEFAULT_CONFIG.updateRate, 1, 60) - local interval = 1 / rate - if accum < interval then return end - accum = 0 - - -- iterate players - local camCFrame = Camera.CFrame - local viewportSize = Camera.ViewportSize - for player, info in pairs(state.espTable) do - local visible = false - local labelText = "" - if not player or not player.Character or not player.Character.Parent then - -- ensure visuals hidden - if info.label then safeSet(info.label, "Text", "") end - if info.drawings then - if info.drawings.box then info.drawings.box.Visible = false end - if info.drawings.text then info.drawings.text.Visible = false end - end - if info.billboard then info.billboard.Enabled = false end - if info.highlight then info.highlight.Enabled = false end - else - local hrp = player.Character:FindFirstChild("HumanoidRootPart") or player.Character:FindFirstChild("Head") - if hrp then - local dist = (Camera.CFrame.Position - hrp.Position).Magnitude - if dist <= state.config.maxDistance then - local screenPos, onScreen = Camera:WorldToViewportPoint(hrp.Position + Vector3.new(0, state.config.labelOffsetY, 0)) - -- hide if offscreen or behind camera - if onScreen then - -- optional: hide when it's near center to avoid obstruction - local hideCenter = false - if state.config.hideIfCentered then - local cx = screenPos.X / viewportSize.X - 0.5 - local cy = screenPos.Y / viewportSize.Y - 0.5 - local radius = state.config.hideCenterRadius - hideCenter = (math.abs(cx) < radius and math.abs(cy) < radius) - end - - if not hideCenter then - -- occlusion check (optional) - if isVisibleFromCamera(hrp.Position) then - visible = true - end - end - end - end - end - labelText = buildLabelText(player) +-- main centralized updater (throttled) +local accumulator = 0 +local updateInterval = 1 / math.max(1, state.config.updateRate) -- secs +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 + + -- gather players (and ensure entries exist) + 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 + + -- iterate entries and decide show/hide + 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 + -- เกิน limit: ซ่อนเพื่อความเสถียร + canShow = false end - -- apply visuals based on renderMode - if state.config.renderMode == "Billboard" then - if info.billboard and info.label then - info.billboard.Enabled = visible and state.config.enabled - if visible and state.config.enabled then - if info.label.Text ~= labelText then info.label.Text = labelText end - if info.label.TextColor3 ~= state.config.espColor then info.label.TextColor3 = state.config.espColor end + if canShow then + visibleCount = visibleCount + 1 + -- ensure billboard exists + 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 - else - if info.drawings then - -- Drawing objects expect 2D coords - if visible and state.config.enabled then - local hrp = player.Character and (player.Character:FindFirstChild("Head") or player.Character:FindFirstChild("HumanoidRootPart")) - if hrp then - local pos3 = hrp.Position + Vector3.new(0, state.config.labelOffsetY, 0) - local screenPos, onScreen = Camera:WorldToViewportPoint(pos3) - if onScreen then - local x = screenPos.X - local y = screenPos.Y - local t = info.drawings.text - local b = info.drawings.box - if t then - t.Text = labelText - t.Position = Vector2.new(x, y - (state.config.textSize)) - t.Color = state.config.espColor - t.Size = state.config.textSize - t.Visible = true - end - if b then - -- small box around head (approx) - local size = state.config.boxThickness * 20 - b.Position = Vector2.new(x - size/2, y - size/2) - b.Size = Vector2.new(size, size) - b.Color = state.config.espColor - b.Visible = true - end - else - -- offscreen - if info.drawings.text then info.drawings.text.Visible = false end - if info.drawings.box then info.drawings.box.Visible = false end - end - end - else - if info.drawings.text then info.drawings.text.Visible = false end - if info.drawings.box then info.drawings.box.Visible = false end + + -- ensure highlight if enabled + 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 + + -- update label text & color only when changed + 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 + -- color + if info.billboardObj.label.TextColor3 ~= state.config.espColor then + info.billboardObj.label.TextColor3 = state.config.espColor end + -- scale / Size adjustments + info.billboardObj.billboard.Size = UDim2.new(0, math.clamp(120 + (#txt * 4), 100, 280), 0, math.clamp(16 * state.config.labelScale, 12, 48)) end - end - -- highlight enable/disable - if info.highlight then - info.highlight.Enabled = visible and state.config.highlightEnabled and state.config.enabled - -- update color/transparency if changed - safeSet(info.highlight, "FillColor", state.config.espColor) - safeSet(info.highlight, "FillTransparency", state.config.highlightFillTransparency) - safeSet(info.highlight, "OutlineTransparency", state.config.highlightOutlineTransparency) + else + -- hide (recycle billboard/highlight) + 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) + end + + lastVisibleCount = visibleCount +end + +-- main connection (unbind old if exists) +if state._espHeartbeatConn then + pcall(function() state._espHeartbeatConn:Disconnect() end) + state._espHeartbeatConn = nil end --- Start/Stop ESP -local function enableESP(val) - state.config.enabled = val and true or false - if state.config.enabled then - -- create for existing players - initPlayers() - -- enable highlights adornee for those with characters - for p, info in pairs(state.espTable) do - if info.highlight and p.Character then - pcall(function() info.highlight.Adornee = p.Character end) +state._espHeartbeatConn = RunService.Heartbeat:Connect(performUpdate) + +-- Player join/leave cleanup +Players.PlayerRemoving:Connect(function(p) + if not p then return end + cleanupEntry(p.UserId) +end) + +-- UI integration: (ใช้ API ที่ให้มาในตัวอย่าง Tabs.*) +-- ฟังก์ชัน applyConfig เพื่อ sync ค่าจาก UI ไป state.config +local function applyConfigFromUI(uiConfig) + for k,v in pairs(uiConfig) do + state.config[k] = v + end +end + +-- Exposed functions for the UI to hook into: +local ESP_API = {} + +function ESP_API.ToggleEnabled(v) + state.config.enabled = v + if not v then + -- immediate cleanup of visuals but keep entries + for uid,info in pairs(state.espTable) do + if info.billboardObj then + returnBillboard(info.billboardObj) + info.billboardObj = nil end - end - startLoop() - else - -- disable visuals but keep objects (so re-enabling is fast) - for p, info in pairs(state.espTable) do - if info.billboard then info.billboard.Enabled = false end - if info.drawings then - if info.drawings.text then info.drawings.text.Visible = false end - if info.drawings.box then info.drawings.box.Visible = false end + if info.highlightObj then + returnHighlight(info.highlightObj) + info.highlightObj = nil end - if info.highlight then info.highlight.Enabled = false end end end end --- Expose Save/Load/Reset functions and UI binding examples --- UI integration (example): hook your Tabs.ESP UI elements to these functions --- Example usage with provided UI API: --- local espToggle = Tabs.ESP:AddToggle("ESPToggle", { Title = "ESP", Default = state.config.enabled }) --- espToggle:OnChanged(function(v) enableESP(v) SaveConfig() end) --- local rateSlider = Tabs.ESP:AddSlider("ESP_UpdateRate", { Title="Update Rate", Default = state.config.updateRate, Min = 1, Max = 60, Rounding = 1 }) --- rateSlider:OnChanged(function(v) state.config.updateRate = v SaveConfig() end) --- local distSlider = Tabs.ESP:AddSlider("ESP_MaxDist", { Title="Max Distance", Default = state.config.maxDistance, Min = 50, Max = 1000, Rounding = 1 }) --- distSlider:OnChanged(function(v) state.config.maxDistance = v SaveConfig() end) --- local colorPicker = Tabs.ESP:AddColorpicker("ESP_Color", { Title = "ESP Color", Default = state.config.espColor }) --- colorPicker:OnChanged(function(c) state.config.espColor = c SaveConfig() end) --- Tabs.ESP:AddToggle("ESP_Highlight", { Title = "Highlight", Default = state.config.highlightEnabled }):OnChanged(function(v) state.config.highlightEnabled = v SaveConfig() end) --- Tabs.ESP:AddButton({ Title = "Reset ESP Config", Description = "Reset to defaults", Callback = function() ResetConfig() end }) --- Tabs.ESP:AddButton({ Title = "Save Config", Description = "Save current config", Callback = function() SaveConfig() end }) --- Tabs.ESP:AddButton({ Title = "Load Config", Description = "Load saved config", Callback = function() LoadConfig() enableESP(state.config.enabled) end }) - --- Load saved config then start with current value -LoadConfig() --- (example) If you want ESP enabled by default from saved config: -if state.config.enabled then - enableESP(true) +function ESP_API.SetColor(c) + state.config.espColor = c end --- Provide API for external scripts to control quickly: -return { - Enable = enableESP, - CreateForPlayer = createESPForPlayer, - RemoveForPlayer = removeESPForPlayer, - SaveConfig = SaveConfig, - LoadConfig = LoadConfig, - ResetConfig = ResetConfig, - GetConfig = function() return deepCopy(state.config) 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(v, 1, 60) 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) end +function ESP_API.SetAlwaysOnTop(v) state.config.alwaysOnTop = v end +function ESP_API.SetHighlightEnabled(v) state.config.highlightEnabled = v 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() + state.config = { + 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, + } +end + +-- expose API object for UI code below +_G.ATG_ESP_API = ESP_API + +-- Example: UI hookup (ปรับให้เข้ากับ API Tabs.* ที่ให้มา) +-- สมมติมี Tabs.ESP อยู่แล้ว +if Tabs and Tabs.ESP then + local espToggle = Tabs.ESP:AddToggle("ESPToggle", { Title = "ESP", Default = state.config.enabled }) + espToggle:OnChanged(function(v) ESP_API.ToggleEnabled(v) end) + + local color = Tabs.ESP:AddColorpicker("ESPColor", { Title = "ESP Color", Default = state.config.espColor }) + color:OnChanged(function(c) ESP_API.SetColor(c) end) + + Tabs.ESP:AddToggle("ESP_Name", { Title = "Show Name", Default = state.config.showName }):OnChanged(function(v) ESP_API.SetShowName(v) end) + Tabs.ESP:AddToggle("ESP_Health", { Title = "Show Health", Default = state.config.showHealth }):OnChanged(function(v) ESP_API.SetShowHealth(v) end) + Tabs.ESP:AddToggle("ESP_Distance", { Title = "Show Distance", Default = state.config.showDistance }):OnChanged(function(v) ESP_API.SetShowDistance(v) end) + + Tabs.ESP:AddToggle("ESP_Highlight", { Title = "Highlight", Default = state.config.highlightEnabled }):OnChanged(function(v) ESP_API.SetHighlightEnabled(v) end) + Tabs.ESP:AddSlider("ESP_HighlightFill", { Title = "Highlight Fill Transparency", Default = state.config.highlightFillTransparency, Min = 0, Max = 1, Rounding = 0.01 }):OnChanged(function(v) ESP_API.SetHighlightFillTrans(v) end) + Tabs.ESP:AddSlider("ESP_HighlightOutline", { Title = "Highlight Outline Transparency", Default = state.config.highlightOutlineTransparency, Min = 0, Max = 1, Rounding = 0.01 }):OnChanged(function(v) ESP_API.SetHighlightOutlineTrans(v) end) + + Tabs.ESP:AddSlider("ESP_Rate", { Title = "Update Rate (per sec)", Default = state.config.updateRate, Min = 1, Max = 60, Rounding = 1 }):OnChanged(function(v) ESP_API.SetUpdateRate(v) end) + Tabs.ESP:AddSlider("ESP_MaxDist", { Title = "Max Distance", Default = state.config.maxDistance, Min = 50, Max = 1000, Rounding = 1 }):OnChanged(function(v) ESP_API.SetMaxDistance(v) end) + Tabs.ESP:AddSlider("ESP_LabelScale", { Title = "Label Scale", Default = state.config.labelScale, Min = 0.5, Max = 3, Rounding = 0.1 }):OnChanged(function(v) ESP_API.SetLabelScale(v) end) + Tabs.ESP:AddToggle("ESP_AlwaysOnTop", { Title = "AlwaysOnTop", Default = state.config.alwaysOnTop }):OnChanged(function(v) ESP_API.SetAlwaysOnTop(v) end) + + Tabs.ESP:AddButton({ + Title = "Reset ESP Config", + Description = "Reset to sane defaults", + Callback = function() + ESP_API.ResetConfig() + -- อัพเดต UI values (ถ้า API library มี method SetValue ให้เรียก; ตัวอย่างไม่รู้ API ชื่อเฉพาะ) + -- ถ้าไม่มีให้แอยด์ผู้ใช้รีสตาร์ท UI หรือเราจะเก็บค่าเริ่มต้นไว้แยก + print("ESP config reset. Reopen UI to sync values.") + end + }) +end +-- end of script From 2ead0ff65292add2f10e0dd3a4d89cf102af3d31 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Wed, 8 Oct 2025 21:04:57 +0700 Subject: [PATCH 04/76] Update ESP.lua --- ESP.lua | 359 +++++++++++++++++++++++++++++++------------------------- 1 file changed, 200 insertions(+), 159 deletions(-) diff --git a/ESP.lua b/ESP.lua index 5e1f330..5bdb8f4 100644 --- a/ESP.lua +++ b/ESP.lua @@ -1,50 +1,41 @@ --- ATG ESP (optimized) --- หลักการสำคัญ: --- 1) ใช้ centralized update loop (Heartbeat) ที่ throttle ด้วย updateInterval (ไม่สร้าง RenderStepped per-player) --- 2) Culling: ระยะ, อยู่ในหน้าจอ, (optionally) raycast สำหรับ visibility แบบ throttled --- 3) Object pooling สำหรับ BillboardGui และ Highlight เพื่อลดการสร้าง/ทำลายบ่อย ๆ --- 4) เก็บ state/config ที่ UI สามารถแก้ได้ และมี Reset/Presets --- 5) เช็ค cleanup เมื่อ PlayerLeft หรือ Unload - +-- ATG ESP (optimized) — patched version (UI sync + Reset fixes) local Players = game:GetService("Players") local RunService = game:GetService("RunService") -local UIS = game:GetService("UserInputService") local LocalPlayer = Players.LocalPlayer local Camera = workspace.CurrentCamera -- state + config (เก็บค่า default ที่เหมาะสม) -local state = state or {} -- บันทึกข้ามสคริปต์ถ้ามี -state.espTable = state.espTable or {} -- map userId -> info +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, -- updates per second (10 => 0.1s interval) - maxDistance = 250, -- เมตร ในเกม units - maxVisibleCount = 60, -- limit จำนวน ESP แสดงพร้อมกัน (ป้องกัน overload) + updateRate = 10, + maxDistance = 250, + maxVisibleCount = 60, showName = true, showHealth = true, showDistance = true, espColor = Color3.fromRGB(255, 50, 50), - labelScale = 1, -- scale ของข้อความ (TextScaled true จะอิงขนาด parent) - alwaysOnTop = false, -- BillboardGui.AlwaysOnTop (ถ้า true จะไม่ถูกบัง) - smartHideCenter = true, -- ซ่อน label ถ้ามันบังหน้าจอกลาง (ปรับได้) - centerHideRadius = 0.12, -- % screen radius จาก center ที่จะซ่อน (0.12 => 12%) - raycastOcclusion = false, -- ถ้าต้องการเพิ่ม check line-of-sight (ถ้าเปิดจะทำงานแบบ throttled) - raycastInterval = 0.6, -- วินาทีต่อการ raycast ต่อตัว (ถ้าเปิด) + labelScale = 1, + alwaysOnTop = false, + smartHideCenter = true, + centerHideRadius = 0.12, + raycastOcclusion = false, + raycastInterval = 0.6, highlightEnabled = false, highlightFillTransparency = 0.6, highlightOutlineTransparency = 0.6, - teamCheck = true, -- ไม่แสดง ESP ของเพื่อนร่วมทีม (ถ้าเกมมีทีม) - ignoreLocalPlayer = true, -- ไม่แสดงตัวเอง + teamCheck = true, + ignoreLocalPlayer = true, } --- pooling helpers +-- 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) @@ -102,7 +93,7 @@ local function returnHighlight(hl) table.insert(state.pools.highlights, hl) end --- helper utilities +-- util local function getHRP(player) if not player or not player.Character then return nil end return player.Character:FindFirstChild("HumanoidRootPart") @@ -111,17 +102,13 @@ local function getHumanoid(char) if not char then return nil end return char:FindFirstChildOfClass("Humanoid") end - local function isSameTeam(a,b) - -- best-effort: ถ้าตัวเกมมี Team, compare Team property 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 + if a.Team and b.Team and a.Team == b.Team then return true end return false end --- create / remove logic (doesn't connect RenderStepped per-player) +-- entries local function ensureEntryForPlayer(p) if not p then return end local uid = p.UserId @@ -129,29 +116,23 @@ local function ensureEntryForPlayer(p) local info = { player = p, - billboardObj = nil, -- {billboard,label} - highlightObj = nil, -- Highlight instance + billboardObj = nil, + highlightObj = nil, lastVisible = false, lastScreenPos = Vector2.new(0,0), lastDistance = math.huge, lastRaycast = -999, - connected = true, -- if we have CharacterAdded connection + connected = true, charConn = nil } - -- connect character added to reattach Adornee when respawn info.charConn = p.CharacterAdded:Connect(function(char) - -- small delay ให้เวลา Head สร้าง 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 + if head then info.billboardObj.billboard.Adornee = head end end + if info.highlightObj then info.highlightObj.Adornee = char end end) state.espTable[uid] = info @@ -163,19 +144,13 @@ local function cleanupEntry(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 + 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 check (distance + on-screen + optional raycast) +-- visibility local function shouldShowFor(info) if not info or not info.player then return false end local p = info.player @@ -188,31 +163,22 @@ local function shouldShowFor(info) 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 + if dist > state.config.maxDistance then return false end - -- screen check 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 - -- smartCenter hide: ถ้า label อยู่ใกล้หน้าจอกลางมากเกินไป if state.config.smartHideCenter then local sx = screenPos.X / Camera.ViewportSize.X local sy = screenPos.Y / Camera.ViewportSize.Y - local cx = 0.5 - local cy = 0.5 - local dx = sx - cx - local dy = sy - cy + 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 + if d < state.config.centerHideRadius then return false end end - -- optional throttled raycast occlusion (ถ้าเปิด) if state.config.raycastOcclusion then local now = tick() if now - info.lastRaycast >= state.config.raycastInterval then @@ -223,27 +189,19 @@ local function shouldShowFor(info) rayParams.FilterDescendantsInstances = {LocalPlayer.Character} rayParams.FilterType = Enum.RaycastFilterType.Blacklist local r = workspace:Raycast(origin, direction, rayParams) - -- ถ้าวัตถุติดกันและไม่ใช่ตัวเป้าหมาย แปลว่าไม่ visible - if r and r.Instance and not r.Instance:IsDescendantOf(p.Character) then - return false - end - else - -- ใช้ last known result (ไม่ทำ raycast ทุก frame) - -- ถ้าไม่มี last result ให้อนุรักษ์ default true + if r and r.Instance and not r.Instance:IsDescendantOf(p.Character) then return false end end end return true end --- update label content (only when changed) +-- 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.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) @@ -255,9 +213,9 @@ local function buildLabelText(p) return table.concat(parts, " | ") end --- main centralized updater (throttled) +-- centralized updater local accumulator = 0 -local updateInterval = 1 / math.max(1, state.config.updateRate) -- secs +local updateInterval = 1 / math.max(1, state.config.updateRate) local lastVisibleCount = 0 local function performUpdate(dt) @@ -266,30 +224,22 @@ local function performUpdate(dt) if accumulator < updateInterval then return end accumulator = accumulator - updateInterval - -- gather players (and ensure entries exist) 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 + if p ~= LocalPlayer or not state.config.ignoreLocalPlayer then ensureEntryForPlayer(p) end end - -- iterate entries and decide show/hide 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 - -- เกิน limit: ซ่อนเพื่อความเสถียร - canShow = false - end + if canShow and visibleCount >= state.config.maxVisibleCount then canShow = false end if canShow then visibleCount = visibleCount + 1 - -- ensure billboard exists 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)) @@ -304,7 +254,6 @@ local function performUpdate(dt) end end - -- ensure highlight if enabled if state.config.highlightEnabled and not info.highlightObj then local hl = borrowHighlight() hl.Adornee = p.Character @@ -320,30 +269,16 @@ local function performUpdate(dt) info.highlightObj = nil end - -- update label text & color only when changed 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 - -- color - if info.billboardObj.label.TextColor3 ~= state.config.espColor then - info.billboardObj.label.TextColor3 = state.config.espColor - end - -- scale / Size adjustments + 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 - -- hide (recycle billboard/highlight) - if info.billboardObj then - returnBillboard(info.billboardObj) - info.billboardObj = nil - end - if info.highlightObj then - returnHighlight(info.highlightObj) - info.highlightObj = 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 end end @@ -351,64 +286,135 @@ local function performUpdate(dt) lastVisibleCount = visibleCount end --- main connection (unbind old if exists) -if state._espHeartbeatConn then - pcall(function() state._espHeartbeatConn:Disconnect() end) - state._espHeartbeatConn = nil +-- 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 -state._espHeartbeatConn = RunService.Heartbeat:Connect(performUpdate) +-- 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 --- Player join/leave cleanup -Players.PlayerRemoving:Connect(function(p) - if not p then return end - cleanupEntry(p.UserId) -end) - --- UI integration: (ใช้ API ที่ให้มาในตัวอย่าง Tabs.*) --- ฟังก์ชัน applyConfig เพื่อ sync ค่าจาก UI ไป state.config -local function applyConfigFromUI(uiConfig) - for k,v in pairs(uiConfig) do - state.config[k] = v + -- 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 functions for the UI to hook into: +-- Exposed API local ESP_API = {} function ESP_API.ToggleEnabled(v) state.config.enabled = v if not v then - -- immediate cleanup of visuals but keep entries 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 + 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(v, 1, 60) 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) end -function ESP_API.SetAlwaysOnTop(v) state.config.alwaysOnTop = v end -function ESP_API.SetHighlightEnabled(v) state.config.highlightEnabled = 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() - state.config = { + local defaults = { enabled = false, updateRate = 10, maxDistance = 250, @@ -429,43 +435,78 @@ function ESP_API.ResetConfig() 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 object for UI code below +-- expose API _G.ATG_ESP_API = ESP_API --- Example: UI hookup (ปรับให้เข้ากับ API Tabs.* ที่ให้มา) --- สมมติมี Tabs.ESP อยู่แล้ว +-- UI hookup (store refs and ensure OnChanged hooks update state) if Tabs and Tabs.ESP then - local espToggle = Tabs.ESP:AddToggle("ESPToggle", { Title = "ESP", Default = state.config.enabled }) - espToggle:OnChanged(function(v) ESP_API.ToggleEnabled(v) end) + -- 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 - local color = Tabs.ESP:AddColorpicker("ESPColor", { Title = "ESP Color", Default = state.config.espColor }) - color:OnChanged(function(c) ESP_API.SetColor(c) 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 - Tabs.ESP:AddToggle("ESP_Name", { Title = "Show Name", Default = state.config.showName }):OnChanged(function(v) ESP_API.SetShowName(v) end) - Tabs.ESP:AddToggle("ESP_Health", { Title = "Show Health", Default = state.config.showHealth }):OnChanged(function(v) ESP_API.SetShowHealth(v) end) - Tabs.ESP:AddToggle("ESP_Distance", { Title = "Show Distance", Default = state.config.showDistance }):OnChanged(function(v) ESP_API.SetShowDistance(v) 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 - Tabs.ESP:AddToggle("ESP_Highlight", { Title = "Highlight", Default = state.config.highlightEnabled }):OnChanged(function(v) ESP_API.SetHighlightEnabled(v) end) - Tabs.ESP:AddSlider("ESP_HighlightFill", { Title = "Highlight Fill Transparency", Default = state.config.highlightFillTransparency, Min = 0, Max = 1, Rounding = 0.01 }):OnChanged(function(v) ESP_API.SetHighlightFillTrans(v) end) - Tabs.ESP:AddSlider("ESP_HighlightOutline", { Title = "Highlight Outline Transparency", Default = state.config.highlightOutlineTransparency, Min = 0, Max = 1, Rounding = 0.01 }):OnChanged(function(v) ESP_API.SetHighlightOutlineTrans(v) 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 - Tabs.ESP:AddSlider("ESP_Rate", { Title = "Update Rate (per sec)", Default = state.config.updateRate, Min = 1, Max = 60, Rounding = 1 }):OnChanged(function(v) ESP_API.SetUpdateRate(v) end) - Tabs.ESP:AddSlider("ESP_MaxDist", { Title = "Max Distance", Default = state.config.maxDistance, Min = 50, Max = 1000, Rounding = 1 }):OnChanged(function(v) ESP_API.SetMaxDistance(v) end) - Tabs.ESP:AddSlider("ESP_LabelScale", { Title = "Label Scale", Default = state.config.labelScale, Min = 0.5, Max = 3, Rounding = 0.1 }):OnChanged(function(v) ESP_API.SetLabelScale(v) end) - Tabs.ESP:AddToggle("ESP_AlwaysOnTop", { Title = "AlwaysOnTop", Default = state.config.alwaysOnTop }):OnChanged(function(v) ESP_API.SetAlwaysOnTop(v) 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() - -- อัพเดต UI values (ถ้า API library มี method SetValue ให้เรียก; ตัวอย่างไม่รู้ API ชื่อเฉพาะ) - -- ถ้าไม่มีให้แอยด์ผู้ใช้รีสตาร์ท UI หรือเราจะเก็บค่าเริ่มต้นไว้แยก - print("ESP config reset. Reopen UI to sync values.") + print("ESP config reset. UI should be synced.") end }) end --- end of script +-- end From 0f99be4a0413e302c45ce08e24e43aabbd3c676a Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 9 Oct 2025 07:19:17 +0700 Subject: [PATCH 05/76] Create config.lua --- config.lua | 210 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 config.lua diff --git a/config.lua b/config.lua new file mode 100644 index 0000000..0fb324f --- /dev/null +++ b/config.lua @@ -0,0 +1,210 @@ +local httpService = game:GetService("HttpService") +local Players = game:GetService("Players") +local Workspace = game:GetService("Workspace") + +local InterfaceManager = {} do + -- root folder (can be changed via SetFolder) + InterfaceManager.FolderRoot = "FluentSettings" + + -- default settings + InterfaceManager.Settings = { + Theme = "Dark", + Acrylic = true, + Transparency = true, + MenuKeybind = "LeftControl" + } + + -- helpers + local function sanitizeFilename(name) + -- แทนที่ตัวอักษรที่ไม่ปลอดภัยด้วย underscore + -- เก็บแค่ A-Z a-z 0-9 - _ และ space (space -> underscore) + name = tostring(name or "") + -- เปลี่ยน space -> _ + name = name:gsub("%s+", "_") + -- ลบตัวอักษรที่ไม่ใช่ alnum, -, _ + name = name:gsub("[^%w%-%_]", "") + if name == "" then + return "Unknown" + end + return name + end + + local function getPlaceId() + -- game.PlaceId จะเป็นตัวเลข; แปลงเป็น string + local success, id = pcall(function() return tostring(game.PlaceId) end) + if success and id then + return id + end + return "UnknownPlace" + end + + local function getMapName() + -- พยายามหา object "Map" ใน Workspace เพื่อเอาชื่อแมพ + local ok, map = pcall(function() return Workspace:FindFirstChild("Map") end) + if ok and map and map:IsA("Instance") then + return sanitizeFilename(map.Name) + end + -- ถ้าไม่เจอ ให้ลองใช้ Workspace.Name หรือ ให้ชื่อ UnknownMap + local ok2, wname = pcall(function() return Workspace.Name end) + if ok2 and wname then + return sanitizeFilename(wname) + end + return "UnknownMap" + end + + local function ensureFolder(path) + if not isfolder(path) then + makefolder(path) + end + end + + -- สร้างโครงสร้างโฟลเดอร์ root -> placeId + function InterfaceManager:BuildFolderTree() + local root = self.FolderRoot + ensureFolder(root) + + local placeId = getPlaceId() + local placeFolder = root .. "/" .. placeId + ensureFolder(placeFolder) + + -- เก็บโฟลเดอร์ settings เป็น legacy / อาจมีประโยชน์ + local settingsFolder = root .. "/settings" + if not isfolder(settingsFolder) then + makefolder(settingsFolder) + end + + -- สร้างอีกชั้นเพื่อความยืดหยุ่น (optional) + local placeSettingsFolder = placeFolder .. "/settings" + if not isfolder(placeSettingsFolder) then + makefolder(placeSettingsFolder) + end + end + + function InterfaceManager:SetFolder(folder) + -- รับค่า root folder ใหม่ (string) + self.FolderRoot = tostring(folder or "FluentSettings") + self:BuildFolderTree() + end + + function InterfaceManager:SetLibrary(library) + self.Library = library + end + + -- ได้ path สำหรับไฟล์ config ของแมพปัจจุบัน + local function getConfigFilePath(self) + local root = self.FolderRoot + local placeId = getPlaceId() + local mapName = getMapName() + -- รูปแบบ: //.json + return root .. "/" .. placeId .. "/" .. mapName .. ".json" + end + + function InterfaceManager:SaveSettings() + local path = getConfigFilePath(self) + -- ensure folder (in caseไม่ถูกสร้าง) + local folder = path:match("^(.*)/[^/]+$") + if folder then + ensureFolder(folder) + end + + local encoded = httpService:JSONEncode(self.Settings or {}) + writefile(path, encoded) + end + + function InterfaceManager:LoadSettings() + -- โหลดไฟล์ config ของแมพปัจจุบัน (ถ้ามี) + local path = getConfigFilePath(self) + + -- legacy path (เดิมเป็น /options.json) — ถ้าเจอ เราจะ migrate ให้เป็น per-map file + local legacyPath = self.FolderRoot .. "/options.json" + + if isfile(path) then + local data = readfile(path) + local success, decoded = pcall(httpService.JSONDecode, httpService, data) + if success and type(decoded) == "table" then + for i, v in next, decoded do + self.Settings[i] = v + end + end + return + end + + -- ถ้าไม่มี per-map file แต่มี legacy file ให้ migrate (คัดลอก) + if isfile(legacyPath) then + local data = readfile(legacyPath) + local success, decoded = pcall(httpService.JSONDecode, httpService, data) + if success and type(decoded) == "table" then + -- นำค่า legacy มา merge แล้วบันทึกใหม่ใน path ใหม่ + for i,v in next, decoded do + self.Settings[i] = v + end + -- ensure folder + local folder = path:match("^(.*)/[^/]+$") + if folder then ensureFolder(folder) end + local encoded = httpService:JSONEncode(self.Settings or {}) + writefile(path, encoded) + end + return + end + + -- ถ้าไม่มีไฟล์ใดๆ ให้ใช้ค่า default ที่มีอยู่แล้ว (ไม่ทำอะไร) + end + + function InterfaceManager:BuildInterfaceSection(tab) + assert(self.Library, "Must set InterfaceManager.Library") + local Library = self.Library + local Settings = InterfaceManager.Settings + + -- โหลดค่า config ของแมพนี้ก่อนแสดง UI + 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) + + 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 From 18b6aef4e9b745f506956edb2d2fa13d149ed78d Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 9 Oct 2025 07:26:40 +0700 Subject: [PATCH 06/76] Create autosave.lua --- autosave.lua | 399 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 399 insertions(+) create mode 100644 autosave.lua diff --git a/autosave.lua b/autosave.lua new file mode 100644 index 0000000..fe40817 --- /dev/null +++ b/autosave.lua @@ -0,0 +1,399 @@ +local httpService = game:GetService("HttpService") +local Workspace = game:GetService("Workspace") + +local SaveManager = {} do + -- root folder (can be changed via SetFolder) + SaveManager.FolderRoot = "FluentSettings" + 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 getMapName() + local ok, map = pcall(function() return Workspace:FindFirstChild("Map") end) + if ok and map and map:IsA("Instance") then + return sanitizeFilename(map.Name) + end + local ok2, wname = pcall(function() return Workspace.Name end) + if ok2 and wname then return sanitizeFilename(wname) end + return "UnknownMap" + end + + local function ensureFolder(path) + if not isfolder(path) then + makefolder(path) + end + end + + -- get configs folder for current place/map + local function getConfigsFolder(self) + local root = self.FolderRoot + local placeId = getPlaceId() + local mapName = getMapName() + -- FluentSettings///settings + return root .. "/" .. placeId .. "/" .. mapName .. "/settings" + end + + local function getConfigFilePath(self, name) + local folder = getConfigsFolder(self) + return folder .. "/" .. name .. ".json" + end + + -- Build folder tree and migrate legacy configs if found (copy only) + function SaveManager:BuildFolderTree() + local root = self.FolderRoot + ensureFolder(root) + + local placeId = getPlaceId() + local placeFolder = root .. "/" .. placeId + ensureFolder(placeFolder) + + local mapName = getMapName() + local mapFolder = placeFolder .. "/" .. mapName + ensureFolder(mapFolder) + + local settingsFolder = mapFolder .. "/settings" + ensureFolder(settingsFolder) + + -- legacy folder: /settings (old layout). If files exist there, copy them into current map settings + 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 = settingsFolder .. "/" .. base .. ".json" + -- copy only if destination does not exist yet + if not isfile(dest) then + local ok, data = pcall(readfile, f) + if ok and data then + local success, err = pcall(writefile, dest, data) + -- ignore write errors but do not fail + end + end + end + end + end + + -- also migrate autoload.txt if present (copy only) + local autopath = legacySettingsFolder .. "/autoload.txt" + if isfile(autopath) then + local autodata = readfile(autopath) + local destAuto = settingsFolder .. "/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 "FluentSettings") + 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 + + -- ensure folder exists + 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: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" then + table.insert(out, name) + end + end + end + return out + end + + function SaveManager:LoadAutoloadConfig() + local autopath = getConfigsFolder(self) .. "/autoload.txt" + if isfile(autopath) then + local name = readfile(autopath) + local success, err = self:Load(name) + if not success then + return self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "Failed to load autoload config: " .. err, + Duration = 7 + }) + end + + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Auto loaded config %q", name), + Duration = 7 + }) + end + end + + function SaveManager:BuildConfigSection(tab) + assert(self.Library, "Must set SaveManager.Library") + + local section = tab:AddSection("Configuration") + + section:AddInput("SaveManager_ConfigName", { Title = "Config name" }) + section:AddDropdown("SaveManager_ConfigList", { Title = "Config list", Values = self:RefreshConfigList(), AllowNull = true }) + + section:AddButton({ + Title = "Create config", + Callback = function() + local name = SaveManager.Options.SaveManager_ConfigName.Value + + if name:gsub(" ", "") == "" then + return self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "Invalid config name (empty)", + Duration = 7 + }) + end + + local success, err = self:Save(name) + if not success then + return self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "Failed to save config: " .. err, + Duration = 7 + }) + end + + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Created config %q", name), + Duration = 7 + }) + + SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) + SaveManager.Options.SaveManager_ConfigList:SetValue(nil) + end + }) + + section:AddButton({Title = "Load config", Callback = function() + local name = SaveManager.Options.SaveManager_ConfigList.Value + + local success, err = self:Load(name) + if not success then + return self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "Failed to load config: " .. err, + Duration = 7 + }) + end + + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Loaded config %q", name), + Duration = 7 + }) + end}) + + section:AddButton({Title = "Overwrite config", Callback = function() + local name = SaveManager.Options.SaveManager_ConfigList.Value + + local success, err = self:Save(name) + if not success then + return self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "Failed to overwrite config: " .. err, + Duration = 7 + }) + end + + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Overwrote config %q", name), + Duration = 7 + }) + end}) + + section:AddButton({Title = "Refresh list", Callback = function() + SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) + SaveManager.Options.SaveManager_ConfigList:SetValue(nil) + end}) + + local AutoloadButton + AutoloadButton = section:AddButton({Title = "Set as autoload", Description = "Current autoload config: none", Callback = function() + local name = SaveManager.Options.SaveManager_ConfigList.Value + local autopath = getConfigsFolder(self) .. "/autoload.txt" + writefile(autopath, name) + AutoloadButton:SetDesc("Current autoload config: " .. name) + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Set %q to auto load", name), + Duration = 7 + }) + end}) + + -- populate current autoload desc if exists + local autop = getConfigsFolder(self) .. "/autoload.txt" + if isfile(autop) then + local name = readfile(autop) + AutoloadButton:SetDesc("Current autoload config: " .. name) + end + + SaveManager:SetIgnoreIndexes({ "SaveManager_ConfigList", "SaveManager_ConfigName" }) + end + + -- initial build + SaveManager:BuildFolderTree() +end + +return SaveManager From a6df94198bb9d66974eb06a52c9042f27b19ef24 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 9 Oct 2025 07:52:11 +0700 Subject: [PATCH 07/76] Update autosave.lua --- autosave.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autosave.lua b/autosave.lua index fe40817..b58cc95 100644 --- a/autosave.lua +++ b/autosave.lua @@ -3,7 +3,7 @@ local Workspace = game:GetService("Workspace") local SaveManager = {} do -- root folder (can be changed via SetFolder) - SaveManager.FolderRoot = "FluentSettings" + SaveManager.FolderRoot = "ATGSettings" SaveManager.Ignore = {} SaveManager.Options = {} SaveManager.Parser = { From aa72634225b9f34565d9d803e0439c3a35accdb6 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 9 Oct 2025 09:37:14 +0700 Subject: [PATCH 08/76] Create walkjump.lua --- walkjump.lua | 271 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 walkjump.lua diff --git a/walkjump.lua b/walkjump.lua new file mode 100644 index 0000000..7293618 --- /dev/null +++ b/walkjump.lua @@ -0,0 +1,271 @@ +-- 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 From 37ef3cbe5a059d079b2f54c02ed9e39d2264ecb2 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 9 Oct 2025 09:41:34 +0700 Subject: [PATCH 09/76] Update walkjump.lua --- walkjump.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/walkjump.lua b/walkjump.lua index 7293618..8e5e479 100644 --- a/walkjump.lua +++ b/walkjump.lua @@ -1,3 +1,4 @@ +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 From 59aa13c94c03c398a3e36e0453dbbca3142cea19 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 9 Oct 2025 09:50:00 +0700 Subject: [PATCH 10/76] Update walkjump.lua --- walkjump.lua | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/walkjump.lua b/walkjump.lua index 8e5e479..4545acd 100644 --- a/walkjump.lua +++ b/walkjump.lua @@ -1,3 +1,13 @@ +-- โหลด Fluent หรือ UI หลักก่อน (สมมติว่าทำไว้แล้ว) +local Fluent = loadstring(game:HttpGet("https://raw.githubusercontent.com/.../FluentLib.lua"))() +local Window = Fluent:CreateWindow({ Title = "ATGHub" }) +local Tabs = { + Players = Window:AddTab({ Title = "Players" }) +} + +-- จากนั้นวางโค้ด Speed & Jump Controller ที่เฟลล์ส่งมาได้เลย + + 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 From 293e81a04b466266a104d94309843346e6307923 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 9 Oct 2025 09:50:35 +0700 Subject: [PATCH 11/76] Update walkjump.lua --- walkjump.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/walkjump.lua b/walkjump.lua index 4545acd..7b8df52 100644 --- a/walkjump.lua +++ b/walkjump.lua @@ -1,5 +1,5 @@ -- โหลด Fluent หรือ UI หลักก่อน (สมมติว่าทำไว้แล้ว) -local Fluent = loadstring(game:HttpGet("https://raw.githubusercontent.com/.../FluentLib.lua"))() +local Fluent = loadstring(game:HttpGet("https://github.com/dawid-scripts/Fluent/releases/latest/download/main.lua"))() local Window = Fluent:CreateWindow({ Title = "ATGHub" }) local Tabs = { Players = Window:AddTab({ Title = "Players" }) From 1ec92c93f6c677245459bf566184f5cddeb0ecf5 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 9 Oct 2025 10:15:55 +0700 Subject: [PATCH 12/76] Update walkjump.lua --- walkjump.lua | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/walkjump.lua b/walkjump.lua index 7b8df52..8e5e479 100644 --- a/walkjump.lua +++ b/walkjump.lua @@ -1,13 +1,3 @@ --- โหลด Fluent หรือ UI หลักก่อน (สมมติว่าทำไว้แล้ว) -local Fluent = loadstring(game:HttpGet("https://github.com/dawid-scripts/Fluent/releases/latest/download/main.lua"))() -local Window = Fluent:CreateWindow({ Title = "ATGHub" }) -local Tabs = { - Players = Window:AddTab({ Title = "Players" }) -} - --- จากนั้นวางโค้ด Speed & Jump Controller ที่เฟลล์ส่งมาได้เลย - - 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 From 9f0fc3f8696ddebd3be273810e9d7e72754a9a2a Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 16 Oct 2025 10:06:10 +0700 Subject: [PATCH 13/76] Update config.lua --- config.lua | 312 +++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 278 insertions(+), 34 deletions(-) diff --git a/config.lua b/config.lua index 0fb324f..be4a6a2 100644 --- a/config.lua +++ b/config.lua @@ -3,8 +3,8 @@ local Players = game:GetService("Players") local Workspace = game:GetService("Workspace") local InterfaceManager = {} do - -- root folder (can be changed via SetFolder) - InterfaceManager.FolderRoot = "FluentSettings" + -- root folder (default changed) + InterfaceManager.FolderRoot = "ATGHubSettings" -- default settings InterfaceManager.Settings = { @@ -16,12 +16,8 @@ local InterfaceManager = {} do -- helpers local function sanitizeFilename(name) - -- แทนที่ตัวอักษรที่ไม่ปลอดภัยด้วย underscore - -- เก็บแค่ A-Z a-z 0-9 - _ และ space (space -> underscore) name = tostring(name or "") - -- เปลี่ยน space -> _ name = name:gsub("%s+", "_") - -- ลบตัวอักษรที่ไม่ใช่ alnum, -, _ name = name:gsub("[^%w%-%_]", "") if name == "" then return "Unknown" @@ -30,7 +26,6 @@ local InterfaceManager = {} do end local function getPlaceId() - -- game.PlaceId จะเป็นตัวเลข; แปลงเป็น string local success, id = pcall(function() return tostring(game.PlaceId) end) if success and id then return id @@ -39,12 +34,10 @@ local InterfaceManager = {} do end local function getMapName() - -- พยายามหา object "Map" ใน Workspace เพื่อเอาชื่อแมพ local ok, map = pcall(function() return Workspace:FindFirstChild("Map") end) - if ok and map and map:IsA("Instance") then + if ok and map and map.IsA and map:IsA("Instance") then return sanitizeFilename(map.Name) end - -- ถ้าไม่เจอ ให้ลองใช้ Workspace.Name หรือ ให้ชื่อ UnknownMap local ok2, wname = pcall(function() return Workspace.Name end) if ok2 and wname then return sanitizeFilename(wname) @@ -58,50 +51,125 @@ local InterfaceManager = {} do end end - -- สร้างโครงสร้างโฟลเดอร์ root -> placeId + -- best-effort copy helpers for migrating legacy folder + local function copyFile(src, dst) + if not isfile(src) then return false end + local ok, content = pcall(readfile, src) + if not ok then return false end + local folder = dst:match("^(.*)/[^/]+$") + if folder and not isfolder(folder) then makefolder(folder) end + pcall(writefile, dst, content) + return true + end + + local function copyFilesInFolder(srcFolder, dstFolder) + -- listfiles might not exist in every executor; guard with pcall + if not listfiles then return end + local ok, files = pcall(listfiles, srcFolder) + if not ok or type(files) ~= "table" then return end + ensureFolder(dstFolder) + for _, f in ipairs(files) do + local base = f:match("([^/\\]+)$") or f + local dst = dstFolder .. "/" .. base + pcall(copyFile, f, dst) + end + end + + -- Migrate legacy FluentSettings -> new root (best-effort, non-destructive) + function InterfaceManager:MigrateLegacyFolder(oldName, newName) + if not isfolder(oldName) then + return false, "no legacy folder" + end + if not newName or newName == "" then newName = self.FolderRoot end + ensureFolder(newName) + + -- copy root-level files + pcall(copyFilesInFolder, oldName, newName) + + -- copy common subfolders + local subs = { "Themes", "settings", "Imports" } + for _, sub in ipairs(subs) do + local s = oldName .. "/" .. sub + local d = newName .. "/" .. sub + if isfolder(s) then + pcall(copyFilesInFolder, s, d) + end + end + + return true, "migrated" + end + + -- build folder tree with Themes, Imports, per-place folders function InterfaceManager:BuildFolderTree() local root = self.FolderRoot + + -- auto-migrate if old folder exists and differs + if root ~= "FluentSettings" and isfolder("FluentSettings") and not isfolder(root) then + -- best-effort copy, do not delete originals + pcall(function() self:MigrateLegacyFolder("FluentSettings", root) end) + end + ensureFolder(root) local placeId = getPlaceId() local placeFolder = root .. "/" .. placeId ensureFolder(placeFolder) - -- เก็บโฟลเดอร์ settings เป็น legacy / อาจมีประโยชน์ + -- legacy settings folder (kept) local settingsFolder = root .. "/settings" - if not isfolder(settingsFolder) then - makefolder(settingsFolder) - end + ensureFolder(settingsFolder) - -- สร้างอีกชั้นเพื่อความยืดหยุ่น (optional) + -- per-place settings local placeSettingsFolder = placeFolder .. "/settings" - if not isfolder(placeSettingsFolder) then - makefolder(placeSettingsFolder) - end + ensureFolder(placeSettingsFolder) + + -- global themes + per-place themes + local themesRoot = root .. "/Themes" + ensureFolder(themesRoot) + + local placeThemes = placeFolder .. "/Themes" + ensureFolder(placeThemes) + + -- imports (where user-imported raw files can be dropped) + local imports = root .. "/Imports" + ensureFolder(imports) end function InterfaceManager:SetFolder(folder) - -- รับค่า root folder ใหม่ (string) - self.FolderRoot = tostring(folder or "FluentSettings") + self.FolderRoot = tostring(folder or "ATGHubSettings") + -- try to migrate legacy FluentSettings -> new folder if present + pcall(function() self:MigrateLegacyFolder("FluentSettings", self.FolderRoot) end) self:BuildFolderTree() end function InterfaceManager:SetLibrary(library) self.Library = library + -- try to register themes immediately when library set + self:RegisterThemesToLibrary(library) + end + + -- helper: prefixed filename for settings -> "ATG Hub - - .json" + local function getPrefixedSettingsFilename() + local placeId = getPlaceId() + local mapName = getMapName() + local fname = "ATG Hub - " .. sanitizeFilename(placeId) .. " - " .. sanitizeFilename(mapName) .. ".json" + return fname end - -- ได้ path สำหรับไฟล์ config ของแมพปัจจุบัน + -- config path per place local function getConfigFilePath(self) local root = self.FolderRoot local placeId = getPlaceId() - local mapName = getMapName() - -- รูปแบบ: //.json - return root .. "/" .. placeId .. "/" .. mapName .. ".json" + -- ensure subfolders exist + local configFolder = root .. "/" .. placeId + ensureFolder(configFolder) + local fname = getPrefixedSettingsFilename() + return configFolder .. "/" .. fname end function InterfaceManager:SaveSettings() local path = getConfigFilePath(self) - -- ensure folder (in caseไม่ถูกสร้าง) + -- ensure folder (in case) local folder = path:match("^(.*)/[^/]+$") if folder then ensureFolder(folder) @@ -129,16 +197,14 @@ local InterfaceManager = {} do return end - -- ถ้าไม่มี per-map file แต่มี legacy file ให้ migrate (คัดลอก) if isfile(legacyPath) then local data = readfile(legacyPath) local success, decoded = pcall(httpService.JSONDecode, httpService, data) if success and type(decoded) == "table" then - -- นำค่า legacy มา merge แล้วบันทึกใหม่ใน path ใหม่ for i,v in next, decoded do self.Settings[i] = v end - -- ensure folder + -- save to new path local folder = path:match("^(.*)/[^/]+$") if folder then ensureFolder(folder) end local encoded = httpService:JSONEncode(self.Settings or {}) @@ -147,26 +213,191 @@ local InterfaceManager = {} do return end - -- ถ้าไม่มีไฟล์ใดๆ ให้ใช้ค่า default ที่มีอยู่แล้ว (ไม่ทำอะไร) + -- ไม่มีไฟล์ใดๆ -> ใช้ default end + -- ==== Theme file utilities ==== + -- returns table of { name = displayName, path = fullPath, ext = "lua"/"json" } + function InterfaceManager:ScanThemes() + local themes = {} + local root = self.FolderRoot + local themePaths = { + root .. "/Themes", + root .. "/" .. getPlaceId() .. "/Themes" + } + for _, folder in ipairs(themePaths) do + if isfolder(folder) and listfiles then + for _, fname in ipairs(listfiles(folder) or {}) do + local lfname = fname + if lfname:match("%.lua$") or lfname:match("%.json$") then + local base = lfname:match("([^/\\]+)$") or lfname + local display = base + display = display:gsub("^ATG Hub %- ", "") + display = display:gsub("%.lua$", ""):gsub("%.json$", "") + display = display:gsub("%_", " ") + local ext = lfname:match("%.([a-zA-Z0-9]+)$") + table.insert(themes, { name = display, path = lfname, ext = ext }) + end + end + end + end + return themes + end + + -- import theme content (string) into root Themes folder + -- name: suggested theme name (used for filename) + -- content: raw file content (string) + -- ext: "lua" or "json" (defaults to lua) + function InterfaceManager:ImportTheme(name, content, ext) + ext = tostring(ext or "lua"):lower() + if ext ~= "lua" and ext ~= "json" then ext = "lua" end + local rootThemes = self.FolderRoot .. "/Themes" + ensureFolder(rootThemes) + + local safe = sanitizeFilename(name) + local fname = "ATG Hub - " .. safe .. "." .. ext + local full = rootThemes .. "/" .. fname + + -- overwrite if exists + writefile(full, tostring(content or "")) + + -- attempt to register immediately + if self.Library then + self:TryRegisterThemeFile(full, ext) + end + + return full + end + + -- try to load a theme file and register to library if possible + function InterfaceManager:TryRegisterThemeFile(fullpath, ext) + if not isfile(fullpath) then return false, "file not found" end + local raw = readfile(fullpath) + local themeTbl = nil + if ext == "json" then + local ok, dec = pcall(httpService.JSONDecode, httpService, raw) + if ok and type(dec) == "table" then + themeTbl = dec + end + else -- lua + local ok, chunk = pcall(loadstring, raw) + if ok and type(chunk) == "function" then + local ok2, result = pcall(chunk) + if ok2 and type(result) == "table" then + themeTbl = result + end + end + end + + if themeTbl and self.Library then + local displayName = fullpath:match("([^/\\]+)$") or fullpath + displayName = displayName:gsub("^ATG Hub %- ", ""):gsub("%.lua$",""):gsub("%.json$",""):gsub("%_"," ") + if type(self.Library.RegisterTheme) == "function" then + pcall(function() self.Library:RegisterTheme(displayName, themeTbl) end) + return true, "registered" + else + local lt = self.Library.Themes + if type(lt) == "table" then + local isMap = false + for k,v in pairs(lt) do + if type(k) ~= "number" then isMap = true break end + end + if isMap then + self.Library.Themes[displayName] = themeTbl + return true, "merged into map" + else + local exists = false + for _,v in ipairs(lt) do if v == displayName then exists = true break end end + if not exists then table.insert(self.Library.Themes, displayName) end + self.Library.DynamicImportedThemes = self.Library.DynamicImportedThemes or {} + self.Library.DynamicImportedThemes[displayName] = themeTbl + return true, "added name + dynamic table" + end + end + end + end + + return false, "could not parse or no library" + end + + -- register all themes found on disk to Library (best-effort) + function InterfaceManager:RegisterThemesToLibrary(library) + if not library then return end + local found = self:ScanThemes() + for _, item in ipairs(found) do + pcall(function() + self:TryRegisterThemeFile(item.path, item.ext) + end) + end + end + + -- helper to produce a list of theme names (merge library + imported ones) + local function getLibraryThemeNames(library) + local names = {} + if not library then return names end + + -- if library.Themes is an array of strings + if type(library.Themes) == "table" then + local numeric = true + for k,v in pairs(library.Themes) do + if type(k) ~= "number" then numeric = false break end + end + if numeric then + for _,v in ipairs(library.Themes) do + if type(v) == "string" then names[v] = true end + end + else + for k,v in pairs(library.Themes) do + if type(k) == "string" then names[k] = true end + end + end + end + + -- also include dynamic imports if any + if library.DynamicImportedThemes then + for k,v in pairs(library.DynamicImportedThemes) do + names[k] = true + end + end + + -- include on-disk themes + local disk = InterfaceManager:ScanThemes() + for _, item in ipairs(disk) do + names[item.name] = true + end + + -- convert to array + local out = {} + for k,_ in pairs(names) do table.insert(out, k) end + table.sort(out) + return out + end + function InterfaceManager:BuildInterfaceSection(tab) assert(self.Library, "Must set InterfaceManager.Library") local Library = self.Library local Settings = InterfaceManager.Settings - -- โหลดค่า config ของแมพนี้ก่อนแสดง UI + -- ensure folders exist & load config of this map before UI + InterfaceManager:BuildFolderTree() InterfaceManager:LoadSettings() local section = tab:AddSection("Interface") + -- generate merged values + local mergedValues = getLibraryThemeNames(Library) + local InterfaceTheme = section:AddDropdown("InterfaceTheme", { Title = "Theme", Description = "Changes the interface theme.", - Values = Library.Themes, + Values = mergedValues, Default = Settings.Theme, Callback = function(Value) - Library:SetTheme(Value) + -- try to set using library API if available + if type(Library.SetTheme) == "function" then + pcall(function() Library:SetTheme(Value) end) + end + Settings.Theme = Value InterfaceManager:SaveSettings() end @@ -192,7 +423,9 @@ local InterfaceManager = {} do Description = "Makes the interface transparent.", Default = Settings.Transparency, Callback = function(Value) - Library:ToggleTransparency(Value) + if type(Library.ToggleTransparency) == "function" then + Library:ToggleTransparency(Value) + end Settings.Transparency = Value InterfaceManager:SaveSettings() end @@ -204,6 +437,17 @@ local InterfaceManager = {} do InterfaceManager:SaveSettings() end) Library.MinimizeKeybind = MenuKeybind + + -- optional: add UI buttons to import themes (if UI supports AddButton) + if section.AddButton then + section:AddButton({ + Title = "Import Theme (paste)", + Description = "Import a theme file (lua or json) by pasting content via script.", + Callback = function() + print("Use InterfaceManager:ImportTheme(name, content, ext) from code to import theme files.") + end + }) + end end end From 68ab504e7242d947fcab31d1b23adc3f9364d56a Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 16 Oct 2025 10:17:36 +0700 Subject: [PATCH 14/76] Update config.lua --- config.lua | 128 +++++++++++++++++++++++++---------------------------- 1 file changed, 60 insertions(+), 68 deletions(-) diff --git a/config.lua b/config.lua index be4a6a2..3b8a4f4 100644 --- a/config.lua +++ b/config.lua @@ -3,8 +3,10 @@ local Players = game:GetService("Players") local Workspace = game:GetService("Workspace") local InterfaceManager = {} do - -- root folder (default changed) + -- เปลี่ยน default root folder เป็น ATGHubSettings InterfaceManager.FolderRoot = "ATGHubSettings" + -- ถ้ามีโฟลเดอร์เก่า ให้ระบุชื่อไว้เพื่อ migration + local LEGACY_FOLDER = "FluentSettings" -- default settings InterfaceManager.Settings = { @@ -51,62 +53,55 @@ local InterfaceManager = {} do end end - -- best-effort copy helpers for migrating legacy folder - local function copyFile(src, dst) - if not isfile(src) then return false end - local ok, content = pcall(readfile, src) - if not ok then return false end - local folder = dst:match("^(.*)/[^/]+$") - if folder and not isfolder(folder) then makefolder(folder) end - pcall(writefile, dst, content) - return true - end - - local function copyFilesInFolder(srcFolder, dstFolder) - -- listfiles might not exist in every executor; guard with pcall - if not listfiles then return end - local ok, files = pcall(listfiles, srcFolder) - if not ok or type(files) ~= "table" then return end - ensureFolder(dstFolder) - for _, f in ipairs(files) do - local base = f:match("([^/\\]+)$") or f - local dst = dstFolder .. "/" .. base - pcall(copyFile, f, dst) + -- copy files from src folder into dest (non-destructive: จะไม่เขียนทับไฟล์ที่มีอยู่) + local function copyFolderNonDestructive(src, dest) + -- ensure dest exists + ensureFolder(dest) + -- try listfiles + if type(listfiles) == "function" then + local ok, files = pcall(listfiles, src) + if ok and type(files) == "table" then + for _, f in ipairs(files) do + -- base filename + local base = f:match("([^/\\]+)$") or f + local target = dest .. "/" .. base + if isfile(f) then + if not isfile(target) then + local ok2, content = pcall(readfile, f) + if ok2 and content then + pcall(writefile, target, content) + end + end + end + end + end end - end - -- Migrate legacy FluentSettings -> new root (best-effort, non-destructive) - function InterfaceManager:MigrateLegacyFolder(oldName, newName) - if not isfolder(oldName) then - return false, "no legacy folder" - end - if not newName or newName == "" then newName = self.FolderRoot end - ensureFolder(newName) - - -- copy root-level files - pcall(copyFilesInFolder, oldName, newName) - - -- copy common subfolders - local subs = { "Themes", "settings", "Imports" } - for _, sub in ipairs(subs) do - local s = oldName .. "/" .. sub - local d = newName .. "/" .. sub - if isfolder(s) then - pcall(copyFilesInFolder, s, d) + -- try listfolders (to recurse) + if type(listfolders) == "function" then + local ok2, folders = pcall(listfolders, src) + if ok2 and type(folders) == "table" then + for _, sub in ipairs(folders) do + local base = sub:match("([^/\\]+)$") or sub + local targetSub = dest .. "/" .. base + copyFolderNonDestructive(sub, targetSub) + end end end - - return true, "migrated" end -- build folder tree with Themes, Imports, per-place folders function InterfaceManager:BuildFolderTree() local root = self.FolderRoot - -- auto-migrate if old folder exists and differs - if root ~= "FluentSettings" and isfolder("FluentSettings") and not isfolder(root) then - -- best-effort copy, do not delete originals - pcall(function() self:MigrateLegacyFolder("FluentSettings", root) end) + -- ถ้ามีโฟลเดอร์ legacy อยู่ แต่โฟลเดอร์ใหม่ยังไม่มี ให้ migrate ไฟล์ + if isfolder(LEGACY_FOLDER) and not isfolder(root) then + pcall(function() + -- create new root then copy + ensureFolder(root) + -- copy root-level files and subfolders non-destructive + copyFolderNonDestructive(LEGACY_FOLDER, root) + end) end ensureFolder(root) @@ -115,7 +110,7 @@ local InterfaceManager = {} do local placeFolder = root .. "/" .. placeId ensureFolder(placeFolder) - -- legacy settings folder (kept) + -- legacy settings folder kept for compatibility local settingsFolder = root .. "/settings" ensureFolder(settingsFolder) @@ -137,8 +132,6 @@ local InterfaceManager = {} do function InterfaceManager:SetFolder(folder) self.FolderRoot = tostring(folder or "ATGHubSettings") - -- try to migrate legacy FluentSettings -> new folder if present - pcall(function() self:MigrateLegacyFolder("FluentSettings", self.FolderRoot) end) self:BuildFolderTree() end @@ -226,17 +219,21 @@ local InterfaceManager = {} do root .. "/" .. getPlaceId() .. "/Themes" } for _, folder in ipairs(themePaths) do - if isfolder(folder) and listfiles then - for _, fname in ipairs(listfiles(folder) or {}) do - local lfname = fname - if lfname:match("%.lua$") or lfname:match("%.json$") then - local base = lfname:match("([^/\\]+)$") or lfname - local display = base - display = display:gsub("^ATG Hub %- ", "") - display = display:gsub("%.lua$", ""):gsub("%.json$", "") - display = display:gsub("%_", " ") - local ext = lfname:match("%.([a-zA-Z0-9]+)$") - table.insert(themes, { name = display, path = lfname, ext = ext }) + if isfolder(folder) then + if type(listfiles) == "function" then + local ok, files = pcall(listfiles, folder) + if ok and type(files) == "table" then + for _, fname in ipairs(files) do + if fname:match("%.lua$") or fname:match("%.json$") then + local base = fname:match("([^/\\]+)$") or fname + local display = base + display = display:gsub("^ATG Hub %- ", "") + display = display:gsub("%.lua$", ""):gsub("%.json$", "") + display = display:gsub("%_", " ") + local ext = fname:match("%.([a-zA-Z0-9]+)$") + table.insert(themes, { name = display, path = fname, ext = ext }) + end + end end end end @@ -258,7 +255,7 @@ local InterfaceManager = {} do local fname = "ATG Hub - " .. safe .. "." .. ext local full = rootThemes .. "/" .. fname - -- overwrite if exists + -- overwrite if exists (import explicitly replaces) writefile(full, tostring(content or "")) -- attempt to register immediately @@ -336,7 +333,6 @@ local InterfaceManager = {} do local names = {} if not library then return names end - -- if library.Themes is an array of strings if type(library.Themes) == "table" then local numeric = true for k,v in pairs(library.Themes) do @@ -353,20 +349,17 @@ local InterfaceManager = {} do end end - -- also include dynamic imports if any if library.DynamicImportedThemes then for k,v in pairs(library.DynamicImportedThemes) do names[k] = true end end - -- include on-disk themes local disk = InterfaceManager:ScanThemes() for _, item in ipairs(disk) do names[item.name] = true end - -- convert to array local out = {} for k,_ in pairs(names) do table.insert(out, k) end table.sort(out) @@ -393,7 +386,6 @@ local InterfaceManager = {} do Values = mergedValues, Default = Settings.Theme, Callback = function(Value) - -- try to set using library API if available if type(Library.SetTheme) == "function" then pcall(function() Library:SetTheme(Value) end) end @@ -424,7 +416,7 @@ local InterfaceManager = {} do Default = Settings.Transparency, Callback = function(Value) if type(Library.ToggleTransparency) == "function" then - Library:ToggleTransparency(Value) + Library.ToggleTransparency(Value) end Settings.Transparency = Value InterfaceManager:SaveSettings() From c6907523dc80af392bddf1856d7e08df7b8d22f3 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 16 Oct 2025 10:25:59 +0700 Subject: [PATCH 15/76] Update config.lua --- config.lua | 293 +++++++++++++++++++++-------------------------------- 1 file changed, 113 insertions(+), 180 deletions(-) diff --git a/config.lua b/config.lua index 3b8a4f4..d84b3af 100644 --- a/config.lua +++ b/config.lua @@ -3,10 +3,8 @@ local Players = game:GetService("Players") local Workspace = game:GetService("Workspace") local InterfaceManager = {} do - -- เปลี่ยน default root folder เป็น ATGHubSettings + -- root folder ใหม่ (จะสร้างใหม่เลย ไม่คัดลอกจากที่เก่า) InterfaceManager.FolderRoot = "ATGHubSettings" - -- ถ้ามีโฟลเดอร์เก่า ให้ระบุชื่อไว้เพื่อ migration - local LEGACY_FOLDER = "FluentSettings" -- default settings InterfaceManager.Settings = { @@ -21,17 +19,13 @@ local InterfaceManager = {} do name = tostring(name or "") name = name:gsub("%s+", "_") name = name:gsub("[^%w%-%_]", "") - if name == "" then - return "Unknown" - end + if name == "" then return "Unknown" end return name end local function getPlaceId() local success, id = pcall(function() return tostring(game.PlaceId) end) - if success and id then - return id - end + if success and id then return id end return "UnknownPlace" end @@ -41,9 +35,7 @@ local InterfaceManager = {} do return sanitizeFilename(map.Name) end local ok2, wname = pcall(function() return Workspace.Name end) - if ok2 and wname then - return sanitizeFilename(wname) - end + if ok2 and wname then return sanitizeFilename(wname) end return "UnknownMap" end @@ -53,81 +45,25 @@ local InterfaceManager = {} do end end - -- copy files from src folder into dest (non-destructive: จะไม่เขียนทับไฟล์ที่มีอยู่) - local function copyFolderNonDestructive(src, dest) - -- ensure dest exists - ensureFolder(dest) - -- try listfiles - if type(listfiles) == "function" then - local ok, files = pcall(listfiles, src) - if ok and type(files) == "table" then - for _, f in ipairs(files) do - -- base filename - local base = f:match("([^/\\]+)$") or f - local target = dest .. "/" .. base - if isfile(f) then - if not isfile(target) then - local ok2, content = pcall(readfile, f) - if ok2 and content then - pcall(writefile, target, content) - end - end - end - end - end - end - - -- try listfolders (to recurse) - if type(listfolders) == "function" then - local ok2, folders = pcall(listfolders, src) - if ok2 and type(folders) == "table" then - for _, sub in ipairs(folders) do - local base = sub:match("([^/\\]+)$") or sub - local targetSub = dest .. "/" .. base - copyFolderNonDestructive(sub, targetSub) - end - end - end - end - - -- build folder tree with Themes, Imports, per-place folders + -- สร้างโฟลเดอร์โครงสร้างใหม่แบบสะอาด (ไม่ migrate/copy ของเก่า) function InterfaceManager:BuildFolderTree() local root = self.FolderRoot - - -- ถ้ามีโฟลเดอร์ legacy อยู่ แต่โฟลเดอร์ใหม่ยังไม่มี ให้ migrate ไฟล์ - if isfolder(LEGACY_FOLDER) and not isfolder(root) then - pcall(function() - -- create new root then copy - ensureFolder(root) - -- copy root-level files and subfolders non-destructive - copyFolderNonDestructive(LEGACY_FOLDER, root) - end) - end - ensureFolder(root) local placeId = getPlaceId() local placeFolder = root .. "/" .. placeId ensureFolder(placeFolder) - -- legacy settings folder kept for compatibility - local settingsFolder = root .. "/settings" - ensureFolder(settingsFolder) - - -- per-place settings - local placeSettingsFolder = placeFolder .. "/settings" - ensureFolder(placeSettingsFolder) + -- settings (global & per-place) + ensureFolder(root .. "/settings") + ensureFolder(placeFolder .. "/settings") - -- global themes + per-place themes - local themesRoot = root .. "/Themes" - ensureFolder(themesRoot) + -- themes (global & per-place) + ensureFolder(root .. "/Themes") + ensureFolder(placeFolder .. "/Themes") - local placeThemes = placeFolder .. "/Themes" - ensureFolder(placeThemes) - - -- imports (where user-imported raw files can be dropped) - local imports = root .. "/Imports" - ensureFolder(imports) + -- imports + ensureFolder(root .. "/Imports") end function InterfaceManager:SetFolder(folder) @@ -137,11 +73,11 @@ local InterfaceManager = {} do function InterfaceManager:SetLibrary(library) self.Library = library - -- try to register themes immediately when library set + -- on set, try to register themes from disk self:RegisterThemesToLibrary(library) end - -- helper: prefixed filename for settings -> "ATG Hub - - .json" + -- prefixed filename for settings -> "ATG Hub - - .json" local function getPrefixedSettingsFilename() local placeId = getPlaceId() local mapName = getMapName() @@ -149,11 +85,9 @@ local InterfaceManager = {} do return fname end - -- config path per place local function getConfigFilePath(self) local root = self.FolderRoot local placeId = getPlaceId() - -- ensure subfolders exist local configFolder = root .. "/" .. placeId ensureFolder(configFolder) local fname = getPrefixedSettingsFilename() @@ -162,42 +96,32 @@ local InterfaceManager = {} do function InterfaceManager:SaveSettings() local path = getConfigFilePath(self) - -- ensure folder (in case) local folder = path:match("^(.*)/[^/]+$") - if folder then - ensureFolder(folder) - end - + if folder then ensureFolder(folder) end local encoded = httpService:JSONEncode(self.Settings or {}) writefile(path, encoded) end function InterfaceManager:LoadSettings() - -- โหลดไฟล์ config ของแมพปัจจุบัน (ถ้ามี) local path = getConfigFilePath(self) - - -- legacy path (เดิมเป็น /options.json) — ถ้าเจอ เราจะ migrate ให้เป็น per-map file - local legacyPath = self.FolderRoot .. "/options.json" + local legacyPath = self.FolderRoot .. "/options.json" if isfile(path) then local data = readfile(path) local success, decoded = pcall(httpService.JSONDecode, httpService, data) if success and type(decoded) == "table" then - for i, v in next, decoded do - self.Settings[i] = v - end + for i, v in next, decoded do self.Settings[i] = v end end return end + -- ถ้าเจอ legacy options.json ให้ย้ายค่า (merge) แต่ไม่คัดลอกทุกไฟล์โฟลเดอร์ if isfile(legacyPath) then local data = readfile(legacyPath) local success, decoded = pcall(httpService.JSONDecode, httpService, data) if success and type(decoded) == "table" then - for i,v in next, decoded do - self.Settings[i] = v - end - -- save to new path + for i,v in next, decoded do self.Settings[i] = v end + -- save to new per-place path local folder = path:match("^(.*)/[^/]+$") if folder then ensureFolder(folder) end local encoded = httpService:JSONEncode(self.Settings or {}) @@ -206,11 +130,12 @@ local InterfaceManager = {} do return end - -- ไม่มีไฟล์ใดๆ -> ใช้ default + -- ถ้าไม่มีไฟล์ใดๆ ให้คง default end - -- ==== Theme file utilities ==== - -- returns table of { name = displayName, path = fullPath, ext = "lua"/"json" } + -- ================= Theme utilities ================= + + -- scan Themes folders and return list of { name, path, ext } function InterfaceManager:ScanThemes() local themes = {} local root = self.FolderRoot @@ -219,20 +144,18 @@ local InterfaceManager = {} do root .. "/" .. getPlaceId() .. "/Themes" } for _, folder in ipairs(themePaths) do - if isfolder(folder) then - if type(listfiles) == "function" then - local ok, files = pcall(listfiles, folder) - if ok and type(files) == "table" then - for _, fname in ipairs(files) do - if fname:match("%.lua$") or fname:match("%.json$") then - local base = fname:match("([^/\\]+)$") or fname - local display = base - display = display:gsub("^ATG Hub %- ", "") - display = display:gsub("%.lua$", ""):gsub("%.json$", "") - display = display:gsub("%_", " ") - local ext = fname:match("%.([a-zA-Z0-9]+)$") - table.insert(themes, { name = display, path = fname, ext = ext }) - end + if isfolder(folder) and type(listfiles) == "function" then + local ok, files = pcall(listfiles, folder) + if ok and type(files) == "table" then + for _, fpath in ipairs(files) do + if fpath:match("%.lua$") or fpath:match("%.json$") then + local base = fpath:match("([^/\\]+)$") or fpath + local display = base + display = display:gsub("^ATG Hub %- ", "") + display = display:gsub("%.lua$", ""):gsub("%.json$", "") + display = display:gsub("%_", " ") + local ext = fpath:match("%.([a-zA-Z0-9]+)$") + table.insert(themes, { name = display, path = fpath, ext = ext }) end end end @@ -241,75 +164,65 @@ local InterfaceManager = {} do return themes end - -- import theme content (string) into root Themes folder - -- name: suggested theme name (used for filename) - -- content: raw file content (string) - -- ext: "lua" or "json" (defaults to lua) + -- import theme content into global Themes (creates ATG Hub - .) function InterfaceManager:ImportTheme(name, content, ext) ext = tostring(ext or "lua"):lower() if ext ~= "lua" and ext ~= "json" then ext = "lua" end local rootThemes = self.FolderRoot .. "/Themes" ensureFolder(rootThemes) - local safe = sanitizeFilename(name) local fname = "ATG Hub - " .. safe .. "." .. ext local full = rootThemes .. "/" .. fname - - -- overwrite if exists (import explicitly replaces) writefile(full, tostring(content or "")) - - -- attempt to register immediately + -- immediately register so it shows up if self.Library then self:TryRegisterThemeFile(full, ext) end - return full end - -- try to load a theme file and register to library if possible + -- try to parse theme file (lua/json) and register/merge into library function InterfaceManager:TryRegisterThemeFile(fullpath, ext) if not isfile(fullpath) then return false, "file not found" end local raw = readfile(fullpath) local themeTbl = nil if ext == "json" then local ok, dec = pcall(httpService.JSONDecode, httpService, raw) - if ok and type(dec) == "table" then - themeTbl = dec - end + if ok and type(dec) == "table" then themeTbl = dec end else -- lua local ok, chunk = pcall(loadstring, raw) if ok and type(chunk) == "function" then local ok2, result = pcall(chunk) - if ok2 and type(result) == "table" then - themeTbl = result - end + if ok2 and type(result) == "table" then themeTbl = result end end end if themeTbl and self.Library then local displayName = fullpath:match("([^/\\]+)$") or fullpath displayName = displayName:gsub("^ATG Hub %- ", ""):gsub("%.lua$",""):gsub("%.json$",""):gsub("%_"," ") + -- prefer RegisterTheme API if type(self.Library.RegisterTheme) == "function" then pcall(function() self.Library:RegisterTheme(displayName, themeTbl) end) return true, "registered" - else - local lt = self.Library.Themes - if type(lt) == "table" then - local isMap = false - for k,v in pairs(lt) do - if type(k) ~= "number" then isMap = true break end - end - if isMap then - self.Library.Themes[displayName] = themeTbl - return true, "merged into map" - else - local exists = false - for _,v in ipairs(lt) do if v == displayName then exists = true break end end - if not exists then table.insert(self.Library.Themes, displayName) end - self.Library.DynamicImportedThemes = self.Library.DynamicImportedThemes or {} - self.Library.DynamicImportedThemes[displayName] = themeTbl - return true, "added name + dynamic table" - end + end + -- fallback: merge into table-style Library.Themes + local lt = self.Library.Themes + if type(lt) == "table" then + -- detect map vs array + local isMap = false + for k,v in pairs(lt) do + if type(k) ~= "number" then isMap = true break end + end + if isMap then + self.Library.Themes[displayName] = themeTbl + return true, "merged into map" + else + local exists = false + for _,v in ipairs(lt) do if v == displayName then exists = true break end end + if not exists then table.insert(self.Library.Themes, displayName) end + self.Library.DynamicImportedThemes = self.Library.DynamicImportedThemes or {} + self.Library.DynamicImportedThemes[displayName] = themeTbl + return true, "added name + dynamic table" end end end @@ -328,37 +241,32 @@ local InterfaceManager = {} do end end - -- helper to produce a list of theme names (merge library + imported ones) + -- build merged theme-name list (library + disk + dynamic imports) local function getLibraryThemeNames(library) local names = {} - if not library then return names end + if not library then return {} end + -- library built-in if type(library.Themes) == "table" then local numeric = true for k,v in pairs(library.Themes) do if type(k) ~= "number" then numeric = false break end end if numeric then - for _,v in ipairs(library.Themes) do - if type(v) == "string" then names[v] = true end - end + for _,v in ipairs(library.Themes) do if type(v)=="string" then names[v]=true end end else - for k,v in pairs(library.Themes) do - if type(k) == "string" then names[k] = true end - end + for k,v in pairs(library.Themes) do if type(k)=="string" then names[k]=true end end end end + -- dynamic imports if library.DynamicImportedThemes then - for k,v in pairs(library.DynamicImportedThemes) do - names[k] = true - end + for k,v in pairs(library.DynamicImportedThemes) do names[k]=true end end + -- disk themes local disk = InterfaceManager:ScanThemes() - for _, item in ipairs(disk) do - names[item.name] = true - end + for _, item in ipairs(disk) do names[item.name] = true end local out = {} for k,_ in pairs(names) do table.insert(out, k) end @@ -374,10 +282,12 @@ local InterfaceManager = {} do -- ensure folders exist & load config of this map before UI InterfaceManager:BuildFolderTree() InterfaceManager:LoadSettings() + -- register disk themes now so Library gets them (best-effort) + InterfaceManager:RegisterThemesToLibrary(Library) local section = tab:AddSection("Interface") - -- generate merged values + -- merged name list local mergedValues = getLibraryThemeNames(Library) local InterfaceTheme = section:AddDropdown("InterfaceTheme", { @@ -386,17 +296,51 @@ local InterfaceManager = {} do Values = mergedValues, Default = Settings.Theme, Callback = function(Value) + -- try library API first if type(Library.SetTheme) == "function" then pcall(function() Library:SetTheme(Value) end) + else + -- if theme was imported dynamically, try register+set + if Library.DynamicImportedThemes and Library.DynamicImportedThemes[Value] then + if type(Library.RegisterTheme) == "function" then + pcall(function() Library:RegisterTheme(Value, Library.DynamicImportedThemes[Value]) end) + pcall(function() Library:SetTheme(Value) end) + end + end end - Settings.Theme = Value InterfaceManager:SaveSettings() end }) InterfaceTheme:SetValue(Settings.Theme) - + + -- add Refresh button to re-scan Themes folder (useful after dropping files manually) + if section.AddButton then + section:AddButton({ + Title = "Refresh Themes", + Description = "Scan ATGHubSettings/Themes and update dropdown (use after adding files).", + Callback = function() + -- re-scan and re-register + local newList = getLibraryThemeNames(Library) + -- try to update dropdown values (best-effort — depends on dropdown API) + if InterfaceTheme.SetValues then + pcall(function() InterfaceTheme:SetValues(newList) end) + elseif InterfaceTheme.SetOptions then + pcall(function() InterfaceTheme:SetOptions(newList) end) + elseif InterfaceTheme.UpdateValues then + pcall(function() InterfaceTheme:UpdateValues(newList) end) + else + -- fallback: notify in console (UI may require re-open) + print("[InterfaceManager] Refreshed themes (re-open menu if dropdown didn't update).") + end + -- also attempt to register any new files to library + InterfaceManager:RegisterThemesToLibrary(Library) + end + }) + end + + -- acrylic toggle if Library.UseAcrylic then section:AddToggle("AcrylicToggle", { Title = "Acrylic", @@ -409,37 +353,26 @@ local InterfaceManager = {} do end }) end - + section:AddToggle("TransparentToggle", { Title = "Transparency", Description = "Makes the interface transparent.", Default = Settings.Transparency, Callback = function(Value) if type(Library.ToggleTransparency) == "function" then - Library.ToggleTransparency(Value) + Library:ToggleTransparency(Value) end 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 - - -- optional: add UI buttons to import themes (if UI supports AddButton) - if section.AddButton then - section:AddButton({ - Title = "Import Theme (paste)", - Description = "Import a theme file (lua or json) by pasting content via script.", - Callback = function() - print("Use InterfaceManager:ImportTheme(name, content, ext) from code to import theme files.") - end - }) - end end end From c58196b7797b32e66bc1efa2cf6c2d80f8333516 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 16 Oct 2025 10:38:25 +0700 Subject: [PATCH 16/76] Update config.lua --- config.lua | 357 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 224 insertions(+), 133 deletions(-) diff --git a/config.lua b/config.lua index d84b3af..230963f 100644 --- a/config.lua +++ b/config.lua @@ -1,9 +1,8 @@ local httpService = game:GetService("HttpService") -local Players = game:GetService("Players") local Workspace = game:GetService("Workspace") local InterfaceManager = {} do - -- root folder ใหม่ (จะสร้างใหม่เลย ไม่คัดลอกจากที่เก่า) + -- root folder (single clean folder) InterfaceManager.FolderRoot = "ATGHubSettings" -- default settings @@ -14,6 +13,9 @@ local InterfaceManager = {} do MenuKeybind = "LeftControl" } + -- internal index: map displayName -> { path, ext } + InterfaceManager.DiskThemeIndex = {} + -- helpers local function sanitizeFilename(name) name = tostring(name or "") @@ -23,17 +25,13 @@ local InterfaceManager = {} do return name end - local function getPlaceId() - local success, id = pcall(function() return tostring(game.PlaceId) end) - if success and id then return id end - return "UnknownPlace" - end - - local function getMapName() + local function getMapFolderName() + -- ใช้ชื่อของ object "Map" ใน Workspace ถ้ามี local ok, map = pcall(function() return Workspace:FindFirstChild("Map") end) - if ok and map and map.IsA and map:IsA("Instance") then + if ok and map and map:IsA and map:IsA("Instance") then return sanitizeFilename(map.Name) end + -- fallback: ใช้ Workspace.Name local ok2, wname = pcall(function() return Workspace.Name end) if ok2 and wname then return sanitizeFilename(wname) end return "UnknownMap" @@ -45,25 +43,21 @@ local InterfaceManager = {} do end end - -- สร้างโฟลเดอร์โครงสร้างใหม่แบบสะอาด (ไม่ migrate/copy ของเก่า) + -- build folder tree (clean create, no migration) function InterfaceManager:BuildFolderTree() local root = self.FolderRoot ensureFolder(root) - local placeId = getPlaceId() - local placeFolder = root .. "/" .. placeId - ensureFolder(placeFolder) + local mapFolder = root .. "/" .. getMapFolderName() + ensureFolder(mapFolder) - -- settings (global & per-place) - ensureFolder(root .. "/settings") - ensureFolder(placeFolder .. "/settings") + ensureFolder(root .. "/settings") -- global settings + ensureFolder(mapFolder .. "/settings") -- per-map settings - -- themes (global & per-place) - ensureFolder(root .. "/Themes") - ensureFolder(placeFolder .. "/Themes") + ensureFolder(root .. "/Themes") -- global themes + ensureFolder(mapFolder .. "/Themes") -- per-map themes (optional) - -- imports - ensureFolder(root .. "/Imports") + ensureFolder(root .. "/Imports") -- manual import drop end function InterfaceManager:SetFolder(folder) @@ -73,25 +67,23 @@ local InterfaceManager = {} do function InterfaceManager:SetLibrary(library) self.Library = library - -- on set, try to register themes from disk + -- register themes found on disk to library (best-effort) self:RegisterThemesToLibrary(library) end - -- prefixed filename for settings -> "ATG Hub - - .json" + -- prefixed filename for per-map settings: ATG Hub - .json local function getPrefixedSettingsFilename() - local placeId = getPlaceId() - local mapName = getMapName() - local fname = "ATG Hub - " .. sanitizeFilename(placeId) .. " - " .. sanitizeFilename(mapName) .. ".json" + local mapName = getMapFolderName() + local fname = "ATG Hub - " .. sanitizeFilename(mapName) .. ".json" return fname end local function getConfigFilePath(self) local root = self.FolderRoot - local placeId = getPlaceId() - local configFolder = root .. "/" .. placeId - ensureFolder(configFolder) + local mapFolder = root .. "/" .. getMapFolderName() + ensureFolder(mapFolder) local fname = getPrefixedSettingsFilename() - return configFolder .. "/" .. fname + return mapFolder .. "/" .. fname end function InterfaceManager:SaveSettings() @@ -104,7 +96,7 @@ local InterfaceManager = {} do function InterfaceManager:LoadSettings() local path = getConfigFilePath(self) - local legacyPath = self.FolderRoot .. "/options.json" + local legacyPath = self.FolderRoot .. "/options.json" -- keep merge if exists if isfile(path) then local data = readfile(path) @@ -115,13 +107,12 @@ local InterfaceManager = {} do return end - -- ถ้าเจอ legacy options.json ให้ย้ายค่า (merge) แต่ไม่คัดลอกทุกไฟล์โฟลเดอร์ if isfile(legacyPath) then local data = readfile(legacyPath) local success, decoded = pcall(httpService.JSONDecode, httpService, data) if success and type(decoded) == "table" then for i,v in next, decoded do self.Settings[i] = v end - -- save to new per-place path + -- save to new per-map path local folder = path:match("^(.*)/[^/]+$") if folder then ensureFolder(folder) end local encoded = httpService:JSONEncode(self.Settings or {}) @@ -129,19 +120,18 @@ local InterfaceManager = {} do end return end - - -- ถ้าไม่มีไฟล์ใดๆ ให้คง default + -- otherwise keep defaults end -- ================= Theme utilities ================= - -- scan Themes folders and return list of { name, path, ext } + -- scan Themes folders and update DiskThemeIndex function InterfaceManager:ScanThemes() - local themes = {} + self.DiskThemeIndex = {} local root = self.FolderRoot local themePaths = { root .. "/Themes", - root .. "/" .. getPlaceId() .. "/Themes" + root .. "/" .. getMapFolderName() .. "/Themes" } for _, folder in ipairs(themePaths) do if isfolder(folder) and type(listfiles) == "function" then @@ -155,16 +145,20 @@ local InterfaceManager = {} do display = display:gsub("%.lua$", ""):gsub("%.json$", "") display = display:gsub("%_", " ") local ext = fpath:match("%.([a-zA-Z0-9]+)$") - table.insert(themes, { name = display, path = fpath, ext = ext }) + self.DiskThemeIndex[display] = { path = fpath, ext = ext } end end end end end - return themes + -- return list of display names + local out = {} + for name,_ in pairs(self.DiskThemeIndex) do table.insert(out, name) end + table.sort(out) + return out end - -- import theme content into global Themes (creates ATG Hub - .) + -- import theme content into global Themes folder function InterfaceManager:ImportTheme(name, content, ext) ext = tostring(ext or "lua"):lower() if ext ~= "lua" and ext ~= "json" then ext = "lua" end @@ -174,100 +168,208 @@ local InterfaceManager = {} do local fname = "ATG Hub - " .. safe .. "." .. ext local full = rootThemes .. "/" .. fname writefile(full, tostring(content or "")) - -- immediately register so it shows up - if self.Library then - self:TryRegisterThemeFile(full, ext) - end + -- update index + self:ScanThemes() + -- attempt to register immediately + if self.Library then self:TryRegisterThemeFile(full, ext) end return full end - -- try to parse theme file (lua/json) and register/merge into library - function InterfaceManager:TryRegisterThemeFile(fullpath, ext) - if not isfile(fullpath) then return false, "file not found" end + -- parse a theme file (return table or nil + err) + function InterfaceManager:ParseThemeFile(fullpath, ext) + if not isfile(fullpath) then return nil, "file not found" end local raw = readfile(fullpath) - local themeTbl = nil if ext == "json" then local ok, dec = pcall(httpService.JSONDecode, httpService, raw) - if ok and type(dec) == "table" then themeTbl = dec end - else -- lua - local ok, chunk = pcall(loadstring, raw) - if ok and type(chunk) == "function" then - local ok2, result = pcall(chunk) - if ok2 and type(result) == "table" then themeTbl = result end - end + if ok and type(dec) == "table" then return dec end + return nil, "json decode failed" + end + -- lua + local ok, chunk = pcall(loadstring, raw) + if not ok or type(chunk) ~= "function" then + return nil, "lua load failed" end + local ok2, result = pcall(chunk) + if ok2 and type(result) == "table" then + return result + end + -- maybe module used globals/tasks and returned nothing; try to run with 'return ' prefix + local ok3, chunk2 = pcall(loadstring, "return " .. raw) + if ok3 and type(chunk2) == "function" then + local ok4, res2 = pcall(chunk2) + if ok4 and type(res2) == "table" then return res2 end + end + return nil, "no table returned" + end - if themeTbl and self.Library then + -- try to register theme file to library (and keep dynamic table) + function InterfaceManager:TryRegisterThemeFile(fullpath, ext) + local ok, themeTbl = pcall(function() return self:ParseThemeFile(fullpath, ext) end) + if not ok or type(themeTbl) ~= "table" then return false, "parse failed" end + + if not self.Library then + -- just store to dynamic store so dropdown can use it later + self.Library = self.Library or {} + self.Library.DynamicImportedThemes = self.Library.DynamicImportedThemes or {} local displayName = fullpath:match("([^/\\]+)$") or fullpath displayName = displayName:gsub("^ATG Hub %- ", ""):gsub("%.lua$",""):gsub("%.json$",""):gsub("%_"," ") - -- prefer RegisterTheme API - if type(self.Library.RegisterTheme) == "function" then - pcall(function() self.Library:RegisterTheme(displayName, themeTbl) end) - return true, "registered" - end - -- fallback: merge into table-style Library.Themes - local lt = self.Library.Themes - if type(lt) == "table" then - -- detect map vs array - local isMap = false - for k,v in pairs(lt) do - if type(k) ~= "number" then isMap = true break end - end - if isMap then - self.Library.Themes[displayName] = themeTbl - return true, "merged into map" - else - local exists = false - for _,v in ipairs(lt) do if v == displayName then exists = true break end end - if not exists then table.insert(self.Library.Themes, displayName) end - self.Library.DynamicImportedThemes = self.Library.DynamicImportedThemes or {} - self.Library.DynamicImportedThemes[displayName] = themeTbl - return true, "added name + dynamic table" - end + self.Library.DynamicImportedThemes[displayName] = themeTbl + return true, "stored dynamic (no library)" + end + + -- prefer RegisterTheme API + local displayName = fullpath:match("([^/\\]+)$") or fullpath + displayName = displayName:gsub("^ATG Hub %- ", ""):gsub("%.lua$",""):gsub("%.json$",""):gsub("%_"," ") + if type(self.Library.RegisterTheme) == "function" then + pcall(function() self.Library:RegisterTheme(displayName, themeTbl) end) + -- also store dynamic copy + self.Library.DynamicImportedThemes = self.Library.DynamicImportedThemes or {} + self.Library.DynamicImportedThemes[displayName] = themeTbl + return true, "registered" + end + + -- fallback: if Library.Themes is a map, merge as table + local lt = self.Library.Themes + if type(lt) == "table" then + -- detect map vs array + local isMap = false + for k,v in pairs(lt) do if type(k) ~= "number" then isMap = true break end end + if isMap then + self.Library.Themes[displayName] = themeTbl + self.Library.DynamicImportedThemes = self.Library.DynamicImportedThemes or {} + self.Library.DynamicImportedThemes[displayName] = themeTbl + return true, "merged into map" + else + -- append name to array and save theme table in DynamicImportedThemes + local exists = false + for _,v in ipairs(lt) do if v == displayName then exists = true break end end + if not exists then table.insert(self.Library.Themes, displayName) end + self.Library.DynamicImportedThemes = self.Library.DynamicImportedThemes or {} + self.Library.DynamicImportedThemes[displayName] = themeTbl + return true, "added name + dynamic table" end end - return false, "could not parse or no library" + return false, "could not merge into library" end - -- register all themes found on disk to Library (best-effort) + -- register all disk themes to library / dynamic store function InterfaceManager:RegisterThemesToLibrary(library) - if not library then return end - local found = self:ScanThemes() - for _, item in ipairs(found) do + if not library and not self.Library then return end + self:ScanThemes() + for name,info in pairs(self.DiskThemeIndex) do pcall(function() - self:TryRegisterThemeFile(item.path, item.ext) + self:TryRegisterThemeFile(info.path, info.ext) end) end end - -- build merged theme-name list (library + disk + dynamic imports) - local function getLibraryThemeNames(library) - local names = {} - if not library then return {} end + -- load theme table by display name (search library dynamic, library map, disk) + function InterfaceManager:LoadThemeTableByName(name) + if not name then return nil, "no name" end + -- check dynamic store + if self.Library and self.Library.DynamicImportedThemes and self.Library.DynamicImportedThemes[name] then + return self.Library.DynamicImportedThemes[name] + end + -- check library.Themes if it's map style + if self.Library and type(self.Library.Themes) == "table" then + -- map style? + local isMap = false + for k,_ in pairs(self.Library.Themes) do if type(k) ~= "number" then isMap = true break end end + if isMap and self.Library.Themes[name] and type(self.Library.Themes[name]) == "table" then + return self.Library.Themes[name] + end + end + -- check disk + if self.DiskThemeIndex[name] then + local info = self.DiskThemeIndex[name] + local ok, tbl = pcall(function() return self:ParseThemeFile(info.path, info.ext) end) + if ok and type(tbl) == "table" then + -- store into dynamic for later + self.Library = self.Library or {} + self.Library.DynamicImportedThemes = self.Library.DynamicImportedThemes or {} + self.Library.DynamicImportedThemes[name] = tbl + return tbl + end + end + return nil, "not found" + end + + -- APPLY theme by name (best-effort) + function InterfaceManager:ApplyTheme(name) + if not name then return false, "no name" end + + -- try library SetTheme directly (preferred) + if self.Library and type(self.Library.SetTheme) == "function" then + local ok, err = pcall(function() self.Library:SetTheme(name) end) + if ok then return true, "SetTheme called" end + end + + -- otherwise try to get theme table and apply via other APIs + local tbl, err = self:LoadThemeTableByName(name) + if not tbl then return false, "load failed: "..tostring(err) end + + -- if library supports RegisterTheme + SetTheme, register then set + if self.Library and type(self.Library.RegisterTheme) == "function" and type(self.Library.SetTheme) == "function" then + pcall(function() self.Library:RegisterTheme(name, tbl) end) + pcall(function() self.Library:SetTheme(name) end) + return true, "registered+set" + end + -- if library supports ApplyThemeFromTable or similar, try that + if self.Library and type(self.Library.ApplyThemeFromTable) == "function" then + pcall(function() self.Library:ApplyThemeFromTable(tbl) end) + return true, "applied via ApplyThemeFromTable" + end + + -- fallback: try to insert into Library.Themes map and attempt SetTheme + if self.Library then + if type(self.Library.Themes) ~= "table" then + self.Library.Themes = {} + end + -- if map, set + local isMap = false + for k,_ in pairs(self.Library.Themes) do if type(k) ~= "number" then isMap = true break end end + if isMap then + self.Library.Themes[name] = tbl + if type(self.Library.SetTheme) == "function" then pcall(function() self.Library:SetTheme(name) end) end + return true, "merged into Library.Themes map" + else + -- array-style library: append name and store dynamic table + local exists = false + for _,v in ipairs(self.Library.Themes) do if v == name then exists = true break end end + if not exists then table.insert(self.Library.Themes, name) end + self.Library.DynamicImportedThemes = self.Library.DynamicImportedThemes or {} + self.Library.DynamicImportedThemes[name] = tbl + if type(self.Library.SetTheme) == "function" then pcall(function() self.Library:SetTheme(name) end) end + return true, "added name + dynamic table" + end + end + + return false, "no library to apply" + end + + -- helper to produce merged dropdown-friendly theme name list + local function getMergedThemeNames(library, selfRef) + local names = {} -- library built-in - if type(library.Themes) == "table" then + if library and type(library.Themes) == "table" then local numeric = true - for k,v in pairs(library.Themes) do - if type(k) ~= "number" then numeric = false break end - end + for k,v in pairs(library.Themes) do if type(k) ~= "number" then numeric = false break end end if numeric then - for _,v in ipairs(library.Themes) do if type(v)=="string" then names[v]=true end end + for _,v in ipairs(library.Themes) do if type(v) == "string" then names[v] = true end end else - for k,v in pairs(library.Themes) do if type(k)=="string" then names[k]=true end end + for k,v in pairs(library.Themes) do if type(k) == "string" then names[k] = true end end end end - -- dynamic imports - if library.DynamicImportedThemes then - for k,v in pairs(library.DynamicImportedThemes) do names[k]=true end + if library and library.DynamicImportedThemes then + for k,_ in pairs(library.DynamicImportedThemes) do names[k] = true end end - -- disk themes - local disk = InterfaceManager:ScanThemes() - for _, item in ipairs(disk) do names[item.name] = true end - + if selfRef and selfRef.DiskThemeIndex then + for k,_ in pairs(selfRef.DiskThemeIndex) do names[k] = true end + end local out = {} for k,_ in pairs(names) do table.insert(out, k) end table.sort(out) @@ -279,16 +381,17 @@ local InterfaceManager = {} do local Library = self.Library local Settings = InterfaceManager.Settings - -- ensure folders exist & load config of this map before UI + -- ensure folders exist & load config before UI InterfaceManager:BuildFolderTree() InterfaceManager:LoadSettings() - -- register disk themes now so Library gets them (best-effort) + -- scan/register disk themes + InterfaceManager:ScanThemes() InterfaceManager:RegisterThemesToLibrary(Library) local section = tab:AddSection("Interface") -- merged name list - local mergedValues = getLibraryThemeNames(Library) + local mergedValues = getMergedThemeNames(Library, InterfaceManager) local InterfaceTheme = section:AddDropdown("InterfaceTheme", { Title = "Theme", @@ -296,17 +399,10 @@ local InterfaceManager = {} do Values = mergedValues, Default = Settings.Theme, Callback = function(Value) - -- try library API first - if type(Library.SetTheme) == "function" then - pcall(function() Library:SetTheme(Value) end) - else - -- if theme was imported dynamically, try register+set - if Library.DynamicImportedThemes and Library.DynamicImportedThemes[Value] then - if type(Library.RegisterTheme) == "function" then - pcall(function() Library:RegisterTheme(Value, Library.DynamicImportedThemes[Value]) end) - pcall(function() Library:SetTheme(Value) end) - end - end + -- apply the theme (best-effort) + local ok, msg = InterfaceManager:ApplyTheme(Value) + if not ok then + warn("[InterfaceManager] ApplyTheme failed:", msg) end Settings.Theme = Value InterfaceManager:SaveSettings() @@ -315,15 +411,15 @@ local InterfaceManager = {} do InterfaceTheme:SetValue(Settings.Theme) - -- add Refresh button to re-scan Themes folder (useful after dropping files manually) + -- add Refresh button if section.AddButton then section:AddButton({ Title = "Refresh Themes", - Description = "Scan ATGHubSettings/Themes and update dropdown (use after adding files).", + Description = "Scan ATGHubSettings/Themes and update dropdown.", Callback = function() - -- re-scan and re-register - local newList = getLibraryThemeNames(Library) - -- try to update dropdown values (best-effort — depends on dropdown API) + InterfaceManager:ScanThemes() + InterfaceManager:RegisterThemesToLibrary(Library) + local newList = getMergedThemeNames(Library, InterfaceManager) if InterfaceTheme.SetValues then pcall(function() InterfaceTheme:SetValues(newList) end) elseif InterfaceTheme.SetOptions then @@ -331,23 +427,20 @@ local InterfaceManager = {} do elseif InterfaceTheme.UpdateValues then pcall(function() InterfaceTheme:UpdateValues(newList) end) else - -- fallback: notify in console (UI may require re-open) - print("[InterfaceManager] Refreshed themes (re-open menu if dropdown didn't update).") + print("[InterfaceManager] Refreshed theme list, re-open menu if dropdown didn't update.") end - -- also attempt to register any new files to library - InterfaceManager:RegisterThemesToLibrary(Library) end }) end - -- acrylic toggle + -- other toggles 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) + if type(Library.ToggleAcrylic) == "function" then pcall(function() Library:ToggleAcrylic(Value) end) end Settings.Acrylic = Value InterfaceManager:SaveSettings() end @@ -359,9 +452,7 @@ local InterfaceManager = {} do Description = "Makes the interface transparent.", Default = Settings.Transparency, Callback = function(Value) - if type(Library.ToggleTransparency) == "function" then - Library:ToggleTransparency(Value) - end + if type(Library.ToggleTransparency) == "function" then pcall(function() Library:ToggleTransparency(Value) end) end Settings.Transparency = Value InterfaceManager:SaveSettings() end From 63cad5a127728e1d76ce9148cedbbc49f2b3d2d5 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 16 Oct 2025 10:41:37 +0700 Subject: [PATCH 17/76] Update config.lua --- config.lua | 497 ++++++++++++++++++++++++++--------------------------- 1 file changed, 245 insertions(+), 252 deletions(-) diff --git a/config.lua b/config.lua index 230963f..e0d90d5 100644 --- a/config.lua +++ b/config.lua @@ -1,22 +1,20 @@ +-- InterfaceManager (ATGHubSettings + dynamic theme apply) local httpService = game:GetService("HttpService") local Workspace = game:GetService("Workspace") local InterfaceManager = {} do - -- root folder (single clean folder) + -- root folder ใหม่ (สร้างใหม่สะอาดๆ) InterfaceManager.FolderRoot = "ATGHubSettings" -- default settings - InterfaceManager.Settings = { - Theme = "Dark", - Acrylic = true, - Transparency = true, - MenuKeybind = "LeftControl" - } - - -- internal index: map displayName -> { path, ext } - InterfaceManager.DiskThemeIndex = {} - - -- helpers + InterfaceManager.Settings = { + Theme = "Dark", + Acrylic = true, + Transparency = true, + MenuKeybind = "LeftControl" + } + + -- internal local function sanitizeFilename(name) name = tostring(name or "") name = name:gsub("%s+", "_") @@ -25,140 +23,135 @@ local InterfaceManager = {} do return name end - local function getMapFolderName() - -- ใช้ชื่อของ object "Map" ใน Workspace ถ้ามี + local function getMapName() local ok, map = pcall(function() return Workspace:FindFirstChild("Map") end) - if ok and map and map:IsA and map:IsA("Instance") then + if ok and map and type(map.Name) == "string" then return sanitizeFilename(map.Name) end - -- fallback: ใช้ Workspace.Name - local ok2, wname = pcall(function() return Workspace.Name end) - if ok2 and wname then return sanitizeFilename(wname) end + -- fallback to place name or "UnknownMap" + local ok2, wn = pcall(function() return Workspace.Name end) + if ok2 and wn then return sanitizeFilename(wn) end return "UnknownMap" end local function ensureFolder(path) - if not isfolder(path) then - makefolder(path) - end + if not isfolder(path) then makefolder(path) end end - -- build folder tree (clean create, no migration) - function InterfaceManager:BuildFolderTree() + -- Build folders: root, Themes, /Themes, settings, Imports + function InterfaceManager:BuildFolderTree() local root = self.FolderRoot ensureFolder(root) - local mapFolder = root .. "/" .. getMapFolderName() + local mapName = getMapName() + local mapFolder = root .. "/" .. mapName ensureFolder(mapFolder) - ensureFolder(root .. "/settings") -- global settings - ensureFolder(mapFolder .. "/settings") -- per-map settings + -- global & per-map settings + ensureFolder(root .. "/settings") + ensureFolder(mapFolder .. "/settings") - ensureFolder(root .. "/Themes") -- global themes - ensureFolder(mapFolder .. "/Themes") -- per-map themes (optional) + -- global & per-map themes + ensureFolder(root .. "/Themes") + ensureFolder(mapFolder .. "/Themes") - ensureFolder(root .. "/Imports") -- manual import drop - end + -- imports + ensureFolder(root .. "/Imports") + end - function InterfaceManager:SetFolder(folder) + function InterfaceManager:SetFolder(folder) self.FolderRoot = tostring(folder or "ATGHubSettings") self:BuildFolderTree() end - function InterfaceManager:SetLibrary(library) - self.Library = library - -- register themes found on disk to library (best-effort) - self:RegisterThemesToLibrary(library) + function InterfaceManager:SetLibrary(lib) + self.Library = lib + -- register disk themes at set time + self:RegisterThemesToLibrary(lib) end - -- prefixed filename for per-map settings: ATG Hub - .json + -- config file path uses map name (not PlaceId) local function getPrefixedSettingsFilename() - local mapName = getMapFolderName() + local mapName = getMapName() local fname = "ATG Hub - " .. sanitizeFilename(mapName) .. ".json" return fname end local function getConfigFilePath(self) local root = self.FolderRoot - local mapFolder = root .. "/" .. getMapFolderName() - ensureFolder(mapFolder) + local mapName = getMapName() + local configFolder = root .. "/" .. mapName + ensureFolder(configFolder) local fname = getPrefixedSettingsFilename() - return mapFolder .. "/" .. fname + return configFolder .. "/" .. fname end - function InterfaceManager:SaveSettings() + function InterfaceManager:SaveSettings() local path = getConfigFilePath(self) local folder = path:match("^(.*)/[^/]+$") if folder then ensureFolder(folder) end - local encoded = httpService:JSONEncode(self.Settings or {}) - writefile(path, encoded) - end - - function InterfaceManager:LoadSettings() - local path = getConfigFilePath(self) - local legacyPath = self.FolderRoot .. "/options.json" -- keep merge if exists - - if isfile(path) then - local data = readfile(path) - local success, decoded = pcall(httpService.JSONDecode, httpService, data) - if success and type(decoded) == "table" then - for i, v in next, decoded do self.Settings[i] = v end - end + local encoded = httpService:JSONEncode(self.Settings or {}) + writefile(path, encoded) + end + + function InterfaceManager:LoadSettings() + local path = getConfigFilePath(self) + local legacyPath = self.FolderRoot .. "/options.json" + + if isfile(path) then + local data = readfile(path) + local ok, dec = pcall(httpService.JSONDecode, httpService, data) + if ok and type(dec) == "table" then + for k,v in pairs(dec) do self.Settings[k] = v end + end return - end + end + -- ถ้ามี legacy options.json ให้ merge ค่า (แต่ไม่คัดลอกโฟลเดอร์ทั้งหมด) if isfile(legacyPath) then local data = readfile(legacyPath) - local success, decoded = pcall(httpService.JSONDecode, httpService, data) - if success and type(decoded) == "table" then - for i,v in next, decoded do self.Settings[i] = v end - -- save to new per-map path + local ok, dec = pcall(httpService.JSONDecode, httpService, data) + if ok and type(dec) == "table" then + for k,v in pairs(dec) do self.Settings[k] = v end local folder = path:match("^(.*)/[^/]+$") if folder then ensureFolder(folder) end - local encoded = httpService:JSONEncode(self.Settings or {}) - writefile(path, encoded) + writefile(path, httpService:JSONEncode(self.Settings or {})) end return end - -- otherwise keep defaults - end + -- else use defaults + end - -- ================= Theme utilities ================= + -- ================= Theme scanning/importing ================= - -- scan Themes folders and update DiskThemeIndex + -- scan Themes folders and return list of { name, path, ext } function InterfaceManager:ScanThemes() - self.DiskThemeIndex = {} + local out = {} local root = self.FolderRoot - local themePaths = { + local mapName = getMapName() + local paths = { root .. "/Themes", - root .. "/" .. getMapFolderName() .. "/Themes" + root .. "/" .. mapName .. "/Themes" } - for _, folder in ipairs(themePaths) do + for _, folder in ipairs(paths) do if isfolder(folder) and type(listfiles) == "function" then local ok, files = pcall(listfiles, folder) if ok and type(files) == "table" then - for _, fpath in ipairs(files) do - if fpath:match("%.lua$") or fpath:match("%.json$") then - local base = fpath:match("([^/\\]+)$") or fpath - local display = base - display = display:gsub("^ATG Hub %- ", "") - display = display:gsub("%.lua$", ""):gsub("%.json$", "") - display = display:gsub("%_", " ") - local ext = fpath:match("%.([a-zA-Z0-9]+)$") - self.DiskThemeIndex[display] = { path = fpath, ext = ext } + for _, f in ipairs(files) do + if f:match("%.lua$") or f:match("%.json$") then + local base = f:match("([^/\\]+)$") or f + local display = base:gsub("^ATG Hub %- ", ""):gsub("%.lua$",""):gsub("%.json$",""):gsub("%_"," ") + local ext = f:match("%.([a-zA-Z0-9]+)$") + table.insert(out, { name = display, path = f, ext = ext }) end end end end end - -- return list of display names - local out = {} - for name,_ in pairs(self.DiskThemeIndex) do table.insert(out, name) end - table.sort(out) return out end - -- import theme content into global Themes folder + -- write theme file to global Themes function InterfaceManager:ImportTheme(name, content, ext) ext = tostring(ext or "lua"):lower() if ext ~= "lua" and ext ~= "json" then ext = "lua" end @@ -168,81 +161,64 @@ local InterfaceManager = {} do local fname = "ATG Hub - " .. safe .. "." .. ext local full = rootThemes .. "/" .. fname writefile(full, tostring(content or "")) - -- update index - self:ScanThemes() - -- attempt to register immediately - if self.Library then self:TryRegisterThemeFile(full, ext) end + -- try to register immediately + if self.Library then + self:TryRegisterThemeFile(full, ext) + end return full end - -- parse a theme file (return table or nil + err) - function InterfaceManager:ParseThemeFile(fullpath, ext) - if not isfile(fullpath) then return nil, "file not found" end + -- parse a theme file and return themeTable (or nil) + local function parseThemeFile(fullpath, ext) + if not isfile(fullpath) then return nil end local raw = readfile(fullpath) if ext == "json" then local ok, dec = pcall(httpService.JSONDecode, httpService, raw) if ok and type(dec) == "table" then return dec end - return nil, "json decode failed" - end - -- lua - local ok, chunk = pcall(loadstring, raw) - if not ok or type(chunk) ~= "function" then - return nil, "lua load failed" + return nil + else + -- lua: try loadstring and expect return table + local ok, chunk = pcall(loadstring, raw) + if not ok or type(chunk) ~= "function" then return nil end + local ok2, result = pcall(chunk) + if ok2 and type(result) == "table" then return result end + return nil end - local ok2, result = pcall(chunk) - if ok2 and type(result) == "table" then - return result - end - -- maybe module used globals/tasks and returned nothing; try to run with 'return ' prefix - local ok3, chunk2 = pcall(loadstring, "return " .. raw) - if ok3 and type(chunk2) == "function" then - local ok4, res2 = pcall(chunk2) - if ok4 and type(res2) == "table" then return res2 end - end - return nil, "no table returned" end - -- try to register theme file to library (and keep dynamic table) + -- Try to register a theme file to the library (best-effort) function InterfaceManager:TryRegisterThemeFile(fullpath, ext) - local ok, themeTbl = pcall(function() return self:ParseThemeFile(fullpath, ext) end) - if not ok or type(themeTbl) ~= "table" then return false, "parse failed" end + local themeTbl = parseThemeFile(fullpath, ext) + if not themeTbl then return false, "could not parse theme" end + if not self.Library then return false, "no library" end - if not self.Library then - -- just store to dynamic store so dropdown can use it later - self.Library = self.Library or {} - self.Library.DynamicImportedThemes = self.Library.DynamicImportedThemes or {} - local displayName = fullpath:match("([^/\\]+)$") or fullpath - displayName = displayName:gsub("^ATG Hub %- ", ""):gsub("%.lua$",""):gsub("%.json$",""):gsub("%_"," ") - self.Library.DynamicImportedThemes[displayName] = themeTbl - return true, "stored dynamic (no library)" - end - - -- prefer RegisterTheme API local displayName = fullpath:match("([^/\\]+)$") or fullpath displayName = displayName:gsub("^ATG Hub %- ", ""):gsub("%.lua$",""):gsub("%.json$",""):gsub("%_"," ") + + -- Prefer API: Library:RegisterTheme(name, table) if type(self.Library.RegisterTheme) == "function" then pcall(function() self.Library:RegisterTheme(displayName, themeTbl) end) - -- also store dynamic copy + -- also store dynamic import table if needed self.Library.DynamicImportedThemes = self.Library.DynamicImportedThemes or {} self.Library.DynamicImportedThemes[displayName] = themeTbl return true, "registered" end - -- fallback: if Library.Themes is a map, merge as table - local lt = self.Library.Themes - if type(lt) == "table" then - -- detect map vs array + -- Fallback: if Library.Themes is map, put it there + if type(self.Library.Themes) == "table" then local isMap = false - for k,v in pairs(lt) do if type(k) ~= "number" then isMap = true break end end + for k,v in pairs(self.Library.Themes) do + if type(k) ~= "number" then isMap = true break end + end if isMap then self.Library.Themes[displayName] = themeTbl self.Library.DynamicImportedThemes = self.Library.DynamicImportedThemes or {} self.Library.DynamicImportedThemes[displayName] = themeTbl return true, "merged into map" else - -- append name to array and save theme table in DynamicImportedThemes + -- array: push name, and keep table in DynamicImportedThemes local exists = false - for _,v in ipairs(lt) do if v == displayName then exists = true break end end + for _,v in ipairs(self.Library.Themes) do if v == displayName then exists = true break end end if not exists then table.insert(self.Library.Themes, displayName) end self.Library.DynamicImportedThemes = self.Library.DynamicImportedThemes or {} self.Library.DynamicImportedThemes[displayName] = themeTbl @@ -250,110 +226,137 @@ local InterfaceManager = {} do end end - return false, "could not merge into library" + return false, "no suitable integration" end - -- register all disk themes to library / dynamic store + -- Register all disk themes to library function InterfaceManager:RegisterThemesToLibrary(library) - if not library and not self.Library then return end - self:ScanThemes() - for name,info in pairs(self.DiskThemeIndex) do - pcall(function() - self:TryRegisterThemeFile(info.path, info.ext) - end) + if not library then return end + local found = self:ScanThemes() + for _, item in ipairs(found) do + pcall(function() self:TryRegisterThemeFile(item.path, item.ext) end) end end - -- load theme table by display name (search library dynamic, library map, disk) - function InterfaceManager:LoadThemeTableByName(name) - if not name then return nil, "no name" end - -- check dynamic store - if self.Library and self.Library.DynamicImportedThemes and self.Library.DynamicImportedThemes[name] then - return self.Library.DynamicImportedThemes[name] - end - -- check library.Themes if it's map style - if self.Library and type(self.Library.Themes) == "table" then - -- map style? - local isMap = false - for k,_ in pairs(self.Library.Themes) do if type(k) ~= "number" then isMap = true break end end - if isMap and self.Library.Themes[name] and type(self.Library.Themes[name]) == "table" then - return self.Library.Themes[name] - end - end - -- check disk - if self.DiskThemeIndex[name] then - local info = self.DiskThemeIndex[name] - local ok, tbl = pcall(function() return self:ParseThemeFile(info.path, info.ext) end) - if ok and type(tbl) == "table" then - -- store into dynamic for later - self.Library = self.Library or {} - self.Library.DynamicImportedThemes = self.Library.DynamicImportedThemes or {} - self.Library.DynamicImportedThemes[name] = tbl - return tbl - end - end - return nil, "not found" + -- ================ Theme apply logic (best-effort, dynamic accent update) ================ + + -- low-level: try to push a single Accent color to Library (multiple API tries) + local function tryUpdateAccentToLibrary(lib, color) + if not lib or not color then return end + -- try API names that library might expose + local ok + if type(lib.UpdateAccent) == "function" then pcall(lib.UpdateAccent, lib, color) end + if type(lib.SetAccent) == "function" then pcall(lib.SetAccent, lib, color) end + if type(lib.SetAccentColor) == "function" then pcall(lib.SetAccentColor, lib, color) end + -- try setting fields directly + pcall(function() lib.Accent = color end) + pcall(function() lib.CurrentAccent = color end) + -- if library has a refresh hook + if type(lib.RefreshTheme) == "function" then pcall(lib.RefreshTheme, lib) end + if type(lib.ApplyTheme) == "function" then pcall(lib.ApplyTheme, lib, lib.CurrentTheme or {}) end end - -- APPLY theme by name (best-effort) - function InterfaceManager:ApplyTheme(name) - if not name then return false, "no name" end - - -- try library SetTheme directly (preferred) - if self.Library and type(self.Library.SetTheme) == "function" then - local ok, err = pcall(function() self.Library:SetTheme(name) end) - if ok then return true, "SetTheme called" end + -- Apply theme by name (search in dynamic imports, library map, or disk), and start polling Accent if theme is dynamic + function InterfaceManager:ApplyThemeByName(name) + if not name or not self.Library then return false, "no name or library" end + local lib = self.Library + + -- first, if library has a SetTheme that accepts name, prefer that + if type(lib.SetTheme) == "function" then + local ok, err = pcall(function() lib:SetTheme(name) end) + if ok then + -- after set, if theme table exists in DynamicImportedThemes, start poll + local themeTbl = (lib.DynamicImportedThemes and lib.DynamicImportedThemes[name]) or nil + if themeTbl and type(themeTbl) == "table" then + self:StartThemeAccentPoll(themeTbl) + end + return true, "SetTheme(name) called" + end end - -- otherwise try to get theme table and apply via other APIs - local tbl, err = self:LoadThemeTableByName(name) - if not tbl then return false, "load failed: "..tostring(err) end - - -- if library supports RegisterTheme + SetTheme, register then set - if self.Library and type(self.Library.RegisterTheme) == "function" and type(self.Library.SetTheme) == "function" then - pcall(function() self.Library:RegisterTheme(name, tbl) end) - pcall(function() self.Library:SetTheme(name) end) - return true, "registered+set" + -- next try dynamic imports map + if lib.DynamicImportedThemes and lib.DynamicImportedThemes[name] then + local themeTbl = lib.DynamicImportedThemes[name] + -- try RegisterTheme + SetTheme if possible + if type(lib.RegisterTheme) == "function" then + pcall(function() lib:RegisterTheme(name, themeTbl) end) + pcall(function() lib:SetTheme(name) end) + elseif type(lib.ApplyTheme) == "function" then + pcall(function() lib:ApplyTheme(themeTbl) end) + else + -- try set fields directly + pcall(function() lib.CurrentTheme = themeTbl end) + if type(lib.RefreshTheme) == "function" then pcall(function() lib:RefreshTheme() end) end + end + -- start polling dynamic Accent + self:StartThemeAccentPoll(themeTbl) + return true, "applied from DynamicImportedThemes" end - -- if library supports ApplyThemeFromTable or similar, try that - if self.Library and type(self.Library.ApplyThemeFromTable) == "function" then - pcall(function() self.Library:ApplyThemeFromTable(tbl) end) - return true, "applied via ApplyThemeFromTable" + -- fallback: scan disk and try to parse theme file and apply table + local found = self:ScanThemes() + for _, item in ipairs(found) do + if item.name == name then + local themeTbl = parseThemeFile(item.path, item.ext) + if themeTbl then + if type(lib.RegisterTheme) == "function" then + pcall(function() lib:RegisterTheme(name, themeTbl) end) + pcall(function() lib:SetTheme(name) end) + elseif type(lib.ApplyTheme) == "function" then + pcall(function() lib:ApplyTheme(themeTbl) end) + else + pcall(function() lib.CurrentTheme = themeTbl end) + if type(lib.RefreshTheme) == "function" then pcall(function() lib:RefreshTheme() end) end + end + self:StartThemeAccentPoll(themeTbl) + return true, "applied from disk" + end + end end - -- fallback: try to insert into Library.Themes map and attempt SetTheme - if self.Library then - if type(self.Library.Themes) ~= "table" then - self.Library.Themes = {} - end - -- if map, set - local isMap = false - for k,_ in pairs(self.Library.Themes) do if type(k) ~= "number" then isMap = true break end end - if isMap then - self.Library.Themes[name] = tbl - if type(self.Library.SetTheme) == "function" then pcall(function() self.Library:SetTheme(name) end) end - return true, "merged into Library.Themes map" - else - -- array-style library: append name and store dynamic table - local exists = false - for _,v in ipairs(self.Library.Themes) do if v == name then exists = true break end end - if not exists then table.insert(self.Library.Themes, name) end - self.Library.DynamicImportedThemes = self.Library.DynamicImportedThemes or {} - self.Library.DynamicImportedThemes[name] = tbl - if type(self.Library.SetTheme) == "function" then pcall(function() self.Library:SetTheme(name) end) end - return true, "added name + dynamic table" + return false, "could not apply theme" + end + + -- poll Theme.Accent and push to library (if theme updates Accent dynamically) + function InterfaceManager:StartThemeAccentPoll(themeTbl) + if not themeTbl or type(themeTbl) ~= "table" then return end + -- avoid starting multiple pollers for same table + themeTbl._ATG_POLLING = themeTbl._ATG_POLLING or true + task.spawn(function() + local last = nil + while themeTbl._ATG_POLLING do + local acc = themeTbl.Accent + -- if Accent is function, call it (support function-based themes) + if type(acc) == "function" then + local ok, res = pcall(acc) + if ok and res then acc = res end + end + -- if color changed, push to lib + if acc and (not last or acc ~= last) then + tryUpdateAccentToLibrary(self.Library, acc) + last = acc + end + task.wait(0.05) end - end + end) + end - return false, "no library to apply" + -- Register all disk themes to library (best-effort) + function InterfaceManager:RegisterThemesToLibrary(library) + if not library then return end + local found = self:ScanThemes() + for _, item in ipairs(found) do + pcall(function() self:TryRegisterThemeFile(item.path, item.ext) end) + end end - -- helper to produce merged dropdown-friendly theme name list - local function getMergedThemeNames(library, selfRef) + -- ======== UI Builder ======== + local function getLibraryThemeNames(library) local names = {} - -- library built-in - if library and type(library.Themes) == "table" then + if not library then return {} end + + -- library.Themes as array or map + if type(library.Themes) == "table" then local numeric = true for k,v in pairs(library.Themes) do if type(k) ~= "number" then numeric = false break end end if numeric then @@ -362,78 +365,68 @@ local InterfaceManager = {} do for k,v in pairs(library.Themes) do if type(k) == "string" then names[k] = true end end end end + -- dynamic imports - if library and library.DynamicImportedThemes then - for k,_ in pairs(library.DynamicImportedThemes) do names[k] = true end + if library.DynamicImportedThemes then + for k,v in pairs(library.DynamicImportedThemes) do names[k] = true end end + -- disk themes - if selfRef and selfRef.DiskThemeIndex then - for k,_ in pairs(selfRef.DiskThemeIndex) do names[k] = true end - end + local disk = InterfaceManager:ScanThemes() + for _, item in ipairs(disk) do names[item.name] = true end + local out = {} for k,_ in pairs(names) do table.insert(out, k) end table.sort(out) return out end - function InterfaceManager:BuildInterfaceSection(tab) - assert(self.Library, "Must set InterfaceManager.Library") + function InterfaceManager:BuildInterfaceSection(tab) + assert(self.Library, "Must set InterfaceManager.Library") local Library = self.Library - local Settings = InterfaceManager.Settings + local Settings = InterfaceManager.Settings - -- ensure folders exist & load config before UI InterfaceManager:BuildFolderTree() - InterfaceManager:LoadSettings() - -- scan/register disk themes - InterfaceManager:ScanThemes() + InterfaceManager:LoadSettings() InterfaceManager:RegisterThemesToLibrary(Library) local section = tab:AddSection("Interface") - -- merged name list - local mergedValues = getMergedThemeNames(Library, InterfaceManager) - + local mergedValues = getLibraryThemeNames(Library) local InterfaceTheme = section:AddDropdown("InterfaceTheme", { Title = "Theme", Description = "Changes the interface theme.", Values = mergedValues, Default = Settings.Theme, Callback = function(Value) - -- apply the theme (best-effort) - local ok, msg = InterfaceManager:ApplyTheme(Value) - if not ok then - warn("[InterfaceManager] ApplyTheme failed:", msg) - end - Settings.Theme = Value - InterfaceManager:SaveSettings() + -- apply using our helper which will attempt various APIs + dynamic accent polling + InterfaceManager:ApplyThemeByName(Value) + Settings.Theme = Value + InterfaceManager:SaveSettings() end }) + InterfaceTheme:SetValue(Settings.Theme) - InterfaceTheme:SetValue(Settings.Theme) - - -- add Refresh button + -- Refresh button for manual rescan if section.AddButton then section:AddButton({ Title = "Refresh Themes", - Description = "Scan ATGHubSettings/Themes and update dropdown.", + Description = "Rescan ATGHubSettings/Themes and update dropdown.", Callback = function() - InterfaceManager:ScanThemes() InterfaceManager:RegisterThemesToLibrary(Library) - local newList = getMergedThemeNames(Library, InterfaceManager) + local newList = getLibraryThemeNames(Library) if InterfaceTheme.SetValues then pcall(function() InterfaceTheme:SetValues(newList) end) elseif InterfaceTheme.SetOptions then pcall(function() InterfaceTheme:SetOptions(newList) end) - elseif InterfaceTheme.UpdateValues then - pcall(function() InterfaceTheme:UpdateValues(newList) end) else - print("[InterfaceManager] Refreshed theme list, re-open menu if dropdown didn't update.") + print("[InterfaceManager] Themes refreshed. Re-open menu if dropdown not updated.") end end }) end - -- other toggles + -- rest of the UI controls... if Library.UseAcrylic then section:AddToggle("AcrylicToggle", { Title = "Acrylic", @@ -441,8 +434,8 @@ local InterfaceManager = {} do Default = Settings.Acrylic, Callback = function(Value) if type(Library.ToggleAcrylic) == "function" then pcall(function() Library:ToggleAcrylic(Value) end) end - Settings.Acrylic = Value - InterfaceManager:SaveSettings() + Settings.Acrylic = Value + InterfaceManager:SaveSettings() end }) end @@ -454,17 +447,17 @@ local InterfaceManager = {} do Callback = function(Value) if type(Library.ToggleTransparency) == "function" then pcall(function() Library:ToggleTransparency(Value) end) end Settings.Transparency = Value - InterfaceManager:SaveSettings() + InterfaceManager:SaveSettings() end }) local MenuKeybind = section:AddKeybind("MenuKeybind", { Title = "Minimize Bind", Default = Settings.MenuKeybind }) MenuKeybind:OnChanged(function() Settings.MenuKeybind = MenuKeybind.Value - InterfaceManager:SaveSettings() + InterfaceManager:SaveSettings() end) Library.MinimizeKeybind = MenuKeybind - end + end end return InterfaceManager From b5a5f87b4b7a0a99db42f8a64d1a35a35b46ec15 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 16 Oct 2025 11:16:03 +0700 Subject: [PATCH 18/76] Update autosave.lua --- autosave.lua | 841 +++++++++++++++++++++++++++------------------------ 1 file changed, 449 insertions(+), 392 deletions(-) diff --git a/autosave.lua b/autosave.lua index b58cc95..fedc861 100644 --- a/autosave.lua +++ b/autosave.lua @@ -1,399 +1,456 @@ +-- SaveManager.lua +-- ปรับปรุงโดย: ChatGPT (อัปเดตเพื่อใช้ชื่อแมพ, ลดการสร้างโฟลเดอร์, เปลี่ยนปุ่ม autoload เป็น Toggle, +-- เพิ่ม Title (อังกฤษ) และ Description (ไทย) ให้กับ UI ทุกตัวในส่วน Configuration) + +-- ตัวอย่างการใช้งาน (ตัวอย่างนี้เป็นคอมเมนต์): +-- local Toggle = Tabs.Main:AddToggle("MyToggle", { Title = "Example Toggle", Description = "ทดสอบสวิตช์ (เปิด/ปิด) -- คำอธิบายภาษาไทย", Default = false }) +-- Toggle:OnChanged(function() +-- print("Toggle changed:", Options.MyToggle.Value) +-- end) +-- SaveManager จะจัดการไฟล์คอนฟิกให้ในโฟลเดอร์: //settings + local httpService = game:GetService("HttpService") local Workspace = game:GetService("Workspace") local SaveManager = {} do - -- root folder (can be changed via SetFolder) - 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 getMapName() - local ok, map = pcall(function() return Workspace:FindFirstChild("Map") end) - if ok and map and map:IsA("Instance") then - return sanitizeFilename(map.Name) - end - local ok2, wname = pcall(function() return Workspace.Name end) - if ok2 and wname then return sanitizeFilename(wname) end - return "UnknownMap" - end - - local function ensureFolder(path) - if not isfolder(path) then - makefolder(path) - end - end - - -- get configs folder for current place/map - local function getConfigsFolder(self) - local root = self.FolderRoot - local placeId = getPlaceId() - local mapName = getMapName() - -- FluentSettings///settings - return root .. "/" .. placeId .. "/" .. mapName .. "/settings" - end - - local function getConfigFilePath(self, name) - local folder = getConfigsFolder(self) - return folder .. "/" .. name .. ".json" - end - - -- Build folder tree and migrate legacy configs if found (copy only) - function SaveManager:BuildFolderTree() - local root = self.FolderRoot - ensureFolder(root) - - local placeId = getPlaceId() - local placeFolder = root .. "/" .. placeId - ensureFolder(placeFolder) - - local mapName = getMapName() - local mapFolder = placeFolder .. "/" .. mapName - ensureFolder(mapFolder) - - local settingsFolder = mapFolder .. "/settings" - ensureFolder(settingsFolder) - - -- legacy folder: /settings (old layout). If files exist there, copy them into current map settings - 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 = settingsFolder .. "/" .. base .. ".json" - -- copy only if destination does not exist yet - if not isfile(dest) then - local ok, data = pcall(readfile, f) - if ok and data then - local success, err = pcall(writefile, dest, data) - -- ignore write errors but do not fail - end - end - end - end - end - - -- also migrate autoload.txt if present (copy only) - local autopath = legacySettingsFolder .. "/autoload.txt" - if isfile(autopath) then - local autodata = readfile(autopath) - local destAuto = settingsFolder .. "/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 "FluentSettings") - 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 - - -- ensure folder exists - 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: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" then - table.insert(out, name) - end - end - end - return out - end - - function SaveManager:LoadAutoloadConfig() - local autopath = getConfigsFolder(self) .. "/autoload.txt" - if isfile(autopath) then - local name = readfile(autopath) - local success, err = self:Load(name) - if not success then - return self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "Failed to load autoload config: " .. err, - Duration = 7 - }) - end - - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Auto loaded config %q", name), - Duration = 7 - }) - end - end - - function SaveManager:BuildConfigSection(tab) - assert(self.Library, "Must set SaveManager.Library") - - local section = tab:AddSection("Configuration") - - section:AddInput("SaveManager_ConfigName", { Title = "Config name" }) - section:AddDropdown("SaveManager_ConfigList", { Title = "Config list", Values = self:RefreshConfigList(), AllowNull = true }) - - section:AddButton({ - Title = "Create config", - Callback = function() - local name = SaveManager.Options.SaveManager_ConfigName.Value - - if name:gsub(" ", "") == "" then - return self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "Invalid config name (empty)", - Duration = 7 - }) - end - - local success, err = self:Save(name) - if not success then - return self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "Failed to save config: " .. err, - Duration = 7 - }) - end - - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Created config %q", name), - Duration = 7 - }) - - SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) - SaveManager.Options.SaveManager_ConfigList:SetValue(nil) - end - }) - - section:AddButton({Title = "Load config", Callback = function() - local name = SaveManager.Options.SaveManager_ConfigList.Value - - local success, err = self:Load(name) - if not success then - return self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "Failed to load config: " .. err, - Duration = 7 - }) - end - - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Loaded config %q", name), - Duration = 7 - }) - end}) - - section:AddButton({Title = "Overwrite config", Callback = function() - local name = SaveManager.Options.SaveManager_ConfigList.Value - - local success, err = self:Save(name) - if not success then - return self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "Failed to overwrite config: " .. err, - Duration = 7 - }) - end - - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Overwrote config %q", name), - Duration = 7 - }) - end}) - - section:AddButton({Title = "Refresh list", Callback = function() - SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) - SaveManager.Options.SaveManager_ConfigList:SetValue(nil) - end}) - - local AutoloadButton - AutoloadButton = section:AddButton({Title = "Set as autoload", Description = "Current autoload config: none", Callback = function() - local name = SaveManager.Options.SaveManager_ConfigList.Value - local autopath = getConfigsFolder(self) .. "/autoload.txt" - writefile(autopath, name) - AutoloadButton:SetDesc("Current autoload config: " .. name) - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Set %q to auto load", name), - Duration = 7 - }) - end}) - - -- populate current autoload desc if exists - local autop = getConfigsFolder(self) .. "/autoload.txt" - if isfile(autop) then - local name = readfile(autop) - AutoloadButton:SetDesc("Current autoload config: " .. name) - end - - SaveManager:SetIgnoreIndexes({ "SaveManager_ConfigList", "SaveManager_ConfigName" }) - end - - -- initial build - SaveManager:BuildFolderTree() + -- root folder (can be changed via SetFolder) + 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 getMapName() + local ok, map = pcall(function() return Workspace:FindFirstChild("Map") end) + if ok and map and map:IsA("Instance") then + return sanitizeFilename(map.Name) + end + local ok2, wname = pcall(function() return Workspace.Name end) + if ok2 and wname then return sanitizeFilename(wname) end + return "UnknownMap" + end + + local function ensureFolder(path) + if not isfolder(path) then + makefolder(path) + end + end + + -- get configs folder for current map (simplified layout: //settings) + local function getConfigsFolder(self) + local root = self.FolderRoot + local mapName = getMapName() + return root .. "/" .. mapName .. "/settings" + end + + local function getConfigFilePath(self, name) + local folder = getConfigsFolder(self) + return folder .. "/" .. name .. ".json" + end + + -- Build folder tree and migrate legacy configs if found (copy only) + function SaveManager:BuildFolderTree() + local root = self.FolderRoot + ensureFolder(root) + + local mapName = getMapName() + local mapFolder = root .. "/" .. mapName + ensureFolder(mapFolder) + + local settingsFolder = mapFolder .. "/settings" + ensureFolder(settingsFolder) + + -- legacy folder: /settings (old layout). If files exist there, copy them into current map settings + 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 = settingsFolder .. "/" .. 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 + + -- migrate autoload.txt if present (copy only) + local autopath = legacySettingsFolder .. "/autoload.txt" + if isfile(autopath) then + local autodata = readfile(autopath) + local destAuto = settingsFolder .. "/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 + + -- ensure folder exists + 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: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" then + table.insert(out, name) + end + end + end + return out + end + + function SaveManager:LoadAutoloadConfig() + local autopath = getConfigsFolder(self) .. "/autoload.txt" + if isfile(autopath) then + local name = readfile(autopath) + local success, err = self:Load(name) + if not success then + return self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "Failed to load autoload config: " .. err, + Duration = 7 + }) + end + + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Auto loaded config %q", name), + Duration = 7 + }) + end + end + + function SaveManager:BuildConfigSection(tab) + assert(self.Library, "Must set SaveManager.Library") + + local section = tab:AddSection("Configuration") + + -- Config name (Title in English, Description in Thai) + section:AddInput("SaveManager_ConfigName", { Title = "Config name", Description = "ชื่อไฟล์สำหรับบันทึกค่าการตั้งค่า (ภาษาไทย)" }) + + -- Config list dropdown + section:AddDropdown("SaveManager_ConfigList", { Title = "Config list", Description = "รายการคอนฟิกที่มีอยู่ (เลือกเพื่อโหลด/แก้ไข)", Values = self:RefreshConfigList(), AllowNull = true }) + + -- Create config + section:AddButton({ + Title = "Create config", + Description = "สร้างไฟล์คอนฟิกจากการตั้งค่าปัจจุบัน", + Callback = function() + local name = SaveManager.Options.SaveManager_ConfigName.Value + + if name:gsub(" ", "") == "" then + return self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "ชื่อคอนฟิกไม่ถูกต้อง (เว้นว่าง)", + Duration = 7 + }) + end + + local success, err = self:Save(name) + if not success then + return self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "การบันทึกคอนฟิกล้มเหลว: " .. err, + Duration = 7 + }) + end + + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Created config %q", name), + Duration = 7 + }) + + SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) + SaveManager.Options.SaveManager_ConfigList:SetValue(nil) + end + }) + + -- Load config + section:AddButton({ + Title = "Load config", + Description = "โหลดการตั้งค่าจากไฟล์คอนฟิกที่เลือก", + Callback = function() + local name = SaveManager.Options.SaveManager_ConfigList.Value + + local success, err = self:Load(name) + if not success then + return self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "การโหลดคอนฟิกล้มเหลว: " .. err, + Duration = 7 + }) + end + + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Loaded config %q", name), + Duration = 7 + }) + end + }) + + -- Overwrite config + section:AddButton({ + Title = "Overwrite config", + Description = "บันทึกทับไฟล์คอนฟิกที่เลือกด้วยการตั้งค่าปัจจุบัน", + Callback = function() + local name = SaveManager.Options.SaveManager_ConfigList.Value + + local success, err = self:Save(name) + if not success then + return self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "การบันทึกทับล้มเหลว: " .. err, + Duration = 7 + }) + end + + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Overwrote config %q", name), + Duration = 7 + }) + end + }) + + -- Refresh list + section:AddButton({ + Title = "Refresh list", + Description = "อัพเดตรายการไฟล์คอนฟิกจากโฟลเดอร์", + Callback = function() + SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) + SaveManager.Options.SaveManager_ConfigList:SetValue(nil) + end + }) + + -- Autoload toggle (replaces previous 'Set as autoload' button) + local AutoToggle = section:AddToggle("SaveManager_AutoLoad", { Title = "Autoload", Description = "ตั้งค่าให้โหลดคอนฟิกนี้อัตโนมัติเมื่อเริ่มเกม", Default = false }) + + AutoToggle:OnChanged(function() + local name = SaveManager.Options.SaveManager_ConfigList.Value + local autopath = getConfigsFolder(self) .. "/autoload.txt" + + if SaveManager.Options.SaveManager_AutoLoad.Value then + if not name or name == "" then + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "โปรดเลือกคอนฟิกก่อนตั้งค่า Autoload", + Duration = 7 + }) + -- revert toggle + SaveManager.Options.SaveManager_AutoLoad:SetValue(false) + return + end + + pcall(writefile, autopath, name) + if SaveManager.Options.SaveManager_AutoLoad.SetDesc then + SaveManager.Options.SaveManager_AutoLoad:SetDesc("กำลังโหลดอัตโนมัติ: " .. name) + end + + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Set %q to autoload", name), + Duration = 7 + }) + else + pcall(function() if isfile(autopath) then pcall(delfile, autopath) end end) + if SaveManager.Options.SaveManager_AutoLoad.SetDesc then + SaveManager.Options.SaveManager_AutoLoad:SetDesc("ไม่มีการโหลดอัตโนมัติ") + end + + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "Autoload cleared", + Duration = 7 + }) + end + end) + + -- populate current autoload desc & initial toggle state if exists + local autop = getConfigsFolder(self) .. "/autoload.txt" + if isfile(autop) then + local name = readfile(autop) + -- set dropdown value (if exists in list) + SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) + SaveManager.Options.SaveManager_ConfigList:SetValue(name) + SaveManager.Options.SaveManager_AutoLoad:SetValue(true) + if SaveManager.Options.SaveManager_AutoLoad.SetDesc then + SaveManager.Options.SaveManager_AutoLoad:SetDesc("กำลังโหลดอัตโนมัติ: " .. name) + end + else + if SaveManager.Options.SaveManager_AutoLoad.SetDesc then + SaveManager.Options.SaveManager_AutoLoad:SetDesc("ไม่มีการโหลดอัตโนมัติ") + end + end + + SaveManager:SetIgnoreIndexes({ "SaveManager_ConfigList", "SaveManager_ConfigName", "SaveManager_AutoLoad" }) + end + + -- initial build + SaveManager:BuildFolderTree() end return SaveManager From cb25819be5b8894a8e8e56f1f5d8b877dea2955b Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 16 Oct 2025 11:26:56 +0700 Subject: [PATCH 19/76] Update autosave.lua --- autosave.lua | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/autosave.lua b/autosave.lua index fedc861..950a0bd 100644 --- a/autosave.lua +++ b/autosave.lua @@ -175,6 +175,14 @@ local SaveManager = {} do function SaveManager:SetLibrary(library) self.Library = library self.Options = library.Options + + -- Try to auto-load config for this map immediately when library is set. + -- This makes the autoload behavior persistent across sessions (if autoload.txt exists). + pcall(function() + -- LoadAutoloadConfig uses self.Library for notifications and self:Load to apply settings. + -- Wrapping in pcall prevents errors if UI options haven't been built yet. + self:LoadAutoloadConfig() + end) end function SaveManager:Save(name) From 2c3d010ececbc2a8878f7ed4b37b81edb578d72f Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 16 Oct 2025 11:37:39 +0700 Subject: [PATCH 20/76] Update autosave.lua --- autosave.lua | 304 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 182 insertions(+), 122 deletions(-) diff --git a/autosave.lua b/autosave.lua index 950a0bd..fc61158 100644 --- a/autosave.lua +++ b/autosave.lua @@ -281,136 +281,150 @@ local SaveManager = {} do end function SaveManager:BuildConfigSection(tab) - assert(self.Library, "Must set SaveManager.Library") - - local section = tab:AddSection("Configuration") - - -- Config name (Title in English, Description in Thai) - section:AddInput("SaveManager_ConfigName", { Title = "Config name", Description = "ชื่อไฟล์สำหรับบันทึกค่าการตั้งค่า (ภาษาไทย)" }) - - -- Config list dropdown - section:AddDropdown("SaveManager_ConfigList", { Title = "Config list", Description = "รายการคอนฟิกที่มีอยู่ (เลือกเพื่อโหลด/แก้ไข)", Values = self:RefreshConfigList(), AllowNull = true }) - - -- Create config - section:AddButton({ - Title = "Create config", - Description = "สร้างไฟล์คอนฟิกจากการตั้งค่าปัจจุบัน", - Callback = function() - local name = SaveManager.Options.SaveManager_ConfigName.Value - - if name:gsub(" ", "") == "" then - return self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "ชื่อคอนฟิกไม่ถูกต้อง (เว้นว่าง)", - Duration = 7 - }) - end + assert(self.Library, "Must set SaveManager.Library") - local success, err = self:Save(name) - if not success then - return self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "การบันทึกคอนฟิกล้มเหลว: " .. err, - Duration = 7 - }) - end + local section = tab:AddSection("Configuration") - self.Library:Notify({ + -- helper for autoload toggle path + local function getAutoloadTogglePath(self) + return getConfigsFolder(self) .. "/autoload_toggle.txt" + end + + local function writeAutoloadToggle(self, enabled) + local path = getAutoloadTogglePath(self) + pcall(function() writefile(path, enabled and "1" or "0") end) + end + + local function readAutoloadToggle(self) + local path = getAutoloadTogglePath(self) + if isfile(path) then + local ok, data = pcall(readfile, path) + if ok and data then + return data:match("1") ~= nil + end + end + return false + end + + -- Config name (Title in English, Description in Thai) + section:AddInput("SaveManager_ConfigName", { Title = "Config name", Description = "ชื่อไฟล์สำหรับบันทึกค่าการตั้งค่า (ภาษาไทย)" }) + + -- Config list dropdown + section:AddDropdown("SaveManager_ConfigList", { Title = "Config list", Description = "รายการคอนฟิกที่มีอยู่ (เลือกเพื่อโหลด/แก้ไข)", Values = self:RefreshConfigList(), AllowNull = true }) + + -- Create config + section:AddButton({ + Title = "Create config", + Description = "สร้างไฟล์คอนฟิกจากการตั้งค่าปัจจุบัน", + Callback = function() + local name = SaveManager.Options.SaveManager_ConfigName.Value + + if name:gsub(" ", "") == "" then + return self.Library:Notify({ Title = "Interface", Content = "Config loader", - SubContent = string.format("Created config %q", name), + SubContent = "ชื่อคอนฟิกไม่ถูกต้อง (เว้นว่าง)", Duration = 7 }) - - SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) - SaveManager.Options.SaveManager_ConfigList:SetValue(nil) end - }) - - -- Load config - section:AddButton({ - Title = "Load config", - Description = "โหลดการตั้งค่าจากไฟล์คอนฟิกที่เลือก", - Callback = function() - local name = SaveManager.Options.SaveManager_ConfigList.Value - - local success, err = self:Load(name) - if not success then - return self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "การโหลดคอนฟิกล้มเหลว: " .. err, - Duration = 7 - }) - end - self.Library:Notify({ + local success, err = self:Save(name) + if not success then + return self.Library:Notify({ Title = "Interface", Content = "Config loader", - SubContent = string.format("Loaded config %q", name), + SubContent = "การบันทึกคอนฟิกล้มเหลว: " .. err, Duration = 7 }) end - }) - - -- Overwrite config - section:AddButton({ - Title = "Overwrite config", - Description = "บันทึกทับไฟล์คอนฟิกที่เลือกด้วยการตั้งค่าปัจจุบัน", - Callback = function() - local name = SaveManager.Options.SaveManager_ConfigList.Value - - local success, err = self:Save(name) - if not success then - return self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "การบันทึกทับล้มเหลว: " .. err, - Duration = 7 - }) - end - self.Library:Notify({ + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Created config %q", name), + Duration = 7 + }) + + SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) + SaveManager.Options.SaveManager_ConfigList:SetValue(nil) + end + }) + + -- Load config + section:AddButton({ + Title = "Load config", + Description = "โหลดการตั้งค่าจากไฟล์คอนฟิกที่เลือก", + Callback = function() + local name = SaveManager.Options.SaveManager_ConfigList.Value + + local success, err = self:Load(name) + if not success then + return self.Library:Notify({ Title = "Interface", Content = "Config loader", - SubContent = string.format("Overwrote config %q", name), + SubContent = "การโหลดคอนฟิกล้มเหลว: " .. err, Duration = 7 }) end - }) - - -- Refresh list - section:AddButton({ - Title = "Refresh list", - Description = "อัพเดตรายการไฟล์คอนฟิกจากโฟลเดอร์", - Callback = function() - SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) - SaveManager.Options.SaveManager_ConfigList:SetValue(nil) - end - }) - -- Autoload toggle (replaces previous 'Set as autoload' button) - local AutoToggle = section:AddToggle("SaveManager_AutoLoad", { Title = "Autoload", Description = "ตั้งค่าให้โหลดคอนฟิกนี้อัตโนมัติเมื่อเริ่มเกม", Default = false }) + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Loaded config %q", name), + Duration = 7 + }) + end + }) - AutoToggle:OnChanged(function() + -- Overwrite config + section:AddButton({ + Title = "Overwrite config", + Description = "บันทึกทับไฟล์คอนฟิกที่เลือกด้วยการตั้งค่าปัจจุบัน", + Callback = function() local name = SaveManager.Options.SaveManager_ConfigList.Value - local autopath = getConfigsFolder(self) .. "/autoload.txt" - - if SaveManager.Options.SaveManager_AutoLoad.Value then - if not name or name == "" then - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "โปรดเลือกคอนฟิกก่อนตั้งค่า Autoload", - Duration = 7 - }) - -- revert toggle - SaveManager.Options.SaveManager_AutoLoad:SetValue(false) - return - end + local success, err = self:Save(name) + if not success then + return self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "การบันทึกทับล้มเหลว: " .. err, + Duration = 7 + }) + end + + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Overwrote config %q", name), + Duration = 7 + }) + end + }) + + -- Refresh list + section:AddButton({ + Title = "Refresh list", + Description = "อัพเดตรายการไฟล์คอนฟิกจากโฟลเดอร์", + Callback = function() + SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) + SaveManager.Options.SaveManager_ConfigList:SetValue(nil) + end + }) + + -- Autoload toggle (replaces previous 'Set as autoload' button) + local AutoToggle = section:AddToggle("SaveManager_AutoLoad", { Title = "Autoload", Description = "ตั้งค่าให้โหลดคอนฟิกนี้อัตโนมัติเมื่อเริ่มเกม", Default = false }) + + AutoToggle:OnChanged(function() + local name = SaveManager.Options.SaveManager_ConfigList.Value + local autopath = getConfigsFolder(self) .. "/autoload.txt" + + -- Persist the toggle state separately so the toggle remains between sessions + writeAutoloadToggle(self, SaveManager.Options.SaveManager_AutoLoad.Value) + + if SaveManager.Options.SaveManager_AutoLoad.Value then + -- If a config is selected, set it as the autoload target + if name and name ~= "" then pcall(writefile, autopath, name) if SaveManager.Options.SaveManager_AutoLoad.SetDesc then SaveManager.Options.SaveManager_AutoLoad:SetDesc("กำลังโหลดอัตโนมัติ: " .. name) @@ -423,40 +437,86 @@ local SaveManager = {} do Duration = 7 }) else - pcall(function() if isfile(autopath) then pcall(delfile, autopath) end end) + -- No config selected, keep toggle on but inform user to select one. if SaveManager.Options.SaveManager_AutoLoad.SetDesc then - SaveManager.Options.SaveManager_AutoLoad:SetDesc("ไม่มีการโหลดอัตโนมัติ") + SaveManager.Options.SaveManager_AutoLoad:SetDesc("ตั้งค่าเป็น Autoload (ยังไม่ได้เลือกคอนฟิก)") end self.Library:Notify({ Title = "Interface", Content = "Config loader", - SubContent = "Autoload cleared", + SubContent = "Autoload เปิดอยู่ แต่ยังไม่ได้เลือกคอนฟิกล — กรุณาเลือกคอนฟิกเพื่อกำหนดเป้าหมาย", Duration = 7 }) end - end) + else + -- Turn off autoload: remove autoload.txt but keep toggle persisted as off + pcall(function() if isfile(autopath) then pcall(delfile, autopath) end end) + if SaveManager.Options.SaveManager_AutoLoad.SetDesc then + SaveManager.Options.SaveManager_AutoLoad:SetDesc("ไม่มีการโหลดอัตโนมัติ") + end - -- populate current autoload desc & initial toggle state if exists - local autop = getConfigsFolder(self) .. "/autoload.txt" - if isfile(autop) then - local name = readfile(autop) - -- set dropdown value (if exists in list) - SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) - SaveManager.Options.SaveManager_ConfigList:SetValue(name) - SaveManager.Options.SaveManager_AutoLoad:SetValue(true) + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "Autoload cleared", + Duration = 7 + }) + end + end) + + -- If the user changes the selected config while Autoload toggle is ON, update autoload target automatically + local cfgDropdown = SaveManager.Options.SaveManager_ConfigList + pcall(function() + if cfgDropdown and cfgDropdown.OnChanged then + cfgDropdown:OnChanged(function() + local selected = SaveManager.Options.SaveManager_ConfigList.Value + local autopath = getConfigsFolder(self) .. "/autoload.txt" + if SaveManager.Options.SaveManager_AutoLoad and SaveManager.Options.SaveManager_AutoLoad.Value then + if selected and selected ~= "" then + pcall(writefile, autopath, selected) + if SaveManager.Options.SaveManager_AutoLoad.SetDesc then + SaveManager.Options.SaveManager_AutoLoad:SetDesc("กำลังโหลดอัตโนมัติ: " .. selected) + end + self.Library:Notify({ Title = "Interface", Content = "Config loader", SubContent = string.format("Updated autoload to %q", selected), Duration = 5 }) + end + end + end) + end + end) + + -- populate current autoload desc & initial toggle state if exists + local autop = getConfigsFolder(self) .. "/autoload.txt" + -- Refresh the dropdown values to ensure selection works + SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) + + local toggleState = readAutoloadToggle(self) + if isfile(autop) then + local name = readfile(autop) + SaveManager.Options.SaveManager_ConfigList:SetValue(name) + -- If autoload.txt exists we consider that autoload target is set; set toggleState true for UI convenience + if not toggleState then toggleState = true end + SaveManager.Options.SaveManager_AutoLoad:SetValue(true) + if SaveManager.Options.SaveManager_AutoLoad.SetDesc then + SaveManager.Options.SaveManager_AutoLoad:SetDesc("กำลังโหลดอัตโนมัติ: " .. name) + end + else + -- No autoload target file. Respect persisted toggle state (autoload_toggle.txt) even if no autoload target exists. + SaveManager.Options.SaveManager_AutoLoad:SetValue(toggleState) + if toggleState then if SaveManager.Options.SaveManager_AutoLoad.SetDesc then - SaveManager.Options.SaveManager_AutoLoad:SetDesc("กำลังโหลดอัตโนมัติ: " .. name) + SaveManager.Options.SaveManager_AutoLoad:SetDesc("ตั้งค่าเป็น Autoload (ยังไม่ได้เลือกคอนฟิก)") end else if SaveManager.Options.SaveManager_AutoLoad.SetDesc then SaveManager.Options.SaveManager_AutoLoad:SetDesc("ไม่มีการโหลดอัตโนมัติ") end end - - SaveManager:SetIgnoreIndexes({ "SaveManager_ConfigList", "SaveManager_ConfigName", "SaveManager_AutoLoad" }) end + SaveManager:SetIgnoreIndexes({ "SaveManager_ConfigList", "SaveManager_ConfigName", "SaveManager_AutoLoad" }) +end + -- initial build SaveManager:BuildFolderTree() end From d86286708c2bb5dce25a0d199df017e4c94b6c36 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 16 Oct 2025 11:45:27 +0700 Subject: [PATCH 21/76] Update autosave.lua --- autosave.lua | 312 ++++++++++++++++++++------------------------------- 1 file changed, 122 insertions(+), 190 deletions(-) diff --git a/autosave.lua b/autosave.lua index fc61158..fedc861 100644 --- a/autosave.lua +++ b/autosave.lua @@ -175,14 +175,6 @@ local SaveManager = {} do function SaveManager:SetLibrary(library) self.Library = library self.Options = library.Options - - -- Try to auto-load config for this map immediately when library is set. - -- This makes the autoload behavior persistent across sessions (if autoload.txt exists). - pcall(function() - -- LoadAutoloadConfig uses self.Library for notifications and self:Load to apply settings. - -- Wrapping in pcall prevents errors if UI options haven't been built yet. - self:LoadAutoloadConfig() - end) end function SaveManager:Save(name) @@ -281,150 +273,136 @@ local SaveManager = {} do end function SaveManager:BuildConfigSection(tab) - assert(self.Library, "Must set SaveManager.Library") - - local section = tab:AddSection("Configuration") - - -- helper for autoload toggle path - local function getAutoloadTogglePath(self) - return getConfigsFolder(self) .. "/autoload_toggle.txt" - end - - local function writeAutoloadToggle(self, enabled) - local path = getAutoloadTogglePath(self) - pcall(function() writefile(path, enabled and "1" or "0") end) - end - - local function readAutoloadToggle(self) - local path = getAutoloadTogglePath(self) - if isfile(path) then - local ok, data = pcall(readfile, path) - if ok and data then - return data:match("1") ~= nil - end - end - return false - end - - -- Config name (Title in English, Description in Thai) - section:AddInput("SaveManager_ConfigName", { Title = "Config name", Description = "ชื่อไฟล์สำหรับบันทึกค่าการตั้งค่า (ภาษาไทย)" }) - - -- Config list dropdown - section:AddDropdown("SaveManager_ConfigList", { Title = "Config list", Description = "รายการคอนฟิกที่มีอยู่ (เลือกเพื่อโหลด/แก้ไข)", Values = self:RefreshConfigList(), AllowNull = true }) + assert(self.Library, "Must set SaveManager.Library") + + local section = tab:AddSection("Configuration") + + -- Config name (Title in English, Description in Thai) + section:AddInput("SaveManager_ConfigName", { Title = "Config name", Description = "ชื่อไฟล์สำหรับบันทึกค่าการตั้งค่า (ภาษาไทย)" }) + + -- Config list dropdown + section:AddDropdown("SaveManager_ConfigList", { Title = "Config list", Description = "รายการคอนฟิกที่มีอยู่ (เลือกเพื่อโหลด/แก้ไข)", Values = self:RefreshConfigList(), AllowNull = true }) + + -- Create config + section:AddButton({ + Title = "Create config", + Description = "สร้างไฟล์คอนฟิกจากการตั้งค่าปัจจุบัน", + Callback = function() + local name = SaveManager.Options.SaveManager_ConfigName.Value + + if name:gsub(" ", "") == "" then + return self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "ชื่อคอนฟิกไม่ถูกต้อง (เว้นว่าง)", + Duration = 7 + }) + end - -- Create config - section:AddButton({ - Title = "Create config", - Description = "สร้างไฟล์คอนฟิกจากการตั้งค่าปัจจุบัน", - Callback = function() - local name = SaveManager.Options.SaveManager_ConfigName.Value + local success, err = self:Save(name) + if not success then + return self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "การบันทึกคอนฟิกล้มเหลว: " .. err, + Duration = 7 + }) + end - if name:gsub(" ", "") == "" then - return self.Library:Notify({ + self.Library:Notify({ Title = "Interface", Content = "Config loader", - SubContent = "ชื่อคอนฟิกไม่ถูกต้อง (เว้นว่าง)", + SubContent = string.format("Created config %q", name), Duration = 7 }) - end - local success, err = self:Save(name) - if not success then - return self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "การบันทึกคอนฟิกล้มเหลว: " .. err, - Duration = 7 - }) + SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) + SaveManager.Options.SaveManager_ConfigList:SetValue(nil) end + }) + + -- Load config + section:AddButton({ + Title = "Load config", + Description = "โหลดการตั้งค่าจากไฟล์คอนฟิกที่เลือก", + Callback = function() + local name = SaveManager.Options.SaveManager_ConfigList.Value + + local success, err = self:Load(name) + if not success then + return self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "การโหลดคอนฟิกล้มเหลว: " .. err, + Duration = 7 + }) + end - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Created config %q", name), - Duration = 7 - }) - - SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) - SaveManager.Options.SaveManager_ConfigList:SetValue(nil) - end - }) - - -- Load config - section:AddButton({ - Title = "Load config", - Description = "โหลดการตั้งค่าจากไฟล์คอนฟิกที่เลือก", - Callback = function() - local name = SaveManager.Options.SaveManager_ConfigList.Value - - local success, err = self:Load(name) - if not success then - return self.Library:Notify({ + self.Library:Notify({ Title = "Interface", Content = "Config loader", - SubContent = "การโหลดคอนฟิกล้มเหลว: " .. err, + SubContent = string.format("Loaded config %q", name), Duration = 7 }) end + }) + + -- Overwrite config + section:AddButton({ + Title = "Overwrite config", + Description = "บันทึกทับไฟล์คอนฟิกที่เลือกด้วยการตั้งค่าปัจจุบัน", + Callback = function() + local name = SaveManager.Options.SaveManager_ConfigList.Value + + local success, err = self:Save(name) + if not success then + return self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "การบันทึกทับล้มเหลว: " .. err, + Duration = 7 + }) + end - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Loaded config %q", name), - Duration = 7 - }) - end - }) - - -- Overwrite config - section:AddButton({ - Title = "Overwrite config", - Description = "บันทึกทับไฟล์คอนฟิกที่เลือกด้วยการตั้งค่าปัจจุบัน", - Callback = function() - local name = SaveManager.Options.SaveManager_ConfigList.Value - - local success, err = self:Save(name) - if not success then - return self.Library:Notify({ + self.Library:Notify({ Title = "Interface", Content = "Config loader", - SubContent = "การบันทึกทับล้มเหลว: " .. err, + SubContent = string.format("Overwrote config %q", name), Duration = 7 }) end + }) + + -- Refresh list + section:AddButton({ + Title = "Refresh list", + Description = "อัพเดตรายการไฟล์คอนฟิกจากโฟลเดอร์", + Callback = function() + SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) + SaveManager.Options.SaveManager_ConfigList:SetValue(nil) + end + }) - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Overwrote config %q", name), - Duration = 7 - }) - end - }) - - -- Refresh list - section:AddButton({ - Title = "Refresh list", - Description = "อัพเดตรายการไฟล์คอนฟิกจากโฟลเดอร์", - Callback = function() - SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) - SaveManager.Options.SaveManager_ConfigList:SetValue(nil) - end - }) - - -- Autoload toggle (replaces previous 'Set as autoload' button) - local AutoToggle = section:AddToggle("SaveManager_AutoLoad", { Title = "Autoload", Description = "ตั้งค่าให้โหลดคอนฟิกนี้อัตโนมัติเมื่อเริ่มเกม", Default = false }) - - AutoToggle:OnChanged(function() - local name = SaveManager.Options.SaveManager_ConfigList.Value - local autopath = getConfigsFolder(self) .. "/autoload.txt" + -- Autoload toggle (replaces previous 'Set as autoload' button) + local AutoToggle = section:AddToggle("SaveManager_AutoLoad", { Title = "Autoload", Description = "ตั้งค่าให้โหลดคอนฟิกนี้อัตโนมัติเมื่อเริ่มเกม", Default = false }) - -- Persist the toggle state separately so the toggle remains between sessions - writeAutoloadToggle(self, SaveManager.Options.SaveManager_AutoLoad.Value) + AutoToggle:OnChanged(function() + local name = SaveManager.Options.SaveManager_ConfigList.Value + local autopath = getConfigsFolder(self) .. "/autoload.txt" + + if SaveManager.Options.SaveManager_AutoLoad.Value then + if not name or name == "" then + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "โปรดเลือกคอนฟิกก่อนตั้งค่า Autoload", + Duration = 7 + }) + -- revert toggle + SaveManager.Options.SaveManager_AutoLoad:SetValue(false) + return + end - if SaveManager.Options.SaveManager_AutoLoad.Value then - -- If a config is selected, set it as the autoload target - if name and name ~= "" then pcall(writefile, autopath, name) if SaveManager.Options.SaveManager_AutoLoad.SetDesc then SaveManager.Options.SaveManager_AutoLoad:SetDesc("กำลังโหลดอัตโนมัติ: " .. name) @@ -437,85 +415,39 @@ local SaveManager = {} do Duration = 7 }) else - -- No config selected, keep toggle on but inform user to select one. + pcall(function() if isfile(autopath) then pcall(delfile, autopath) end end) if SaveManager.Options.SaveManager_AutoLoad.SetDesc then - SaveManager.Options.SaveManager_AutoLoad:SetDesc("ตั้งค่าเป็น Autoload (ยังไม่ได้เลือกคอนฟิก)") + SaveManager.Options.SaveManager_AutoLoad:SetDesc("ไม่มีการโหลดอัตโนมัติ") end self.Library:Notify({ Title = "Interface", Content = "Config loader", - SubContent = "Autoload เปิดอยู่ แต่ยังไม่ได้เลือกคอนฟิกล — กรุณาเลือกคอนฟิกเพื่อกำหนดเป้าหมาย", + SubContent = "Autoload cleared", Duration = 7 }) end - else - -- Turn off autoload: remove autoload.txt but keep toggle persisted as off - pcall(function() if isfile(autopath) then pcall(delfile, autopath) end end) - if SaveManager.Options.SaveManager_AutoLoad.SetDesc then - SaveManager.Options.SaveManager_AutoLoad:SetDesc("ไม่มีการโหลดอัตโนมัติ") - end + end) - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "Autoload cleared", - Duration = 7 - }) - end - end) - - -- If the user changes the selected config while Autoload toggle is ON, update autoload target automatically - local cfgDropdown = SaveManager.Options.SaveManager_ConfigList - pcall(function() - if cfgDropdown and cfgDropdown.OnChanged then - cfgDropdown:OnChanged(function() - local selected = SaveManager.Options.SaveManager_ConfigList.Value - local autopath = getConfigsFolder(self) .. "/autoload.txt" - if SaveManager.Options.SaveManager_AutoLoad and SaveManager.Options.SaveManager_AutoLoad.Value then - if selected and selected ~= "" then - pcall(writefile, autopath, selected) - if SaveManager.Options.SaveManager_AutoLoad.SetDesc then - SaveManager.Options.SaveManager_AutoLoad:SetDesc("กำลังโหลดอัตโนมัติ: " .. selected) - end - self.Library:Notify({ Title = "Interface", Content = "Config loader", SubContent = string.format("Updated autoload to %q", selected), Duration = 5 }) - end - end - end) - end - end) - - -- populate current autoload desc & initial toggle state if exists - local autop = getConfigsFolder(self) .. "/autoload.txt" - -- Refresh the dropdown values to ensure selection works - SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) - - local toggleState = readAutoloadToggle(self) - if isfile(autop) then - local name = readfile(autop) - SaveManager.Options.SaveManager_ConfigList:SetValue(name) - -- If autoload.txt exists we consider that autoload target is set; set toggleState true for UI convenience - if not toggleState then toggleState = true end - SaveManager.Options.SaveManager_AutoLoad:SetValue(true) - if SaveManager.Options.SaveManager_AutoLoad.SetDesc then - SaveManager.Options.SaveManager_AutoLoad:SetDesc("กำลังโหลดอัตโนมัติ: " .. name) - end - else - -- No autoload target file. Respect persisted toggle state (autoload_toggle.txt) even if no autoload target exists. - SaveManager.Options.SaveManager_AutoLoad:SetValue(toggleState) - if toggleState then + -- populate current autoload desc & initial toggle state if exists + local autop = getConfigsFolder(self) .. "/autoload.txt" + if isfile(autop) then + local name = readfile(autop) + -- set dropdown value (if exists in list) + SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) + SaveManager.Options.SaveManager_ConfigList:SetValue(name) + SaveManager.Options.SaveManager_AutoLoad:SetValue(true) if SaveManager.Options.SaveManager_AutoLoad.SetDesc then - SaveManager.Options.SaveManager_AutoLoad:SetDesc("ตั้งค่าเป็น Autoload (ยังไม่ได้เลือกคอนฟิก)") + SaveManager.Options.SaveManager_AutoLoad:SetDesc("กำลังโหลดอัตโนมัติ: " .. name) end else if SaveManager.Options.SaveManager_AutoLoad.SetDesc then SaveManager.Options.SaveManager_AutoLoad:SetDesc("ไม่มีการโหลดอัตโนมัติ") end end - end - SaveManager:SetIgnoreIndexes({ "SaveManager_ConfigList", "SaveManager_ConfigName", "SaveManager_AutoLoad" }) -end + SaveManager:SetIgnoreIndexes({ "SaveManager_ConfigList", "SaveManager_ConfigName", "SaveManager_AutoLoad" }) + end -- initial build SaveManager:BuildFolderTree() From fa6e4c32bb11d5fa6c4231389542053792be72cf Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 16 Oct 2025 11:50:32 +0700 Subject: [PATCH 22/76] Update autosave.lua --- autosave.lua | 144 ++++++++++++++++++++++++++++++++------------------- 1 file changed, 90 insertions(+), 54 deletions(-) diff --git a/autosave.lua b/autosave.lua index fedc861..2d3d678 100644 --- a/autosave.lua +++ b/autosave.lua @@ -383,72 +383,108 @@ local SaveManager = {} do end }) - -- Autoload toggle (replaces previous 'Set as autoload' button) - local AutoToggle = section:AddToggle("SaveManager_AutoLoad", { Title = "Autoload", Description = "ตั้งค่าให้โหลดคอนฟิกนี้อัตโนมัติเมื่อเริ่มเกม", Default = false }) + -- สมมติว่ามีฟังก์ชัน getConfigsFolder(self) และ SaveManager, self.Library, ฯลฯ ตามโค้ดคุณ +local AutoToggle = section:AddToggle("SaveManager_AutoLoad", { + Title = "Autoload", + Description = "ตั้งค่าให้โหลดคอนฟิกนี้อัตโนมัติเมื่อเริ่มเกม", + Default = false +}) + +-- guard flag เพื่อไม่ให้ OnChanged ทำงานขณะเรากำลังตั้งค่าเริ่มต้น +local initializing = true + +-- helper paths +local autopath = getConfigsFolder(self) .. "/autoload.txt" + +-- ฟังก์ชันช่วย: ตั้งคำอธิบายของ toggle แบบปลอดภัย (เช็คว่ามีเมธอด SetDesc หรือไม่) +local function setToggleDesc(val) + if AutoToggle.SetDesc then + AutoToggle:SetDesc(val) + end +end - AutoToggle:OnChanged(function() - local name = SaveManager.Options.SaveManager_ConfigList.Value - local autopath = getConfigsFolder(self) .. "/autoload.txt" +-- OnChanged callback (จะไม่ทำงานในช่วง initializing) +AutoToggle:OnChanged(function() + if initializing then return end -- ถ้าเรากำลังตั้งค่าเริ่มต้น ให้ข้าม + local name = SaveManager.Options.SaveManager_ConfigList.Value - if SaveManager.Options.SaveManager_AutoLoad.Value then - if not name or name == "" then - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "โปรดเลือกคอนฟิกก่อนตั้งค่า Autoload", - Duration = 7 - }) - -- revert toggle - SaveManager.Options.SaveManager_AutoLoad:SetValue(false) - return - end + if SaveManager.Options.SaveManager_AutoLoad.Value then + -- ถ้าเปิด แต่ไม่มีคอนฟิกที่เลือก => แจ้งเตือนแล้ว revert + if not name or name == "" then + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "โปรดเลือกคอนฟิกก่อนตั้งค่า Autoload", + Duration = 7 + }) + -- revert toggle (ใช้ SetValue เพื่อให้ UI อัปเดต) + SaveManager.Options.SaveManager_AutoLoad:SetValue(false) + return + end - pcall(writefile, autopath, name) - if SaveManager.Options.SaveManager_AutoLoad.SetDesc then - SaveManager.Options.SaveManager_AutoLoad:SetDesc("กำลังโหลดอัตโนมัติ: " .. name) - end + -- เขียนไฟล์อย่างปลอดภัย + pcall(function() writefile(autopath, name) end) + setToggleDesc("กำลังโหลดอัตโนมัติ: " .. name) - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Set %q to autoload", name), - Duration = 7 - }) - else - pcall(function() if isfile(autopath) then pcall(delfile, autopath) end end) - if SaveManager.Options.SaveManager_AutoLoad.SetDesc then - SaveManager.Options.SaveManager_AutoLoad:SetDesc("ไม่มีการโหลดอัตโนมัติ") - end - - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "Autoload cleared", - Duration = 7 - }) + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Set %q to autoload", name), + Duration = 7 + }) + else + -- ปิด autoload -> ลบไฟล์ถ้ามี + pcall(function() + if isfile(autopath) then + pcall(delfile, autopath) end end) + setToggleDesc("ไม่มีการโหลดอัตโนมัติ") - -- populate current autoload desc & initial toggle state if exists - local autop = getConfigsFolder(self) .. "/autoload.txt" - if isfile(autop) then - local name = readfile(autop) - -- set dropdown value (if exists in list) - SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) - SaveManager.Options.SaveManager_ConfigList:SetValue(name) - SaveManager.Options.SaveManager_AutoLoad:SetValue(true) - if SaveManager.Options.SaveManager_AutoLoad.SetDesc then - SaveManager.Options.SaveManager_AutoLoad:SetDesc("กำลังโหลดอัตโนมัติ: " .. name) + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "Autoload cleared", + Duration = 7 + }) + end +end) + +-- === ตอนเริ่มต้นของสคริปต์: อ่านไฟล์ก่อน แล้วตั้งค่า dropdown + toggle แบบเงียบ ๆ === +do + -- รีเฟรชรายการคอนฟิกก่อน เพื่อให้ dropdown มีค่า + if SaveManager.Options and SaveManager.Options.SaveManager_ConfigList and SaveManager.Options.SaveManager_ConfigList.SetValues then + SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) + end + + if isfile(autopath) then + local ok, name = pcall(readfile, autopath) + if ok and name and name ~= "" then + -- ตั้ง dropdown ให้เป็นคอนฟิกที่บันทึกไว้ (ถ้ามีใน list) + if SaveManager.Options.SaveManager_ConfigList.SetValue then + SaveManager.Options.SaveManager_ConfigList:SetValue(name) end - else - if SaveManager.Options.SaveManager_AutoLoad.SetDesc then - SaveManager.Options.SaveManager_AutoLoad:SetDesc("ไม่มีการโหลดอัตโนมัติ") + -- ตั้ง toggle แต่ไม่ให้ callback ทำงานเพราะ initializing = true + if SaveManager.Options.SaveManager_AutoLoad.SetValue then + SaveManager.Options.SaveManager_AutoLoad:SetValue(true) end + setToggleDesc("กำลังโหลดอัตโนมัติ: " .. name) + else + -- ถ้าอ่านไฟล์ไม่ได้ ให้ลบไฟล์ทิ้ง (optional) + pcall(delfile, autopath) + setToggleDesc("ไม่มีการโหลดอัตโนมัติ") end - - SaveManager:SetIgnoreIndexes({ "SaveManager_ConfigList", "SaveManager_ConfigName", "SaveManager_AutoLoad" }) + else + setToggleDesc("ไม่มีการโหลดอัตโนมัติ") end + -- เสร็จขั้นตอน initialization -> ปลด guard เพื่อให้ callback ทำงานตามปกติ + initializing = false +end + +-- หากต้องการ อย่าลืมเพิ่ม index ให้ SaveManager ไม่เซฟ toggle เป็นส่วนหนึ่งของ config ปกติ +SaveManager:SetIgnoreIndexes({ "SaveManager_ConfigList", "SaveManager_ConfigName", "SaveManager_AutoLoad" }) + -- initial build SaveManager:BuildFolderTree() end From 3cd4777dfe1f7a52286ab83e070b16bef99c0440 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 16 Oct 2025 11:54:30 +0700 Subject: [PATCH 23/76] Update autosave.lua --- autosave.lua | 178 ++++++++++++++++++++++++++------------------------- 1 file changed, 91 insertions(+), 87 deletions(-) diff --git a/autosave.lua b/autosave.lua index 2d3d678..29a97a3 100644 --- a/autosave.lua +++ b/autosave.lua @@ -383,107 +383,111 @@ local SaveManager = {} do end }) - -- สมมติว่ามีฟังก์ชัน getConfigsFolder(self) และ SaveManager, self.Library, ฯลฯ ตามโค้ดคุณ -local AutoToggle = section:AddToggle("SaveManager_AutoLoad", { - Title = "Autoload", - Description = "ตั้งค่าให้โหลดคอนฟิกนี้อัตโนมัติเมื่อเริ่มเกม", - Default = false -}) - --- guard flag เพื่อไม่ให้ OnChanged ทำงานขณะเรากำลังตั้งค่าเริ่มต้น -local initializing = true - --- helper paths -local autopath = getConfigsFolder(self) .. "/autoload.txt" - --- ฟังก์ชันช่วย: ตั้งคำอธิบายของ toggle แบบปลอดภัย (เช็คว่ามีเมธอด SetDesc หรือไม่) -local function setToggleDesc(val) - if AutoToggle.SetDesc then - AutoToggle:SetDesc(val) - end -end + -- Autoload toggle (replaces previous 'Set as autoload' button) + local AutoToggle = section:AddToggle("SaveManager_AutoLoad", { Title = "Autoload", Description = "ตั้งค่าให้โหลดคอนฟิกนี้อัตโนมัติเมื่อเริ่มเกม", Default = false }) --- OnChanged callback (จะไม่ทำงานในช่วง initializing) -AutoToggle:OnChanged(function() - if initializing then return end -- ถ้าเรากำลังตั้งค่าเริ่มต้น ให้ข้าม - local name = SaveManager.Options.SaveManager_ConfigList.Value + -- guard flag เพื่อไม่ให้ handler ทำงานขณะกำลัง init UI programmatically + local initializing_autoload = true - if SaveManager.Options.SaveManager_AutoLoad.Value then - -- ถ้าเปิด แต่ไม่มีคอนฟิกที่เลือก => แจ้งเตือนแล้ว revert - if not name or name == "" then - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "โปรดเลือกคอนฟิกก่อนตั้งค่า Autoload", - Duration = 7 - }) - -- revert toggle (ใช้ SetValue เพื่อให้ UI อัปเดต) - SaveManager.Options.SaveManager_AutoLoad:SetValue(false) - return - end + AutoToggle:OnChanged(function() + -- ถ้าเรากำลังตั้งค่าเริ่มต้น ให้ข้าม handler นี้ + if initializing_autoload then return end - -- เขียนไฟล์อย่างปลอดภัย - pcall(function() writefile(autopath, name) end) - setToggleDesc("กำลังโหลดอัตโนมัติ: " .. name) + local name = SaveManager.Options.SaveManager_ConfigList.Value + local autopath = getConfigsFolder(self) .. "/autoload.txt" - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Set %q to autoload", name), - Duration = 7 - }) - else - -- ปิด autoload -> ลบไฟล์ถ้ามี - pcall(function() - if isfile(autopath) then - pcall(delfile, autopath) + if SaveManager.Options.SaveManager_AutoLoad.Value then + -- user เปิด toggle + if not name or name == "" then + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "โปรดเลือกคอนฟิกก่อนตั้งค่า Autoload", + Duration = 7 + }) + -- revert toggle แต่ต้องทำแบบไม่ให้ trigger handler อีก => ใช้ flag + initializing_autoload = true + SaveManager.Options.SaveManager_AutoLoad:SetValue(false) + initializing_autoload = false + return + end + + -- เขียนไฟล์ autoload + pcall(writefile, autopath, name) + if SaveManager.Options.SaveManager_AutoLoad.SetDesc then + SaveManager.Options.SaveManager_AutoLoad:SetDesc("กำลังโหลดอัตโนมัติ: " .. name) + end + + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Set %q to autoload", name), + Duration = 7 + }) + else + -- user ปิด toggle -> ลบไฟล์ autoload + pcall(function() if isfile(autopath) then pcall(delfile, autopath) end end) + if SaveManager.Options.SaveManager_AutoLoad.SetDesc then + SaveManager.Options.SaveManager_AutoLoad:SetDesc("ไม่มีการโหลดอัตโนมัติ") + end + + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "Autoload cleared", + Duration = 7 + }) end end) - setToggleDesc("ไม่มีการโหลดอัตโนมัติ") - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "Autoload cleared", - Duration = 7 - }) - end -end) - --- === ตอนเริ่มต้นของสคริปต์: อ่านไฟล์ก่อน แล้วตั้งค่า dropdown + toggle แบบเงียบ ๆ === -do - -- รีเฟรชรายการคอนฟิกก่อน เพื่อให้ dropdown มีค่า - if SaveManager.Options and SaveManager.Options.SaveManager_ConfigList and SaveManager.Options.SaveManager_ConfigList.SetValues then - SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) - end + -- populate current autoload desc & initial toggle state if exists + local autop = getConfigsFolder(self) .. "/autoload.txt" + if isfile(autop) then + local name = readfile(autop) + + -- รีเฟรชรายการคอนฟิกก่อน + local values = self:RefreshConfigList() + SaveManager.Options.SaveManager_ConfigList:SetValues(values) + + -- ตรวจว่าไฟล์ autoload อ้างถึงคอนฟิกที่มีจริงหรือไม่ + local found = false + for _, v in ipairs(values) do + if v == name then + found = true + break + end + end - if isfile(autopath) then - local ok, name = pcall(readfile, autopath) - if ok and name and name ~= "" then - -- ตั้ง dropdown ให้เป็นคอนฟิกที่บันทึกไว้ (ถ้ามีใน list) - if SaveManager.Options.SaveManager_ConfigList.SetValue then + if found then + -- ตั้ง dropdown และ toggle โดยปิด handler ชั่วคราว SaveManager.Options.SaveManager_ConfigList:SetValue(name) - end - -- ตั้ง toggle แต่ไม่ให้ callback ทำงานเพราะ initializing = true - if SaveManager.Options.SaveManager_AutoLoad.SetValue then + initializing_autoload = true SaveManager.Options.SaveManager_AutoLoad:SetValue(true) + initializing_autoload = false + + if SaveManager.Options.SaveManager_AutoLoad.SetDesc then + SaveManager.Options.SaveManager_AutoLoad:SetDesc("กำลังโหลดอัตโนมัติ: " .. name) + end + else + -- ถ้าไม่เจอคอนฟิก ให้แจ้งผู้ใช้แต่ **อย่า** ลบ autoload.txt โดยอัตโนมัติ + if SaveManager.Options.SaveManager_AutoLoad.SetDesc then + SaveManager.Options.SaveManager_AutoLoad:SetDesc("ไฟล์ autoload ชี้ไปที่คอนฟิกที่ไม่มีอยู่: " .. name) + end + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "Autoload file references missing config: " .. tostring(name), + Duration = 7 + }) end - setToggleDesc("กำลังโหลดอัตโนมัติ: " .. name) else - -- ถ้าอ่านไฟล์ไม่ได้ ให้ลบไฟล์ทิ้ง (optional) - pcall(delfile, autopath) - setToggleDesc("ไม่มีการโหลดอัตโนมัติ") + if SaveManager.Options.SaveManager_AutoLoad.SetDesc then + SaveManager.Options.SaveManager_AutoLoad:SetDesc("ไม่มีการโหลดอัตโนมัติ") + end end - else - setToggleDesc("ไม่มีการโหลดอัตโนมัติ") - end - -- เสร็จขั้นตอน initialization -> ปลด guard เพื่อให้ callback ทำงานตามปกติ - initializing = false -end - --- หากต้องการ อย่าลืมเพิ่ม index ให้ SaveManager ไม่เซฟ toggle เป็นส่วนหนึ่งของ config ปกติ -SaveManager:SetIgnoreIndexes({ "SaveManager_ConfigList", "SaveManager_ConfigName", "SaveManager_AutoLoad" }) + SaveManager:SetIgnoreIndexes({ "SaveManager_ConfigList", "SaveManager_ConfigName", "SaveManager_AutoLoad" }) + end -- initial build SaveManager:BuildFolderTree() From 83019a74aec074bbfef67a56aa26b9c9a8ce24a4 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 16 Oct 2025 11:57:54 +0700 Subject: [PATCH 24/76] Update autosave.lua --- autosave.lua | 98 ++++++++++++++++++++++------------------------------ 1 file changed, 41 insertions(+), 57 deletions(-) diff --git a/autosave.lua b/autosave.lua index 29a97a3..e72332a 100644 --- a/autosave.lua +++ b/autosave.lua @@ -1,14 +1,3 @@ --- SaveManager.lua --- ปรับปรุงโดย: ChatGPT (อัปเดตเพื่อใช้ชื่อแมพ, ลดการสร้างโฟลเดอร์, เปลี่ยนปุ่ม autoload เป็น Toggle, --- เพิ่ม Title (อังกฤษ) และ Description (ไทย) ให้กับ UI ทุกตัวในส่วน Configuration) - --- ตัวอย่างการใช้งาน (ตัวอย่างนี้เป็นคอมเมนต์): --- local Toggle = Tabs.Main:AddToggle("MyToggle", { Title = "Example Toggle", Description = "ทดสอบสวิตช์ (เปิด/ปิด) -- คำอธิบายภาษาไทย", Default = false }) --- Toggle:OnChanged(function() --- print("Toggle changed:", Options.MyToggle.Value) --- end) --- SaveManager จะจัดการไฟล์คอนฟิกให้ในโฟลเดอร์: //settings - local httpService = game:GetService("HttpService") local Workspace = game:GetService("Workspace") @@ -383,21 +372,21 @@ local SaveManager = {} do end }) - -- Autoload toggle (replaces previous 'Set as autoload' button) + -- Autoload toggle (replaces previous 'Set as autoload' button) + local autoloadInitializing = false + local autopath = getConfigsFolder(self) .. "/autoload.txt" local AutoToggle = section:AddToggle("SaveManager_AutoLoad", { Title = "Autoload", Description = "ตั้งค่าให้โหลดคอนฟิกนี้อัตโนมัติเมื่อเริ่มเกม", Default = false }) - -- guard flag เพื่อไม่ให้ handler ทำงานขณะกำลัง init UI programmatically - local initializing_autoload = true - AutoToggle:OnChanged(function() - -- ถ้าเรากำลังตั้งค่าเริ่มต้น ให้ข้าม handler นี้ - if initializing_autoload then return end + -- if we're in initialization stage, don't perform file write/delete side-effects + if autoloadInitializing then + return + end local name = SaveManager.Options.SaveManager_ConfigList.Value - local autopath = getConfigsFolder(self) .. "/autoload.txt" + local autopath_local = autopath if SaveManager.Options.SaveManager_AutoLoad.Value then - -- user เปิด toggle if not name or name == "" then self.Library:Notify({ Title = "Interface", @@ -405,15 +394,12 @@ local SaveManager = {} do SubContent = "โปรดเลือกคอนฟิกก่อนตั้งค่า Autoload", Duration = 7 }) - -- revert toggle แต่ต้องทำแบบไม่ให้ trigger handler อีก => ใช้ flag - initializing_autoload = true + -- revert toggle SaveManager.Options.SaveManager_AutoLoad:SetValue(false) - initializing_autoload = false return end - -- เขียนไฟล์ autoload - pcall(writefile, autopath, name) + pcall(writefile, autopath_local, name) if SaveManager.Options.SaveManager_AutoLoad.SetDesc then SaveManager.Options.SaveManager_AutoLoad:SetDesc("กำลังโหลดอัตโนมัติ: " .. name) end @@ -425,8 +411,7 @@ local SaveManager = {} do Duration = 7 }) else - -- user ปิด toggle -> ลบไฟล์ autoload - pcall(function() if isfile(autopath) then pcall(delfile, autopath) end end) + pcall(function() if isfile(autopath_local) then pcall(delfile, autopath_local) end end) if SaveManager.Options.SaveManager_AutoLoad.SetDesc then SaveManager.Options.SaveManager_AutoLoad:SetDesc("ไม่มีการโหลดอัตโนมัติ") end @@ -441,44 +426,43 @@ local SaveManager = {} do end) -- populate current autoload desc & initial toggle state if exists - local autop = getConfigsFolder(self) .. "/autoload.txt" + local autop = autopath if isfile(autop) then - local name = readfile(autop) - - -- รีเฟรชรายการคอนฟิกก่อน - local values = self:RefreshConfigList() - SaveManager.Options.SaveManager_ConfigList:SetValues(values) - - -- ตรวจว่าไฟล์ autoload อ้างถึงคอนฟิกที่มีจริงหรือไม่ - local found = false - for _, v in ipairs(values) do - if v == name then - found = true - break + local ok, name = pcall(readfile, autop) + if ok and name and name ~= "" then + -- set dropdown value (if exists in list) + local list = self:RefreshConfigList() + SaveManager.Options.SaveManager_ConfigList:SetValues(list) + + local found = false + for _, v in ipairs(list) do + if v == name then found = true break end end - end - if found then - -- ตั้ง dropdown และ toggle โดยปิด handler ชั่วคราว - SaveManager.Options.SaveManager_ConfigList:SetValue(name) - initializing_autoload = true - SaveManager.Options.SaveManager_AutoLoad:SetValue(true) - initializing_autoload = false - - if SaveManager.Options.SaveManager_AutoLoad.SetDesc then - SaveManager.Options.SaveManager_AutoLoad:SetDesc("กำลังโหลดอัตโนมัติ: " .. name) + if found then + autoloadInitializing = true + SaveManager.Options.SaveManager_ConfigList:SetValue(name) + SaveManager.Options.SaveManager_AutoLoad:SetValue(true) + if SaveManager.Options.SaveManager_AutoLoad.SetDesc then + SaveManager.Options.SaveManager_AutoLoad:SetDesc("กำลังโหลดอัตโนมัติ: " .. name) + end + autoloadInitializing = false + else + -- referenced config missing — don't enable toggle automatically, but inform user + if SaveManager.Options.SaveManager_AutoLoad.SetDesc then + SaveManager.Options.SaveManager_AutoLoad:SetDesc("ไฟล์ autoload ชี้ไปยังคอนฟิกที่หายไป: " .. name) + end + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Autoload file references missing config %q — please recreate or choose another config", name), + Duration = 8 + }) end else - -- ถ้าไม่เจอคอนฟิก ให้แจ้งผู้ใช้แต่ **อย่า** ลบ autoload.txt โดยอัตโนมัติ if SaveManager.Options.SaveManager_AutoLoad.SetDesc then - SaveManager.Options.SaveManager_AutoLoad:SetDesc("ไฟล์ autoload ชี้ไปที่คอนฟิกที่ไม่มีอยู่: " .. name) + SaveManager.Options.SaveManager_AutoLoad:SetDesc("ไม่มีการโหลดอัตโนมัติ") end - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "Autoload file references missing config: " .. tostring(name), - Duration = 7 - }) end else if SaveManager.Options.SaveManager_AutoLoad.SetDesc then From 6c032a7b6bf3a112273206aa794b14fd8b77949a Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 16 Oct 2025 12:00:47 +0700 Subject: [PATCH 25/76] Update autosave.lua --- autosave.lua | 873 +++++++++++++++++++++++---------------------------- 1 file changed, 400 insertions(+), 473 deletions(-) diff --git a/autosave.lua b/autosave.lua index e72332a..7438bb2 100644 --- a/autosave.lua +++ b/autosave.lua @@ -1,480 +1,407 @@ +local Toggle = Tabs.Main:AddToggle("MyToggle", {Title = "Toggle", Default = false }) + + Toggle:OnChanged(function() + print("Toggle changed:", Options.MyToggle.Value) + end) + + Options.MyToggle:SetValue(false) + local httpService = game:GetService("HttpService") local Workspace = game:GetService("Workspace") local SaveManager = {} do - -- root folder (can be changed via SetFolder) - 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 getMapName() - local ok, map = pcall(function() return Workspace:FindFirstChild("Map") end) - if ok and map and map:IsA("Instance") then - return sanitizeFilename(map.Name) - end - local ok2, wname = pcall(function() return Workspace.Name end) - if ok2 and wname then return sanitizeFilename(wname) end - return "UnknownMap" - end - - local function ensureFolder(path) - if not isfolder(path) then - makefolder(path) - end - end - - -- get configs folder for current map (simplified layout: //settings) - local function getConfigsFolder(self) - local root = self.FolderRoot - local mapName = getMapName() - return root .. "/" .. mapName .. "/settings" - end - - local function getConfigFilePath(self, name) - local folder = getConfigsFolder(self) - return folder .. "/" .. name .. ".json" - end - - -- Build folder tree and migrate legacy configs if found (copy only) - function SaveManager:BuildFolderTree() - local root = self.FolderRoot - ensureFolder(root) - - local mapName = getMapName() - local mapFolder = root .. "/" .. mapName - ensureFolder(mapFolder) - - local settingsFolder = mapFolder .. "/settings" - ensureFolder(settingsFolder) - - -- legacy folder: /settings (old layout). If files exist there, copy them into current map settings - 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 = settingsFolder .. "/" .. 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 - - -- migrate autoload.txt if present (copy only) - local autopath = legacySettingsFolder .. "/autoload.txt" - if isfile(autopath) then - local autodata = readfile(autopath) - local destAuto = settingsFolder .. "/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 - - -- ensure folder exists - 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: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" then - table.insert(out, name) - end - end - end - return out - end - - function SaveManager:LoadAutoloadConfig() - local autopath = getConfigsFolder(self) .. "/autoload.txt" - if isfile(autopath) then - local name = readfile(autopath) - local success, err = self:Load(name) - if not success then - return self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "Failed to load autoload config: " .. err, - Duration = 7 - }) - end - - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Auto loaded config %q", name), - Duration = 7 - }) - end - end - - function SaveManager:BuildConfigSection(tab) - assert(self.Library, "Must set SaveManager.Library") - - local section = tab:AddSection("Configuration") - - -- Config name (Title in English, Description in Thai) - section:AddInput("SaveManager_ConfigName", { Title = "Config name", Description = "ชื่อไฟล์สำหรับบันทึกค่าการตั้งค่า (ภาษาไทย)" }) - - -- Config list dropdown - section:AddDropdown("SaveManager_ConfigList", { Title = "Config list", Description = "รายการคอนฟิกที่มีอยู่ (เลือกเพื่อโหลด/แก้ไข)", Values = self:RefreshConfigList(), AllowNull = true }) - - -- Create config - section:AddButton({ - Title = "Create config", - Description = "สร้างไฟล์คอนฟิกจากการตั้งค่าปัจจุบัน", - Callback = function() - local name = SaveManager.Options.SaveManager_ConfigName.Value - - if name:gsub(" ", "") == "" then - return self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "ชื่อคอนฟิกไม่ถูกต้อง (เว้นว่าง)", - Duration = 7 - }) - end - - local success, err = self:Save(name) - if not success then - return self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "การบันทึกคอนฟิกล้มเหลว: " .. err, - Duration = 7 - }) - end - - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Created config %q", name), - Duration = 7 - }) - - SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) - SaveManager.Options.SaveManager_ConfigList:SetValue(nil) - end - }) - - -- Load config - section:AddButton({ - Title = "Load config", - Description = "โหลดการตั้งค่าจากไฟล์คอนฟิกที่เลือก", - Callback = function() - local name = SaveManager.Options.SaveManager_ConfigList.Value - - local success, err = self:Load(name) - if not success then - return self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "การโหลดคอนฟิกล้มเหลว: " .. err, - Duration = 7 - }) - end - - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Loaded config %q", name), - Duration = 7 - }) - end - }) - - -- Overwrite config - section:AddButton({ - Title = "Overwrite config", - Description = "บันทึกทับไฟล์คอนฟิกที่เลือกด้วยการตั้งค่าปัจจุบัน", - Callback = function() - local name = SaveManager.Options.SaveManager_ConfigList.Value - - local success, err = self:Save(name) - if not success then - return self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "การบันทึกทับล้มเหลว: " .. err, - Duration = 7 - }) - end - - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Overwrote config %q", name), - Duration = 7 - }) - end - }) - - -- Refresh list - section:AddButton({ - Title = "Refresh list", - Description = "อัพเดตรายการไฟล์คอนฟิกจากโฟลเดอร์", - Callback = function() - SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) - SaveManager.Options.SaveManager_ConfigList:SetValue(nil) - end - }) - - -- Autoload toggle (replaces previous 'Set as autoload' button) - local autoloadInitializing = false - local autopath = getConfigsFolder(self) .. "/autoload.txt" - local AutoToggle = section:AddToggle("SaveManager_AutoLoad", { Title = "Autoload", Description = "ตั้งค่าให้โหลดคอนฟิกนี้อัตโนมัติเมื่อเริ่มเกม", Default = false }) - - AutoToggle:OnChanged(function() - -- if we're in initialization stage, don't perform file write/delete side-effects - if autoloadInitializing then - return - end - - local name = SaveManager.Options.SaveManager_ConfigList.Value - local autopath_local = autopath - - if SaveManager.Options.SaveManager_AutoLoad.Value then - if not name or name == "" then - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "โปรดเลือกคอนฟิกก่อนตั้งค่า Autoload", - Duration = 7 - }) - -- revert toggle - SaveManager.Options.SaveManager_AutoLoad:SetValue(false) - return - end - - pcall(writefile, autopath_local, name) - if SaveManager.Options.SaveManager_AutoLoad.SetDesc then - SaveManager.Options.SaveManager_AutoLoad:SetDesc("กำลังโหลดอัตโนมัติ: " .. name) - end - - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Set %q to autoload", name), - Duration = 7 - }) - else - pcall(function() if isfile(autopath_local) then pcall(delfile, autopath_local) end end) - if SaveManager.Options.SaveManager_AutoLoad.SetDesc then - SaveManager.Options.SaveManager_AutoLoad:SetDesc("ไม่มีการโหลดอัตโนมัติ") - end - - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "Autoload cleared", - Duration = 7 - }) - end - end) - - -- populate current autoload desc & initial toggle state if exists - local autop = autopath - if isfile(autop) then - local ok, name = pcall(readfile, autop) - if ok and name and name ~= "" then - -- set dropdown value (if exists in list) - local list = self:RefreshConfigList() - SaveManager.Options.SaveManager_ConfigList:SetValues(list) - - local found = false - for _, v in ipairs(list) do - if v == name then found = true break end - end - - if found then - autoloadInitializing = true - SaveManager.Options.SaveManager_ConfigList:SetValue(name) - SaveManager.Options.SaveManager_AutoLoad:SetValue(true) - if SaveManager.Options.SaveManager_AutoLoad.SetDesc then - SaveManager.Options.SaveManager_AutoLoad:SetDesc("กำลังโหลดอัตโนมัติ: " .. name) - end - autoloadInitializing = false - else - -- referenced config missing — don't enable toggle automatically, but inform user - if SaveManager.Options.SaveManager_AutoLoad.SetDesc then - SaveManager.Options.SaveManager_AutoLoad:SetDesc("ไฟล์ autoload ชี้ไปยังคอนฟิกที่หายไป: " .. name) - end - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Autoload file references missing config %q — please recreate or choose another config", name), - Duration = 8 - }) - end - else - if SaveManager.Options.SaveManager_AutoLoad.SetDesc then - SaveManager.Options.SaveManager_AutoLoad:SetDesc("ไม่มีการโหลดอัตโนมัติ") - end - end - else - if SaveManager.Options.SaveManager_AutoLoad.SetDesc then - SaveManager.Options.SaveManager_AutoLoad:SetDesc("ไม่มีการโหลดอัตโนมัติ") - end - end - - SaveManager:SetIgnoreIndexes({ "SaveManager_ConfigList", "SaveManager_ConfigName", "SaveManager_AutoLoad" }) - end - - -- initial build - SaveManager:BuildFolderTree() + -- root folder (can be changed via SetFolder) + 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 getMapName() + local ok, map = pcall(function() return Workspace:FindFirstChild("Map") end) + if ok and map and map:IsA("Instance") then + return sanitizeFilename(map.Name) + end + local ok2, wname = pcall(function() return Workspace.Name end) + if ok2 and wname then return sanitizeFilename(wname) end + return "UnknownMap" + end + + local function ensureFolder(path) + if not isfolder(path) then + makefolder(path) + end + end + + -- get configs folder for current place/map + local function getConfigsFolder(self) + local root = self.FolderRoot + local placeId = getPlaceId() + local mapName = getMapName() + -- FluentSettings///settings + return root .. "/" .. placeId .. "/" .. mapName .. "/settings" + end + + local function getConfigFilePath(self, name) + local folder = getConfigsFolder(self) + return folder .. "/" .. name .. ".json" + end + + -- Build folder tree and migrate legacy configs if found (copy only) + function SaveManager:BuildFolderTree() + local root = self.FolderRoot + ensureFolder(root) + + local placeId = getPlaceId() + local placeFolder = root .. "/" .. placeId + ensureFolder(placeFolder) + + local mapName = getMapName() + local mapFolder = placeFolder .. "/" .. mapName + ensureFolder(mapFolder) + + local settingsFolder = mapFolder .. "/settings" + ensureFolder(settingsFolder) + + -- legacy folder: /settings (old layout). If files exist there, copy them into current map settings + 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 = settingsFolder .. "/" .. base .. ".json" + -- copy only if destination does not exist yet + if not isfile(dest) then + local ok, data = pcall(readfile, f) + if ok and data then + local success, err = pcall(writefile, dest, data) + -- ignore write errors but do not fail + end + end + end + end + end + + -- also migrate autoload.txt if present (copy only) + local autopath = legacySettingsFolder .. "/autoload.txt" + if isfile(autopath) then + local autodata = readfile(autopath) + local destAuto = settingsFolder .. "/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 "FluentSettings") + 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 + + -- ensure folder exists + 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: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" then + table.insert(out, name) + end + end + end + return out + end + + function SaveManager:LoadAutoloadConfig() + local autopath = getConfigsFolder(self) .. "/autoload.txt" + if isfile(autopath) then + local name = readfile(autopath) + local success, err = self:Load(name) + if not success then + return self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "Failed to load autoload config: " .. err, + Duration = 7 + }) + end + + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Auto loaded config %q", name), + Duration = 7 + }) + end + end + + function SaveManager:BuildConfigSection(tab) + assert(self.Library, "Must set SaveManager.Library") + + local section = tab:AddSection("Configuration") + + section:AddInput("SaveManager_ConfigName", { Title = "Config name" }) + section:AddDropdown("SaveManager_ConfigList", { Title = "Config list", Values = self:RefreshConfigList(), AllowNull = true }) + + section:AddButton({ + Title = "Create config", + Callback = function() + local name = SaveManager.Options.SaveManager_ConfigName.Value + + if name:gsub(" ", "") == "" then + return self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "Invalid config name (empty)", + Duration = 7 + }) + end + + local success, err = self:Save(name) + if not success then + return self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "Failed to save config: " .. err, + Duration = 7 + }) + end + + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Created config %q", name), + Duration = 7 + }) + + SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) + SaveManager.Options.SaveManager_ConfigList:SetValue(nil) + end + }) + + section:AddButton({Title = "Load config", Callback = function() + local name = SaveManager.Options.SaveManager_ConfigList.Value + + local success, err = self:Load(name) + if not success then + return self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "Failed to load config: " .. err, + Duration = 7 + }) + end + + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Loaded config %q", name), + Duration = 7 + }) + end}) + + section:AddButton({Title = "Overwrite config", Callback = function() + local name = SaveManager.Options.SaveManager_ConfigList.Value + + local success, err = self:Save(name) + if not success then + return self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "Failed to overwrite config: " .. err, + Duration = 7 + }) + end + + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Overwrote config %q", name), + Duration = 7 + }) + end}) + + section:AddButton({Title = "Refresh list", Callback = function() + SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) + SaveManager.Options.SaveManager_ConfigList:SetValue(nil) + end}) + + local AutoloadButton + AutoloadButton = section:AddButton({Title = "Set as autoload", Description = "Current autoload config: none", Callback = function() + local name = SaveManager.Options.SaveManager_ConfigList.Value + local autopath = getConfigsFolder(self) .. "/autoload.txt" + writefile(autopath, name) + AutoloadButton:SetDesc("Current autoload config: " .. name) + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Set %q to auto load", name), + Duration = 7 + }) + end}) + + -- populate current autoload desc if exists + local autop = getConfigsFolder(self) .. "/autoload.txt" + if isfile(autop) then + local name = readfile(autop) + AutoloadButton:SetDesc("Current autoload config: " .. name) + end + + SaveManager:SetIgnoreIndexes({ "SaveManager_ConfigList", "SaveManager_ConfigName" }) + end + + -- initial build + SaveManager:BuildFolderTree() end return SaveManager From e8a4d187a0758c87fbe77222dc7a98d3093b9a5e Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 16 Oct 2025 12:07:12 +0700 Subject: [PATCH 26/76] Update autosave.lua --- autosave.lua | 201 +++++++++++++++++++++++++++++---------------------- 1 file changed, 113 insertions(+), 88 deletions(-) diff --git a/autosave.lua b/autosave.lua index 7438bb2..80dd3b8 100644 --- a/autosave.lua +++ b/autosave.lua @@ -1,19 +1,24 @@ -local Toggle = Tabs.Main:AddToggle("MyToggle", {Title = "Toggle", Default = false }) +-- SaveManager (ปรับปรุง: โครงสร้างไฟล์เรียบง่ายขึ้น, เพิ่ม Description(ไทย) + Title(อังกฤษ) ให้ UI) +local Toggle = Tabs.Main:AddToggle("MyToggle", { Title = "Toggle", Description = "สวิตช์เปิด/ปิดการทำงาน", Default = false }) - Toggle:OnChanged(function() - print("Toggle changed:", Options.MyToggle.Value) - end) +Toggle:OnChanged(function() + print("Toggle changed:", Options.MyToggle.Value) +end) - Options.MyToggle:SetValue(false) +Options.MyToggle:SetValue(false) -local httpService = game:GetService("HttpService") +local HttpService = game:GetService("HttpService") local Workspace = game:GetService("Workspace") local SaveManager = {} do -- root folder (can be changed via SetFolder) + -- ค่าเริ่มต้นเก็บไว้เป็น ATGSettings ตามที่เดิม SaveManager.FolderRoot = "ATGSettings" SaveManager.Ignore = {} SaveManager.Options = {} + SaveManager.Library = nil + + -- Parser สำหรับ option ต่าง ๆ (ไม่เปลี่ยน) SaveManager.Parser = { Toggle = { Save = function(idx, object) @@ -108,37 +113,30 @@ local SaveManager = {} do end end - -- get configs folder for current place/map + -- ปรับโครงสร้างเป็น Root/_/settings เพื่อไม่ต้องสร้างชั้นลึกมาก local function getConfigsFolder(self) - local root = self.FolderRoot + local root = tostring(self.FolderRoot or "ATGSettings") local placeId = getPlaceId() local mapName = getMapName() - -- FluentSettings///settings - return root .. "/" .. placeId .. "/" .. mapName .. "/settings" + local folderName = sanitizeFilename(placeId .. "_" .. mapName) + return root .. "/" .. folderName .. "/settings" end local function getConfigFilePath(self, name) local folder = getConfigsFolder(self) - return folder .. "/" .. name .. ".json" + return folder .. "/" .. sanitizeFilename(name) .. ".json" end -- Build folder tree and migrate legacy configs if found (copy only) function SaveManager:BuildFolderTree() - local root = self.FolderRoot + local root = tostring(self.FolderRoot or "ATGSettings") ensureFolder(root) - local placeId = getPlaceId() - local placeFolder = root .. "/" .. placeId - ensureFolder(placeFolder) - - local mapName = getMapName() - local mapFolder = placeFolder .. "/" .. mapName - ensureFolder(mapFolder) - - local settingsFolder = mapFolder .. "/settings" - ensureFolder(settingsFolder) + -- สร้างเฉพาะโฟลเดอร์ที่จำเป็น: โฟลเดอร์ของการตั้งค่าสำหรับ place+map + local configsFolder = getConfigsFolder(self) + ensureFolder(configsFolder) - -- legacy folder: /settings (old layout). If files exist there, copy them into current map settings + -- legacy folder migration: /settings (old layout) local legacySettingsFolder = root .. "/settings" if isfolder(legacySettingsFolder) then local files = listfiles(legacySettingsFolder) @@ -147,24 +145,23 @@ local SaveManager = {} do if f:sub(-5) == ".json" then local base = f:match("([^/\\]+)%.json$") if base and base ~= "options" then - local dest = settingsFolder .. "/" .. base .. ".json" + local dest = configsFolder .. "/" .. base .. ".json" -- copy only if destination does not exist yet if not isfile(dest) then local ok, data = pcall(readfile, f) if ok and data then - local success, err = pcall(writefile, dest, data) - -- ignore write errors but do not fail + pcall(writefile, dest, data) end end end end end - -- also migrate autoload.txt if present (copy only) + -- migrate autoload.txt if present (copy only) local autopath = legacySettingsFolder .. "/autoload.txt" if isfile(autopath) then local autodata = readfile(autopath) - local destAuto = settingsFolder .. "/autoload.txt" + local destAuto = configsFolder .. "/autoload.txt" if not isfile(destAuto) then pcall(writefile, destAuto, autodata) end @@ -179,7 +176,7 @@ local SaveManager = {} do end function SaveManager:SetFolder(folder) - self.FolderRoot = tostring(folder or "FluentSettings") + self.FolderRoot = tostring(folder or "ATGSettings") self:BuildFolderTree() end @@ -204,7 +201,7 @@ local SaveManager = {} do table.insert(data.objects, self.Parser[option.Type].Save(idx, option)) end - local success, encoded = pcall(httpService.JSONEncode, httpService, data) + local success, encoded = pcall(HttpService.JSONEncode, HttpService, data) if not success then return false, "failed to encode data" end @@ -213,7 +210,11 @@ local SaveManager = {} do local folder = fullPath:match("^(.*)/[^/]+$") if folder then ensureFolder(folder) end - writefile(fullPath, encoded) + local ok, err = pcall(function() writefile(fullPath, encoded) end) + if not ok then + return false, "failed to write file: " .. tostring(err) + end + return true end @@ -225,7 +226,7 @@ local SaveManager = {} do local file = getConfigFilePath(self, name) if not isfile(file) then return false, "invalid file" end - local success, decoded = pcall(httpService.JSONDecode, httpService, readfile(file)) + 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 @@ -268,37 +269,43 @@ local SaveManager = {} do local name = readfile(autopath) local success, err = self:Load(name) if not success then - return self.Library:Notify({ + if self.Library and self.Library.Notify then + return self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "Failed to load autoload config: " .. err, + Duration = 7 + }) + end + end + + if self.Library and self.Library.Notify then + self.Library:Notify({ Title = "Interface", Content = "Config loader", - SubContent = "Failed to load autoload config: " .. err, + SubContent = string.format("Auto loaded config %q", name), Duration = 7 }) end - - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Auto loaded config %q", name), - Duration = 7 - }) end end + -- UI: สร้าง section สำหรับจัดการ config (เติม Description ภาษาไทย + Title ภาษาอังกฤษ) function SaveManager:BuildConfigSection(tab) assert(self.Library, "Must set SaveManager.Library") local section = tab:AddSection("Configuration") - section:AddInput("SaveManager_ConfigName", { Title = "Config name" }) - section:AddDropdown("SaveManager_ConfigList", { Title = "Config list", Values = self:RefreshConfigList(), AllowNull = true }) + section:AddInput("SaveManager_ConfigName", { Title = "Config name", Description = "ชื่อไฟล์คอนฟิก (ไม่ต้องใส่นามสกุล .json)" }) + section:AddDropdown("SaveManager_ConfigList", { Title = "Config list", Values = self:RefreshConfigList(), AllowNull = true, Description = "รายการไฟล์คอนฟิกที่มีอยู่" }) section:AddButton({ Title = "Create config", + Description = "สร้างไฟล์คอนฟิกใหม่จากค่าปัจจุบัน", Callback = function() local name = SaveManager.Options.SaveManager_ConfigName.Value - if name:gsub(" ", "") == "" then + if not name or name:gsub(" ", "") == "" then return self.Library:Notify({ Title = "Interface", Content = "Config loader", @@ -329,78 +336,96 @@ local SaveManager = {} do end }) - section:AddButton({Title = "Load config", Callback = function() - local name = SaveManager.Options.SaveManager_ConfigList.Value + section:AddButton({ + Title = "Load config", + Description = "โหลดค่าจากไฟล์คอนฟิกที่เลือก", + Callback = function() + local name = SaveManager.Options.SaveManager_ConfigList.Value - local success, err = self:Load(name) - if not success then - return self.Library:Notify({ + local success, err = self:Load(name) + if not success then + return self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "Failed to load config: " .. err, + Duration = 7 + }) + end + + self.Library:Notify({ Title = "Interface", Content = "Config loader", - SubContent = "Failed to load config: " .. err, + SubContent = string.format("Loaded config %q", name), Duration = 7 }) end + }) - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Loaded config %q", name), - Duration = 7 - }) - end}) + section:AddButton({ + Title = "Overwrite config", + Description = "บันทึกทับไฟล์คอนฟิกที่เลือก", + Callback = function() + local name = SaveManager.Options.SaveManager_ConfigList.Value - section:AddButton({Title = "Overwrite config", Callback = function() - local name = SaveManager.Options.SaveManager_ConfigList.Value + local success, err = self:Save(name) + if not success then + return self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "Failed to overwrite config: " .. err, + Duration = 7 + }) + end - local success, err = self:Save(name) - if not success then - return self.Library:Notify({ + self.Library:Notify({ Title = "Interface", Content = "Config loader", - SubContent = "Failed to overwrite config: " .. err, + SubContent = string.format("Overwrote config %q", name), Duration = 7 }) end + }) - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Overwrote config %q", name), - Duration = 7 - }) - end}) - - section:AddButton({Title = "Refresh list", Callback = function() - SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) - SaveManager.Options.SaveManager_ConfigList:SetValue(nil) - end}) + section:AddButton({ + Title = "Refresh list", + Description = "อัปเดตรายการไฟล์คอนฟิก", + Callback = function() + SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) + SaveManager.Options.SaveManager_ConfigList:SetValue(nil) + end + }) local AutoloadButton - AutoloadButton = section:AddButton({Title = "Set as autoload", Description = "Current autoload config: none", Callback = function() - local name = SaveManager.Options.SaveManager_ConfigList.Value - local autopath = getConfigsFolder(self) .. "/autoload.txt" - writefile(autopath, name) - AutoloadButton:SetDesc("Current autoload config: " .. name) - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Set %q to auto load", name), - Duration = 7 - }) - end}) + AutoloadButton = section:AddButton({ + Title = "Set as autoload", + Description = "ตั้งไฟล์ที่เลือกให้โหลดอัตโนมัติเมื่อเริ่ม", + Callback = function() + local name = SaveManager.Options.SaveManager_ConfigList.Value + local autopath = getConfigsFolder(self) .. "/autoload.txt" + writefile(autopath, tostring(name or "")) + AutoloadButton:SetDesc("Current autoload config: " .. tostring(name or "none")) + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Set %q to auto load", name), + Duration = 7 + }) + end + }) -- populate current autoload desc if exists local autop = getConfigsFolder(self) .. "/autoload.txt" if isfile(autop) then local name = readfile(autop) AutoloadButton:SetDesc("Current autoload config: " .. name) + else + AutoloadButton:SetDesc("Current autoload config: none") end SaveManager:SetIgnoreIndexes({ "SaveManager_ConfigList", "SaveManager_ConfigName" }) end - -- initial build + -- initial build: สร้างโฟลเดอร์ที่จำเป็น (เรียบง่าย) SaveManager:BuildFolderTree() end From 79f183a35c1549c0a2f8e264f92a2ac8c534fa19 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 16 Oct 2025 12:12:04 +0700 Subject: [PATCH 27/76] Update autosave.lua --- autosave.lua | 199 +++++++++++++++++++++------------------------------ 1 file changed, 83 insertions(+), 116 deletions(-) diff --git a/autosave.lua b/autosave.lua index 80dd3b8..b58cc95 100644 --- a/autosave.lua +++ b/autosave.lua @@ -1,24 +1,11 @@ --- SaveManager (ปรับปรุง: โครงสร้างไฟล์เรียบง่ายขึ้น, เพิ่ม Description(ไทย) + Title(อังกฤษ) ให้ UI) -local Toggle = Tabs.Main:AddToggle("MyToggle", { Title = "Toggle", Description = "สวิตช์เปิด/ปิดการทำงาน", Default = false }) - -Toggle:OnChanged(function() - print("Toggle changed:", Options.MyToggle.Value) -end) - -Options.MyToggle:SetValue(false) - -local HttpService = game:GetService("HttpService") +local httpService = game:GetService("HttpService") local Workspace = game:GetService("Workspace") local SaveManager = {} do -- root folder (can be changed via SetFolder) - -- ค่าเริ่มต้นเก็บไว้เป็น ATGSettings ตามที่เดิม SaveManager.FolderRoot = "ATGSettings" SaveManager.Ignore = {} SaveManager.Options = {} - SaveManager.Library = nil - - -- Parser สำหรับ option ต่าง ๆ (ไม่เปลี่ยน) SaveManager.Parser = { Toggle = { Save = function(idx, object) @@ -113,30 +100,37 @@ local SaveManager = {} do end end - -- ปรับโครงสร้างเป็น Root/_/settings เพื่อไม่ต้องสร้างชั้นลึกมาก + -- get configs folder for current place/map local function getConfigsFolder(self) - local root = tostring(self.FolderRoot or "ATGSettings") + local root = self.FolderRoot local placeId = getPlaceId() local mapName = getMapName() - local folderName = sanitizeFilename(placeId .. "_" .. mapName) - return root .. "/" .. folderName .. "/settings" + -- FluentSettings///settings + return root .. "/" .. placeId .. "/" .. mapName .. "/settings" end local function getConfigFilePath(self, name) local folder = getConfigsFolder(self) - return folder .. "/" .. sanitizeFilename(name) .. ".json" + return folder .. "/" .. name .. ".json" end -- Build folder tree and migrate legacy configs if found (copy only) function SaveManager:BuildFolderTree() - local root = tostring(self.FolderRoot or "ATGSettings") + local root = self.FolderRoot ensureFolder(root) - -- สร้างเฉพาะโฟลเดอร์ที่จำเป็น: โฟลเดอร์ของการตั้งค่าสำหรับ place+map - local configsFolder = getConfigsFolder(self) - ensureFolder(configsFolder) + local placeId = getPlaceId() + local placeFolder = root .. "/" .. placeId + ensureFolder(placeFolder) + + local mapName = getMapName() + local mapFolder = placeFolder .. "/" .. mapName + ensureFolder(mapFolder) + + local settingsFolder = mapFolder .. "/settings" + ensureFolder(settingsFolder) - -- legacy folder migration: /settings (old layout) + -- legacy folder: /settings (old layout). If files exist there, copy them into current map settings local legacySettingsFolder = root .. "/settings" if isfolder(legacySettingsFolder) then local files = listfiles(legacySettingsFolder) @@ -145,23 +139,24 @@ local SaveManager = {} do if f:sub(-5) == ".json" then local base = f:match("([^/\\]+)%.json$") if base and base ~= "options" then - local dest = configsFolder .. "/" .. base .. ".json" + local dest = settingsFolder .. "/" .. base .. ".json" -- copy only if destination does not exist yet if not isfile(dest) then local ok, data = pcall(readfile, f) if ok and data then - pcall(writefile, dest, data) + local success, err = pcall(writefile, dest, data) + -- ignore write errors but do not fail end end end end end - -- migrate autoload.txt if present (copy only) + -- also migrate autoload.txt if present (copy only) local autopath = legacySettingsFolder .. "/autoload.txt" if isfile(autopath) then local autodata = readfile(autopath) - local destAuto = configsFolder .. "/autoload.txt" + local destAuto = settingsFolder .. "/autoload.txt" if not isfile(destAuto) then pcall(writefile, destAuto, autodata) end @@ -176,7 +171,7 @@ local SaveManager = {} do end function SaveManager:SetFolder(folder) - self.FolderRoot = tostring(folder or "ATGSettings") + self.FolderRoot = tostring(folder or "FluentSettings") self:BuildFolderTree() end @@ -201,7 +196,7 @@ local SaveManager = {} do table.insert(data.objects, self.Parser[option.Type].Save(idx, option)) end - local success, encoded = pcall(HttpService.JSONEncode, HttpService, data) + local success, encoded = pcall(httpService.JSONEncode, httpService, data) if not success then return false, "failed to encode data" end @@ -210,11 +205,7 @@ local SaveManager = {} do local folder = fullPath:match("^(.*)/[^/]+$") if folder then ensureFolder(folder) end - local ok, err = pcall(function() writefile(fullPath, encoded) end) - if not ok then - return false, "failed to write file: " .. tostring(err) - end - + writefile(fullPath, encoded) return true end @@ -226,7 +217,7 @@ local SaveManager = {} do local file = getConfigFilePath(self, name) if not isfile(file) then return false, "invalid file" end - local success, decoded = pcall(HttpService.JSONDecode, HttpService, readfile(file)) + 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 @@ -269,43 +260,37 @@ local SaveManager = {} do local name = readfile(autopath) local success, err = self:Load(name) if not success then - if self.Library and self.Library.Notify then - return self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "Failed to load autoload config: " .. err, - Duration = 7 - }) - end - end - - if self.Library and self.Library.Notify then - self.Library:Notify({ + return self.Library:Notify({ Title = "Interface", Content = "Config loader", - SubContent = string.format("Auto loaded config %q", name), + SubContent = "Failed to load autoload config: " .. err, Duration = 7 }) end + + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Auto loaded config %q", name), + Duration = 7 + }) end end - -- UI: สร้าง section สำหรับจัดการ config (เติม Description ภาษาไทย + Title ภาษาอังกฤษ) function SaveManager:BuildConfigSection(tab) assert(self.Library, "Must set SaveManager.Library") local section = tab:AddSection("Configuration") - section:AddInput("SaveManager_ConfigName", { Title = "Config name", Description = "ชื่อไฟล์คอนฟิก (ไม่ต้องใส่นามสกุล .json)" }) - section:AddDropdown("SaveManager_ConfigList", { Title = "Config list", Values = self:RefreshConfigList(), AllowNull = true, Description = "รายการไฟล์คอนฟิกที่มีอยู่" }) + section:AddInput("SaveManager_ConfigName", { Title = "Config name" }) + section:AddDropdown("SaveManager_ConfigList", { Title = "Config list", Values = self:RefreshConfigList(), AllowNull = true }) section:AddButton({ Title = "Create config", - Description = "สร้างไฟล์คอนฟิกใหม่จากค่าปัจจุบัน", Callback = function() local name = SaveManager.Options.SaveManager_ConfigName.Value - if not name or name:gsub(" ", "") == "" then + if name:gsub(" ", "") == "" then return self.Library:Notify({ Title = "Interface", Content = "Config loader", @@ -336,96 +321,78 @@ local SaveManager = {} do end }) - section:AddButton({ - Title = "Load config", - Description = "โหลดค่าจากไฟล์คอนฟิกที่เลือก", - Callback = function() - local name = SaveManager.Options.SaveManager_ConfigList.Value - - local success, err = self:Load(name) - if not success then - return self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "Failed to load config: " .. err, - Duration = 7 - }) - end + section:AddButton({Title = "Load config", Callback = function() + local name = SaveManager.Options.SaveManager_ConfigList.Value - self.Library:Notify({ + local success, err = self:Load(name) + if not success then + return self.Library:Notify({ Title = "Interface", Content = "Config loader", - SubContent = string.format("Loaded config %q", name), + SubContent = "Failed to load config: " .. err, Duration = 7 }) end - }) - section:AddButton({ - Title = "Overwrite config", - Description = "บันทึกทับไฟล์คอนฟิกที่เลือก", - Callback = function() - local name = SaveManager.Options.SaveManager_ConfigList.Value + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Loaded config %q", name), + Duration = 7 + }) + end}) - local success, err = self:Save(name) - if not success then - return self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "Failed to overwrite config: " .. err, - Duration = 7 - }) - end + section:AddButton({Title = "Overwrite config", Callback = function() + local name = SaveManager.Options.SaveManager_ConfigList.Value - self.Library:Notify({ + local success, err = self:Save(name) + if not success then + return self.Library:Notify({ Title = "Interface", Content = "Config loader", - SubContent = string.format("Overwrote config %q", name), + SubContent = "Failed to overwrite config: " .. err, Duration = 7 }) end - }) - section:AddButton({ - Title = "Refresh list", - Description = "อัปเดตรายการไฟล์คอนฟิก", - Callback = function() - SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) - SaveManager.Options.SaveManager_ConfigList:SetValue(nil) - end - }) + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Overwrote config %q", name), + Duration = 7 + }) + end}) + + section:AddButton({Title = "Refresh list", Callback = function() + SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) + SaveManager.Options.SaveManager_ConfigList:SetValue(nil) + end}) local AutoloadButton - AutoloadButton = section:AddButton({ - Title = "Set as autoload", - Description = "ตั้งไฟล์ที่เลือกให้โหลดอัตโนมัติเมื่อเริ่ม", - Callback = function() - local name = SaveManager.Options.SaveManager_ConfigList.Value - local autopath = getConfigsFolder(self) .. "/autoload.txt" - writefile(autopath, tostring(name or "")) - AutoloadButton:SetDesc("Current autoload config: " .. tostring(name or "none")) - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Set %q to auto load", name), - Duration = 7 - }) - end - }) + AutoloadButton = section:AddButton({Title = "Set as autoload", Description = "Current autoload config: none", Callback = function() + local name = SaveManager.Options.SaveManager_ConfigList.Value + local autopath = getConfigsFolder(self) .. "/autoload.txt" + writefile(autopath, name) + AutoloadButton:SetDesc("Current autoload config: " .. name) + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Set %q to auto load", name), + Duration = 7 + }) + end}) -- populate current autoload desc if exists local autop = getConfigsFolder(self) .. "/autoload.txt" if isfile(autop) then local name = readfile(autop) AutoloadButton:SetDesc("Current autoload config: " .. name) - else - AutoloadButton:SetDesc("Current autoload config: none") end SaveManager:SetIgnoreIndexes({ "SaveManager_ConfigList", "SaveManager_ConfigName" }) end - -- initial build: สร้างโฟลเดอร์ที่จำเป็น (เรียบง่าย) + -- initial build SaveManager:BuildFolderTree() end From 316b9af220fd6e7fb96c1b962280557a351af552 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 30 Oct 2025 22:12:38 +0700 Subject: [PATCH 28/76] Create Macro.lua --- Macro.lua | 580 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 580 insertions(+) create mode 100644 Macro.lua diff --git a/Macro.lua b/Macro.lua new file mode 100644 index 0000000..d72a6ed --- /dev/null +++ b/Macro.lua @@ -0,0 +1,580 @@ +-- สร้างตัวแปร UI ที่ต้องใช้ข้ามบล็อก (เพื่อให้ Settings เข้าถึง FileDropdown ได้) +local FilenameInput +local FileDropdown + +-- ตัวแปรหลัก +local recording = false +local playing = false +local macroData = {} +local startTime = 0 +local looping = false +local hookInstalled = false +local oldNamecall + +-- ตัวแปรสำหรับแสดงสถานะ +local statusText = "🟢 พร้อมใช้งาน" +local eventsCount = 0 +local durationTime = 0 + +-- พาธโฟลเดอร์สำหรับมาโคร +local BASE_FOLDER = "ATGHUB_Macro" +local SUB_FOLDER = BASE_FOLDER .. "/Anime Last Stand" + +-- ตรวจสอบและสร้างโฟลเดอร์ (ถ้ามีฟังก์ชัน makefolder) +local function ensureMacroFolders() + pcall(function() + if makefolder then + -- ถ้า isfolder มี ให้เช็คก่อนสร้าง (บาง exploit มีบางอันไม่มี) + if isfolder then + if not isfolder(BASE_FOLDER) then + makefolder(BASE_FOLDER) + end + if not isfolder(SUB_FOLDER) then + makefolder(SUB_FOLDER) + end + else + -- ถ้าไม่มี isfolder แต่มี makefolder ให้เรียกสร้างเผื่อไว้ + makefolder(BASE_FOLDER) + makefolder(SUB_FOLDER) + end + end + end) +end + +-- ฟังก์ชันแปลง Arguments +local function serializeArg(arg) + local argType = typeof(arg) + + if argType == "Instance" then + return { + type = "Instance", + path = arg:GetFullName(), + className = arg.ClassName + } + elseif argType == "CFrame" then + return { + type = "CFrame", + components = {arg:GetComponents()} + } + elseif argType == "Vector3" then + return { + type = "Vector3", + x = arg.X, y = arg.Y, z = arg.Z + } + elseif argType == "Color3" then + return { + type = "Color3", + r = arg.R, g = arg.G, b = arg.B + } + elseif argType == "table" then + local serialized = {} + for k, v in pairs(arg) do + serialized[k] = serializeArg(v) + end + return { + type = "table", + data = serialized + } + else + return { + type = argType, + value = arg + } + end +end + +-- ฟังก์ชันแปลงกลับ +local function deserializeArg(data) + if type(data) ~= "table" or not data.type then + return data + end + + if data.type == "Instance" then + local success, result = pcall(function() + local obj = game + for part in data.path:gmatch("[^.]+") do + if part ~= "game" then + obj = obj:WaitForChild(part, 3) + end + end + return obj + end) + return success and result or nil + elseif data.type == "CFrame" then + return CFrame.new(unpack(data.components)) + elseif data.type == "Vector3" then + return Vector3.new(data.x, data.y, data.z) + elseif data.type == "Color3" then + return Color3.new(data.r, data.g, data.b) + elseif data.type == "table" then + local result = {} + for k, v in pairs(data.data) do + result[k] = deserializeArg(v) + end + return result + else + return data.value + end +end + +-- ฟังก์ชันติดตั้ง Hook แบบ SimpleSpy +local function installHook() + if hookInstalled then return true end + + local success, err = pcall(function() + -- ใช้วิธีการ Hook แบบเดียวกับ SimpleSpy + oldNamecall = hookmetamethod(game, "__namecall", function(self, ...) + local method = getnamecallmethod() + local args = {...} + + -- จับ FireServer และ InvokeServer + if (method == "FireServer" or method == "InvokeServer") and recording then + if self:IsA("RemoteEvent") or self:IsA("RemoteFunction") then + local currentTime = tick() - startTime + local serializedArgs = {} + + -- Serialize arguments + for i, arg in ipairs(args) do + serializedArgs[i] = serializeArg(arg) + end + + -- บันทึก + table.insert(macroData, { + time = currentTime, + remoteName = self.Name, + remotePath = self:GetFullName(), + remoteType = method, + args = serializedArgs + }) + + -- อัพเดท UI + eventsCount = #macroData + durationTime = currentTime + + -- อัพเดท Paragraph (ถ้ามี) + if Tabs.Main then + local successSet = pcall(function() + Tabs.Main:GetChildren()[1]:GetChildren()[1]:GetChildren()[2]:SetValue( + string.format("📊 Events: %d\n⏱️ Duration: %.3fs\n🔗 Hook: Active ✓", eventsCount, durationTime) + ) + end) + end + end + end + + -- ส่งต่อการเรียกไปยัง Server ตามปกติ + return oldNamecall(self, ...) + end) + + hookInstalled = true + return true + end) + + if not success then + warn("⚠️ Hook installation failed: " .. tostring(err)) + return false + end + + return true +end + +-- ฟังก์ชันเล่นมาโคร +local function playMacro(loop) + if #macroData == 0 then + Fluent:Notify({ + Title = "Macro System", + Content = "❌ ไม่มีมาโครให้เล่น!", + Duration = 3 + }) + return + end + + playing = true + looping = loop or false + + task.spawn(function() + repeat + local playStart = tick() + + for i, action in ipairs(macroData) do + if not playing then break end + + -- รอให้ถึงเวลา + local targetTime = playStart + action.time + local waitTime = targetTime - tick() + if waitTime > 0 then + task.wait(waitTime) + end + + if not playing then break end + + -- เล่น Remote + local success, err = pcall(function() + local remote = game + for part in action.remotePath:gmatch("[^.]+") do + if part ~= "game" then + remote = remote:FindFirstChild(part) or remote:WaitForChild(part, 2) + end + end + + if remote then + local deserializedArgs = {} + for i, arg in ipairs(action.args) do + deserializedArgs[i] = deserializeArg(arg) + end + + if action.remoteType == "FireServer" then + remote:FireServer(unpack(deserializedArgs)) + else + remote:InvokeServer(unpack(deserializedArgs)) + end + end + end) + end + + if looping and playing then + task.wait(0.5) + end + until not looping or not playing + + playing = false + end) +end + +-- ฟังก์ชันบันทึกไฟล์ (เก็บใน ATGHUB_Macro/Anime Last Stand/.json) +local function saveMacro(filename) + if #macroData == 0 then + Fluent:Notify({ + Title = "Macro System", + Content = "❌ ไม่มีข้อมูลมาโครให้บันทึก!", + Duration = 3 + }) + return false + end + + if not filename or filename == "" then + Fluent:Notify({ + Title = "Macro System", + Content = "❌ กรุณากรอกชื่อไฟล์!", + Duration = 3 + }) + return false + end + + ensureMacroFolders() + + local success, result = pcall(function() + local data = { + macroData = macroData, + timestamp = os.time(), + totalEvents = #macroData, + totalDuration = durationTime + } + + local json = HttpService:JSONEncode(data) + + if writefile then + -- บันทึกลงไฟล์ + local fullPath = SUB_FOLDER .. "/" .. filename .. ".json" + writefile(fullPath, json) + return true + else + -- แสดงข้อมูลให้คัดลอก + setclipboard(json) + return "clipboard" + end + end) + + if success then + if result == "clipboard" then + Fluent:Notify({ + Title = "Macro System", + Content = "📋 บันทึกแล้ว! ข้อมูลถูกคัดลอกไปยังคลิปบอร์ด", + Duration = 5 + }) + else + Fluent:Notify({ + Title = "Macro System", + Content = "💾 บันทึกไฟล์สำเร็จ!", + Duration = 3 + }) + end + return true + else + Fluent:Notify({ + Title = "Macro System", + Content = "❌ การบันทึกล้มเหลว: " .. tostring(result), + Duration = 5 + }) + return false + end +end + +-- ฟังก์ชันโหลดไฟล์ +local function loadMacro(filename) + if not filename or filename == "" then + Fluent:Notify({ + Title = "Macro System", + Content = "❌ กรุณาเลือกไฟล์!", + Duration = 3 + }) + return false + end + + local success, result = pcall(function() + if readfile then + local fullPath = SUB_FOLDER .. "/" .. filename .. ".json" + if not isfile then + -- ถ้าไม่มี isfile ให้ลอง readfile โดยจับ error แทน + local content = readfile(fullPath) + local data = HttpService:JSONDecode(content) + return data + else + if not isfile(fullPath) then + return nil, "ไฟล์ไม่พบ" + end + local content = readfile(fullPath) + local data = HttpService:JSONDecode(content) + return data + end + else + return nil, "ระบบไฟล์ไม่รองรับ" + end + end) + + if success and result then + macroData = result.macroData or {} + eventsCount = #macroData + durationTime = result.totalDuration or 0 + + -- อัพเดท UI + if Tabs.Main then + pcall(function() + Tabs.Main:GetChildren()[1]:GetChildren()[1]:GetChildren()[2]:SetValue( + string.format("📊 Events: %d\n⏱️ Duration: %.3fs\n🔗 Hook: Active ✓", eventsCount, durationTime) + ) + end) + end + + Fluent:Notify({ + Title = "Macro System", + Content = "✅ โหลดมาโครสำเร็จ!", + Duration = 3 + }) + return true + else + Fluent:Notify({ + Title = "Macro System", + Content = "❌ โหลดไฟล์ล้มเหลว: " .. tostring(result), + Duration = 5 + }) + return false + end +end + +-- ฟังก์ชันรับรายการไฟล์จากโฟลเดอร์ ATGHUB_Macro/Anime Last Stand +local function getMacroFiles() + ensureMacroFolders() + if not listfiles then return {"ไม่มีระบบไฟล์"} end + + local files = {} + local success, result = pcall(function() + local all = listfiles(SUB_FOLDER) + for _, file in pairs(all) do + -- หาชื่อไฟล์สุดท้ายก่อน .json + local name = file:match("([^/\\]+)%.json$") + if name and name ~= "" then + table.insert(files, name) + end + end + return files + end) + + if success and #files > 0 then + return result + else + return {"ไม่มีไฟล์ที่บันทึกไว้"} + end +end + +-- สร้าง UI ในแท็บ Main +do + -- Input สำหรับกรอกชื่อไฟล์ + local Section = Tabs.Macro:AddSection("Macro") + FilenameInput = Tabs.Macro:AddInput("FilenameInput", { + Title = "ชื่อไฟล์", + Default = "", + Placeholder = "กรอกชื่อไฟล์ (ไม่ต้องมี .json)", + Numeric = false, + Finished = false, + }) + + -- Dropdown สำหรับเลือกไฟล์ + FileDropdown = Tabs.Macro:AddDropdown("FileDropdown", { + Title = "เลือกไฟล์มาโคร", + Values = getMacroFiles(), + Multi = false, + Default = 1, + }) + + -- Button สร้างไฟล์ + Tabs.Macro:AddButton({ + Title = "💾 บันทึกมาโคร", + Description = "บันทึกมาโครปัจจุบันลงไฟล์", + Callback = function() + local filename = Options.FilenameInput.Value + if filename == "" then + filename = os.date("macro_%Y%m%d_%H%M%S") + end + saveMacro(filename) + + -- อัพเดท dropdown + pcall(function() + FileDropdown:SetValues(getMacroFiles()) + end) + end + }) + + -- Toggle บันทึกมาโคร + local RecordToggle = Tabs.Macro:AddToggle("RecordToggle", { + Title = "⏺️ บันทึกมาโคร", + Description = "เริ่ม/หยุด การบันทึกมาโคร", + Default = false + }) + + RecordToggle:OnChanged(function() + if Options.RecordToggle.Value then + -- เริ่มบันทึก + if not hookInstalled then + if not installHook() then + Fluent:Notify({ + Title = "Macro System", + Content = "❌ ติดตั้ง Hook ไม่สำเร็จ!", + Duration = 3 + }) + RecordToggle:SetValue(false) + return + end + end + + recording = true + macroData = {} + startTime = tick() + eventsCount = 0 + durationTime = 0 + + Fluent:Notify({ + Title = "Macro System", + Content = "🎬 เริ่มบันทึกมาโครแล้ว", + Duration = 2 + }) + else + -- หยุดบันทึก + recording = false + Fluent:Notify({ + Title = "Macro System", + Content = string.format("✅ บันทึกเสร็จสิ้น: %d events", #macroData), + Duration = 3 + }) + end + end) + + -- Toggle เล่นมาโครอัตโนมัติ + local AutoPlayToggle = Tabs.Macro:AddToggle("AutoPlayToggle", { + Title = "🔁 เล่นมาโครอัตโนมัติ", + Description = "เล่นมาโครวนลูปเมื่อเปิด", + Default = false + }) + + AutoPlayToggle:OnChanged(function() + if Options.AutoPlayToggle.Value then + if not playing then + playMacro(true) + Fluent:Notify({ + Title = "Macro System", + Content = "🔁 เริ่มเล่นมาโครแบบวนลูป", + Duration = 2 + }) + end + else + looping = false + playing = false + Fluent:Notify({ + Title = "Macro System", + Content = "⏹️ หยุดเล่นมาโคร", + Duration = 2 + }) + end + end) + + -- Button เล่นมาโคร + Tabs.Macro:AddButton({ + Title = "▶️ เล่นมาโคร", + Description = "เล่นมาโครหนึ่งรอบ", + Callback = function() + if playing then + Fluent:Notify({ + Title = "Macro System", + Content = "⏸️ หยุดเล่นมาโครชั่วคราว", + Duration = 2 + }) + playing = false + looping = false + Options.AutoPlayToggle:SetValue(false) + else + playMacro(false) + Fluent:Notify({ + Title = "Macro System", + Content = "▶️ เริ่มเล่นมาโครหนึ่งรอบ", + Duration = 2 + }) + end + end + }) + + -- Button โหลดไฟล์ + Tabs.Macro:AddButton({ + Title = "📂 โหลดมาโคร", + Description = "โหลดมาโครจากไฟล์", + Callback = function() + local selectedFile = Options.FileDropdown.Value + if selectedFile and selectedFile ~= "ไม่มีไฟล์ที่บันทึกไว้" and selectedFile ~= "ไม่มีระบบไฟล์" then + loadMacro(selectedFile) + else + Fluent:Notify({ + Title = "Macro System", + Content = "❌ กรุณาเลือกไฟล์ที่ถูกต้อง", + Duration = 3 + }) + end + end + }) + + -- Button ล้างข้อมูล + Tabs.Macro:AddButton({ + Title = "🗑️ ล้างข้อมูล", + Description = "ล้างข้อมูลมาโครปัจจุบัน", + Callback = function() + macroData = {} + eventsCount = 0 + durationTime = 0 + end + }) +end + +do + Tabs.Macro:AddButton({ + Title = "อัพเดทรายการไฟล์", + Description = "รีเฟรชรายการไฟล์มาโคร (จากโฟลเดอร์ ATGHUB_Macro/Anime Last Stand)", + Callback = function() + pcall(function() + FileDropdown:SetValues(getMacroFiles()) + end) + Fluent:Notify({ + Title = "Macro System", + Content = "🔄 อัพเดทรายการไฟล์แล้ว", + Duration = 2 + }) + end + }) +end From f16c0e66ac73b92ceeab2b5d30649fe80cb35eb6 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 30 Oct 2025 22:19:32 +0700 Subject: [PATCH 29/76] Update Macro.lua --- Macro.lua | 492 +++++++++++++++++++++--------------------------------- 1 file changed, 191 insertions(+), 301 deletions(-) diff --git a/Macro.lua b/Macro.lua index d72a6ed..ee171bf 100644 --- a/Macro.lua +++ b/Macro.lua @@ -1,30 +1,33 @@ --- สร้างตัวแปร UI ที่ต้องใช้ข้ามบล็อก (เพื่อให้ Settings เข้าถึง FileDropdown ได้) -local FilenameInput -local FileDropdown +-- MacroModule.lua +-- ModuleScript: แปลงมาโครระบบเป็นโมดูลที่สามารถ require ได้ +local MacroModule = {} +MacroModule.__index = MacroModule --- ตัวแปรหลัก +-- ภายใน state (ไม่ใช่ global) local recording = false local playing = false local macroData = {} local startTime = 0 local looping = false local hookInstalled = false -local oldNamecall +local oldNamecall = nil --- ตัวแปรสำหรับแสดงสถานะ -local statusText = "🟢 พร้อมใช้งาน" local eventsCount = 0 local durationTime = 0 --- พาธโฟลเดอร์สำหรับมาโคร local BASE_FOLDER = "ATGHUB_Macro" local SUB_FOLDER = BASE_FOLDER .. "/Anime Last Stand" --- ตรวจสอบและสร้างโฟลเดอร์ (ถ้ามีฟังก์ชัน makefolder) +-- dependencies (จะถูกเซ็ตโดย Init) +local Tabs, Fluent, HttpService, StatusUpdater + +-- public: เก็บ UI objects ที่สร้างขึ้น (ให้สคริปต์เรียกอ่าน/แก้ได้) +MacroModule.Options = {} + +-- ตรวจสอบ/สร้างโฟลเดอร์ local function ensureMacroFolders() pcall(function() if makefolder then - -- ถ้า isfolder มี ให้เช็คก่อนสร้าง (บาง exploit มีบางอันไม่มี) if isfolder then if not isfolder(BASE_FOLDER) then makefolder(BASE_FOLDER) @@ -33,7 +36,6 @@ local function ensureMacroFolders() makefolder(SUB_FOLDER) end else - -- ถ้าไม่มี isfolder แต่มี makefolder ให้เรียกสร้างเผื่อไว้ makefolder(BASE_FOLDER) makefolder(SUB_FOLDER) end @@ -41,54 +43,43 @@ local function ensureMacroFolders() end) end --- ฟังก์ชันแปลง Arguments +-- ฟังก์ชันช่วยอัปเดท status (ใช้ StatusUpdater ถ้ามี, ถ้าไม่จะพยายามอัปเดท Tabs.Main แบบเดิมใน pcall) +local function updateStatus() + local text = string.format("📊 Events: %d\n⏱️ Duration: %.3fs\n🔗 Hook: %s", eventsCount, durationTime, (hookInstalled and "Active ✓" or "Inactive ✗")) + if StatusUpdater and type(StatusUpdater) == "function" then + pcall(StatusUpdater, text) + return + end + if Tabs and Tabs.Main then + pcall(function() + -- พยายามอัปเดทพื้นที่ตามโครงเดิม (ถ้ามี) + Tabs.Main:GetChildren()[1]:GetChildren()[1]:GetChildren()[2]:SetValue(text) + end) + end +end + +-- Serialize / Deserialize arguments local function serializeArg(arg) local argType = typeof(arg) - if argType == "Instance" then - return { - type = "Instance", - path = arg:GetFullName(), - className = arg.ClassName - } + return { type = "Instance", path = arg:GetFullName(), className = arg.ClassName } elseif argType == "CFrame" then - return { - type = "CFrame", - components = {arg:GetComponents()} - } + return { type = "CFrame", components = {arg:GetComponents()} } elseif argType == "Vector3" then - return { - type = "Vector3", - x = arg.X, y = arg.Y, z = arg.Z - } + return { type = "Vector3", x = arg.X, y = arg.Y, z = arg.Z } elseif argType == "Color3" then - return { - type = "Color3", - r = arg.R, g = arg.G, b = arg.B - } + return { type = "Color3", r = arg.R, g = arg.G, b = arg.B } elseif argType == "table" then local serialized = {} - for k, v in pairs(arg) do - serialized[k] = serializeArg(v) - end - return { - type = "table", - data = serialized - } + for k, v in pairs(arg) do serialized[k] = serializeArg(v) end + return { type = "table", data = serialized } else - return { - type = argType, - value = arg - } + return { type = argType, value = arg } end end --- ฟังก์ชันแปลงกลับ local function deserializeArg(data) - if type(data) ~= "table" or not data.type then - return data - end - + if type(data) ~= "table" or not data.type then return data end if data.type == "Instance" then local success, result = pcall(function() local obj = game @@ -108,37 +99,28 @@ local function deserializeArg(data) return Color3.new(data.r, data.g, data.b) elseif data.type == "table" then local result = {} - for k, v in pairs(data.data) do - result[k] = deserializeArg(v) - end + for k, v in pairs(data.data) do result[k] = deserializeArg(v) end return result else return data.value end end --- ฟังก์ชันติดตั้ง Hook แบบ SimpleSpy -local function installHook() +-- ติดตั้ง Hook แบบ SimpleSpy +function MacroModule.InstallHook() if hookInstalled then return true end - - local success, err = pcall(function() - -- ใช้วิธีการ Hook แบบเดียวกับ SimpleSpy + local ok, err = pcall(function() + -- require hookmetamethod & getnamecallmethod ใน exploit environment oldNamecall = hookmetamethod(game, "__namecall", function(self, ...) local method = getnamecallmethod() local args = {...} - - -- จับ FireServer และ InvokeServer + if (method == "FireServer" or method == "InvokeServer") and recording then - if self:IsA("RemoteEvent") or self:IsA("RemoteFunction") then + if (self and (self:IsA and (self:IsA("RemoteEvent") or self:IsA("RemoteFunction")))) then local currentTime = tick() - startTime local serializedArgs = {} - - -- Serialize arguments - for i, arg in ipairs(args) do - serializedArgs[i] = serializeArg(arg) - end - - -- บันทึก + for i, arg in ipairs(args) do serializedArgs[i] = serializeArg(arg) end + table.insert(macroData, { time = currentTime, remoteName = self.Name, @@ -146,262 +128,181 @@ local function installHook() remoteType = method, args = serializedArgs }) - - -- อัพเดท UI + eventsCount = #macroData durationTime = currentTime - - -- อัพเดท Paragraph (ถ้ามี) - if Tabs.Main then - local successSet = pcall(function() - Tabs.Main:GetChildren()[1]:GetChildren()[1]:GetChildren()[2]:SetValue( - string.format("📊 Events: %d\n⏱️ Duration: %.3fs\n🔗 Hook: Active ✓", eventsCount, durationTime) - ) - end) - end + updateStatus() end end - - -- ส่งต่อการเรียกไปยัง Server ตามปกติ + + -- ส่งต่อการเรียก return oldNamecall(self, ...) end) - + hookInstalled = true return true end) - - if not success then + if not ok then warn("⚠️ Hook installation failed: " .. tostring(err)) return false end - + updateStatus() return true end --- ฟังก์ชันเล่นมาโคร -local function playMacro(loop) +-- เล่นมาโคร (loop ถ้า loop = true) +function MacroModule.PlayMacro(loop) if #macroData == 0 then - Fluent:Notify({ - Title = "Macro System", - Content = "❌ ไม่มีมาโครให้เล่น!", - Duration = 3 - }) + if Fluent then + pcall(function() + Fluent:Notify({ Title = "Macro System", Content = "❌ ไม่มีมาโครให้เล่น!", Duration = 3 }) + end) + end return end - + playing = true looping = loop or false - + task.spawn(function() repeat local playStart = tick() - - for i, action in ipairs(macroData) do + for _, action in ipairs(macroData) do if not playing then break end - - -- รอให้ถึงเวลา + local targetTime = playStart + action.time local waitTime = targetTime - tick() - if waitTime > 0 then - task.wait(waitTime) - end - + if waitTime > 0 then task.wait(waitTime) end if not playing then break end - - -- เล่น Remote - local success, err = pcall(function() + + pcall(function() local remote = game for part in action.remotePath:gmatch("[^.]+") do if part ~= "game" then remote = remote:FindFirstChild(part) or remote:WaitForChild(part, 2) end end - if remote then local deserializedArgs = {} - for i, arg in ipairs(action.args) do - deserializedArgs[i] = deserializeArg(arg) - end - - if action.remoteType == "FireServer" then + for i, arg in ipairs(action.args) do deserializedArgs[i] = deserializeArg(arg) end + if action.remoteType == "FireServer" and remote.FireServer then remote:FireServer(unpack(deserializedArgs)) - else + elseif action.remoteType == "InvokeServer" and remote.InvokeServer then remote:InvokeServer(unpack(deserializedArgs)) end end end) end - - if looping and playing then - task.wait(0.5) - end + + if looping and playing then task.wait(0.5) end until not looping or not playing - + playing = false end) end --- ฟังก์ชันบันทึกไฟล์ (เก็บใน ATGHUB_Macro/Anime Last Stand/.json) -local function saveMacro(filename) +function MacroModule.StopPlaying() + looping = false + playing = false + MacroModule.Options.AutoPlayToggle and MacroModule.Options.AutoPlayToggle:SetValue(false) +end + +-- Save macro ลงไฟล์หรือคัดลอกไป clipboard +function MacroModule.SaveMacro(filename) if #macroData == 0 then - Fluent:Notify({ - Title = "Macro System", - Content = "❌ ไม่มีข้อมูลมาโครให้บันทึก!", - Duration = 3 - }) + pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "❌ ไม่มีข้อมูลมาโครให้บันทึก!", Duration = 3 }) end end) return false end - if not filename or filename == "" then - Fluent:Notify({ - Title = "Macro System", - Content = "❌ กรุณากรอกชื่อไฟล์!", - Duration = 3 - }) + pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "❌ กรุณากรอกชื่อไฟล์!", Duration = 3 }) end end) return false end - + ensureMacroFolders() - local success, result = pcall(function() - local data = { - macroData = macroData, - timestamp = os.time(), - totalEvents = #macroData, - totalDuration = durationTime - } - + local data = { macroData = macroData, timestamp = os.time(), totalEvents = #macroData, totalDuration = durationTime } local json = HttpService:JSONEncode(data) - if writefile then - -- บันทึกลงไฟล์ local fullPath = SUB_FOLDER .. "/" .. filename .. ".json" writefile(fullPath, json) return true else - -- แสดงข้อมูลให้คัดลอก setclipboard(json) return "clipboard" end end) - + if success then if result == "clipboard" then - Fluent:Notify({ - Title = "Macro System", - Content = "📋 บันทึกแล้ว! ข้อมูลถูกคัดลอกไปยังคลิปบอร์ด", - Duration = 5 - }) + pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "📋 บันทึกแล้ว! ข้อมูลถูกคัดลอกไปยังคลิปบอร์ด", Duration = 5 }) end end) else - Fluent:Notify({ - Title = "Macro System", - Content = "💾 บันทึกไฟล์สำเร็จ!", - Duration = 3 - }) + pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "💾 บันทึกไฟล์สำเร็จ!", Duration = 3 }) end end) end return true else - Fluent:Notify({ - Title = "Macro System", - Content = "❌ การบันทึกล้มเหลว: " .. tostring(result), - Duration = 5 - }) + pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "❌ การบันทึกล้มเหลว: " .. tostring(result), Duration = 5 }) end end) return false end end --- ฟังก์ชันโหลดไฟล์ -local function loadMacro(filename) +-- Load macro จากไฟล์ +function MacroModule.LoadMacro(filename) if not filename or filename == "" then - Fluent:Notify({ - Title = "Macro System", - Content = "❌ กรุณาเลือกไฟล์!", - Duration = 3 - }) + pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "❌ กรุณาเลือกไฟล์!", Duration = 3 }) end end) return false end - + local success, result = pcall(function() if readfile then local fullPath = SUB_FOLDER .. "/" .. filename .. ".json" - if not isfile then - -- ถ้าไม่มี isfile ให้ลอง readfile โดยจับ error แทน - local content = readfile(fullPath) - local data = HttpService:JSONDecode(content) - return data - else - if not isfile(fullPath) then - return nil, "ไฟล์ไม่พบ" - end - local content = readfile(fullPath) - local data = HttpService:JSONDecode(content) - return data + if isfile and not isfile(fullPath) then + return nil, "ไฟล์ไม่พบ" end + local content = readfile(fullPath) + local data = HttpService:JSONDecode(content) + return data else return nil, "ระบบไฟล์ไม่รองรับ" end end) - + if success and result then macroData = result.macroData or {} eventsCount = #macroData durationTime = result.totalDuration or 0 - - -- อัพเดท UI - if Tabs.Main then - pcall(function() - Tabs.Main:GetChildren()[1]:GetChildren()[1]:GetChildren()[2]:SetValue( - string.format("📊 Events: %d\n⏱️ Duration: %.3fs\n🔗 Hook: Active ✓", eventsCount, durationTime) - ) - end) - end - - Fluent:Notify({ - Title = "Macro System", - Content = "✅ โหลดมาโครสำเร็จ!", - Duration = 3 - }) + updateStatus() + pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "✅ โหลดมาโครสำเร็จ!", Duration = 3 }) end end) return true else - Fluent:Notify({ - Title = "Macro System", - Content = "❌ โหลดไฟล์ล้มเหลว: " .. tostring(result), - Duration = 5 - }) + pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "❌ โหลดไฟล์ล้มเหลว: " .. tostring(result), Duration = 5 }) end end) return false end end --- ฟังก์ชันรับรายการไฟล์จากโฟลเดอร์ ATGHUB_Macro/Anime Last Stand -local function getMacroFiles() +-- ดึงรายการไฟล์จากโฟลเดอร์ +function MacroModule.GetMacroFiles() ensureMacroFolders() if not listfiles then return {"ไม่มีระบบไฟล์"} end - local files = {} - local success, result = pcall(function() + local ok, res = pcall(function() local all = listfiles(SUB_FOLDER) for _, file in pairs(all) do - -- หาชื่อไฟล์สุดท้ายก่อน .json local name = file:match("([^/\\]+)%.json$") - if name and name ~= "" then - table.insert(files, name) - end + if name and name ~= "" then table.insert(files, name) end end return files end) - - if success and #files > 0 then - return result - else - return {"ไม่มีไฟล์ที่บันทึกไว้"} - end + if ok and #res > 0 then return res end + return {"ไม่มีไฟล์ที่บันทึกไว้"} end --- สร้าง UI ในแท็บ Main -do - -- Input สำหรับกรอกชื่อไฟล์ +-- สร้าง UI ใน Tabs.Macro และผูก callback (Init จะเรียก) +function MacroModule.SetupUI() + assert(Tabs and Tabs.Macro, "MacroModule.Init: ต้องส่ง Tabs ที่มี Tabs.Macro") + local Section = Tabs.Macro:AddSection("Macro") - FilenameInput = Tabs.Macro:AddInput("FilenameInput", { + + MacroModule.Options.FilenameInput = Tabs.Macro:AddInput("FilenameInput", { Title = "ชื่อไฟล์", Default = "", Placeholder = "กรอกชื่อไฟล์ (ไม่ต้องมี .json)", @@ -409,148 +310,101 @@ do Finished = false, }) - -- Dropdown สำหรับเลือกไฟล์ - FileDropdown = Tabs.Macro:AddDropdown("FileDropdown", { + MacroModule.Options.FileDropdown = Tabs.Macro:AddDropdown("FileDropdown", { Title = "เลือกไฟล์มาโคร", - Values = getMacroFiles(), + Values = MacroModule.GetMacroFiles(), Multi = false, Default = 1, }) - -- Button สร้างไฟล์ Tabs.Macro:AddButton({ Title = "💾 บันทึกมาโคร", Description = "บันทึกมาโครปัจจุบันลงไฟล์", Callback = function() - local filename = Options.FilenameInput.Value - if filename == "" then + local filename = MacroModule.Options.FilenameInput.Value + if filename == "" or not filename then filename = os.date("macro_%Y%m%d_%H%M%S") end - saveMacro(filename) - - -- อัพเดท dropdown - pcall(function() - FileDropdown:SetValues(getMacroFiles()) - end) + MacroModule.SaveMacro(filename) + pcall(function() MacroModule.Options.FileDropdown:SetValues(MacroModule.GetMacroFiles()) end) end }) - -- Toggle บันทึกมาโคร - local RecordToggle = Tabs.Macro:AddToggle("RecordToggle", { + MacroModule.Options.RecordToggle = Tabs.Macro:AddToggle("RecordToggle", { Title = "⏺️ บันทึกมาโคร", Description = "เริ่ม/หยุด การบันทึกมาโคร", Default = false }) - RecordToggle:OnChanged(function() - if Options.RecordToggle.Value then - -- เริ่มบันทึก + MacroModule.Options.RecordToggle:OnChanged(function() + if MacroModule.Options.RecordToggle.Value then if not hookInstalled then - if not installHook() then - Fluent:Notify({ - Title = "Macro System", - Content = "❌ ติดตั้ง Hook ไม่สำเร็จ!", - Duration = 3 - }) - RecordToggle:SetValue(false) + if not MacroModule.InstallHook() then + pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "❌ ติดตั้ง Hook ไม่สำเร็จ!", Duration = 3 }) end end) + MacroModule.Options.RecordToggle:SetValue(false) return end end - recording = true macroData = {} startTime = tick() eventsCount = 0 durationTime = 0 - - Fluent:Notify({ - Title = "Macro System", - Content = "🎬 เริ่มบันทึกมาโครแล้ว", - Duration = 2 - }) + updateStatus() + pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "🎬 เริ่มบันทึกมาโครแล้ว", Duration = 2 }) end end) else - -- หยุดบันทึก recording = false - Fluent:Notify({ - Title = "Macro System", - Content = string.format("✅ บันทึกเสร็จสิ้น: %d events", #macroData), - Duration = 3 - }) + pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = string.format("✅ บันทึกเสร็จสิ้น: %d events", #macroData), Duration = 3 }) end end) + updateStatus() end end) - -- Toggle เล่นมาโครอัตโนมัติ - local AutoPlayToggle = Tabs.Macro:AddToggle("AutoPlayToggle", { + MacroModule.Options.AutoPlayToggle = Tabs.Macro:AddToggle("AutoPlayToggle", { Title = "🔁 เล่นมาโครอัตโนมัติ", Description = "เล่นมาโครวนลูปเมื่อเปิด", Default = false }) - AutoPlayToggle:OnChanged(function() - if Options.AutoPlayToggle.Value then + MacroModule.Options.AutoPlayToggle:OnChanged(function() + if MacroModule.Options.AutoPlayToggle.Value then if not playing then - playMacro(true) - Fluent:Notify({ - Title = "Macro System", - Content = "🔁 เริ่มเล่นมาโครแบบวนลูป", - Duration = 2 - }) + MacroModule.PlayMacro(true) + pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "🔁 เริ่มเล่นมาโครแบบวนลูป", Duration = 2 }) end end) end else - looping = false - playing = false - Fluent:Notify({ - Title = "Macro System", - Content = "⏹️ หยุดเล่นมาโคร", - Duration = 2 - }) + MacroModule.StopPlaying() + pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "⏹️ หยุดเล่นมาโคร", Duration = 2 }) end end) end end) - -- Button เล่นมาโคร Tabs.Macro:AddButton({ Title = "▶️ เล่นมาโคร", Description = "เล่นมาโครหนึ่งรอบ", Callback = function() if playing then - Fluent:Notify({ - Title = "Macro System", - Content = "⏸️ หยุดเล่นมาโครชั่วคราว", - Duration = 2 - }) - playing = false - looping = false - Options.AutoPlayToggle:SetValue(false) + pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "⏸️ หยุดเล่นมาโครชั่วคราว", Duration = 2 }) end end) + MacroModule.StopPlaying() + MacroModule.Options.AutoPlayToggle:SetValue(false) else - playMacro(false) - Fluent:Notify({ - Title = "Macro System", - Content = "▶️ เริ่มเล่นมาโครหนึ่งรอบ", - Duration = 2 - }) + MacroModule.PlayMacro(false) + pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "▶️ เริ่มเล่นมาโครหนึ่งรอบ", Duration = 2 }) end end) end end }) - -- Button โหลดไฟล์ Tabs.Macro:AddButton({ Title = "📂 โหลดมาโคร", Description = "โหลดมาโครจากไฟล์", Callback = function() - local selectedFile = Options.FileDropdown.Value + local selectedFile = MacroModule.Options.FileDropdown.Value if selectedFile and selectedFile ~= "ไม่มีไฟล์ที่บันทึกไว้" and selectedFile ~= "ไม่มีระบบไฟล์" then - loadMacro(selectedFile) + MacroModule.LoadMacro(selectedFile) else - Fluent:Notify({ - Title = "Macro System", - Content = "❌ กรุณาเลือกไฟล์ที่ถูกต้อง", - Duration = 3 - }) + pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "❌ กรุณาเลือกไฟล์ที่ถูกต้อง", Duration = 3 }) end end) end end }) - -- Button ล้างข้อมูล Tabs.Macro:AddButton({ Title = "🗑️ ล้างข้อมูล", Description = "ล้างข้อมูลมาโครปัจจุบัน", @@ -558,23 +412,59 @@ do macroData = {} eventsCount = 0 durationTime = 0 + updateStatus() end }) -end -do Tabs.Macro:AddButton({ Title = "อัพเดทรายการไฟล์", Description = "รีเฟรชรายการไฟล์มาโคร (จากโฟลเดอร์ ATGHUB_Macro/Anime Last Stand)", Callback = function() - pcall(function() - FileDropdown:SetValues(getMacroFiles()) - end) - Fluent:Notify({ - Title = "Macro System", - Content = "🔄 อัพเดทรายการไฟล์แล้ว", - Duration = 2 - }) + pcall(function() MacroModule.Options.FileDropdown:SetValues(MacroModule.GetMacroFiles()) end) + pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "🔄 อัพเดทรายการไฟล์แล้ว", Duration = 2 }) end end) end }) end + +-- เริ่มต้นโมดูล: ส่ง dependencies เป็น table +-- ตัวอย่าง dependencies: { Tabs = Tabs, Fluent = Fluent, HttpService = game:GetService("HttpService"), StatusUpdater = function(txt) ... end } +function MacroModule.Init(deps) + assert(type(deps) == "table", "MacroModule.Init expects a table of dependencies") + Tabs = deps.Tabs + Fluent = deps.Fluent + HttpService = deps.HttpService or game:GetService("HttpService") + StatusUpdater = deps.StatusUpdater + + -- สร้าง UI + MacroModule.SetupUI() + updateStatus() + + -- คืนค่า module (พร้อม Options) + return MacroModule +end + +-- ผลักข้อมูลออก (ให้สคริปต์เรียกใช้ได้) +function MacroModule.GetState() + return { + recording = recording, + playing = playing, + macroData = macroData, + eventsCount = eventsCount, + durationTime = durationTime, + hookInstalled = hookInstalled + } +end + +-- expose เพิ่มเติม (ถ้าต้องการเรียกตรงๆ) +MacroModule.StartRecording = function() if not hookInstalled then MacroModule.InstallHook() end; MacroModule.Options.RecordToggle:SetValue(true) end +MacroModule.StopRecording = function() MacroModule.Options.RecordToggle:SetValue(false) end +MacroModule.GetMacroFiles = MacroModule.GetMacroFiles +MacroModule.SaveMacro = MacroModule.SaveMacro +MacroModule.LoadMacro = MacroModule.LoadMacro +MacroModule.PlayOnce = function() MacroModule.PlayMacro(false) end +MacroModule.PlayLoop = function() MacroModule.PlayMacro(true) end +MacroModule.Stop = MacroModule.StopPlaying +MacroModule.InstallHook = MacroModule.InstallHook +MacroModule.UpdateStatus = updateStatus + +return MacroModule From 31e73a835a4fb90eaf544b071e890dce793bf735 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 30 Oct 2025 22:30:29 +0700 Subject: [PATCH 30/76] Update Macro.lua --- Macro.lua | 516 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 364 insertions(+), 152 deletions(-) diff --git a/Macro.lua b/Macro.lua index ee171bf..58576bc 100644 --- a/Macro.lua +++ b/Macro.lua @@ -1,9 +1,11 @@ -- MacroModule.lua --- ModuleScript: แปลงมาโครระบบเป็นโมดูลที่สามารถ require ได้ +local HttpService = game:GetService("HttpService") +local Workspace = game:GetService("Workspace") + local MacroModule = {} MacroModule.__index = MacroModule --- ภายใน state (ไม่ใช่ global) +-- internal state local recording = false local playing = false local macroData = {} @@ -15,50 +17,137 @@ local oldNamecall = nil local eventsCount = 0 local durationTime = 0 -local BASE_FOLDER = "ATGHUB_Macro" -local SUB_FOLDER = BASE_FOLDER .. "/Anime Last Stand" +-- default folder structure (can be overridden with SetFolder) +MacroModule.FolderRoot = "ATGHUB_Macro" +local SUB_FOLDER = MacroModule.FolderRoot .. "/Anime Last Stand" + +-- compatibility / config fields (SaveManager-like) +MacroModule.Library = nil +MacroModule.Options = {} -- populated when SetLibrary is called (Library.Options) +MacroModule.Ignore = {} +local IGNORE_THEME = false + +-- helpers: sanitize & filesystem 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 --- dependencies (จะถูกเซ็ตโดย Init) -local Tabs, Fluent, HttpService, StatusUpdater +local function getPlaceId() + local ok, id = pcall(function() return tostring(game.PlaceId) end) + if ok and id then return id end + return "UnknownPlace" +end --- public: เก็บ UI objects ที่สร้างขึ้น (ให้สคริปต์เรียกอ่าน/แก้ได้) -MacroModule.Options = {} +local function getMapName() + local ok, map = pcall(function() return Workspace:FindFirstChild("Map") end) + if ok and map and map:IsA("Instance") then + return sanitizeFilename(map.Name) + end + local ok2, wname = pcall(function() return Workspace.Name end) + if ok2 and wname then return sanitizeFilename(wname) end + return "UnknownMap" +end --- ตรวจสอบ/สร้างโฟลเดอร์ -local function ensureMacroFolders() - pcall(function() - if makefolder then - if isfolder then - if not isfolder(BASE_FOLDER) then - makefolder(BASE_FOLDER) - end - if not isfolder(SUB_FOLDER) then - makefolder(SUB_FOLDER) +local function ensureFolder(path) + if not isfolder then return end + if not isfolder(path) then + makefolder(path) + end +end + +-- build tree similar to SaveManager: ///settings +function MacroModule:BuildFolderTree() + local root = self.FolderRoot or "ATGHUB_Macro" + ensureFolder(root) + + local placeId = getPlaceId() + local placeFolder = root .. "/" .. placeId + ensureFolder(placeFolder) + + local mapName = getMapName() + local mapFolder = placeFolder .. "/" .. mapName + ensureFolder(mapFolder) + + local settingsFolder = mapFolder .. "/settings" + ensureFolder(settingsFolder) + + -- migrate legacy if exists (/settings) + local legacySettingsFolder = root .. "/settings" + if isfolder and 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 = settingsFolder .. "/" .. 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 - else - makefolder(BASE_FOLDER) - makefolder(SUB_FOLDER) end end - end) + + local autopath = legacySettingsFolder .. "/autoload.txt" + if isfile(autopath) then + local autodata = readfile(autopath) + local destAuto = settingsFolder .. "/autoload.txt" + if not isfile(destAuto) then + pcall(writefile, destAuto, autodata) + end + end + end +end + +local function getConfigsFolder(self) + local root = self.FolderRoot or MacroModule.FolderRoot or "ATGHUB_Macro" + local placeId = getPlaceId() + local mapName = getMapName() + return root .. "/" .. placeId .. "/" .. mapName .. "/settings" +end + +local function getConfigFilePath(self, name) + local folder = getConfigsFolder(self) + return folder .. "/" .. name .. ".json" +end + +-- status updater (uses Library:Notify for messages and Library.Options area if provided) +local function notify(title, content, duration) + if MacroModule.Library and MacroModule.Library.Notify then + pcall(function() + MacroModule.Library:Notify({ Title = title or "Macro System", Content = content or "", Duration = duration or 3 }) + end) + else + -- fallback + print(("[MacroModule] %s: %s"):format(title or "Info", tostring(content))) + end end --- ฟังก์ชันช่วยอัปเดท status (ใช้ StatusUpdater ถ้ามี, ถ้าไม่จะพยายามอัปเดท Tabs.Main แบบเดิมใน pcall) local function updateStatus() local text = string.format("📊 Events: %d\n⏱️ Duration: %.3fs\n🔗 Hook: %s", eventsCount, durationTime, (hookInstalled and "Active ✓" or "Inactive ✗")) - if StatusUpdater and type(StatusUpdater) == "function" then - pcall(StatusUpdater, text) - return - end - if Tabs and Tabs.Main then + -- try to update Library.Options location if exists (best-effort) + if MacroModule.Library and MacroModule.Library.Options and MacroModule.Library.Options.StatusText then pcall(function() - -- พยายามอัปเดทพื้นที่ตามโครงเดิม (ถ้ามี) - Tabs.Main:GetChildren()[1]:GetChildren()[1]:GetChildren()[2]:SetValue(text) + if MacroModule.Library.Options.StatusText.SetValue then + MacroModule.Library.Options.StatusText:SetValue(text) + end end) + return + end + -- otherwise attempt to set known Tab/Main update if present in Options table + if MacroModule.Options and MacroModule.Options.Status and MacroModule.Options.Status.SetValue then + pcall(function() MacroModule.Options.Status:SetValue(text) end) end end --- Serialize / Deserialize arguments +-- Serialization helpers (same as earlier) local function serializeArg(arg) local argType = typeof(arg) if argType == "Instance" then @@ -106,15 +195,13 @@ local function deserializeArg(data) end end --- ติดตั้ง Hook แบบ SimpleSpy -function MacroModule.InstallHook() +-- Hook installer (SimpleSpy-style) +function MacroModule:InstallHook() if hookInstalled then return true end local ok, err = pcall(function() - -- require hookmetamethod & getnamecallmethod ใน exploit environment oldNamecall = hookmetamethod(game, "__namecall", function(self, ...) local method = getnamecallmethod() local args = {...} - if (method == "FireServer" or method == "InvokeServer") and recording then if (self and (self:IsA and (self:IsA("RemoteEvent") or self:IsA("RemoteFunction")))) then local currentTime = tick() - startTime @@ -134,30 +221,23 @@ function MacroModule.InstallHook() updateStatus() end end - - -- ส่งต่อการเรียก return oldNamecall(self, ...) end) - hookInstalled = true return true end) if not ok then - warn("⚠️ Hook installation failed: " .. tostring(err)) + warn("Hook installation failed: " .. tostring(err)) return false end updateStatus() return true end --- เล่นมาโคร (loop ถ้า loop = true) -function MacroModule.PlayMacro(loop) +-- Play macro +function MacroModule:PlayMacro(loop) if #macroData == 0 then - if Fluent then - pcall(function() - Fluent:Notify({ Title = "Macro System", Content = "❌ ไม่มีมาโครให้เล่น!", Duration = 3 }) - end) - end + notify("Macro System", "❌ ไม่มีมาโครให้เล่น!", 3) return end @@ -169,7 +249,6 @@ function MacroModule.PlayMacro(loop) local playStart = tick() for _, action in ipairs(macroData) do if not playing then break end - local targetTime = playStart + action.time local waitTime = targetTime - tick() if waitTime > 0 then task.wait(waitTime) end @@ -201,29 +280,40 @@ function MacroModule.PlayMacro(loop) end) end -function MacroModule.StopPlaying() +function MacroModule:StopPlaying() looping = false playing = false - MacroModule.Options.AutoPlayToggle and MacroModule.Options.AutoPlayToggle:SetValue(false) + if MacroModule.Options and MacroModule.Options.AutoPlayToggle and MacroModule.Options.AutoPlayToggle.SetValue then + pcall(function() MacroModule.Options.AutoPlayToggle:SetValue(false) end) + end end --- Save macro ลงไฟล์หรือคัดลอกไป clipboard -function MacroModule.SaveMacro(filename) +-- Save macro to file or clipboard +function MacroModule:SaveMacro(filename) if #macroData == 0 then - pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "❌ ไม่มีข้อมูลมาโครให้บันทึก!", Duration = 3 }) end end) + notify("Macro System", "❌ ไม่มีข้อมูลมาโครให้บันทึก!", 3) return false end if not filename or filename == "" then - pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "❌ กรุณากรอกชื่อไฟล์!", Duration = 3 }) end end) + notify("Macro System", "❌ กรุณากรอกชื่อไฟล์!", 3) return false end - ensureMacroFolders() + -- build folder tree for configs + self:BuildFolderTree() local success, result = pcall(function() - local data = { macroData = macroData, timestamp = os.time(), totalEvents = #macroData, totalDuration = durationTime } + local data = { + macroData = macroData, + timestamp = os.time(), + totalEvents = #macroData, + totalDuration = durationTime + } local json = HttpService:JSONEncode(data) if writefile then - local fullPath = SUB_FOLDER .. "/" .. filename .. ".json" + local fullPath = getConfigFilePath(self, filename) + -- ensure folder + local folder = fullPath:match("^(.*)/[^/]+$") + if folder then ensureFolder(folder) end writefile(fullPath, json) return true else @@ -234,75 +324,119 @@ function MacroModule.SaveMacro(filename) if success then if result == "clipboard" then - pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "📋 บันทึกแล้ว! ข้อมูลถูกคัดลอกไปยังคลิปบอร์ด", Duration = 5 }) end end) + notify("Macro System", "📋 บันทึกแล้ว! ข้อมูลถูกคัดลอกไปยังคลิปบอร์ด", 5) else - pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "💾 บันทึกไฟล์สำเร็จ!", Duration = 3 }) end end) + notify("Macro System", "💾 บันทึกไฟล์สำเร็จ!", 3) end return true else - pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "❌ การบันทึกล้มเหลว: " .. tostring(result), Duration = 5 }) end end) + notify("Macro System", "❌ การบันทึกล้มเหลว: " .. tostring(result), 5) return false end end --- Load macro จากไฟล์ -function MacroModule.LoadMacro(filename) +-- Load macro from file +function MacroModule:LoadMacro(filename) if not filename or filename == "" then - pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "❌ กรุณาเลือกไฟล์!", Duration = 3 }) end end) + notify("Macro System", "❌ กรุณาเลือกไฟล์!", 3) return false end - local success, result = pcall(function() + local ok, res = pcall(function() if readfile then - local fullPath = SUB_FOLDER .. "/" .. filename .. ".json" + local fullPath = getConfigFilePath(self, filename) if isfile and not isfile(fullPath) then return nil, "ไฟล์ไม่พบ" end local content = readfile(fullPath) - local data = HttpService:JSONDecode(content) - return data + return HttpService:JSONDecode(content) else return nil, "ระบบไฟล์ไม่รองรับ" end end) - if success and result then - macroData = result.macroData or {} + if ok and res then + macroData = res.macroData or {} eventsCount = #macroData - durationTime = result.totalDuration or 0 + durationTime = res.totalDuration or 0 updateStatus() - pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "✅ โหลดมาโครสำเร็จ!", Duration = 3 }) end end) + notify("Macro System", "✅ โหลดมาโครสำเร็จ!", 3) return true else - pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "❌ โหลดไฟล์ล้มเหลว: " .. tostring(result), Duration = 5 }) end end) + notify("Macro System", "❌ โหลดไฟล์ล้มเหลว: " .. tostring(res), 5) return false end end --- ดึงรายการไฟล์จากโฟลเดอร์ -function MacroModule.GetMacroFiles() - ensureMacroFolders() - if not listfiles then return {"ไม่มีระบบไฟล์"} end - local files = {} - local ok, res = pcall(function() - local all = listfiles(SUB_FOLDER) - for _, file in pairs(all) do +-- refresh config list +function MacroModule:RefreshConfigList() + self:BuildFolderTree() + local folder = getConfigsFolder(self) + if not isfolder or 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 ~= "" then table.insert(files, name) end + if name and name ~= "options" then + table.insert(out, name) + end end - return files - end) - if ok and #res > 0 then return res end - return {"ไม่มีไฟล์ที่บันทึกไว้"} + end + return out end --- สร้าง UI ใน Tabs.Macro และผูก callback (Init จะเรียก) -function MacroModule.SetupUI() - assert(Tabs and Tabs.Macro, "MacroModule.Init: ต้องส่ง Tabs ที่มี Tabs.Macro") +function MacroModule:LoadAutoloadConfig() + local autopath = getConfigsFolder(self) .. "/autoload.txt" + if isfile and isfile(autopath) then + local name = readfile(autopath) + local success, err = self:LoadMacro(name) + if not success then + notify("Interface", "Failed to load autoload config: " .. tostring(err), 7) + return + end + notify("Interface", string.format("Auto loaded config %q", name), 7) + end +end + +-- API: compatibility methods +function MacroModule:SetLibrary(library) + self.Library = library + self.Options = library.Options or self.Options +end + +function MacroModule:SetFolder(folder) + folder = tostring(folder or self.FolderRoot or "ATGHUB_Macro") + self.FolderRoot = folder + -- update SUB_FOLDER style for backward compat (keep default subpath if not explicit settings path) + SUB_FOLDER = folder + self:BuildFolderTree() +end + +function MacroModule:SetIgnoreIndexes(list) + if type(list) ~= "table" then return end + for _, key in next, list do + self.Ignore[key] = true + end +end + +function MacroModule:IgnoreThemeSettings() + self:SetIgnoreIndexes({ "InterfaceTheme", "AcrylicToggle", "TransparentToggle", "MenuKeybind" }) +end + +-- UI builder: BuildInterfaceSection(tab) +function MacroModule:BuildInterfaceSection(tab) + assert(tab, "BuildInterfaceSection expects a tab object") + -- We'll create UI controls under the provided tab + -- store reference to tab-based options for update callbacks + self.Tab = tab - local Section = Tabs.Macro:AddSection("Macro") + -- Try to create a "Macro" section and inputs (best-effort; depends on Fluent API) + local section = tab:AddSection("Macro") - MacroModule.Options.FilenameInput = Tabs.Macro:AddInput("FilenameInput", { + -- Filename input + self.Options.FilenameInput = tab:AddInput("Macro_FilenameInput", { Title = "ชื่อไฟล์", Default = "", Placeholder = "กรอกชื่อไฟล์ (ไม่ต้องมี .json)", @@ -310,38 +444,40 @@ function MacroModule.SetupUI() Finished = false, }) - MacroModule.Options.FileDropdown = Tabs.Macro:AddDropdown("FileDropdown", { + -- File dropdown + self.Options.FileDropdown = tab:AddDropdown("Macro_FileDropdown", { Title = "เลือกไฟล์มาโคร", - Values = MacroModule.GetMacroFiles(), + Values = self:RefreshConfigList(), Multi = false, Default = 1, }) - Tabs.Macro:AddButton({ + tab:AddButton({ Title = "💾 บันทึกมาโคร", Description = "บันทึกมาโครปัจจุบันลงไฟล์", Callback = function() - local filename = MacroModule.Options.FilenameInput.Value + local filename = (self.Options.FilenameInput and self.Options.FilenameInput.Value) or "" if filename == "" or not filename then filename = os.date("macro_%Y%m%d_%H%M%S") end - MacroModule.SaveMacro(filename) - pcall(function() MacroModule.Options.FileDropdown:SetValues(MacroModule.GetMacroFiles()) end) + self:SaveMacro(filename) + pcall(function() self.Options.FileDropdown:SetValues(self:RefreshConfigList()) end) end }) - MacroModule.Options.RecordToggle = Tabs.Macro:AddToggle("RecordToggle", { + -- Record toggle + self.Options.RecordToggle = tab:AddToggle("Macro_RecordToggle", { Title = "⏺️ บันทึกมาโคร", Description = "เริ่ม/หยุด การบันทึกมาโคร", Default = false }) - MacroModule.Options.RecordToggle:OnChanged(function() - if MacroModule.Options.RecordToggle.Value then + self.Options.RecordToggle:OnChanged(function() + if self.Options.RecordToggle.Value then if not hookInstalled then - if not MacroModule.InstallHook() then - pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "❌ ติดตั้ง Hook ไม่สำเร็จ!", Duration = 3 }) end end) - MacroModule.Options.RecordToggle:SetValue(false) + if not self:InstallHook() then + notify("Macro System", "❌ ติดตั้ง Hook ไม่สำเร็จ!", 3) + self.Options.RecordToggle:SetValue(false) return end end @@ -351,61 +487,64 @@ function MacroModule.SetupUI() eventsCount = 0 durationTime = 0 updateStatus() - pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "🎬 เริ่มบันทึกมาโครแล้ว", Duration = 2 }) end end) + notify("Macro System", "🎬 เริ่มบันทึกมาโครแล้ว", 2) else recording = false - pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = string.format("✅ บันทึกเสร็จสิ้น: %d events", #macroData), Duration = 3 }) end end) + notify("Macro System", string.format("✅ บันทึกเสร็จสิ้น: %d events", #macroData), 3) updateStatus() end end) - MacroModule.Options.AutoPlayToggle = Tabs.Macro:AddToggle("AutoPlayToggle", { + -- AutoPlay toggle + self.Options.AutoPlayToggle = tab:AddToggle("Macro_AutoPlayToggle", { Title = "🔁 เล่นมาโครอัตโนมัติ", Description = "เล่นมาโครวนลูปเมื่อเปิด", Default = false }) - MacroModule.Options.AutoPlayToggle:OnChanged(function() - if MacroModule.Options.AutoPlayToggle.Value then + self.Options.AutoPlayToggle:OnChanged(function() + if self.Options.AutoPlayToggle.Value then if not playing then - MacroModule.PlayMacro(true) - pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "🔁 เริ่มเล่นมาโครแบบวนลูป", Duration = 2 }) end end) + self:PlayMacro(true) + notify("Macro System", "🔁 เริ่มเล่นมาโครแบบวนลูป", 2) end else - MacroModule.StopPlaying() - pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "⏹️ หยุดเล่นมาโคร", Duration = 2 }) end end) + self:StopPlaying() + notify("Macro System", "⏹️ หยุดเล่นมาโคร", 2) end end) - Tabs.Macro:AddButton({ + tab:AddButton({ Title = "▶️ เล่นมาโคร", Description = "เล่นมาโครหนึ่งรอบ", Callback = function() if playing then - pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "⏸️ หยุดเล่นมาโครชั่วคราว", Duration = 2 }) end end) - MacroModule.StopPlaying() - MacroModule.Options.AutoPlayToggle:SetValue(false) + notify("Macro System", "⏸️ หยุดเล่นมาโครชั่วคราว", 2) + self:StopPlaying() + if self.Options.AutoPlayToggle and self.Options.AutoPlayToggle.SetValue then + self.Options.AutoPlayToggle:SetValue(false) + end else - MacroModule.PlayMacro(false) - pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "▶️ เริ่มเล่นมาโครหนึ่งรอบ", Duration = 2 }) end end) + self:PlayMacro(false) + notify("Macro System", "▶️ เริ่มเล่นมาโครหนึ่งรอบ", 2) end end }) - Tabs.Macro:AddButton({ + tab:AddButton({ Title = "📂 โหลดมาโคร", Description = "โหลดมาโครจากไฟล์", Callback = function() - local selectedFile = MacroModule.Options.FileDropdown.Value + local selectedFile = (self.Options.FileDropdown and self.Options.FileDropdown.Value) or nil if selectedFile and selectedFile ~= "ไม่มีไฟล์ที่บันทึกไว้" and selectedFile ~= "ไม่มีระบบไฟล์" then - MacroModule.LoadMacro(selectedFile) + self:LoadMacro(selectedFile) else - pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "❌ กรุณาเลือกไฟล์ที่ถูกต้อง", Duration = 3 }) end end) + notify("Macro System", "❌ กรุณาเลือกไฟล์ที่ถูกต้อง", 3) end end }) - Tabs.Macro:AddButton({ + tab:AddButton({ Title = "🗑️ ล้างข้อมูล", Description = "ล้างข้อมูลมาโครปัจจุบัน", Callback = function() @@ -416,55 +555,128 @@ function MacroModule.SetupUI() end }) - Tabs.Macro:AddButton({ + tab:AddButton({ Title = "อัพเดทรายการไฟล์", - Description = "รีเฟรชรายการไฟล์มาโคร (จากโฟลเดอร์ ATGHUB_Macro/Anime Last Stand)", + Description = "รีเฟรชรายการไฟล์มาโคร (จากโฟลเดอร์)", Callback = function() - pcall(function() MacroModule.Options.FileDropdown:SetValues(MacroModule.GetMacroFiles()) end) - pcall(function() if Fluent then Fluent:Notify({ Title = "Macro System", Content = "🔄 อัพเดทรายการไฟล์แล้ว", Duration = 2 }) end end) + pcall(function() self.Options.FileDropdown:SetValues(self:RefreshConfigList()) end) + notify("Macro System", "🔄 อัพเดทรายการไฟล์แล้ว", 2) end }) end --- เริ่มต้นโมดูล: ส่ง dependencies เป็น table --- ตัวอย่าง dependencies: { Tabs = Tabs, Fluent = Fluent, HttpService = game:GetService("HttpService"), StatusUpdater = function(txt) ... end } -function MacroModule.Init(deps) - assert(type(deps) == "table", "MacroModule.Init expects a table of dependencies") - Tabs = deps.Tabs - Fluent = deps.Fluent - HttpService = deps.HttpService or game:GetService("HttpService") - StatusUpdater = deps.StatusUpdater - - -- สร้าง UI - MacroModule.SetupUI() - updateStatus() +-- BuildConfigSection: ให้ UI สำหรับจัดการไฟล์ (Create/Load/Overwrite/Refresh/Autoload) +function MacroModule:BuildConfigSection(tab) + assert(self.Library, "Must set MacroModule.Library via SetLibrary before BuildConfigSection") + + local section = tab:AddSection("Macro Configuration") + + section:AddInput("Macro_ConfigName", { Title = "Config name" }) + section:AddDropdown("Macro_ConfigList", { Title = "Config list", Values = self:RefreshConfigList(), AllowNull = true }) - -- คืนค่า module (พร้อม Options) - return MacroModule + section:AddButton({ + Title = "Create config", + Callback = function() + local name = self.Options.Macro_ConfigName and self.Options.Macro_ConfigName.Value or "" + if name:gsub(" ", "") == "" then + return self.Library:Notify({ Title = "Interface", Content = "Config loader", SubContent = "Invalid config name (empty)", Duration = 7 }) + end + local success, err = self:SaveMacro(name) + if not success then + return self.Library:Notify({ Title = "Interface", Content = "Config loader", SubContent = "Failed to save config: " .. tostring(err), Duration = 7 }) + end + self.Library:Notify({ Title = "Interface", Content = "Config loader", SubContent = string.format("Created config %q", name), Duration = 7 }) + -- refresh list + pcall(function() self.Options.Macro_ConfigList:SetValues(self:RefreshConfigList()); self.Options.Macro_ConfigList:SetValue(nil) end) + end + }) + + section:AddButton({ + Title = "Load config", + Callback = function() + local name = self.Options.Macro_ConfigList and self.Options.Macro_ConfigList.Value + local success, err = self:LoadMacro(name) + if not success then + return self.Library:Notify({ Title = "Interface", Content = "Config loader", SubContent = "Failed to load config: " .. tostring(err), Duration = 7 }) + end + self.Library:Notify({ Title = "Interface", Content = "Config loader", SubContent = string.format("Loaded config %q", name), Duration = 7 }) + end + }) + + section:AddButton({ + Title = "Overwrite config", + Callback = function() + local name = self.Options.Macro_ConfigList and self.Options.Macro_ConfigList.Value + local success, err = self:SaveMacro(name) + if not success then + return self.Library:Notify({ Title = "Interface", Content = "Config loader", SubContent = "Failed to overwrite config: " .. tostring(err), Duration = 7 }) + end + self.Library:Notify({ Title = "Interface", Content = "Config loader", SubContent = string.format("Overwrote config %q", name), Duration = 7 }) + end + }) + + section:AddButton({ Title = "Refresh list", Callback = function() + pcall(function() self.Options.Macro_ConfigList:SetValues(self:RefreshConfigList()); self.Options.Macro_ConfigList:SetValue(nil) end) + end }) + + local AutoloadButton + AutoloadButton = section:AddButton({ + Title = "Set as autoload", + Description = "Current autoload config: none", + Callback = function() + local name = self.Options.Macro_ConfigList and self.Options.Macro_ConfigList.Value + local autopath = getConfigsFolder(self) .. "/autoload.txt" + if writefile then + writefile(autopath, tostring(name)) + AutoloadButton:SetDesc("Current autoload config: " .. tostring(name)) + self.Library:Notify({ Title = "Interface", Content = "Config loader", SubContent = string.format("Set %q to auto load", name), Duration = 7 }) + else + self.Library:Notify({ Title = "Interface", Content = "Config loader", SubContent = "Filesystem not supported", Duration = 7 }) + end + end + }) + + -- populate current autoload desc if exists + local autop = getConfigsFolder(self) .. "/autoload.txt" + if isfile and isfile(autop) then + local name = readfile(autop) + pcall(function() AutoloadButton:SetDesc("Current autoload config: " .. tostring(name)) end) + end + + -- mark ignore indexes for these UI fields (so SaveManager-like behavior) + self:SetIgnoreIndexes({ "Macro_ConfigList", "Macro_ConfigName" }) end --- ผลักข้อมูลออก (ให้สคริปต์เรียกใช้ได้) -function MacroModule.GetState() +-- convenience aliases & Init +function MacroModule:Init(deps) + deps = deps or {} + if deps.Library then self:SetLibrary(deps.Library) end + if deps.Folder then self:SetFolder(deps.Folder) end + return self +end + +-- expose some util getters +function MacroModule:GetState() return { recording = recording, playing = playing, - macroData = macroData, eventsCount = eventsCount, durationTime = durationTime, - hookInstalled = hookInstalled + hookInstalled = hookInstalled, + macros = macroData } end --- expose เพิ่มเติม (ถ้าต้องการเรียกตรงๆ) -MacroModule.StartRecording = function() if not hookInstalled then MacroModule.InstallHook() end; MacroModule.Options.RecordToggle:SetValue(true) end -MacroModule.StopRecording = function() MacroModule.Options.RecordToggle:SetValue(false) end -MacroModule.GetMacroFiles = MacroModule.GetMacroFiles +-- expose methods +MacroModule.InstallHook = MacroModule.InstallHook +MacroModule.PlayMacro = MacroModule.PlayMacro +MacroModule.StopPlaying = MacroModule.StopPlaying MacroModule.SaveMacro = MacroModule.SaveMacro MacroModule.LoadMacro = MacroModule.LoadMacro -MacroModule.PlayOnce = function() MacroModule.PlayMacro(false) end -MacroModule.PlayLoop = function() MacroModule.PlayMacro(true) end -MacroModule.Stop = MacroModule.StopPlaying -MacroModule.InstallHook = MacroModule.InstallHook -MacroModule.UpdateStatus = updateStatus +MacroModule.RefreshConfigList = MacroModule.RefreshConfigList +MacroModule.LoadAutoloadConfig = MacroModule.LoadAutoloadConfig + +-- ensure default folder structure on load +MacroModule:BuildFolderTree() return MacroModule From c380cc0c89ffc022f4dbe0b01d5be80141357099 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 30 Oct 2025 23:14:04 +0700 Subject: [PATCH 31/76] Delete Macro.lua --- Macro.lua | 682 ------------------------------------------------------ 1 file changed, 682 deletions(-) delete mode 100644 Macro.lua diff --git a/Macro.lua b/Macro.lua deleted file mode 100644 index 58576bc..0000000 --- a/Macro.lua +++ /dev/null @@ -1,682 +0,0 @@ --- MacroModule.lua -local HttpService = game:GetService("HttpService") -local Workspace = game:GetService("Workspace") - -local MacroModule = {} -MacroModule.__index = MacroModule - --- internal state -local recording = false -local playing = false -local macroData = {} -local startTime = 0 -local looping = false -local hookInstalled = false -local oldNamecall = nil - -local eventsCount = 0 -local durationTime = 0 - --- default folder structure (can be overridden with SetFolder) -MacroModule.FolderRoot = "ATGHUB_Macro" -local SUB_FOLDER = MacroModule.FolderRoot .. "/Anime Last Stand" - --- compatibility / config fields (SaveManager-like) -MacroModule.Library = nil -MacroModule.Options = {} -- populated when SetLibrary is called (Library.Options) -MacroModule.Ignore = {} -local IGNORE_THEME = false - --- helpers: sanitize & filesystem 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 getMapName() - local ok, map = pcall(function() return Workspace:FindFirstChild("Map") end) - if ok and map and map:IsA("Instance") then - return sanitizeFilename(map.Name) - end - local ok2, wname = pcall(function() return Workspace.Name end) - if ok2 and wname then return sanitizeFilename(wname) end - return "UnknownMap" -end - -local function ensureFolder(path) - if not isfolder then return end - if not isfolder(path) then - makefolder(path) - end -end - --- build tree similar to SaveManager: ///settings -function MacroModule:BuildFolderTree() - local root = self.FolderRoot or "ATGHUB_Macro" - ensureFolder(root) - - local placeId = getPlaceId() - local placeFolder = root .. "/" .. placeId - ensureFolder(placeFolder) - - local mapName = getMapName() - local mapFolder = placeFolder .. "/" .. mapName - ensureFolder(mapFolder) - - local settingsFolder = mapFolder .. "/settings" - ensureFolder(settingsFolder) - - -- migrate legacy if exists (/settings) - local legacySettingsFolder = root .. "/settings" - if isfolder and 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 = settingsFolder .. "/" .. 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 = settingsFolder .. "/autoload.txt" - if not isfile(destAuto) then - pcall(writefile, destAuto, autodata) - end - end - end -end - -local function getConfigsFolder(self) - local root = self.FolderRoot or MacroModule.FolderRoot or "ATGHUB_Macro" - local placeId = getPlaceId() - local mapName = getMapName() - return root .. "/" .. placeId .. "/" .. mapName .. "/settings" -end - -local function getConfigFilePath(self, name) - local folder = getConfigsFolder(self) - return folder .. "/" .. name .. ".json" -end - --- status updater (uses Library:Notify for messages and Library.Options area if provided) -local function notify(title, content, duration) - if MacroModule.Library and MacroModule.Library.Notify then - pcall(function() - MacroModule.Library:Notify({ Title = title or "Macro System", Content = content or "", Duration = duration or 3 }) - end) - else - -- fallback - print(("[MacroModule] %s: %s"):format(title or "Info", tostring(content))) - end -end - -local function updateStatus() - local text = string.format("📊 Events: %d\n⏱️ Duration: %.3fs\n🔗 Hook: %s", eventsCount, durationTime, (hookInstalled and "Active ✓" or "Inactive ✗")) - -- try to update Library.Options location if exists (best-effort) - if MacroModule.Library and MacroModule.Library.Options and MacroModule.Library.Options.StatusText then - pcall(function() - if MacroModule.Library.Options.StatusText.SetValue then - MacroModule.Library.Options.StatusText:SetValue(text) - end - end) - return - end - -- otherwise attempt to set known Tab/Main update if present in Options table - if MacroModule.Options and MacroModule.Options.Status and MacroModule.Options.Status.SetValue then - pcall(function() MacroModule.Options.Status:SetValue(text) end) - end -end - --- Serialization helpers (same as earlier) -local function serializeArg(arg) - local argType = typeof(arg) - if argType == "Instance" then - return { type = "Instance", path = arg:GetFullName(), className = arg.ClassName } - elseif argType == "CFrame" then - return { type = "CFrame", components = {arg:GetComponents()} } - elseif argType == "Vector3" then - return { type = "Vector3", x = arg.X, y = arg.Y, z = arg.Z } - elseif argType == "Color3" then - return { type = "Color3", r = arg.R, g = arg.G, b = arg.B } - elseif argType == "table" then - local serialized = {} - for k, v in pairs(arg) do serialized[k] = serializeArg(v) end - return { type = "table", data = serialized } - else - return { type = argType, value = arg } - end -end - -local function deserializeArg(data) - if type(data) ~= "table" or not data.type then return data end - if data.type == "Instance" then - local success, result = pcall(function() - local obj = game - for part in data.path:gmatch("[^.]+") do - if part ~= "game" then - obj = obj:WaitForChild(part, 3) - end - end - return obj - end) - return success and result or nil - elseif data.type == "CFrame" then - return CFrame.new(unpack(data.components)) - elseif data.type == "Vector3" then - return Vector3.new(data.x, data.y, data.z) - elseif data.type == "Color3" then - return Color3.new(data.r, data.g, data.b) - elseif data.type == "table" then - local result = {} - for k, v in pairs(data.data) do result[k] = deserializeArg(v) end - return result - else - return data.value - end -end - --- Hook installer (SimpleSpy-style) -function MacroModule:InstallHook() - if hookInstalled then return true end - local ok, err = pcall(function() - oldNamecall = hookmetamethod(game, "__namecall", function(self, ...) - local method = getnamecallmethod() - local args = {...} - if (method == "FireServer" or method == "InvokeServer") and recording then - if (self and (self:IsA and (self:IsA("RemoteEvent") or self:IsA("RemoteFunction")))) then - local currentTime = tick() - startTime - local serializedArgs = {} - for i, arg in ipairs(args) do serializedArgs[i] = serializeArg(arg) end - - table.insert(macroData, { - time = currentTime, - remoteName = self.Name, - remotePath = self:GetFullName(), - remoteType = method, - args = serializedArgs - }) - - eventsCount = #macroData - durationTime = currentTime - updateStatus() - end - end - return oldNamecall(self, ...) - end) - hookInstalled = true - return true - end) - if not ok then - warn("Hook installation failed: " .. tostring(err)) - return false - end - updateStatus() - return true -end - --- Play macro -function MacroModule:PlayMacro(loop) - if #macroData == 0 then - notify("Macro System", "❌ ไม่มีมาโครให้เล่น!", 3) - return - end - - playing = true - looping = loop or false - - task.spawn(function() - repeat - local playStart = tick() - for _, action in ipairs(macroData) do - if not playing then break end - local targetTime = playStart + action.time - local waitTime = targetTime - tick() - if waitTime > 0 then task.wait(waitTime) end - if not playing then break end - - pcall(function() - local remote = game - for part in action.remotePath:gmatch("[^.]+") do - if part ~= "game" then - remote = remote:FindFirstChild(part) or remote:WaitForChild(part, 2) - end - end - if remote then - local deserializedArgs = {} - for i, arg in ipairs(action.args) do deserializedArgs[i] = deserializeArg(arg) end - if action.remoteType == "FireServer" and remote.FireServer then - remote:FireServer(unpack(deserializedArgs)) - elseif action.remoteType == "InvokeServer" and remote.InvokeServer then - remote:InvokeServer(unpack(deserializedArgs)) - end - end - end) - end - - if looping and playing then task.wait(0.5) end - until not looping or not playing - - playing = false - end) -end - -function MacroModule:StopPlaying() - looping = false - playing = false - if MacroModule.Options and MacroModule.Options.AutoPlayToggle and MacroModule.Options.AutoPlayToggle.SetValue then - pcall(function() MacroModule.Options.AutoPlayToggle:SetValue(false) end) - end -end - --- Save macro to file or clipboard -function MacroModule:SaveMacro(filename) - if #macroData == 0 then - notify("Macro System", "❌ ไม่มีข้อมูลมาโครให้บันทึก!", 3) - return false - end - if not filename or filename == "" then - notify("Macro System", "❌ กรุณากรอกชื่อไฟล์!", 3) - return false - end - - -- build folder tree for configs - self:BuildFolderTree() - local success, result = pcall(function() - local data = { - macroData = macroData, - timestamp = os.time(), - totalEvents = #macroData, - totalDuration = durationTime - } - local json = HttpService:JSONEncode(data) - if writefile then - local fullPath = getConfigFilePath(self, filename) - -- ensure folder - local folder = fullPath:match("^(.*)/[^/]+$") - if folder then ensureFolder(folder) end - writefile(fullPath, json) - return true - else - setclipboard(json) - return "clipboard" - end - end) - - if success then - if result == "clipboard" then - notify("Macro System", "📋 บันทึกแล้ว! ข้อมูลถูกคัดลอกไปยังคลิปบอร์ด", 5) - else - notify("Macro System", "💾 บันทึกไฟล์สำเร็จ!", 3) - end - return true - else - notify("Macro System", "❌ การบันทึกล้มเหลว: " .. tostring(result), 5) - return false - end -end - --- Load macro from file -function MacroModule:LoadMacro(filename) - if not filename or filename == "" then - notify("Macro System", "❌ กรุณาเลือกไฟล์!", 3) - return false - end - - local ok, res = pcall(function() - if readfile then - local fullPath = getConfigFilePath(self, filename) - if isfile and not isfile(fullPath) then - return nil, "ไฟล์ไม่พบ" - end - local content = readfile(fullPath) - return HttpService:JSONDecode(content) - else - return nil, "ระบบไฟล์ไม่รองรับ" - end - end) - - if ok and res then - macroData = res.macroData or {} - eventsCount = #macroData - durationTime = res.totalDuration or 0 - updateStatus() - notify("Macro System", "✅ โหลดมาโครสำเร็จ!", 3) - return true - else - notify("Macro System", "❌ โหลดไฟล์ล้มเหลว: " .. tostring(res), 5) - return false - end -end - --- refresh config list -function MacroModule:RefreshConfigList() - self:BuildFolderTree() - local folder = getConfigsFolder(self) - if not isfolder or 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" then - table.insert(out, name) - end - end - end - return out -end - -function MacroModule:LoadAutoloadConfig() - local autopath = getConfigsFolder(self) .. "/autoload.txt" - if isfile and isfile(autopath) then - local name = readfile(autopath) - local success, err = self:LoadMacro(name) - if not success then - notify("Interface", "Failed to load autoload config: " .. tostring(err), 7) - return - end - notify("Interface", string.format("Auto loaded config %q", name), 7) - end -end - --- API: compatibility methods -function MacroModule:SetLibrary(library) - self.Library = library - self.Options = library.Options or self.Options -end - -function MacroModule:SetFolder(folder) - folder = tostring(folder or self.FolderRoot or "ATGHUB_Macro") - self.FolderRoot = folder - -- update SUB_FOLDER style for backward compat (keep default subpath if not explicit settings path) - SUB_FOLDER = folder - self:BuildFolderTree() -end - -function MacroModule:SetIgnoreIndexes(list) - if type(list) ~= "table" then return end - for _, key in next, list do - self.Ignore[key] = true - end -end - -function MacroModule:IgnoreThemeSettings() - self:SetIgnoreIndexes({ "InterfaceTheme", "AcrylicToggle", "TransparentToggle", "MenuKeybind" }) -end - --- UI builder: BuildInterfaceSection(tab) -function MacroModule:BuildInterfaceSection(tab) - assert(tab, "BuildInterfaceSection expects a tab object") - -- We'll create UI controls under the provided tab - -- store reference to tab-based options for update callbacks - self.Tab = tab - - -- Try to create a "Macro" section and inputs (best-effort; depends on Fluent API) - local section = tab:AddSection("Macro") - - -- Filename input - self.Options.FilenameInput = tab:AddInput("Macro_FilenameInput", { - Title = "ชื่อไฟล์", - Default = "", - Placeholder = "กรอกชื่อไฟล์ (ไม่ต้องมี .json)", - Numeric = false, - Finished = false, - }) - - -- File dropdown - self.Options.FileDropdown = tab:AddDropdown("Macro_FileDropdown", { - Title = "เลือกไฟล์มาโคร", - Values = self:RefreshConfigList(), - Multi = false, - Default = 1, - }) - - tab:AddButton({ - Title = "💾 บันทึกมาโคร", - Description = "บันทึกมาโครปัจจุบันลงไฟล์", - Callback = function() - local filename = (self.Options.FilenameInput and self.Options.FilenameInput.Value) or "" - if filename == "" or not filename then - filename = os.date("macro_%Y%m%d_%H%M%S") - end - self:SaveMacro(filename) - pcall(function() self.Options.FileDropdown:SetValues(self:RefreshConfigList()) end) - end - }) - - -- Record toggle - self.Options.RecordToggle = tab:AddToggle("Macro_RecordToggle", { - Title = "⏺️ บันทึกมาโคร", - Description = "เริ่ม/หยุด การบันทึกมาโคร", - Default = false - }) - - self.Options.RecordToggle:OnChanged(function() - if self.Options.RecordToggle.Value then - if not hookInstalled then - if not self:InstallHook() then - notify("Macro System", "❌ ติดตั้ง Hook ไม่สำเร็จ!", 3) - self.Options.RecordToggle:SetValue(false) - return - end - end - recording = true - macroData = {} - startTime = tick() - eventsCount = 0 - durationTime = 0 - updateStatus() - notify("Macro System", "🎬 เริ่มบันทึกมาโครแล้ว", 2) - else - recording = false - notify("Macro System", string.format("✅ บันทึกเสร็จสิ้น: %d events", #macroData), 3) - updateStatus() - end - end) - - -- AutoPlay toggle - self.Options.AutoPlayToggle = tab:AddToggle("Macro_AutoPlayToggle", { - Title = "🔁 เล่นมาโครอัตโนมัติ", - Description = "เล่นมาโครวนลูปเมื่อเปิด", - Default = false - }) - - self.Options.AutoPlayToggle:OnChanged(function() - if self.Options.AutoPlayToggle.Value then - if not playing then - self:PlayMacro(true) - notify("Macro System", "🔁 เริ่มเล่นมาโครแบบวนลูป", 2) - end - else - self:StopPlaying() - notify("Macro System", "⏹️ หยุดเล่นมาโคร", 2) - end - end) - - tab:AddButton({ - Title = "▶️ เล่นมาโคร", - Description = "เล่นมาโครหนึ่งรอบ", - Callback = function() - if playing then - notify("Macro System", "⏸️ หยุดเล่นมาโครชั่วคราว", 2) - self:StopPlaying() - if self.Options.AutoPlayToggle and self.Options.AutoPlayToggle.SetValue then - self.Options.AutoPlayToggle:SetValue(false) - end - else - self:PlayMacro(false) - notify("Macro System", "▶️ เริ่มเล่นมาโครหนึ่งรอบ", 2) - end - end - }) - - tab:AddButton({ - Title = "📂 โหลดมาโคร", - Description = "โหลดมาโครจากไฟล์", - Callback = function() - local selectedFile = (self.Options.FileDropdown and self.Options.FileDropdown.Value) or nil - if selectedFile and selectedFile ~= "ไม่มีไฟล์ที่บันทึกไว้" and selectedFile ~= "ไม่มีระบบไฟล์" then - self:LoadMacro(selectedFile) - else - notify("Macro System", "❌ กรุณาเลือกไฟล์ที่ถูกต้อง", 3) - end - end - }) - - tab:AddButton({ - Title = "🗑️ ล้างข้อมูล", - Description = "ล้างข้อมูลมาโครปัจจุบัน", - Callback = function() - macroData = {} - eventsCount = 0 - durationTime = 0 - updateStatus() - end - }) - - tab:AddButton({ - Title = "อัพเดทรายการไฟล์", - Description = "รีเฟรชรายการไฟล์มาโคร (จากโฟลเดอร์)", - Callback = function() - pcall(function() self.Options.FileDropdown:SetValues(self:RefreshConfigList()) end) - notify("Macro System", "🔄 อัพเดทรายการไฟล์แล้ว", 2) - end - }) -end - --- BuildConfigSection: ให้ UI สำหรับจัดการไฟล์ (Create/Load/Overwrite/Refresh/Autoload) -function MacroModule:BuildConfigSection(tab) - assert(self.Library, "Must set MacroModule.Library via SetLibrary before BuildConfigSection") - - local section = tab:AddSection("Macro Configuration") - - section:AddInput("Macro_ConfigName", { Title = "Config name" }) - section:AddDropdown("Macro_ConfigList", { Title = "Config list", Values = self:RefreshConfigList(), AllowNull = true }) - - section:AddButton({ - Title = "Create config", - Callback = function() - local name = self.Options.Macro_ConfigName and self.Options.Macro_ConfigName.Value or "" - if name:gsub(" ", "") == "" then - return self.Library:Notify({ Title = "Interface", Content = "Config loader", SubContent = "Invalid config name (empty)", Duration = 7 }) - end - local success, err = self:SaveMacro(name) - if not success then - return self.Library:Notify({ Title = "Interface", Content = "Config loader", SubContent = "Failed to save config: " .. tostring(err), Duration = 7 }) - end - self.Library:Notify({ Title = "Interface", Content = "Config loader", SubContent = string.format("Created config %q", name), Duration = 7 }) - -- refresh list - pcall(function() self.Options.Macro_ConfigList:SetValues(self:RefreshConfigList()); self.Options.Macro_ConfigList:SetValue(nil) end) - end - }) - - section:AddButton({ - Title = "Load config", - Callback = function() - local name = self.Options.Macro_ConfigList and self.Options.Macro_ConfigList.Value - local success, err = self:LoadMacro(name) - if not success then - return self.Library:Notify({ Title = "Interface", Content = "Config loader", SubContent = "Failed to load config: " .. tostring(err), Duration = 7 }) - end - self.Library:Notify({ Title = "Interface", Content = "Config loader", SubContent = string.format("Loaded config %q", name), Duration = 7 }) - end - }) - - section:AddButton({ - Title = "Overwrite config", - Callback = function() - local name = self.Options.Macro_ConfigList and self.Options.Macro_ConfigList.Value - local success, err = self:SaveMacro(name) - if not success then - return self.Library:Notify({ Title = "Interface", Content = "Config loader", SubContent = "Failed to overwrite config: " .. tostring(err), Duration = 7 }) - end - self.Library:Notify({ Title = "Interface", Content = "Config loader", SubContent = string.format("Overwrote config %q", name), Duration = 7 }) - end - }) - - section:AddButton({ Title = "Refresh list", Callback = function() - pcall(function() self.Options.Macro_ConfigList:SetValues(self:RefreshConfigList()); self.Options.Macro_ConfigList:SetValue(nil) end) - end }) - - local AutoloadButton - AutoloadButton = section:AddButton({ - Title = "Set as autoload", - Description = "Current autoload config: none", - Callback = function() - local name = self.Options.Macro_ConfigList and self.Options.Macro_ConfigList.Value - local autopath = getConfigsFolder(self) .. "/autoload.txt" - if writefile then - writefile(autopath, tostring(name)) - AutoloadButton:SetDesc("Current autoload config: " .. tostring(name)) - self.Library:Notify({ Title = "Interface", Content = "Config loader", SubContent = string.format("Set %q to auto load", name), Duration = 7 }) - else - self.Library:Notify({ Title = "Interface", Content = "Config loader", SubContent = "Filesystem not supported", Duration = 7 }) - end - end - }) - - -- populate current autoload desc if exists - local autop = getConfigsFolder(self) .. "/autoload.txt" - if isfile and isfile(autop) then - local name = readfile(autop) - pcall(function() AutoloadButton:SetDesc("Current autoload config: " .. tostring(name)) end) - end - - -- mark ignore indexes for these UI fields (so SaveManager-like behavior) - self:SetIgnoreIndexes({ "Macro_ConfigList", "Macro_ConfigName" }) -end - --- convenience aliases & Init -function MacroModule:Init(deps) - deps = deps or {} - if deps.Library then self:SetLibrary(deps.Library) end - if deps.Folder then self:SetFolder(deps.Folder) end - return self -end - --- expose some util getters -function MacroModule:GetState() - return { - recording = recording, - playing = playing, - eventsCount = eventsCount, - durationTime = durationTime, - hookInstalled = hookInstalled, - macros = macroData - } -end - --- expose methods -MacroModule.InstallHook = MacroModule.InstallHook -MacroModule.PlayMacro = MacroModule.PlayMacro -MacroModule.StopPlaying = MacroModule.StopPlaying -MacroModule.SaveMacro = MacroModule.SaveMacro -MacroModule.LoadMacro = MacroModule.LoadMacro -MacroModule.RefreshConfigList = MacroModule.RefreshConfigList -MacroModule.LoadAutoloadConfig = MacroModule.LoadAutoloadConfig - --- ensure default folder structure on load -MacroModule:BuildFolderTree() - -return MacroModule From 169cd4e1a78b2aa7cdb1f981b054ef699b973d1f Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:45:43 +0700 Subject: [PATCH 32/76] Update config.lua --- config.lua | 1052 +++++++++++++++++++++++++++++----------------------- 1 file changed, 591 insertions(+), 461 deletions(-) diff --git a/config.lua b/config.lua index e0d90d5..094e503 100644 --- a/config.lua +++ b/config.lua @@ -1,463 +1,593 @@ --- InterfaceManager (ATGHubSettings + dynamic theme apply) -local httpService = game:GetService("HttpService") -local Workspace = game:GetService("Workspace") - -local InterfaceManager = {} do - -- root folder ใหม่ (สร้างใหม่สะอาดๆ) - InterfaceManager.FolderRoot = "ATGHubSettings" - - -- default settings - InterfaceManager.Settings = { - Theme = "Dark", - Acrylic = true, - Transparency = true, - MenuKeybind = "LeftControl" - } - - -- internal - 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 getMapName() - local ok, map = pcall(function() return Workspace:FindFirstChild("Map") end) - if ok and map and type(map.Name) == "string" then - return sanitizeFilename(map.Name) - end - -- fallback to place name or "UnknownMap" - local ok2, wn = pcall(function() return Workspace.Name end) - if ok2 and wn then return sanitizeFilename(wn) end - return "UnknownMap" - end - - local function ensureFolder(path) - if not isfolder(path) then makefolder(path) end - end - - -- Build folders: root, Themes, /Themes, settings, Imports - function InterfaceManager:BuildFolderTree() - local root = self.FolderRoot - ensureFolder(root) - - local mapName = getMapName() - local mapFolder = root .. "/" .. mapName - ensureFolder(mapFolder) - - -- global & per-map settings - ensureFolder(root .. "/settings") - ensureFolder(mapFolder .. "/settings") - - -- global & per-map themes - ensureFolder(root .. "/Themes") - ensureFolder(mapFolder .. "/Themes") - - -- imports - ensureFolder(root .. "/Imports") - end - - function InterfaceManager:SetFolder(folder) - self.FolderRoot = tostring(folder or "ATGHubSettings") - self:BuildFolderTree() - end - - function InterfaceManager:SetLibrary(lib) - self.Library = lib - -- register disk themes at set time - self:RegisterThemesToLibrary(lib) - end - - -- config file path uses map name (not PlaceId) - local function getPrefixedSettingsFilename() - local mapName = getMapName() - local fname = "ATG Hub - " .. sanitizeFilename(mapName) .. ".json" - return fname - end - - local function getConfigFilePath(self) - local root = self.FolderRoot - local mapName = getMapName() - local configFolder = root .. "/" .. mapName - ensureFolder(configFolder) - local fname = getPrefixedSettingsFilename() - return configFolder .. "/" .. fname - end - - function InterfaceManager:SaveSettings() - local path = getConfigFilePath(self) - local folder = path:match("^(.*)/[^/]+$") - if folder then ensureFolder(folder) end - local encoded = httpService:JSONEncode(self.Settings or {}) - writefile(path, encoded) - end - - function InterfaceManager:LoadSettings() - local path = getConfigFilePath(self) - local legacyPath = self.FolderRoot .. "/options.json" - - if isfile(path) then - local data = readfile(path) - local ok, dec = pcall(httpService.JSONDecode, httpService, data) - if ok and type(dec) == "table" then - for k,v in pairs(dec) do self.Settings[k] = v end - end - return - end - - -- ถ้ามี legacy options.json ให้ merge ค่า (แต่ไม่คัดลอกโฟลเดอร์ทั้งหมด) - if isfile(legacyPath) then - local data = readfile(legacyPath) - local ok, dec = pcall(httpService.JSONDecode, httpService, data) - if ok and type(dec) == "table" then - for k,v in pairs(dec) do self.Settings[k] = v end - local folder = path:match("^(.*)/[^/]+$") - if folder then ensureFolder(folder) end - writefile(path, httpService:JSONEncode(self.Settings or {})) - end - return - end - -- else use defaults - end - - -- ================= Theme scanning/importing ================= - - -- scan Themes folders and return list of { name, path, ext } - function InterfaceManager:ScanThemes() - local out = {} - local root = self.FolderRoot - local mapName = getMapName() - local paths = { - root .. "/Themes", - root .. "/" .. mapName .. "/Themes" - } - for _, folder in ipairs(paths) do - if isfolder(folder) and type(listfiles) == "function" then - local ok, files = pcall(listfiles, folder) - if ok and type(files) == "table" then - for _, f in ipairs(files) do - if f:match("%.lua$") or f:match("%.json$") then - local base = f:match("([^/\\]+)$") or f - local display = base:gsub("^ATG Hub %- ", ""):gsub("%.lua$",""):gsub("%.json$",""):gsub("%_"," ") - local ext = f:match("%.([a-zA-Z0-9]+)$") - table.insert(out, { name = display, path = f, ext = ext }) - end - end - end - end - end - return out - end - - -- write theme file to global Themes - function InterfaceManager:ImportTheme(name, content, ext) - ext = tostring(ext or "lua"):lower() - if ext ~= "lua" and ext ~= "json" then ext = "lua" end - local rootThemes = self.FolderRoot .. "/Themes" - ensureFolder(rootThemes) - local safe = sanitizeFilename(name) - local fname = "ATG Hub - " .. safe .. "." .. ext - local full = rootThemes .. "/" .. fname - writefile(full, tostring(content or "")) - -- try to register immediately - if self.Library then - self:TryRegisterThemeFile(full, ext) - end - return full - end - - -- parse a theme file and return themeTable (or nil) - local function parseThemeFile(fullpath, ext) - if not isfile(fullpath) then return nil end - local raw = readfile(fullpath) - if ext == "json" then - local ok, dec = pcall(httpService.JSONDecode, httpService, raw) - if ok and type(dec) == "table" then return dec end - return nil - else - -- lua: try loadstring and expect return table - local ok, chunk = pcall(loadstring, raw) - if not ok or type(chunk) ~= "function" then return nil end - local ok2, result = pcall(chunk) - if ok2 and type(result) == "table" then return result end - return nil - end - end - - -- Try to register a theme file to the library (best-effort) - function InterfaceManager:TryRegisterThemeFile(fullpath, ext) - local themeTbl = parseThemeFile(fullpath, ext) - if not themeTbl then return false, "could not parse theme" end - if not self.Library then return false, "no library" end - - local displayName = fullpath:match("([^/\\]+)$") or fullpath - displayName = displayName:gsub("^ATG Hub %- ", ""):gsub("%.lua$",""):gsub("%.json$",""):gsub("%_"," ") - - -- Prefer API: Library:RegisterTheme(name, table) - if type(self.Library.RegisterTheme) == "function" then - pcall(function() self.Library:RegisterTheme(displayName, themeTbl) end) - -- also store dynamic import table if needed - self.Library.DynamicImportedThemes = self.Library.DynamicImportedThemes or {} - self.Library.DynamicImportedThemes[displayName] = themeTbl - return true, "registered" - end - - -- Fallback: if Library.Themes is map, put it there - if type(self.Library.Themes) == "table" then - local isMap = false - for k,v in pairs(self.Library.Themes) do - if type(k) ~= "number" then isMap = true break end - end - if isMap then - self.Library.Themes[displayName] = themeTbl - self.Library.DynamicImportedThemes = self.Library.DynamicImportedThemes or {} - self.Library.DynamicImportedThemes[displayName] = themeTbl - return true, "merged into map" - else - -- array: push name, and keep table in DynamicImportedThemes - local exists = false - for _,v in ipairs(self.Library.Themes) do if v == displayName then exists = true break end end - if not exists then table.insert(self.Library.Themes, displayName) end - self.Library.DynamicImportedThemes = self.Library.DynamicImportedThemes or {} - self.Library.DynamicImportedThemes[displayName] = themeTbl - return true, "added name + dynamic table" - end - end - - return false, "no suitable integration" - end - - -- Register all disk themes to library - function InterfaceManager:RegisterThemesToLibrary(library) - if not library then return end - local found = self:ScanThemes() - for _, item in ipairs(found) do - pcall(function() self:TryRegisterThemeFile(item.path, item.ext) end) - end - end - - -- ================ Theme apply logic (best-effort, dynamic accent update) ================ - - -- low-level: try to push a single Accent color to Library (multiple API tries) - local function tryUpdateAccentToLibrary(lib, color) - if not lib or not color then return end - -- try API names that library might expose - local ok - if type(lib.UpdateAccent) == "function" then pcall(lib.UpdateAccent, lib, color) end - if type(lib.SetAccent) == "function" then pcall(lib.SetAccent, lib, color) end - if type(lib.SetAccentColor) == "function" then pcall(lib.SetAccentColor, lib, color) end - -- try setting fields directly - pcall(function() lib.Accent = color end) - pcall(function() lib.CurrentAccent = color end) - -- if library has a refresh hook - if type(lib.RefreshTheme) == "function" then pcall(lib.RefreshTheme, lib) end - if type(lib.ApplyTheme) == "function" then pcall(lib.ApplyTheme, lib, lib.CurrentTheme or {}) end - end - - -- Apply theme by name (search in dynamic imports, library map, or disk), and start polling Accent if theme is dynamic - function InterfaceManager:ApplyThemeByName(name) - if not name or not self.Library then return false, "no name or library" end - local lib = self.Library - - -- first, if library has a SetTheme that accepts name, prefer that - if type(lib.SetTheme) == "function" then - local ok, err = pcall(function() lib:SetTheme(name) end) - if ok then - -- after set, if theme table exists in DynamicImportedThemes, start poll - local themeTbl = (lib.DynamicImportedThemes and lib.DynamicImportedThemes[name]) or nil - if themeTbl and type(themeTbl) == "table" then - self:StartThemeAccentPoll(themeTbl) - end - return true, "SetTheme(name) called" - end - end - - -- next try dynamic imports map - if lib.DynamicImportedThemes and lib.DynamicImportedThemes[name] then - local themeTbl = lib.DynamicImportedThemes[name] - -- try RegisterTheme + SetTheme if possible - if type(lib.RegisterTheme) == "function" then - pcall(function() lib:RegisterTheme(name, themeTbl) end) - pcall(function() lib:SetTheme(name) end) - elseif type(lib.ApplyTheme) == "function" then - pcall(function() lib:ApplyTheme(themeTbl) end) - else - -- try set fields directly - pcall(function() lib.CurrentTheme = themeTbl end) - if type(lib.RefreshTheme) == "function" then pcall(function() lib:RefreshTheme() end) end - end - -- start polling dynamic Accent - self:StartThemeAccentPoll(themeTbl) - return true, "applied from DynamicImportedThemes" - end - - -- fallback: scan disk and try to parse theme file and apply table - local found = self:ScanThemes() - for _, item in ipairs(found) do - if item.name == name then - local themeTbl = parseThemeFile(item.path, item.ext) - if themeTbl then - if type(lib.RegisterTheme) == "function" then - pcall(function() lib:RegisterTheme(name, themeTbl) end) - pcall(function() lib:SetTheme(name) end) - elseif type(lib.ApplyTheme) == "function" then - pcall(function() lib:ApplyTheme(themeTbl) end) - else - pcall(function() lib.CurrentTheme = themeTbl end) - if type(lib.RefreshTheme) == "function" then pcall(function() lib:RefreshTheme() end) end - end - self:StartThemeAccentPoll(themeTbl) - return true, "applied from disk" - end - end - end - - return false, "could not apply theme" - end - - -- poll Theme.Accent and push to library (if theme updates Accent dynamically) - function InterfaceManager:StartThemeAccentPoll(themeTbl) - if not themeTbl or type(themeTbl) ~= "table" then return end - -- avoid starting multiple pollers for same table - themeTbl._ATG_POLLING = themeTbl._ATG_POLLING or true - task.spawn(function() - local last = nil - while themeTbl._ATG_POLLING do - local acc = themeTbl.Accent - -- if Accent is function, call it (support function-based themes) - if type(acc) == "function" then - local ok, res = pcall(acc) - if ok and res then acc = res end - end - -- if color changed, push to lib - if acc and (not last or acc ~= last) then - tryUpdateAccentToLibrary(self.Library, acc) - last = acc - end - task.wait(0.05) - end - end) - end - - -- Register all disk themes to library (best-effort) - function InterfaceManager:RegisterThemesToLibrary(library) - if not library then return end - local found = self:ScanThemes() - for _, item in ipairs(found) do - pcall(function() self:TryRegisterThemeFile(item.path, item.ext) end) - end - end - - -- ======== UI Builder ======== - local function getLibraryThemeNames(library) - local names = {} - if not library then return {} end - - -- library.Themes as array or map - if type(library.Themes) == "table" then - local numeric = true - for k,v in pairs(library.Themes) do if type(k) ~= "number" then numeric = false break end end - if numeric then - for _,v in ipairs(library.Themes) do if type(v) == "string" then names[v] = true end end - else - for k,v in pairs(library.Themes) do if type(k) == "string" then names[k] = true end end - end - end - - -- dynamic imports - if library.DynamicImportedThemes then - for k,v in pairs(library.DynamicImportedThemes) do names[k] = true end - end - - -- disk themes - local disk = InterfaceManager:ScanThemes() - for _, item in ipairs(disk) do names[item.name] = true end - - local out = {} - for k,_ in pairs(names) do table.insert(out, k) end - table.sort(out) - return out - end - - function InterfaceManager:BuildInterfaceSection(tab) - assert(self.Library, "Must set InterfaceManager.Library") - local Library = self.Library - local Settings = InterfaceManager.Settings - - InterfaceManager:BuildFolderTree() - InterfaceManager:LoadSettings() - InterfaceManager:RegisterThemesToLibrary(Library) - - local section = tab:AddSection("Interface") - - local mergedValues = getLibraryThemeNames(Library) - local InterfaceTheme = section:AddDropdown("InterfaceTheme", { - Title = "Theme", - Description = "Changes the interface theme.", - Values = mergedValues, - Default = Settings.Theme, - Callback = function(Value) - -- apply using our helper which will attempt various APIs + dynamic accent polling - InterfaceManager:ApplyThemeByName(Value) - Settings.Theme = Value - InterfaceManager:SaveSettings() - end - }) - InterfaceTheme:SetValue(Settings.Theme) - - -- Refresh button for manual rescan - if section.AddButton then - section:AddButton({ - Title = "Refresh Themes", - Description = "Rescan ATGHubSettings/Themes and update dropdown.", - Callback = function() - InterfaceManager:RegisterThemesToLibrary(Library) - local newList = getLibraryThemeNames(Library) - if InterfaceTheme.SetValues then - pcall(function() InterfaceTheme:SetValues(newList) end) - elseif InterfaceTheme.SetOptions then - pcall(function() InterfaceTheme:SetOptions(newList) end) - else - print("[InterfaceManager] Themes refreshed. Re-open menu if dropdown not updated.") - end - end - }) - end - - -- rest of the UI controls... - if Library.UseAcrylic then - section:AddToggle("AcrylicToggle", { - Title = "Acrylic", - Description = "The blurred background requires graphic quality 8+", - Default = Settings.Acrylic, - Callback = function(Value) - if type(Library.ToggleAcrylic) == "function" then pcall(function() Library:ToggleAcrylic(Value) end) end - Settings.Acrylic = Value - InterfaceManager:SaveSettings() - end - }) - end - - section:AddToggle("TransparentToggle", { - Title = "Transparency", - Description = "Makes the interface transparent.", - Default = Settings.Transparency, - Callback = function(Value) - if type(Library.ToggleTransparency) == "function" then pcall(function() Library:ToggleTransparency(Value) end) end - 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 +-- CombinedThemesAndInterfaceManager.lua +-- Single-module pack: Themes + InterfaceManager +local HttpService = game:GetService("HttpService") + +local Module = {} +Module.Themes = { + Names = {} +} + +-- helper to register a theme into Module.Themes +local function addTheme(t) + if type(t) ~= "table" or type(t.Name) ~= "string" then return end + Module.Themes[t.Name] = t + table.insert(Module.Themes.Names, t.Name) end -return InterfaceManager +-- ============================================ +-- Theme definitions (add or edit here) +-- ============================================ + +addTheme({ + Name = "Emerald", + Accent = Color3.fromRGB(16, 185, 129), + AcrylicMain = Color3.fromRGB(18, 18, 18), + AcrylicBorder = Color3.fromRGB(52, 211, 153), + AcrylicGradient = ColorSequence.new(Color3.fromRGB(16, 185, 129), Color3.fromRGB(5, 150, 105)), + AcrylicNoise = 0.92, + TitleBarLine = Color3.fromRGB(16, 185, 129), + Tab = Color3.fromRGB(110, 231, 183), + Element = Color3.fromRGB(52, 211, 153), + ElementBorder = Color3.fromRGB(6, 95, 70), + InElementBorder = Color3.fromRGB(16, 185, 129), + ElementTransparency = 0.87, + ToggleSlider = Color3.fromRGB(52, 211, 153), + ToggleToggled = Color3.fromRGB(0, 0, 0), + SliderRail = Color3.fromRGB(52, 211, 153), + DropdownFrame = Color3.fromRGB(110, 231, 183), + DropdownHolder = Color3.fromRGB(6, 78, 59), + DropdownBorder = Color3.fromRGB(4, 120, 87), + DropdownOption = Color3.fromRGB(52, 211, 153), + Keybind = Color3.fromRGB(52, 211, 153), + Input = Color3.fromRGB(52, 211, 153), + InputFocused = Color3.fromRGB(6, 78, 59), + InputIndicator = Color3.fromRGB(110, 231, 183), + Dialog = Color3.fromRGB(6, 78, 59), + DialogHolder = Color3.fromRGB(4, 120, 87), + DialogHolderLine = Color3.fromRGB(6, 95, 70), + DialogButton = Color3.fromRGB(16, 185, 129), + DialogButtonBorder = Color3.fromRGB(52, 211, 153), + DialogBorder = Color3.fromRGB(16, 185, 129), + DialogInput = Color3.fromRGB(6, 95, 70), + DialogInputLine = Color3.fromRGB(110, 231, 183), + Text = Color3.fromRGB(240, 253, 244), + SubText = Color3.fromRGB(167, 243, 208), + Hover = Color3.fromRGB(52, 211, 153), + HoverChange = 0.04, +}) + +addTheme({ + Name = "Crimson", + Accent = Color3.fromRGB(220, 38, 38), + AcrylicMain = Color3.fromRGB(20, 20, 20), + AcrylicBorder = Color3.fromRGB(239, 68, 68), + AcrylicGradient = ColorSequence.new(Color3.fromRGB(220, 38, 38), Color3.fromRGB(153, 27, 27)), + AcrylicNoise = 0.92, + TitleBarLine = Color3.fromRGB(220, 38, 38), + Tab = Color3.fromRGB(252, 165, 165), + Element = Color3.fromRGB(239, 68, 68), + ElementBorder = Color3.fromRGB(127, 29, 29), + InElementBorder = Color3.fromRGB(185, 28, 28), + ElementTransparency = 0.87, + ToggleSlider = Color3.fromRGB(239, 68, 68), + ToggleToggled = Color3.fromRGB(0, 0, 0), + SliderRail = Color3.fromRGB(239, 68, 68), + DropdownFrame = Color3.fromRGB(252, 165, 165), + DropdownHolder = Color3.fromRGB(127, 29, 29), + DropdownBorder = Color3.fromRGB(153, 27, 27), + DropdownOption = Color3.fromRGB(239, 68, 68), + Keybind = Color3.fromRGB(239, 68, 68), + Input = Color3.fromRGB(239, 68, 68), + InputFocused = Color3.fromRGB(127, 29, 29), + InputIndicator = Color3.fromRGB(252, 165, 165), + Dialog = Color3.fromRGB(127, 29, 29), + DialogHolder = Color3.fromRGB(153, 27, 27), + DialogHolderLine = Color3.fromRGB(185, 28, 28), + DialogButton = Color3.fromRGB(220, 38, 38), + DialogButtonBorder = Color3.fromRGB(239, 68, 68), + DialogBorder = Color3.fromRGB(220, 38, 38), + DialogInput = Color3.fromRGB(153, 27, 27), + DialogInputLine = Color3.fromRGB(252, 165, 165), + Text = Color3.fromRGB(254, 242, 242), + SubText = Color3.fromRGB(254, 202, 202), + Hover = Color3.fromRGB(239, 68, 68), + HoverChange = 0.04, +}) + +addTheme({ + Name = "Ocean", + Accent = Color3.fromRGB(14, 116, 144), + AcrylicMain = Color3.fromRGB(15, 23, 42), + AcrylicBorder = Color3.fromRGB(34, 211, 238), + AcrylicGradient = ColorSequence.new(Color3.fromRGB(14, 116, 144), Color3.fromRGB(8, 51, 68)), + AcrylicNoise = 0.92, + TitleBarLine = Color3.fromRGB(14, 165, 233), + Tab = Color3.fromRGB(125, 211, 252), + Element = Color3.fromRGB(34, 211, 238), + ElementBorder = Color3.fromRGB(8, 51, 68), + InElementBorder = Color3.fromRGB(14, 116, 144), + ElementTransparency = 0.87, + ToggleSlider = Color3.fromRGB(34, 211, 238), + ToggleToggled = Color3.fromRGB(0, 0, 0), + SliderRail = Color3.fromRGB(34, 211, 238), + DropdownFrame = Color3.fromRGB(165, 243, 252), + DropdownHolder = Color3.fromRGB(8, 51, 68), + DropdownBorder = Color3.fromRGB(14, 116, 144), + DropdownOption = Color3.fromRGB(34, 211, 238), + Keybind = Color3.fromRGB(34, 211, 238), + Input = Color3.fromRGB(34, 211, 238), + InputFocused = Color3.fromRGB(8, 51, 68), + InputIndicator = Color3.fromRGB(165, 243, 252), + Dialog = Color3.fromRGB(8, 51, 68), + DialogHolder = Color3.fromRGB(14, 116, 144), + DialogHolderLine = Color3.fromRGB(6, 182, 212), + DialogButton = Color3.fromRGB(14, 165, 233), + DialogButtonBorder = Color3.fromRGB(34, 211, 238), + DialogBorder = Color3.fromRGB(14, 116, 144), + DialogInput = Color3.fromRGB(8, 51, 68), + DialogInputLine = Color3.fromRGB(165, 243, 252), + Text = Color3.fromRGB(240, 249, 255), + SubText = Color3.fromRGB(186, 230, 253), + Hover = Color3.fromRGB(34, 211, 238), + HoverChange = 0.04, +}) + +addTheme({ + Name = "Sunset", + Accent = Color3.fromRGB(251, 146, 60), + AcrylicMain = Color3.fromRGB(20, 20, 20), + AcrylicBorder = Color3.fromRGB(251, 146, 60), + AcrylicGradient = ColorSequence.new(Color3.fromRGB(251, 146, 60), Color3.fromRGB(234, 88, 12)), + AcrylicNoise = 0.92, + TitleBarLine = Color3.fromRGB(251, 146, 60), + Tab = Color3.fromRGB(254, 215, 170), + Element = Color3.fromRGB(251, 146, 60), + ElementBorder = Color3.fromRGB(124, 45, 18), + InElementBorder = Color3.fromRGB(194, 65, 12), + ElementTransparency = 0.87, + ToggleSlider = Color3.fromRGB(251, 146, 60), + ToggleToggled = Color3.fromRGB(0, 0, 0), + SliderRail = Color3.fromRGB(251, 146, 60), + DropdownFrame = Color3.fromRGB(254, 215, 170), + DropdownHolder = Color3.fromRGB(124, 45, 18), + DropdownBorder = Color3.fromRGB(194, 65, 12), + DropdownOption = Color3.fromRGB(251, 146, 60), + Keybind = Color3.fromRGB(251, 146, 60), + Input = Color3.fromRGB(251, 146, 60), + InputFocused = Color3.fromRGB(124, 45, 18), + InputIndicator = Color3.fromRGB(254, 215, 170), + Dialog = Color3.fromRGB(124, 45, 18), + DialogHolder = Color3.fromRGB(194, 65, 12), + DialogHolderLine = Color3.fromRGB(234, 88, 12), + DialogButton = Color3.fromRGB(234, 88, 12), + DialogButtonBorder = Color3.fromRGB(251, 146, 60), + DialogBorder = Color3.fromRGB(234, 88, 12), + DialogInput = Color3.fromRGB(194, 65, 12), + DialogInputLine = Color3.fromRGB(254, 215, 170), + Text = Color3.fromRGB(255, 251, 235), + SubText = Color3.fromRGB(254, 215, 170), + Hover = Color3.fromRGB(251, 146, 60), + HoverChange = 0.04, +}) + +addTheme({ + Name = "Lavender", + Accent = Color3.fromRGB(167, 139, 250), + AcrylicMain = Color3.fromRGB(20, 20, 25), + AcrylicBorder = Color3.fromRGB(196, 181, 253), + AcrylicGradient = ColorSequence.new(Color3.fromRGB(167, 139, 250), Color3.fromRGB(109, 40, 217)), + AcrylicNoise = 0.92, + TitleBarLine = Color3.fromRGB(167, 139, 250), + Tab = Color3.fromRGB(221, 214, 254), + Element = Color3.fromRGB(196, 181, 253), + ElementBorder = Color3.fromRGB(55, 48, 163), + InElementBorder = Color3.fromRGB(124, 58, 237), + ElementTransparency = 0.87, + ToggleSlider = Color3.fromRGB(196, 181, 253), + ToggleToggled = Color3.fromRGB(0, 0, 0), + SliderRail = Color3.fromRGB(196, 181, 253), + DropdownFrame = Color3.fromRGB(237, 233, 254), + DropdownHolder = Color3.fromRGB(55, 48, 163), + DropdownBorder = Color3.fromRGB(109, 40, 217), + DropdownOption = Color3.fromRGB(196, 181, 253), + Keybind = Color3.fromRGB(196, 181, 253), + Input = Color3.fromRGB(196, 181, 253), + InputFocused = Color3.fromRGB(55, 48, 163), + InputIndicator = Color3.fromRGB(237, 233, 254), + Dialog = Color3.fromRGB(55, 48, 163), + DialogHolder = Color3.fromRGB(109, 40, 217), + DialogHolderLine = Color3.fromRGB(124, 58, 237), + DialogButton = Color3.fromRGB(139, 92, 246), + DialogButtonBorder = Color3.fromRGB(196, 181, 253), + DialogBorder = Color3.fromRGB(124, 58, 237), + DialogInput = Color3.fromRGB(109, 40, 217), + DialogInputLine = Color3.fromRGB(237, 233, 254), + Text = Color3.fromRGB(250, 245, 255), + SubText = Color3.fromRGB(221, 214, 254), + Hover = Color3.fromRGB(196, 181, 253), + HoverChange = 0.04, +}) + +addTheme({ + Name = "Mint", + Accent = Color3.fromRGB(52, 211, 153), + AcrylicMain = Color3.fromRGB(17, 24, 39), + AcrylicBorder = Color3.fromRGB(110, 231, 183), + AcrylicGradient = ColorSequence.new(Color3.fromRGB(52, 211, 153), Color3.fromRGB(16, 185, 129)), + AcrylicNoise = 0.92, + TitleBarLine = Color3.fromRGB(52, 211, 153), + Tab = Color3.fromRGB(167, 243, 208), + Element = Color3.fromRGB(110, 231, 183), + ElementBorder = Color3.fromRGB(6, 78, 59), + InElementBorder = Color3.fromRGB(16, 185, 129), + ElementTransparency = 0.87, + ToggleSlider = Color3.fromRGB(110, 231, 183), + ToggleToggled = Color3.fromRGB(0, 0, 0), + SliderRail = Color3.fromRGB(110, 231, 183), + DropdownFrame = Color3.fromRGB(209, 250, 229), + DropdownHolder = Color3.fromRGB(6, 78, 59), + DropdownBorder = Color3.fromRGB(16, 185, 129), + DropdownOption = Color3.fromRGB(110, 231, 183), + Keybind = Color3.fromRGB(110, 231, 183), + Input = Color3.fromRGB(110, 231, 183), + InputFocused = Color3.fromRGB(6, 78, 59), + InputIndicator = Color3.fromRGB(209, 250, 229), + Dialog = Color3.fromRGB(6, 78, 59), + DialogHolder = Color3.fromRGB(16, 185, 129), + DialogHolderLine = Color3.fromRGB(52, 211, 153), + DialogButton = Color3.fromRGB(52, 211, 153), + DialogButtonBorder = Color3.fromRGB(110, 231, 183), + DialogBorder = Color3.fromRGB(16, 185, 129), + DialogInput = Color3.fromRGB(6, 95, 70), + DialogInputLine = Color3.fromRGB(209, 250, 229), + Text = Color3.fromRGB(236, 253, 245), + SubText = Color3.fromRGB(167, 243, 208), + Hover = Color3.fromRGB(110, 231, 183), + HoverChange = 0.04, +}) + +addTheme({ + Name = "Coral", + Accent = Color3.fromRGB(251, 113, 133), + AcrylicMain = Color3.fromRGB(24, 20, 25), + AcrylicBorder = Color3.fromRGB(251, 113, 133), + AcrylicGradient = ColorSequence.new(Color3.fromRGB(251, 113, 133), Color3.fromRGB(244, 63, 94)), + AcrylicNoise = 0.92, + TitleBarLine = Color3.fromRGB(251, 113, 133), + Tab = Color3.fromRGB(253, 164, 175), + Element = Color3.fromRGB(251, 113, 133), + ElementBorder = Color3.fromRGB(136, 19, 55), + InElementBorder = Color3.fromRGB(225, 29, 72), + ElementTransparency = 0.87, + ToggleSlider = Color3.fromRGB(251, 113, 133), + ToggleToggled = Color3.fromRGB(0, 0, 0), + SliderRail = Color3.fromRGB(251, 113, 133), + DropdownFrame = Color3.fromRGB(254, 205, 211), + DropdownHolder = Color3.fromRGB(136, 19, 55), + DropdownBorder = Color3.fromRGB(225, 29, 72), + DropdownOption = Color3.fromRGB(251, 113, 133), + Keybind = Color3.fromRGB(251, 113, 133), + Input = Color3.fromRGB(251, 113, 133), + InputFocused = Color3.fromRGB(136, 19, 55), + InputIndicator = Color3.fromRGB(254, 205, 211), + Dialog = Color3.fromRGB(136, 19, 55), + DialogHolder = Color3.fromRGB(225, 29, 72), + DialogHolderLine = Color3.fromRGB(244, 63, 94), + DialogButton = Color3.fromRGB(244, 63, 94), + DialogButtonBorder = Color3.fromRGB(251, 113, 133), + DialogBorder = Color3.fromRGB(225, 29, 72), + DialogInput = Color3.fromRGB(190, 18, 60), + DialogInputLine = Color3.fromRGB(254, 205, 211), + Text = Color3.fromRGB(255, 241, 242), + SubText = Color3.fromRGB(254, 205, 211), + Hover = Color3.fromRGB(251, 113, 133), + HoverChange = 0.04, +}) + +addTheme({ + Name = "Gold", + Accent = Color3.fromRGB(245, 158, 11), + AcrylicMain = Color3.fromRGB(20, 20, 18), + AcrylicBorder = Color3.fromRGB(251, 191, 36), + AcrylicGradient = ColorSequence.new(Color3.fromRGB(245, 158, 11), Color3.fromRGB(180, 83, 9)), + AcrylicNoise = 0.92, + TitleBarLine = Color3.fromRGB(245, 158, 11), + Tab = Color3.fromRGB(253, 224, 71), + Element = Color3.fromRGB(251, 191, 36), + ElementBorder = Color3.fromRGB(120, 53, 15), + InElementBorder = Color3.fromRGB(217, 119, 6), + ElementTransparency = 0.87, + ToggleSlider = Color3.fromRGB(251, 191, 36), + ToggleToggled = Color3.fromRGB(0, 0, 0), + SliderRail = Color3.fromRGB(251, 191, 36), + DropdownFrame = Color3.fromRGB(254, 240, 138), + DropdownHolder = Color3.fromRGB(120, 53, 15), + DropdownBorder = Color3.fromRGB(180, 83, 9), + DropdownOption = Color3.fromRGB(251, 191, 36), + Keybind = Color3.fromRGB(251, 191, 36), + Input = Color3.fromRGB(251, 191, 36), + InputFocused = Color3.fromRGB(120, 53, 15), + InputIndicator = Color3.fromRGB(254, 240, 138), + Dialog = Color3.fromRGB(120, 53, 15), + DialogHolder = Color3.fromRGB(180, 83, 9), + DialogHolderLine = Color3.fromRGB(217, 119, 6), + DialogButton = Color3.fromRGB(245, 158, 11), + DialogButtonBorder = Color3.fromRGB(251, 191, 36), + DialogBorder = Color3.fromRGB(217, 119, 6), + DialogInput = Color3.fromRGB(180, 83, 9), + DialogInputLine = Color3.fromRGB(254, 240, 138), + Text = Color3.fromRGB(254, 252, 232), + SubText = Color3.fromRGB(253, 230, 138), + Hover = Color3.fromRGB(251, 191, 36), + HoverChange = 0.04, +}) + +addTheme({ + Name = "Midnight", + Accent = Color3.fromRGB(99, 102, 241), + AcrylicMain = Color3.fromRGB(15, 23, 42), + AcrylicBorder = Color3.fromRGB(129, 140, 248), + AcrylicGradient = ColorSequence.new(Color3.fromRGB(99, 102, 241), Color3.fromRGB(67, 56, 202)), + AcrylicNoise = 0.92, + TitleBarLine = Color3.fromRGB(99, 102, 241), + Tab = Color3.fromRGB(165, 180, 252), + Element = Color3.fromRGB(129, 140, 248), + ElementBorder = Color3.fromRGB(30, 27, 75), + InElementBorder = Color3.fromRGB(67, 56, 202), + ElementTransparency = 0.87, + ToggleSlider = Color3.fromRGB(129, 140, 248), + ToggleToggled = Color3.fromRGB(0, 0, 0), + SliderRail = Color3.fromRGB(129, 140, 248), + DropdownFrame = Color3.fromRGB(199, 210, 254), + DropdownHolder = Color3.fromRGB(30, 27, 75), + DropdownBorder = Color3.fromRGB(67, 56, 202), + DropdownOption = Color3.fromRGB(129, 140, 248), + Keybind = Color3.fromRGB(129, 140, 248), + Input = Color3.fromRGB(129, 140, 248), + InputFocused = Color3.fromRGB(30, 27, 75), + InputIndicator = Color3.fromRGB(199, 210, 254), + Dialog = Color3.fromRGB(30, 27, 75), + DialogHolder = Color3.fromRGB(67, 56, 202), + DialogHolderLine = Color3.fromRGB(79, 70, 229), + DialogButton = Color3.fromRGB(99, 102, 241), + DialogButtonBorder = Color3.fromRGB(129, 140, 248), + DialogBorder = Color3.fromRGB(67, 56, 202), + DialogInput = Color3.fromRGB(49, 46, 129), + DialogInputLine = Color3.fromRGB(199, 210, 254), + Text = Color3.fromRGB(238, 242, 255), + SubText = Color3.fromRGB(199, 210, 254), + Hover = Color3.fromRGB(129, 140, 248), + HoverChange = 0.04, +}) + +addTheme({ + Name = "Forest", + Accent = Color3.fromRGB(34, 197, 94), + AcrylicMain = Color3.fromRGB(20, 25, 20), + AcrylicBorder = Color3.fromRGB(74, 222, 128), + AcrylicGradient = ColorSequence.new(Color3.fromRGB(34, 197, 94), Color3.fromRGB(21, 128, 61)), + AcrylicNoise = 0.92, + TitleBarLine = Color3.fromRGB(34, 197, 94), + Tab = Color3.fromRGB(134, 239, 172), + Element = Color3.fromRGB(74, 222, 128), + ElementBorder = Color3.fromRGB(20, 83, 45), + InElementBorder = Color3.fromRGB(22, 163, 74), + ElementTransparency = 0.87, + ToggleSlider = Color3.fromRGB(74, 222, 128), + ToggleToggled = Color3.fromRGB(0, 0, 0), + SliderRail = Color3.fromRGB(74, 222, 128), + DropdownFrame = Color3.fromRGB(187, 247, 208), + DropdownHolder = Color3.fromRGB(20, 83, 45), + DropdownBorder = Color3.fromRGB(21, 128, 61), + DropdownOption = Color3.fromRGB(74, 222, 128), + Keybind = Color3.fromRGB(74, 222, 128), + Input = Color3.fromRGB(74, 222, 128), + InputFocused = Color3.fromRGB(20, 83, 45), + InputIndicator = Color3.fromRGB(187, 247, 208), + Dialog = Color3.fromRGB(20, 83, 45), + DialogHolder = Color3.fromRGB(21, 128, 61), + DialogHolderLine = Color3.fromRGB(22, 163, 74), + DialogButton = Color3.fromRGB(34, 197, 94), + DialogButtonBorder = Color3.fromRGB(74, 222, 128), + DialogBorder = Color3.fromRGB(22, 163, 74), + DialogInput = Color3.fromRGB(21, 128, 61), + DialogInputLine = Color3.fromRGB(187, 247, 208), + Text = Color3.fromRGB(240, 253, 244), + SubText = Color3.fromRGB(187, 247, 208), + Hover = Color3.fromRGB(74, 222, 128), + HoverChange = 0.04, +}) + +-- ============================================ +-- InterfaceManager +-- ============================================ +local InterfaceManager = {} +InterfaceManager.Folder = "FluentSettings" +InterfaceManager.Settings = { + Theme = "Dark", + Acrylic = true, + Transparency = true, + MenuKeybind = "LeftControl" +} + +-- safe checks for file API (exploit environment may vary) +local has_isfile = type(isfile) == "function" +local has_readfile = type(readfile) == "function" +local has_writefile = type(writefile) == "function" +local has_makefolder = type(makefolder) == "function" +local has_isfolder = type(isfolder) == "function" + +function InterfaceManager:SetFolder(folder) + self.Folder = folder or self.Folder + self:BuildFolderTree() +end + +function InterfaceManager:SetLibrary(library) + self.Library = library +end + +function InterfaceManager:BuildFolderTree() + if not has_makefolder or not has_isfolder then + -- cannot create folders in this environment; skip safely + return + end + + local paths = {} + local parts = string.split(self.Folder, "/") + for idx = 1, #parts do + paths[#paths + 1] = table.concat(parts, "/", 1, idx) + end + + 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() + if not has_writefile then + return + end + + local ok, encoded = pcall(function() + return HttpService:JSONEncode(self.Settings) + end) + if ok and type(encoded) == "string" then + pcall(function() + writefile(self.Folder .. "/options.json", encoded) + end) + end +end + +function InterfaceManager:LoadSettings() + if not has_isfile or not has_readfile then + return + end + + local path = self.Folder .. "/options.json" + if isfile(path) then + local raw = readfile(path) + local success, decoded = pcall(function() + return HttpService:JSONDecode(raw) + end) + if success and type(decoded) == "table" then + for k, v in pairs(decoded) do + self.Settings[k] = v + end + end + end +end + +-- BuildInterfaceSection expects a 'tab' object with AddSection / AddDropdown / AddToggle / AddKeybind methods +-- and a Library object that can SetTheme / ToggleAcrylic / ToggleTransparency etc. +function InterfaceManager:BuildInterfaceSection(tab) + assert(tab, "BuildInterfaceSection(tab) requires a tab object") + + local Library = self.Library or {} + local Settings = self.Settings + + -- fallback to module Themes if library doesn't provide a themes list + local themeValues = {} + if Library.Themes and type(Library.Themes.Names) == "table" then + themeValues = Library.Themes.Names + else + themeValues = Module.Themes.Names + end + + self:LoadSettings() + + local section = tab:AddSection and tab:AddSection("Interface") or error("tab:AddSection missing") + + local InterfaceTheme = section:AddDropdown and section:AddDropdown("InterfaceTheme", { + Title = "Theme", + Description = "Changes the interface theme.", + Values = themeValues, + Default = Settings.Theme, + Callback = function(Value) + if Library.SetTheme then + Library:SetTheme(Value) + end + Settings.Theme = Value + self:SaveSettings() + end + }) or error("section:AddDropdown missing") + + -- set initial value if dropdown supports SetValue + if InterfaceTheme and InterfaceTheme.SetValue then + InterfaceTheme:SetValue(Settings.Theme) + end + + if Library.UseAcrylic then + section:AddToggle("AcrylicToggle", { + Title = "Acrylic", + Description = "The blurred background requires graphic quality 8+", + Default = Settings.Acrylic, + Callback = function(Value) + if Library.ToggleAcrylic then + Library:ToggleAcrylic(Value) + end + Settings.Acrylic = Value + self:SaveSettings() + end + }) + end + + section:AddToggle("TransparentToggle", { + Title = "Transparency", + Description = "Makes the interface transparent.", + Default = Settings.Transparency, + Callback = function(Value) + if Library.ToggleTransparency then + Library:ToggleTransparency(Value) + end + Settings.Transparency = Value + self:SaveSettings() + end + }) + + local MenuKeybind = section:AddKeybind and section:AddKeybind("MenuKeybind", { Title = "Minimize Bind", Default = Settings.MenuKeybind }) + if MenuKeybind and MenuKeybind.OnChanged then + MenuKeybind:OnChanged(function() + Settings.MenuKeybind = MenuKeybind.Value + self:SaveSettings() + end) + if Library then + Library.MinimizeKeybind = MenuKeybind + end + end +end + +InterfaceManager.SetTheme = function(self, name) + -- convenience: set theme from module if available + if Module.Themes[name] then + self.Settings.Theme = name + self:SaveSettings() + if self.Library and self.Library.SetTheme then + self.Library:SetTheme(name) + end + end +end + +-- expose InterfaceManager on Module +Module.InterfaceManager = InterfaceManager + +-- convenience API: get theme data +function Module:GetTheme(name) + return self.Themes[name] +end + +function Module:GetThemeNames() + return self.Themes.Names +end + +-- convenience: add runtime theme +function Module:RegisterTheme(themeTable) + addTheme(themeTable) +end + +-- final return +return Module From bdde79c1ff6deb88ebe42d8402b80a54da646b78 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:48:50 +0700 Subject: [PATCH 33/76] Update config.lua --- config.lua | 644 ++++++++++++----------------------------------------- 1 file changed, 137 insertions(+), 507 deletions(-) diff --git a/config.lua b/config.lua index 094e503..f634f8f 100644 --- a/config.lua +++ b/config.lua @@ -1,406 +1,13 @@ --- CombinedThemesAndInterfaceManager.lua --- Single-module pack: Themes + InterfaceManager -local HttpService = game:GetService("HttpService") - -local Module = {} -Module.Themes = { - Names = {} -} - --- helper to register a theme into Module.Themes -local function addTheme(t) - if type(t) ~= "table" or type(t.Name) ~= "string" then return end - Module.Themes[t.Name] = t - table.insert(Module.Themes.Names, t.Name) -end - --- ============================================ --- Theme definitions (add or edit here) --- ============================================ - -addTheme({ - Name = "Emerald", - Accent = Color3.fromRGB(16, 185, 129), - AcrylicMain = Color3.fromRGB(18, 18, 18), - AcrylicBorder = Color3.fromRGB(52, 211, 153), - AcrylicGradient = ColorSequence.new(Color3.fromRGB(16, 185, 129), Color3.fromRGB(5, 150, 105)), - AcrylicNoise = 0.92, - TitleBarLine = Color3.fromRGB(16, 185, 129), - Tab = Color3.fromRGB(110, 231, 183), - Element = Color3.fromRGB(52, 211, 153), - ElementBorder = Color3.fromRGB(6, 95, 70), - InElementBorder = Color3.fromRGB(16, 185, 129), - ElementTransparency = 0.87, - ToggleSlider = Color3.fromRGB(52, 211, 153), - ToggleToggled = Color3.fromRGB(0, 0, 0), - SliderRail = Color3.fromRGB(52, 211, 153), - DropdownFrame = Color3.fromRGB(110, 231, 183), - DropdownHolder = Color3.fromRGB(6, 78, 59), - DropdownBorder = Color3.fromRGB(4, 120, 87), - DropdownOption = Color3.fromRGB(52, 211, 153), - Keybind = Color3.fromRGB(52, 211, 153), - Input = Color3.fromRGB(52, 211, 153), - InputFocused = Color3.fromRGB(6, 78, 59), - InputIndicator = Color3.fromRGB(110, 231, 183), - Dialog = Color3.fromRGB(6, 78, 59), - DialogHolder = Color3.fromRGB(4, 120, 87), - DialogHolderLine = Color3.fromRGB(6, 95, 70), - DialogButton = Color3.fromRGB(16, 185, 129), - DialogButtonBorder = Color3.fromRGB(52, 211, 153), - DialogBorder = Color3.fromRGB(16, 185, 129), - DialogInput = Color3.fromRGB(6, 95, 70), - DialogInputLine = Color3.fromRGB(110, 231, 183), - Text = Color3.fromRGB(240, 253, 244), - SubText = Color3.fromRGB(167, 243, 208), - Hover = Color3.fromRGB(52, 211, 153), - HoverChange = 0.04, -}) - -addTheme({ - Name = "Crimson", - Accent = Color3.fromRGB(220, 38, 38), - AcrylicMain = Color3.fromRGB(20, 20, 20), - AcrylicBorder = Color3.fromRGB(239, 68, 68), - AcrylicGradient = ColorSequence.new(Color3.fromRGB(220, 38, 38), Color3.fromRGB(153, 27, 27)), - AcrylicNoise = 0.92, - TitleBarLine = Color3.fromRGB(220, 38, 38), - Tab = Color3.fromRGB(252, 165, 165), - Element = Color3.fromRGB(239, 68, 68), - ElementBorder = Color3.fromRGB(127, 29, 29), - InElementBorder = Color3.fromRGB(185, 28, 28), - ElementTransparency = 0.87, - ToggleSlider = Color3.fromRGB(239, 68, 68), - ToggleToggled = Color3.fromRGB(0, 0, 0), - SliderRail = Color3.fromRGB(239, 68, 68), - DropdownFrame = Color3.fromRGB(252, 165, 165), - DropdownHolder = Color3.fromRGB(127, 29, 29), - DropdownBorder = Color3.fromRGB(153, 27, 27), - DropdownOption = Color3.fromRGB(239, 68, 68), - Keybind = Color3.fromRGB(239, 68, 68), - Input = Color3.fromRGB(239, 68, 68), - InputFocused = Color3.fromRGB(127, 29, 29), - InputIndicator = Color3.fromRGB(252, 165, 165), - Dialog = Color3.fromRGB(127, 29, 29), - DialogHolder = Color3.fromRGB(153, 27, 27), - DialogHolderLine = Color3.fromRGB(185, 28, 28), - DialogButton = Color3.fromRGB(220, 38, 38), - DialogButtonBorder = Color3.fromRGB(239, 68, 68), - DialogBorder = Color3.fromRGB(220, 38, 38), - DialogInput = Color3.fromRGB(153, 27, 27), - DialogInputLine = Color3.fromRGB(252, 165, 165), - Text = Color3.fromRGB(254, 242, 242), - SubText = Color3.fromRGB(254, 202, 202), - Hover = Color3.fromRGB(239, 68, 68), - HoverChange = 0.04, -}) - -addTheme({ - Name = "Ocean", - Accent = Color3.fromRGB(14, 116, 144), - AcrylicMain = Color3.fromRGB(15, 23, 42), - AcrylicBorder = Color3.fromRGB(34, 211, 238), - AcrylicGradient = ColorSequence.new(Color3.fromRGB(14, 116, 144), Color3.fromRGB(8, 51, 68)), - AcrylicNoise = 0.92, - TitleBarLine = Color3.fromRGB(14, 165, 233), - Tab = Color3.fromRGB(125, 211, 252), - Element = Color3.fromRGB(34, 211, 238), - ElementBorder = Color3.fromRGB(8, 51, 68), - InElementBorder = Color3.fromRGB(14, 116, 144), - ElementTransparency = 0.87, - ToggleSlider = Color3.fromRGB(34, 211, 238), - ToggleToggled = Color3.fromRGB(0, 0, 0), - SliderRail = Color3.fromRGB(34, 211, 238), - DropdownFrame = Color3.fromRGB(165, 243, 252), - DropdownHolder = Color3.fromRGB(8, 51, 68), - DropdownBorder = Color3.fromRGB(14, 116, 144), - DropdownOption = Color3.fromRGB(34, 211, 238), - Keybind = Color3.fromRGB(34, 211, 238), - Input = Color3.fromRGB(34, 211, 238), - InputFocused = Color3.fromRGB(8, 51, 68), - InputIndicator = Color3.fromRGB(165, 243, 252), - Dialog = Color3.fromRGB(8, 51, 68), - DialogHolder = Color3.fromRGB(14, 116, 144), - DialogHolderLine = Color3.fromRGB(6, 182, 212), - DialogButton = Color3.fromRGB(14, 165, 233), - DialogButtonBorder = Color3.fromRGB(34, 211, 238), - DialogBorder = Color3.fromRGB(14, 116, 144), - DialogInput = Color3.fromRGB(8, 51, 68), - DialogInputLine = Color3.fromRGB(165, 243, 252), - Text = Color3.fromRGB(240, 249, 255), - SubText = Color3.fromRGB(186, 230, 253), - Hover = Color3.fromRGB(34, 211, 238), - HoverChange = 0.04, -}) +-- InterfaceManager.lua +-- Replacement InterfaceManager module compatible with Fluent usage: +-- Usage: +-- local InterfaceManager = require(path.to.InterfaceManager) +-- InterfaceManager:SetLibrary(Fluent) +-- InterfaceManager:SetFolder("MyFolder") +-- InterfaceManager:BuildInterfaceSection(tab) -addTheme({ - Name = "Sunset", - Accent = Color3.fromRGB(251, 146, 60), - AcrylicMain = Color3.fromRGB(20, 20, 20), - AcrylicBorder = Color3.fromRGB(251, 146, 60), - AcrylicGradient = ColorSequence.new(Color3.fromRGB(251, 146, 60), Color3.fromRGB(234, 88, 12)), - AcrylicNoise = 0.92, - TitleBarLine = Color3.fromRGB(251, 146, 60), - Tab = Color3.fromRGB(254, 215, 170), - Element = Color3.fromRGB(251, 146, 60), - ElementBorder = Color3.fromRGB(124, 45, 18), - InElementBorder = Color3.fromRGB(194, 65, 12), - ElementTransparency = 0.87, - ToggleSlider = Color3.fromRGB(251, 146, 60), - ToggleToggled = Color3.fromRGB(0, 0, 0), - SliderRail = Color3.fromRGB(251, 146, 60), - DropdownFrame = Color3.fromRGB(254, 215, 170), - DropdownHolder = Color3.fromRGB(124, 45, 18), - DropdownBorder = Color3.fromRGB(194, 65, 12), - DropdownOption = Color3.fromRGB(251, 146, 60), - Keybind = Color3.fromRGB(251, 146, 60), - Input = Color3.fromRGB(251, 146, 60), - InputFocused = Color3.fromRGB(124, 45, 18), - InputIndicator = Color3.fromRGB(254, 215, 170), - Dialog = Color3.fromRGB(124, 45, 18), - DialogHolder = Color3.fromRGB(194, 65, 12), - DialogHolderLine = Color3.fromRGB(234, 88, 12), - DialogButton = Color3.fromRGB(234, 88, 12), - DialogButtonBorder = Color3.fromRGB(251, 146, 60), - DialogBorder = Color3.fromRGB(234, 88, 12), - DialogInput = Color3.fromRGB(194, 65, 12), - DialogInputLine = Color3.fromRGB(254, 215, 170), - Text = Color3.fromRGB(255, 251, 235), - SubText = Color3.fromRGB(254, 215, 170), - Hover = Color3.fromRGB(251, 146, 60), - HoverChange = 0.04, -}) - -addTheme({ - Name = "Lavender", - Accent = Color3.fromRGB(167, 139, 250), - AcrylicMain = Color3.fromRGB(20, 20, 25), - AcrylicBorder = Color3.fromRGB(196, 181, 253), - AcrylicGradient = ColorSequence.new(Color3.fromRGB(167, 139, 250), Color3.fromRGB(109, 40, 217)), - AcrylicNoise = 0.92, - TitleBarLine = Color3.fromRGB(167, 139, 250), - Tab = Color3.fromRGB(221, 214, 254), - Element = Color3.fromRGB(196, 181, 253), - ElementBorder = Color3.fromRGB(55, 48, 163), - InElementBorder = Color3.fromRGB(124, 58, 237), - ElementTransparency = 0.87, - ToggleSlider = Color3.fromRGB(196, 181, 253), - ToggleToggled = Color3.fromRGB(0, 0, 0), - SliderRail = Color3.fromRGB(196, 181, 253), - DropdownFrame = Color3.fromRGB(237, 233, 254), - DropdownHolder = Color3.fromRGB(55, 48, 163), - DropdownBorder = Color3.fromRGB(109, 40, 217), - DropdownOption = Color3.fromRGB(196, 181, 253), - Keybind = Color3.fromRGB(196, 181, 253), - Input = Color3.fromRGB(196, 181, 253), - InputFocused = Color3.fromRGB(55, 48, 163), - InputIndicator = Color3.fromRGB(237, 233, 254), - Dialog = Color3.fromRGB(55, 48, 163), - DialogHolder = Color3.fromRGB(109, 40, 217), - DialogHolderLine = Color3.fromRGB(124, 58, 237), - DialogButton = Color3.fromRGB(139, 92, 246), - DialogButtonBorder = Color3.fromRGB(196, 181, 253), - DialogBorder = Color3.fromRGB(124, 58, 237), - DialogInput = Color3.fromRGB(109, 40, 217), - DialogInputLine = Color3.fromRGB(237, 233, 254), - Text = Color3.fromRGB(250, 245, 255), - SubText = Color3.fromRGB(221, 214, 254), - Hover = Color3.fromRGB(196, 181, 253), - HoverChange = 0.04, -}) - -addTheme({ - Name = "Mint", - Accent = Color3.fromRGB(52, 211, 153), - AcrylicMain = Color3.fromRGB(17, 24, 39), - AcrylicBorder = Color3.fromRGB(110, 231, 183), - AcrylicGradient = ColorSequence.new(Color3.fromRGB(52, 211, 153), Color3.fromRGB(16, 185, 129)), - AcrylicNoise = 0.92, - TitleBarLine = Color3.fromRGB(52, 211, 153), - Tab = Color3.fromRGB(167, 243, 208), - Element = Color3.fromRGB(110, 231, 183), - ElementBorder = Color3.fromRGB(6, 78, 59), - InElementBorder = Color3.fromRGB(16, 185, 129), - ElementTransparency = 0.87, - ToggleSlider = Color3.fromRGB(110, 231, 183), - ToggleToggled = Color3.fromRGB(0, 0, 0), - SliderRail = Color3.fromRGB(110, 231, 183), - DropdownFrame = Color3.fromRGB(209, 250, 229), - DropdownHolder = Color3.fromRGB(6, 78, 59), - DropdownBorder = Color3.fromRGB(16, 185, 129), - DropdownOption = Color3.fromRGB(110, 231, 183), - Keybind = Color3.fromRGB(110, 231, 183), - Input = Color3.fromRGB(110, 231, 183), - InputFocused = Color3.fromRGB(6, 78, 59), - InputIndicator = Color3.fromRGB(209, 250, 229), - Dialog = Color3.fromRGB(6, 78, 59), - DialogHolder = Color3.fromRGB(16, 185, 129), - DialogHolderLine = Color3.fromRGB(52, 211, 153), - DialogButton = Color3.fromRGB(52, 211, 153), - DialogButtonBorder = Color3.fromRGB(110, 231, 183), - DialogBorder = Color3.fromRGB(16, 185, 129), - DialogInput = Color3.fromRGB(6, 95, 70), - DialogInputLine = Color3.fromRGB(209, 250, 229), - Text = Color3.fromRGB(236, 253, 245), - SubText = Color3.fromRGB(167, 243, 208), - Hover = Color3.fromRGB(110, 231, 183), - HoverChange = 0.04, -}) - -addTheme({ - Name = "Coral", - Accent = Color3.fromRGB(251, 113, 133), - AcrylicMain = Color3.fromRGB(24, 20, 25), - AcrylicBorder = Color3.fromRGB(251, 113, 133), - AcrylicGradient = ColorSequence.new(Color3.fromRGB(251, 113, 133), Color3.fromRGB(244, 63, 94)), - AcrylicNoise = 0.92, - TitleBarLine = Color3.fromRGB(251, 113, 133), - Tab = Color3.fromRGB(253, 164, 175), - Element = Color3.fromRGB(251, 113, 133), - ElementBorder = Color3.fromRGB(136, 19, 55), - InElementBorder = Color3.fromRGB(225, 29, 72), - ElementTransparency = 0.87, - ToggleSlider = Color3.fromRGB(251, 113, 133), - ToggleToggled = Color3.fromRGB(0, 0, 0), - SliderRail = Color3.fromRGB(251, 113, 133), - DropdownFrame = Color3.fromRGB(254, 205, 211), - DropdownHolder = Color3.fromRGB(136, 19, 55), - DropdownBorder = Color3.fromRGB(225, 29, 72), - DropdownOption = Color3.fromRGB(251, 113, 133), - Keybind = Color3.fromRGB(251, 113, 133), - Input = Color3.fromRGB(251, 113, 133), - InputFocused = Color3.fromRGB(136, 19, 55), - InputIndicator = Color3.fromRGB(254, 205, 211), - Dialog = Color3.fromRGB(136, 19, 55), - DialogHolder = Color3.fromRGB(225, 29, 72), - DialogHolderLine = Color3.fromRGB(244, 63, 94), - DialogButton = Color3.fromRGB(244, 63, 94), - DialogButtonBorder = Color3.fromRGB(251, 113, 133), - DialogBorder = Color3.fromRGB(225, 29, 72), - DialogInput = Color3.fromRGB(190, 18, 60), - DialogInputLine = Color3.fromRGB(254, 205, 211), - Text = Color3.fromRGB(255, 241, 242), - SubText = Color3.fromRGB(254, 205, 211), - Hover = Color3.fromRGB(251, 113, 133), - HoverChange = 0.04, -}) - -addTheme({ - Name = "Gold", - Accent = Color3.fromRGB(245, 158, 11), - AcrylicMain = Color3.fromRGB(20, 20, 18), - AcrylicBorder = Color3.fromRGB(251, 191, 36), - AcrylicGradient = ColorSequence.new(Color3.fromRGB(245, 158, 11), Color3.fromRGB(180, 83, 9)), - AcrylicNoise = 0.92, - TitleBarLine = Color3.fromRGB(245, 158, 11), - Tab = Color3.fromRGB(253, 224, 71), - Element = Color3.fromRGB(251, 191, 36), - ElementBorder = Color3.fromRGB(120, 53, 15), - InElementBorder = Color3.fromRGB(217, 119, 6), - ElementTransparency = 0.87, - ToggleSlider = Color3.fromRGB(251, 191, 36), - ToggleToggled = Color3.fromRGB(0, 0, 0), - SliderRail = Color3.fromRGB(251, 191, 36), - DropdownFrame = Color3.fromRGB(254, 240, 138), - DropdownHolder = Color3.fromRGB(120, 53, 15), - DropdownBorder = Color3.fromRGB(180, 83, 9), - DropdownOption = Color3.fromRGB(251, 191, 36), - Keybind = Color3.fromRGB(251, 191, 36), - Input = Color3.fromRGB(251, 191, 36), - InputFocused = Color3.fromRGB(120, 53, 15), - InputIndicator = Color3.fromRGB(254, 240, 138), - Dialog = Color3.fromRGB(120, 53, 15), - DialogHolder = Color3.fromRGB(180, 83, 9), - DialogHolderLine = Color3.fromRGB(217, 119, 6), - DialogButton = Color3.fromRGB(245, 158, 11), - DialogButtonBorder = Color3.fromRGB(251, 191, 36), - DialogBorder = Color3.fromRGB(217, 119, 6), - DialogInput = Color3.fromRGB(180, 83, 9), - DialogInputLine = Color3.fromRGB(254, 240, 138), - Text = Color3.fromRGB(254, 252, 232), - SubText = Color3.fromRGB(253, 230, 138), - Hover = Color3.fromRGB(251, 191, 36), - HoverChange = 0.04, -}) - -addTheme({ - Name = "Midnight", - Accent = Color3.fromRGB(99, 102, 241), - AcrylicMain = Color3.fromRGB(15, 23, 42), - AcrylicBorder = Color3.fromRGB(129, 140, 248), - AcrylicGradient = ColorSequence.new(Color3.fromRGB(99, 102, 241), Color3.fromRGB(67, 56, 202)), - AcrylicNoise = 0.92, - TitleBarLine = Color3.fromRGB(99, 102, 241), - Tab = Color3.fromRGB(165, 180, 252), - Element = Color3.fromRGB(129, 140, 248), - ElementBorder = Color3.fromRGB(30, 27, 75), - InElementBorder = Color3.fromRGB(67, 56, 202), - ElementTransparency = 0.87, - ToggleSlider = Color3.fromRGB(129, 140, 248), - ToggleToggled = Color3.fromRGB(0, 0, 0), - SliderRail = Color3.fromRGB(129, 140, 248), - DropdownFrame = Color3.fromRGB(199, 210, 254), - DropdownHolder = Color3.fromRGB(30, 27, 75), - DropdownBorder = Color3.fromRGB(67, 56, 202), - DropdownOption = Color3.fromRGB(129, 140, 248), - Keybind = Color3.fromRGB(129, 140, 248), - Input = Color3.fromRGB(129, 140, 248), - InputFocused = Color3.fromRGB(30, 27, 75), - InputIndicator = Color3.fromRGB(199, 210, 254), - Dialog = Color3.fromRGB(30, 27, 75), - DialogHolder = Color3.fromRGB(67, 56, 202), - DialogHolderLine = Color3.fromRGB(79, 70, 229), - DialogButton = Color3.fromRGB(99, 102, 241), - DialogButtonBorder = Color3.fromRGB(129, 140, 248), - DialogBorder = Color3.fromRGB(67, 56, 202), - DialogInput = Color3.fromRGB(49, 46, 129), - DialogInputLine = Color3.fromRGB(199, 210, 254), - Text = Color3.fromRGB(238, 242, 255), - SubText = Color3.fromRGB(199, 210, 254), - Hover = Color3.fromRGB(129, 140, 248), - HoverChange = 0.04, -}) - -addTheme({ - Name = "Forest", - Accent = Color3.fromRGB(34, 197, 94), - AcrylicMain = Color3.fromRGB(20, 25, 20), - AcrylicBorder = Color3.fromRGB(74, 222, 128), - AcrylicGradient = ColorSequence.new(Color3.fromRGB(34, 197, 94), Color3.fromRGB(21, 128, 61)), - AcrylicNoise = 0.92, - TitleBarLine = Color3.fromRGB(34, 197, 94), - Tab = Color3.fromRGB(134, 239, 172), - Element = Color3.fromRGB(74, 222, 128), - ElementBorder = Color3.fromRGB(20, 83, 45), - InElementBorder = Color3.fromRGB(22, 163, 74), - ElementTransparency = 0.87, - ToggleSlider = Color3.fromRGB(74, 222, 128), - ToggleToggled = Color3.fromRGB(0, 0, 0), - SliderRail = Color3.fromRGB(74, 222, 128), - DropdownFrame = Color3.fromRGB(187, 247, 208), - DropdownHolder = Color3.fromRGB(20, 83, 45), - DropdownBorder = Color3.fromRGB(21, 128, 61), - DropdownOption = Color3.fromRGB(74, 222, 128), - Keybind = Color3.fromRGB(74, 222, 128), - Input = Color3.fromRGB(74, 222, 128), - InputFocused = Color3.fromRGB(20, 83, 45), - InputIndicator = Color3.fromRGB(187, 247, 208), - Dialog = Color3.fromRGB(20, 83, 45), - DialogHolder = Color3.fromRGB(21, 128, 61), - DialogHolderLine = Color3.fromRGB(22, 163, 74), - DialogButton = Color3.fromRGB(34, 197, 94), - DialogButtonBorder = Color3.fromRGB(74, 222, 128), - DialogBorder = Color3.fromRGB(22, 163, 74), - DialogInput = Color3.fromRGB(21, 128, 61), - DialogInputLine = Color3.fromRGB(187, 247, 208), - Text = Color3.fromRGB(240, 253, 244), - SubText = Color3.fromRGB(187, 247, 208), - Hover = Color3.fromRGB(74, 222, 128), - HoverChange = 0.04, -}) +local HttpService = game:GetService("HttpService") --- ============================================ --- InterfaceManager --- ============================================ local InterfaceManager = {} InterfaceManager.Folder = "FluentSettings" InterfaceManager.Settings = { @@ -410,16 +17,40 @@ InterfaceManager.Settings = { MenuKeybind = "LeftControl" } --- safe checks for file API (exploit environment may vary) +-- File API detection (exploit environments vary) -------------------------------- local has_isfile = type(isfile) == "function" local has_readfile = type(readfile) == "function" local has_writefile = type(writefile) == "function" local has_makefolder = type(makefolder) == "function" local has_isfolder = type(isfolder) == "function" +-- Internal helpers -------------------------------------------------------------- +local function safeEncode(tab) + local ok, res = pcall(function() return HttpService:JSONEncode(tab) end) + if ok then return res end + return nil +end + +local function safeDecode(str) + local ok, res = pcall(function() return HttpService:JSONDecode(str) end) + if ok then return res end + return nil +end + +-- Fallback theme names (used if Fluent library doesn't provide Themes) +local FALLBACK_THEME_NAMES = { + "Dark","Darker","Light","Aqua","Amethyst","Rose","Emerald","Crimson", + "Ocean","Sunset","Lavender","Mint","Coral","Gold","Midnight","Forest" +} + +-- Public API ------------------------------------------------------------------- function InterfaceManager:SetFolder(folder) - self.Folder = folder or self.Folder - self:BuildFolderTree() + if type(folder) ~= "string" then return end + self.Folder = folder + -- try to build folder tree if possible + if has_makefolder and has_isfolder then + self:BuildFolderTree() + end end function InterfaceManager:SetLibrary(library) @@ -428,7 +59,6 @@ end function InterfaceManager:BuildFolderTree() if not has_makefolder or not has_isfolder then - -- cannot create folders in this environment; skip safely return end @@ -437,98 +67,117 @@ function InterfaceManager:BuildFolderTree() for idx = 1, #parts do paths[#paths + 1] = table.concat(parts, "/", 1, idx) end - table.insert(paths, self.Folder .. "/settings") for i = 1, #paths do - local str = paths[i] - if not isfolder(str) then - makefolder(str) + local p = paths[i] + if not isfolder(p) then + makefolder(p) end end end function InterfaceManager:SaveSettings() - if not has_writefile then - return - end - - local ok, encoded = pcall(function() - return HttpService:JSONEncode(self.Settings) + if not has_writefile then return end + local encoded = safeEncode(self.Settings) + if not encoded then return end + local ok = pcall(function() + writefile(self.Folder .. "/options.json", encoded) end) - if ok and type(encoded) == "string" then - pcall(function() - writefile(self.Folder .. "/options.json", encoded) - end) - end + return ok end function InterfaceManager:LoadSettings() - if not has_isfile or not has_readfile then - return - end - + if not has_isfile or not has_readfile then return end local path = self.Folder .. "/options.json" - if isfile(path) then - local raw = readfile(path) - local success, decoded = pcall(function() - return HttpService:JSONDecode(raw) - end) - if success and type(decoded) == "table" then - for k, v in pairs(decoded) do - self.Settings[k] = v - end + if not isfile(path) then return end + local raw = readfile(path) + local decoded = safeDecode(raw) + if type(decoded) == "table" then + for k, v in pairs(decoded) do + self.Settings[k] = v end end end --- BuildInterfaceSection expects a 'tab' object with AddSection / AddDropdown / AddToggle / AddKeybind methods --- and a Library object that can SetTheme / ToggleAcrylic / ToggleTransparency etc. +-- Convenience: change theme programmatically +function InterfaceManager:SetTheme(name) + if type(name) ~= "string" then return end + self.Settings.Theme = name + self:SaveSettings() + if self.Library and type(self.Library.SetTheme) == "function" then + pcall(function() self.Library:SetTheme(name) end) + end +end + +-- BuildInterfaceSection: +-- Expects 'tab' object with AddSection function and section with AddDropdown / AddToggle / AddKeybind methods. +-- The function will attempt to use Library.Themes.Names if available, otherwise fallback list. function InterfaceManager:BuildInterfaceSection(tab) - assert(tab, "BuildInterfaceSection(tab) requires a tab object") + assert(tab, "InterfaceManager:BuildInterfaceSection requires a tab object") local Library = self.Library or {} local Settings = self.Settings - -- fallback to module Themes if library doesn't provide a themes list - local themeValues = {} + self:LoadSettings() + + -- derive theme list from library if available + local themeValues = FALLBACK_THEME_NAMES if Library.Themes and type(Library.Themes.Names) == "table" then themeValues = Library.Themes.Names - else - themeValues = Module.Themes.Names + elseif Library.Themes and type(Library.Themes) == "table" then + -- maybe it's keyed by name; build a names list + local names = {} + for k, v in pairs(Library.Themes) do + if type(k) == "string" then + table.insert(names, k) + end + end + if #names > 0 then themeValues = names end end - self:LoadSettings() - - local section = tab:AddSection and tab:AddSection("Interface") or error("tab:AddSection missing") + -- Create Interface section + local section = nil + if tab.AddSection then + section = tab:AddSection("Interface") + else + error("tab:AddSection missing - cannot build interface section") + end - local InterfaceTheme = section:AddDropdown and section:AddDropdown("InterfaceTheme", { - Title = "Theme", - Description = "Changes the interface theme.", - Values = themeValues, - Default = Settings.Theme, - Callback = function(Value) - if Library.SetTheme then - Library:SetTheme(Value) + -- Dropdown for theme selection + if section.AddDropdown then + local InterfaceTheme = section:AddDropdown("InterfaceTheme", { + Title = "Theme", + Description = "Changes the interface theme.", + Values = themeValues, + Default = Settings.Theme, + Callback = function(Value) + -- call library set theme if present + if Library and type(Library.SetTheme) == "function" then + pcall(function() Library:SetTheme(Value) end) + end + Settings.Theme = Value + self:SaveSettings() end - Settings.Theme = Value - self:SaveSettings() - end - }) or error("section:AddDropdown missing") + }) - -- set initial value if dropdown supports SetValue - if InterfaceTheme and InterfaceTheme.SetValue then - InterfaceTheme:SetValue(Settings.Theme) + -- Set initial value if supported + if InterfaceTheme and type(InterfaceTheme.SetValue) == "function" then + pcall(function() InterfaceTheme:SetValue(Settings.Theme) end) + end + else + error("section:AddDropdown missing - cannot build theme dropdown") end - if Library.UseAcrylic then + -- Acrylic toggle (only add if library indicates support) + if Library and Library.UseAcrylic and section.AddToggle then section:AddToggle("AcrylicToggle", { Title = "Acrylic", Description = "The blurred background requires graphic quality 8+", Default = Settings.Acrylic, Callback = function(Value) - if Library.ToggleAcrylic then - Library:ToggleAcrylic(Value) + if Library and type(Library.ToggleAcrylic) == "function" then + pcall(function() Library:ToggleAcrylic(Value) end) end Settings.Acrylic = Value self:SaveSettings() @@ -536,58 +185,39 @@ function InterfaceManager:BuildInterfaceSection(tab) }) end - section:AddToggle("TransparentToggle", { - Title = "Transparency", - Description = "Makes the interface transparent.", - Default = Settings.Transparency, - Callback = function(Value) - if Library.ToggleTransparency then - Library:ToggleTransparency(Value) + -- Transparency toggle + if section.AddToggle then + section:AddToggle("TransparentToggle", { + Title = "Transparency", + Description = "Makes the interface transparent.", + Default = Settings.Transparency, + Callback = function(Value) + if Library and type(Library.ToggleTransparency) == "function" then + pcall(function() Library:ToggleTransparency(Value) end) + end + Settings.Transparency = Value + self:SaveSettings() end - Settings.Transparency = Value - self:SaveSettings() - end - }) + }) + end - local MenuKeybind = section:AddKeybind and section:AddKeybind("MenuKeybind", { Title = "Minimize Bind", Default = Settings.MenuKeybind }) - if MenuKeybind and MenuKeybind.OnChanged then - MenuKeybind:OnChanged(function() - Settings.MenuKeybind = MenuKeybind.Value - self:SaveSettings() - end) + -- Keybind for minimize (if tab supports AddKeybind) + if section.AddKeybind then + local MenuKeybind = section:AddKeybind("MenuKeybind", { Title = "Minimize Bind", Default = Settings.MenuKeybind }) + if MenuKeybind and type(MenuKeybind.OnChanged) == "function" then + MenuKeybind:OnChanged(function() + Settings.MenuKeybind = MenuKeybind.Value + self:SaveSettings() + end) + end + -- expose for library usage if Library then Library.MinimizeKeybind = MenuKeybind end end -end - -InterfaceManager.SetTheme = function(self, name) - -- convenience: set theme from module if available - if Module.Themes[name] then - self.Settings.Theme = name - self:SaveSettings() - if self.Library and self.Library.SetTheme then - self.Library:SetTheme(name) - end - end -end - --- expose InterfaceManager on Module -Module.InterfaceManager = InterfaceManager - --- convenience API: get theme data -function Module:GetTheme(name) - return self.Themes[name] -end - -function Module:GetThemeNames() - return self.Themes.Names -end --- convenience: add runtime theme -function Module:RegisterTheme(themeTable) - addTheme(themeTable) + return true end --- final return -return Module +-- Return module +return InterfaceManager From 4db7605aae4fa89c10ff3205c6df60122a6cb3fd Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:55:00 +0700 Subject: [PATCH 34/76] Update config.lua --- config.lua | 8 -------- 1 file changed, 8 deletions(-) diff --git a/config.lua b/config.lua index f634f8f..be8ba09 100644 --- a/config.lua +++ b/config.lua @@ -1,11 +1,3 @@ --- InterfaceManager.lua --- Replacement InterfaceManager module compatible with Fluent usage: --- Usage: --- local InterfaceManager = require(path.to.InterfaceManager) --- InterfaceManager:SetLibrary(Fluent) --- InterfaceManager:SetFolder("MyFolder") --- InterfaceManager:BuildInterfaceSection(tab) - local HttpService = game:GetService("HttpService") local InterfaceManager = {} From 773069340c25f7d6950dad0de02ab31c7ad28572 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Fri, 31 Oct 2025 16:07:34 +0700 Subject: [PATCH 35/76] Update config.lua --- config.lua | 460 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 449 insertions(+), 11 deletions(-) diff --git a/config.lua b/config.lua index be8ba09..f70a554 100644 --- a/config.lua +++ b/config.lua @@ -1,3 +1,4 @@ +-- Combined InterfaceManager + Themes (single module) local HttpService = game:GetService("HttpService") local InterfaceManager = {} @@ -9,14 +10,14 @@ InterfaceManager.Settings = { MenuKeybind = "LeftControl" } --- File API detection (exploit environments vary) -------------------------------- +-- File API detection (exploit environments vary) local has_isfile = type(isfile) == "function" local has_readfile = type(readfile) == "function" local has_writefile = type(writefile) == "function" local has_makefolder = type(makefolder) == "function" local has_isfolder = type(isfolder) == "function" --- Internal helpers -------------------------------------------------------------- +-- Internal helpers local function safeEncode(tab) local ok, res = pcall(function() return HttpService:JSONEncode(tab) end) if ok then return res end @@ -29,12 +30,447 @@ local function safeDecode(str) return nil end --- Fallback theme names (used if Fluent library doesn't provide Themes) -local FALLBACK_THEME_NAMES = { +-- ====================================================================== +-- Themes table (all themes embedded here) +-- ====================================================================== +local Themes = {} +Themes.Names = { "Dark","Darker","Light","Aqua","Amethyst","Rose","Emerald","Crimson", - "Ocean","Sunset","Lavender","Mint","Coral","Gold","Midnight","Forest" + "Ocean","Sunset","Lavender","Mint","Coral","Gold","Midnight","Forest", } +-- Emerald +Themes["Emerald"] = { + Name = "Emerald", + Accent = Color3.fromRGB(16, 185, 129), + AcrylicMain = Color3.fromRGB(18, 18, 18), + AcrylicBorder = Color3.fromRGB(52, 211, 153), + AcrylicGradient = ColorSequence.new(Color3.fromRGB(16, 185, 129), Color3.fromRGB(5, 150, 105)), + AcrylicNoise = 0.92, + TitleBarLine = Color3.fromRGB(16, 185, 129), + Tab = Color3.fromRGB(110, 231, 183), + Element = Color3.fromRGB(52, 211, 153), + ElementBorder = Color3.fromRGB(6, 95, 70), + InElementBorder = Color3.fromRGB(16, 185, 129), + ElementTransparency = 0.87, + ToggleSlider = Color3.fromRGB(52, 211, 153), + ToggleToggled = Color3.fromRGB(0, 0, 0), + SliderRail = Color3.fromRGB(52, 211, 153), + DropdownFrame = Color3.fromRGB(110, 231, 183), + DropdownHolder = Color3.fromRGB(6, 78, 59), + DropdownBorder = Color3.fromRGB(4, 120, 87), + DropdownOption = Color3.fromRGB(52, 211, 153), + Keybind = Color3.fromRGB(52, 211, 153), + Input = Color3.fromRGB(52, 211, 153), + InputFocused = Color3.fromRGB(6, 78, 59), + InputIndicator = Color3.fromRGB(110, 231, 183), + Dialog = Color3.fromRGB(6, 78, 59), + DialogHolder = Color3.fromRGB(4, 120, 87), + DialogHolderLine = Color3.fromRGB(6, 95, 70), + DialogButton = Color3.fromRGB(16, 185, 129), + DialogButtonBorder = Color3.fromRGB(52, 211, 153), + DialogBorder = Color3.fromRGB(16, 185, 129), + DialogInput = Color3.fromRGB(6, 95, 70), + DialogInputLine = Color3.fromRGB(110, 231, 183), + Text = Color3.fromRGB(240, 253, 244), + SubText = Color3.fromRGB(167, 243, 208), + Hover = Color3.fromRGB(52, 211, 153), + HoverChange = 0.04, +} + +-- Crimson +Themes["Crimson"] = { + Name = "Crimson", + Accent = Color3.fromRGB(220, 38, 38), + AcrylicMain = Color3.fromRGB(20, 20, 20), + AcrylicBorder = Color3.fromRGB(239, 68, 68), + AcrylicGradient = ColorSequence.new(Color3.fromRGB(220, 38, 38), Color3.fromRGB(153, 27, 27)), + AcrylicNoise = 0.92, + TitleBarLine = Color3.fromRGB(220, 38, 38), + Tab = Color3.fromRGB(252, 165, 165), + Element = Color3.fromRGB(239, 68, 68), + ElementBorder = Color3.fromRGB(127, 29, 29), + InElementBorder = Color3.fromRGB(185, 28, 28), + ElementTransparency = 0.87, + ToggleSlider = Color3.fromRGB(239, 68, 68), + ToggleToggled = Color3.fromRGB(0, 0, 0), + SliderRail = Color3.fromRGB(239, 68, 68), + DropdownFrame = Color3.fromRGB(252, 165, 165), + DropdownHolder = Color3.fromRGB(127, 29, 29), + DropdownBorder = Color3.fromRGB(153, 27, 27), + DropdownOption = Color3.fromRGB(239, 68, 68), + Keybind = Color3.fromRGB(239, 68, 68), + Input = Color3.fromRGB(239, 68, 68), + InputFocused = Color3.fromRGB(127, 29, 29), + InputIndicator = Color3.fromRGB(252, 165, 165), + Dialog = Color3.fromRGB(127, 29, 29), + DialogHolder = Color3.fromRGB(153, 27, 27), + DialogHolderLine = Color3.fromRGB(185, 28, 28), + DialogButton = Color3.fromRGB(220, 38, 38), + DialogButtonBorder = Color3.fromRGB(239, 68, 68), + DialogBorder = Color3.fromRGB(220, 38, 38), + DialogInput = Color3.fromRGB(153, 27, 27), + DialogInputLine = Color3.fromRGB(252, 165, 165), + Text = Color3.fromRGB(254, 242, 242), + SubText = Color3.fromRGB(254, 202, 202), + Hover = Color3.fromRGB(239, 68, 68), + HoverChange = 0.04, +} + +-- Ocean +Themes["Ocean"] = { + Name = "Ocean", + Accent = Color3.fromRGB(14, 116, 144), + AcrylicMain = Color3.fromRGB(15, 23, 42), + AcrylicBorder = Color3.fromRGB(34, 211, 238), + AcrylicGradient = ColorSequence.new(Color3.fromRGB(14, 116, 144), Color3.fromRGB(8, 51, 68)), + AcrylicNoise = 0.92, + TitleBarLine = Color3.fromRGB(14, 165, 233), + Tab = Color3.fromRGB(125, 211, 252), + Element = Color3.fromRGB(34, 211, 238), + ElementBorder = Color3.fromRGB(8, 51, 68), + InElementBorder = Color3.fromRGB(14, 116, 144), + ElementTransparency = 0.87, + ToggleSlider = Color3.fromRGB(34, 211, 238), + ToggleToggled = Color3.fromRGB(0, 0, 0), + SliderRail = Color3.fromRGB(34, 211, 238), + DropdownFrame = Color3.fromRGB(165, 243, 252), + DropdownHolder = Color3.fromRGB(8, 51, 68), + DropdownBorder = Color3.fromRGB(14, 116, 144), + DropdownOption = Color3.fromRGB(34, 211, 238), + Keybind = Color3.fromRGB(34, 211, 238), + Input = Color3.fromRGB(34, 211, 238), + InputFocused = Color3.fromRGB(8, 51, 68), + InputIndicator = Color3.fromRGB(165, 243, 252), + Dialog = Color3.fromRGB(8, 51, 68), + DialogHolder = Color3.fromRGB(14, 116, 144), + DialogHolderLine = Color3.fromRGB(6, 182, 212), + DialogButton = Color3.fromRGB(14, 165, 233), + DialogButtonBorder = Color3.fromRGB(34, 211, 238), + DialogBorder = Color3.fromRGB(14, 116, 144), + DialogInput = Color3.fromRGB(8, 51, 68), + DialogInputLine = Color3.fromRGB(165, 243, 252), + Text = Color3.fromRGB(240, 249, 255), + SubText = Color3.fromRGB(186, 230, 253), + Hover = Color3.fromRGB(34, 211, 238), + HoverChange = 0.04, +} + +-- Sunset +Themes["Sunset"] = { + Name = "Sunset", + Accent = Color3.fromRGB(251, 146, 60), + AcrylicMain = Color3.fromRGB(20, 20, 20), + AcrylicBorder = Color3.fromRGB(251, 146, 60), + AcrylicGradient = ColorSequence.new(Color3.fromRGB(251, 146, 60), Color3.fromRGB(234, 88, 12)), + AcrylicNoise = 0.92, + TitleBarLine = Color3.fromRGB(251, 146, 60), + Tab = Color3.fromRGB(254, 215, 170), + Element = Color3.fromRGB(251, 146, 60), + ElementBorder = Color3.fromRGB(124, 45, 18), + InElementBorder = Color3.fromRGB(194, 65, 12), + ElementTransparency = 0.87, + ToggleSlider = Color3.fromRGB(251, 146, 60), + ToggleToggled = Color3.fromRGB(0, 0, 0), + SliderRail = Color3.fromRGB(251, 146, 60), + DropdownFrame = Color3.fromRGB(254, 215, 170), + DropdownHolder = Color3.fromRGB(124, 45, 18), + DropdownBorder = Color3.fromRGB(194, 65, 12), + DropdownOption = Color3.fromRGB(251, 146, 60), + Keybind = Color3.fromRGB(251, 146, 60), + Input = Color3.fromRGB(251, 146, 60), + InputFocused = Color3.fromRGB(124, 45, 18), + InputIndicator = Color3.fromRGB(254, 215, 170), + Dialog = Color3.fromRGB(124, 45, 18), + DialogHolder = Color3.fromRGB(194, 65, 12), + DialogHolderLine = Color3.fromRGB(234, 88, 12), + DialogButton = Color3.fromRGB(234, 88, 12), + DialogButtonBorder = Color3.fromRGB(251, 146, 60), + DialogBorder = Color3.fromRGB(234, 88, 12), + DialogInput = Color3.fromRGB(194, 65, 12), + DialogInputLine = Color3.fromRGB(254, 215, 170), + Text = Color3.fromRGB(255, 251, 235), + SubText = Color3.fromRGB(254, 215, 170), + Hover = Color3.fromRGB(251, 146, 60), + HoverChange = 0.04, +} + +-- Lavender +Themes["Lavender"] = { + Name = "Lavender", + Accent = Color3.fromRGB(167, 139, 250), + AcrylicMain = Color3.fromRGB(20, 20, 25), + AcrylicBorder = Color3.fromRGB(196, 181, 253), + AcrylicGradient = ColorSequence.new(Color3.fromRGB(167, 139, 250), Color3.fromRGB(109, 40, 217)), + AcrylicNoise = 0.92, + TitleBarLine = Color3.fromRGB(167, 139, 250), + Tab = Color3.fromRGB(221, 214, 254), + Element = Color3.fromRGB(196, 181, 253), + ElementBorder = Color3.fromRGB(55, 48, 163), + InElementBorder = Color3.fromRGB(124, 58, 237), + ElementTransparency = 0.87, + ToggleSlider = Color3.fromRGB(196, 181, 253), + ToggleToggled = Color3.fromRGB(0, 0, 0), + SliderRail = Color3.fromRGB(196, 181, 253), + DropdownFrame = Color3.fromRGB(237, 233, 254), + DropdownHolder = Color3.fromRGB(55, 48, 163), + DropdownBorder = Color3.fromRGB(109, 40, 217), + DropdownOption = Color3.fromRGB(196, 181, 253), + Keybind = Color3.fromRGB(196, 181, 253), + Input = Color3.fromRGB(196, 181, 253), + InputFocused = Color3.fromRGB(55, 48, 163), + InputIndicator = Color3.fromRGB(237, 233, 254), + Dialog = Color3.fromRGB(55, 48, 163), + DialogHolder = Color3.fromRGB(109, 40, 217), + DialogHolderLine = Color3.fromRGB(124, 58, 237), + DialogButton = Color3.fromRGB(139, 92, 246), + DialogButtonBorder = Color3.fromRGB(196, 181, 253), + DialogBorder = Color3.fromRGB(124, 58, 237), + DialogInput = Color3.fromRGB(109, 40, 217), + DialogInputLine = Color3.fromRGB(237, 233, 254), + Text = Color3.fromRGB(250, 245, 255), + SubText = Color3.fromRGB(221, 214, 254), + Hover = Color3.fromRGB(196, 181, 253), + HoverChange = 0.04, +} + +-- Mint +Themes["Mint"] = { + Name = "Mint", + Accent = Color3.fromRGB(52, 211, 153), + AcrylicMain = Color3.fromRGB(17, 24, 39), + AcrylicBorder = Color3.fromRGB(110, 231, 183), + AcrylicGradient = ColorSequence.new(Color3.fromRGB(52, 211, 153), Color3.fromRGB(16, 185, 129)), + AcrylicNoise = 0.92, + TitleBarLine = Color3.fromRGB(52, 211, 153), + Tab = Color3.fromRGB(167, 243, 208), + Element = Color3.fromRGB(110, 231, 183), + ElementBorder = Color3.fromRGB(6, 78, 59), + InElementBorder = Color3.fromRGB(16, 185, 129), + ElementTransparency = 0.87, + ToggleSlider = Color3.fromRGB(110, 231, 183), + ToggleToggled = Color3.fromRGB(0, 0, 0), + SliderRail = Color3.fromRGB(110, 231, 183), + DropdownFrame = Color3.fromRGB(209, 250, 229), + DropdownHolder = Color3.fromRGB(6, 78, 59), + DropdownBorder = Color3.fromRGB(16, 185, 129), + DropdownOption = Color3.fromRGB(110, 231, 183), + Keybind = Color3.fromRGB(110, 231, 183), + Input = Color3.fromRGB(110, 231, 183), + InputFocused = Color3.fromRGB(6, 78, 59), + InputIndicator = Color3.fromRGB(209, 250, 229), + Dialog = Color3.fromRGB(6, 78, 59), + DialogHolder = Color3.fromRGB(16, 185, 129), + DialogHolderLine = Color3.fromRGB(52, 211, 153), + DialogButton = Color3.fromRGB(52, 211, 153), + DialogButtonBorder = Color3.fromRGB(110, 231, 183), + DialogBorder = Color3.fromRGB(16, 185, 129), + DialogInput = Color3.fromRGB(6, 95, 70), + DialogInputLine = Color3.fromRGB(209, 250, 229), + Text = Color3.fromRGB(236, 253, 245), + SubText = Color3.fromRGB(167, 243, 208), + Hover = Color3.fromRGB(110, 231, 183), + HoverChange = 0.04, +} + +-- Coral +Themes["Coral"] = { + Name = "Coral", + Accent = Color3.fromRGB(251, 113, 133), + AcrylicMain = Color3.fromRGB(24, 20, 25), + AcrylicBorder = Color3.fromRGB(251, 113, 133), + AcrylicGradient = ColorSequence.new(Color3.fromRGB(251, 113, 133), Color3.fromRGB(244, 63, 94)), + AcrylicNoise = 0.92, + TitleBarLine = Color3.fromRGB(251, 113, 133), + Tab = Color3.fromRGB(253, 164, 175), + Element = Color3.fromRGB(251, 113, 133), + ElementBorder = Color3.fromRGB(136, 19, 55), + InElementBorder = Color3.fromRGB(225, 29, 72), + ElementTransparency = 0.87, + ToggleSlider = Color3.fromRGB(251, 113, 133), + ToggleToggled = Color3.fromRGB(0, 0, 0), + SliderRail = Color3.fromRGB(251, 113, 133), + DropdownFrame = Color3.fromRGB(254, 205, 211), + DropdownHolder = Color3.fromRGB(136, 19, 55), + DropdownBorder = Color3.fromRGB(225, 29, 72), + DropdownOption = Color3.fromRGB(251, 113, 133), + Keybind = Color3.fromRGB(251, 113, 133), + Input = Color3.fromRGB(251, 113, 133), + InputFocused = Color3.fromRGB(136, 19, 55), + InputIndicator = Color3.fromRGB(254, 205, 211), + Dialog = Color3.fromRGB(136, 19, 55), + DialogHolder = Color3.fromRGB(225, 29, 72), + DialogHolderLine = Color3.fromRGB(244, 63, 94), + DialogButton = Color3.fromRGB(244, 63, 94), + DialogButtonBorder = Color3.fromRGB(251, 113, 133), + DialogBorder = Color3.fromRGB(225, 29, 72), + DialogInput = Color3.fromRGB(190, 18, 60), + DialogInputLine = Color3.fromRGB(254, 205, 211), + Text = Color3.fromRGB(255, 241, 242), + SubText = Color3.fromRGB(254, 205, 211), + Hover = Color3.fromRGB(251, 113, 133), + HoverChange = 0.04, +} + +-- Gold +Themes["Gold"] = { + Name = "Gold", + Accent = Color3.fromRGB(245, 158, 11), + AcrylicMain = Color3.fromRGB(20, 20, 18), + AcrylicBorder = Color3.fromRGB(251, 191, 36), + AcrylicGradient = ColorSequence.new(Color3.fromRGB(245, 158, 11), Color3.fromRGB(180, 83, 9)), + AcrylicNoise = 0.92, + TitleBarLine = Color3.fromRGB(245, 158, 11), + Tab = Color3.fromRGB(253, 224, 71), + Element = Color3.fromRGB(251, 191, 36), + ElementBorder = Color3.fromRGB(120, 53, 15), + InElementBorder = Color3.fromRGB(217, 119, 6), + ElementTransparency = 0.87, + ToggleSlider = Color3.fromRGB(251, 191, 36), + ToggleToggled = Color3.fromRGB(0, 0, 0), + SliderRail = Color3.fromRGB(251, 191, 36), + DropdownFrame = Color3.fromRGB(254, 240, 138), + DropdownHolder = Color3.fromRGB(120, 53, 15), + DropdownBorder = Color3.fromRGB(180, 83, 9), + DropdownOption = Color3.fromRGB(251, 191, 36), + Keybind = Color3.fromRGB(251, 191, 36), + Input = Color3.fromRGB(251, 191, 36), + InputFocused = Color3.fromRGB(120, 53, 15), + InputIndicator = Color3.fromRGB(254, 240, 138), + Dialog = Color3.fromRGB(120, 53, 15), + DialogHolder = Color3.fromRGB(180, 83, 9), + DialogHolderLine = Color3.fromRGB(217, 119, 6), + DialogButton = Color3.fromRGB(245, 158, 11), + DialogButtonBorder = Color3.fromRGB(251, 191, 36), + DialogBorder = Color3.fromRGB(217, 119, 6), + DialogInput = Color3.fromRGB(180, 83, 9), + DialogInputLine = Color3.fromRGB(254, 240, 138), + Text = Color3.fromRGB(254, 252, 232), + SubText = Color3.fromRGB(253, 230, 138), + Hover = Color3.fromRGB(251, 191, 36), + HoverChange = 0.04, +} + +-- Midnight +Themes["Midnight"] = { + Name = "Midnight", + Accent = Color3.fromRGB(99, 102, 241), + AcrylicMain = Color3.fromRGB(15, 23, 42), + AcrylicBorder = Color3.fromRGB(129, 140, 248), + AcrylicGradient = ColorSequence.new(Color3.fromRGB(99, 102, 241), Color3.fromRGB(67, 56, 202)), + AcrylicNoise = 0.92, + TitleBarLine = Color3.fromRGB(99, 102, 241), + Tab = Color3.fromRGB(165, 180, 252), + Element = Color3.fromRGB(129, 140, 248), + ElementBorder = Color3.fromRGB(30, 27, 75), + InElementBorder = Color3.fromRGB(67, 56, 202), + ElementTransparency = 0.87, + ToggleSlider = Color3.fromRGB(129, 140, 248), + ToggleToggled = Color3.fromRGB(0, 0, 0), + SliderRail = Color3.fromRGB(129, 140, 248), + DropdownFrame = Color3.fromRGB(199, 210, 254), + DropdownHolder = Color3.fromRGB(30, 27, 75), + DropdownBorder = Color3.fromRGB(67, 56, 202), + DropdownOption = Color3.fromRGB(129, 140, 248), + Keybind = Color3.fromRGB(129, 140, 248), + Input = Color3.fromRGB(129, 140, 248), + InputFocused = Color3.fromRGB(30, 27, 75), + InputIndicator = Color3.fromRGB(199, 210, 254), + Dialog = Color3.fromRGB(30, 27, 75), + DialogHolder = Color3.fromRGB(67, 56, 202), + DialogHolderLine = Color3.fromRGB(79, 70, 229), + DialogButton = Color3.fromRGB(99, 102, 241), + DialogButtonBorder = Color3.fromRGB(129, 140, 248), + DialogBorder = Color3.fromRGB(67, 56, 202), + DialogInput = Color3.fromRGB(49, 46, 129), + DialogInputLine = Color3.fromRGB(199, 210, 254), + Text = Color3.fromRGB(238, 242, 255), + SubText = Color3.fromRGB(199, 210, 254), + Hover = Color3.fromRGB(129, 140, 248), + HoverChange = 0.04, +} + +-- Forest +Themes["Forest"] = { + Name = "Forest", + Accent = Color3.fromRGB(34, 197, 94), + AcrylicMain = Color3.fromRGB(20, 25, 20), + AcrylicBorder = Color3.fromRGB(74, 222, 128), + AcrylicGradient = ColorSequence.new(Color3.fromRGB(34, 197, 94), Color3.fromRGB(21, 128, 61)), + AcrylicNoise = 0.92, + TitleBarLine = Color3.fromRGB(34, 197, 94), + Tab = Color3.fromRGB(134, 239, 172), + Element = Color3.fromRGB(74, 222, 128), + ElementBorder = Color3.fromRGB(20, 83, 45), + InElementBorder = Color3.fromRGB(22, 163, 74), + ElementTransparency = 0.87, + ToggleSlider = Color3.fromRGB(74, 222, 128), + ToggleToggled = Color3.fromRGB(0, 0, 0), + SliderRail = Color3.fromRGB(74, 222, 128), + DropdownFrame = Color3.fromRGB(187, 247, 208), + DropdownHolder = Color3.fromRGB(20, 83, 45), + DropdownBorder = Color3.fromRGB(21, 128, 61), + DropdownOption = Color3.fromRGB(74, 222, 128), + Keybind = Color3.fromRGB(74, 222, 128), + Input = Color3.fromRGB(74, 222, 128), + InputFocused = Color3.fromRGB(20, 83, 45), + InputIndicator = Color3.fromRGB(187, 247, 208), + Dialog = Color3.fromRGB(20, 83, 45), + DialogHolder = Color3.fromRGB(21, 128, 61), + DialogHolderLine = Color3.fromRGB(22, 163, 74), + DialogButton = Color3.fromRGB(34, 197, 94), + DialogButtonBorder = Color3.fromRGB(74, 222, 128), + DialogBorder = Color3.fromRGB(22, 163, 74), + DialogInput = Color3.fromRGB(21, 128, 61), + DialogInputLine = Color3.fromRGB(187, 247, 208), + Text = Color3.fromRGB(240, 253, 244), + SubText = Color3.fromRGB(187, 247, 208), + Hover = Color3.fromRGB(74, 222, 128), + HoverChange = 0.04, +} + +-- (You can add more themes by inserting into Themes[...] and adding the name to Themes.Names) + +-- ====================================================================== +-- Minimal "Library" wrapper so InterfaceManager can call library methods directly +-- ====================================================================== +local DefaultLibrary = {} +DefaultLibrary.Themes = Themes +DefaultLibrary.CurrentTheme = Themes[InterfaceManager.Settings.Theme] or nil + +function DefaultLibrary:SetTheme(name) + if type(name) ~= "string" then return end + InterfaceManager.Settings.Theme = name + DefaultLibrary.CurrentTheme = Themes[name] or DefaultLibrary.CurrentTheme + -- optional callback hook: InterfaceManager.OnThemeChanged + if InterfaceManager.OnThemeChanged then + pcall(function() InterfaceManager.OnThemeChanged(name, DefaultLibrary.CurrentTheme) end) + end +end + +function DefaultLibrary:ToggleAcrylic(enabled) + if type(enabled) ~= "boolean" then return end + InterfaceManager.Settings.Acrylic = enabled + if InterfaceManager.OnAcrylicToggled then + pcall(function() InterfaceManager.OnAcrylicToggled(enabled) end) + end +end + +function DefaultLibrary:ToggleTransparency(enabled) + if type(enabled) ~= "boolean" then return end + InterfaceManager.Settings.Transparency = enabled + if InterfaceManager.OnTransparencyToggled then + pcall(function() InterfaceManager.OnTransparencyToggled(enabled) end) + end +end + +-- expose a MinimizeKeybind placeholder (will be set by BuildInterfaceSection) +DefaultLibrary.MinimizeKeybind = nil + +-- Attach library into InterfaceManager by default +InterfaceManager.Library = DefaultLibrary +InterfaceManager.Themes = Themes + -- Public API ------------------------------------------------------------------- function InterfaceManager:SetFolder(folder) if type(folder) ~= "string" then return end @@ -46,7 +482,12 @@ function InterfaceManager:SetFolder(folder) end function InterfaceManager:SetLibrary(library) + if type(library) ~= "table" then return end self.Library = library + -- if the provided library has Themes, expose them too + if library.Themes then + self.Themes = library.Themes + end end function InterfaceManager:BuildFolderTree() @@ -102,9 +543,7 @@ function InterfaceManager:SetTheme(name) end end --- BuildInterfaceSection: --- Expects 'tab' object with AddSection function and section with AddDropdown / AddToggle / AddKeybind methods. --- The function will attempt to use Library.Themes.Names if available, otherwise fallback list. +-- BuildInterfaceSection (uses Library.Themes or fallback) function InterfaceManager:BuildInterfaceSection(tab) assert(tab, "InterfaceManager:BuildInterfaceSection requires a tab object") @@ -114,11 +553,10 @@ function InterfaceManager:BuildInterfaceSection(tab) self:LoadSettings() -- derive theme list from library if available - local themeValues = FALLBACK_THEME_NAMES + local themeValues = Themes.Names or {} if Library.Themes and type(Library.Themes.Names) == "table" then themeValues = Library.Themes.Names elseif Library.Themes and type(Library.Themes) == "table" then - -- maybe it's keyed by name; build a names list local names = {} for k, v in pairs(Library.Themes) do if type(k) == "string" then @@ -211,5 +649,5 @@ function InterfaceManager:BuildInterfaceSection(tab) return true end --- Return module +-- Return InterfaceManager as module (themes also accessible at InterfaceManager.Themes) return InterfaceManager From 06135a8a9171d88b821ed099342367252216e3e8 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Fri, 31 Oct 2025 16:15:40 +0700 Subject: [PATCH 36/76] Update config.lua --- config.lua | 397 ++++------------------------------------------------- 1 file changed, 27 insertions(+), 370 deletions(-) diff --git a/config.lua b/config.lua index f70a554..d756216 100644 --- a/config.lua +++ b/config.lua @@ -31,7 +31,7 @@ local function safeDecode(str) end -- ====================================================================== --- Themes table (all themes embedded here) +-- Themes table (embedded themes + auto-load from script children) -- ====================================================================== local Themes = {} Themes.Names = { @@ -39,7 +39,7 @@ Themes.Names = { "Ocean","Sunset","Lavender","Mint","Coral","Gold","Midnight","Forest", } --- Emerald +-- (Built-in theme examples) Themes["Emerald"] = { Name = "Emerald", Accent = Color3.fromRGB(16, 185, 129), @@ -78,358 +78,30 @@ Themes["Emerald"] = { HoverChange = 0.04, } --- Crimson -Themes["Crimson"] = { - Name = "Crimson", - Accent = Color3.fromRGB(220, 38, 38), - AcrylicMain = Color3.fromRGB(20, 20, 20), - AcrylicBorder = Color3.fromRGB(239, 68, 68), - AcrylicGradient = ColorSequence.new(Color3.fromRGB(220, 38, 38), Color3.fromRGB(153, 27, 27)), - AcrylicNoise = 0.92, - TitleBarLine = Color3.fromRGB(220, 38, 38), - Tab = Color3.fromRGB(252, 165, 165), - Element = Color3.fromRGB(239, 68, 68), - ElementBorder = Color3.fromRGB(127, 29, 29), - InElementBorder = Color3.fromRGB(185, 28, 28), - ElementTransparency = 0.87, - ToggleSlider = Color3.fromRGB(239, 68, 68), - ToggleToggled = Color3.fromRGB(0, 0, 0), - SliderRail = Color3.fromRGB(239, 68, 68), - DropdownFrame = Color3.fromRGB(252, 165, 165), - DropdownHolder = Color3.fromRGB(127, 29, 29), - DropdownBorder = Color3.fromRGB(153, 27, 27), - DropdownOption = Color3.fromRGB(239, 68, 68), - Keybind = Color3.fromRGB(239, 68, 68), - Input = Color3.fromRGB(239, 68, 68), - InputFocused = Color3.fromRGB(127, 29, 29), - InputIndicator = Color3.fromRGB(252, 165, 165), - Dialog = Color3.fromRGB(127, 29, 29), - DialogHolder = Color3.fromRGB(153, 27, 27), - DialogHolderLine = Color3.fromRGB(185, 28, 28), - DialogButton = Color3.fromRGB(220, 38, 38), - DialogButtonBorder = Color3.fromRGB(239, 68, 68), - DialogBorder = Color3.fromRGB(220, 38, 38), - DialogInput = Color3.fromRGB(153, 27, 27), - DialogInputLine = Color3.fromRGB(252, 165, 165), - Text = Color3.fromRGB(254, 242, 242), - SubText = Color3.fromRGB(254, 202, 202), - Hover = Color3.fromRGB(239, 68, 68), - HoverChange = 0.04, -} - --- Ocean -Themes["Ocean"] = { - Name = "Ocean", - Accent = Color3.fromRGB(14, 116, 144), - AcrylicMain = Color3.fromRGB(15, 23, 42), - AcrylicBorder = Color3.fromRGB(34, 211, 238), - AcrylicGradient = ColorSequence.new(Color3.fromRGB(14, 116, 144), Color3.fromRGB(8, 51, 68)), - AcrylicNoise = 0.92, - TitleBarLine = Color3.fromRGB(14, 165, 233), - Tab = Color3.fromRGB(125, 211, 252), - Element = Color3.fromRGB(34, 211, 238), - ElementBorder = Color3.fromRGB(8, 51, 68), - InElementBorder = Color3.fromRGB(14, 116, 144), - ElementTransparency = 0.87, - ToggleSlider = Color3.fromRGB(34, 211, 238), - ToggleToggled = Color3.fromRGB(0, 0, 0), - SliderRail = Color3.fromRGB(34, 211, 238), - DropdownFrame = Color3.fromRGB(165, 243, 252), - DropdownHolder = Color3.fromRGB(8, 51, 68), - DropdownBorder = Color3.fromRGB(14, 116, 144), - DropdownOption = Color3.fromRGB(34, 211, 238), - Keybind = Color3.fromRGB(34, 211, 238), - Input = Color3.fromRGB(34, 211, 238), - InputFocused = Color3.fromRGB(8, 51, 68), - InputIndicator = Color3.fromRGB(165, 243, 252), - Dialog = Color3.fromRGB(8, 51, 68), - DialogHolder = Color3.fromRGB(14, 116, 144), - DialogHolderLine = Color3.fromRGB(6, 182, 212), - DialogButton = Color3.fromRGB(14, 165, 233), - DialogButtonBorder = Color3.fromRGB(34, 211, 238), - DialogBorder = Color3.fromRGB(14, 116, 144), - DialogInput = Color3.fromRGB(8, 51, 68), - DialogInputLine = Color3.fromRGB(165, 243, 252), - Text = Color3.fromRGB(240, 249, 255), - SubText = Color3.fromRGB(186, 230, 253), - Hover = Color3.fromRGB(34, 211, 238), - HoverChange = 0.04, -} - --- Sunset -Themes["Sunset"] = { - Name = "Sunset", - Accent = Color3.fromRGB(251, 146, 60), - AcrylicMain = Color3.fromRGB(20, 20, 20), - AcrylicBorder = Color3.fromRGB(251, 146, 60), - AcrylicGradient = ColorSequence.new(Color3.fromRGB(251, 146, 60), Color3.fromRGB(234, 88, 12)), - AcrylicNoise = 0.92, - TitleBarLine = Color3.fromRGB(251, 146, 60), - Tab = Color3.fromRGB(254, 215, 170), - Element = Color3.fromRGB(251, 146, 60), - ElementBorder = Color3.fromRGB(124, 45, 18), - InElementBorder = Color3.fromRGB(194, 65, 12), - ElementTransparency = 0.87, - ToggleSlider = Color3.fromRGB(251, 146, 60), - ToggleToggled = Color3.fromRGB(0, 0, 0), - SliderRail = Color3.fromRGB(251, 146, 60), - DropdownFrame = Color3.fromRGB(254, 215, 170), - DropdownHolder = Color3.fromRGB(124, 45, 18), - DropdownBorder = Color3.fromRGB(194, 65, 12), - DropdownOption = Color3.fromRGB(251, 146, 60), - Keybind = Color3.fromRGB(251, 146, 60), - Input = Color3.fromRGB(251, 146, 60), - InputFocused = Color3.fromRGB(124, 45, 18), - InputIndicator = Color3.fromRGB(254, 215, 170), - Dialog = Color3.fromRGB(124, 45, 18), - DialogHolder = Color3.fromRGB(194, 65, 12), - DialogHolderLine = Color3.fromRGB(234, 88, 12), - DialogButton = Color3.fromRGB(234, 88, 12), - DialogButtonBorder = Color3.fromRGB(251, 146, 60), - DialogBorder = Color3.fromRGB(234, 88, 12), - DialogInput = Color3.fromRGB(194, 65, 12), - DialogInputLine = Color3.fromRGB(254, 215, 170), - Text = Color3.fromRGB(255, 251, 235), - SubText = Color3.fromRGB(254, 215, 170), - Hover = Color3.fromRGB(251, 146, 60), - HoverChange = 0.04, -} - --- Lavender -Themes["Lavender"] = { - Name = "Lavender", - Accent = Color3.fromRGB(167, 139, 250), - AcrylicMain = Color3.fromRGB(20, 20, 25), - AcrylicBorder = Color3.fromRGB(196, 181, 253), - AcrylicGradient = ColorSequence.new(Color3.fromRGB(167, 139, 250), Color3.fromRGB(109, 40, 217)), - AcrylicNoise = 0.92, - TitleBarLine = Color3.fromRGB(167, 139, 250), - Tab = Color3.fromRGB(221, 214, 254), - Element = Color3.fromRGB(196, 181, 253), - ElementBorder = Color3.fromRGB(55, 48, 163), - InElementBorder = Color3.fromRGB(124, 58, 237), - ElementTransparency = 0.87, - ToggleSlider = Color3.fromRGB(196, 181, 253), - ToggleToggled = Color3.fromRGB(0, 0, 0), - SliderRail = Color3.fromRGB(196, 181, 253), - DropdownFrame = Color3.fromRGB(237, 233, 254), - DropdownHolder = Color3.fromRGB(55, 48, 163), - DropdownBorder = Color3.fromRGB(109, 40, 217), - DropdownOption = Color3.fromRGB(196, 181, 253), - Keybind = Color3.fromRGB(196, 181, 253), - Input = Color3.fromRGB(196, 181, 253), - InputFocused = Color3.fromRGB(55, 48, 163), - InputIndicator = Color3.fromRGB(237, 233, 254), - Dialog = Color3.fromRGB(55, 48, 163), - DialogHolder = Color3.fromRGB(109, 40, 217), - DialogHolderLine = Color3.fromRGB(124, 58, 237), - DialogButton = Color3.fromRGB(139, 92, 246), - DialogButtonBorder = Color3.fromRGB(196, 181, 253), - DialogBorder = Color3.fromRGB(124, 58, 237), - DialogInput = Color3.fromRGB(109, 40, 217), - DialogInputLine = Color3.fromRGB(237, 233, 254), - Text = Color3.fromRGB(250, 245, 255), - SubText = Color3.fromRGB(221, 214, 254), - Hover = Color3.fromRGB(196, 181, 253), - HoverChange = 0.04, -} - --- Mint -Themes["Mint"] = { - Name = "Mint", - Accent = Color3.fromRGB(52, 211, 153), - AcrylicMain = Color3.fromRGB(17, 24, 39), - AcrylicBorder = Color3.fromRGB(110, 231, 183), - AcrylicGradient = ColorSequence.new(Color3.fromRGB(52, 211, 153), Color3.fromRGB(16, 185, 129)), - AcrylicNoise = 0.92, - TitleBarLine = Color3.fromRGB(52, 211, 153), - Tab = Color3.fromRGB(167, 243, 208), - Element = Color3.fromRGB(110, 231, 183), - ElementBorder = Color3.fromRGB(6, 78, 59), - InElementBorder = Color3.fromRGB(16, 185, 129), - ElementTransparency = 0.87, - ToggleSlider = Color3.fromRGB(110, 231, 183), - ToggleToggled = Color3.fromRGB(0, 0, 0), - SliderRail = Color3.fromRGB(110, 231, 183), - DropdownFrame = Color3.fromRGB(209, 250, 229), - DropdownHolder = Color3.fromRGB(6, 78, 59), - DropdownBorder = Color3.fromRGB(16, 185, 129), - DropdownOption = Color3.fromRGB(110, 231, 183), - Keybind = Color3.fromRGB(110, 231, 183), - Input = Color3.fromRGB(110, 231, 183), - InputFocused = Color3.fromRGB(6, 78, 59), - InputIndicator = Color3.fromRGB(209, 250, 229), - Dialog = Color3.fromRGB(6, 78, 59), - DialogHolder = Color3.fromRGB(16, 185, 129), - DialogHolderLine = Color3.fromRGB(52, 211, 153), - DialogButton = Color3.fromRGB(52, 211, 153), - DialogButtonBorder = Color3.fromRGB(110, 231, 183), - DialogBorder = Color3.fromRGB(16, 185, 129), - DialogInput = Color3.fromRGB(6, 95, 70), - DialogInputLine = Color3.fromRGB(209, 250, 229), - Text = Color3.fromRGB(236, 253, 245), - SubText = Color3.fromRGB(167, 243, 208), - Hover = Color3.fromRGB(110, 231, 183), - HoverChange = 0.04, -} - --- Coral -Themes["Coral"] = { - Name = "Coral", - Accent = Color3.fromRGB(251, 113, 133), - AcrylicMain = Color3.fromRGB(24, 20, 25), - AcrylicBorder = Color3.fromRGB(251, 113, 133), - AcrylicGradient = ColorSequence.new(Color3.fromRGB(251, 113, 133), Color3.fromRGB(244, 63, 94)), - AcrylicNoise = 0.92, - TitleBarLine = Color3.fromRGB(251, 113, 133), - Tab = Color3.fromRGB(253, 164, 175), - Element = Color3.fromRGB(251, 113, 133), - ElementBorder = Color3.fromRGB(136, 19, 55), - InElementBorder = Color3.fromRGB(225, 29, 72), - ElementTransparency = 0.87, - ToggleSlider = Color3.fromRGB(251, 113, 133), - ToggleToggled = Color3.fromRGB(0, 0, 0), - SliderRail = Color3.fromRGB(251, 113, 133), - DropdownFrame = Color3.fromRGB(254, 205, 211), - DropdownHolder = Color3.fromRGB(136, 19, 55), - DropdownBorder = Color3.fromRGB(225, 29, 72), - DropdownOption = Color3.fromRGB(251, 113, 133), - Keybind = Color3.fromRGB(251, 113, 133), - Input = Color3.fromRGB(251, 113, 133), - InputFocused = Color3.fromRGB(136, 19, 55), - InputIndicator = Color3.fromRGB(254, 205, 211), - Dialog = Color3.fromRGB(136, 19, 55), - DialogHolder = Color3.fromRGB(225, 29, 72), - DialogHolderLine = Color3.fromRGB(244, 63, 94), - DialogButton = Color3.fromRGB(244, 63, 94), - DialogButtonBorder = Color3.fromRGB(251, 113, 133), - DialogBorder = Color3.fromRGB(225, 29, 72), - DialogInput = Color3.fromRGB(190, 18, 60), - DialogInputLine = Color3.fromRGB(254, 205, 211), - Text = Color3.fromRGB(255, 241, 242), - SubText = Color3.fromRGB(254, 205, 211), - Hover = Color3.fromRGB(251, 113, 133), - HoverChange = 0.04, -} - --- Gold -Themes["Gold"] = { - Name = "Gold", - Accent = Color3.fromRGB(245, 158, 11), - AcrylicMain = Color3.fromRGB(20, 20, 18), - AcrylicBorder = Color3.fromRGB(251, 191, 36), - AcrylicGradient = ColorSequence.new(Color3.fromRGB(245, 158, 11), Color3.fromRGB(180, 83, 9)), - AcrylicNoise = 0.92, - TitleBarLine = Color3.fromRGB(245, 158, 11), - Tab = Color3.fromRGB(253, 224, 71), - Element = Color3.fromRGB(251, 191, 36), - ElementBorder = Color3.fromRGB(120, 53, 15), - InElementBorder = Color3.fromRGB(217, 119, 6), - ElementTransparency = 0.87, - ToggleSlider = Color3.fromRGB(251, 191, 36), - ToggleToggled = Color3.fromRGB(0, 0, 0), - SliderRail = Color3.fromRGB(251, 191, 36), - DropdownFrame = Color3.fromRGB(254, 240, 138), - DropdownHolder = Color3.fromRGB(120, 53, 15), - DropdownBorder = Color3.fromRGB(180, 83, 9), - DropdownOption = Color3.fromRGB(251, 191, 36), - Keybind = Color3.fromRGB(251, 191, 36), - Input = Color3.fromRGB(251, 191, 36), - InputFocused = Color3.fromRGB(120, 53, 15), - InputIndicator = Color3.fromRGB(254, 240, 138), - Dialog = Color3.fromRGB(120, 53, 15), - DialogHolder = Color3.fromRGB(180, 83, 9), - DialogHolderLine = Color3.fromRGB(217, 119, 6), - DialogButton = Color3.fromRGB(245, 158, 11), - DialogButtonBorder = Color3.fromRGB(251, 191, 36), - DialogBorder = Color3.fromRGB(217, 119, 6), - DialogInput = Color3.fromRGB(180, 83, 9), - DialogInputLine = Color3.fromRGB(254, 240, 138), - Text = Color3.fromRGB(254, 252, 232), - SubText = Color3.fromRGB(253, 230, 138), - Hover = Color3.fromRGB(251, 191, 36), - HoverChange = 0.04, -} - --- Midnight -Themes["Midnight"] = { - Name = "Midnight", - Accent = Color3.fromRGB(99, 102, 241), - AcrylicMain = Color3.fromRGB(15, 23, 42), - AcrylicBorder = Color3.fromRGB(129, 140, 248), - AcrylicGradient = ColorSequence.new(Color3.fromRGB(99, 102, 241), Color3.fromRGB(67, 56, 202)), - AcrylicNoise = 0.92, - TitleBarLine = Color3.fromRGB(99, 102, 241), - Tab = Color3.fromRGB(165, 180, 252), - Element = Color3.fromRGB(129, 140, 248), - ElementBorder = Color3.fromRGB(30, 27, 75), - InElementBorder = Color3.fromRGB(67, 56, 202), - ElementTransparency = 0.87, - ToggleSlider = Color3.fromRGB(129, 140, 248), - ToggleToggled = Color3.fromRGB(0, 0, 0), - SliderRail = Color3.fromRGB(129, 140, 248), - DropdownFrame = Color3.fromRGB(199, 210, 254), - DropdownHolder = Color3.fromRGB(30, 27, 75), - DropdownBorder = Color3.fromRGB(67, 56, 202), - DropdownOption = Color3.fromRGB(129, 140, 248), - Keybind = Color3.fromRGB(129, 140, 248), - Input = Color3.fromRGB(129, 140, 248), - InputFocused = Color3.fromRGB(30, 27, 75), - InputIndicator = Color3.fromRGB(199, 210, 254), - Dialog = Color3.fromRGB(30, 27, 75), - DialogHolder = Color3.fromRGB(67, 56, 202), - DialogHolderLine = Color3.fromRGB(79, 70, 229), - DialogButton = Color3.fromRGB(99, 102, 241), - DialogButtonBorder = Color3.fromRGB(129, 140, 248), - DialogBorder = Color3.fromRGB(67, 56, 202), - DialogInput = Color3.fromRGB(49, 46, 129), - DialogInputLine = Color3.fromRGB(199, 210, 254), - Text = Color3.fromRGB(238, 242, 255), - SubText = Color3.fromRGB(199, 210, 254), - Hover = Color3.fromRGB(129, 140, 248), - HoverChange = 0.04, -} - --- Forest -Themes["Forest"] = { - Name = "Forest", - Accent = Color3.fromRGB(34, 197, 94), - AcrylicMain = Color3.fromRGB(20, 25, 20), - AcrylicBorder = Color3.fromRGB(74, 222, 128), - AcrylicGradient = ColorSequence.new(Color3.fromRGB(34, 197, 94), Color3.fromRGB(21, 128, 61)), - AcrylicNoise = 0.92, - TitleBarLine = Color3.fromRGB(34, 197, 94), - Tab = Color3.fromRGB(134, 239, 172), - Element = Color3.fromRGB(74, 222, 128), - ElementBorder = Color3.fromRGB(20, 83, 45), - InElementBorder = Color3.fromRGB(22, 163, 74), - ElementTransparency = 0.87, - ToggleSlider = Color3.fromRGB(74, 222, 128), - ToggleToggled = Color3.fromRGB(0, 0, 0), - SliderRail = Color3.fromRGB(74, 222, 128), - DropdownFrame = Color3.fromRGB(187, 247, 208), - DropdownHolder = Color3.fromRGB(20, 83, 45), - DropdownBorder = Color3.fromRGB(21, 128, 61), - DropdownOption = Color3.fromRGB(74, 222, 128), - Keybind = Color3.fromRGB(74, 222, 128), - Input = Color3.fromRGB(74, 222, 128), - InputFocused = Color3.fromRGB(20, 83, 45), - InputIndicator = Color3.fromRGB(187, 247, 208), - Dialog = Color3.fromRGB(20, 83, 45), - DialogHolder = Color3.fromRGB(21, 128, 61), - DialogHolderLine = Color3.fromRGB(22, 163, 74), - DialogButton = Color3.fromRGB(34, 197, 94), - DialogButtonBorder = Color3.fromRGB(74, 222, 128), - DialogBorder = Color3.fromRGB(22, 163, 74), - DialogInput = Color3.fromRGB(21, 128, 61), - DialogInputLine = Color3.fromRGB(187, 247, 208), - Text = Color3.fromRGB(240, 253, 244), - SubText = Color3.fromRGB(187, 247, 208), - Hover = Color3.fromRGB(74, 222, 128), - HoverChange = 0.04, -} - --- (You can add more themes by inserting into Themes[...] and adding the name to Themes.Names) +-- (other built-in themes omitted here for brevity in this snippet; +-- include the rest exactly as in your working file) + +-- Auto-load child modules as themes (like your snippet): +if type(script) == "table" and type(script.GetChildren) == "function" then + for _, child in next, script:GetChildren() do + -- try to require; pcall so a bad child won't crash the module + local ok, required = pcall(function() return require(child) end) + if ok and type(required) == "table" and type(required.Name) == "string" then + Themes[required.Name] = required + -- add name if not already in Names + local exists = false + for _, n in ipairs(Themes.Names) do + if n == required.Name then + exists = true + break + end + end + if not exists then + table.insert(Themes.Names, required.Name) + end + end + end +end -- ====================================================================== -- Minimal "Library" wrapper so InterfaceManager can call library methods directly @@ -442,7 +114,6 @@ function DefaultLibrary:SetTheme(name) if type(name) ~= "string" then return end InterfaceManager.Settings.Theme = name DefaultLibrary.CurrentTheme = Themes[name] or DefaultLibrary.CurrentTheme - -- optional callback hook: InterfaceManager.OnThemeChanged if InterfaceManager.OnThemeChanged then pcall(function() InterfaceManager.OnThemeChanged(name, DefaultLibrary.CurrentTheme) end) end @@ -464,7 +135,6 @@ function DefaultLibrary:ToggleTransparency(enabled) end end --- expose a MinimizeKeybind placeholder (will be set by BuildInterfaceSection) DefaultLibrary.MinimizeKeybind = nil -- Attach library into InterfaceManager by default @@ -475,7 +145,6 @@ InterfaceManager.Themes = Themes function InterfaceManager:SetFolder(folder) if type(folder) ~= "string" then return end self.Folder = folder - -- try to build folder tree if possible if has_makefolder and has_isfolder then self:BuildFolderTree() end @@ -484,7 +153,6 @@ end function InterfaceManager:SetLibrary(library) if type(library) ~= "table" then return end self.Library = library - -- if the provided library has Themes, expose them too if library.Themes then self.Themes = library.Themes end @@ -533,7 +201,6 @@ function InterfaceManager:LoadSettings() end end --- Convenience: change theme programmatically function InterfaceManager:SetTheme(name) if type(name) ~= "string" then return end self.Settings.Theme = name @@ -543,7 +210,6 @@ function InterfaceManager:SetTheme(name) end end --- BuildInterfaceSection (uses Library.Themes or fallback) function InterfaceManager:BuildInterfaceSection(tab) assert(tab, "InterfaceManager:BuildInterfaceSection requires a tab object") @@ -552,7 +218,6 @@ function InterfaceManager:BuildInterfaceSection(tab) self:LoadSettings() - -- derive theme list from library if available local themeValues = Themes.Names or {} if Library.Themes and type(Library.Themes.Names) == "table" then themeValues = Library.Themes.Names @@ -566,7 +231,6 @@ function InterfaceManager:BuildInterfaceSection(tab) if #names > 0 then themeValues = names end end - -- Create Interface section local section = nil if tab.AddSection then section = tab:AddSection("Interface") @@ -574,7 +238,6 @@ function InterfaceManager:BuildInterfaceSection(tab) error("tab:AddSection missing - cannot build interface section") end - -- Dropdown for theme selection if section.AddDropdown then local InterfaceTheme = section:AddDropdown("InterfaceTheme", { Title = "Theme", @@ -582,7 +245,6 @@ function InterfaceManager:BuildInterfaceSection(tab) Values = themeValues, Default = Settings.Theme, Callback = function(Value) - -- call library set theme if present if Library and type(Library.SetTheme) == "function" then pcall(function() Library:SetTheme(Value) end) end @@ -591,7 +253,6 @@ function InterfaceManager:BuildInterfaceSection(tab) end }) - -- Set initial value if supported if InterfaceTheme and type(InterfaceTheme.SetValue) == "function" then pcall(function() InterfaceTheme:SetValue(Settings.Theme) end) end @@ -599,7 +260,6 @@ function InterfaceManager:BuildInterfaceSection(tab) error("section:AddDropdown missing - cannot build theme dropdown") end - -- Acrylic toggle (only add if library indicates support) if Library and Library.UseAcrylic and section.AddToggle then section:AddToggle("AcrylicToggle", { Title = "Acrylic", @@ -615,7 +275,6 @@ function InterfaceManager:BuildInterfaceSection(tab) }) end - -- Transparency toggle if section.AddToggle then section:AddToggle("TransparentToggle", { Title = "Transparency", @@ -631,7 +290,6 @@ function InterfaceManager:BuildInterfaceSection(tab) }) end - -- Keybind for minimize (if tab supports AddKeybind) if section.AddKeybind then local MenuKeybind = section:AddKeybind("MenuKeybind", { Title = "Minimize Bind", Default = Settings.MenuKeybind }) if MenuKeybind and type(MenuKeybind.OnChanged) == "function" then @@ -640,7 +298,6 @@ function InterfaceManager:BuildInterfaceSection(tab) self:SaveSettings() end) end - -- expose for library usage if Library then Library.MinimizeKeybind = MenuKeybind end @@ -649,5 +306,5 @@ function InterfaceManager:BuildInterfaceSection(tab) return true end --- Return InterfaceManager as module (themes also accessible at InterfaceManager.Themes) +-- Return InterfaceManager as module (themes accessible at InterfaceManager.Themes) return InterfaceManager From af41823cfe5b3c35c90cedb7f487e21ff48e8059 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Fri, 31 Oct 2025 16:22:26 +0700 Subject: [PATCH 37/76] Update config.lua --- config.lua | 252 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 218 insertions(+), 34 deletions(-) diff --git a/config.lua b/config.lua index d756216..f4900b0 100644 --- a/config.lua +++ b/config.lua @@ -1,4 +1,14 @@ --- Combined InterfaceManager + Themes (single module) +-- InterfaceManager (improved) +-- - embeds built-in themes +-- - auto-loads theme modules from script:GetChildren() +-- - ApplyTheme tries multiple library/window APIs as fallback +-- - Save/Load settings to file if file API available +-- - BuildInterfaceSection uses SetTheme which calls ApplyTheme +-- Usage: +-- local IM = require(path.to.InterfaceManager) +-- IM:SetLibrary(Fluent) -- optional (will default to internal DefaultLibrary) +-- IM:BuildInterfaceSection(tab) + local HttpService = game:GetService("HttpService") local InterfaceManager = {} @@ -17,7 +27,7 @@ local has_writefile = type(writefile) == "function" local has_makefolder = type(makefolder) == "function" local has_isfolder = type(isfolder) == "function" --- Internal helpers +-- Internal helpers --------------------------------------------------------- local function safeEncode(tab) local ok, res = pcall(function() return HttpService:JSONEncode(tab) end) if ok then return res end @@ -31,7 +41,7 @@ local function safeDecode(str) end -- ====================================================================== --- Themes table (embedded themes + auto-load from script children) +-- Themes table (embedded) -- ====================================================================== local Themes = {} Themes.Names = { @@ -39,7 +49,7 @@ Themes.Names = { "Ocean","Sunset","Lavender","Mint","Coral","Gold","Midnight","Forest", } --- (Built-in theme examples) +-- (Only a subset expanded here for brevity — include any themes you want) Themes["Emerald"] = { Name = "Emerald", Accent = Color3.fromRGB(16, 185, 129), @@ -78,49 +88,92 @@ Themes["Emerald"] = { HoverChange = 0.04, } --- (other built-in themes omitted here for brevity in this snippet; --- include the rest exactly as in your working file) +Themes["Crimson"] = { + Name = "Crimson", + Accent = Color3.fromRGB(220, 38, 38), + AcrylicMain = Color3.fromRGB(20, 20, 20), + AcrylicBorder = Color3.fromRGB(239, 68, 68), + AcrylicGradient = ColorSequence.new(Color3.fromRGB(220, 38, 38), Color3.fromRGB(153, 27, 27)), + AcrylicNoise = 0.92, + TitleBarLine = Color3.fromRGB(220, 38, 38), + Tab = Color3.fromRGB(252, 165, 165), + Element = Color3.fromRGB(239, 68, 68), + ElementBorder = Color3.fromRGB(127, 29, 29), + InElementBorder = Color3.fromRGB(185, 28, 28), + ElementTransparency = 0.87, + ToggleSlider = Color3.fromRGB(239, 68, 68), + ToggleToggled = Color3.fromRGB(0, 0, 0), + SliderRail = Color3.fromRGB(239, 68, 68), + DropdownFrame = Color3.fromRGB(252, 165, 165), + DropdownHolder = Color3.fromRGB(127, 29, 29), + DropdownBorder = Color3.fromRGB(153, 27, 27), + DropdownOption = Color3.fromRGB(239, 68, 68), + Keybind = Color3.fromRGB(239, 68, 68), + Input = Color3.fromRGB(239, 68, 68), + InputFocused = Color3.fromRGB(127, 29, 29), + InputIndicator = Color3.fromRGB(252, 165, 165), + Dialog = Color3.fromRGB(127, 29, 29), + DialogHolder = Color3.fromRGB(153, 27, 27), + DialogHolderLine = Color3.fromRGB(185, 28, 28), + DialogButton = Color3.fromRGB(220, 38, 38), + DialogButtonBorder = Color3.fromRGB(239, 68, 68), + DialogBorder = Color3.fromRGB(220, 38, 38), + DialogInput = Color3.fromRGB(153, 27, 27), + DialogInputLine = Color3.fromRGB(252, 165, 165), + Text = Color3.fromRGB(254, 242, 242), + SubText = Color3.fromRGB(254, 202, 202), + Hover = Color3.fromRGB(239, 68, 68), + HoverChange = 0.04, +} + +-- Add more embedded themes here as needed... +-- ====================================================================== --- Auto-load child modules as themes (like your snippet): +-- Auto-load child modules as themes (safe require) if type(script) == "table" and type(script.GetChildren) == "function" then for _, child in next, script:GetChildren() do - -- try to require; pcall so a bad child won't crash the module local ok, required = pcall(function() return require(child) end) if ok and type(required) == "table" and type(required.Name) == "string" then Themes[required.Name] = required - -- add name if not already in Names + -- add name to Names if not present local exists = false for _, n in ipairs(Themes.Names) do - if n == required.Name then - exists = true - break - end + if n == required.Name then exists = true break end end if not exists then table.insert(Themes.Names, required.Name) end + else + if not ok then + -- debug: child failed to require; don't error out + warn("[InterfaceManager] failed to require child theme:", tostring(child.Name), tostring(required)) + end end end end -- ====================================================================== --- Minimal "Library" wrapper so InterfaceManager can call library methods directly +-- DefaultLibrary wrapper — so IM:SetLibrary() can accept other libs -- ====================================================================== local DefaultLibrary = {} DefaultLibrary.Themes = Themes DefaultLibrary.CurrentTheme = Themes[InterfaceManager.Settings.Theme] or nil +DefaultLibrary.MinimizeKeybind = nil +DefaultLibrary.UseAcrylic = true -- flag for BuildInterfaceSection to show Acrylic toggle -function DefaultLibrary:SetTheme(name) - if type(name) ~= "string" then return end - InterfaceManager.Settings.Theme = name - DefaultLibrary.CurrentTheme = Themes[name] or DefaultLibrary.CurrentTheme +function DefaultLibrary:SetTheme(nameOrTable) + if type(nameOrTable) == "string" then + DefaultLibrary.CurrentTheme = Themes[nameOrTable] or DefaultLibrary.CurrentTheme + elseif type(nameOrTable) == "table" and type(nameOrTable.Name) == "string" then + DefaultLibrary.CurrentTheme = nameOrTable + end + -- If user provided a hook, call it if InterfaceManager.OnThemeChanged then - pcall(function() InterfaceManager.OnThemeChanged(name, DefaultLibrary.CurrentTheme) end) + pcall(function() InterfaceManager.OnThemeChanged(DefaultLibrary.CurrentTheme and DefaultLibrary.CurrentTheme.Name or nil, DefaultLibrary.CurrentTheme) end) end end function DefaultLibrary:ToggleAcrylic(enabled) - if type(enabled) ~= "boolean" then return end InterfaceManager.Settings.Acrylic = enabled if InterfaceManager.OnAcrylicToggled then pcall(function() InterfaceManager.OnAcrylicToggled(enabled) end) @@ -128,20 +181,19 @@ function DefaultLibrary:ToggleAcrylic(enabled) end function DefaultLibrary:ToggleTransparency(enabled) - if type(enabled) ~= "boolean" then return end InterfaceManager.Settings.Transparency = enabled if InterfaceManager.OnTransparencyToggled then pcall(function() InterfaceManager.OnTransparencyToggled(enabled) end) end end -DefaultLibrary.MinimizeKeybind = nil - --- Attach library into InterfaceManager by default +-- Attach by default InterfaceManager.Library = DefaultLibrary InterfaceManager.Themes = Themes --- Public API ------------------------------------------------------------------- +-- ====================================================================== +-- Public API: File/Folder management +-- ====================================================================== function InterfaceManager:SetFolder(folder) if type(folder) ~= "string" then return end self.Folder = folder @@ -201,36 +253,142 @@ function InterfaceManager:LoadSettings() end end +-- ====================================================================== +-- Theme applying logic (tries many fallbacks) +-- ====================================================================== +function InterfaceManager:ApplyTheme(name) + if type(name) ~= "string" then return false end + + local lib = self.Library or {} + local themeTable = nil + + -- Find theme table from library or internal Themes + if lib.Themes and type(lib.Themes[name]) == "table" then + themeTable = lib.Themes[name] + elseif self.Themes and type(self.Themes[name]) == "table" then + themeTable = self.Themes[name] + end + + local tried = {} + local success = false + + local function tryCall(target, fnName, arg) + if success then return end + if not target or type(target[fnName]) ~= "function" then return end + local ok, err = pcall(function() + target[fnName](target, arg) + end) + table.insert(tried, {target = target, fn = fnName, ok = ok, err = err}) + if ok then success = true end + end + + -- 1) Try common library-level methods (string) + local libFnNames_str = {"SetTheme", "SetThemeName", "ChangeTheme", "ApplyThemeName", "SetThemeByName"} + for _, fn in ipairs(libFnNames_str) do + tryCall(lib, fn, name) + if success then break end + end + + -- 2) Try library-level methods with table + if not success and themeTable then + local libFnNames_tab = {"SetTheme", "ApplyTheme", "UpdateTheme", "SetThemeTable", "ApplyThemeTable"} + for _, fn in ipairs(libFnNames_tab) do + tryCall(lib, fn, themeTable) + if success then break end + end + end + + -- 3) Try window objects inside library (common patterns) + if not success then + local candidates = {} + if lib.Windows and type(lib.Windows) == "table" then + for _, w in pairs(lib.Windows) do table.insert(candidates, w) end + end + if lib.Window and type(lib.Window) == "table" then table.insert(candidates, lib.Window) end + -- also scan top-level fields for Table-like windows + for k, v in pairs(lib) do + if type(v) == "table" and (v.SetTheme or v.ApplyTheme or v.UpdateTheme) then + table.insert(candidates, v) + end + end + + for _, win in ipairs(candidates) do + if success then break end + local tryFns = {"SetTheme", "ApplyTheme", "SetThemeName", "UpdateTheme", "ApplyThemeTable"} + for _, fn in ipairs(tryFns) do + tryCall(win, fn, themeTable or name) + if success then break end + end + end + end + + -- 4) Last-resort generic attempts + if not success then + local fallbacks = {"ApplyTheme", "UpdateTheme", "SetColors", "ReloadTheme", "LoadTheme"} + for _, fn in ipairs(fallbacks) do + tryCall(lib, fn, themeTable or name) + if success then break end + end + end + + -- Save settings and call hooks if success + if success then + self.Settings.Theme = name + pcall(function() self:SaveSettings() end) + if self.OnThemeChanged then + pcall(function() self.OnThemeChanged(name, themeTable) end) + end + else + -- debug output to help user understand what was tried + warn("[InterfaceManager] ApplyTheme('" .. tostring(name) .. "') failed. Methods tried:") + for _, t in ipairs(tried) do + local tname = tostring(t.fn) .. " on " .. (t.target and tostring(t.target) or "nil") + warn(" ", tname, " ok=", tostring(t.ok)) + if not t.ok and t.err then warn(" err:", tostring(t.err)) end + end + end + + return success +end + +-- Convenience: change theme programmatically (uses ApplyTheme) function InterfaceManager:SetTheme(name) if type(name) ~= "string" then return end - self.Settings.Theme = name - self:SaveSettings() - if self.Library and type(self.Library.SetTheme) == "function" then - pcall(function() self.Library:SetTheme(name) end) + local ok = self:ApplyTheme(name) + if not ok then + -- Still save so UI shows the selected value + self.Settings.Theme = name + pcall(function() self:SaveSettings() end) + warn("[InterfaceManager] SetTheme: couldn't apply theme '"..tostring(name).."', saved as default only.") end end +-- ====================================================================== +-- Build interface section for a tab +-- Expects tab:AddSection, section:AddDropdown/AddToggle/AddKeybind etc. +-- ====================================================================== function InterfaceManager:BuildInterfaceSection(tab) assert(tab, "InterfaceManager:BuildInterfaceSection requires a tab object") local Library = self.Library or {} local Settings = self.Settings + -- load saved settings first self:LoadSettings() + -- derive theme list local themeValues = Themes.Names or {} if Library.Themes and type(Library.Themes.Names) == "table" then themeValues = Library.Themes.Names elseif Library.Themes and type(Library.Themes) == "table" then local names = {} for k, v in pairs(Library.Themes) do - if type(k) == "string" then - table.insert(names, k) - end + if type(k) == "string" then table.insert(names, k) end end if #names > 0 then themeValues = names end end + -- create section local section = nil if tab.AddSection then section = tab:AddSection("Interface") @@ -238,6 +396,7 @@ function InterfaceManager:BuildInterfaceSection(tab) error("tab:AddSection missing - cannot build interface section") end + -- Dropdown for theme selection if section.AddDropdown then local InterfaceTheme = section:AddDropdown("InterfaceTheme", { Title = "Theme", @@ -245,9 +404,24 @@ function InterfaceManager:BuildInterfaceSection(tab) Values = themeValues, Default = Settings.Theme, Callback = function(Value) + -- Use InterfaceManager:SetTheme which handles fallbacks & saving + pcall(function() self:SetTheme(Value) end) + + -- Also try calling library directly if it has a SetTheme that expects a table + -- (keeps backward compatibility) if Library and type(Library.SetTheme) == "function" then - pcall(function() Library:SetTheme(Value) end) + -- try both string and table + pcall(function() + local use = Library.Themes and Library.Themes[Value] or Settings.Theme + if use then + -- if library expects table, passing table is safe; if expects string, it might ignore + Library:SetTheme(use) + else + Library:SetTheme(Value) + end + end) end + Settings.Theme = Value self:SaveSettings() end @@ -260,6 +434,7 @@ function InterfaceManager:BuildInterfaceSection(tab) error("section:AddDropdown missing - cannot build theme dropdown") end + -- Acrylic toggle (only add if library indicates support) if Library and Library.UseAcrylic and section.AddToggle then section:AddToggle("AcrylicToggle", { Title = "Acrylic", @@ -275,6 +450,7 @@ function InterfaceManager:BuildInterfaceSection(tab) }) end + -- Transparency toggle if section.AddToggle then section:AddToggle("TransparentToggle", { Title = "Transparency", @@ -290,6 +466,7 @@ function InterfaceManager:BuildInterfaceSection(tab) }) end + -- Keybind for minimize (if supported) if section.AddKeybind then local MenuKeybind = section:AddKeybind("MenuKeybind", { Title = "Minimize Bind", Default = Settings.MenuKeybind }) if MenuKeybind and type(MenuKeybind.OnChanged) == "function" then @@ -306,5 +483,12 @@ function InterfaceManager:BuildInterfaceSection(tab) return true end --- Return InterfaceManager as module (themes accessible at InterfaceManager.Themes) +-- expose Themes for direct access +InterfaceManager.Themes = Themes + +-- allow user code to override hook callbacks: +-- InterfaceManager.OnThemeChanged = function(name, themeTable) end +-- InterfaceManager.OnAcrylicToggled = function(bool) end +-- InterfaceManager.OnTransparencyToggled = function(bool) end + return InterfaceManager From 7fa59c305dda4d23d62b8087addc30e985243e94 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Fri, 31 Oct 2025 16:25:37 +0700 Subject: [PATCH 38/76] Update config.lua --- config.lua | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/config.lua b/config.lua index f4900b0..58e72c6 100644 --- a/config.lua +++ b/config.lua @@ -485,10 +485,15 @@ end -- expose Themes for direct access InterfaceManager.Themes = Themes +print("---- Fluent API ----") +for k,v in pairs(Fluent) do print(k, type(v)) end +if Fluent.Window then + print("Fluent.Window methods:") + for k,v in pairs(Fluent.Window) do print(" ", k, type(v)) end +end +if Fluent.Themes then + print("Fluent.Themes:", table.concat(Fluent.Themes.Names or {}, ", ")) +end --- allow user code to override hook callbacks: --- InterfaceManager.OnThemeChanged = function(name, themeTable) end --- InterfaceManager.OnAcrylicToggled = function(bool) end --- InterfaceManager.OnTransparencyToggled = function(bool) end return InterfaceManager From cbd59b3b6142bbced3a2b49f80d9dcb2f1f4d023 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Fri, 31 Oct 2025 17:21:25 +0700 Subject: [PATCH 39/76] Update ATGComkub.lua --- ATGComkub.lua | 164 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 113 insertions(+), 51 deletions(-) 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)() +)() From eb66c65e041ac7bbf296b8670517422cfd0e0c29 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Sun, 9 Nov 2025 02:04:50 +0700 Subject: [PATCH 40/76] Update autosave.lua --- autosave.lua | 399 ++++++++++++++++++++------------------------------- 1 file changed, 153 insertions(+), 246 deletions(-) diff --git a/autosave.lua b/autosave.lua index b58cc95..c7cac4f 100644 --- a/autosave.lua +++ b/autosave.lua @@ -1,11 +1,14 @@ local httpService = game:GetService("HttpService") -local Workspace = game:GetService("Workspace") +local runService = game:GetService("RunService") local SaveManager = {} do - -- root folder (can be changed via SetFolder) - SaveManager.FolderRoot = "ATGSettings" + SaveManager.Folder = "FluentSettings" SaveManager.Ignore = {} - SaveManager.Options = {} + SaveManager.AutoSaveEnabled = true + SaveManager.AutoLoadEnabled = true + SaveManager.SaveDebounce = false + SaveManager.SaveDelay = 0.5 -- ดีเลย์ในการบันทึกหลังจากมีการเปลี่ยนแปลง (วินาที) + SaveManager.Parser = { Toggle = { Save = function(idx, object) @@ -69,124 +72,68 @@ local SaveManager = {} do }, } - -- 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 getMapName() - local ok, map = pcall(function() return Workspace:FindFirstChild("Map") end) - if ok and map and map:IsA("Instance") then - return sanitizeFilename(map.Name) + function SaveManager:SetIgnoreIndexes(list) + for _, key in next, list do + self.Ignore[key] = true end - local ok2, wname = pcall(function() return Workspace.Name end) - if ok2 and wname then return sanitizeFilename(wname) end - return "UnknownMap" end - local function ensureFolder(path) - if not isfolder(path) then - makefolder(path) - end + function SaveManager:SetFolder(folder) + self.Folder = folder + self:BuildFolderTree() end - -- get configs folder for current place/map - local function getConfigsFolder(self) - local root = self.FolderRoot - local placeId = getPlaceId() - local mapName = getMapName() - -- FluentSettings///settings - return root .. "/" .. placeId .. "/" .. mapName .. "/settings" + function SaveManager:GetConfigPath() + local placeId = game.PlaceId + return self.Folder .. "/MapConfig." .. placeId .. ".json" end - local function getConfigFilePath(self, name) - local folder = getConfigsFolder(self) - return folder .. "/" .. name .. ".json" + function SaveManager:GetSettingsPath() + return self.Folder .. "/SaveSetting.json" end - -- Build folder tree and migrate legacy configs if found (copy only) - function SaveManager:BuildFolderTree() - local root = self.FolderRoot - ensureFolder(root) - - local placeId = getPlaceId() - local placeFolder = root .. "/" .. placeId - ensureFolder(placeFolder) - - local mapName = getMapName() - local mapFolder = placeFolder .. "/" .. mapName - ensureFolder(mapFolder) - - local settingsFolder = mapFolder .. "/settings" - ensureFolder(settingsFolder) - - -- legacy folder: /settings (old layout). If files exist there, copy them into current map settings - 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 = settingsFolder .. "/" .. base .. ".json" - -- copy only if destination does not exist yet - if not isfile(dest) then - local ok, data = pcall(readfile, f) - if ok and data then - local success, err = pcall(writefile, dest, data) - -- ignore write errors but do not fail - end - end - end - end - end - - -- also migrate autoload.txt if present (copy only) - local autopath = legacySettingsFolder .. "/autoload.txt" - if isfile(autopath) then - local autodata = readfile(autopath) - local destAuto = settingsFolder .. "/autoload.txt" - if not isfile(destAuto) then - pcall(writefile, destAuto, autodata) - end + function SaveManager:LoadSettings() + local settingsPath = self:GetSettingsPath() + if isfile(settingsPath) then + local success, decoded = pcall(function() + return httpService:JSONDecode(readfile(settingsPath)) + end) + + if success and decoded then + self.AutoSaveEnabled = decoded.AutoSave ~= false + self.AutoLoadEnabled = decoded.AutoLoad ~= false end end end - function SaveManager:SetIgnoreIndexes(list) - for _, key in next, list do - self.Ignore[key] = true + function SaveManager:SaveSettings() + local settingsPath = self:GetSettingsPath() + local data = { + AutoSave = self.AutoSaveEnabled, + AutoLoad = self.AutoLoadEnabled + } + + local success, encoded = pcall(httpService.JSONEncode, httpService, data) + if success then + writefile(settingsPath, encoded) end end - function SaveManager:SetFolder(folder) - self.FolderRoot = tostring(folder or "FluentSettings") - self:BuildFolderTree() - end - - function SaveManager:SetLibrary(library) - self.Library = library - self.Options = library.Options + function SaveManager:AutoSave() + if not self.AutoSaveEnabled then return end + if self.SaveDebounce then return end + + self.SaveDebounce = true + task.delay(self.SaveDelay, function() + self:Save() + self.SaveDebounce = false + end) end - function SaveManager:Save(name) - if (not name) then - return false, "no config file is selected" - end - - local fullPath = getConfigFilePath(self, name) + function SaveManager:Save() + if not self.AutoSaveEnabled then return false, "auto save is disabled" end + local fullPath = self:GetConfigPath() local data = { objects = {} } for idx, option in next, SaveManager.Options do @@ -194,35 +141,31 @@ local SaveManager = {} do if self.Ignore[idx] then continue end table.insert(data.objects, self.Parser[option.Type].Save(idx, option)) - end + end local success, encoded = pcall(httpService.JSONEncode, httpService, data) if not success then return false, "failed to encode data" end - -- ensure folder exists - 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 + function SaveManager:Load() + if not self.AutoLoadEnabled then return false, "auto load is disabled" end + + local file = self:GetConfigPath() + if not isfile(file) then return false, "config file not found" 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) + task.spawn(function() + self.Parser[option.type].Load(option.idx, option) + end) end end @@ -230,169 +173,133 @@ local SaveManager = {} do end function SaveManager:IgnoreThemeSettings() - self:SetIgnoreIndexes({ + self:SetIgnoreIndexes({ "InterfaceTheme", "AcrylicToggle", "TransparentToggle", "MenuKeybind" }) end - function SaveManager:RefreshConfigList() - local folder = getConfigsFolder(self) - if not isfolder(folder) then - return {} + function SaveManager:BuildFolderTree() + if not isfolder(self.Folder) then + makefolder(self.Folder) 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" then - table.insert(out, name) - end + end + + function SaveManager:SetLibrary(library) + self.Library = library + self.Options = library.Options + end + + function SaveManager:SetupAutoSave() + -- เชื่อมต่อกับทุก Options ให้บันทึกอัตโนมัติเมื่อมีการเปลี่ยนแปลง + for idx, option in pairs(self.Options) do + if self.Ignore[idx] then continue end + + if option.Changed then + option:Changed(function() + self:AutoSave() + end) + elseif option.OnChanged then + option:OnChanged(function() + self:AutoSave() + end) end end - return out end - function SaveManager:LoadAutoloadConfig() - local autopath = getConfigsFolder(self) .. "/autoload.txt" - if isfile(autopath) then - local name = readfile(autopath) - local success, err = self:Load(name) - if not success then - return self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "Failed to load autoload config: " .. err, - Duration = 7 - }) - end + function SaveManager:LoadAutoConfig() + -- โหลด Settings ก่อน + self:LoadSettings() + + if not self.AutoLoadEnabled then + return self.Library:Notify({ + Title = "Interface", + Content = "Config Loader", + SubContent = "Auto load is disabled", + Duration = 5 + }) + end - self.Library:Notify({ + local success, err = self:Load() + if not success then + return self.Library:Notify({ Title = "Interface", - Content = "Config loader", - SubContent = string.format("Auto loaded config %q", name), - Duration = 7 + Content = "Config Loader", + SubContent = "Failed to auto load: " .. err, + Duration = 5 }) end + + self.Library:Notify({ + Title = "Interface", + Content = "Config Loader", + SubContent = "Auto loaded config for Place ID: " .. game.PlaceId, + Duration = 5 + }) end function SaveManager:BuildConfigSection(tab) assert(self.Library, "Must set SaveManager.Library") - local section = tab:AddSection("Configuration") - - section:AddInput("SaveManager_ConfigName", { Title = "Config name" }) - section:AddDropdown("SaveManager_ConfigList", { Title = "Config list", Values = self:RefreshConfigList(), AllowNull = true }) - - section:AddButton({ - Title = "Create config", - Callback = function() - local name = SaveManager.Options.SaveManager_ConfigName.Value - - if name:gsub(" ", "") == "" then - return self.Library:Notify({ + local section = tab:AddSection("Auto Save Configuration") + + -- Toggle Auto Save + local autoSaveToggle = section:AddToggle("AutoSaveToggle", { + Title = "Auto Save", + Description = "Automatically save settings in real-time", + Default = self.AutoSaveEnabled, + Callback = function(value) + self.AutoSaveEnabled = value + self:SaveSettings() + + if value then + self:Save() + self.Library:Notify({ Title = "Interface", - Content = "Config loader", - SubContent = "Invalid config name (empty)", - Duration = 7 + Content = "Config Loader", + SubContent = "Auto save enabled", + Duration = 3 }) - end - - local success, err = self:Save(name) - if not success then - return self.Library:Notify({ + else + self.Library:Notify({ Title = "Interface", - Content = "Config loader", - SubContent = "Failed to save config: " .. err, - Duration = 7 + Content = "Config Loader", + SubContent = "Auto save disabled", + Duration = 3 }) end - - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Created config %q", name), - Duration = 7 - }) - - SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) - SaveManager.Options.SaveManager_ConfigList:SetValue(nil) end }) - section:AddButton({Title = "Load config", Callback = function() - local name = SaveManager.Options.SaveManager_ConfigList.Value - - local success, err = self:Load(name) - if not success then - return self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "Failed to load config: " .. err, - Duration = 7 - }) - end - - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Loaded config %q", name), - Duration = 7 - }) - end}) - - section:AddButton({Title = "Overwrite config", Callback = function() - local name = SaveManager.Options.SaveManager_ConfigList.Value - - local success, err = self:Save(name) - if not success then - return self.Library:Notify({ + -- Toggle Auto Load + local autoLoadToggle = section:AddToggle("AutoLoadToggle", { + Title = "Auto Load", + Description = "Automatically load settings on startup", + Default = self.AutoLoadEnabled, + Callback = function(value) + self.AutoLoadEnabled = value + self:SaveSettings() + + self.Library:Notify({ Title = "Interface", - Content = "Config loader", - SubContent = "Failed to overwrite config: " .. err, - Duration = 7 + Content = "Config Loader", + SubContent = "Auto load " .. (value and "enabled" or "disabled"), + Duration = 3 }) end + }) - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Overwrote config %q", name), - Duration = 7 - }) - end}) - - section:AddButton({Title = "Refresh list", Callback = function() - SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) - SaveManager.Options.SaveManager_ConfigList:SetValue(nil) - end}) - - local AutoloadButton - AutoloadButton = section:AddButton({Title = "Set as autoload", Description = "Current autoload config: none", Callback = function() - local name = SaveManager.Options.SaveManager_ConfigList.Value - local autopath = getConfigsFolder(self) .. "/autoload.txt" - writefile(autopath, name) - AutoloadButton:SetDesc("Current autoload config: " .. name) - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Set %q to auto load", name), - Duration = 7 - }) - end}) - - -- populate current autoload desc if exists - local autop = getConfigsFolder(self) .. "/autoload.txt" - if isfile(autop) then - local name = readfile(autop) - AutoloadButton:SetDesc("Current autoload config: " .. name) - end + -- ข้อมูลเพิ่มเติม + section:AddParagraph({ + Title = "Info", + Content = "Config file: MapConfig." .. game.PlaceId .. ".json\n" .. + "Settings are saved automatically when changed.\n" .. + "Config is specific to this Place ID." + }) - SaveManager:SetIgnoreIndexes({ "SaveManager_ConfigList", "SaveManager_ConfigName" }) + -- เพิ่ม Toggle เข้า Ignore list + self:SetIgnoreIndexes({ "AutoSaveToggle", "AutoLoadToggle" }) end - -- initial build SaveManager:BuildFolderTree() end From 96cf108c229792096712c4cc4250bb9c7b282b76 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Sun, 9 Nov 2025 02:18:10 +0700 Subject: [PATCH 41/76] Update autosave.lua --- autosave.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autosave.lua b/autosave.lua index c7cac4f..fd50987 100644 --- a/autosave.lua +++ b/autosave.lua @@ -2,7 +2,7 @@ local httpService = game:GetService("HttpService") local runService = game:GetService("RunService") local SaveManager = {} do - SaveManager.Folder = "FluentSettings" + SaveManager.Folder = "ATGSaveSetting" SaveManager.Ignore = {} SaveManager.AutoSaveEnabled = true SaveManager.AutoLoadEnabled = true From 5bbe7dd8abdb5b848f3b092a1d379abec64eb9d6 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Sun, 9 Nov 2025 02:22:27 +0700 Subject: [PATCH 42/76] Update autosave.lua --- autosave.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autosave.lua b/autosave.lua index fd50987..c7cac4f 100644 --- a/autosave.lua +++ b/autosave.lua @@ -2,7 +2,7 @@ local httpService = game:GetService("HttpService") local runService = game:GetService("RunService") local SaveManager = {} do - SaveManager.Folder = "ATGSaveSetting" + SaveManager.Folder = "FluentSettings" SaveManager.Ignore = {} SaveManager.AutoSaveEnabled = true SaveManager.AutoLoadEnabled = true From d0781c1f39eebe6001eeaa2511f7fd4ef6d22ccd Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Sun, 9 Nov 2025 02:27:23 +0700 Subject: [PATCH 43/76] Update autosave.lua --- autosave.lua | 399 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 246 insertions(+), 153 deletions(-) diff --git a/autosave.lua b/autosave.lua index c7cac4f..b58cc95 100644 --- a/autosave.lua +++ b/autosave.lua @@ -1,14 +1,11 @@ local httpService = game:GetService("HttpService") -local runService = game:GetService("RunService") +local Workspace = game:GetService("Workspace") local SaveManager = {} do - SaveManager.Folder = "FluentSettings" + -- root folder (can be changed via SetFolder) + SaveManager.FolderRoot = "ATGSettings" SaveManager.Ignore = {} - SaveManager.AutoSaveEnabled = true - SaveManager.AutoLoadEnabled = true - SaveManager.SaveDebounce = false - SaveManager.SaveDelay = 0.5 -- ดีเลย์ในการบันทึกหลังจากมีการเปลี่ยนแปลง (วินาที) - + SaveManager.Options = {} SaveManager.Parser = { Toggle = { Save = function(idx, object) @@ -72,68 +69,124 @@ local SaveManager = {} do }, } - function SaveManager:SetIgnoreIndexes(list) - for _, key in next, list do - self.Ignore[key] = true + -- 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 getMapName() + local ok, map = pcall(function() return Workspace:FindFirstChild("Map") end) + if ok and map and map:IsA("Instance") then + return sanitizeFilename(map.Name) end + local ok2, wname = pcall(function() return Workspace.Name end) + if ok2 and wname then return sanitizeFilename(wname) end + return "UnknownMap" end - function SaveManager:SetFolder(folder) - self.Folder = folder - self:BuildFolderTree() + local function ensureFolder(path) + if not isfolder(path) then + makefolder(path) + end end - function SaveManager:GetConfigPath() - local placeId = game.PlaceId - return self.Folder .. "/MapConfig." .. placeId .. ".json" + -- get configs folder for current place/map + local function getConfigsFolder(self) + local root = self.FolderRoot + local placeId = getPlaceId() + local mapName = getMapName() + -- FluentSettings///settings + return root .. "/" .. placeId .. "/" .. mapName .. "/settings" end - function SaveManager:GetSettingsPath() - return self.Folder .. "/SaveSetting.json" + local function getConfigFilePath(self, name) + local folder = getConfigsFolder(self) + return folder .. "/" .. name .. ".json" end - function SaveManager:LoadSettings() - local settingsPath = self:GetSettingsPath() - if isfile(settingsPath) then - local success, decoded = pcall(function() - return httpService:JSONDecode(readfile(settingsPath)) - end) - - if success and decoded then - self.AutoSaveEnabled = decoded.AutoSave ~= false - self.AutoLoadEnabled = decoded.AutoLoad ~= false + -- Build folder tree and migrate legacy configs if found (copy only) + function SaveManager:BuildFolderTree() + local root = self.FolderRoot + ensureFolder(root) + + local placeId = getPlaceId() + local placeFolder = root .. "/" .. placeId + ensureFolder(placeFolder) + + local mapName = getMapName() + local mapFolder = placeFolder .. "/" .. mapName + ensureFolder(mapFolder) + + local settingsFolder = mapFolder .. "/settings" + ensureFolder(settingsFolder) + + -- legacy folder: /settings (old layout). If files exist there, copy them into current map settings + 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 = settingsFolder .. "/" .. base .. ".json" + -- copy only if destination does not exist yet + if not isfile(dest) then + local ok, data = pcall(readfile, f) + if ok and data then + local success, err = pcall(writefile, dest, data) + -- ignore write errors but do not fail + end + end + end + end + end + + -- also migrate autoload.txt if present (copy only) + local autopath = legacySettingsFolder .. "/autoload.txt" + if isfile(autopath) then + local autodata = readfile(autopath) + local destAuto = settingsFolder .. "/autoload.txt" + if not isfile(destAuto) then + pcall(writefile, destAuto, autodata) + end end end end - function SaveManager:SaveSettings() - local settingsPath = self:GetSettingsPath() - local data = { - AutoSave = self.AutoSaveEnabled, - AutoLoad = self.AutoLoadEnabled - } - - local success, encoded = pcall(httpService.JSONEncode, httpService, data) - if success then - writefile(settingsPath, encoded) + function SaveManager:SetIgnoreIndexes(list) + for _, key in next, list do + self.Ignore[key] = true end end - function SaveManager:AutoSave() - if not self.AutoSaveEnabled then return end - if self.SaveDebounce then return end - - self.SaveDebounce = true - task.delay(self.SaveDelay, function() - self:Save() - self.SaveDebounce = false - end) + function SaveManager:SetFolder(folder) + self.FolderRoot = tostring(folder or "FluentSettings") + self:BuildFolderTree() + end + + function SaveManager:SetLibrary(library) + self.Library = library + self.Options = library.Options end - function SaveManager:Save() - if not self.AutoSaveEnabled then return false, "auto save is disabled" end + function SaveManager:Save(name) + if (not name) then + return false, "no config file is selected" + end + + local fullPath = getConfigFilePath(self, name) - local fullPath = self:GetConfigPath() local data = { objects = {} } for idx, option in next, SaveManager.Options do @@ -141,31 +194,35 @@ local SaveManager = {} do if self.Ignore[idx] then continue end table.insert(data.objects, self.Parser[option.Type].Save(idx, option)) - end + end local success, encoded = pcall(httpService.JSONEncode, httpService, data) if not success then return false, "failed to encode data" end + -- ensure folder exists + local folder = fullPath:match("^(.*)/[^/]+$") + if folder then ensureFolder(folder) end + writefile(fullPath, encoded) return true end - function SaveManager:Load() - if not self.AutoLoadEnabled then return false, "auto load is disabled" end - - local file = self:GetConfigPath() - if not isfile(file) then return false, "config file not found" 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) + task.spawn(function() self.Parser[option.type].Load(option.idx, option) end) end end @@ -173,133 +230,169 @@ local SaveManager = {} do end function SaveManager:IgnoreThemeSettings() - self:SetIgnoreIndexes({ + self:SetIgnoreIndexes({ "InterfaceTheme", "AcrylicToggle", "TransparentToggle", "MenuKeybind" }) end - function SaveManager:BuildFolderTree() - if not isfolder(self.Folder) then - makefolder(self.Folder) + function SaveManager:RefreshConfigList() + local folder = getConfigsFolder(self) + if not isfolder(folder) then + return {} end - end - - function SaveManager:SetLibrary(library) - self.Library = library - self.Options = library.Options - end - - function SaveManager:SetupAutoSave() - -- เชื่อมต่อกับทุก Options ให้บันทึกอัตโนมัติเมื่อมีการเปลี่ยนแปลง - for idx, option in pairs(self.Options) do - if self.Ignore[idx] then continue end - - if option.Changed then - option:Changed(function() - self:AutoSave() - end) - elseif option.OnChanged then - option:OnChanged(function() - self:AutoSave() - 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" then + table.insert(out, name) + end end end + return out end - function SaveManager:LoadAutoConfig() - -- โหลด Settings ก่อน - self:LoadSettings() - - if not self.AutoLoadEnabled then - return self.Library:Notify({ - Title = "Interface", - Content = "Config Loader", - SubContent = "Auto load is disabled", - Duration = 5 - }) - end + function SaveManager:LoadAutoloadConfig() + local autopath = getConfigsFolder(self) .. "/autoload.txt" + if isfile(autopath) then + local name = readfile(autopath) + local success, err = self:Load(name) + if not success then + return self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "Failed to load autoload config: " .. err, + Duration = 7 + }) + end - local success, err = self:Load() - if not success then - return self.Library:Notify({ + self.Library:Notify({ Title = "Interface", - Content = "Config Loader", - SubContent = "Failed to auto load: " .. err, - Duration = 5 + Content = "Config loader", + SubContent = string.format("Auto loaded config %q", name), + Duration = 7 }) end - - self.Library:Notify({ - Title = "Interface", - Content = "Config Loader", - SubContent = "Auto loaded config for Place ID: " .. game.PlaceId, - Duration = 5 - }) end function SaveManager:BuildConfigSection(tab) assert(self.Library, "Must set SaveManager.Library") - local section = tab:AddSection("Auto Save Configuration") - - -- Toggle Auto Save - local autoSaveToggle = section:AddToggle("AutoSaveToggle", { - Title = "Auto Save", - Description = "Automatically save settings in real-time", - Default = self.AutoSaveEnabled, - Callback = function(value) - self.AutoSaveEnabled = value - self:SaveSettings() - - if value then - self:Save() - self.Library:Notify({ + local section = tab:AddSection("Configuration") + + section:AddInput("SaveManager_ConfigName", { Title = "Config name" }) + section:AddDropdown("SaveManager_ConfigList", { Title = "Config list", Values = self:RefreshConfigList(), AllowNull = true }) + + section:AddButton({ + Title = "Create config", + Callback = function() + local name = SaveManager.Options.SaveManager_ConfigName.Value + + if name:gsub(" ", "") == "" then + return self.Library:Notify({ Title = "Interface", - Content = "Config Loader", - SubContent = "Auto save enabled", - Duration = 3 + Content = "Config loader", + SubContent = "Invalid config name (empty)", + Duration = 7 }) - else - self.Library:Notify({ + end + + local success, err = self:Save(name) + if not success then + return self.Library:Notify({ Title = "Interface", - Content = "Config Loader", - SubContent = "Auto save disabled", - Duration = 3 + Content = "Config loader", + SubContent = "Failed to save config: " .. err, + Duration = 7 }) end - end - }) - -- Toggle Auto Load - local autoLoadToggle = section:AddToggle("AutoLoadToggle", { - Title = "Auto Load", - Description = "Automatically load settings on startup", - Default = self.AutoLoadEnabled, - Callback = function(value) - self.AutoLoadEnabled = value - self:SaveSettings() - self.Library:Notify({ Title = "Interface", - Content = "Config Loader", - SubContent = "Auto load " .. (value and "enabled" or "disabled"), - Duration = 3 + Content = "Config loader", + SubContent = string.format("Created config %q", name), + Duration = 7 }) + + SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) + SaveManager.Options.SaveManager_ConfigList:SetValue(nil) end }) - -- ข้อมูลเพิ่มเติม - section:AddParagraph({ - Title = "Info", - Content = "Config file: MapConfig." .. game.PlaceId .. ".json\n" .. - "Settings are saved automatically when changed.\n" .. - "Config is specific to this Place ID." - }) + section:AddButton({Title = "Load config", Callback = function() + local name = SaveManager.Options.SaveManager_ConfigList.Value + + local success, err = self:Load(name) + if not success then + return self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "Failed to load config: " .. err, + Duration = 7 + }) + end + + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Loaded config %q", name), + Duration = 7 + }) + end}) + + section:AddButton({Title = "Overwrite config", Callback = function() + local name = SaveManager.Options.SaveManager_ConfigList.Value + + local success, err = self:Save(name) + if not success then + return self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = "Failed to overwrite config: " .. err, + Duration = 7 + }) + end + + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Overwrote config %q", name), + Duration = 7 + }) + end}) + + section:AddButton({Title = "Refresh list", Callback = function() + SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) + SaveManager.Options.SaveManager_ConfigList:SetValue(nil) + end}) + + local AutoloadButton + AutoloadButton = section:AddButton({Title = "Set as autoload", Description = "Current autoload config: none", Callback = function() + local name = SaveManager.Options.SaveManager_ConfigList.Value + local autopath = getConfigsFolder(self) .. "/autoload.txt" + writefile(autopath, name) + AutoloadButton:SetDesc("Current autoload config: " .. name) + self.Library:Notify({ + Title = "Interface", + Content = "Config loader", + SubContent = string.format("Set %q to auto load", name), + Duration = 7 + }) + end}) + + -- populate current autoload desc if exists + local autop = getConfigsFolder(self) .. "/autoload.txt" + if isfile(autop) then + local name = readfile(autop) + AutoloadButton:SetDesc("Current autoload config: " .. name) + end - -- เพิ่ม Toggle เข้า Ignore list - self:SetIgnoreIndexes({ "AutoSaveToggle", "AutoLoadToggle" }) + SaveManager:SetIgnoreIndexes({ "SaveManager_ConfigList", "SaveManager_ConfigName" }) end + -- initial build SaveManager:BuildFolderTree() end From d0480c456071df37da6d8f39cffdd59978b1398d Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Sun, 9 Nov 2025 02:37:48 +0700 Subject: [PATCH 44/76] Update config.lua --- config.lua | 841 ++++++++++++++++++++++++++--------------------------- 1 file changed, 415 insertions(+), 426 deletions(-) diff --git a/config.lua b/config.lua index 58e72c6..aa7eadc 100644 --- a/config.lua +++ b/config.lua @@ -1,499 +1,488 @@ --- InterfaceManager (improved) --- - embeds built-in themes --- - auto-loads theme modules from script:GetChildren() --- - ApplyTheme tries multiple library/window APIs as fallback --- - Save/Load settings to file if file API available --- - BuildInterfaceSection uses SetTheme which calls ApplyTheme --- Usage: --- local IM = require(path.to.InterfaceManager) --- IM:SetLibrary(Fluent) -- optional (will default to internal DefaultLibrary) --- IM:BuildInterfaceSection(tab) - -local HttpService = game:GetService("HttpService") - -local InterfaceManager = {} -InterfaceManager.Folder = "FluentSettings" -InterfaceManager.Settings = { - Theme = "Dark", - Acrylic = true, - Transparency = true, - MenuKeybind = "LeftControl" -} - --- File API detection (exploit environments vary) -local has_isfile = type(isfile) == "function" -local has_readfile = type(readfile) == "function" -local has_writefile = type(writefile) == "function" -local has_makefolder = type(makefolder) == "function" -local has_isfolder = type(isfolder) == "function" - --- Internal helpers --------------------------------------------------------- -local function safeEncode(tab) - local ok, res = pcall(function() return HttpService:JSONEncode(tab) end) - if ok then return res end - return nil -end - -local function safeDecode(str) - local ok, res = pcall(function() return HttpService:JSONDecode(str) end) - if ok then return res end - return nil -end - --- ====================================================================== --- Themes table (embedded) --- ====================================================================== -local Themes = {} -Themes.Names = { - "Dark","Darker","Light","Aqua","Amethyst","Rose","Emerald","Crimson", - "Ocean","Sunset","Lavender","Mint","Coral","Gold","Midnight","Forest", -} - --- (Only a subset expanded here for brevity — include any themes you want) -Themes["Emerald"] = { - Name = "Emerald", - Accent = Color3.fromRGB(16, 185, 129), - AcrylicMain = Color3.fromRGB(18, 18, 18), - AcrylicBorder = Color3.fromRGB(52, 211, 153), - AcrylicGradient = ColorSequence.new(Color3.fromRGB(16, 185, 129), Color3.fromRGB(5, 150, 105)), - AcrylicNoise = 0.92, - TitleBarLine = Color3.fromRGB(16, 185, 129), - Tab = Color3.fromRGB(110, 231, 183), - Element = Color3.fromRGB(52, 211, 153), - ElementBorder = Color3.fromRGB(6, 95, 70), - InElementBorder = Color3.fromRGB(16, 185, 129), - ElementTransparency = 0.87, - ToggleSlider = Color3.fromRGB(52, 211, 153), - ToggleToggled = Color3.fromRGB(0, 0, 0), - SliderRail = Color3.fromRGB(52, 211, 153), - DropdownFrame = Color3.fromRGB(110, 231, 183), - DropdownHolder = Color3.fromRGB(6, 78, 59), - DropdownBorder = Color3.fromRGB(4, 120, 87), - DropdownOption = Color3.fromRGB(52, 211, 153), - Keybind = Color3.fromRGB(52, 211, 153), - Input = Color3.fromRGB(52, 211, 153), - InputFocused = Color3.fromRGB(6, 78, 59), - InputIndicator = Color3.fromRGB(110, 231, 183), - Dialog = Color3.fromRGB(6, 78, 59), - DialogHolder = Color3.fromRGB(4, 120, 87), - DialogHolderLine = Color3.fromRGB(6, 95, 70), - DialogButton = Color3.fromRGB(16, 185, 129), - DialogButtonBorder = Color3.fromRGB(52, 211, 153), - DialogBorder = Color3.fromRGB(16, 185, 129), - DialogInput = Color3.fromRGB(6, 95, 70), - DialogInputLine = Color3.fromRGB(110, 231, 183), - Text = Color3.fromRGB(240, 253, 244), - SubText = Color3.fromRGB(167, 243, 208), - Hover = Color3.fromRGB(52, 211, 153), - HoverChange = 0.04, -} - -Themes["Crimson"] = { - Name = "Crimson", - Accent = Color3.fromRGB(220, 38, 38), - AcrylicMain = Color3.fromRGB(20, 20, 20), - AcrylicBorder = Color3.fromRGB(239, 68, 68), - AcrylicGradient = ColorSequence.new(Color3.fromRGB(220, 38, 38), Color3.fromRGB(153, 27, 27)), - AcrylicNoise = 0.92, - TitleBarLine = Color3.fromRGB(220, 38, 38), - Tab = Color3.fromRGB(252, 165, 165), - Element = Color3.fromRGB(239, 68, 68), - ElementBorder = Color3.fromRGB(127, 29, 29), - InElementBorder = Color3.fromRGB(185, 28, 28), - ElementTransparency = 0.87, - ToggleSlider = Color3.fromRGB(239, 68, 68), - ToggleToggled = Color3.fromRGB(0, 0, 0), - SliderRail = Color3.fromRGB(239, 68, 68), - DropdownFrame = Color3.fromRGB(252, 165, 165), - DropdownHolder = Color3.fromRGB(127, 29, 29), - DropdownBorder = Color3.fromRGB(153, 27, 27), - DropdownOption = Color3.fromRGB(239, 68, 68), - Keybind = Color3.fromRGB(239, 68, 68), - Input = Color3.fromRGB(239, 68, 68), - InputFocused = Color3.fromRGB(127, 29, 29), - InputIndicator = Color3.fromRGB(252, 165, 165), - Dialog = Color3.fromRGB(127, 29, 29), - DialogHolder = Color3.fromRGB(153, 27, 27), - DialogHolderLine = Color3.fromRGB(185, 28, 28), - DialogButton = Color3.fromRGB(220, 38, 38), - DialogButtonBorder = Color3.fromRGB(239, 68, 68), - DialogBorder = Color3.fromRGB(220, 38, 38), - DialogInput = Color3.fromRGB(153, 27, 27), - DialogInputLine = Color3.fromRGB(252, 165, 165), - Text = Color3.fromRGB(254, 242, 242), - SubText = Color3.fromRGB(254, 202, 202), - Hover = Color3.fromRGB(239, 68, 68), - HoverChange = 0.04, -} - --- Add more embedded themes here as needed... --- ====================================================================== - --- Auto-load child modules as themes (safe require) -if type(script) == "table" and type(script.GetChildren) == "function" then - for _, child in next, script:GetChildren() do - local ok, required = pcall(function() return require(child) end) - if ok and type(required) == "table" and type(required.Name) == "string" then - Themes[required.Name] = required - -- add name to Names if not present - local exists = false - for _, n in ipairs(Themes.Names) do - if n == required.Name then exists = true break end +local httpService = game:GetService("HttpService") + +--[[ + SaveManager - ระบบ Auto Save/Load อัตโนมัติ + รองรับ: Toggle, Slider, Dropdown, Input, Keybind, Colorpicker +]] + +local SaveManager = {} do + SaveManager.Folder = "FluentSettings" + SaveManager.Ignore = {} -- Elements ที่ไม่ต้องการ save + SaveManager.Parser = { + Toggle = { + Save = function(idx, object) + return { type = "Toggle", idx = idx, value = object.Value } + end, + Load = function(idx, data) + if SaveManager.Library.Options[idx] then + SaveManager.Library.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.Library.Options[idx] then + SaveManager.Library.Options[idx]:SetValue(tonumber(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.Library.Options[idx] then + SaveManager.Library.Options[idx]:SetValue(data.value) + end end - if not exists then - table.insert(Themes.Names, required.Name) + }, + Colorpicker = { + Save = function(idx, object) + return { + type = "Colorpicker", + idx = idx, + value = object.Value:ToHex(), + transparency = object.Transparency + } + end, + Load = function(idx, data) + if SaveManager.Library.Options[idx] then + SaveManager.Library.Options[idx]:SetValueRGB(Color3.fromHex(data.value), data.transparency) + end end - else - if not ok then - -- debug: child failed to require; don't error out - warn("[InterfaceManager] failed to require child theme:", tostring(child.Name), tostring(required)) + }, + Keybind = { + Save = function(idx, object) + return { type = "Keybind", idx = idx, value = object.Value, mode = object.Mode } + end, + Load = function(idx, data) + if SaveManager.Library.Options[idx] then + SaveManager.Library.Options[idx]:SetValue(data.value, data.mode) + end end + }, + Input = { + Save = function(idx, object) + return { type = "Input", idx = idx, value = object.Value } + end, + Load = function(idx, data) + if SaveManager.Library.Options[idx] then + SaveManager.Library.Options[idx]:SetValue(data.value) + end + end + } + } + + function SaveManager:SetIgnoreIndexes(list) + for _, v in next, list do + self.Ignore[v] = true end end -end --- ====================================================================== --- DefaultLibrary wrapper — so IM:SetLibrary() can accept other libs --- ====================================================================== -local DefaultLibrary = {} -DefaultLibrary.Themes = Themes -DefaultLibrary.CurrentTheme = Themes[InterfaceManager.Settings.Theme] or nil -DefaultLibrary.MinimizeKeybind = nil -DefaultLibrary.UseAcrylic = true -- flag for BuildInterfaceSection to show Acrylic toggle - -function DefaultLibrary:SetTheme(nameOrTable) - if type(nameOrTable) == "string" then - DefaultLibrary.CurrentTheme = Themes[nameOrTable] or DefaultLibrary.CurrentTheme - elseif type(nameOrTable) == "table" and type(nameOrTable.Name) == "string" then - DefaultLibrary.CurrentTheme = nameOrTable - end - -- If user provided a hook, call it - if InterfaceManager.OnThemeChanged then - pcall(function() InterfaceManager.OnThemeChanged(DefaultLibrary.CurrentTheme and DefaultLibrary.CurrentTheme.Name or nil, DefaultLibrary.CurrentTheme) end) + function SaveManager:SetFolder(folder) + self.Folder = folder + self:BuildFolderTree() end -end -function DefaultLibrary:ToggleAcrylic(enabled) - InterfaceManager.Settings.Acrylic = enabled - if InterfaceManager.OnAcrylicToggled then - pcall(function() InterfaceManager.OnAcrylicToggled(enabled) end) + function SaveManager:SetLibrary(library) + self.Library = library end -end -function DefaultLibrary:ToggleTransparency(enabled) - InterfaceManager.Settings.Transparency = enabled - if InterfaceManager.OnTransparencyToggled then - pcall(function() InterfaceManager.OnTransparencyToggled(enabled) end) - end -end + function SaveManager:BuildFolderTree() + local paths = {} + local parts = self.Folder:split("/") + + for idx = 1, #parts do + paths[#paths + 1] = table.concat(parts, "/", 1, idx) + end --- Attach by default -InterfaceManager.Library = DefaultLibrary -InterfaceManager.Themes = Themes - --- ====================================================================== --- Public API: File/Folder management --- ====================================================================== -function InterfaceManager:SetFolder(folder) - if type(folder) ~= "string" then return end - self.Folder = folder - if has_makefolder and has_isfolder then - self:BuildFolderTree() - end -end + table.insert(paths, self.Folder .. "/configs") -function InterfaceManager:SetLibrary(library) - if type(library) ~= "table" then return end - self.Library = library - if library.Themes then - self.Themes = library.Themes + for _, str in next, paths do + if not isfolder(str) then + makefolder(str) + end + end end -end -function InterfaceManager:BuildFolderTree() - if not has_makefolder or not has_isfolder then - return - end + function SaveManager:SaveConfig(name) + if not self.Library then + return warn("[SaveManager] Library not set") + end - local paths = {} - local parts = string.split(self.Folder, "/") - for idx = 1, #parts do - paths[#paths + 1] = table.concat(parts, "/", 1, idx) - end - table.insert(paths, self.Folder .. "/settings") + local fullPath = self.Folder .. "/configs/" .. name .. ".json" + local data = { + objects = {} + } - for i = 1, #paths do - local p = paths[i] - if not isfolder(p) then - makefolder(p) + for idx, option in next, self.Library.Options do + if not self.Ignore[idx] then + local optionType = option.Type + if self.Parser[optionType] then + data.objects[idx] = self.Parser[optionType].Save(idx, option) + end + end end - end -end -function InterfaceManager:SaveSettings() - if not has_writefile then return end - local encoded = safeEncode(self.Settings) - if not encoded then return end - local ok = pcall(function() - writefile(self.Folder .. "/options.json", encoded) - end) - return ok -end + local success, encoded = pcall(httpService.JSONEncode, httpService, data) + if not success then + return warn("[SaveManager] Failed to encode config: " .. name) + end -function InterfaceManager:LoadSettings() - if not has_isfile or not has_readfile then return end - local path = self.Folder .. "/options.json" - if not isfile(path) then return end - local raw = readfile(path) - local decoded = safeDecode(raw) - if type(decoded) == "table" then - for k, v in pairs(decoded) do - self.Settings[k] = v + writefile(fullPath, encoded) + + -- แจ้งเตือน + if self.Library then + self.Library:Notify({ + Title = "Config Saved", + Content = "Configuration '" .. name .. "' has been saved successfully!", + Duration = 3 + }) end end -end --- ====================================================================== --- Theme applying logic (tries many fallbacks) --- ====================================================================== -function InterfaceManager:ApplyTheme(name) - if type(name) ~= "string" then return false end + function SaveManager:LoadConfig(name) + if not self.Library then + return warn("[SaveManager] Library not set") + end - local lib = self.Library or {} - local themeTable = nil + local fullPath = self.Folder .. "/configs/" .. name .. ".json" + + if not isfile(fullPath) then + return warn("[SaveManager] Config file not found: " .. name) + end - -- Find theme table from library or internal Themes - if lib.Themes and type(lib.Themes[name]) == "table" then - themeTable = lib.Themes[name] - elseif self.Themes and type(self.Themes[name]) == "table" then - themeTable = self.Themes[name] - end + local success, decoded = pcall(function() + return httpService:JSONDecode(readfile(fullPath)) + end) - local tried = {} - local success = false + if not success then + return warn("[SaveManager] Failed to decode config: " .. name) + end - local function tryCall(target, fnName, arg) - if success then return end - if not target or type(target[fnName]) ~= "function" then return end - local ok, err = pcall(function() - target[fnName](target, arg) - end) - table.insert(tried, {target = target, fn = fnName, ok = ok, err = err}) - if ok then success = true end - end + -- โหลดค่าทั้งหมด + for idx, data in next, decoded.objects do + local optionType = data.type + if self.Parser[optionType] then + task.spawn(function() + self.Parser[optionType].Load(idx, data) + end) + end + end - -- 1) Try common library-level methods (string) - local libFnNames_str = {"SetTheme", "SetThemeName", "ChangeTheme", "ApplyThemeName", "SetThemeByName"} - for _, fn in ipairs(libFnNames_str) do - tryCall(lib, fn, name) - if success then break end + -- แจ้งเตือน + if self.Library then + self.Library:Notify({ + Title = "Config Loaded", + Content = "Configuration '" .. name .. "' has been loaded successfully!", + Duration = 3 + }) + end end - -- 2) Try library-level methods with table - if not success and themeTable then - local libFnNames_tab = {"SetTheme", "ApplyTheme", "UpdateTheme", "SetThemeTable", "ApplyThemeTable"} - for _, fn in ipairs(libFnNames_tab) do - tryCall(lib, fn, themeTable) - if success then break end + function SaveManager:DeleteConfig(name) + local fullPath = self.Folder .. "/configs/" .. name .. ".json" + + if isfile(fullPath) then + delfile(fullPath) + + if self.Library then + self.Library:Notify({ + Title = "Config Deleted", + Content = "Configuration '" .. name .. "' has been deleted.", + Duration = 3 + }) + end end end - -- 3) Try window objects inside library (common patterns) - if not success then - local candidates = {} - if lib.Windows and type(lib.Windows) == "table" then - for _, w in pairs(lib.Windows) do table.insert(candidates, w) end - end - if lib.Window and type(lib.Window) == "table" then table.insert(candidates, lib.Window) end - -- also scan top-level fields for Table-like windows - for k, v in pairs(lib) do - if type(v) == "table" and (v.SetTheme or v.ApplyTheme or v.UpdateTheme) then - table.insert(candidates, v) - end + function SaveManager:GetConfigs() + if not isfolder(self.Folder .. "/configs") then + makefolder(self.Folder .. "/configs") end - for _, win in ipairs(candidates) do - if success then break end - local tryFns = {"SetTheme", "ApplyTheme", "SetThemeName", "UpdateTheme", "ApplyThemeTable"} - for _, fn in ipairs(tryFns) do - tryCall(win, fn, themeTable or name) - if success then break end - end + local configs = {} + for _, file in next, listfiles(self.Folder .. "/configs") do + local name = file:gsub(self.Folder .. "/configs/", ""):gsub("%.json", "") + table.insert(configs, name) end + + return configs end - -- 4) Last-resort generic attempts - if not success then - local fallbacks = {"ApplyTheme", "UpdateTheme", "SetColors", "ReloadTheme", "LoadTheme"} - for _, fn in ipairs(fallbacks) do - tryCall(lib, fn, themeTable or name) - if success then break end + function SaveManager:LoadAutoloadConfig() + local path = self.Folder .. "/autoload.txt" + if isfile(path) then + local name = readfile(path) + + -- รอให้ UI โหลดเสร็จก่อน + task.wait(1) + + if isfile(self.Folder .. "/configs/" .. name .. ".json") then + self:LoadConfig(name) + end end end - -- Save settings and call hooks if success - if success then - self.Settings.Theme = name - pcall(function() self:SaveSettings() end) - if self.OnThemeChanged then - pcall(function() self.OnThemeChanged(name, themeTable) end) - end - else - -- debug output to help user understand what was tried - warn("[InterfaceManager] ApplyTheme('" .. tostring(name) .. "') failed. Methods tried:") - for _, t in ipairs(tried) do - local tname = tostring(t.fn) .. " on " .. (t.target and tostring(t.target) or "nil") - warn(" ", tname, " ok=", tostring(t.ok)) - if not t.ok and t.err then warn(" err:", tostring(t.err)) end + function SaveManager:SetAutoloadConfig(name) + writefile(self.Folder .. "/autoload.txt", name) + + if self.Library then + self.Library:Notify({ + Title = "Autoload Set", + Content = "'" .. name .. "' will load automatically on startup.", + Duration = 3 + }) end end - return success -end + -- สร้าง UI สำหรับจัดการ Config + function SaveManager:BuildConfigSection(tab) + assert(self.Library, "SaveManager.Library must be set") + + local section = tab:AddSection("Configuration") + + local configList = section:AddDropdown("ConfigList", { + Title = "Select Config", + Values = self:GetConfigs(), + Default = nil + }) + + local configName = section:AddInput("ConfigName", { + Title = "Config Name", + Placeholder = "Enter config name...", + Default = "" + }) + + -- ปุ่ม Save + section:AddButton({ + Title = "Save Config", + Description = "Save current settings", + Callback = function() + local name = configName.Value + if name and name ~= "" then + self:SaveConfig(name) + configList:SetValues(self:GetConfigs()) + configList:SetValue(name) + else + self.Library:Notify({ + Title = "Error", + Content = "Please enter a config name!", + Duration = 3 + }) + end + end + }) + + -- ปุ่ม Load + section:AddButton({ + Title = "Load Config", + Description = "Load selected configuration", + Callback = function() + local selected = configList.Value + if selected then + self:LoadConfig(selected) + else + self.Library:Notify({ + Title = "Error", + Content = "Please select a config to load!", + Duration = 3 + }) + end + end + }) + + -- ปุ่ม Delete + section:AddButton({ + Title = "Delete Config", + Description = "Delete selected configuration", + Callback = function() + local selected = configList.Value + if selected then + self:DeleteConfig(selected) + configList:SetValues(self:GetConfigs()) + else + self.Library:Notify({ + Title = "Error", + Content = "Please select a config to delete!", + Duration = 3 + }) + end + end + }) + + -- ปุ่ม Refresh + section:AddButton({ + Title = "Refresh List", + Description = "Refresh config list", + Callback = function() + configList:SetValues(self:GetConfigs()) + end + }) + + -- Autoload Toggle + section:AddToggle("AutoloadToggle", { + Title = "Autoload Config", + Description = "Automatically load selected config on startup", + Default = false, + Callback = function(value) + if value then + local selected = configList.Value + if selected then + self:SetAutoloadConfig(selected) + end + else + if isfile(self.Folder .. "/autoload.txt") then + delfile(self.Folder .. "/autoload.txt") + end + end + end + }) --- Convenience: change theme programmatically (uses ApplyTheme) -function InterfaceManager:SetTheme(name) - if type(name) ~= "string" then return end - local ok = self:ApplyTheme(name) - if not ok then - -- Still save so UI shows the selected value - self.Settings.Theme = name - pcall(function() self:SaveSettings() end) - warn("[InterfaceManager] SetTheme: couldn't apply theme '"..tostring(name).."', saved as default only.") + -- ปุ่ม Refresh (เพื่ออัพเดทรายการ) + section:AddButton({ + Title = "Set Autoload", + Description = "Set selected config as autoload", + Callback = function() + local selected = configList.Value + if selected then + self:SetAutoloadConfig(selected) + else + self.Library:Notify({ + Title = "Error", + Content = "Please select a config first!", + Duration = 3 + }) + end + end + }) + + return section end end --- ====================================================================== --- Build interface section for a tab --- Expects tab:AddSection, section:AddDropdown/AddToggle/AddKeybind etc. --- ====================================================================== -function InterfaceManager:BuildInterfaceSection(tab) - assert(tab, "InterfaceManager:BuildInterfaceSection requires a tab object") - - local Library = self.Library or {} - local Settings = self.Settings - - -- load saved settings first - self:LoadSettings() - - -- derive theme list - local themeValues = Themes.Names or {} - if Library.Themes and type(Library.Themes.Names) == "table" then - themeValues = Library.Themes.Names - elseif Library.Themes and type(Library.Themes) == "table" then - local names = {} - for k, v in pairs(Library.Themes) do - if type(k) == "string" then table.insert(names, k) end +-- InterfaceManager (ปรับปรุงเล็กน้อย) +local InterfaceManager = {} do + InterfaceManager.Folder = "FluentSettings" + InterfaceManager.Settings = { + Theme = "Dark", + Acrylic = true, + Transparency = true, + MenuKeybind = "LeftControl" + } + + 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 _, str in next, paths do + if not isfolder(str) then + makefolder(str) + end end - if #names > 0 then themeValues = names end end - -- create section - local section = nil - if tab.AddSection then - section = tab:AddSection("Interface") - else - error("tab:AddSection missing - cannot build interface section") + 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 success, decoded = pcall(function() + return httpService:JSONDecode(readfile(path)) + end) + + if success then + for i, v in next, decoded do + InterfaceManager.Settings[i] = v + end + end + end end - -- Dropdown for theme selection - if section.AddDropdown then + 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 = themeValues, + Values = Library.Themes, Default = Settings.Theme, Callback = function(Value) - -- Use InterfaceManager:SetTheme which handles fallbacks & saving - pcall(function() self:SetTheme(Value) end) - - -- Also try calling library directly if it has a SetTheme that expects a table - -- (keeps backward compatibility) - if Library and type(Library.SetTheme) == "function" then - -- try both string and table - pcall(function() - local use = Library.Themes and Library.Themes[Value] or Settings.Theme - if use then - -- if library expects table, passing table is safe; if expects string, it might ignore - Library:SetTheme(use) - else - Library:SetTheme(Value) - end - end) - end - + Library:SetTheme(Value) Settings.Theme = Value - self:SaveSettings() + InterfaceManager:SaveSettings() end }) - if InterfaceTheme and type(InterfaceTheme.SetValue) == "function" then - pcall(function() InterfaceTheme:SetValue(Settings.Theme) end) - end - else - error("section:AddDropdown missing - cannot build theme dropdown") - end - - -- Acrylic toggle (only add if library indicates support) - if Library and Library.UseAcrylic and section.AddToggle then - section:AddToggle("AcrylicToggle", { - Title = "Acrylic", - Description = "The blurred background requires graphic quality 8+", - Default = Settings.Acrylic, - Callback = function(Value) - if Library and type(Library.ToggleAcrylic) == "function" then - pcall(function() Library:ToggleAcrylic(Value) end) + InterfaceTheme:SetValue(Settings.Theme) + + 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 - Settings.Acrylic = Value - self:SaveSettings() - end - }) - end - - -- Transparency toggle - if section.AddToggle then + }) + end + section:AddToggle("TransparentToggle", { Title = "Transparency", Description = "Makes the interface transparent.", Default = Settings.Transparency, Callback = function(Value) - if Library and type(Library.ToggleTransparency) == "function" then - pcall(function() Library:ToggleTransparency(Value) end) - end + Library:ToggleTransparency(Value) Settings.Transparency = Value - self:SaveSettings() + 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 - - -- Keybind for minimize (if supported) - if section.AddKeybind then - local MenuKeybind = section:AddKeybind("MenuKeybind", { Title = "Minimize Bind", Default = Settings.MenuKeybind }) - if MenuKeybind and type(MenuKeybind.OnChanged) == "function" then - MenuKeybind:OnChanged(function() - Settings.MenuKeybind = MenuKeybind.Value - self:SaveSettings() - end) - end - if Library then - Library.MinimizeKeybind = MenuKeybind - end - end - - return true -end - --- expose Themes for direct access -InterfaceManager.Themes = Themes -print("---- Fluent API ----") -for k,v in pairs(Fluent) do print(k, type(v)) end -if Fluent.Window then - print("Fluent.Window methods:") - for k,v in pairs(Fluent.Window) do print(" ", k, type(v)) end -end -if Fluent.Themes then - print("Fluent.Themes:", table.concat(Fluent.Themes.Names or {}, ", ")) end - -return InterfaceManager +-- ส่งออกทั้งสองตัว +return { + SaveManager = SaveManager, + InterfaceManager = InterfaceManager +} From e4883b0480af8bac62dabdb702a819930e1598d9 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Sun, 9 Nov 2025 02:43:49 +0700 Subject: [PATCH 45/76] Update config.lua --- config.lua | 1078 +++++++++++++++++++++++++++++----------------------- 1 file changed, 595 insertions(+), 483 deletions(-) diff --git a/config.lua b/config.lua index aa7eadc..4d18462 100644 --- a/config.lua +++ b/config.lua @@ -1,488 +1,600 @@ local httpService = game:GetService("HttpService") - ---[[ - SaveManager - ระบบ Auto Save/Load อัตโนมัติ - รองรับ: Toggle, Slider, Dropdown, Input, Keybind, Colorpicker -]] +local Workspace = game:GetService("Workspace") local SaveManager = {} do - SaveManager.Folder = "FluentSettings" - SaveManager.Ignore = {} -- Elements ที่ไม่ต้องการ save - SaveManager.Parser = { - Toggle = { - Save = function(idx, object) - return { type = "Toggle", idx = idx, value = object.Value } - end, - Load = function(idx, data) - if SaveManager.Library.Options[idx] then - SaveManager.Library.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.Library.Options[idx] then - SaveManager.Library.Options[idx]:SetValue(tonumber(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.Library.Options[idx] then - SaveManager.Library.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.Library.Options[idx] then - SaveManager.Library.Options[idx]:SetValueRGB(Color3.fromHex(data.value), data.transparency) - end - end - }, - Keybind = { - Save = function(idx, object) - return { type = "Keybind", idx = idx, value = object.Value, mode = object.Mode } - end, - Load = function(idx, data) - if SaveManager.Library.Options[idx] then - SaveManager.Library.Options[idx]:SetValue(data.value, data.mode) - end - end - }, - Input = { - Save = function(idx, object) - return { type = "Input", idx = idx, value = object.Value } - end, - Load = function(idx, data) - if SaveManager.Library.Options[idx] then - SaveManager.Library.Options[idx]:SetValue(data.value) - end - end - } - } - - function SaveManager:SetIgnoreIndexes(list) - for _, v in next, list do - self.Ignore[v] = true - end - end - - function SaveManager:SetFolder(folder) - self.Folder = folder - self:BuildFolderTree() - end - - function SaveManager:SetLibrary(library) - self.Library = library - end - - function SaveManager: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 .. "/configs") - - for _, str in next, paths do - if not isfolder(str) then - makefolder(str) - end - end - end - - function SaveManager:SaveConfig(name) - if not self.Library then - return warn("[SaveManager] Library not set") - end - - local fullPath = self.Folder .. "/configs/" .. name .. ".json" - local data = { - objects = {} - } - - for idx, option in next, self.Library.Options do - if not self.Ignore[idx] then - local optionType = option.Type - if self.Parser[optionType] then - data.objects[idx] = self.Parser[optionType].Save(idx, option) - end - end - end - - local success, encoded = pcall(httpService.JSONEncode, httpService, data) - if not success then - return warn("[SaveManager] Failed to encode config: " .. name) - end - - writefile(fullPath, encoded) - - -- แจ้งเตือน - if self.Library then - self.Library:Notify({ - Title = "Config Saved", - Content = "Configuration '" .. name .. "' has been saved successfully!", - Duration = 3 - }) - end - end - - function SaveManager:LoadConfig(name) - if not self.Library then - return warn("[SaveManager] Library not set") - end - - local fullPath = self.Folder .. "/configs/" .. name .. ".json" - - if not isfile(fullPath) then - return warn("[SaveManager] Config file not found: " .. name) - end - - local success, decoded = pcall(function() - return httpService:JSONDecode(readfile(fullPath)) - end) - - if not success then - return warn("[SaveManager] Failed to decode config: " .. name) - end - - -- โหลดค่าทั้งหมด - for idx, data in next, decoded.objects do - local optionType = data.type - if self.Parser[optionType] then - task.spawn(function() - self.Parser[optionType].Load(idx, data) - end) - end - end - - -- แจ้งเตือน - if self.Library then - self.Library:Notify({ - Title = "Config Loaded", - Content = "Configuration '" .. name .. "' has been loaded successfully!", - Duration = 3 - }) - end - end - - function SaveManager:DeleteConfig(name) - local fullPath = self.Folder .. "/configs/" .. name .. ".json" - - if isfile(fullPath) then - delfile(fullPath) - - if self.Library then - self.Library:Notify({ - Title = "Config Deleted", - Content = "Configuration '" .. name .. "' has been deleted.", - Duration = 3 - }) - end - end - end - - function SaveManager:GetConfigs() - if not isfolder(self.Folder .. "/configs") then - makefolder(self.Folder .. "/configs") - end - - local configs = {} - for _, file in next, listfiles(self.Folder .. "/configs") do - local name = file:gsub(self.Folder .. "/configs/", ""):gsub("%.json", "") - table.insert(configs, name) - end - - return configs - end - - function SaveManager:LoadAutoloadConfig() - local path = self.Folder .. "/autoload.txt" - if isfile(path) then - local name = readfile(path) - - -- รอให้ UI โหลดเสร็จก่อน - task.wait(1) - - if isfile(self.Folder .. "/configs/" .. name .. ".json") then - self:LoadConfig(name) - end - end - end - - function SaveManager:SetAutoloadConfig(name) - writefile(self.Folder .. "/autoload.txt", name) - - if self.Library then - self.Library:Notify({ - Title = "Autoload Set", - Content = "'" .. name .. "' will load automatically on startup.", - Duration = 3 - }) - end - end - - -- สร้าง UI สำหรับจัดการ Config - function SaveManager:BuildConfigSection(tab) - assert(self.Library, "SaveManager.Library must be set") - - local section = tab:AddSection("Configuration") - - local configList = section:AddDropdown("ConfigList", { - Title = "Select Config", - Values = self:GetConfigs(), - Default = nil - }) - - local configName = section:AddInput("ConfigName", { - Title = "Config Name", - Placeholder = "Enter config name...", - Default = "" - }) - - -- ปุ่ม Save - section:AddButton({ - Title = "Save Config", - Description = "Save current settings", - Callback = function() - local name = configName.Value - if name and name ~= "" then - self:SaveConfig(name) - configList:SetValues(self:GetConfigs()) - configList:SetValue(name) - else - self.Library:Notify({ - Title = "Error", - Content = "Please enter a config name!", - Duration = 3 - }) - end - end - }) - - -- ปุ่ม Load - section:AddButton({ - Title = "Load Config", - Description = "Load selected configuration", - Callback = function() - local selected = configList.Value - if selected then - self:LoadConfig(selected) - else - self.Library:Notify({ - Title = "Error", - Content = "Please select a config to load!", - Duration = 3 - }) - end - end - }) - - -- ปุ่ม Delete - section:AddButton({ - Title = "Delete Config", - Description = "Delete selected configuration", - Callback = function() - local selected = configList.Value - if selected then - self:DeleteConfig(selected) - configList:SetValues(self:GetConfigs()) - else - self.Library:Notify({ - Title = "Error", - Content = "Please select a config to delete!", - Duration = 3 - }) - end - end - }) - - -- ปุ่ม Refresh - section:AddButton({ - Title = "Refresh List", - Description = "Refresh config list", - Callback = function() - configList:SetValues(self:GetConfigs()) - end - }) - - -- Autoload Toggle - section:AddToggle("AutoloadToggle", { - Title = "Autoload Config", - Description = "Automatically load selected config on startup", - Default = false, - Callback = function(value) - if value then - local selected = configList.Value - if selected then - self:SetAutoloadConfig(selected) - end - else - if isfile(self.Folder .. "/autoload.txt") then - delfile(self.Folder .. "/autoload.txt") - end - end - end - }) - - -- ปุ่ม Refresh (เพื่ออัพเดทรายการ) - section:AddButton({ - Title = "Set Autoload", - Description = "Set selected config as autoload", - Callback = function() - local selected = configList.Value - if selected then - self:SetAutoloadConfig(selected) - else - self.Library:Notify({ - Title = "Error", - Content = "Please select a config first!", - Duration = 3 - }) - end - end - }) - - return section - end -end - --- InterfaceManager (ปรับปรุงเล็กน้อย) -local InterfaceManager = {} do - InterfaceManager.Folder = "FluentSettings" - InterfaceManager.Settings = { - Theme = "Dark", - Acrylic = true, - Transparency = true, - MenuKeybind = "LeftControl" - } - - 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 _, str in next, paths do - 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 success, decoded = pcall(function() - return httpService:JSONDecode(readfile(path)) - end) - - 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) - - 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 + 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 getMapName() + local ok, map = pcall(function() return Workspace:FindFirstChild("Map") end) + if ok and map and map:IsA("Instance") then + return sanitizeFilename(map.Name) + end + local ok2, wname = pcall(function() return Workspace.Name end) + if ok2 and wname then return sanitizeFilename(wname) end + return "UnknownMap" + end + + local function ensureFolder(path) + if not isfolder(path) then + makefolder(path) + end + end + + local function getConfigsFolder(self) + local root = self.FolderRoot + local placeId = getPlaceId() + local mapName = getMapName() + return root .. "/" .. placeId .. "/" .. mapName .. "/settings" + 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) + + local mapName = getMapName() + local mapFolder = placeFolder .. "/" .. mapName + ensureFolder(mapFolder) + + local settingsFolder = mapFolder .. "/settings" + ensureFolder(settingsFolder) + + -- 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 = settingsFolder .. "/" .. 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 = settingsFolder .. "/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 + + -- ฟังก์ชันลบ Config + 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 + + -- ฟังก์ชันเช็ค Autoload + function SaveManager:GetAutoloadConfig() + local autopath = getConfigsFolder(self) .. "/autoload.txt" + if isfile(autopath) then + return readfile(autopath) + end + return nil + end + + -- ฟังก์ชันตั้ง Autoload + 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 + + -- ฟังก์ชันปิด Autoload + 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" 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 + 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 + AutoloadToggle:SetDesc('Current: "' .. selectedConfig .. '"') + self.Library:Notify({ + Title = "Config Loader", + Content = "Autoload Enabled", + SubContent = string.format('"%s" will load automatically', selectedConfig), + Duration = 3 + }) + else + 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:SetDesc("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 + AutoloadToggle:SetValue(true) + AutoloadToggle:SetDesc('Current: "' .. currentAutoload .. '"') + else + AutoloadToggle:SetValue(false) + AutoloadToggle:SetDesc("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 + }) + + SaveManager:SetIgnoreIndexes({ + "SaveManager_ConfigList", + "SaveManager_ConfigName", + "SaveManager_AutoloadToggle" + }) + end + + SaveManager:BuildFolderTree() end --- ส่งออกทั้งสองตัว -return { - SaveManager = SaveManager, - InterfaceManager = InterfaceManager -} +return SaveManager From c713c0c60a39bbef2c2f103a8e7b8f4d8141f82f Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Sun, 9 Nov 2025 03:11:58 +0700 Subject: [PATCH 46/76] Update config.lua --- config.lua | 297 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 167 insertions(+), 130 deletions(-) diff --git a/config.lua b/config.lua index 4d18462..86562c6 100644 --- a/config.lua +++ b/config.lua @@ -5,6 +5,8 @@ local SaveManager = {} do SaveManager.FolderRoot = "ATGSettings" SaveManager.Ignore = {} SaveManager.Options = {} + SaveManager.AutoSaveEnabled = false + SaveManager.AutoSaveConnection = nil SaveManager.Parser = { Toggle = { Save = function(idx, object) @@ -28,7 +30,7 @@ local SaveManager = {} do }, Dropdown = { Save = function(idx, object) - return { type = "Dropdown", idx = idx, value = object.Value, mutli = object.Multi } + return { type = "Dropdown", idx = idx, value = object.Value, multi = object.Multi } end, Load = function(idx, data) if SaveManager.Options[idx] then @@ -83,16 +85,6 @@ local SaveManager = {} do return "UnknownPlace" end - local function getMapName() - local ok, map = pcall(function() return Workspace:FindFirstChild("Map") end) - if ok and map and map:IsA("Instance") then - return sanitizeFilename(map.Name) - end - local ok2, wname = pcall(function() return Workspace.Name end) - if ok2 and wname then return sanitizeFilename(wname) end - return "UnknownMap" - end - local function ensureFolder(path) if not isfolder(path) then makefolder(path) @@ -102,8 +94,7 @@ local SaveManager = {} do local function getConfigsFolder(self) local root = self.FolderRoot local placeId = getPlaceId() - local mapName = getMapName() - return root .. "/" .. placeId .. "/" .. mapName .. "/settings" + return root .. "/" .. placeId end local function getConfigFilePath(self, name) @@ -119,39 +110,54 @@ local SaveManager = {} do local placeFolder = root .. "/" .. placeId ensureFolder(placeFolder) - local mapName = getMapName() - local mapFolder = placeFolder .. "/" .. mapName - ensureFolder(mapFolder) - - local settingsFolder = mapFolder .. "/settings" - ensureFolder(settingsFolder) - -- 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 = settingsFolder .. "/" .. base .. ".json" - if not isfile(dest) then - local ok, data = pcall(readfile, f) - if ok and data then - pcall(writefile, dest, data) + local legacyPaths = { + root .. "/settings", + root .. "/" .. placeId .. "/*/settings" + } + + for _, legacyPattern in ipairs(legacyPaths) do + if isfolder(legacyPattern) or legacyPattern:find("*") then + local function tryMigrate(legacySettingsFolder) + 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 - end - local autopath = legacySettingsFolder .. "/autoload.txt" - if isfile(autopath) then - local autodata = readfile(autopath) - local destAuto = settingsFolder .. "/autoload.txt" - if not isfile(destAuto) then - pcall(writefile, destAuto, autodata) + if legacyPattern:find("*") then + local baseFolder = legacyPattern:match("^(.*)/%*") + if isfolder(baseFolder) then + local subfolders = listfiles(baseFolder) + for _, subfolder in ipairs(subfolders) do + tryMigrate(subfolder .. "/settings") + end + end + else + tryMigrate(legacyPattern) end end end @@ -219,7 +225,6 @@ local SaveManager = {} do return true end - -- ฟังก์ชันลบ Config function SaveManager:Delete(name) if not name then return false, "no config file is selected" @@ -232,7 +237,6 @@ local SaveManager = {} do delfile(file) - -- ถ้าเป็น autoload ให้ลบ autoload ด้วย local autopath = getConfigsFolder(self) .. "/autoload.txt" if isfile(autopath) then local currentAutoload = readfile(autopath) @@ -244,7 +248,6 @@ local SaveManager = {} do return true end - -- ฟังก์ชันเช็ค Autoload function SaveManager:GetAutoloadConfig() local autopath = getConfigsFolder(self) .. "/autoload.txt" if isfile(autopath) then @@ -253,7 +256,6 @@ local SaveManager = {} do return nil end - -- ฟังก์ชันตั้ง Autoload function SaveManager:SetAutoloadConfig(name) if not name then return false, "no config name provided" @@ -269,7 +271,6 @@ local SaveManager = {} do return true end - -- ฟังก์ชันปิด Autoload function SaveManager:DisableAutoload() local autopath = getConfigsFolder(self) .. "/autoload.txt" if isfile(autopath) then @@ -279,6 +280,36 @@ local SaveManager = {} do return false, "no autoload config set" end + function SaveManager:StartAutoSave(configName) + self:StopAutoSave() + self.AutoSaveEnabled = true + + self.AutoSaveConnection = task.spawn(function() + while self.AutoSaveEnabled do + task.wait(60) -- 60 วินาที = 1 นาที + if self.AutoSaveEnabled and configName then + local success, err = self:Save(configName) + if success and self.Library then + self.Library:Notify({ + Title = "Auto Save", + Content = "Config Saved", + SubContent = string.format('Auto-saved "%s"', configName), + Duration = 2 + }) + end + end + end + end) + end + + function SaveManager:StopAutoSave() + self.AutoSaveEnabled = false + if self.AutoSaveConnection then + task.cancel(self.AutoSaveConnection) + self.AutoSaveConnection = nil + end + end + function SaveManager:IgnoreThemeSettings() self:SetIgnoreIndexes({ "InterfaceTheme", "AcrylicToggle", "TransparentToggle", "MenuKeybind" @@ -335,7 +366,7 @@ local SaveManager = {} do section:AddInput("SaveManager_ConfigName", { Title = "💾 Config Name", Placeholder = "Enter config name...", - Description = "Type a name for your new config" + Description = "Type a name for your config file" }) -- Config List Dropdown @@ -346,7 +377,7 @@ local SaveManager = {} do Description = "Select a config to manage" }) - -- Autoload Status Display + -- Autoload Toggle local currentAutoload = self:GetAutoloadConfig() local AutoloadToggle = section:AddToggle("SaveManager_AutoloadToggle", { Title = "🔄 Auto Load", @@ -356,9 +387,10 @@ local SaveManager = {} do local selectedConfig = SaveManager.Options.SaveManager_ConfigList.Value if value then - -- เปิด Autoload if not selectedConfig then - AutoloadToggle:SetValue(false) + if AutoloadToggle.SetValue then + AutoloadToggle:SetValue(false) + end return self.Library:Notify({ Title = "Config Loader", Content = "Error", @@ -369,7 +401,9 @@ local SaveManager = {} do local success, err = self:SetAutoloadConfig(selectedConfig) if success then - AutoloadToggle:SetDesc('Current: "' .. selectedConfig .. '"') + if AutoloadToggle.SetDescription then + AutoloadToggle:SetDescription('Current: "' .. selectedConfig .. '"') + end self.Library:Notify({ Title = "Config Loader", Content = "Autoload Enabled", @@ -377,7 +411,9 @@ local SaveManager = {} do Duration = 3 }) else - AutoloadToggle:SetValue(false) + if AutoloadToggle.SetValue then + AutoloadToggle:SetValue(false) + end self.Library:Notify({ Title = "Config Loader", Content = "Error", @@ -386,10 +422,11 @@ local SaveManager = {} do }) end else - -- ปิด Autoload local success, err = self:DisableAutoload() if success then - AutoloadToggle:SetDesc("No autoload config set") + if AutoloadToggle.SetDescription then + AutoloadToggle:SetDescription("No autoload config set") + end self.Library:Notify({ Title = "Config Loader", Content = "Autoload Disabled", @@ -401,89 +438,73 @@ local SaveManager = {} do end }) - section:AddButton({ - Title = "💾 Save New Config", - Description = "Create a new configuration file", - Callback = function() - local name = SaveManager.Options.SaveManager_ConfigName.Value + -- Auto Save Toggle + local AutoSaveToggle = section:AddToggle("SaveManager_AutoSaveToggle", { + Title = "💾 Auto Save", + Description = "Automatically save config every 1 minute", + Default = false, + Callback = function(value) + local selectedConfig = SaveManager.Options.SaveManager_ConfigList.Value + + if value then + if not selectedConfig then + if AutoSaveToggle.SetValue then + AutoSaveToggle:SetValue(false) + end + return self.Library:Notify({ + Title = "Auto Save", + Content = "Error", + SubContent = "Please select a config first", + Duration = 3 + }) + end - if name:gsub(" ", "") == "" then - return self.Library:Notify({ - Title = "Config Loader", - Content = "Invalid Name", - SubContent = "Config name cannot be empty", + self:StartAutoSave(selectedConfig) + self.Library:Notify({ + Title = "Auto Save", + Content = "Auto Save Enabled", + SubContent = string.format('"%s" will save every 1 minute', selectedConfig), 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 + else + self:StopAutoSave() + self.Library:Notify({ + Title = "Auto Save", + Content = "Auto Save Disabled", + SubContent = "Automatic saving stopped", + Duration = 3 }) 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 - }) + -- Update Auto Save when config selection changes + ConfigListDropdown.Changed = function(value) + if SaveManager.AutoSaveEnabled then + self:StopAutoSave() + if AutoSaveToggle.SetValue then + AutoSaveToggle:SetValue(false) end - self.Library:Notify({ - Title = "Config Loader", - Content = "Config Loaded", - SubContent = string.format('Loaded "%s"', name), - Duration = 3 + Title = "Auto Save", + Content = "Auto Save Stopped", + SubContent = "Config selection changed", + Duration = 2 }) end - }) + end section:AddButton({ - Title = "✏️ Overwrite Config", - Description = "Save current settings to selected config", + Title = "📝 Create Config File", + Description = "Create a new empty configuration file", Callback = function() - local name = SaveManager.Options.SaveManager_ConfigList.Value + local name = SaveManager.Options.SaveManager_ConfigName.Value - if not name then + if name:gsub(" ", "") == "" then return self.Library:Notify({ Title = "Config Loader", - Content = "No Config Selected", - SubContent = "Please select a config to overwrite", + Content = "Invalid Name", + SubContent = "Config name cannot be empty", Duration = 3 }) end @@ -492,7 +513,7 @@ local SaveManager = {} do if not success then return self.Library:Notify({ Title = "Config Loader", - Content = "Save Failed", + Content = "Creation Failed", SubContent = err or "Unknown error", Duration = 5 }) @@ -500,10 +521,13 @@ local SaveManager = {} do self.Library:Notify({ Title = "Config Loader", - Content = "Config Updated", - SubContent = string.format('Overwrote "%s"', name), + Content = "Config Created", + SubContent = string.format('Created "%s"', name), Duration = 3 }) + + ConfigListDropdown:SetValues(self:RefreshConfigList()) + ConfigListDropdown:SetValue(name) end }) @@ -522,7 +546,6 @@ local SaveManager = {} do }) end - -- Confirmation dialog self.Library:Dialog({ Title = "Delete Config", Content = string.format('Are you sure you want to delete "%s"?', name), @@ -530,6 +553,14 @@ local SaveManager = {} do { Title = "Delete", Callback = function() + -- Stop auto save if deleting active config + if self.AutoSaveEnabled then + self:StopAutoSave() + if AutoSaveToggle.SetValue then + AutoSaveToggle:SetValue(false) + end + end + local success, err = self:Delete(name) if not success then return self.Library:Notify({ @@ -547,18 +578,24 @@ local SaveManager = {} do 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 - AutoloadToggle:SetValue(true) - AutoloadToggle:SetDesc('Current: "' .. currentAutoload .. '"') + if AutoloadToggle.SetValue then + AutoloadToggle:SetValue(true) + end + if AutoloadToggle.SetDescription then + AutoloadToggle:SetDescription('Current: "' .. currentAutoload .. '"') + end else - AutoloadToggle:SetValue(false) - AutoloadToggle:SetDesc("No autoload config set") + if AutoloadToggle.SetValue then + AutoloadToggle:SetValue(false) + end + if AutoloadToggle.SetDescription then + AutoloadToggle:SetDescription("No autoload config set") + end end end }, @@ -576,7 +613,6 @@ local SaveManager = {} do Callback = function() local configs = self:RefreshConfigList() ConfigListDropdown:SetValues(configs) - ConfigListDropdown:SetValue(nil) self.Library:Notify({ Title = "Config Loader", @@ -590,7 +626,8 @@ local SaveManager = {} do SaveManager:SetIgnoreIndexes({ "SaveManager_ConfigList", "SaveManager_ConfigName", - "SaveManager_AutoloadToggle" + "SaveManager_AutoloadToggle", + "SaveManager_AutoSaveToggle" }) end From 7618610de91b97c8c5296e9e203a108d7049fdca Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Sun, 9 Nov 2025 03:18:13 +0700 Subject: [PATCH 47/76] Update config.lua --- config.lua | 301 +++++++++++++++++++++++------------------------------ 1 file changed, 130 insertions(+), 171 deletions(-) diff --git a/config.lua b/config.lua index 86562c6..2289b80 100644 --- a/config.lua +++ b/config.lua @@ -5,8 +5,6 @@ local SaveManager = {} do SaveManager.FolderRoot = "ATGSettings" SaveManager.Ignore = {} SaveManager.Options = {} - SaveManager.AutoSaveEnabled = false - SaveManager.AutoSaveConnection = nil SaveManager.Parser = { Toggle = { Save = function(idx, object) @@ -30,11 +28,26 @@ local SaveManager = {} do }, Dropdown = { Save = function(idx, object) - return { type = "Dropdown", idx = idx, value = object.Value, multi = object.Multi } + -- แก้ไข: เก็บค่า Multi ด้วยเพื่อรองรับ Multi-select dropdown + 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) + -- รองรับทั้ง single และ multi-select + if data.multi and type(data.value) == "table" then + -- Multi-select: ตั้งค่าทีละตัว + for key, val in pairs(data.value) do + SaveManager.Options[idx]:SetValue(key, val) + end + else + -- Single-select + SaveManager.Options[idx]:SetValue(data.value) + end end end, }, @@ -91,6 +104,7 @@ local SaveManager = {} do end end + -- เปลี่ยนโครงสร้าง: ATGSettings/PlaceId/ local function getConfigsFolder(self) local root = self.FolderRoot local placeId = getPlaceId() @@ -110,54 +124,32 @@ local SaveManager = {} do local placeFolder = root .. "/" .. placeId ensureFolder(placeFolder) - -- Migrate legacy configs - local legacyPaths = { - root .. "/settings", - root .. "/" .. placeId .. "/*/settings" - } - - for _, legacyPattern in ipairs(legacyPaths) do - if isfolder(legacyPattern) or legacyPattern:find("*") then - local function tryMigrate(legacySettingsFolder) - 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) + -- 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 - if legacyPattern:find("*") then - local baseFolder = legacyPattern:match("^(.*)/%*") - if isfolder(baseFolder) then - local subfolders = listfiles(baseFolder) - for _, subfolder in ipairs(subfolders) do - tryMigrate(subfolder .. "/settings") - end - end - else - tryMigrate(legacyPattern) + 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 @@ -237,6 +229,7 @@ local SaveManager = {} do delfile(file) + -- ถ้าเป็น autoload ให้ลบ autoload ด้วย local autopath = getConfigsFolder(self) .. "/autoload.txt" if isfile(autopath) then local currentAutoload = readfile(autopath) @@ -280,36 +273,6 @@ local SaveManager = {} do return false, "no autoload config set" end - function SaveManager:StartAutoSave(configName) - self:StopAutoSave() - self.AutoSaveEnabled = true - - self.AutoSaveConnection = task.spawn(function() - while self.AutoSaveEnabled do - task.wait(60) -- 60 วินาที = 1 นาที - if self.AutoSaveEnabled and configName then - local success, err = self:Save(configName) - if success and self.Library then - self.Library:Notify({ - Title = "Auto Save", - Content = "Config Saved", - SubContent = string.format('Auto-saved "%s"', configName), - Duration = 2 - }) - end - end - end - end) - end - - function SaveManager:StopAutoSave() - self.AutoSaveEnabled = false - if self.AutoSaveConnection then - task.cancel(self.AutoSaveConnection) - self.AutoSaveConnection = nil - end - end - function SaveManager:IgnoreThemeSettings() self:SetIgnoreIndexes({ "InterfaceTheme", "AcrylicToggle", "TransparentToggle", "MenuKeybind" @@ -327,7 +290,7 @@ local SaveManager = {} do local file = list[i] if file:sub(-5) == ".json" then local name = file:match("([^/\\]+)%.json$") - if name and name ~= "options" then + if name and name ~= "options" and name ~= "autoload" then table.insert(out, name) end end @@ -366,10 +329,10 @@ local SaveManager = {} do section:AddInput("SaveManager_ConfigName", { Title = "💾 Config Name", Placeholder = "Enter config name...", - Description = "Type a name for your config file" + Description = "Type a name for your new config" }) - -- Config List Dropdown + -- Config List Dropdown (เก็บค่าที่เลือกไว้) local ConfigListDropdown = section:AddDropdown("SaveManager_ConfigList", { Title = "📋 Available Configs", Values = self:RefreshConfigList(), @@ -377,7 +340,7 @@ local SaveManager = {} do Description = "Select a config to manage" }) - -- Autoload Toggle + -- Autoload Status Display local currentAutoload = self:GetAutoloadConfig() local AutoloadToggle = section:AddToggle("SaveManager_AutoloadToggle", { Title = "🔄 Auto Load", @@ -387,10 +350,10 @@ local SaveManager = {} do local selectedConfig = SaveManager.Options.SaveManager_ConfigList.Value if value then + -- เปิด Autoload if not selectedConfig then - if AutoloadToggle.SetValue then - AutoloadToggle:SetValue(false) - end + -- แก้ไข: ใช้ Options แทน Toggle object โดยตรง + SaveManager.Options.SaveManager_AutoloadToggle:SetValue(false) return self.Library:Notify({ Title = "Config Loader", Content = "Error", @@ -401,9 +364,8 @@ local SaveManager = {} do local success, err = self:SetAutoloadConfig(selectedConfig) if success then - if AutoloadToggle.SetDescription then - AutoloadToggle:SetDescription('Current: "' .. selectedConfig .. '"') - end + -- แก้ไข: อัพเดท Description + AutoloadToggle.Description = 'Current: "' .. selectedConfig .. '"' self.Library:Notify({ Title = "Config Loader", Content = "Autoload Enabled", @@ -411,9 +373,7 @@ local SaveManager = {} do Duration = 3 }) else - if AutoloadToggle.SetValue then - AutoloadToggle:SetValue(false) - end + SaveManager.Options.SaveManager_AutoloadToggle:SetValue(false) self.Library:Notify({ Title = "Config Loader", Content = "Error", @@ -422,11 +382,10 @@ local SaveManager = {} do }) end else + -- ปิด Autoload local success, err = self:DisableAutoload() if success then - if AutoloadToggle.SetDescription then - AutoloadToggle:SetDescription("No autoload config set") - end + AutoloadToggle.Description = "No autoload config set" self.Library:Notify({ Title = "Config Loader", Content = "Autoload Disabled", @@ -438,73 +397,89 @@ local SaveManager = {} do end }) - -- Auto Save Toggle - local AutoSaveToggle = section:AddToggle("SaveManager_AutoSaveToggle", { - Title = "💾 Auto Save", - Description = "Automatically save config every 1 minute", - Default = false, - Callback = function(value) - local selectedConfig = SaveManager.Options.SaveManager_ConfigList.Value - - if value then - if not selectedConfig then - if AutoSaveToggle.SetValue then - AutoSaveToggle:SetValue(false) - end - return self.Library:Notify({ - Title = "Auto Save", - Content = "Error", - SubContent = "Please select a config first", - Duration = 3 - }) - end + section:AddButton({ + Title = "💾 Save New Config", + Description = "Create a new configuration file", + Callback = function() + local name = SaveManager.Options.SaveManager_ConfigName.Value - self:StartAutoSave(selectedConfig) - self.Library:Notify({ - Title = "Auto Save", - Content = "Auto Save Enabled", - SubContent = string.format('"%s" will save every 1 minute', selectedConfig), + if name:gsub(" ", "") == "" then + return self.Library:Notify({ + Title = "Config Loader", + Content = "Invalid Name", + SubContent = "Config name cannot be empty", Duration = 3 }) - else - self:StopAutoSave() - self.Library:Notify({ - Title = "Auto Save", - Content = "Auto Save Disabled", - SubContent = "Automatic saving stopped", - 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 }) - -- Update Auto Save when config selection changes - ConfigListDropdown.Changed = function(value) - if SaveManager.AutoSaveEnabled then - self:StopAutoSave() - if AutoSaveToggle.SetValue then - AutoSaveToggle:SetValue(false) + 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 = "Auto Save", - Content = "Auto Save Stopped", - SubContent = "Config selection changed", - Duration = 2 + Title = "Config Loader", + Content = "Config Loaded", + SubContent = string.format('Loaded "%s"', name), + Duration = 3 }) end - end + }) section:AddButton({ - Title = "📝 Create Config File", - Description = "Create a new empty configuration file", + Title = "✏️ Overwrite Config", + Description = "Save current settings to selected config", Callback = function() - local name = SaveManager.Options.SaveManager_ConfigName.Value + local name = SaveManager.Options.SaveManager_ConfigList.Value - if name:gsub(" ", "") == "" then + if not name then return self.Library:Notify({ Title = "Config Loader", - Content = "Invalid Name", - SubContent = "Config name cannot be empty", + Content = "No Config Selected", + SubContent = "Please select a config to overwrite", Duration = 3 }) end @@ -513,7 +488,7 @@ local SaveManager = {} do if not success then return self.Library:Notify({ Title = "Config Loader", - Content = "Creation Failed", + Content = "Save Failed", SubContent = err or "Unknown error", Duration = 5 }) @@ -521,13 +496,10 @@ local SaveManager = {} do self.Library:Notify({ Title = "Config Loader", - Content = "Config Created", - SubContent = string.format('Created "%s"', name), + Content = "Config Updated", + SubContent = string.format('Overwrote "%s"', name), Duration = 3 }) - - ConfigListDropdown:SetValues(self:RefreshConfigList()) - ConfigListDropdown:SetValue(name) end }) @@ -546,6 +518,7 @@ local SaveManager = {} do }) end + -- Confirmation dialog self.Library:Dialog({ Title = "Delete Config", Content = string.format('Are you sure you want to delete "%s"?', name), @@ -553,14 +526,6 @@ local SaveManager = {} do { Title = "Delete", Callback = function() - -- Stop auto save if deleting active config - if self.AutoSaveEnabled then - self:StopAutoSave() - if AutoSaveToggle.SetValue then - AutoSaveToggle:SetValue(false) - end - end - local success, err = self:Delete(name) if not success then return self.Library:Notify({ @@ -578,24 +543,18 @@ local SaveManager = {} do 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 - if AutoloadToggle.SetValue then - AutoloadToggle:SetValue(true) - end - if AutoloadToggle.SetDescription then - AutoloadToggle:SetDescription('Current: "' .. currentAutoload .. '"') - end + SaveManager.Options.SaveManager_AutoloadToggle:SetValue(true) + AutoloadToggle.Description = 'Current: "' .. currentAutoload .. '"' else - if AutoloadToggle.SetValue then - AutoloadToggle:SetValue(false) - end - if AutoloadToggle.SetDescription then - AutoloadToggle:SetDescription("No autoload config set") - end + SaveManager.Options.SaveManager_AutoloadToggle:SetValue(false) + AutoloadToggle.Description = "No autoload config set" end end }, @@ -613,6 +572,7 @@ local SaveManager = {} do Callback = function() local configs = self:RefreshConfigList() ConfigListDropdown:SetValues(configs) + ConfigListDropdown:SetValue(nil) self.Library:Notify({ Title = "Config Loader", @@ -623,11 +583,10 @@ local SaveManager = {} do end }) + -- อย่าลืม Ignore UI controls ของ SaveManager เอง SaveManager:SetIgnoreIndexes({ - "SaveManager_ConfigList", "SaveManager_ConfigName", - "SaveManager_AutoloadToggle", - "SaveManager_AutoSaveToggle" + "SaveManager_AutoloadToggle" }) end From 184997961dd0916e536d9e524057e85ed591071f Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Sun, 9 Nov 2025 03:22:06 +0700 Subject: [PATCH 48/76] Update config.lua --- config.lua | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/config.lua b/config.lua index 2289b80..9c83ed2 100644 --- a/config.lua +++ b/config.lua @@ -28,26 +28,11 @@ local SaveManager = {} do }, Dropdown = { Save = function(idx, object) - -- แก้ไข: เก็บค่า Multi ด้วยเพื่อรองรับ Multi-select dropdown - return { - type = "Dropdown", - idx = idx, - value = object.Value, - multi = object.Multi - } + return { type = "Dropdown", idx = idx, value = object.Value, mutli = object.Multi } end, Load = function(idx, data) if SaveManager.Options[idx] then - -- รองรับทั้ง single และ multi-select - if data.multi and type(data.value) == "table" then - -- Multi-select: ตั้งค่าทีละตัว - for key, val in pairs(data.value) do - SaveManager.Options[idx]:SetValue(key, val) - end - else - -- Single-select - SaveManager.Options[idx]:SetValue(data.value) - end + SaveManager.Options[idx]:SetValue(data.value) end end, }, @@ -71,6 +56,7 @@ local SaveManager = {} do end end, }, + Input = { Save = function(idx, object) return { type = "Input", idx = idx, text = object.Value } From e2f31da585b55feb5eec7769919ed9447a0ac6f6 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Sun, 9 Nov 2025 19:49:19 +0700 Subject: [PATCH 49/76] Update autosave.lua --- autosave.lua | 429 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 306 insertions(+), 123 deletions(-) diff --git a/autosave.lua b/autosave.lua index b58cc95..9c83ed2 100644 --- a/autosave.lua +++ b/autosave.lua @@ -2,7 +2,6 @@ local httpService = game:GetService("HttpService") local Workspace = game:GetService("Workspace") local SaveManager = {} do - -- root folder (can be changed via SetFolder) SaveManager.FolderRoot = "ATGSettings" SaveManager.Ignore = {} SaveManager.Options = {} @@ -57,6 +56,7 @@ local SaveManager = {} do end end, }, + Input = { Save = function(idx, object) return { type = "Input", idx = idx, text = object.Value } @@ -84,29 +84,17 @@ local SaveManager = {} do return "UnknownPlace" end - local function getMapName() - local ok, map = pcall(function() return Workspace:FindFirstChild("Map") end) - if ok and map and map:IsA("Instance") then - return sanitizeFilename(map.Name) - end - local ok2, wname = pcall(function() return Workspace.Name end) - if ok2 and wname then return sanitizeFilename(wname) end - return "UnknownMap" - end - local function ensureFolder(path) if not isfolder(path) then makefolder(path) end end - -- get configs folder for current place/map + -- เปลี่ยนโครงสร้าง: ATGSettings/PlaceId/ local function getConfigsFolder(self) local root = self.FolderRoot local placeId = getPlaceId() - local mapName = getMapName() - -- FluentSettings///settings - return root .. "/" .. placeId .. "/" .. mapName .. "/settings" + return root .. "/" .. placeId end local function getConfigFilePath(self, name) @@ -114,7 +102,6 @@ local SaveManager = {} do return folder .. "/" .. name .. ".json" end - -- Build folder tree and migrate legacy configs if found (copy only) function SaveManager:BuildFolderTree() local root = self.FolderRoot ensureFolder(root) @@ -123,14 +110,7 @@ local SaveManager = {} do local placeFolder = root .. "/" .. placeId ensureFolder(placeFolder) - local mapName = getMapName() - local mapFolder = placeFolder .. "/" .. mapName - ensureFolder(mapFolder) - - local settingsFolder = mapFolder .. "/settings" - ensureFolder(settingsFolder) - - -- legacy folder: /settings (old layout). If files exist there, copy them into current map settings + -- Migrate legacy configs (จากโครงสร้างเก่า) local legacySettingsFolder = root .. "/settings" if isfolder(legacySettingsFolder) then local files = listfiles(legacySettingsFolder) @@ -139,24 +119,21 @@ local SaveManager = {} do if f:sub(-5) == ".json" then local base = f:match("([^/\\]+)%.json$") if base and base ~= "options" then - local dest = settingsFolder .. "/" .. base .. ".json" - -- copy only if destination does not exist yet + local dest = placeFolder .. "/" .. base .. ".json" if not isfile(dest) then local ok, data = pcall(readfile, f) if ok and data then - local success, err = pcall(writefile, dest, data) - -- ignore write errors but do not fail + pcall(writefile, dest, data) end end end end end - -- also migrate autoload.txt if present (copy only) local autopath = legacySettingsFolder .. "/autoload.txt" if isfile(autopath) then local autodata = readfile(autopath) - local destAuto = settingsFolder .. "/autoload.txt" + local destAuto = placeFolder .. "/autoload.txt" if not isfile(destAuto) then pcall(writefile, destAuto, autodata) end @@ -171,7 +148,7 @@ local SaveManager = {} do end function SaveManager:SetFolder(folder) - self.FolderRoot = tostring(folder or "FluentSettings") + self.FolderRoot = tostring(folder or "ATGSettings") self:BuildFolderTree() end @@ -186,13 +163,11 @@ local SaveManager = {} do 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 @@ -201,7 +176,6 @@ local SaveManager = {} do return false, "failed to encode data" end - -- ensure folder exists local folder = fullPath:match("^(.*)/[^/]+$") if folder then ensureFolder(folder) end @@ -229,6 +203,62 @@ local SaveManager = {} do 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" @@ -246,7 +276,7 @@ local SaveManager = {} do local file = list[i] if file:sub(-5) == ".json" then local name = file:match("([^/\\]+)%.json$") - if name and name ~= "options" then + if name and name ~= "options" and name ~= "autoload" then table.insert(out, name) end end @@ -255,24 +285,23 @@ local SaveManager = {} do end function SaveManager:LoadAutoloadConfig() - local autopath = getConfigsFolder(self) .. "/autoload.txt" - if isfile(autopath) then - local name = readfile(autopath) + local name = self:GetAutoloadConfig() + if name then local success, err = self:Load(name) if not success then return self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "Failed to load autoload config: " .. err, - Duration = 7 + Title = "Config Loader", + Content = "Failed to load autoload config", + SubContent = err, + Duration = 5 }) end self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Auto loaded config %q", name), - Duration = 7 + Title = "Config Loader", + Content = "Autoload Success", + SubContent = string.format('Loaded "%s"', name), + Duration = 3 }) end end @@ -280,119 +309,273 @@ local SaveManager = {} do function SaveManager:BuildConfigSection(tab) assert(self.Library, "Must set SaveManager.Library") - local section = tab:AddSection("Configuration") + local section = tab:AddSection("📁 Configuration Manager") - section:AddInput("SaveManager_ConfigName", { Title = "Config name" }) - section:AddDropdown("SaveManager_ConfigList", { Title = "Config list", Values = self:RefreshConfigList(), AllowNull = true }) + -- 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 = "Create config", + 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 = "Interface", - Content = "Config loader", - SubContent = "Invalid config name (empty)", - Duration = 7 + 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 = "Interface", - Content = "Config loader", - SubContent = "Failed to save config: " .. err, - Duration = 7 + Title = "Config Loader", + Content = "Save Failed", + SubContent = err or "Unknown error", + Duration = 5 }) end self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Created config %q", name), - Duration = 7 + Title = "Config Loader", + Content = "Config Saved", + SubContent = string.format('Created "%s"', name), + Duration = 3 }) - SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) - SaveManager.Options.SaveManager_ConfigList:SetValue(nil) + -- Refresh dropdown + ConfigListDropdown:SetValues(self:RefreshConfigList()) + ConfigListDropdown:SetValue(name) end }) - section:AddButton({Title = "Load config", Callback = function() - local name = SaveManager.Options.SaveManager_ConfigList.Value + section:AddButton({ + Title = "📂 Load Config", + Description = "Load selected configuration", + Callback = function() + local name = SaveManager.Options.SaveManager_ConfigList.Value - local success, err = self:Load(name) - if not success then - return self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = "Failed to load config: " .. err, - Duration = 7 + 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 + }) - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Loaded config %q", name), - Duration = 7 - }) - end}) + section:AddButton({ + Title = "✏️ Overwrite Config", + Description = "Save current settings to selected config", + Callback = function() + local name = SaveManager.Options.SaveManager_ConfigList.Value - section:AddButton({Title = "Overwrite 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 = "Interface", - Content = "Config loader", - SubContent = "Failed to overwrite config: " .. err, - Duration = 7 + 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 + }) - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Overwrote config %q", name), - Duration = 7 - }) - end}) - - section:AddButton({Title = "Refresh list", Callback = function() - SaveManager.Options.SaveManager_ConfigList:SetValues(self:RefreshConfigList()) - SaveManager.Options.SaveManager_ConfigList:SetValue(nil) - end}) - - local AutoloadButton - AutoloadButton = section:AddButton({Title = "Set as autoload", Description = "Current autoload config: none", Callback = function() - local name = SaveManager.Options.SaveManager_ConfigList.Value - local autopath = getConfigsFolder(self) .. "/autoload.txt" - writefile(autopath, name) - AutoloadButton:SetDesc("Current autoload config: " .. name) - self.Library:Notify({ - Title = "Interface", - Content = "Config loader", - SubContent = string.format("Set %q to auto load", name), - Duration = 7 - }) - end}) + section:AddButton({ + Title = "🗑️ Delete Config", + Description = "Permanently delete selected config", + Callback = function() + local name = SaveManager.Options.SaveManager_ConfigList.Value - -- populate current autoload desc if exists - local autop = getConfigsFolder(self) .. "/autoload.txt" - if isfile(autop) then - local name = readfile(autop) - AutoloadButton:SetDesc("Current autoload config: " .. name) - end + 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 - SaveManager:SetIgnoreIndexes({ "SaveManager_ConfigList", "SaveManager_ConfigName" }) + -- 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 - -- initial build SaveManager:BuildFolderTree() end From 24016850132e255e67ce62068a7c74619d72494c Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Mon, 10 Nov 2025 14:32:25 +0700 Subject: [PATCH 50/76] Update autosave.lua --- autosave.lua | 314 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 221 insertions(+), 93 deletions(-) diff --git a/autosave.lua b/autosave.lua index 9c83ed2..efdb49e 100644 --- a/autosave.lua +++ b/autosave.lua @@ -5,6 +5,8 @@ local SaveManager = {} do SaveManager.FolderRoot = "ATGSettings" SaveManager.Ignore = {} SaveManager.Options = {} + SaveManager.AutoSaveEnabled = false + SaveManager.AutoSaveConfig = nil SaveManager.Parser = { Toggle = { Save = function(idx, object) @@ -90,7 +92,6 @@ local SaveManager = {} do end end - -- เปลี่ยนโครงสร้าง: ATGSettings/PlaceId/ local function getConfigsFolder(self) local root = self.FolderRoot local placeId = getPlaceId() @@ -102,6 +103,12 @@ local SaveManager = {} do return folder .. "/" .. name .. ".json" end + -- ไฟล์สำหรับเซฟ UI ของ SaveManager เอง + local function getSaveManagerUIPath(self) + local folder = getConfigsFolder(self) + return folder .. "/savemanager_ui.json" + end + function SaveManager:BuildFolderTree() local root = self.FolderRoot ensureFolder(root) @@ -110,7 +117,7 @@ local SaveManager = {} do local placeFolder = root .. "/" .. placeId ensureFolder(placeFolder) - -- Migrate legacy configs (จากโครงสร้างเก่า) + -- Migrate legacy configs local legacySettingsFolder = root .. "/settings" if isfolder(legacySettingsFolder) then local files = listfiles(legacySettingsFolder) @@ -183,6 +190,33 @@ local SaveManager = {} do return true end + -- เซฟ UI ของ SaveManager แยกต่างหาก + function SaveManager:SaveUI() + local uiPath = getSaveManagerUIPath(self) + local uiData = { + autoload = self:GetAutoloadConfig(), + autosave_enabled = self.AutoSaveEnabled, + autosave_config = self.AutoSaveConfig + } + + local success, encoded = pcall(httpService.JSONEncode, httpService, uiData) + if success then + writefile(uiPath, encoded) + end + end + + -- โหลด UI ของ SaveManager + 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 + function SaveManager:Load(name) if (not name) then return false, "no config file is selected" @@ -247,6 +281,7 @@ local SaveManager = {} do local autopath = getConfigsFolder(self) .. "/autoload.txt" writefile(autopath, name) + self:SaveUI() return true end @@ -254,6 +289,7 @@ local SaveManager = {} do local autopath = getConfigsFolder(self) .. "/autoload.txt" if isfile(autopath) then delfile(autopath) + self:SaveUI() return true end return false, "no autoload config set" @@ -276,7 +312,7 @@ local SaveManager = {} 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 + if name and name ~= "options" and name ~= "autoload" and name ~= "savemanager_ui" then table.insert(out, name) end end @@ -291,7 +327,7 @@ local SaveManager = {} do if not success then return self.Library:Notify({ Title = "Config Loader", - Content = "Failed to load autoload config", + Content = "ล้มเหลวในการโหลดคอนฟิกอัตโนมัติ", SubContent = err, Duration = 5 }) @@ -299,83 +335,140 @@ local SaveManager = {} do self.Library:Notify({ Title = "Config Loader", - Content = "Autoload Success", - SubContent = string.format('Loaded "%s"', name), + Content = "โหลดอัตโนมัติสำเร็จ", + SubContent = string.format('โหลด "%s" แล้ว', name), Duration = 3 }) end end + -- ฟังก์ชัน Auto Save + function SaveManager:EnableAutoSave(configName) + self.AutoSaveEnabled = true + self.AutoSaveConfig = configName + self:SaveUI() + + -- ตั้งค่า listener สำหรับทุก option + for idx, option in next, self.Options do + if not self.Ignore[idx] and self.Parser[option.Type] then + -- เก็บ callback เดิมไว้ + local originalCallback = option.Callback + + -- สร้าง callback ใหม่ที่รวม auto save + option.Callback = function(...) + -- เรียก callback เดิม + if originalCallback then + originalCallback(...) + end + + -- Auto save + if self.AutoSaveEnabled and self.AutoSaveConfig then + task.spawn(function() + task.wait(0.5) -- รอเล็กน้อยเพื่อไม่ให้เซฟบ่อยเกินไป + self:Save(self.AutoSaveConfig) + end) + end + end + end + end + end + + function SaveManager:DisableAutoSave() + self.AutoSaveEnabled = false + self.AutoSaveConfig = nil + self:SaveUI() + end + function SaveManager:BuildConfigSection(tab) assert(self.Library, "Must set SaveManager.Library") local section = tab:AddSection("📁 Configuration Manager") + -- โหลด UI settings + local uiSettings = self:LoadUI() + -- Config Name Input section:AddInput("SaveManager_ConfigName", { Title = "💾 Config Name", - Placeholder = "Enter config name...", - Description = "Type a name for your new config" + Placeholder = "ใส่ชื่อคอนฟิก...", + Description = "พิมพ์ชื่อสำหรับคอนฟิกใหม่" }) - -- Config List Dropdown (เก็บค่าที่เลือกไว้) + -- Config List Dropdown + local configs = self:RefreshConfigList() local ConfigListDropdown = section:AddDropdown("SaveManager_ConfigList", { Title = "📋 Available Configs", - Values = self:RefreshConfigList(), + Values = configs, AllowNull = true, - Description = "Select a config to manage" + Description = "เลือกคอนฟิกที่ต้องการจัดการ" }) + -- สร้าง AutoSave.json ถ้าไม่มีไฟล์เลย + if #configs == 0 then + local success = self:Save("AutoSave") + if success then + configs = self:RefreshConfigList() + ConfigListDropdown:SetValues(configs) + ConfigListDropdown:SetValue("AutoSave") + + if uiSettings then + self.AutoSaveConfig = "AutoSave" + self.AutoSaveEnabled = uiSettings.autosave_enabled or false + end + end + elseif uiSettings and uiSettings.autosave_config then + -- โหลดค่า autosave config จาก UI settings + ConfigListDropdown:SetValue(uiSettings.autosave_config) + self.AutoSaveConfig = uiSettings.autosave_config + self.AutoSaveEnabled = uiSettings.autosave_enabled or false + end + -- 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", + Description = currentAutoload and ('ปัจจุบัน: "' .. currentAutoload .. '"') or "ไม่มีคอนฟิกโหลดอัตโนมัติ", 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", + Content = "เกิดข้อผิดพลาด", + SubContent = "กรุณาเลือกคอนฟิกก่อน", Duration = 3 }) end local success, err = self:SetAutoloadConfig(selectedConfig) if success then - -- แก้ไข: อัพเดท Description - AutoloadToggle.Description = 'Current: "' .. selectedConfig .. '"' + AutoloadToggle.Description = 'ปัจจุบัน: "' .. selectedConfig .. '"' self.Library:Notify({ Title = "Config Loader", - Content = "Autoload Enabled", - SubContent = string.format('"%s" will load automatically', selectedConfig), + Content = "เปิดโหลดอัตโนมัติแล้ว", + SubContent = string.format('"%s" จะโหลดอัตโนมัติทุกครั้ง', 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", + Content = "เกิดข้อผิดพลาด", + SubContent = err or "ไม่สามารถตั้งค่าโหลดอัตโนมัติได้", Duration = 3 }) end else - -- ปิด Autoload local success, err = self:DisableAutoload() if success then - AutoloadToggle.Description = "No autoload config set" + AutoloadToggle.Description = "ไม่มีคอนฟิกโหลดอัตโนมัติ" self.Library:Notify({ Title = "Config Loader", - Content = "Autoload Disabled", - SubContent = "Configs will no longer auto-load", + Content = "ปิดโหลดอัตโนมัติแล้ว", + SubContent = "คอนฟิกจะไม่โหลดอัตโนมัติอีกต่อไป", Duration = 3 }) end @@ -383,107 +476,126 @@ local SaveManager = {} do end }) - section:AddButton({ - Title = "💾 Save New Config", - Description = "Create a new configuration file", - Callback = function() - local name = SaveManager.Options.SaveManager_ConfigName.Value + -- Auto Save Toggle (แทนที่ปุ่ม Overwrite) + local AutoSaveToggle = section:AddToggle("SaveManager_AutoSaveToggle", { + Title = "💾 Auto Save", + Description = self.AutoSaveConfig and ('กำลังบันทึกอัตโนมัติไปที่: "' .. self.AutoSaveConfig .. '"') or "เลือกคอนฟิกเพื่อเปิดใช้งาน", + Default = self.AutoSaveEnabled, + Callback = function(value) + local selectedConfig = SaveManager.Options.SaveManager_ConfigList.Value + + if value then + if not selectedConfig then + SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(false) + return self.Library:Notify({ + Title = "Config Loader", + Content = "เกิดข้อผิดพลาด", + SubContent = "กรุณาเลือกคอนฟิกก่อน", + Duration = 3 + }) + end - if name:gsub(" ", "") == "" then - return self.Library:Notify({ + self:EnableAutoSave(selectedConfig) + AutoSaveToggle.Description = 'กำลังบันทึกอัตโนมัติไปที่: "' .. selectedConfig .. '"' + + self.Library:Notify({ Title = "Config Loader", - Content = "Invalid Name", - SubContent = "Config name cannot be empty", + Content = "เปิดบันทึกอัตโนมัติแล้ว", + SubContent = string.format('การตั้งค่าจะบันทึกไปที่ "%s" อัตโนมัติ', selectedConfig), Duration = 3 }) - end - - local success, err = self:Save(name) - if not success then - return self.Library:Notify({ + else + self:DisableAutoSave() + AutoSaveToggle.Description = "เลือกคอนฟิกเพื่อเปิดใช้งาน" + + self.Library:Notify({ Title = "Config Loader", - Content = "Save Failed", - SubContent = err or "Unknown error", - Duration = 5 - }) + Content = "ปิดบันทึกอัตโนมัติแล้ว", + SubContent = "จะไม่บันทึกอัตโนมัติอีกต่อไป", + Duration = 3 + }) 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 }) + -- เมื่อเลือก config ใน dropdown + ConfigListDropdown.Changed = function(value) + if value then + -- อัพเดท Auto Save + if self.AutoSaveEnabled then + self.AutoSaveConfig = value + AutoSaveToggle.Description = 'กำลังบันทึกอัตโนมัติไปที่: "' .. value .. '"' + self:SaveUI() + end + end + end + section:AddButton({ - Title = "📂 Load Config", - Description = "Load selected configuration", + Title = "💾 Save New Config", + Description = "สร้างไฟล์คอนฟิกใหม่", Callback = function() - local name = SaveManager.Options.SaveManager_ConfigList.Value + local name = SaveManager.Options.SaveManager_ConfigName.Value - if not name then + if name:gsub(" ", "") == "" then return self.Library:Notify({ Title = "Config Loader", - Content = "No Config Selected", - SubContent = "Please select a config to load", + Content = "ชื่อไม่ถูกต้อง", + SubContent = "ชื่อคอนฟิกต้องไม่เป็นค่าว่าง", Duration = 3 }) end - local success, err = self:Load(name) + local success, err = self:Save(name) if not success then return self.Library:Notify({ Title = "Config Loader", - Content = "Load Failed", - SubContent = err or "Unknown error", + Content = "บันทึกล้มเหลว", + SubContent = err or "เกิดข้อผิดพลาดที่ไม่ทราบสาเหตุ", Duration = 5 }) end self.Library:Notify({ Title = "Config Loader", - Content = "Config Loaded", - SubContent = string.format('Loaded "%s"', name), + Content = "บันทึกคอนฟิกแล้ว", + SubContent = string.format('สร้าง "%s" เรียบร้อย', name), Duration = 3 }) + + ConfigListDropdown:SetValues(self:RefreshConfigList()) + ConfigListDropdown:SetValue(name) end }) section:AddButton({ - Title = "✏️ Overwrite Config", - Description = "Save current settings to selected config", + Title = "📂 Load Config", + Description = "โหลดคอนฟิกที่เลือก", 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", + Content = "ไม่ได้เลือกคอนฟิก", + SubContent = "กรุณาเลือกคอนฟิกที่ต้องการโหลด", Duration = 3 }) end - local success, err = self:Save(name) + local success, err = self:Load(name) if not success then return self.Library:Notify({ Title = "Config Loader", - Content = "Save Failed", - SubContent = err or "Unknown error", + Content = "โหลดล้มเหลว", + SubContent = err or "เกิดข้อผิดพลาดที่ไม่ทราบสาเหตุ", Duration = 5 }) end self.Library:Notify({ Title = "Config Loader", - Content = "Config Updated", - SubContent = string.format('Overwrote "%s"', name), + Content = "โหลดคอนฟิกแล้ว", + SubContent = string.format('โหลด "%s" เรียบร้อย', name), Duration = 3 }) end @@ -491,41 +603,41 @@ local SaveManager = {} do section:AddButton({ Title = "🗑️ Delete Config", - Description = "Permanently delete selected config", + Description = "ลบคอนฟิกที่เลือกถาวร", 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", + Content = "ไม่ได้เลือกคอนฟิก", + SubContent = "กรุณาเลือกคอนฟิกที่ต้องการลบ", Duration = 3 }) end -- Confirmation dialog self.Library:Dialog({ - Title = "Delete Config", - Content = string.format('Are you sure you want to delete "%s"?', name), + Title = "ลบคอนฟิก", + Content = string.format('คุณแน่ใจหรือไม่ว่าต้องการลบ "%s"?', name), Buttons = { { - Title = "Delete", + Title = "ลบ", 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", + Content = "ลบล้มเหลว", + SubContent = err or "เกิดข้อผิดพลาดที่ไม่ทราบสาเหตุ", Duration = 5 }) end self.Library:Notify({ Title = "Config Loader", - Content = "Config Deleted", - SubContent = string.format('Deleted "%s"', name), + Content = "ลบคอนฟิกแล้ว", + SubContent = string.format('ลบ "%s" เรียบร้อย', name), Duration = 3 }) @@ -537,15 +649,23 @@ local SaveManager = {} do local currentAutoload = self:GetAutoloadConfig() if currentAutoload then SaveManager.Options.SaveManager_AutoloadToggle:SetValue(true) - AutoloadToggle.Description = 'Current: "' .. currentAutoload .. '"' + AutoloadToggle.Description = 'ปัจจุบัน: "' .. currentAutoload .. '"' else SaveManager.Options.SaveManager_AutoloadToggle:SetValue(false) - AutoloadToggle.Description = "No autoload config set" + AutoloadToggle.Description = "ไม่มีคอนฟิกโหลดอัตโนมัติ" + end + + -- Update autosave if deleted config was autosave + if self.AutoSaveConfig == name then + self:DisableAutoSave() + SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(false) + AutoSaveToggle.Description = "เลือกคอนฟิกเพื่อเปิดใช้งาน" end end }, { - Title = "Cancel" + Title = "ยกเลิก", + Callback = function() end } } }) @@ -554,7 +674,7 @@ local SaveManager = {} do section:AddButton({ Title = "🔄 Refresh List", - Description = "Update available configs list", + Description = "อัพเดทรายการคอนฟิกที่มี", Callback = function() local configs = self:RefreshConfigList() ConfigListDropdown:SetValues(configs) @@ -562,18 +682,26 @@ local SaveManager = {} do self.Library:Notify({ Title = "Config Loader", - Content = "List Refreshed", - SubContent = string.format("Found %d config(s)", #configs), + Content = "รีเฟรชรายการแล้ว", + SubContent = string.format("พบ %d คอนฟิก", #configs), Duration = 2 }) end }) - -- อย่าลืม Ignore UI controls ของ SaveManager เอง + -- Ignore UI controls ของ SaveManager SaveManager:SetIgnoreIndexes({ "SaveManager_ConfigName", - "SaveManager_AutoloadToggle" + "SaveManager_ConfigList", + "SaveManager_AutoloadToggle", + "SaveManager_AutoSaveToggle" }) + + -- โหลด UI settings และเปิดใช้ auto save ถ้าเคยเปิดไว้ + if uiSettings and uiSettings.autosave_enabled and uiSettings.autosave_config then + self:EnableAutoSave(uiSettings.autosave_config) + SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(true) + end end SaveManager:BuildFolderTree() From aa8e663722688c15f77818d1d4dcb94150396fc1 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Mon, 10 Nov 2025 14:43:45 +0700 Subject: [PATCH 51/76] Update autosave.lua --- autosave.lua | 86 ++++++++++++++++++++++++++++------------------------ 1 file changed, 46 insertions(+), 40 deletions(-) diff --git a/autosave.lua b/autosave.lua index efdb49e..769613c 100644 --- a/autosave.lua +++ b/autosave.lua @@ -7,6 +7,8 @@ local SaveManager = {} do SaveManager.Options = {} SaveManager.AutoSaveEnabled = false SaveManager.AutoSaveConfig = nil + SaveManager.AutoSaveDebounce = false + SaveManager.OriginalCallbacks = {} SaveManager.Parser = { Toggle = { Save = function(idx, object) @@ -342,30 +344,36 @@ local SaveManager = {} do end end - -- ฟังก์ชัน Auto Save + -- ฟังก์ชัน Auto Save (แก้ไข Stack Overflow) function SaveManager:EnableAutoSave(configName) self.AutoSaveEnabled = true self.AutoSaveConfig = configName self:SaveUI() - -- ตั้งค่า listener สำหรับทุก option + -- บันทึก callback เดิมและตั้ง callback ใหม่ for idx, option in next, self.Options do if not self.Ignore[idx] and self.Parser[option.Type] then - -- เก็บ callback เดิมไว้ - local originalCallback = option.Callback + -- เก็บ callback เดิมไว้ถ้ายังไม่เคยเก็บ + if not self.OriginalCallbacks[idx] then + self.OriginalCallbacks[idx] = option.Callback + end - -- สร้าง callback ใหม่ที่รวม auto save + -- สร้าง callback ใหม่ option.Callback = function(...) -- เรียก callback เดิม - if originalCallback then - originalCallback(...) + if self.OriginalCallbacks[idx] then + self.OriginalCallbacks[idx](...) end - -- Auto save - if self.AutoSaveEnabled and self.AutoSaveConfig then + -- Auto save ด้วย debounce + if self.AutoSaveEnabled and self.AutoSaveConfig and not self.AutoSaveDebounce then + self.AutoSaveDebounce = true task.spawn(function() - task.wait(0.5) -- รอเล็กน้อยเพื่อไม่ให้เซฟบ่อยเกินไป - self:Save(self.AutoSaveConfig) + task.wait(1) -- รอ 1 วินาทีก่อนเซฟ + if self.AutoSaveEnabled and self.AutoSaveConfig then + self:Save(self.AutoSaveConfig) + end + self.AutoSaveDebounce = false end) end end @@ -377,6 +385,13 @@ local SaveManager = {} do self.AutoSaveEnabled = false self.AutoSaveConfig = nil self:SaveUI() + + -- คืนค่า callback เดิม + 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) @@ -423,11 +438,13 @@ local SaveManager = {} do self.AutoSaveEnabled = uiSettings.autosave_enabled or false end - -- Autoload Status Display + -- Autoload Toggle local currentAutoload = self:GetAutoloadConfig() + local autoloadDesc = currentAutoload and ('ปัจจุบัน: "' .. currentAutoload .. '"') or "ไม่มีคอนฟิกโหลดอัตโนมัติ" + local AutoloadToggle = section:AddToggle("SaveManager_AutoloadToggle", { Title = "🔄 Auto Load", - Description = currentAutoload and ('ปัจจุบัน: "' .. currentAutoload .. '"') or "ไม่มีคอนฟิกโหลดอัตโนมัติ", + Description = autoloadDesc, Default = currentAutoload ~= nil, Callback = function(value) local selectedConfig = SaveManager.Options.SaveManager_ConfigList.Value @@ -445,7 +462,7 @@ local SaveManager = {} do local success, err = self:SetAutoloadConfig(selectedConfig) if success then - AutoloadToggle.Description = 'ปัจจุบัน: "' .. selectedConfig .. '"' + self:SaveUI() self.Library:Notify({ Title = "Config Loader", Content = "เปิดโหลดอัตโนมัติแล้ว", @@ -464,7 +481,6 @@ local SaveManager = {} do else local success, err = self:DisableAutoload() if success then - AutoloadToggle.Description = "ไม่มีคอนฟิกโหลดอัตโนมัติ" self.Library:Notify({ Title = "Config Loader", Content = "ปิดโหลดอัตโนมัติแล้ว", @@ -476,10 +492,12 @@ local SaveManager = {} do end }) - -- Auto Save Toggle (แทนที่ปุ่ม Overwrite) + -- Auto Save Toggle + local autosaveDesc = self.AutoSaveConfig and ('กำลังบันทึกอัตโนมัติไปที่: "' .. self.AutoSaveConfig .. '"') or "เลือกคอนฟิกเพื่อเปิดใช้งาน" + local AutoSaveToggle = section:AddToggle("SaveManager_AutoSaveToggle", { Title = "💾 Auto Save", - Description = self.AutoSaveConfig and ('กำลังบันทึกอัตโนมัติไปที่: "' .. self.AutoSaveConfig .. '"') or "เลือกคอนฟิกเพื่อเปิดใช้งาน", + Description = autosaveDesc, Default = self.AutoSaveEnabled, Callback = function(value) local selectedConfig = SaveManager.Options.SaveManager_ConfigList.Value @@ -496,7 +514,6 @@ local SaveManager = {} do end self:EnableAutoSave(selectedConfig) - AutoSaveToggle.Description = 'กำลังบันทึกอัตโนมัติไปที่: "' .. selectedConfig .. '"' self.Library:Notify({ Title = "Config Loader", @@ -506,27 +523,22 @@ local SaveManager = {} do }) else self:DisableAutoSave() - AutoSaveToggle.Description = "เลือกคอนฟิกเพื่อเปิดใช้งาน" self.Library:Notify({ Title = "Config Loader", Content = "ปิดบันทึกอัตโนมัติแล้ว", SubContent = "จะไม่บันทึกอัตโนมัติอีกต่อไป", Duration = 3 - }) + }) end end }) -- เมื่อเลือก config ใน dropdown ConfigListDropdown.Changed = function(value) - if value then - -- อัพเดท Auto Save - if self.AutoSaveEnabled then - self.AutoSaveConfig = value - AutoSaveToggle.Description = 'กำลังบันทึกอัตโนมัติไปที่: "' .. value .. '"' - self:SaveUI() - end + if value and self.AutoSaveEnabled then + self.AutoSaveConfig = value + self:SaveUI() end end @@ -641,31 +653,25 @@ local SaveManager = {} do Duration = 3 }) - -- Update UI + -- Update dropdown 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 = 'ปัจจุบัน: "' .. currentAutoload .. '"' - else - SaveManager.Options.SaveManager_AutoloadToggle:SetValue(false) - AutoloadToggle.Description = "ไม่มีคอนฟิกโหลดอัตโนมัติ" - end - -- Update autosave if deleted config was autosave if self.AutoSaveConfig == name then self:DisableAutoSave() SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(false) - AutoSaveToggle.Description = "เลือกคอนฟิกเพื่อเปิดใช้งาน" end + + -- บันทึก UI + self:SaveUI() end }, { Title = "ยกเลิก", - Callback = function() end + Callback = function() + -- ไม่ทำอะไร + end } } }) From 744ff0f4c1e3b9545bf664c3a0e1440012020468 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Mon, 10 Nov 2025 14:51:09 +0700 Subject: [PATCH 52/76] Update autosave.lua --- autosave.lua | 154 +++++---------------------------------------------- 1 file changed, 14 insertions(+), 140 deletions(-) diff --git a/autosave.lua b/autosave.lua index 769613c..808d21f 100644 --- a/autosave.lua +++ b/autosave.lua @@ -325,26 +325,11 @@ local SaveManager = {} do 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 = "ล้มเหลวในการโหลดคอนฟิกอัตโนมัติ", - SubContent = err, - Duration = 5 - }) - end - - self.Library:Notify({ - Title = "Config Loader", - Content = "โหลดอัตโนมัติสำเร็จ", - SubContent = string.format('โหลด "%s" แล้ว', name), - Duration = 3 - }) + self:Load(name) end end - -- ฟังก์ชัน Auto Save (แก้ไข Stack Overflow) + -- ฟังก์ชัน Auto Save function SaveManager:EnableAutoSave(configName) self.AutoSaveEnabled = true self.AutoSaveConfig = configName @@ -452,42 +437,12 @@ local SaveManager = {} do if value then if not selectedConfig then SaveManager.Options.SaveManager_AutoloadToggle:SetValue(false) - return self.Library:Notify({ - Title = "Config Loader", - Content = "เกิดข้อผิดพลาด", - SubContent = "กรุณาเลือกคอนฟิกก่อน", - Duration = 3 - }) + return end - local success, err = self:SetAutoloadConfig(selectedConfig) - if success then - self:SaveUI() - self.Library:Notify({ - Title = "Config Loader", - Content = "เปิดโหลดอัตโนมัติแล้ว", - SubContent = string.format('"%s" จะโหลดอัตโนมัติทุกครั้ง', selectedConfig), - Duration = 3 - }) - else - SaveManager.Options.SaveManager_AutoloadToggle:SetValue(false) - self.Library:Notify({ - Title = "Config Loader", - Content = "เกิดข้อผิดพลาด", - SubContent = err or "ไม่สามารถตั้งค่าโหลดอัตโนมัติได้", - Duration = 3 - }) - end + self:SetAutoloadConfig(selectedConfig) else - local success, err = self:DisableAutoload() - if success then - self.Library:Notify({ - Title = "Config Loader", - Content = "ปิดโหลดอัตโนมัติแล้ว", - SubContent = "คอนฟิกจะไม่โหลดอัตโนมัติอีกต่อไป", - Duration = 3 - }) - end + self:DisableAutoload() end end }) @@ -505,42 +460,23 @@ local SaveManager = {} do if value then if not selectedConfig then SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(false) - return self.Library:Notify({ - Title = "Config Loader", - Content = "เกิดข้อผิดพลาด", - SubContent = "กรุณาเลือกคอนฟิกก่อน", - Duration = 3 - }) + return end self:EnableAutoSave(selectedConfig) - - self.Library:Notify({ - Title = "Config Loader", - Content = "เปิดบันทึกอัตโนมัติแล้ว", - SubContent = string.format('การตั้งค่าจะบันทึกไปที่ "%s" อัตโนมัติ', selectedConfig), - Duration = 3 - }) else self:DisableAutoSave() - - self.Library:Notify({ - Title = "Config Loader", - Content = "ปิดบันทึกอัตโนมัติแล้ว", - SubContent = "จะไม่บันทึกอัตโนมัติอีกต่อไป", - Duration = 3 - }) end end }) -- เมื่อเลือก config ใน dropdown - ConfigListDropdown.Changed = function(value) + ConfigListDropdown:OnChanged(function(value) if value and self.AutoSaveEnabled then self.AutoSaveConfig = value self:SaveUI() end - end + end) section:AddButton({ Title = "💾 Save New Config", @@ -549,31 +485,14 @@ local SaveManager = {} do local name = SaveManager.Options.SaveManager_ConfigName.Value if name:gsub(" ", "") == "" then - return self.Library:Notify({ - Title = "Config Loader", - Content = "ชื่อไม่ถูกต้อง", - SubContent = "ชื่อคอนฟิกต้องไม่เป็นค่าว่าง", - Duration = 3 - }) + return end local success, err = self:Save(name) if not success then - return self.Library:Notify({ - Title = "Config Loader", - Content = "บันทึกล้มเหลว", - SubContent = err or "เกิดข้อผิดพลาดที่ไม่ทราบสาเหตุ", - Duration = 5 - }) + return end - self.Library:Notify({ - Title = "Config Loader", - Content = "บันทึกคอนฟิกแล้ว", - SubContent = string.format('สร้าง "%s" เรียบร้อย', name), - Duration = 3 - }) - ConfigListDropdown:SetValues(self:RefreshConfigList()) ConfigListDropdown:SetValue(name) end @@ -586,30 +505,10 @@ local SaveManager = {} do local name = SaveManager.Options.SaveManager_ConfigList.Value if not name then - return self.Library:Notify({ - Title = "Config Loader", - Content = "ไม่ได้เลือกคอนฟิก", - SubContent = "กรุณาเลือกคอนฟิกที่ต้องการโหลด", - Duration = 3 - }) - end - - local success, err = self:Load(name) - if not success then - return self.Library:Notify({ - Title = "Config Loader", - Content = "โหลดล้มเหลว", - SubContent = err or "เกิดข้อผิดพลาดที่ไม่ทราบสาเหตุ", - Duration = 5 - }) + return end - self.Library:Notify({ - Title = "Config Loader", - Content = "โหลดคอนฟิกแล้ว", - SubContent = string.format('โหลด "%s" เรียบร้อย', name), - Duration = 3 - }) + self:Load(name) end }) @@ -620,12 +519,7 @@ local SaveManager = {} do local name = SaveManager.Options.SaveManager_ConfigList.Value if not name then - return self.Library:Notify({ - Title = "Config Loader", - Content = "ไม่ได้เลือกคอนฟิก", - SubContent = "กรุณาเลือกคอนฟิกที่ต้องการลบ", - Duration = 3 - }) + return end -- Confirmation dialog @@ -638,21 +532,9 @@ local SaveManager = {} do Callback = function() local success, err = self:Delete(name) if not success then - return self.Library:Notify({ - Title = "Config Loader", - Content = "ลบล้มเหลว", - SubContent = err or "เกิดข้อผิดพลาดที่ไม่ทราบสาเหตุ", - Duration = 5 - }) + return end - self.Library:Notify({ - Title = "Config Loader", - Content = "ลบคอนฟิกแล้ว", - SubContent = string.format('ลบ "%s" เรียบร้อย', name), - Duration = 3 - }) - -- Update dropdown ConfigListDropdown:SetValues(self:RefreshConfigList()) ConfigListDropdown:SetValue(nil) @@ -670,7 +552,6 @@ local SaveManager = {} do { Title = "ยกเลิก", Callback = function() - -- ไม่ทำอะไร end } } @@ -685,13 +566,6 @@ local SaveManager = {} do local configs = self:RefreshConfigList() ConfigListDropdown:SetValues(configs) ConfigListDropdown:SetValue(nil) - - self.Library:Notify({ - Title = "Config Loader", - Content = "รีเฟรชรายการแล้ว", - SubContent = string.format("พบ %d คอนฟิก", #configs), - Duration = 2 - }) end }) From e8c0a5757073971f91b5786d709f8d6c97ccd676 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Mon, 10 Nov 2025 14:56:14 +0700 Subject: [PATCH 53/76] Update autosave.lua --- autosave.lua | 93 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 23 deletions(-) diff --git a/autosave.lua b/autosave.lua index 808d21f..73b88cb 100644 --- a/autosave.lua +++ b/autosave.lua @@ -404,33 +404,39 @@ local SaveManager = {} do }) -- สร้าง AutoSave.json ถ้าไม่มีไฟล์เลย + local shouldEnableAutoLoad = false + local shouldEnableAutoSave = false + if #configs == 0 then local success = self:Save("AutoSave") if success then configs = self:RefreshConfigList() ConfigListDropdown:SetValues(configs) ConfigListDropdown:SetValue("AutoSave") + self.AutoSaveConfig = "AutoSave" - if uiSettings then - self.AutoSaveConfig = "AutoSave" - self.AutoSaveEnabled = uiSettings.autosave_enabled or false - end + -- เปิด Auto Load และ Auto Save เริ่มต้น + self:SetAutoloadConfig("AutoSave") + shouldEnableAutoLoad = true + shouldEnableAutoSave = true + end + elseif uiSettings then + -- โหลดค่าจาก UI settings + if uiSettings.autosave_config then + ConfigListDropdown:SetValue(uiSettings.autosave_config) + self.AutoSaveConfig = uiSettings.autosave_config end - elseif uiSettings and uiSettings.autosave_config then - -- โหลดค่า autosave config จาก UI settings - ConfigListDropdown:SetValue(uiSettings.autosave_config) - self.AutoSaveConfig = uiSettings.autosave_config - self.AutoSaveEnabled = uiSettings.autosave_enabled or false + shouldEnableAutoSave = uiSettings.autosave_enabled or false + shouldEnableAutoLoad = self:GetAutoloadConfig() ~= nil end -- Autoload Toggle local currentAutoload = self:GetAutoloadConfig() - local autoloadDesc = currentAutoload and ('ปัจจุบัน: "' .. currentAutoload .. '"') or "ไม่มีคอนฟิกโหลดอัตโนมัติ" local AutoloadToggle = section:AddToggle("SaveManager_AutoloadToggle", { Title = "🔄 Auto Load", - Description = autoloadDesc, - Default = currentAutoload ~= nil, + Description = currentAutoload and ('ปัจจุบัน: "' .. currentAutoload .. '"') or "ไม่มีคอนฟิกโหลดอัตโนมัติ", + Default = shouldEnableAutoLoad, Callback = function(value) local selectedConfig = SaveManager.Options.SaveManager_ConfigList.Value @@ -441,19 +447,27 @@ local SaveManager = {} do end self:SetAutoloadConfig(selectedConfig) + -- อัพเดท Description แบบ Dynamic + local toggleObj = SaveManager.Options.SaveManager_AutoloadToggle + if toggleObj and toggleObj.SetDescription then + toggleObj:SetDescription('ปัจจุบัน: "' .. selectedConfig .. '"') + end else self:DisableAutoload() + -- อัพเดท Description แบบ Dynamic + local toggleObj = SaveManager.Options.SaveManager_AutoloadToggle + if toggleObj and toggleObj.SetDescription then + toggleObj:SetDescription("ไม่มีคอนฟิกโหลดอัตโนมัติ") + end end end }) -- Auto Save Toggle - local autosaveDesc = self.AutoSaveConfig and ('กำลังบันทึกอัตโนมัติไปที่: "' .. self.AutoSaveConfig .. '"') or "เลือกคอนฟิกเพื่อเปิดใช้งาน" - local AutoSaveToggle = section:AddToggle("SaveManager_AutoSaveToggle", { Title = "💾 Auto Save", - Description = autosaveDesc, - Default = self.AutoSaveEnabled, + Description = self.AutoSaveConfig and ('กำลังบันทึกอัตโนมัติไปที่: "' .. self.AutoSaveConfig .. '"') or "เลือกคอนฟิกเพื่อเปิดใช้งาน", + Default = shouldEnableAutoSave, Callback = function(value) local selectedConfig = SaveManager.Options.SaveManager_ConfigList.Value @@ -464,17 +478,35 @@ local SaveManager = {} do end self:EnableAutoSave(selectedConfig) + -- อัพเดท Description แบบ Dynamic + local toggleObj = SaveManager.Options.SaveManager_AutoSaveToggle + if toggleObj and toggleObj.SetDescription then + toggleObj:SetDescription('กำลังบันทึกอัตโนมัติไปที่: "' .. selectedConfig .. '"') + end else self:DisableAutoSave() + -- อัพเดท Description แบบ Dynamic + local toggleObj = SaveManager.Options.SaveManager_AutoSaveToggle + if toggleObj and toggleObj.SetDescription then + toggleObj:SetDescription("เลือกคอนฟิกเพื่อเปิดใช้งาน") + end end end }) -- เมื่อเลือก config ใน dropdown ConfigListDropdown:OnChanged(function(value) - if value and self.AutoSaveEnabled then - self.AutoSaveConfig = value - self:SaveUI() + if value then + -- อัพเดท Auto Save config + if self.AutoSaveEnabled then + self.AutoSaveConfig = value + self:SaveUI() + -- อัพเดท Description + local toggleObj = SaveManager.Options.SaveManager_AutoSaveToggle + if toggleObj and toggleObj.SetDescription then + toggleObj:SetDescription('กำลังบันทึกอัตโนมัติไปที่: "' .. value .. '"') + end + end end end) @@ -543,6 +575,22 @@ local SaveManager = {} do if self.AutoSaveConfig == name then self:DisableAutoSave() SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(false) + -- อัพเดท Description + local toggleObj = SaveManager.Options.SaveManager_AutoSaveToggle + if toggleObj and toggleObj.SetDescription then + toggleObj:SetDescription("เลือกคอนฟิกเพื่อเปิดใช้งาน") + end + end + + -- Update autoload if deleted config was autoload + if self:GetAutoloadConfig() == name then + self:DisableAutoload() + SaveManager.Options.SaveManager_AutoloadToggle:SetValue(false) + -- อัพเดท Description + local toggleObj = SaveManager.Options.SaveManager_AutoloadToggle + if toggleObj and toggleObj.SetDescription then + toggleObj:SetDescription("ไม่มีคอนฟิกโหลดอัตโนมัติ") + end end -- บันทึก UI @@ -577,10 +625,9 @@ local SaveManager = {} do "SaveManager_AutoSaveToggle" }) - -- โหลด UI settings และเปิดใช้ auto save ถ้าเคยเปิดไว้ - if uiSettings and uiSettings.autosave_enabled and uiSettings.autosave_config then - self:EnableAutoSave(uiSettings.autosave_config) - SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(true) + -- เปิดใช้งาน auto save ถ้าต้องการ + if shouldEnableAutoSave and self.AutoSaveConfig then + self:EnableAutoSave(self.AutoSaveConfig) end end From 2b9863ee982220c7a92fdcc7d989c3285d006893 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:13:19 +0700 Subject: [PATCH 54/76] Update autosave.lua --- autosave.lua | 93 +++++++++++++--------------------------------------- 1 file changed, 23 insertions(+), 70 deletions(-) diff --git a/autosave.lua b/autosave.lua index 73b88cb..808d21f 100644 --- a/autosave.lua +++ b/autosave.lua @@ -404,39 +404,33 @@ local SaveManager = {} do }) -- สร้าง AutoSave.json ถ้าไม่มีไฟล์เลย - local shouldEnableAutoLoad = false - local shouldEnableAutoSave = false - if #configs == 0 then local success = self:Save("AutoSave") if success then configs = self:RefreshConfigList() ConfigListDropdown:SetValues(configs) ConfigListDropdown:SetValue("AutoSave") - self.AutoSaveConfig = "AutoSave" - -- เปิด Auto Load และ Auto Save เริ่มต้น - self:SetAutoloadConfig("AutoSave") - shouldEnableAutoLoad = true - shouldEnableAutoSave = true - end - elseif uiSettings then - -- โหลดค่าจาก UI settings - if uiSettings.autosave_config then - ConfigListDropdown:SetValue(uiSettings.autosave_config) - self.AutoSaveConfig = uiSettings.autosave_config + if uiSettings then + self.AutoSaveConfig = "AutoSave" + self.AutoSaveEnabled = uiSettings.autosave_enabled or false + end end - shouldEnableAutoSave = uiSettings.autosave_enabled or false - shouldEnableAutoLoad = self:GetAutoloadConfig() ~= nil + elseif uiSettings and uiSettings.autosave_config then + -- โหลดค่า autosave config จาก UI settings + ConfigListDropdown:SetValue(uiSettings.autosave_config) + self.AutoSaveConfig = uiSettings.autosave_config + self.AutoSaveEnabled = uiSettings.autosave_enabled or false end -- Autoload Toggle local currentAutoload = self:GetAutoloadConfig() + local autoloadDesc = currentAutoload and ('ปัจจุบัน: "' .. currentAutoload .. '"') or "ไม่มีคอนฟิกโหลดอัตโนมัติ" local AutoloadToggle = section:AddToggle("SaveManager_AutoloadToggle", { Title = "🔄 Auto Load", - Description = currentAutoload and ('ปัจจุบัน: "' .. currentAutoload .. '"') or "ไม่มีคอนฟิกโหลดอัตโนมัติ", - Default = shouldEnableAutoLoad, + Description = autoloadDesc, + Default = currentAutoload ~= nil, Callback = function(value) local selectedConfig = SaveManager.Options.SaveManager_ConfigList.Value @@ -447,27 +441,19 @@ local SaveManager = {} do end self:SetAutoloadConfig(selectedConfig) - -- อัพเดท Description แบบ Dynamic - local toggleObj = SaveManager.Options.SaveManager_AutoloadToggle - if toggleObj and toggleObj.SetDescription then - toggleObj:SetDescription('ปัจจุบัน: "' .. selectedConfig .. '"') - end else self:DisableAutoload() - -- อัพเดท Description แบบ Dynamic - local toggleObj = SaveManager.Options.SaveManager_AutoloadToggle - if toggleObj and toggleObj.SetDescription then - toggleObj:SetDescription("ไม่มีคอนฟิกโหลดอัตโนมัติ") - end end end }) -- Auto Save Toggle + local autosaveDesc = self.AutoSaveConfig and ('กำลังบันทึกอัตโนมัติไปที่: "' .. self.AutoSaveConfig .. '"') or "เลือกคอนฟิกเพื่อเปิดใช้งาน" + local AutoSaveToggle = section:AddToggle("SaveManager_AutoSaveToggle", { Title = "💾 Auto Save", - Description = self.AutoSaveConfig and ('กำลังบันทึกอัตโนมัติไปที่: "' .. self.AutoSaveConfig .. '"') or "เลือกคอนฟิกเพื่อเปิดใช้งาน", - Default = shouldEnableAutoSave, + Description = autosaveDesc, + Default = self.AutoSaveEnabled, Callback = function(value) local selectedConfig = SaveManager.Options.SaveManager_ConfigList.Value @@ -478,35 +464,17 @@ local SaveManager = {} do end self:EnableAutoSave(selectedConfig) - -- อัพเดท Description แบบ Dynamic - local toggleObj = SaveManager.Options.SaveManager_AutoSaveToggle - if toggleObj and toggleObj.SetDescription then - toggleObj:SetDescription('กำลังบันทึกอัตโนมัติไปที่: "' .. selectedConfig .. '"') - end else self:DisableAutoSave() - -- อัพเดท Description แบบ Dynamic - local toggleObj = SaveManager.Options.SaveManager_AutoSaveToggle - if toggleObj and toggleObj.SetDescription then - toggleObj:SetDescription("เลือกคอนฟิกเพื่อเปิดใช้งาน") - end end end }) -- เมื่อเลือก config ใน dropdown ConfigListDropdown:OnChanged(function(value) - if value then - -- อัพเดท Auto Save config - if self.AutoSaveEnabled then - self.AutoSaveConfig = value - self:SaveUI() - -- อัพเดท Description - local toggleObj = SaveManager.Options.SaveManager_AutoSaveToggle - if toggleObj and toggleObj.SetDescription then - toggleObj:SetDescription('กำลังบันทึกอัตโนมัติไปที่: "' .. value .. '"') - end - end + if value and self.AutoSaveEnabled then + self.AutoSaveConfig = value + self:SaveUI() end end) @@ -575,22 +543,6 @@ local SaveManager = {} do if self.AutoSaveConfig == name then self:DisableAutoSave() SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(false) - -- อัพเดท Description - local toggleObj = SaveManager.Options.SaveManager_AutoSaveToggle - if toggleObj and toggleObj.SetDescription then - toggleObj:SetDescription("เลือกคอนฟิกเพื่อเปิดใช้งาน") - end - end - - -- Update autoload if deleted config was autoload - if self:GetAutoloadConfig() == name then - self:DisableAutoload() - SaveManager.Options.SaveManager_AutoloadToggle:SetValue(false) - -- อัพเดท Description - local toggleObj = SaveManager.Options.SaveManager_AutoloadToggle - if toggleObj and toggleObj.SetDescription then - toggleObj:SetDescription("ไม่มีคอนฟิกโหลดอัตโนมัติ") - end end -- บันทึก UI @@ -625,9 +577,10 @@ local SaveManager = {} do "SaveManager_AutoSaveToggle" }) - -- เปิดใช้งาน auto save ถ้าต้องการ - if shouldEnableAutoSave and self.AutoSaveConfig then - self:EnableAutoSave(self.AutoSaveConfig) + -- โหลด UI settings และเปิดใช้ auto save ถ้าเคยเปิดไว้ + if uiSettings and uiSettings.autosave_enabled and uiSettings.autosave_config then + self:EnableAutoSave(uiSettings.autosave_config) + SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(true) end end From 27bb9ea034e9dc7c8c7caebd2b613c189a97dd9f Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:32:51 +0700 Subject: [PATCH 55/76] Update autosave.lua --- autosave.lua | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/autosave.lua b/autosave.lua index 808d21f..59c172d 100644 --- a/autosave.lua +++ b/autosave.lua @@ -459,7 +459,7 @@ local SaveManager = {} do if value then if not selectedConfig then - SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(false) + SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(true) return end @@ -577,10 +577,21 @@ local SaveManager = {} do "SaveManager_AutoSaveToggle" }) - -- โหลด UI settings และเปิดใช้ auto save ถ้าเคยเปิดไว้ - if uiSettings and uiSettings.autosave_enabled and uiSettings.autosave_config then - self:EnableAutoSave(uiSettings.autosave_config) - SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(true) + -- โหลด UI settings และเปิดใช้ auto save / auto load ถ้าเคยเปิดไว้ + if uiSettings then + -- Auto Load + if uiSettings.autoload_enabled and uiSettings.autoload_config then + task.spawn(function() + SaveManager:Load(uiSettings.autoload_config) + SaveManager.Options.SaveManager_AutoloadToggle:SetValue(true) + end) + end + + -- Auto Save + if uiSettings.autosave_enabled and uiSettings.autosave_config then + self:EnableAutoSave(uiSettings.autosave_config) + SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(true) + end end end From 4dfd499142a030aa1d95fde2614e973114d128b8 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Mon, 10 Nov 2025 21:23:47 +0700 Subject: [PATCH 56/76] Update autosave.lua --- autosave.lua | 207 ++++++++++++--------------------------------------- 1 file changed, 46 insertions(+), 161 deletions(-) diff --git a/autosave.lua b/autosave.lua index 59c172d..b841f18 100644 --- a/autosave.lua +++ b/autosave.lua @@ -196,13 +196,16 @@ local SaveManager = {} do function SaveManager:SaveUI() local uiPath = getSaveManagerUIPath(self) local uiData = { - autoload = self:GetAutoloadConfig(), + 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 @@ -379,6 +382,7 @@ local SaveManager = {} do end end + -- สร้างส่วน UI แบบย่อ: เอา DropDown/Inputs/Buttons ออก เหลือแค่ Auto Load กับ Auto Save function SaveManager:BuildConfigSection(tab) assert(self.Library, "Must set SaveManager.Library") @@ -387,192 +391,64 @@ local SaveManager = {} do -- โหลด UI settings local uiSettings = self:LoadUI() - -- Config Name Input - section:AddInput("SaveManager_ConfigName", { - Title = "💾 Config Name", - Placeholder = "ใส่ชื่อคอนฟิก...", - Description = "พิมพ์ชื่อสำหรับคอนฟิกใหม่" - }) - - -- Config List Dropdown - local configs = self:RefreshConfigList() - local ConfigListDropdown = section:AddDropdown("SaveManager_ConfigList", { - Title = "📋 Available Configs", - Values = configs, - AllowNull = true, - Description = "เลือกคอนฟิกที่ต้องการจัดการ" - }) - - -- สร้าง AutoSave.json ถ้าไม่มีไฟล์เลย - if #configs == 0 then - local success = self:Save("AutoSave") - if success then - configs = self:RefreshConfigList() - ConfigListDropdown:SetValues(configs) - ConfigListDropdown:SetValue("AutoSave") - - if uiSettings then - self.AutoSaveConfig = "AutoSave" - self.AutoSaveEnabled = uiSettings.autosave_enabled or false - end - end - elseif uiSettings and uiSettings.autosave_config then - -- โหลดค่า autosave config จาก UI settings - ConfigListDropdown:SetValue(uiSettings.autosave_config) - self.AutoSaveConfig = uiSettings.autosave_config - self.AutoSaveEnabled = uiSettings.autosave_enabled or false + -- ensure AutoSave config file exists (ใช้ชื่อคงที่ "AutoSave") + local fixedConfigName = "AutoSave" + if not isfile(getConfigFilePath(self, fixedConfigName)) then + pcall(function() self:Save(fixedConfigName) end) end - -- Autoload Toggle + -- Autoload Toggle (ใช้ไฟล์ AutoSave.json แบบคงที่) local currentAutoload = self:GetAutoloadConfig() - local autoloadDesc = currentAutoload and ('ปัจจุบัน: "' .. currentAutoload .. '"') or "ไม่มีคอนฟิกโหลดอัตโนมัติ" + local autoloadDesc = currentAutoload and ('ปัจจุบัน: "' .. currentAutoload .. '"') or 'จะโหลดไฟล์ "AutoSave.json" อัตโนมัติเมื่อเปิด' local AutoloadToggle = section:AddToggle("SaveManager_AutoloadToggle", { Title = "🔄 Auto Load", Description = autoloadDesc, - Default = currentAutoload ~= nil, + Default = (uiSettings and uiSettings.autoload_enabled) or false, Callback = function(value) - local selectedConfig = SaveManager.Options.SaveManager_ConfigList.Value - if value then - if not selectedConfig then - SaveManager.Options.SaveManager_AutoloadToggle:SetValue(false) - return + -- ถ้าไฟล์ยังไม่มี ให้สร้าง + if not isfile(getConfigFilePath(self, fixedConfigName)) then + self:Save(fixedConfigName) end - self:SetAutoloadConfig(selectedConfig) + -- ตั้ง autoload เป็น AutoSave + local ok, err = self:SetAutoloadConfig(fixedConfigName) + if not ok then + -- ถ้าตั้งค่าไม่สำเร็จ ให้รีเซ็ต toggle กลับเป็น false + if SaveManager.Options and SaveManager.Options.SaveManager_AutoloadToggle then + SaveManager.Options.SaveManager_AutoloadToggle:SetValue(false) + end + end else self:DisableAutoload() end end }) - -- Auto Save Toggle - local autosaveDesc = self.AutoSaveConfig and ('กำลังบันทึกอัตโนมัติไปที่: "' .. self.AutoSaveConfig .. '"') or "เลือกคอนฟิกเพื่อเปิดใช้งาน" + -- Auto Save Toggle (บันทึกไปที่ AutoSave.json แบบคงที่) + local autosaveDesc = self.AutoSaveConfig and ('กำลังบันทึกอัตโนมัติไปที่: "' .. tostring(self.AutoSaveConfig) .. '"') or 'บันทึกอัตโนมัติไปที่ "AutoSave.json"' local AutoSaveToggle = section:AddToggle("SaveManager_AutoSaveToggle", { Title = "💾 Auto Save", Description = autosaveDesc, - Default = self.AutoSaveEnabled, + Default = (uiSettings and uiSettings.autosave_enabled) or false, Callback = function(value) - local selectedConfig = SaveManager.Options.SaveManager_ConfigList.Value - if value then - if not selectedConfig then - SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(true) - return + -- สร้างไฟล์ถ้ายังไม่มี + if not isfile(getConfigFilePath(self, fixedConfigName)) then + self:Save(fixedConfigName) end - self:EnableAutoSave(selectedConfig) + self:EnableAutoSave(fixedConfigName) else self:DisableAutoSave() end end }) - -- เมื่อเลือก config ใน dropdown - ConfigListDropdown:OnChanged(function(value) - if value and self.AutoSaveEnabled then - self.AutoSaveConfig = value - self:SaveUI() - end - end) - - section:AddButton({ - Title = "💾 Save New Config", - Description = "สร้างไฟล์คอนฟิกใหม่", - Callback = function() - local name = SaveManager.Options.SaveManager_ConfigName.Value - - if name:gsub(" ", "") == "" then - return - end - - local success, err = self:Save(name) - if not success then - return - end - - ConfigListDropdown:SetValues(self:RefreshConfigList()) - ConfigListDropdown:SetValue(name) - end - }) - - section:AddButton({ - Title = "📂 Load Config", - Description = "โหลดคอนฟิกที่เลือก", - Callback = function() - local name = SaveManager.Options.SaveManager_ConfigList.Value - - if not name then - return - end - - self:Load(name) - end - }) - - section:AddButton({ - Title = "🗑️ Delete Config", - Description = "ลบคอนฟิกที่เลือกถาวร", - Callback = function() - local name = SaveManager.Options.SaveManager_ConfigList.Value - - if not name then - return - end - - -- Confirmation dialog - self.Library:Dialog({ - Title = "ลบคอนฟิก", - Content = string.format('คุณแน่ใจหรือไม่ว่าต้องการลบ "%s"?', name), - Buttons = { - { - Title = "ลบ", - Callback = function() - local success, err = self:Delete(name) - if not success then - return - end - - -- Update dropdown - ConfigListDropdown:SetValues(self:RefreshConfigList()) - ConfigListDropdown:SetValue(nil) - - -- Update autosave if deleted config was autosave - if self.AutoSaveConfig == name then - self:DisableAutoSave() - SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(false) - end - - -- บันทึก UI - self:SaveUI() - end - }, - { - Title = "ยกเลิก", - Callback = function() - end - } - } - }) - end - }) - - section:AddButton({ - Title = "🔄 Refresh List", - Description = "อัพเดทรายการคอนฟิกที่มี", - Callback = function() - local configs = self:RefreshConfigList() - ConfigListDropdown:SetValues(configs) - ConfigListDropdown:SetValue(nil) - end - }) - - -- Ignore UI controls ของ SaveManager + -- ตั้งให้ SaveManager ไม่เซฟค่าของ Toggle ตัวจัดการนี้เอง SaveManager:SetIgnoreIndexes({ - "SaveManager_ConfigName", - "SaveManager_ConfigList", "SaveManager_AutoloadToggle", "SaveManager_AutoSaveToggle" }) @@ -580,17 +456,26 @@ local SaveManager = {} do -- โหลด UI settings และเปิดใช้ auto save / auto load ถ้าเคยเปิดไว้ if uiSettings then -- Auto Load - if uiSettings.autoload_enabled and uiSettings.autoload_config then + if uiSettings.autoload_enabled then task.spawn(function() - SaveManager:Load(uiSettings.autoload_config) - SaveManager.Options.SaveManager_AutoloadToggle:SetValue(true) + -- พยายามโหลด AutoSave + if isfile(getConfigFilePath(self, fixedConfigName)) then + SaveManager:Load(fixedConfigName) + if SaveManager.Options and SaveManager.Options.SaveManager_AutoloadToggle then + SaveManager.Options.SaveManager_AutoloadToggle:SetValue(true) + end + end end) end -- Auto Save - if uiSettings.autosave_enabled and uiSettings.autosave_config then - self:EnableAutoSave(uiSettings.autosave_config) - SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(true) + if uiSettings.autosave_enabled then + if isfile(getConfigFilePath(self, fixedConfigName)) then + self:EnableAutoSave(fixedConfigName) + if SaveManager.Options and SaveManager.Options.SaveManager_AutoSaveToggle then + SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(true) + end + end end end end From 46340d735d25942ca149ad040198d02a42d4bedc Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Tue, 18 Nov 2025 11:00:22 +0700 Subject: [PATCH 57/76] Update autosave.lua --- autosave.lua | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/autosave.lua b/autosave.lua index b841f18..cd272b3 100644 --- a/autosave.lua +++ b/autosave.lua @@ -32,10 +32,10 @@ local SaveManager = {} do }, Dropdown = { Save = function(idx, object) - return { type = "Dropdown", idx = idx, value = object.Value, mutli = object.Multi } + return { type = "Dropdown", idx = idx, value = object.Value, multi = object.Multi } end, Load = function(idx, data) - if SaveManager.Options[idx] then + if SaveManager.Options[idx] then SaveManager.Options[idx]:SetValue(data.value) end end, @@ -337,7 +337,7 @@ local SaveManager = {} do self.AutoSaveEnabled = true self.AutoSaveConfig = configName self:SaveUI() - + -- บันทึก callback เดิมและตั้ง callback ใหม่ for idx, option in next, self.Options do if not self.Ignore[idx] and self.Parser[option.Type] then @@ -345,14 +345,27 @@ local SaveManager = {} do if not self.OriginalCallbacks[idx] then self.OriginalCallbacks[idx] = option.Callback end - - -- สร้าง callback ใหม่ + + -- สร้าง callback ใหม่ที่ป้องกัน stack overflow + local originalCallback = self.OriginalCallbacks[idx] option.Callback = function(...) + -- ป้องกัน recursion ด้วยการใช้ flag + if option._isInCallback then + return + end + + option._isInCallback = true + -- เรียก callback เดิม - if self.OriginalCallbacks[idx] then - self.OriginalCallbacks[idx](...) + if originalCallback then + local success, err = pcall(originalCallback, ...) + if not success then + warn("Callback error for " .. tostring(idx) .. ": " .. tostring(err)) + end end - + + option._isInCallback = false + -- Auto save ด้วย debounce if self.AutoSaveEnabled and self.AutoSaveConfig and not self.AutoSaveDebounce then self.AutoSaveDebounce = true From f8d0b54b1b4b51fd9983ed8bb3d11922a1a19162 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Fri, 21 Nov 2025 18:35:03 +0700 Subject: [PATCH 58/76] Create InterfaceManager.lua --- InterfaceManager.lua | 190 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 InterfaceManager.lua diff --git a/InterfaceManager.lua b/InterfaceManager.lua new file mode 100644 index 0000000..046dc95 --- /dev/null +++ b/InterfaceManager.lua @@ -0,0 +1,190 @@ +local httpService = game:GetService("HttpService") + +-- ═══════════════════════════════════════════════════════════════ +-- Load Language System +-- ═══════════════════════════════════════════════════════════════ +local Lang = loadstring(game:HttpGet("https://raw.githubusercontent.com/ATGFAIL/ATGHub/main/Languages/AllInOne.lua"))() + +local InterfaceManager = {} do + InterfaceManager.Folder = "FluentSettings" + InterfaceManager.Settings = { + Theme = "Dark", + Acrylic = true, + Transparency = true, + MenuKeybind = "LeftControl", + Language = "EN" -- Added Language setting + } + + 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 + + -- Apply loaded language + if InterfaceManager.Settings.Language then + Lang:SetLanguage(InterfaceManager.Settings.Language) + 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(Lang:Get("tab_settings")) + + -- ═══════════════════════════════════════════════════════════ + -- Language Selector + -- ═══════════════════════════════════════════════════════════ + local languageNames = {} + local languageCodes = {} + local availableLanguages = Lang:GetAvailableLanguagesWithNames() + + for _, langData in ipairs(availableLanguages) do + table.insert(languageNames, langData.name) + languageCodes[langData.name] = langData.code + end + + local currentLangName = Lang:GetCurrentLanguageName() + for _, langData in ipairs(availableLanguages) do + if langData.code == Settings.Language then + currentLangName = langData.name + break + end + end + + local LanguageDropdown = section:AddDropdown("InterfaceLanguage", { + Title = Lang:Get("language"), + Description = Lang:Get("language_changed_desc"), + Values = languageNames, + Default = currentLangName, + Callback = function(Value) + local langCode = languageCodes[Value] + if langCode then + Lang:SetLanguage(langCode) + Settings.Language = langCode + InterfaceManager:SaveSettings() + + -- Notify user + Library:Notify({ + Title = Lang:Get("language_changed"), + Content = Lang:Get("language_changed_desc"), + Duration = 3 + }) + + -- Reload UI after short delay + task.wait(1) + Library:Notify({ + Title = Lang:Get("notification_info"), + Content = "UI will reload to apply language...", + Duration = 2 + }) + end + end + }) + + -- ═══════════════════════════════════════════════════════════ + -- Theme Selector + -- ═══════════════════════════════════════════════════════════ + local InterfaceTheme = section:AddDropdown("InterfaceTheme", { + Title = Lang:Get("tab_settings"), + 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) + + -- ═══════════════════════════════════════════════════════════ + -- Acrylic Toggle + -- ═══════════════════════════════════════════════════════════ + 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 + + -- ═══════════════════════════════════════════════════════════ + -- Transparency Toggle + -- ═══════════════════════════════════════════════════════════ + 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 + }) + + -- ═══════════════════════════════════════════════════════════ + -- Menu Keybind + -- ═══════════════════════════════════════════════════════════ + 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 From 0968ccfd58cc3bff570247ed7c93ef015796aaa3 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Fri, 21 Nov 2025 18:36:02 +0700 Subject: [PATCH 59/76] Update InterfaceManager.lua --- InterfaceManager.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InterfaceManager.lua b/InterfaceManager.lua index 046dc95..3814a47 100644 --- a/InterfaceManager.lua +++ b/InterfaceManager.lua @@ -3,7 +3,7 @@ local httpService = game:GetService("HttpService") -- ═══════════════════════════════════════════════════════════════ -- Load Language System -- ═══════════════════════════════════════════════════════════════ -local Lang = loadstring(game:HttpGet("https://raw.githubusercontent.com/ATGFAIL/ATGHub/main/Languages/AllInOne.lua"))() +local Lang = loadstring(game:HttpGet("https://raw.githubusercontent.com/ATGFAIL/ATGHub/main/Languages/LanguageSystem.lua"))() local InterfaceManager = {} do InterfaceManager.Folder = "FluentSettings" From b5cdb42248008a58f8373e6405cad3375dd3295e Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Tue, 2 Dec 2025 00:30:55 +0700 Subject: [PATCH 60/76] Update autosave.lua --- autosave.lua | 1201 +++++++++++++++++++++++++++++--------------------- 1 file changed, 705 insertions(+), 496 deletions(-) diff --git a/autosave.lua b/autosave.lua index cd272b3..67c76ac 100644 --- a/autosave.lua +++ b/autosave.lua @@ -1,499 +1,708 @@ +---@diagnostic disable: undefined-global +local Players = game:GetService("Players") +local CoreGui = game:GetService("CoreGui") +local RunService = game:GetService("RunService") local httpService = game:GetService("HttpService") local Workspace = game:GetService("Workspace") -local SaveManager = {} do - SaveManager.FolderRoot = "ATGSettings" - 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 placeId = getPlaceId() - return root .. "/" .. placeId - end - - local function getConfigFilePath(self, name) - local folder = getConfigsFolder(self) - return folder .. "/" .. name .. ".json" - end - - -- ไฟล์สำหรับเซฟ UI ของ SaveManager เอง - local function getSaveManagerUIPath(self) - local folder = getConfigsFolder(self) - return folder .. "/savemanager_ui.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 - - -- เซฟ UI ของ SaveManager แยกต่างหาก - 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 - - -- โหลด UI ของ SaveManager - 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 - - 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) - 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 - self:Load(name) - end - end - - -- ฟังก์ชัน Auto Save - function SaveManager:EnableAutoSave(configName) - self.AutoSaveEnabled = true - self.AutoSaveConfig = configName - self:SaveUI() - - -- บันทึก callback เดิมและตั้ง callback ใหม่ - for idx, option in next, self.Options do - if not self.Ignore[idx] and self.Parser[option.Type] then - -- เก็บ callback เดิมไว้ถ้ายังไม่เคยเก็บ - if not self.OriginalCallbacks[idx] then - self.OriginalCallbacks[idx] = option.Callback - end - - -- สร้าง callback ใหม่ที่ป้องกัน stack overflow - local originalCallback = self.OriginalCallbacks[idx] - option.Callback = function(...) - -- ป้องกัน recursion ด้วยการใช้ flag - if option._isInCallback then - return - end - - option._isInCallback = true - - -- เรียก callback เดิม - if originalCallback then - local success, err = pcall(originalCallback, ...) - if not success then - warn("Callback error for " .. tostring(idx) .. ": " .. tostring(err)) - end - end - - option._isInCallback = false - - -- Auto save ด้วย debounce - if self.AutoSaveEnabled and self.AutoSaveConfig and not self.AutoSaveDebounce then - self.AutoSaveDebounce = true - task.spawn(function() - task.wait(1) -- รอ 1 วินาทีก่อนเซฟ - if self.AutoSaveEnabled and self.AutoSaveConfig then - self:Save(self.AutoSaveConfig) - end - self.AutoSaveDebounce = false - end) - end - end - end - end - end - - function SaveManager:DisableAutoSave() - self.AutoSaveEnabled = false - self.AutoSaveConfig = nil - self:SaveUI() - - -- คืนค่า callback เดิม - for idx, option in next, self.Options do - if self.OriginalCallbacks[idx] then - option.Callback = self.OriginalCallbacks[idx] - end - end - end - - -- สร้างส่วน UI แบบย่อ: เอา DropDown/Inputs/Buttons ออก เหลือแค่ Auto Load กับ Auto Save - function SaveManager:BuildConfigSection(tab) - assert(self.Library, "Must set SaveManager.Library") - - local section = tab:AddSection("📁 Configuration Manager") - - -- โหลด UI settings - local uiSettings = self:LoadUI() - - -- ensure AutoSave config file exists (ใช้ชื่อคงที่ "AutoSave") - local fixedConfigName = "AutoSave" - if not isfile(getConfigFilePath(self, fixedConfigName)) then - pcall(function() self:Save(fixedConfigName) end) - end - - -- Autoload Toggle (ใช้ไฟล์ AutoSave.json แบบคงที่) - local currentAutoload = self:GetAutoloadConfig() - local autoloadDesc = currentAutoload and ('ปัจจุบัน: "' .. currentAutoload .. '"') or 'จะโหลดไฟล์ "AutoSave.json" อัตโนมัติเมื่อเปิด' - - local AutoloadToggle = section:AddToggle("SaveManager_AutoloadToggle", { - Title = "🔄 Auto Load", - Description = autoloadDesc, - 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 - - -- ตั้ง autoload เป็น AutoSave - local ok, err = self:SetAutoloadConfig(fixedConfigName) - if not ok then - -- ถ้าตั้งค่าไม่สำเร็จ ให้รีเซ็ต toggle กลับเป็น false - if SaveManager.Options and SaveManager.Options.SaveManager_AutoloadToggle then - SaveManager.Options.SaveManager_AutoloadToggle:SetValue(false) - end - end - else - self:DisableAutoload() - end - end - }) - - -- Auto Save Toggle (บันทึกไปที่ AutoSave.json แบบคงที่) - local autosaveDesc = self.AutoSaveConfig and ('กำลังบันทึกอัตโนมัติไปที่: "' .. tostring(self.AutoSaveConfig) .. '"') or 'บันทึกอัตโนมัติไปที่ "AutoSave.json"' - - local AutoSaveToggle = section:AddToggle("SaveManager_AutoSaveToggle", { - Title = "💾 Auto Save", - Description = autosaveDesc, - 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 ไม่เซฟค่าของ Toggle ตัวจัดการนี้เอง - SaveManager:SetIgnoreIndexes({ - "SaveManager_AutoloadToggle", - "SaveManager_AutoSaveToggle" - }) - - -- โหลด UI settings และเปิดใช้ auto save / auto load ถ้าเคยเปิดไว้ - if uiSettings then - -- Auto Load - if uiSettings.autoload_enabled then - task.spawn(function() - -- พยายามโหลด AutoSave - if isfile(getConfigFilePath(self, fixedConfigName)) then - SaveManager:Load(fixedConfigName) - if SaveManager.Options and SaveManager.Options.SaveManager_AutoloadToggle then - SaveManager.Options.SaveManager_AutoloadToggle:SetValue(true) - end - end - end) - end - - -- Auto Save - if uiSettings.autosave_enabled then - if isfile(getConfigFilePath(self, fixedConfigName)) then - self:EnableAutoSave(fixedConfigName) - if SaveManager.Options and SaveManager.Options.SaveManager_AutoSaveToggle then - SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(true) - end - end - end - end - end - - SaveManager:BuildFolderTree() -end - -return SaveManager +local LocalPlayer = Players.LocalPlayer +local UserId = tostring(LocalPlayer.UserId) + +-- ==================== STREAMER MODE SYSTEM ==================== +local StreamerMode = {} + +-- [[ Config ]] +StreamerMode.ReplaceText = "Protected By ATG Hub" +StreamerMode.ReplaceImage = "rbxassetid://90989180960460" +StreamerMode.ReplaceExact = false + +local StringProperties = {"Text", "PlaceholderText", "Value", "Title", "ToolTip", "Description"} + +-- [[ Systems ]] +StreamerMode.OriginalValues = {} -- Store original values: { [Instance] = { [Property] = "OldValue" } } +StreamerMode.WatchedObjects = {} -- Store objects being locked +StreamerMode.Connections = {} -- Store events for disconnection +StreamerMode.Targets = {} + +-- Helper function to find targets +local function GetTargets() + local t = {} + if LocalPlayer then + if LocalPlayer.Name and LocalPlayer.Name ~= "" then table.insert(t, LocalPlayer.Name) end + if LocalPlayer.DisplayName and LocalPlayer.DisplayName ~= "" then table.insert(t, LocalPlayer.DisplayName) end + end + return t +end + +-- Case-insensitive replace function +local function CI_Replace(s, target, repl) + if type(s) ~= "string" then return s end + local lower_s = s:lower() + local lower_t = target:lower() + local res = "" + local i = 1 + while true do + local startPos, endPos = lower_s:find(lower_t, i, true) + if not startPos then + res = res .. s:sub(i) + break + end + res = res .. s:sub(i, startPos - 1) .. repl + i = endPos + 1 + end + return res +end + +local function StringContains(s, target) + if type(s) ~= "string" then return false end + return s:lower():find(target:lower(), 1, true) ~= nil +end + +-- Backup original value +local function StoreOriginal(inst, prop, value) + if not StreamerMode.OriginalValues[inst] then + StreamerMode.OriginalValues[inst] = {} + end + -- Only save the first time to preserve the true original value + if StreamerMode.OriginalValues[inst][prop] == nil then + StreamerMode.OriginalValues[inst][prop] = value + end +end + +-- Check and replace core logic +local function CheckAndReplaceCore(inst) + if not inst then return end + local foundMatch = false + + -- 1. Handle images + if inst:IsA("ImageLabel") or inst:IsA("ImageButton") then + if inst.Image and inst.Image ~= StreamerMode.ReplaceImage then + if string.find(inst.Image, UserId) then + -- Backup original value + StoreOriginal(inst, "Image", inst.Image) + -- Replace value + pcall(function() inst.Image = StreamerMode.ReplaceImage end) + foundMatch = true + end + end + end + + -- 2. Handle text + for _, prop in ipairs(StringProperties) do + local success, val = pcall(function() return inst[prop] end) + if success and type(val) == "string" and val ~= "" and val ~= StreamerMode.ReplaceText then + for _, target in ipairs(StreamerMode.Targets) do + if StringContains(val, target) then + -- Backup original value + StoreOriginal(inst, prop, val) + + -- Calculate new value + local newVal + if StreamerMode.ReplaceExact then + newVal = StreamerMode.ReplaceText + else + newVal = CI_Replace(val, target, StreamerMode.ReplaceText) + end + + -- Replace value + pcall(function() inst[prop] = newVal end) + foundMatch = true + end + end + end + end + + if foundMatch then + StreamerMode.WatchedObjects[inst] = true + end +end + +-- Toggle character GUI visibility +local function ToggleCharacterGui(char, visible) + if not char then return end + -- Wait a moment for new character to load + if visible == false then task.wait(0.5) end + + for _, v in ipairs(char:GetDescendants()) do + if v:IsA("BillboardGui") or v:IsA("SurfaceGui") then + if visible == false then + -- Hide (backup enabled state) + StoreOriginal(v, "Enabled", v.Enabled) + v.Enabled = false + else + -- Restore (use backup if available, otherwise enable) + if StreamerMode.OriginalValues[v] and StreamerMode.OriginalValues[v]["Enabled"] ~= nil then + v.Enabled = StreamerMode.OriginalValues[v]["Enabled"] + else + v.Enabled = true + end + end + end + end +end + +-- Start protection +function StreamerMode:Start() + self.Targets = GetTargets() + + -- 1. Handle character (hide GUI) + if LocalPlayer.Character then + task.spawn(function() ToggleCharacterGui(LocalPlayer.Character, false) end) + end + self.Connections["CharAdded"] = LocalPlayer.CharacterAdded:Connect(function(char) + task.spawn(function() ToggleCharacterGui(char, false) end) + end) + + self.Connections["DisplayName"] = LocalPlayer:GetPropertyChangedSignal("DisplayName"):Connect(function() + self.Targets = GetTargets() + end) + + -- 2. Scan existing CoreGui + for _, desc in ipairs(CoreGui:GetDescendants()) do + CheckAndReplaceCore(desc) + end + + -- 3. Detect new CoreGui elements + self.Connections["CoreAdded"] = CoreGui.DescendantAdded:Connect(function(desc) + task.delay(0.1, function() CheckAndReplaceCore(desc) end) + end) + + -- 4. Heartbeat loop (lock values) + self.Connections["Heartbeat"] = RunService.Heartbeat:Connect(function() + for inst, _ in pairs(self.WatchedObjects) do + if inst and inst.Parent then + CheckAndReplaceCore(inst) + else + self.WatchedObjects[inst] = nil + end + end + end) +end + +-- Stop protection and restore +function StreamerMode:Stop() + -- 1. Disconnect all connections + for _, conn in pairs(self.Connections) do + conn:Disconnect() + end + self.Connections = {} + self.WatchedObjects = {} + + -- 2. Restore original values to all modified objects + for inst, props in pairs(self.OriginalValues) do + if inst and inst.Parent then + for propName, originalVal in pairs(props) do + pcall(function() + inst[propName] = originalVal + end) + end + end + end + + -- 3. Clear backup table + self.OriginalValues = {} + + -- 4. Show character GUI again + if LocalPlayer.Character then + ToggleCharacterGui(LocalPlayer.Character, true) + end +end + +-- ==================== SAVE MANAGER SYSTEM ==================== +local SaveManager = {} + +SaveManager.FolderRoot = "ATGSettings" +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, + }, +} + +-- Helper functions +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 placeId = getPlaceId() + return root .. "/" .. placeId +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 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: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 + +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) + + 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 + self:Load(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 option._isInCallback then + return + end + + option._isInCallback = true + + if originalCallback then + local success, err = pcall(originalCallback, ...) + if not success then + warn("Callback error for " .. tostring(idx) .. ": " .. tostring(err)) + end + end + + option._isInCallback = false + + if self.AutoSaveEnabled and self.AutoSaveConfig and not self.AutoSaveDebounce then + self.AutoSaveDebounce = true + task.spawn(function() + task.wait(1) + if self.AutoSaveEnabled and self.AutoSaveConfig then + self:Save(self.AutoSaveConfig) + end + self.AutoSaveDebounce = false + 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 + + local currentAutoload = self:GetAutoloadConfig() + local autoloadDesc = currentAutoload and ('Current: "' .. currentAutoload .. '"') or 'Will load "AutoSave.json" automatically on startup' + + local AutoloadToggle = section:AddToggle("SaveManager_AutoloadToggle", { + Title = "Auto Load", + Description = autoloadDesc, + 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, err = 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 + }) + + local autosaveDesc = self.AutoSaveConfig and ('Currently auto-saving to: "' .. tostring(self.AutoSaveConfig) .. '"') or 'Auto-save to "AutoSave.json"' + + local AutoSaveToggle = section:AddToggle("SaveManager_AutoSaveToggle", { + Title = "Auto Save", + Description = autosaveDesc, + 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" + }) + + if uiSettings then + if uiSettings.autoload_enabled then + task.spawn(function() + if isfile(getConfigFilePath(self, fixedConfigName)) then + SaveManager:Load(fixedConfigName) + if SaveManager.Options and SaveManager.Options.SaveManager_AutoloadToggle then + SaveManager.Options.SaveManager_AutoloadToggle:SetValue(true) + end + end + end) + end + + if uiSettings.autosave_enabled then + if isfile(getConfigFilePath(self, fixedConfigName)) then + self:EnableAutoSave(fixedConfigName) + if SaveManager.Options and SaveManager.Options.SaveManager_AutoSaveToggle then + SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(true) + end + end + end + end +end + +SaveManager:BuildFolderTree() + +-- ==================== UI SETUP ==================== +-- Streamer Mode Section +local StreamerSection = Tabs.Main:AddSection("Miscellaneous") + +local StreamerToggle = StreamerSection:AddToggle("StreamerModeToggle", { + Title = "Protect Name", + Description = "Protects your name", + Default = false +}) + +StreamerToggle:OnChanged(function() + if Options.StreamerModeToggle.Value then + StreamerMode:Start() + else + StreamerMode:Stop() + end +end) + +Options.StreamerModeToggle:SetValue(false) + +-- Configuration Manager Section +SaveManager:SetLibrary(Library) +SaveManager:BuildConfigSection(Tabs.Main) + +-- Return both modules +return { + StreamerMode = StreamerMode, + SaveManager = SaveManager +} From c9ca1da3fa7ad43ddf6caea0f76a6da189239d39 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Tue, 2 Dec 2025 00:31:53 +0700 Subject: [PATCH 61/76] Update autosave.lua --- autosave.lua | 1201 +++++++++++++++++++++----------------------------- 1 file changed, 496 insertions(+), 705 deletions(-) diff --git a/autosave.lua b/autosave.lua index 67c76ac..cd272b3 100644 --- a/autosave.lua +++ b/autosave.lua @@ -1,708 +1,499 @@ ----@diagnostic disable: undefined-global -local Players = game:GetService("Players") -local CoreGui = game:GetService("CoreGui") -local RunService = game:GetService("RunService") local httpService = game:GetService("HttpService") local Workspace = game:GetService("Workspace") -local LocalPlayer = Players.LocalPlayer -local UserId = tostring(LocalPlayer.UserId) - --- ==================== STREAMER MODE SYSTEM ==================== -local StreamerMode = {} - --- [[ Config ]] -StreamerMode.ReplaceText = "Protected By ATG Hub" -StreamerMode.ReplaceImage = "rbxassetid://90989180960460" -StreamerMode.ReplaceExact = false - -local StringProperties = {"Text", "PlaceholderText", "Value", "Title", "ToolTip", "Description"} - --- [[ Systems ]] -StreamerMode.OriginalValues = {} -- Store original values: { [Instance] = { [Property] = "OldValue" } } -StreamerMode.WatchedObjects = {} -- Store objects being locked -StreamerMode.Connections = {} -- Store events for disconnection -StreamerMode.Targets = {} - --- Helper function to find targets -local function GetTargets() - local t = {} - if LocalPlayer then - if LocalPlayer.Name and LocalPlayer.Name ~= "" then table.insert(t, LocalPlayer.Name) end - if LocalPlayer.DisplayName and LocalPlayer.DisplayName ~= "" then table.insert(t, LocalPlayer.DisplayName) end - end - return t -end - --- Case-insensitive replace function -local function CI_Replace(s, target, repl) - if type(s) ~= "string" then return s end - local lower_s = s:lower() - local lower_t = target:lower() - local res = "" - local i = 1 - while true do - local startPos, endPos = lower_s:find(lower_t, i, true) - if not startPos then - res = res .. s:sub(i) - break - end - res = res .. s:sub(i, startPos - 1) .. repl - i = endPos + 1 - end - return res -end - -local function StringContains(s, target) - if type(s) ~= "string" then return false end - return s:lower():find(target:lower(), 1, true) ~= nil -end - --- Backup original value -local function StoreOriginal(inst, prop, value) - if not StreamerMode.OriginalValues[inst] then - StreamerMode.OriginalValues[inst] = {} - end - -- Only save the first time to preserve the true original value - if StreamerMode.OriginalValues[inst][prop] == nil then - StreamerMode.OriginalValues[inst][prop] = value - end -end - --- Check and replace core logic -local function CheckAndReplaceCore(inst) - if not inst then return end - local foundMatch = false - - -- 1. Handle images - if inst:IsA("ImageLabel") or inst:IsA("ImageButton") then - if inst.Image and inst.Image ~= StreamerMode.ReplaceImage then - if string.find(inst.Image, UserId) then - -- Backup original value - StoreOriginal(inst, "Image", inst.Image) - -- Replace value - pcall(function() inst.Image = StreamerMode.ReplaceImage end) - foundMatch = true - end - end - end - - -- 2. Handle text - for _, prop in ipairs(StringProperties) do - local success, val = pcall(function() return inst[prop] end) - if success and type(val) == "string" and val ~= "" and val ~= StreamerMode.ReplaceText then - for _, target in ipairs(StreamerMode.Targets) do - if StringContains(val, target) then - -- Backup original value - StoreOriginal(inst, prop, val) - - -- Calculate new value - local newVal - if StreamerMode.ReplaceExact then - newVal = StreamerMode.ReplaceText - else - newVal = CI_Replace(val, target, StreamerMode.ReplaceText) - end - - -- Replace value - pcall(function() inst[prop] = newVal end) - foundMatch = true - end - end - end - end - - if foundMatch then - StreamerMode.WatchedObjects[inst] = true - end -end - --- Toggle character GUI visibility -local function ToggleCharacterGui(char, visible) - if not char then return end - -- Wait a moment for new character to load - if visible == false then task.wait(0.5) end - - for _, v in ipairs(char:GetDescendants()) do - if v:IsA("BillboardGui") or v:IsA("SurfaceGui") then - if visible == false then - -- Hide (backup enabled state) - StoreOriginal(v, "Enabled", v.Enabled) - v.Enabled = false - else - -- Restore (use backup if available, otherwise enable) - if StreamerMode.OriginalValues[v] and StreamerMode.OriginalValues[v]["Enabled"] ~= nil then - v.Enabled = StreamerMode.OriginalValues[v]["Enabled"] - else - v.Enabled = true - end - end - end - end -end - --- Start protection -function StreamerMode:Start() - self.Targets = GetTargets() - - -- 1. Handle character (hide GUI) - if LocalPlayer.Character then - task.spawn(function() ToggleCharacterGui(LocalPlayer.Character, false) end) - end - self.Connections["CharAdded"] = LocalPlayer.CharacterAdded:Connect(function(char) - task.spawn(function() ToggleCharacterGui(char, false) end) - end) - - self.Connections["DisplayName"] = LocalPlayer:GetPropertyChangedSignal("DisplayName"):Connect(function() - self.Targets = GetTargets() - end) - - -- 2. Scan existing CoreGui - for _, desc in ipairs(CoreGui:GetDescendants()) do - CheckAndReplaceCore(desc) - end - - -- 3. Detect new CoreGui elements - self.Connections["CoreAdded"] = CoreGui.DescendantAdded:Connect(function(desc) - task.delay(0.1, function() CheckAndReplaceCore(desc) end) - end) - - -- 4. Heartbeat loop (lock values) - self.Connections["Heartbeat"] = RunService.Heartbeat:Connect(function() - for inst, _ in pairs(self.WatchedObjects) do - if inst and inst.Parent then - CheckAndReplaceCore(inst) - else - self.WatchedObjects[inst] = nil - end - end - end) -end - --- Stop protection and restore -function StreamerMode:Stop() - -- 1. Disconnect all connections - for _, conn in pairs(self.Connections) do - conn:Disconnect() - end - self.Connections = {} - self.WatchedObjects = {} - - -- 2. Restore original values to all modified objects - for inst, props in pairs(self.OriginalValues) do - if inst and inst.Parent then - for propName, originalVal in pairs(props) do - pcall(function() - inst[propName] = originalVal - end) - end - end - end - - -- 3. Clear backup table - self.OriginalValues = {} - - -- 4. Show character GUI again - if LocalPlayer.Character then - ToggleCharacterGui(LocalPlayer.Character, true) - end -end - --- ==================== SAVE MANAGER SYSTEM ==================== -local SaveManager = {} - -SaveManager.FolderRoot = "ATGSettings" -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, - }, -} - --- Helper functions -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 placeId = getPlaceId() - return root .. "/" .. placeId -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 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: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 - -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) - - 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 - self:Load(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 option._isInCallback then - return - end - - option._isInCallback = true - - if originalCallback then - local success, err = pcall(originalCallback, ...) - if not success then - warn("Callback error for " .. tostring(idx) .. ": " .. tostring(err)) - end - end - - option._isInCallback = false - - if self.AutoSaveEnabled and self.AutoSaveConfig and not self.AutoSaveDebounce then - self.AutoSaveDebounce = true - task.spawn(function() - task.wait(1) - if self.AutoSaveEnabled and self.AutoSaveConfig then - self:Save(self.AutoSaveConfig) - end - self.AutoSaveDebounce = false - 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 - - local currentAutoload = self:GetAutoloadConfig() - local autoloadDesc = currentAutoload and ('Current: "' .. currentAutoload .. '"') or 'Will load "AutoSave.json" automatically on startup' - - local AutoloadToggle = section:AddToggle("SaveManager_AutoloadToggle", { - Title = "Auto Load", - Description = autoloadDesc, - 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, err = 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 - }) - - local autosaveDesc = self.AutoSaveConfig and ('Currently auto-saving to: "' .. tostring(self.AutoSaveConfig) .. '"') or 'Auto-save to "AutoSave.json"' - - local AutoSaveToggle = section:AddToggle("SaveManager_AutoSaveToggle", { - Title = "Auto Save", - Description = autosaveDesc, - 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" - }) - - if uiSettings then - if uiSettings.autoload_enabled then - task.spawn(function() - if isfile(getConfigFilePath(self, fixedConfigName)) then - SaveManager:Load(fixedConfigName) - if SaveManager.Options and SaveManager.Options.SaveManager_AutoloadToggle then - SaveManager.Options.SaveManager_AutoloadToggle:SetValue(true) - end - end - end) - end - - if uiSettings.autosave_enabled then - if isfile(getConfigFilePath(self, fixedConfigName)) then - self:EnableAutoSave(fixedConfigName) - if SaveManager.Options and SaveManager.Options.SaveManager_AutoSaveToggle then - SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(true) - end - end - end - end -end - -SaveManager:BuildFolderTree() - --- ==================== UI SETUP ==================== --- Streamer Mode Section -local StreamerSection = Tabs.Main:AddSection("Miscellaneous") - -local StreamerToggle = StreamerSection:AddToggle("StreamerModeToggle", { - Title = "Protect Name", - Description = "Protects your name", - Default = false -}) - -StreamerToggle:OnChanged(function() - if Options.StreamerModeToggle.Value then - StreamerMode:Start() - else - StreamerMode:Stop() - end -end) - -Options.StreamerModeToggle:SetValue(false) - --- Configuration Manager Section -SaveManager:SetLibrary(Library) -SaveManager:BuildConfigSection(Tabs.Main) - --- Return both modules -return { - StreamerMode = StreamerMode, - SaveManager = SaveManager -} +local SaveManager = {} do + SaveManager.FolderRoot = "ATGSettings" + 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 placeId = getPlaceId() + return root .. "/" .. placeId + end + + local function getConfigFilePath(self, name) + local folder = getConfigsFolder(self) + return folder .. "/" .. name .. ".json" + end + + -- ไฟล์สำหรับเซฟ UI ของ SaveManager เอง + local function getSaveManagerUIPath(self) + local folder = getConfigsFolder(self) + return folder .. "/savemanager_ui.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 + + -- เซฟ UI ของ SaveManager แยกต่างหาก + 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 + + -- โหลด UI ของ SaveManager + 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 + + 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) + 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 + self:Load(name) + end + end + + -- ฟังก์ชัน Auto Save + function SaveManager:EnableAutoSave(configName) + self.AutoSaveEnabled = true + self.AutoSaveConfig = configName + self:SaveUI() + + -- บันทึก callback เดิมและตั้ง callback ใหม่ + for idx, option in next, self.Options do + if not self.Ignore[idx] and self.Parser[option.Type] then + -- เก็บ callback เดิมไว้ถ้ายังไม่เคยเก็บ + if not self.OriginalCallbacks[idx] then + self.OriginalCallbacks[idx] = option.Callback + end + + -- สร้าง callback ใหม่ที่ป้องกัน stack overflow + local originalCallback = self.OriginalCallbacks[idx] + option.Callback = function(...) + -- ป้องกัน recursion ด้วยการใช้ flag + if option._isInCallback then + return + end + + option._isInCallback = true + + -- เรียก callback เดิม + if originalCallback then + local success, err = pcall(originalCallback, ...) + if not success then + warn("Callback error for " .. tostring(idx) .. ": " .. tostring(err)) + end + end + + option._isInCallback = false + + -- Auto save ด้วย debounce + if self.AutoSaveEnabled and self.AutoSaveConfig and not self.AutoSaveDebounce then + self.AutoSaveDebounce = true + task.spawn(function() + task.wait(1) -- รอ 1 วินาทีก่อนเซฟ + if self.AutoSaveEnabled and self.AutoSaveConfig then + self:Save(self.AutoSaveConfig) + end + self.AutoSaveDebounce = false + end) + end + end + end + end + end + + function SaveManager:DisableAutoSave() + self.AutoSaveEnabled = false + self.AutoSaveConfig = nil + self:SaveUI() + + -- คืนค่า callback เดิม + for idx, option in next, self.Options do + if self.OriginalCallbacks[idx] then + option.Callback = self.OriginalCallbacks[idx] + end + end + end + + -- สร้างส่วน UI แบบย่อ: เอา DropDown/Inputs/Buttons ออก เหลือแค่ Auto Load กับ Auto Save + function SaveManager:BuildConfigSection(tab) + assert(self.Library, "Must set SaveManager.Library") + + local section = tab:AddSection("📁 Configuration Manager") + + -- โหลด UI settings + local uiSettings = self:LoadUI() + + -- ensure AutoSave config file exists (ใช้ชื่อคงที่ "AutoSave") + local fixedConfigName = "AutoSave" + if not isfile(getConfigFilePath(self, fixedConfigName)) then + pcall(function() self:Save(fixedConfigName) end) + end + + -- Autoload Toggle (ใช้ไฟล์ AutoSave.json แบบคงที่) + local currentAutoload = self:GetAutoloadConfig() + local autoloadDesc = currentAutoload and ('ปัจจุบัน: "' .. currentAutoload .. '"') or 'จะโหลดไฟล์ "AutoSave.json" อัตโนมัติเมื่อเปิด' + + local AutoloadToggle = section:AddToggle("SaveManager_AutoloadToggle", { + Title = "🔄 Auto Load", + Description = autoloadDesc, + 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 + + -- ตั้ง autoload เป็น AutoSave + local ok, err = self:SetAutoloadConfig(fixedConfigName) + if not ok then + -- ถ้าตั้งค่าไม่สำเร็จ ให้รีเซ็ต toggle กลับเป็น false + if SaveManager.Options and SaveManager.Options.SaveManager_AutoloadToggle then + SaveManager.Options.SaveManager_AutoloadToggle:SetValue(false) + end + end + else + self:DisableAutoload() + end + end + }) + + -- Auto Save Toggle (บันทึกไปที่ AutoSave.json แบบคงที่) + local autosaveDesc = self.AutoSaveConfig and ('กำลังบันทึกอัตโนมัติไปที่: "' .. tostring(self.AutoSaveConfig) .. '"') or 'บันทึกอัตโนมัติไปที่ "AutoSave.json"' + + local AutoSaveToggle = section:AddToggle("SaveManager_AutoSaveToggle", { + Title = "💾 Auto Save", + Description = autosaveDesc, + 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 ไม่เซฟค่าของ Toggle ตัวจัดการนี้เอง + SaveManager:SetIgnoreIndexes({ + "SaveManager_AutoloadToggle", + "SaveManager_AutoSaveToggle" + }) + + -- โหลด UI settings และเปิดใช้ auto save / auto load ถ้าเคยเปิดไว้ + if uiSettings then + -- Auto Load + if uiSettings.autoload_enabled then + task.spawn(function() + -- พยายามโหลด AutoSave + if isfile(getConfigFilePath(self, fixedConfigName)) then + SaveManager:Load(fixedConfigName) + if SaveManager.Options and SaveManager.Options.SaveManager_AutoloadToggle then + SaveManager.Options.SaveManager_AutoloadToggle:SetValue(true) + end + end + end) + end + + -- Auto Save + if uiSettings.autosave_enabled then + if isfile(getConfigFilePath(self, fixedConfigName)) then + self:EnableAutoSave(fixedConfigName) + if SaveManager.Options and SaveManager.Options.SaveManager_AutoSaveToggle then + SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(true) + end + end + end + end + end + + SaveManager:BuildFolderTree() +end + +return SaveManager From 6290b4668de85aa55a90cbac55ff8f2329d0ab41 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Tue, 2 Dec 2025 00:39:00 +0700 Subject: [PATCH 62/76] Update autosave.lua --- autosave.lua | 1201 +++++++++++++++++++++++++++++--------------------- 1 file changed, 705 insertions(+), 496 deletions(-) diff --git a/autosave.lua b/autosave.lua index cd272b3..05c0ce9 100644 --- a/autosave.lua +++ b/autosave.lua @@ -1,499 +1,708 @@ +---@diagnostic disable: undefined-global +local Players = game:GetService("Players") +local CoreGui = game:GetService("CoreGui") +local RunService = game:GetService("RunService") local httpService = game:GetService("HttpService") local Workspace = game:GetService("Workspace") -local SaveManager = {} do - SaveManager.FolderRoot = "ATGSettings" - 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 placeId = getPlaceId() - return root .. "/" .. placeId - end - - local function getConfigFilePath(self, name) - local folder = getConfigsFolder(self) - return folder .. "/" .. name .. ".json" - end - - -- ไฟล์สำหรับเซฟ UI ของ SaveManager เอง - local function getSaveManagerUIPath(self) - local folder = getConfigsFolder(self) - return folder .. "/savemanager_ui.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 - - -- เซฟ UI ของ SaveManager แยกต่างหาก - 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 - - -- โหลด UI ของ SaveManager - 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 - - 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) - 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 - self:Load(name) - end - end - - -- ฟังก์ชัน Auto Save - function SaveManager:EnableAutoSave(configName) - self.AutoSaveEnabled = true - self.AutoSaveConfig = configName - self:SaveUI() - - -- บันทึก callback เดิมและตั้ง callback ใหม่ - for idx, option in next, self.Options do - if not self.Ignore[idx] and self.Parser[option.Type] then - -- เก็บ callback เดิมไว้ถ้ายังไม่เคยเก็บ - if not self.OriginalCallbacks[idx] then - self.OriginalCallbacks[idx] = option.Callback - end - - -- สร้าง callback ใหม่ที่ป้องกัน stack overflow - local originalCallback = self.OriginalCallbacks[idx] - option.Callback = function(...) - -- ป้องกัน recursion ด้วยการใช้ flag - if option._isInCallback then - return - end - - option._isInCallback = true - - -- เรียก callback เดิม - if originalCallback then - local success, err = pcall(originalCallback, ...) - if not success then - warn("Callback error for " .. tostring(idx) .. ": " .. tostring(err)) - end - end - - option._isInCallback = false - - -- Auto save ด้วย debounce - if self.AutoSaveEnabled and self.AutoSaveConfig and not self.AutoSaveDebounce then - self.AutoSaveDebounce = true - task.spawn(function() - task.wait(1) -- รอ 1 วินาทีก่อนเซฟ - if self.AutoSaveEnabled and self.AutoSaveConfig then - self:Save(self.AutoSaveConfig) - end - self.AutoSaveDebounce = false - end) - end - end - end - end - end - - function SaveManager:DisableAutoSave() - self.AutoSaveEnabled = false - self.AutoSaveConfig = nil - self:SaveUI() - - -- คืนค่า callback เดิม - for idx, option in next, self.Options do - if self.OriginalCallbacks[idx] then - option.Callback = self.OriginalCallbacks[idx] - end - end - end - - -- สร้างส่วน UI แบบย่อ: เอา DropDown/Inputs/Buttons ออก เหลือแค่ Auto Load กับ Auto Save - function SaveManager:BuildConfigSection(tab) - assert(self.Library, "Must set SaveManager.Library") - - local section = tab:AddSection("📁 Configuration Manager") - - -- โหลด UI settings - local uiSettings = self:LoadUI() - - -- ensure AutoSave config file exists (ใช้ชื่อคงที่ "AutoSave") - local fixedConfigName = "AutoSave" - if not isfile(getConfigFilePath(self, fixedConfigName)) then - pcall(function() self:Save(fixedConfigName) end) - end - - -- Autoload Toggle (ใช้ไฟล์ AutoSave.json แบบคงที่) - local currentAutoload = self:GetAutoloadConfig() - local autoloadDesc = currentAutoload and ('ปัจจุบัน: "' .. currentAutoload .. '"') or 'จะโหลดไฟล์ "AutoSave.json" อัตโนมัติเมื่อเปิด' - - local AutoloadToggle = section:AddToggle("SaveManager_AutoloadToggle", { - Title = "🔄 Auto Load", - Description = autoloadDesc, - 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 - - -- ตั้ง autoload เป็น AutoSave - local ok, err = self:SetAutoloadConfig(fixedConfigName) - if not ok then - -- ถ้าตั้งค่าไม่สำเร็จ ให้รีเซ็ต toggle กลับเป็น false - if SaveManager.Options and SaveManager.Options.SaveManager_AutoloadToggle then - SaveManager.Options.SaveManager_AutoloadToggle:SetValue(false) - end - end - else - self:DisableAutoload() - end - end - }) - - -- Auto Save Toggle (บันทึกไปที่ AutoSave.json แบบคงที่) - local autosaveDesc = self.AutoSaveConfig and ('กำลังบันทึกอัตโนมัติไปที่: "' .. tostring(self.AutoSaveConfig) .. '"') or 'บันทึกอัตโนมัติไปที่ "AutoSave.json"' - - local AutoSaveToggle = section:AddToggle("SaveManager_AutoSaveToggle", { - Title = "💾 Auto Save", - Description = autosaveDesc, - 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 ไม่เซฟค่าของ Toggle ตัวจัดการนี้เอง - SaveManager:SetIgnoreIndexes({ - "SaveManager_AutoloadToggle", - "SaveManager_AutoSaveToggle" - }) - - -- โหลด UI settings และเปิดใช้ auto save / auto load ถ้าเคยเปิดไว้ - if uiSettings then - -- Auto Load - if uiSettings.autoload_enabled then - task.spawn(function() - -- พยายามโหลด AutoSave - if isfile(getConfigFilePath(self, fixedConfigName)) then - SaveManager:Load(fixedConfigName) - if SaveManager.Options and SaveManager.Options.SaveManager_AutoloadToggle then - SaveManager.Options.SaveManager_AutoloadToggle:SetValue(true) - end - end - end) - end - - -- Auto Save - if uiSettings.autosave_enabled then - if isfile(getConfigFilePath(self, fixedConfigName)) then - self:EnableAutoSave(fixedConfigName) - if SaveManager.Options and SaveManager.Options.SaveManager_AutoSaveToggle then - SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(true) - end - end - end - end - end - - SaveManager:BuildFolderTree() -end - -return SaveManager +local LocalPlayer = Players.LocalPlayer +local UserId = tostring(LocalPlayer.UserId) + +-- ==================== STREAMER MODE SYSTEM ==================== +local StreamerMode = {} + +-- [[ Config ]] +StreamerMode.ReplaceText = "Protected By ATG Hub" +StreamerMode.ReplaceImage = "rbxassetid://90989180960460" +StreamerMode.ReplaceExact = false + +local StringProperties = {"Text", "PlaceholderText", "Value", "Title", "ToolTip", "Description"} + +-- [[ Systems ]] +StreamerMode.OriginalValues = {} -- Store original values: { [Instance] = { [Property] = "OldValue" } } +StreamerMode.WatchedObjects = {} -- Store objects being locked +StreamerMode.Connections = {} -- Store events for disconnection +StreamerMode.Targets = {} + +-- Helper function to find targets +local function GetTargets() + local t = {} + if LocalPlayer then + if LocalPlayer.Name and LocalPlayer.Name ~= "" then table.insert(t, LocalPlayer.Name) end + if LocalPlayer.DisplayName and LocalPlayer.DisplayName ~= "" then table.insert(t, LocalPlayer.DisplayName) end + end + return t +end + +-- Case-insensitive replace function +local function CI_Replace(s, target, repl) + if type(s) ~= "string" then return s end + local lower_s = s:lower() + local lower_t = target:lower() + local res = "" + local i = 1 + while true do + local startPos, endPos = lower_s:find(lower_t, i, true) + if not startPos then + res = res .. s:sub(i) + break + end + res = res .. s:sub(i, startPos - 1) .. repl + i = endPos + 1 + end + return res +end + +local function StringContains(s, target) + if type(s) ~= "string" then return false end + return s:lower():find(target:lower(), 1, true) ~= nil +end + +-- Backup original value +local function StoreOriginal(inst, prop, value) + if not StreamerMode.OriginalValues[inst] then + StreamerMode.OriginalValues[inst] = {} + end + -- Only save the first time to preserve the true original value + if StreamerMode.OriginalValues[inst][prop] == nil then + StreamerMode.OriginalValues[inst][prop] = value + end +end + +-- Check and replace core logic +local function CheckAndReplaceCore(inst) + if not inst then return end + local foundMatch = false + + -- 1. Handle images + if inst:IsA("ImageLabel") or inst:IsA("ImageButton") then + if inst.Image and inst.Image ~= StreamerMode.ReplaceImage then + if string.find(inst.Image, UserId) then + -- Backup original value + StoreOriginal(inst, "Image", inst.Image) + -- Replace value + pcall(function() inst.Image = StreamerMode.ReplaceImage end) + foundMatch = true + end + end + end + + -- 2. Handle text + for _, prop in ipairs(StringProperties) do + local success, val = pcall(function() return inst[prop] end) + if success and type(val) == "string" and val ~= "" and val ~= StreamerMode.ReplaceText then + for _, target in ipairs(StreamerMode.Targets) do + if StringContains(val, target) then + -- Backup original value + StoreOriginal(inst, prop, val) + + -- Calculate new value + local newVal + if StreamerMode.ReplaceExact then + newVal = StreamerMode.ReplaceText + else + newVal = CI_Replace(val, target, StreamerMode.ReplaceText) + end + + -- Replace value + pcall(function() inst[prop] = newVal end) + foundMatch = true + end + end + end + end + + if foundMatch then + StreamerMode.WatchedObjects[inst] = true + end +end + +-- Toggle character GUI visibility +local function ToggleCharacterGui(char, visible) + if not char then return end + -- Wait a moment for new character to load + if visible == false then task.wait(0.5) end + + for _, v in ipairs(char:GetDescendants()) do + if v:IsA("BillboardGui") or v:IsA("SurfaceGui") then + if visible == false then + -- Hide (backup enabled state) + StoreOriginal(v, "Enabled", v.Enabled) + v.Enabled = false + else + -- Restore (use backup if available, otherwise enable) + if StreamerMode.OriginalValues[v] and StreamerMode.OriginalValues[v]["Enabled"] ~= nil then + v.Enabled = StreamerMode.OriginalValues[v]["Enabled"] + else + v.Enabled = true + end + end + end + end +end + +-- Start protection +function StreamerMode:Start() + self.Targets = GetTargets() + + -- 1. Handle character (hide GUI) + if LocalPlayer.Character then + task.spawn(function() ToggleCharacterGui(LocalPlayer.Character, false) end) + end + self.Connections["CharAdded"] = LocalPlayer.CharacterAdded:Connect(function(char) + task.spawn(function() ToggleCharacterGui(char, false) end) + end) + + self.Connections["DisplayName"] = LocalPlayer:GetPropertyChangedSignal("DisplayName"):Connect(function() + self.Targets = GetTargets() + end) + + -- 2. Scan existing CoreGui + for _, desc in ipairs(CoreGui:GetDescendants()) do + CheckAndReplaceCore(desc) + end + + -- 3. Detect new CoreGui elements + self.Connections["CoreAdded"] = CoreGui.DescendantAdded:Connect(function(desc) + task.delay(0.1, function() CheckAndReplaceCore(desc) end) + end) + + -- 4. Heartbeat loop (lock values) + self.Connections["Heartbeat"] = RunService.Heartbeat:Connect(function() + for inst, _ in pairs(self.WatchedObjects) do + if inst and inst.Parent then + CheckAndReplaceCore(inst) + else + self.WatchedObjects[inst] = nil + end + end + end) +end + +-- Stop protection and restore +function StreamerMode:Stop() + -- 1. Disconnect all connections + for _, conn in pairs(self.Connections) do + conn:Disconnect() + end + self.Connections = {} + self.WatchedObjects = {} + + -- 2. Restore original values to all modified objects + for inst, props in pairs(self.OriginalValues) do + if inst and inst.Parent then + for propName, originalVal in pairs(props) do + pcall(function() + inst[propName] = originalVal + end) + end + end + end + + -- 3. Clear backup table + self.OriginalValues = {} + + -- 4. Show character GUI again + if LocalPlayer.Character then + ToggleCharacterGui(LocalPlayer.Character, true) + end +end + +-- ==================== SAVE MANAGER SYSTEM ==================== +local SaveManager = {} + +SaveManager.FolderRoot = "ATGSettings" +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, + }, +} + +-- Helper functions +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 placeId = getPlaceId() + return root .. "/" .. placeId +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 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: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 + +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) + + 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 + self:Load(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 option._isInCallback then + return + end + + option._isInCallback = true + + if originalCallback then + local success, err = pcall(originalCallback, ...) + if not success then + warn("Callback error for " .. tostring(idx) .. ": " .. tostring(err)) + end + end + + option._isInCallback = false + + if self.AutoSaveEnabled and self.AutoSaveConfig and not self.AutoSaveDebounce then + self.AutoSaveDebounce = true + task.spawn(function() + task.wait(1) + if self.AutoSaveEnabled and self.AutoSaveConfig then + self:Save(self.AutoSaveConfig) + end + self.AutoSaveDebounce = false + 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 + + local currentAutoload = self:GetAutoloadConfig() + local autoloadDesc = currentAutoload and ('Current: "' .. currentAutoload .. '"') or 'Will load "AutoSave.json" automatically on startup' + + local AutoloadToggle = section:AddToggle("SaveManager_AutoloadToggle", { + Title = "Auto Load", + Description = autoloadDesc, + 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, err = 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 + }) + + local autosaveDesc = self.AutoSaveConfig and ('Currently auto-saving to: "' .. tostring(self.AutoSaveConfig) .. '"') or 'Auto-save to "AutoSave.json"' + + local AutoSaveToggle = section:AddToggle("SaveManager_AutoSaveToggle", { + Title = "Auto Save", + Description = autosaveDesc, + 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", + "StreamerModeToggle" + }) + + if uiSettings then + if uiSettings.autoload_enabled then + task.spawn(function() + if isfile(getConfigFilePath(self, fixedConfigName)) then + SaveManager:Load(fixedConfigName) + if SaveManager.Options and SaveManager.Options.SaveManager_AutoloadToggle then + SaveManager.Options.SaveManager_AutoloadToggle:SetValue(true) + end + end + end) + end + + if uiSettings.autosave_enabled then + if isfile(getConfigFilePath(self, fixedConfigName)) then + self:EnableAutoSave(fixedConfigName) + if SaveManager.Options and SaveManager.Options.SaveManager_AutoSaveToggle then + SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(true) + end + end + end + end +end + +SaveManager:BuildFolderTree() + +-- ==================== UI LOADER ==================== +local function LoadConfigSystem(Tabs, Library) + -- Streamer Mode Section + local StreamerSection = Tabs.Main:AddSection("Miscellaneous") + + local StreamerToggle = StreamerSection:AddToggle("StreamerModeToggle", { + Title = "Protect Name", + Description = "Protects your name while streaming", + Default = false + }) + + StreamerToggle:OnChanged(function() + if Library.Options.StreamerModeToggle.Value then + StreamerMode:Start() + else + StreamerMode:Stop() + end + end) + + Library.Options.StreamerModeToggle:SetValue(false) + + -- Configuration Manager Section + SaveManager:SetLibrary(Library) + SaveManager:BuildConfigSection(Tabs.Main) +end + +-- Return loader function +return LoadConfigSystem From 28c9f238062b1e75ba35f3c883a7ba0ba735749c Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Tue, 2 Dec 2025 00:44:16 +0700 Subject: [PATCH 63/76] Update autosave.lua --- autosave.lua | 1201 +++++++++++++++++++++----------------------------- 1 file changed, 496 insertions(+), 705 deletions(-) diff --git a/autosave.lua b/autosave.lua index 05c0ce9..cd272b3 100644 --- a/autosave.lua +++ b/autosave.lua @@ -1,708 +1,499 @@ ----@diagnostic disable: undefined-global -local Players = game:GetService("Players") -local CoreGui = game:GetService("CoreGui") -local RunService = game:GetService("RunService") local httpService = game:GetService("HttpService") local Workspace = game:GetService("Workspace") -local LocalPlayer = Players.LocalPlayer -local UserId = tostring(LocalPlayer.UserId) - --- ==================== STREAMER MODE SYSTEM ==================== -local StreamerMode = {} - --- [[ Config ]] -StreamerMode.ReplaceText = "Protected By ATG Hub" -StreamerMode.ReplaceImage = "rbxassetid://90989180960460" -StreamerMode.ReplaceExact = false - -local StringProperties = {"Text", "PlaceholderText", "Value", "Title", "ToolTip", "Description"} - --- [[ Systems ]] -StreamerMode.OriginalValues = {} -- Store original values: { [Instance] = { [Property] = "OldValue" } } -StreamerMode.WatchedObjects = {} -- Store objects being locked -StreamerMode.Connections = {} -- Store events for disconnection -StreamerMode.Targets = {} - --- Helper function to find targets -local function GetTargets() - local t = {} - if LocalPlayer then - if LocalPlayer.Name and LocalPlayer.Name ~= "" then table.insert(t, LocalPlayer.Name) end - if LocalPlayer.DisplayName and LocalPlayer.DisplayName ~= "" then table.insert(t, LocalPlayer.DisplayName) end - end - return t -end - --- Case-insensitive replace function -local function CI_Replace(s, target, repl) - if type(s) ~= "string" then return s end - local lower_s = s:lower() - local lower_t = target:lower() - local res = "" - local i = 1 - while true do - local startPos, endPos = lower_s:find(lower_t, i, true) - if not startPos then - res = res .. s:sub(i) - break - end - res = res .. s:sub(i, startPos - 1) .. repl - i = endPos + 1 - end - return res -end - -local function StringContains(s, target) - if type(s) ~= "string" then return false end - return s:lower():find(target:lower(), 1, true) ~= nil -end - --- Backup original value -local function StoreOriginal(inst, prop, value) - if not StreamerMode.OriginalValues[inst] then - StreamerMode.OriginalValues[inst] = {} - end - -- Only save the first time to preserve the true original value - if StreamerMode.OriginalValues[inst][prop] == nil then - StreamerMode.OriginalValues[inst][prop] = value - end -end - --- Check and replace core logic -local function CheckAndReplaceCore(inst) - if not inst then return end - local foundMatch = false - - -- 1. Handle images - if inst:IsA("ImageLabel") or inst:IsA("ImageButton") then - if inst.Image and inst.Image ~= StreamerMode.ReplaceImage then - if string.find(inst.Image, UserId) then - -- Backup original value - StoreOriginal(inst, "Image", inst.Image) - -- Replace value - pcall(function() inst.Image = StreamerMode.ReplaceImage end) - foundMatch = true - end - end - end - - -- 2. Handle text - for _, prop in ipairs(StringProperties) do - local success, val = pcall(function() return inst[prop] end) - if success and type(val) == "string" and val ~= "" and val ~= StreamerMode.ReplaceText then - for _, target in ipairs(StreamerMode.Targets) do - if StringContains(val, target) then - -- Backup original value - StoreOriginal(inst, prop, val) - - -- Calculate new value - local newVal - if StreamerMode.ReplaceExact then - newVal = StreamerMode.ReplaceText - else - newVal = CI_Replace(val, target, StreamerMode.ReplaceText) - end - - -- Replace value - pcall(function() inst[prop] = newVal end) - foundMatch = true - end - end - end - end - - if foundMatch then - StreamerMode.WatchedObjects[inst] = true - end -end - --- Toggle character GUI visibility -local function ToggleCharacterGui(char, visible) - if not char then return end - -- Wait a moment for new character to load - if visible == false then task.wait(0.5) end - - for _, v in ipairs(char:GetDescendants()) do - if v:IsA("BillboardGui") or v:IsA("SurfaceGui") then - if visible == false then - -- Hide (backup enabled state) - StoreOriginal(v, "Enabled", v.Enabled) - v.Enabled = false - else - -- Restore (use backup if available, otherwise enable) - if StreamerMode.OriginalValues[v] and StreamerMode.OriginalValues[v]["Enabled"] ~= nil then - v.Enabled = StreamerMode.OriginalValues[v]["Enabled"] - else - v.Enabled = true - end - end - end - end -end - --- Start protection -function StreamerMode:Start() - self.Targets = GetTargets() - - -- 1. Handle character (hide GUI) - if LocalPlayer.Character then - task.spawn(function() ToggleCharacterGui(LocalPlayer.Character, false) end) - end - self.Connections["CharAdded"] = LocalPlayer.CharacterAdded:Connect(function(char) - task.spawn(function() ToggleCharacterGui(char, false) end) - end) - - self.Connections["DisplayName"] = LocalPlayer:GetPropertyChangedSignal("DisplayName"):Connect(function() - self.Targets = GetTargets() - end) - - -- 2. Scan existing CoreGui - for _, desc in ipairs(CoreGui:GetDescendants()) do - CheckAndReplaceCore(desc) - end - - -- 3. Detect new CoreGui elements - self.Connections["CoreAdded"] = CoreGui.DescendantAdded:Connect(function(desc) - task.delay(0.1, function() CheckAndReplaceCore(desc) end) - end) - - -- 4. Heartbeat loop (lock values) - self.Connections["Heartbeat"] = RunService.Heartbeat:Connect(function() - for inst, _ in pairs(self.WatchedObjects) do - if inst and inst.Parent then - CheckAndReplaceCore(inst) - else - self.WatchedObjects[inst] = nil - end - end - end) -end - --- Stop protection and restore -function StreamerMode:Stop() - -- 1. Disconnect all connections - for _, conn in pairs(self.Connections) do - conn:Disconnect() - end - self.Connections = {} - self.WatchedObjects = {} - - -- 2. Restore original values to all modified objects - for inst, props in pairs(self.OriginalValues) do - if inst and inst.Parent then - for propName, originalVal in pairs(props) do - pcall(function() - inst[propName] = originalVal - end) - end - end - end - - -- 3. Clear backup table - self.OriginalValues = {} - - -- 4. Show character GUI again - if LocalPlayer.Character then - ToggleCharacterGui(LocalPlayer.Character, true) - end -end - --- ==================== SAVE MANAGER SYSTEM ==================== -local SaveManager = {} - -SaveManager.FolderRoot = "ATGSettings" -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, - }, -} - --- Helper functions -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 placeId = getPlaceId() - return root .. "/" .. placeId -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 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: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 - -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) - - 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 - self:Load(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 option._isInCallback then - return - end - - option._isInCallback = true - - if originalCallback then - local success, err = pcall(originalCallback, ...) - if not success then - warn("Callback error for " .. tostring(idx) .. ": " .. tostring(err)) - end - end - - option._isInCallback = false - - if self.AutoSaveEnabled and self.AutoSaveConfig and not self.AutoSaveDebounce then - self.AutoSaveDebounce = true - task.spawn(function() - task.wait(1) - if self.AutoSaveEnabled and self.AutoSaveConfig then - self:Save(self.AutoSaveConfig) - end - self.AutoSaveDebounce = false - 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 - - local currentAutoload = self:GetAutoloadConfig() - local autoloadDesc = currentAutoload and ('Current: "' .. currentAutoload .. '"') or 'Will load "AutoSave.json" automatically on startup' - - local AutoloadToggle = section:AddToggle("SaveManager_AutoloadToggle", { - Title = "Auto Load", - Description = autoloadDesc, - 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, err = 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 - }) - - local autosaveDesc = self.AutoSaveConfig and ('Currently auto-saving to: "' .. tostring(self.AutoSaveConfig) .. '"') or 'Auto-save to "AutoSave.json"' - - local AutoSaveToggle = section:AddToggle("SaveManager_AutoSaveToggle", { - Title = "Auto Save", - Description = autosaveDesc, - 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", - "StreamerModeToggle" - }) - - if uiSettings then - if uiSettings.autoload_enabled then - task.spawn(function() - if isfile(getConfigFilePath(self, fixedConfigName)) then - SaveManager:Load(fixedConfigName) - if SaveManager.Options and SaveManager.Options.SaveManager_AutoloadToggle then - SaveManager.Options.SaveManager_AutoloadToggle:SetValue(true) - end - end - end) - end - - if uiSettings.autosave_enabled then - if isfile(getConfigFilePath(self, fixedConfigName)) then - self:EnableAutoSave(fixedConfigName) - if SaveManager.Options and SaveManager.Options.SaveManager_AutoSaveToggle then - SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(true) - end - end - end - end -end - -SaveManager:BuildFolderTree() - --- ==================== UI LOADER ==================== -local function LoadConfigSystem(Tabs, Library) - -- Streamer Mode Section - local StreamerSection = Tabs.Main:AddSection("Miscellaneous") - - local StreamerToggle = StreamerSection:AddToggle("StreamerModeToggle", { - Title = "Protect Name", - Description = "Protects your name while streaming", - Default = false - }) - - StreamerToggle:OnChanged(function() - if Library.Options.StreamerModeToggle.Value then - StreamerMode:Start() - else - StreamerMode:Stop() - end - end) - - Library.Options.StreamerModeToggle:SetValue(false) - - -- Configuration Manager Section - SaveManager:SetLibrary(Library) - SaveManager:BuildConfigSection(Tabs.Main) -end - --- Return loader function -return LoadConfigSystem +local SaveManager = {} do + SaveManager.FolderRoot = "ATGSettings" + 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 placeId = getPlaceId() + return root .. "/" .. placeId + end + + local function getConfigFilePath(self, name) + local folder = getConfigsFolder(self) + return folder .. "/" .. name .. ".json" + end + + -- ไฟล์สำหรับเซฟ UI ของ SaveManager เอง + local function getSaveManagerUIPath(self) + local folder = getConfigsFolder(self) + return folder .. "/savemanager_ui.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 + + -- เซฟ UI ของ SaveManager แยกต่างหาก + 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 + + -- โหลด UI ของ SaveManager + 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 + + 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) + 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 + self:Load(name) + end + end + + -- ฟังก์ชัน Auto Save + function SaveManager:EnableAutoSave(configName) + self.AutoSaveEnabled = true + self.AutoSaveConfig = configName + self:SaveUI() + + -- บันทึก callback เดิมและตั้ง callback ใหม่ + for idx, option in next, self.Options do + if not self.Ignore[idx] and self.Parser[option.Type] then + -- เก็บ callback เดิมไว้ถ้ายังไม่เคยเก็บ + if not self.OriginalCallbacks[idx] then + self.OriginalCallbacks[idx] = option.Callback + end + + -- สร้าง callback ใหม่ที่ป้องกัน stack overflow + local originalCallback = self.OriginalCallbacks[idx] + option.Callback = function(...) + -- ป้องกัน recursion ด้วยการใช้ flag + if option._isInCallback then + return + end + + option._isInCallback = true + + -- เรียก callback เดิม + if originalCallback then + local success, err = pcall(originalCallback, ...) + if not success then + warn("Callback error for " .. tostring(idx) .. ": " .. tostring(err)) + end + end + + option._isInCallback = false + + -- Auto save ด้วย debounce + if self.AutoSaveEnabled and self.AutoSaveConfig and not self.AutoSaveDebounce then + self.AutoSaveDebounce = true + task.spawn(function() + task.wait(1) -- รอ 1 วินาทีก่อนเซฟ + if self.AutoSaveEnabled and self.AutoSaveConfig then + self:Save(self.AutoSaveConfig) + end + self.AutoSaveDebounce = false + end) + end + end + end + end + end + + function SaveManager:DisableAutoSave() + self.AutoSaveEnabled = false + self.AutoSaveConfig = nil + self:SaveUI() + + -- คืนค่า callback เดิม + for idx, option in next, self.Options do + if self.OriginalCallbacks[idx] then + option.Callback = self.OriginalCallbacks[idx] + end + end + end + + -- สร้างส่วน UI แบบย่อ: เอา DropDown/Inputs/Buttons ออก เหลือแค่ Auto Load กับ Auto Save + function SaveManager:BuildConfigSection(tab) + assert(self.Library, "Must set SaveManager.Library") + + local section = tab:AddSection("📁 Configuration Manager") + + -- โหลด UI settings + local uiSettings = self:LoadUI() + + -- ensure AutoSave config file exists (ใช้ชื่อคงที่ "AutoSave") + local fixedConfigName = "AutoSave" + if not isfile(getConfigFilePath(self, fixedConfigName)) then + pcall(function() self:Save(fixedConfigName) end) + end + + -- Autoload Toggle (ใช้ไฟล์ AutoSave.json แบบคงที่) + local currentAutoload = self:GetAutoloadConfig() + local autoloadDesc = currentAutoload and ('ปัจจุบัน: "' .. currentAutoload .. '"') or 'จะโหลดไฟล์ "AutoSave.json" อัตโนมัติเมื่อเปิด' + + local AutoloadToggle = section:AddToggle("SaveManager_AutoloadToggle", { + Title = "🔄 Auto Load", + Description = autoloadDesc, + 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 + + -- ตั้ง autoload เป็น AutoSave + local ok, err = self:SetAutoloadConfig(fixedConfigName) + if not ok then + -- ถ้าตั้งค่าไม่สำเร็จ ให้รีเซ็ต toggle กลับเป็น false + if SaveManager.Options and SaveManager.Options.SaveManager_AutoloadToggle then + SaveManager.Options.SaveManager_AutoloadToggle:SetValue(false) + end + end + else + self:DisableAutoload() + end + end + }) + + -- Auto Save Toggle (บันทึกไปที่ AutoSave.json แบบคงที่) + local autosaveDesc = self.AutoSaveConfig and ('กำลังบันทึกอัตโนมัติไปที่: "' .. tostring(self.AutoSaveConfig) .. '"') or 'บันทึกอัตโนมัติไปที่ "AutoSave.json"' + + local AutoSaveToggle = section:AddToggle("SaveManager_AutoSaveToggle", { + Title = "💾 Auto Save", + Description = autosaveDesc, + 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 ไม่เซฟค่าของ Toggle ตัวจัดการนี้เอง + SaveManager:SetIgnoreIndexes({ + "SaveManager_AutoloadToggle", + "SaveManager_AutoSaveToggle" + }) + + -- โหลด UI settings และเปิดใช้ auto save / auto load ถ้าเคยเปิดไว้ + if uiSettings then + -- Auto Load + if uiSettings.autoload_enabled then + task.spawn(function() + -- พยายามโหลด AutoSave + if isfile(getConfigFilePath(self, fixedConfigName)) then + SaveManager:Load(fixedConfigName) + if SaveManager.Options and SaveManager.Options.SaveManager_AutoloadToggle then + SaveManager.Options.SaveManager_AutoloadToggle:SetValue(true) + end + end + end) + end + + -- Auto Save + if uiSettings.autosave_enabled then + if isfile(getConfigFilePath(self, fixedConfigName)) then + self:EnableAutoSave(fixedConfigName) + if SaveManager.Options and SaveManager.Options.SaveManager_AutoSaveToggle then + SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(true) + end + end + end + end + end + + SaveManager:BuildFolderTree() +end + +return SaveManager From f4de88174cb2ed44512eab10c0d1d5138c927059 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Tue, 2 Dec 2025 00:47:42 +0700 Subject: [PATCH 64/76] Update autosave.lua --- autosave.lua | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/autosave.lua b/autosave.lua index cd272b3..810781f 100644 --- a/autosave.lua +++ b/autosave.lua @@ -1,3 +1,4 @@ +---@diagnostic disable: undefined-global local httpService = game:GetService("HttpService") local Workspace = game:GetService("Workspace") @@ -399,7 +400,7 @@ local SaveManager = {} do function SaveManager:BuildConfigSection(tab) assert(self.Library, "Must set SaveManager.Library") - local section = tab:AddSection("📁 Configuration Manager") + local section = tab:AddSection("[ 📁 ] Configuration Manager") -- โหลด UI settings local uiSettings = self:LoadUI() @@ -412,11 +413,10 @@ local SaveManager = {} do -- Autoload Toggle (ใช้ไฟล์ AutoSave.json แบบคงที่) local currentAutoload = self:GetAutoloadConfig() - local autoloadDesc = currentAutoload and ('ปัจจุบัน: "' .. currentAutoload .. '"') or 'จะโหลดไฟล์ "AutoSave.json" อัตโนมัติเมื่อเปิด' - + local AutoloadToggle = section:AddToggle("SaveManager_AutoloadToggle", { - Title = "🔄 Auto Load", - Description = autoloadDesc, + Title = "Auto Load", + Description = "Auto Load Save", Default = (uiSettings and uiSettings.autoload_enabled) or false, Callback = function(value) if value then @@ -439,12 +439,9 @@ local SaveManager = {} do end }) - -- Auto Save Toggle (บันทึกไปที่ AutoSave.json แบบคงที่) - local autosaveDesc = self.AutoSaveConfig and ('กำลังบันทึกอัตโนมัติไปที่: "' .. tostring(self.AutoSaveConfig) .. '"') or 'บันทึกอัตโนมัติไปที่ "AutoSave.json"' - local AutoSaveToggle = section:AddToggle("SaveManager_AutoSaveToggle", { - Title = "💾 Auto Save", - Description = autosaveDesc, + Title = "Auto Save", + Description = "Auto Save When You Settings", Default = (uiSettings and uiSettings.autosave_enabled) or false, Callback = function(value) if value then From 0df3b5b390c3dcad0ba6e6a821c31bbd0a9bd376 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Tue, 2 Dec 2025 07:13:08 +0700 Subject: [PATCH 65/76] Update autosave.lua --- autosave.lua | 62 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/autosave.lua b/autosave.lua index 810781f..44217a5 100644 --- a/autosave.lua +++ b/autosave.lua @@ -10,6 +10,7 @@ local SaveManager = {} do SaveManager.AutoSaveConfig = nil SaveManager.AutoSaveDebounce = false SaveManager.OriginalCallbacks = {} + SaveManager.IsLoading = false -- เพิ่ม flag สำหรับป้องกัน callback ตอน load SaveManager.Parser = { Toggle = { Save = function(idx, object) @@ -223,6 +224,7 @@ local SaveManager = {} do return nil end + -- แก้ไขฟังก์ชัน Load ให้โหลดทีละตัว function SaveManager:Load(name) if (not name) then return false, "no config file is selected" @@ -234,11 +236,28 @@ local SaveManager = {} do 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) + -- เปิด flag IsLoading เพื่อป้องกัน auto save ระหว่างโหลด + self.IsLoading = true + + -- โหลดทีละตัวด้วย task.wait() เล็กน้อยระหว่างแต่ละตัว + task.spawn(function() + for i, option in ipairs(decoded.objects) do + if self.Parser[option.type] then + pcall(function() + self.Parser[option.type].Load(option.idx, option) + end) + end + + -- รอเล็กน้อยระหว่างแต่ละ option (ป้องกัน stack overflow) + if i % 5 == 0 then -- ทุก 5 options รอครั้งนึง + task.wait() + end end - end + + -- ปิด flag IsLoading หลังจากโหลดเสร็จ + task.wait(0.5) -- รอเพิ่มเล็กน้อยให้แน่ใจว่าทุกอย่างเสร็จ + self.IsLoading = false + end) return true end @@ -333,7 +352,7 @@ local SaveManager = {} do end end - -- ฟังก์ชัน Auto Save + -- ฟังก์ชัน Auto Save (แก้ไขให้ป้องกัน stack overflow) function SaveManager:EnableAutoSave(configName) self.AutoSaveEnabled = true self.AutoSaveConfig = configName @@ -350,6 +369,11 @@ local SaveManager = {} do -- สร้าง callback ใหม่ที่ป้องกัน stack overflow local originalCallback = self.OriginalCallbacks[idx] option.Callback = function(...) + -- ป้องกัน callback ตอน loading + if self.IsLoading then + return + end + -- ป้องกัน recursion ด้วยการใช้ flag if option._isInCallback then return @@ -367,12 +391,12 @@ local SaveManager = {} do option._isInCallback = false - -- Auto save ด้วย debounce - if self.AutoSaveEnabled and self.AutoSaveConfig and not self.AutoSaveDebounce then + -- Auto save ด้วย debounce (เพิ่มเงื่อนไขไม่ให้เซฟตอน loading) + if self.AutoSaveEnabled and self.AutoSaveConfig and not self.AutoSaveDebounce and not self.IsLoading then self.AutoSaveDebounce = true task.spawn(function() task.wait(1) -- รอ 1 วินาทีก่อนเซฟ - if self.AutoSaveEnabled and self.AutoSaveConfig then + if self.AutoSaveEnabled and self.AutoSaveConfig and not self.IsLoading then self:Save(self.AutoSaveConfig) end self.AutoSaveDebounce = false @@ -468,9 +492,16 @@ local SaveManager = {} do -- Auto Load if uiSettings.autoload_enabled then task.spawn(function() + -- รอให้ UI โหลดเสร็จก่อน + task.wait(1) + -- พยายามโหลด AutoSave if isfile(getConfigFilePath(self, fixedConfigName)) then SaveManager:Load(fixedConfigName) + + -- รอให้โหลดเสร็จก่อนอัพเดท toggle + task.wait(2) + if SaveManager.Options and SaveManager.Options.SaveManager_AutoloadToggle then SaveManager.Options.SaveManager_AutoloadToggle:SetValue(true) end @@ -480,12 +511,17 @@ local SaveManager = {} do -- Auto Save if uiSettings.autosave_enabled then - if isfile(getConfigFilePath(self, fixedConfigName)) then - self:EnableAutoSave(fixedConfigName) - if SaveManager.Options and SaveManager.Options.SaveManager_AutoSaveToggle then - SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(true) + task.spawn(function() + -- รอให้โหลดเสร็จก่อนเปิด auto save + task.wait(3) + + if isfile(getConfigFilePath(self, fixedConfigName)) then + self:EnableAutoSave(fixedConfigName) + if SaveManager.Options and SaveManager.Options.SaveManager_AutoSaveToggle then + SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(true) + end end - end + end) end end end From a701e45d70d32c961d18fe8cdc48747201a8307d Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Tue, 2 Dec 2025 07:30:21 +0700 Subject: [PATCH 66/76] Update autosave.lua --- autosave.lua | 208 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 138 insertions(+), 70 deletions(-) diff --git a/autosave.lua b/autosave.lua index 44217a5..0703bd3 100644 --- a/autosave.lua +++ b/autosave.lua @@ -10,7 +10,10 @@ local SaveManager = {} do SaveManager.AutoSaveConfig = nil SaveManager.AutoSaveDebounce = false SaveManager.OriginalCallbacks = {} - SaveManager.IsLoading = false -- เพิ่ม flag สำหรับป้องกัน callback ตอน load + SaveManager.IsLoading = false + SaveManager.IsSaving = false -- เพิ่ม flag สำหรับป้องกันการเซฟซ้ำ + SaveManager.CallbackLocks = {} -- เก็บ lock สำหรับแต่ละ option + SaveManager.Parser = { Toggle = { Save = function(idx, object) @@ -107,7 +110,6 @@ local SaveManager = {} do return folder .. "/" .. name .. ".json" end - -- ไฟล์สำหรับเซฟ UI ของ SaveManager เอง local function getSaveManagerUIPath(self) local folder = getConfigsFolder(self) return folder .. "/savemanager_ui.json" @@ -173,17 +175,32 @@ local SaveManager = {} do return false, "no config file is selected" end + -- ป้องกันการเซฟซ้อนกัน + if self.IsSaving then + return false, "already saving" + end + + self.IsSaving = true + 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)) + + local success, result = pcall(function() + return self.Parser[option.Type].Save(idx, option) + end) + + if success and result then + table.insert(data.objects, result) + end end local success, encoded = pcall(httpService.JSONEncode, httpService, data) if not success then + self.IsSaving = false return false, "failed to encode data" end @@ -191,10 +208,11 @@ local SaveManager = {} do if folder then ensureFolder(folder) end writefile(fullPath, encoded) + + self.IsSaving = false return true end - -- เซฟ UI ของ SaveManager แยกต่างหาก function SaveManager:SaveUI() local uiPath = getSaveManagerUIPath(self) local uiData = { @@ -212,7 +230,6 @@ local SaveManager = {} do end end - -- โหลด UI ของ SaveManager function SaveManager:LoadUI() local uiPath = getSaveManagerUIPath(self) if not isfile(uiPath) then return nil end @@ -224,7 +241,29 @@ local SaveManager = {} do return nil end - -- แก้ไขฟังก์ชัน Load ให้โหลดทีละตัว + -- ปิด callback ทั้งหมดชั่วคราว + function SaveManager:DisableAllCallbacks() + for idx, option in next, self.Options do + if option.Callback then + -- เก็บ callback เดิม + if not self.OriginalCallbacks[idx] then + self.OriginalCallbacks[idx] = option.Callback + end + -- ตั้งเป็น function ว่าง + option.Callback = function() end + end + end + end + + -- เปิด callback ทั้งหมดกลับมา + function SaveManager:EnableAllCallbacks() + for idx, option in next, self.Options do + if self.OriginalCallbacks[idx] then + option.Callback = self.OriginalCallbacks[idx] + end + end + end + function SaveManager:Load(name) if (not name) then return false, "no config file is selected" @@ -236,26 +275,43 @@ local SaveManager = {} do local success, decoded = pcall(httpService.JSONDecode, httpService, readfile(file)) if not success then return false, "decode error" end - -- เปิด flag IsLoading เพื่อป้องกัน auto save ระหว่างโหลด + -- เปิด flag IsLoading self.IsLoading = true - -- โหลดทีละตัวด้วย task.wait() เล็กน้อยระหว่างแต่ละตัว + -- ปิด callback ทั้งหมดก่อนโหลด + self:DisableAllCallbacks() + + -- โหลดข้อมูลทีละตัว task.spawn(function() for i, option in ipairs(decoded.objects) do if self.Parser[option.type] then - pcall(function() + local success, err = pcall(function() self.Parser[option.type].Load(option.idx, option) end) + + if not success then + warn("Load error for " .. tostring(option.idx) .. ": " .. tostring(err)) + end end - -- รอเล็กน้อยระหว่างแต่ละ option (ป้องกัน stack overflow) - if i % 5 == 0 then -- ทุก 5 options รอครั้งนึง - task.wait() + -- รอทุก 3 options + if i % 3 == 0 then + task.wait(0.05) -- รอสั้นๆ end end - -- ปิด flag IsLoading หลังจากโหลดเสร็จ - task.wait(0.5) -- รอเพิ่มเล็กน้อยให้แน่ใจว่าทุกอย่างเสร็จ + -- รอให้ทุกอย่างเสร็จสมบูรณ์ + task.wait(1) + + -- เปิด callback กลับมา + self:EnableAllCallbacks() + + -- ถ้ามี auto save ให้ตั้งค่า callback ใหม่ + if self.AutoSaveEnabled and self.AutoSaveConfig then + self:SetupAutoSaveCallbacks() + end + + -- ปิด flag IsLoading self.IsLoading = false end) @@ -274,7 +330,6 @@ local SaveManager = {} do delfile(file) - -- ถ้าเป็น autoload ให้ลบ autoload ด้วย local autopath = getConfigsFolder(self) .. "/autoload.txt" if isfile(autopath) then local currentAutoload = readfile(autopath) @@ -352,61 +407,79 @@ local SaveManager = {} do end end - -- ฟังก์ชัน Auto Save (แก้ไขให้ป้องกัน stack overflow) - function SaveManager:EnableAutoSave(configName) - self.AutoSaveEnabled = true - self.AutoSaveConfig = configName - self:SaveUI() - - -- บันทึก callback เดิมและตั้ง callback ใหม่ + -- ตั้งค่า callback สำหรับ auto save แยกออกมาเป็นฟังก์ชันต่างหาก + function SaveManager:SetupAutoSaveCallbacks() for idx, option in next, self.Options do if not self.Ignore[idx] and self.Parser[option.Type] then - -- เก็บ callback เดิมไว้ถ้ายังไม่เคยเก็บ + -- เก็บ callback เดิมถ้ายังไม่เคยเก็บ if not self.OriginalCallbacks[idx] then self.OriginalCallbacks[idx] = option.Callback end - -- สร้าง callback ใหม่ที่ป้องกัน stack overflow local originalCallback = self.OriginalCallbacks[idx] + + -- สร้าง wrapper callback ที่ปลอดภัย option.Callback = function(...) - -- ป้องกัน callback ตอน loading - if self.IsLoading then + local args = {...} + + -- ป้องกันการเรียกซ้ำ + if self.CallbackLocks[idx] then return end - - -- ป้องกัน recursion ด้วยการใช้ flag - if option._isInCallback then + + -- ไม่เรียก callback ตอน loading หรือ saving + if self.IsLoading or self.IsSaving then return end - option._isInCallback = true - - -- เรียก callback เดิม - if originalCallback then - local success, err = pcall(originalCallback, ...) - if not success then - warn("Callback error for " .. tostring(idx) .. ": " .. tostring(err)) - end - end - - option._isInCallback = false - - -- Auto save ด้วย debounce (เพิ่มเงื่อนไขไม่ให้เซฟตอน loading) - if self.AutoSaveEnabled and self.AutoSaveConfig and not self.AutoSaveDebounce and not self.IsLoading then - self.AutoSaveDebounce = true - task.spawn(function() - task.wait(1) -- รอ 1 วินาทีก่อนเซฟ - if self.AutoSaveEnabled and self.AutoSaveConfig and not self.IsLoading then - self:Save(self.AutoSaveConfig) + self.CallbackLocks[idx] = true + + -- เรียก original callback ใน coroutine แยก + task.spawn(function() + if originalCallback then + local success, err = pcall(function() + originalCallback(table.unpack(args)) + end) + + if not success then + warn("Callback error for " .. tostring(idx) .. ": " .. tostring(err)) end - self.AutoSaveDebounce = false - end) - end + end + + -- รอให้ callback เสร็จก่อน unlock + task.wait(0.1) + self.CallbackLocks[idx] = false + + -- Auto save หลังจาก callback เสร็จ + if self.AutoSaveEnabled and self.AutoSaveConfig and not self.AutoSaveDebounce and not self.IsLoading and not self.IsSaving then + self.AutoSaveDebounce = true + + task.spawn(function() + task.wait(1.5) -- รอนานขึ้นเพื่อความปลอดภัย + + if self.AutoSaveEnabled and self.AutoSaveConfig and not self.IsLoading and not self.IsSaving then + self:Save(self.AutoSaveConfig) + end + + task.wait(0.5) + self.AutoSaveDebounce = false + end) + end + end) end end end end + function SaveManager:EnableAutoSave(configName) + self.AutoSaveEnabled = true + self.AutoSaveConfig = configName + self:SaveUI() + + -- ตั้งค่า callback + self:SetupAutoSaveCallbacks() + end + function SaveManager:DisableAutoSave() self.AutoSaveEnabled = false self.AutoSaveConfig = nil @@ -418,24 +491,23 @@ local SaveManager = {} do option.Callback = self.OriginalCallbacks[idx] end end + + -- ล้าง locks + self.CallbackLocks = {} end - -- สร้างส่วน UI แบบย่อ: เอา DropDown/Inputs/Buttons ออก เหลือแค่ Auto Load กับ Auto Save function SaveManager:BuildConfigSection(tab) assert(self.Library, "Must set SaveManager.Library") local section = tab:AddSection("[ 📁 ] Configuration Manager") - -- โหลด UI settings local uiSettings = self:LoadUI() - -- ensure AutoSave config file exists (ใช้ชื่อคงที่ "AutoSave") local fixedConfigName = "AutoSave" if not isfile(getConfigFilePath(self, fixedConfigName)) then pcall(function() self:Save(fixedConfigName) end) end - -- Autoload Toggle (ใช้ไฟล์ AutoSave.json แบบคงที่) local currentAutoload = self:GetAutoloadConfig() local AutoloadToggle = section:AddToggle("SaveManager_AutoloadToggle", { @@ -444,15 +516,12 @@ local SaveManager = {} do 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 - -- ตั้ง autoload เป็น AutoSave local ok, err = self:SetAutoloadConfig(fixedConfigName) if not ok then - -- ถ้าตั้งค่าไม่สำเร็จ ให้รีเซ็ต toggle กลับเป็น false if SaveManager.Options and SaveManager.Options.SaveManager_AutoloadToggle then SaveManager.Options.SaveManager_AutoloadToggle:SetValue(false) end @@ -469,7 +538,6 @@ local SaveManager = {} do 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 @@ -481,29 +549,27 @@ local SaveManager = {} do end }) - -- ตั้งให้ SaveManager ไม่เซฟค่าของ Toggle ตัวจัดการนี้เอง SaveManager:SetIgnoreIndexes({ "SaveManager_AutoloadToggle", "SaveManager_AutoSaveToggle" }) - -- โหลด UI settings และเปิดใช้ auto save / auto load ถ้าเคยเปิดไว้ if uiSettings then -- Auto Load if uiSettings.autoload_enabled then task.spawn(function() - -- รอให้ UI โหลดเสร็จก่อน - task.wait(1) + task.wait(2) - -- พยายามโหลด AutoSave if isfile(getConfigFilePath(self, fixedConfigName)) then SaveManager:Load(fixedConfigName) - -- รอให้โหลดเสร็จก่อนอัพเดท toggle - task.wait(2) + task.wait(3) if SaveManager.Options and SaveManager.Options.SaveManager_AutoloadToggle then - SaveManager.Options.SaveManager_AutoloadToggle:SetValue(true) + -- ใช้ pcall ป้องกัน error + pcall(function() + SaveManager.Options.SaveManager_AutoloadToggle:SetValue(true) + end) end end end) @@ -512,13 +578,15 @@ local SaveManager = {} do -- Auto Save if uiSettings.autosave_enabled then task.spawn(function() - -- รอให้โหลดเสร็จก่อนเปิด auto save - task.wait(3) + task.wait(5) -- รอนานกว่าเดิม if isfile(getConfigFilePath(self, fixedConfigName)) then self:EnableAutoSave(fixedConfigName) + if SaveManager.Options and SaveManager.Options.SaveManager_AutoSaveToggle then - SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(true) + pcall(function() + SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(true) + end) end end end) From e6c308285f2caf2c10e6a3ba1f36e69c352e6182 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Wed, 3 Dec 2025 01:29:56 +0700 Subject: [PATCH 67/76] Update autosave.lua --- autosave.lua | 285 +++++++++++++++++++-------------------------------- 1 file changed, 105 insertions(+), 180 deletions(-) diff --git a/autosave.lua b/autosave.lua index 0703bd3..f900d2c 100644 --- a/autosave.lua +++ b/autosave.lua @@ -10,10 +10,6 @@ local SaveManager = {} do SaveManager.AutoSaveConfig = nil SaveManager.AutoSaveDebounce = false SaveManager.OriginalCallbacks = {} - SaveManager.IsLoading = false - SaveManager.IsSaving = false -- เพิ่ม flag สำหรับป้องกันการเซฟซ้ำ - SaveManager.CallbackLocks = {} -- เก็บ lock สำหรับแต่ละ option - SaveManager.Parser = { Toggle = { Save = function(idx, object) @@ -110,6 +106,7 @@ local SaveManager = {} do return folder .. "/" .. name .. ".json" end + -- ไฟล์สำหรับเซฟ UI ของ SaveManager เอง local function getSaveManagerUIPath(self) local folder = getConfigsFolder(self) return folder .. "/savemanager_ui.json" @@ -123,35 +120,37 @@ local SaveManager = {} do 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) + -- Migrate legacy configs (ทำในพื้นหลังเพื่อไม่ให้ค้าง) + task.spawn(function() + 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 - 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) + 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 + end) end function SaveManager:SetIgnoreIndexes(list) @@ -175,32 +174,17 @@ local SaveManager = {} do return false, "no config file is selected" end - -- ป้องกันการเซฟซ้อนกัน - if self.IsSaving then - return false, "already saving" - end - - self.IsSaving = true - 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 - - local success, result = pcall(function() - return self.Parser[option.Type].Save(idx, option) - end) - - if success and result then - table.insert(data.objects, result) - 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 - self.IsSaving = false return false, "failed to encode data" end @@ -208,11 +192,10 @@ local SaveManager = {} do if folder then ensureFolder(folder) end writefile(fullPath, encoded) - - self.IsSaving = false return true end + -- เซฟ UI ของ SaveManager แยกต่างหาก function SaveManager:SaveUI() local uiPath = getSaveManagerUIPath(self) local uiData = { @@ -230,6 +213,7 @@ local SaveManager = {} do end end + -- โหลด UI ของ SaveManager function SaveManager:LoadUI() local uiPath = getSaveManagerUIPath(self) if not isfile(uiPath) then return nil end @@ -241,29 +225,6 @@ local SaveManager = {} do return nil end - -- ปิด callback ทั้งหมดชั่วคราว - function SaveManager:DisableAllCallbacks() - for idx, option in next, self.Options do - if option.Callback then - -- เก็บ callback เดิม - if not self.OriginalCallbacks[idx] then - self.OriginalCallbacks[idx] = option.Callback - end - -- ตั้งเป็น function ว่าง - option.Callback = function() end - end - end - end - - -- เปิด callback ทั้งหมดกลับมา - function SaveManager:EnableAllCallbacks() - for idx, option in next, self.Options do - if self.OriginalCallbacks[idx] then - option.Callback = self.OriginalCallbacks[idx] - end - end - end - function SaveManager:Load(name) if (not name) then return false, "no config file is selected" @@ -275,45 +236,20 @@ local SaveManager = {} do local success, decoded = pcall(httpService.JSONDecode, httpService, readfile(file)) if not success then return false, "decode error" end - -- เปิด flag IsLoading - self.IsLoading = true - - -- ปิด callback ทั้งหมดก่อนโหลด - self:DisableAllCallbacks() - - -- โหลดข้อมูลทีละตัว - task.spawn(function() - for i, option in ipairs(decoded.objects) do - if self.Parser[option.type] then - local success, err = pcall(function() - self.Parser[option.type].Load(option.idx, option) - end) - - if not success then - warn("Load error for " .. tostring(option.idx) .. ": " .. tostring(err)) - end - end - - -- รอทุก 3 options - if i % 3 == 0 then - task.wait(0.05) -- รอสั้นๆ + -- โหลดแบบ batch เพื่อลด overhead + local loadCount = 0 + for _, option in next, decoded.objects do + if self.Parser[option.type] then + local parser = self.Parser[option.type] + pcall(parser.Load, option.idx, option) + loadCount = loadCount + 1 + + -- yield ทุกๆ 10 options เพื่อไม่ให้ค้าง + if loadCount % 10 == 0 then + task.wait() end end - - -- รอให้ทุกอย่างเสร็จสมบูรณ์ - task.wait(1) - - -- เปิด callback กลับมา - self:EnableAllCallbacks() - - -- ถ้ามี auto save ให้ตั้งค่า callback ใหม่ - if self.AutoSaveEnabled and self.AutoSaveConfig then - self:SetupAutoSaveCallbacks() - end - - -- ปิด flag IsLoading - self.IsLoading = false - end) + end return true end @@ -330,6 +266,7 @@ local SaveManager = {} do delfile(file) + -- ถ้าเป็น autoload ให้ลบ autoload ด้วย local autopath = getConfigsFolder(self) .. "/autoload.txt" if isfile(autopath) then local currentAutoload = readfile(autopath) @@ -381,11 +318,22 @@ local SaveManager = {} do }) end + -- Cache config list เพื่อความเร็ว + SaveManager._configListCache = nil + SaveManager._configListCacheTime = 0 + function SaveManager:RefreshConfigList() local folder = getConfigsFolder(self) if not isfolder(folder) then return {} end + + -- ใช้ cache ถ้าเรียกภายใน 1 วินาที + local now = os.clock() + if self._configListCache and (now - self._configListCacheTime) < 1 then + return self._configListCache + end + local list = listfiles(folder) local out = {} for i = 1, #list do @@ -397,6 +345,9 @@ local SaveManager = {} do end end end + + self._configListCache = out + self._configListCacheTime = now return out end @@ -407,79 +358,56 @@ local SaveManager = {} do end end - -- ตั้งค่า callback สำหรับ auto save แยกออกมาเป็นฟังก์ชันต่างหาก - function SaveManager:SetupAutoSaveCallbacks() + -- ฟังก์ชัน Auto Save + function SaveManager:EnableAutoSave(configName) + self.AutoSaveEnabled = true + self.AutoSaveConfig = configName + self:SaveUI() + + -- บันทึก callback เดิมและตั้ง callback ใหม่ for idx, option in next, self.Options do if not self.Ignore[idx] and self.Parser[option.Type] then - -- เก็บ callback เดิมถ้ายังไม่เคยเก็บ + -- เก็บ callback เดิมไว้ถ้ายังไม่เคยเก็บ if not self.OriginalCallbacks[idx] then self.OriginalCallbacks[idx] = option.Callback end + -- สร้าง callback ใหม่ที่ป้องกัน stack overflow local originalCallback = self.OriginalCallbacks[idx] - - -- สร้าง wrapper callback ที่ปลอดภัย option.Callback = function(...) - local args = {...} - - -- ป้องกันการเรียกซ้ำ - if self.CallbackLocks[idx] then + -- ป้องกัน recursion ด้วยการใช้ flag + if option._isInCallback then return end - - -- ไม่เรียก callback ตอน loading หรือ saving - if self.IsLoading or self.IsSaving then - return + + option._isInCallback = true + + -- เรียก callback เดิม + if originalCallback then + local success, err = pcall(originalCallback, ...) + if not success then + warn("Callback error for " .. tostring(idx) .. ": " .. tostring(err)) + end end - self.CallbackLocks[idx] = true - - -- เรียก original callback ใน coroutine แยก - task.spawn(function() - if originalCallback then - local success, err = pcall(function() - originalCallback(table.unpack(args)) - end) - - if not success then - warn("Callback error for " .. tostring(idx) .. ": " .. tostring(err)) + option._isInCallback = false + + -- Auto save ด้วย debounce + if self.AutoSaveEnabled and self.AutoSaveConfig and not self.AutoSaveDebounce then + self.AutoSaveDebounce = true + task.spawn(function() + task.wait(1) -- รอ 1 วินาทีก่อนเซฟ + if self.AutoSaveEnabled and self.AutoSaveConfig then + self:Save(self.AutoSaveConfig) end - end - - -- รอให้ callback เสร็จก่อน unlock - task.wait(0.1) - self.CallbackLocks[idx] = false - - -- Auto save หลังจาก callback เสร็จ - if self.AutoSaveEnabled and self.AutoSaveConfig and not self.AutoSaveDebounce and not self.IsLoading and not self.IsSaving then - self.AutoSaveDebounce = true - - task.spawn(function() - task.wait(1.5) -- รอนานขึ้นเพื่อความปลอดภัย - - if self.AutoSaveEnabled and self.AutoSaveConfig and not self.IsLoading and not self.IsSaving then - self:Save(self.AutoSaveConfig) - end - - task.wait(0.5) - self.AutoSaveDebounce = false - end) - end - end) + self.AutoSaveDebounce = false + end) + end end end end end - function SaveManager:EnableAutoSave(configName) - self.AutoSaveEnabled = true - self.AutoSaveConfig = configName - self:SaveUI() - - -- ตั้งค่า callback - self:SetupAutoSaveCallbacks() - end - function SaveManager:DisableAutoSave() self.AutoSaveEnabled = false self.AutoSaveConfig = nil @@ -491,23 +419,24 @@ local SaveManager = {} do option.Callback = self.OriginalCallbacks[idx] end end - - -- ล้าง locks - self.CallbackLocks = {} end + -- สร้างส่วน UI แบบย่อ: เอา DropDown/Inputs/Buttons ออก เหลือแค่ Auto Load กับ Auto Save function SaveManager:BuildConfigSection(tab) assert(self.Library, "Must set SaveManager.Library") local section = tab:AddSection("[ 📁 ] Configuration Manager") + -- โหลด UI settings local uiSettings = self:LoadUI() + -- ensure AutoSave config file exists (ใช้ชื่อคงที่ "AutoSave") local fixedConfigName = "AutoSave" if not isfile(getConfigFilePath(self, fixedConfigName)) then pcall(function() self:Save(fixedConfigName) end) end + -- Autoload Toggle (ใช้ไฟล์ AutoSave.json แบบคงที่) local currentAutoload = self:GetAutoloadConfig() local AutoloadToggle = section:AddToggle("SaveManager_AutoloadToggle", { @@ -516,12 +445,15 @@ local SaveManager = {} do 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 + -- ตั้ง autoload เป็น AutoSave local ok, err = self:SetAutoloadConfig(fixedConfigName) if not ok then + -- ถ้าตั้งค่าไม่สำเร็จ ให้รีเซ็ต toggle กลับเป็น false if SaveManager.Options and SaveManager.Options.SaveManager_AutoloadToggle then SaveManager.Options.SaveManager_AutoloadToggle:SetValue(false) end @@ -538,6 +470,7 @@ local SaveManager = {} do 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 @@ -549,27 +482,23 @@ local SaveManager = {} do end }) + -- ตั้งให้ SaveManager ไม่เซฟค่าของ Toggle ตัวจัดการนี้เอง SaveManager:SetIgnoreIndexes({ "SaveManager_AutoloadToggle", "SaveManager_AutoSaveToggle" }) + -- โหลด UI settings และเปิดใช้ auto save / auto load ถ้าเคยเปิดไว้ if uiSettings then - -- Auto Load + -- Auto Load (ใช้ defer เพื่อไม่ให้ block) if uiSettings.autoload_enabled then - task.spawn(function() - task.wait(2) - + task.defer(function() + -- พยายามโหลด AutoSave if isfile(getConfigFilePath(self, fixedConfigName)) then SaveManager:Load(fixedConfigName) - - task.wait(3) - + task.wait(0.1) -- รอให้ UI พร้อม if SaveManager.Options and SaveManager.Options.SaveManager_AutoloadToggle then - -- ใช้ pcall ป้องกัน error - pcall(function() - SaveManager.Options.SaveManager_AutoloadToggle:SetValue(true) - end) + SaveManager.Options.SaveManager_AutoloadToggle:SetValue(true) end end end) @@ -577,16 +506,12 @@ local SaveManager = {} do -- Auto Save if uiSettings.autosave_enabled then - task.spawn(function() - task.wait(5) -- รอนานกว่าเดิม - + task.defer(function() if isfile(getConfigFilePath(self, fixedConfigName)) then self:EnableAutoSave(fixedConfigName) - + task.wait(0.1) -- รอให้ UI พร้อม if SaveManager.Options and SaveManager.Options.SaveManager_AutoSaveToggle then - pcall(function() - SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(true) - end) + SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(true) end end end) From d02f91614e9e62dcb64ff0ff3bb6d917fe856729 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Tue, 9 Dec 2025 12:10:33 +0700 Subject: [PATCH 68/76] Update InterfaceManager.lua --- InterfaceManager.lua | 105 +++++++++++-------------------------------- 1 file changed, 25 insertions(+), 80 deletions(-) diff --git a/InterfaceManager.lua b/InterfaceManager.lua index 3814a47..672bf93 100644 --- a/InterfaceManager.lua +++ b/InterfaceManager.lua @@ -1,10 +1,5 @@ local httpService = game:GetService("HttpService") --- ═══════════════════════════════════════════════════════════════ --- Load Language System --- ═══════════════════════════════════════════════════════════════ -local Lang = loadstring(game:HttpGet("https://raw.githubusercontent.com/ATGFAIL/ATGHub/main/Languages/LanguageSystem.lua"))() - local InterfaceManager = {} do InterfaceManager.Folder = "FluentSettings" InterfaceManager.Settings = { @@ -12,7 +7,7 @@ local InterfaceManager = {} do Acrylic = true, Transparency = true, MenuKeybind = "LeftControl", - Language = "EN" -- Added Language setting + Language = "en" } function InterfaceManager:SetFolder(folder) @@ -57,11 +52,6 @@ local InterfaceManager = {} do for i, v in next, decoded do InterfaceManager.Settings[i] = v end - - -- Apply loaded language - if InterfaceManager.Settings.Language then - Lang:SetLanguage(InterfaceManager.Settings.Language) - end end end end @@ -73,63 +63,10 @@ local InterfaceManager = {} do InterfaceManager:LoadSettings() - local section = tab:AddSection(Lang:Get("tab_settings")) - - -- ═══════════════════════════════════════════════════════════ - -- Language Selector - -- ═══════════════════════════════════════════════════════════ - local languageNames = {} - local languageCodes = {} - local availableLanguages = Lang:GetAvailableLanguagesWithNames() - - for _, langData in ipairs(availableLanguages) do - table.insert(languageNames, langData.name) - languageCodes[langData.name] = langData.code - end - - local currentLangName = Lang:GetCurrentLanguageName() - for _, langData in ipairs(availableLanguages) do - if langData.code == Settings.Language then - currentLangName = langData.name - break - end - end - - local LanguageDropdown = section:AddDropdown("InterfaceLanguage", { - Title = Lang:Get("language"), - Description = Lang:Get("language_changed_desc"), - Values = languageNames, - Default = currentLangName, - Callback = function(Value) - local langCode = languageCodes[Value] - if langCode then - Lang:SetLanguage(langCode) - Settings.Language = langCode - InterfaceManager:SaveSettings() - - -- Notify user - Library:Notify({ - Title = Lang:Get("language_changed"), - Content = Lang:Get("language_changed_desc"), - Duration = 3 - }) - - -- Reload UI after short delay - task.wait(1) - Library:Notify({ - Title = Lang:Get("notification_info"), - Content = "UI will reload to apply language...", - Duration = 2 - }) - end - end - }) + local section = tab:AddSection("Interface") - -- ═══════════════════════════════════════════════════════════ - -- Theme Selector - -- ═══════════════════════════════════════════════════════════ local InterfaceTheme = section:AddDropdown("InterfaceTheme", { - Title = Lang:Get("tab_settings"), + Title = "Theme", Description = "Changes the interface theme.", Values = Library.Themes, Default = Settings.Theme, @@ -141,10 +78,27 @@ local InterfaceManager = {} do }) InterfaceTheme:SetValue(Settings.Theme) - - -- ═══════════════════════════════════════════════════════════ - -- Acrylic Toggle - -- ═══════════════════════════════════════════════════════════ + + -- Language Dropdown + 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 + if Library.UseAcrylic then section:AddToggle("AcrylicToggle", { Title = "Acrylic", @@ -158,9 +112,6 @@ local InterfaceManager = {} do }) end - -- ═══════════════════════════════════════════════════════════ - -- Transparency Toggle - -- ═══════════════════════════════════════════════════════════ section:AddToggle("TransparentToggle", { Title = "Transparency", Description = "Makes the interface transparent.", @@ -172,13 +123,7 @@ local InterfaceManager = {} do end }) - -- ═══════════════════════════════════════════════════════════ - -- Menu Keybind - -- ═══════════════════════════════════════════════════════════ - local MenuKeybind = section:AddKeybind("MenuKeybind", { - Title = "Minimize Bind", - Default = Settings.MenuKeybind - }) + local MenuKeybind = section:AddKeybind("MenuKeybind", { Title = "Minimize Bind", Default = Settings.MenuKeybind }) MenuKeybind:OnChanged(function() Settings.MenuKeybind = MenuKeybind.Value InterfaceManager:SaveSettings() From 44435b20d166432245b246450517c4a3d6f089c5 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Tue, 9 Dec 2025 12:27:00 +0700 Subject: [PATCH 69/76] Update InterfaceManager.lua --- InterfaceManager.lua | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/InterfaceManager.lua b/InterfaceManager.lua index 672bf93..0d85885 100644 --- a/InterfaceManager.lua +++ b/InterfaceManager.lua @@ -79,24 +79,26 @@ local InterfaceManager = {} do InterfaceTheme:SetValue(Settings.Theme) - -- Language Dropdown - 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 - }) + -- 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()) + -- 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 From 78ae345da0d5a90418cb41ab8436344bde6c2ce1 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 11 Dec 2025 12:43:00 +0700 Subject: [PATCH 70/76] Update autosave.lua --- autosave.lua | 473 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 288 insertions(+), 185 deletions(-) diff --git a/autosave.lua b/autosave.lua index f900d2c..9c6744c 100644 --- a/autosave.lua +++ b/autosave.lua @@ -1,6 +1,5 @@ ---@diagnostic disable: undefined-global local httpService = game:GetService("HttpService") -local Workspace = game:GetService("Workspace") local SaveManager = {} do SaveManager.FolderRoot = "ATGSettings" @@ -10,9 +9,15 @@ local SaveManager = {} do SaveManager.AutoSaveConfig = nil SaveManager.AutoSaveDebounce = false SaveManager.OriginalCallbacks = {} + SaveManager.DefaultValues = {} + SaveManager._hookedTabs = {} SaveManager.Parser = { Toggle = { Save = function(idx, object) + local defaultValue = SaveManager.DefaultValues[idx] + if defaultValue ~= nil and defaultValue == object.Value then + return nil + end return { type = "Toggle", idx = idx, value = object.Value } end, Load = function(idx, data) @@ -23,16 +28,47 @@ local SaveManager = {} do }, Slider = { Save = function(idx, object) + local defaultValue = SaveManager.DefaultValues[idx] + if defaultValue ~= nil and tonumber(defaultValue) == tonumber(object.Value) then + return nil + end 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) + SaveManager.Options[idx]:SetValue(tonumber(data.value)) end end, }, Dropdown = { Save = function(idx, object) + local defaultValue = SaveManager.DefaultValues[idx] + + if object.Multi then + if defaultValue ~= nil then + local isDefault = true + for k, v in pairs(object.Value) do + if defaultValue[k] ~= v then + isDefault = false + break + end + end + if isDefault then + for k, v in pairs(defaultValue) do + if object.Value[k] ~= v then + isDefault = false + break + end + end + end + if isDefault then return nil end + end + else + if defaultValue ~= nil and defaultValue == object.Value then + return nil + end + end + return { type = "Dropdown", idx = idx, value = object.Value, multi = object.Multi } end, Load = function(idx, data) @@ -43,7 +79,20 @@ local SaveManager = {} do }, Colorpicker = { Save = function(idx, object) - return { type = "Colorpicker", idx = idx, value = object.Value:ToHex(), transparency = object.Transparency } + local hexValue = object.Value:ToHex() + local defaultValue = SaveManager.DefaultValues[idx] + local defaultTransparency = SaveManager.DefaultValues[idx .. "_transparency"] + + if defaultValue ~= nil then + local defaultHex = defaultValue:ToHex() + local currentTransparency = object.Transparency or 0 + local defTrans = defaultTransparency or 0 + + if defaultHex == hexValue and currentTransparency == defTrans then + return nil + end + end + return { type = "Colorpicker", idx = idx, value = hexValue, transparency = object.Transparency or 0 } end, Load = function(idx, data) if SaveManager.Options[idx] then @@ -53,6 +102,11 @@ local SaveManager = {} do }, Keybind = { Save = function(idx, object) + local defaultValue = SaveManager.DefaultValues[idx] + local defaultMode = SaveManager.DefaultValues[idx .. "_mode"] + if defaultValue ~= nil and defaultValue == object.Value and defaultMode == object.Mode then + return nil + end return { type = "Keybind", idx = idx, mode = object.Mode, key = object.Value } end, Load = function(idx, data) @@ -61,9 +115,12 @@ local SaveManager = {} do end end, }, - Input = { Save = function(idx, object) + local defaultValue = SaveManager.DefaultValues[idx] + if defaultValue ~= nil and defaultValue == object.Value then + return nil + end return { type = "Input", idx = idx, text = object.Value } end, Load = function(idx, data) @@ -74,7 +131,6 @@ local SaveManager = {} do }, } - -- helpers local function sanitizeFilename(name) name = tostring(name or "") name = name:gsub("%s+", "_") @@ -90,67 +146,24 @@ local SaveManager = {} do end local function ensureFolder(path) - if not isfolder(path) then - makefolder(path) - end + if not isfolder(path) then makefolder(path) end end local function getConfigsFolder(self) - local root = self.FolderRoot - local placeId = getPlaceId() - return root .. "/" .. placeId + return self.FolderRoot .. "/" .. getPlaceId() end local function getConfigFilePath(self, name) - local folder = getConfigsFolder(self) - return folder .. "/" .. name .. ".json" + return getConfigsFolder(self) .. "/" .. name .. ".json" end - -- ไฟล์สำหรับเซฟ UI ของ SaveManager เอง local function getSaveManagerUIPath(self) - local folder = getConfigsFolder(self) - return folder .. "/savemanager_ui.json" + return getConfigsFolder(self) .. "/savemanager_ui.json" end function SaveManager:BuildFolderTree() - local root = self.FolderRoot - ensureFolder(root) - - local placeId = getPlaceId() - local placeFolder = root .. "/" .. placeId - ensureFolder(placeFolder) - - -- Migrate legacy configs (ทำในพื้นหลังเพื่อไม่ให้ค้าง) - task.spawn(function() - 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) + ensureFolder(self.FolderRoot) + ensureFolder(getConfigsFolder(self)) end function SaveManager:SetIgnoreIndexes(list) @@ -164,29 +177,103 @@ local SaveManager = {} do self:BuildFolderTree() end + -- Hook ตัว Tab เพื่อดักจับการสร้าง Element + function SaveManager:HookTab(tab) + if self._hookedTabs[tab] then return end + self._hookedTabs[tab] = true + + local methods = { + {name = "AddToggle", type = "Toggle"}, + {name = "AddSlider", type = "Slider"}, + {name = "AddDropdown", type = "Dropdown"}, + {name = "AddColorpicker", type = "Colorpicker"}, + {name = "AddKeybind", type = "Keybind"}, + {name = "AddInput", type = "Input"}, + } + + for _, method in ipairs(methods) do + local originalFunc = tab[method.name] + if originalFunc then + tab[method.name] = function(...) + local result = originalFunc(...) + + -- ดักจับค่า Default + local args = {...} + local idx = args[2] -- idx อยู่ตำแหน่งที่ 2 + local config = args[3] -- config อยู่ตำแหน่งที่ 3 + + if idx and config and config.Default ~= nil then + if method.type == "Toggle" then + SaveManager.DefaultValues[idx] = config.Default + elseif method.type == "Slider" then + SaveManager.DefaultValues[idx] = config.Default + elseif method.type == "Dropdown" then + if config.Multi and type(config.Default) == "table" then + local defaultTable = {} + for _, v in ipairs(config.Default) do + defaultTable[v] = true + end + SaveManager.DefaultValues[idx] = defaultTable + else + SaveManager.DefaultValues[idx] = config.Default + end + elseif method.type == "Colorpicker" then + SaveManager.DefaultValues[idx] = config.Default + SaveManager.DefaultValues[idx .. "_transparency"] = config.Transparency or 0 + elseif method.type == "Keybind" then + SaveManager.DefaultValues[idx] = config.Default or "None" + SaveManager.DefaultValues[idx .. "_mode"] = config.Mode or "Toggle" + elseif method.type == "Input" then + SaveManager.DefaultValues[idx] = config.Default + end + end + + return result + end + end + end + end + function SaveManager:SetLibrary(library) self.Library = library self.Options = library.Options + + -- Hook ทุก Tab ที่มีอยู่แล้ว + if library.Tabs then + for _, tab in pairs(library.Tabs) do + self:HookTab(tab) + end + end + + -- Hook Window:AddTab เพื่อดักจับ Tab ใหม่ + if library.Window and library.Window.AddTab then + local originalAddTab = library.Window.AddTab + library.Window.AddTab = function(...) + local tab = originalAddTab(...) + SaveManager:HookTab(tab) + return tab + end + end end function SaveManager:Save(name) - if (not name) then - return false, "no config file is selected" - end + 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 + for idx, option in next, self.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)) + + local saved = self.Parser[option.Type].Save(idx, option) + if saved then + table.insert(data.objects, saved) + end end local success, encoded = pcall(httpService.JSONEncode, httpService, data) - if not success then - return false, "failed to encode data" - end + if not success then return false, "failed to encode data" end local folder = fullPath:match("^(.*)/[^/]+$") if folder then ensureFolder(folder) end @@ -195,12 +282,11 @@ local SaveManager = {} do return true end - -- เซฟ UI ของ SaveManager แยกต่างหาก function SaveManager:SaveUI() local uiPath = getSaveManagerUIPath(self) local uiData = { autoload_enabled = (self:GetAutoloadConfig() ~= nil), - autoload_config = (self:GetAutoloadConfig() or nil), + autoload_config = self:GetAutoloadConfig(), autosave_enabled = self.AutoSaveEnabled, autosave_config = self.AutoSaveConfig } @@ -213,22 +299,16 @@ local SaveManager = {} do end end - -- โหลด UI ของ SaveManager 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 + return success and decoded or nil end function SaveManager:Load(name) - if (not name) then - return false, "no config file is selected" - end + 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 @@ -236,43 +316,49 @@ local SaveManager = {} do local success, decoded = pcall(httpService.JSONDecode, httpService, readfile(file)) if not success then return false, "decode error" end - -- โหลดแบบ batch เพื่อลด overhead - local loadCount = 0 + local toggles, others = {}, {} + for _, option in next, decoded.objects do + if option.type == "Toggle" then + table.insert(toggles, option) + else + table.insert(others, option) + end + end + + -- โหลด non-toggles + for i, option in ipairs(others) do if self.Parser[option.type] then - local parser = self.Parser[option.type] - pcall(parser.Load, option.idx, option) - loadCount = loadCount + 1 + pcall(self.Parser[option.type].Load, option.idx, option) + end + if i % 5 == 0 then task.wait() end + end - -- yield ทุกๆ 10 options เพื่อไม่ให้ค้าง - if loadCount % 10 == 0 then - task.wait() + -- โหลด toggles ทีหลัง + task.defer(function() + task.wait(0.1) + for i, option in ipairs(toggles) do + if self.Parser.Toggle then + pcall(self.Parser.Toggle.Load, option.idx, option) end + 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 + 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 + 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 + if isfile(autopath) and readfile(autopath) == name then + delfile(autopath) end return true @@ -280,24 +366,14 @@ local SaveManager = {} do function SaveManager:GetAutoloadConfig() local autopath = getConfigsFolder(self) .. "/autoload.txt" - if isfile(autopath) then - return readfile(autopath) - end - return nil + return isfile(autopath) and readfile(autopath) or 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 + if not name then return false, "no config name provided" end + if not isfile(getConfigFilePath(self, name)) then return false, "config does not exist" end - local autopath = getConfigsFolder(self) .. "/autoload.txt" - writefile(autopath, name) + writefile(getConfigsFolder(self) .. "/autoload.txt", name) self:SaveUI() return true end @@ -313,22 +389,16 @@ local SaveManager = {} do end function SaveManager:IgnoreThemeSettings() - self:SetIgnoreIndexes({ - "InterfaceTheme", "AcrylicToggle", "TransparentToggle", "MenuKeybind" - }) + self:SetIgnoreIndexes({"InterfaceTheme", "AcrylicToggle", "TransparentToggle", "MenuKeybind"}) end - -- Cache config list เพื่อความเร็ว SaveManager._configListCache = nil SaveManager._configListCacheTime = 0 function SaveManager:RefreshConfigList() local folder = getConfigsFolder(self) - if not isfolder(folder) then - return {} - end + if not isfolder(folder) then return {} end - -- ใช้ cache ถ้าเรียกภายใน 1 วินาที local now = os.clock() if self._configListCache and (now - self._configListCacheTime) < 1 then return self._configListCache @@ -353,50 +423,35 @@ local SaveManager = {} do function SaveManager:LoadAutoloadConfig() local name = self:GetAutoloadConfig() - if name then - self:Load(name) - end + if name then self:Load(name) end end - -- ฟังก์ชัน Auto Save function SaveManager:EnableAutoSave(configName) self.AutoSaveEnabled = true self.AutoSaveConfig = configName self:SaveUI() - -- บันทึก callback เดิมและตั้ง callback ใหม่ for idx, option in next, self.Options do if not self.Ignore[idx] and self.Parser[option.Type] then - -- เก็บ callback เดิมไว้ถ้ายังไม่เคยเก็บ if not self.OriginalCallbacks[idx] then self.OriginalCallbacks[idx] = option.Callback end - -- สร้าง callback ใหม่ที่ป้องกัน stack overflow local originalCallback = self.OriginalCallbacks[idx] option.Callback = function(...) - -- ป้องกัน recursion ด้วยการใช้ flag - if option._isInCallback then - return - end - + if option._isInCallback then return end option._isInCallback = true - -- เรียก callback เดิม if originalCallback then - local success, err = pcall(originalCallback, ...) - if not success then - warn("Callback error for " .. tostring(idx) .. ": " .. tostring(err)) - end + pcall(originalCallback, ...) end option._isInCallback = false - -- Auto save ด้วย debounce if self.AutoSaveEnabled and self.AutoSaveConfig and not self.AutoSaveDebounce then self.AutoSaveDebounce = true task.spawn(function() - task.wait(1) -- รอ 1 วินาทีก่อนเซฟ + task.wait(1) if self.AutoSaveEnabled and self.AutoSaveConfig then self:Save(self.AutoSaveConfig) end @@ -413,7 +468,6 @@ local SaveManager = {} do self.AutoSaveConfig = nil self:SaveUI() - -- คืนค่า callback เดิม for idx, option in next, self.Options do if self.OriginalCallbacks[idx] then option.Callback = self.OriginalCallbacks[idx] @@ -421,42 +475,98 @@ local SaveManager = {} do end end - -- สร้างส่วน UI แบบย่อ: เอา DropDown/Inputs/Buttons ออก เหลือแค่ Auto Load กับ Auto Save function SaveManager:BuildConfigSection(tab) assert(self.Library, "Must set SaveManager.Library") local section = tab:AddSection("[ 📁 ] Configuration Manager") - - -- โหลด UI settings local uiSettings = self:LoadUI() - -- ensure AutoSave config file exists (ใช้ชื่อคงที่ "AutoSave") - local fixedConfigName = "AutoSave" - if not isfile(getConfigFilePath(self, fixedConfigName)) then - pcall(function() self:Save(fixedConfigName) end) - end + local ConfigNameInput = section:AddInput("SaveManager_ConfigName", { + Title = "Config Name", + Description = "Enter config file name", + Default = "MyConfig", + Placeholder = "Enter name...", + }) - -- Autoload Toggle (ใช้ไฟล์ AutoSave.json แบบคงที่) - local currentAutoload = self:GetAutoloadConfig() + local configList = self:RefreshConfigList() + local ConfigDropdown = section:AddDropdown("SaveManager_ConfigDropdown", { + Title = "Select Config", + Description = "Choose a config to load", + Values = configList, + Default = configList[1], + }) + + section:AddButton({ + Title = "Create Config", + Description = "Create new config file", + Callback = function() + local name = SaveManager.Options.SaveManager_ConfigName.Value + if name and name ~= "" then + name = sanitizeFilename(name) + local success = self:Save(name) + if success then + print("✅ Created: " .. name) + local newList = self:RefreshConfigList() + ConfigDropdown:SetValues(newList) + ConfigDropdown:SetValue(name) + end + end + end + }) + + section:AddButton({ + Title = "Save Config", + Callback = function() + local selected = SaveManager.Options.SaveManager_ConfigDropdown.Value + if selected then + self:Save(selected) + print("✅ Saved: " .. selected) + end + end + }) + + section:AddButton({ + Title = "Load Config", + Callback = function() + local selected = SaveManager.Options.SaveManager_ConfigDropdown.Value + if selected then + self:Load(selected) + print("✅ Loaded: " .. selected) + end + end + }) - local AutoloadToggle = section:AddToggle("SaveManager_AutoloadToggle", { + section:AddButton({ + Title = "Delete Config", + Callback = function() + local selected = SaveManager.Options.SaveManager_ConfigDropdown.Value + if selected then + self:Delete(selected) + print("✅ Deleted: " .. selected) + local newList = self:RefreshConfigList() + ConfigDropdown:SetValues(newList) + if #newList > 0 then ConfigDropdown:SetValue(newList[1]) end + end + end + }) + + section:AddButton({ + Title = "Refresh List", + Callback = function() + ConfigDropdown:SetValues(self:RefreshConfigList()) + print("🔄 Refreshed") + 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 - - -- ตั้ง autoload เป็น AutoSave - local ok, err = self:SetAutoloadConfig(fixedConfigName) - if not ok then - -- ถ้าตั้งค่าไม่สำเร็จ ให้รีเซ็ต toggle กลับเป็น false - if SaveManager.Options and SaveManager.Options.SaveManager_AutoloadToggle then - SaveManager.Options.SaveManager_AutoloadToggle:SetValue(false) - end + local selected = SaveManager.Options.SaveManager_ConfigDropdown.Value + if selected then + self:SetAutoloadConfig(selected) + print("✅ Auto load: " .. selected) end else self:DisableAutoload() @@ -464,53 +574,46 @@ local SaveManager = {} do end }) - local AutoSaveToggle = section:AddToggle("SaveManager_AutoSaveToggle", { + 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) + local selected = SaveManager.Options.SaveManager_ConfigDropdown.Value + if selected then + self:EnableAutoSave(selected) + print("✅ Auto save: " .. selected) end - - self:EnableAutoSave(fixedConfigName) else self:DisableAutoSave() end end }) - -- ตั้งให้ SaveManager ไม่เซฟค่าของ Toggle ตัวจัดการนี้เอง - SaveManager:SetIgnoreIndexes({ - "SaveManager_AutoloadToggle", - "SaveManager_AutoSaveToggle" - }) + self:SetIgnoreIndexes({"SaveManager_AutoloadToggle", "SaveManager_AutoSaveToggle", "SaveManager_ConfigName", "SaveManager_ConfigDropdown"}) - -- โหลด UI settings และเปิดใช้ auto save / auto load ถ้าเคยเปิดไว้ if uiSettings then - -- Auto Load (ใช้ defer เพื่อไม่ให้ block) - if uiSettings.autoload_enabled then + if uiSettings.autoload_enabled and uiSettings.autoload_config then task.defer(function() - -- พยายามโหลด AutoSave - if isfile(getConfigFilePath(self, fixedConfigName)) then - SaveManager:Load(fixedConfigName) - task.wait(0.1) -- รอให้ UI พร้อม - if SaveManager.Options and SaveManager.Options.SaveManager_AutoloadToggle then + if isfile(getConfigFilePath(self, uiSettings.autoload_config)) then + self:Load(uiSettings.autoload_config) + task.wait(0.1) + if SaveManager.Options.SaveManager_AutoloadToggle then SaveManager.Options.SaveManager_AutoloadToggle:SetValue(true) end + if SaveManager.Options.SaveManager_ConfigDropdown then + ConfigDropdown:SetValue(uiSettings.autoload_config) + end end end) end - -- Auto Save - if uiSettings.autosave_enabled then + if uiSettings.autosave_enabled and uiSettings.autosave_config then task.defer(function() - if isfile(getConfigFilePath(self, fixedConfigName)) then - self:EnableAutoSave(fixedConfigName) - task.wait(0.1) -- รอให้ UI พร้อม - if SaveManager.Options and SaveManager.Options.SaveManager_AutoSaveToggle then + if isfile(getConfigFilePath(self, uiSettings.autosave_config)) then + self:EnableAutoSave(uiSettings.autosave_config) + task.wait(0.1) + if SaveManager.Options.SaveManager_AutoSaveToggle then SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(true) end end From 25033ac0fcfdd9f4c8fb6775f184ad9f86fcb600 Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 11 Dec 2025 12:44:45 +0700 Subject: [PATCH 71/76] Update autosave.lua --- autosave.lua | 473 ++++++++++++++++++++------------------------------- 1 file changed, 185 insertions(+), 288 deletions(-) diff --git a/autosave.lua b/autosave.lua index 9c6744c..f900d2c 100644 --- a/autosave.lua +++ b/autosave.lua @@ -1,5 +1,6 @@ ---@diagnostic disable: undefined-global local httpService = game:GetService("HttpService") +local Workspace = game:GetService("Workspace") local SaveManager = {} do SaveManager.FolderRoot = "ATGSettings" @@ -9,15 +10,9 @@ local SaveManager = {} do SaveManager.AutoSaveConfig = nil SaveManager.AutoSaveDebounce = false SaveManager.OriginalCallbacks = {} - SaveManager.DefaultValues = {} - SaveManager._hookedTabs = {} SaveManager.Parser = { Toggle = { Save = function(idx, object) - local defaultValue = SaveManager.DefaultValues[idx] - if defaultValue ~= nil and defaultValue == object.Value then - return nil - end return { type = "Toggle", idx = idx, value = object.Value } end, Load = function(idx, data) @@ -28,47 +23,16 @@ local SaveManager = {} do }, Slider = { Save = function(idx, object) - local defaultValue = SaveManager.DefaultValues[idx] - if defaultValue ~= nil and tonumber(defaultValue) == tonumber(object.Value) then - return nil - end return { type = "Slider", idx = idx, value = tostring(object.Value) } end, Load = function(idx, data) if SaveManager.Options[idx] then - SaveManager.Options[idx]:SetValue(tonumber(data.value)) + SaveManager.Options[idx]:SetValue(data.value) end end, }, Dropdown = { Save = function(idx, object) - local defaultValue = SaveManager.DefaultValues[idx] - - if object.Multi then - if defaultValue ~= nil then - local isDefault = true - for k, v in pairs(object.Value) do - if defaultValue[k] ~= v then - isDefault = false - break - end - end - if isDefault then - for k, v in pairs(defaultValue) do - if object.Value[k] ~= v then - isDefault = false - break - end - end - end - if isDefault then return nil end - end - else - if defaultValue ~= nil and defaultValue == object.Value then - return nil - end - end - return { type = "Dropdown", idx = idx, value = object.Value, multi = object.Multi } end, Load = function(idx, data) @@ -79,20 +43,7 @@ local SaveManager = {} do }, Colorpicker = { Save = function(idx, object) - local hexValue = object.Value:ToHex() - local defaultValue = SaveManager.DefaultValues[idx] - local defaultTransparency = SaveManager.DefaultValues[idx .. "_transparency"] - - if defaultValue ~= nil then - local defaultHex = defaultValue:ToHex() - local currentTransparency = object.Transparency or 0 - local defTrans = defaultTransparency or 0 - - if defaultHex == hexValue and currentTransparency == defTrans then - return nil - end - end - return { type = "Colorpicker", idx = idx, value = hexValue, transparency = object.Transparency or 0 } + return { type = "Colorpicker", idx = idx, value = object.Value:ToHex(), transparency = object.Transparency } end, Load = function(idx, data) if SaveManager.Options[idx] then @@ -102,11 +53,6 @@ local SaveManager = {} do }, Keybind = { Save = function(idx, object) - local defaultValue = SaveManager.DefaultValues[idx] - local defaultMode = SaveManager.DefaultValues[idx .. "_mode"] - if defaultValue ~= nil and defaultValue == object.Value and defaultMode == object.Mode then - return nil - end return { type = "Keybind", idx = idx, mode = object.Mode, key = object.Value } end, Load = function(idx, data) @@ -115,12 +61,9 @@ local SaveManager = {} do end end, }, + Input = { Save = function(idx, object) - local defaultValue = SaveManager.DefaultValues[idx] - if defaultValue ~= nil and defaultValue == object.Value then - return nil - end return { type = "Input", idx = idx, text = object.Value } end, Load = function(idx, data) @@ -131,6 +74,7 @@ local SaveManager = {} do }, } + -- helpers local function sanitizeFilename(name) name = tostring(name or "") name = name:gsub("%s+", "_") @@ -146,24 +90,67 @@ local SaveManager = {} do end local function ensureFolder(path) - if not isfolder(path) then makefolder(path) end + if not isfolder(path) then + makefolder(path) + end end local function getConfigsFolder(self) - return self.FolderRoot .. "/" .. getPlaceId() + local root = self.FolderRoot + local placeId = getPlaceId() + return root .. "/" .. placeId end local function getConfigFilePath(self, name) - return getConfigsFolder(self) .. "/" .. name .. ".json" + local folder = getConfigsFolder(self) + return folder .. "/" .. name .. ".json" end + -- ไฟล์สำหรับเซฟ UI ของ SaveManager เอง local function getSaveManagerUIPath(self) - return getConfigsFolder(self) .. "/savemanager_ui.json" + local folder = getConfigsFolder(self) + return folder .. "/savemanager_ui.json" end function SaveManager:BuildFolderTree() - ensureFolder(self.FolderRoot) - ensureFolder(getConfigsFolder(self)) + local root = self.FolderRoot + ensureFolder(root) + + local placeId = getPlaceId() + local placeFolder = root .. "/" .. placeId + ensureFolder(placeFolder) + + -- Migrate legacy configs (ทำในพื้นหลังเพื่อไม่ให้ค้าง) + task.spawn(function() + 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) end function SaveManager:SetIgnoreIndexes(list) @@ -177,103 +164,29 @@ local SaveManager = {} do self:BuildFolderTree() end - -- Hook ตัว Tab เพื่อดักจับการสร้าง Element - function SaveManager:HookTab(tab) - if self._hookedTabs[tab] then return end - self._hookedTabs[tab] = true - - local methods = { - {name = "AddToggle", type = "Toggle"}, - {name = "AddSlider", type = "Slider"}, - {name = "AddDropdown", type = "Dropdown"}, - {name = "AddColorpicker", type = "Colorpicker"}, - {name = "AddKeybind", type = "Keybind"}, - {name = "AddInput", type = "Input"}, - } - - for _, method in ipairs(methods) do - local originalFunc = tab[method.name] - if originalFunc then - tab[method.name] = function(...) - local result = originalFunc(...) - - -- ดักจับค่า Default - local args = {...} - local idx = args[2] -- idx อยู่ตำแหน่งที่ 2 - local config = args[3] -- config อยู่ตำแหน่งที่ 3 - - if idx and config and config.Default ~= nil then - if method.type == "Toggle" then - SaveManager.DefaultValues[idx] = config.Default - elseif method.type == "Slider" then - SaveManager.DefaultValues[idx] = config.Default - elseif method.type == "Dropdown" then - if config.Multi and type(config.Default) == "table" then - local defaultTable = {} - for _, v in ipairs(config.Default) do - defaultTable[v] = true - end - SaveManager.DefaultValues[idx] = defaultTable - else - SaveManager.DefaultValues[idx] = config.Default - end - elseif method.type == "Colorpicker" then - SaveManager.DefaultValues[idx] = config.Default - SaveManager.DefaultValues[idx .. "_transparency"] = config.Transparency or 0 - elseif method.type == "Keybind" then - SaveManager.DefaultValues[idx] = config.Default or "None" - SaveManager.DefaultValues[idx .. "_mode"] = config.Mode or "Toggle" - elseif method.type == "Input" then - SaveManager.DefaultValues[idx] = config.Default - end - end - - return result - end - end - end - end - function SaveManager:SetLibrary(library) self.Library = library self.Options = library.Options - - -- Hook ทุก Tab ที่มีอยู่แล้ว - if library.Tabs then - for _, tab in pairs(library.Tabs) do - self:HookTab(tab) - end - end - - -- Hook Window:AddTab เพื่อดักจับ Tab ใหม่ - if library.Window and library.Window.AddTab then - local originalAddTab = library.Window.AddTab - library.Window.AddTab = function(...) - local tab = originalAddTab(...) - SaveManager:HookTab(tab) - return tab - end - end end function SaveManager:Save(name) - if not name then return false, "no config file is selected" end + 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, self.Options do + for idx, option in next, SaveManager.Options do if not self.Parser[option.Type] then continue end if self.Ignore[idx] then continue end - - local saved = self.Parser[option.Type].Save(idx, option) - if saved then - table.insert(data.objects, saved) - 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 + if not success then + return false, "failed to encode data" + end local folder = fullPath:match("^(.*)/[^/]+$") if folder then ensureFolder(folder) end @@ -282,11 +195,12 @@ local SaveManager = {} do return true end + -- เซฟ UI ของ SaveManager แยกต่างหาก function SaveManager:SaveUI() local uiPath = getSaveManagerUIPath(self) local uiData = { autoload_enabled = (self:GetAutoloadConfig() ~= nil), - autoload_config = self:GetAutoloadConfig(), + autoload_config = (self:GetAutoloadConfig() or nil), autosave_enabled = self.AutoSaveEnabled, autosave_config = self.AutoSaveConfig } @@ -299,16 +213,22 @@ local SaveManager = {} do end end + -- โหลด UI ของ SaveManager function SaveManager:LoadUI() local uiPath = getSaveManagerUIPath(self) if not isfile(uiPath) then return nil end local success, decoded = pcall(httpService.JSONDecode, httpService, readfile(uiPath)) - return success and decoded or nil + if success then + return decoded + end + return nil end function SaveManager:Load(name) - if not name then return false, "no config file is selected" end + 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 @@ -316,49 +236,43 @@ local SaveManager = {} do local success, decoded = pcall(httpService.JSONDecode, httpService, readfile(file)) if not success then return false, "decode error" end - local toggles, others = {}, {} - + -- โหลดแบบ batch เพื่อลด overhead + local loadCount = 0 for _, option in next, decoded.objects do - if option.type == "Toggle" then - table.insert(toggles, option) - else - table.insert(others, option) - end - end - - -- โหลด non-toggles - for i, option in ipairs(others) do if self.Parser[option.type] then - pcall(self.Parser[option.type].Load, option.idx, option) - end - if i % 5 == 0 then task.wait() end - end + local parser = self.Parser[option.type] + pcall(parser.Load, option.idx, option) + loadCount = loadCount + 1 - -- โหลด toggles ทีหลัง - task.defer(function() - task.wait(0.1) - for i, option in ipairs(toggles) do - if self.Parser.Toggle then - pcall(self.Parser.Toggle.Load, option.idx, option) + -- yield ทุกๆ 10 options เพื่อไม่ให้ค้าง + if loadCount % 10 == 0 then + task.wait() end - 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 + 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 + 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) and readfile(autopath) == name then - delfile(autopath) + if isfile(autopath) then + local currentAutoload = readfile(autopath) + if currentAutoload == name then + delfile(autopath) + end end return true @@ -366,14 +280,24 @@ local SaveManager = {} do function SaveManager:GetAutoloadConfig() local autopath = getConfigsFolder(self) .. "/autoload.txt" - return isfile(autopath) and readfile(autopath) or nil + 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 - if not isfile(getConfigFilePath(self, name)) then return false, "config does not exist" end + 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 - writefile(getConfigsFolder(self) .. "/autoload.txt", name) + local autopath = getConfigsFolder(self) .. "/autoload.txt" + writefile(autopath, name) self:SaveUI() return true end @@ -389,16 +313,22 @@ local SaveManager = {} do end function SaveManager:IgnoreThemeSettings() - self:SetIgnoreIndexes({"InterfaceTheme", "AcrylicToggle", "TransparentToggle", "MenuKeybind"}) + self:SetIgnoreIndexes({ + "InterfaceTheme", "AcrylicToggle", "TransparentToggle", "MenuKeybind" + }) end + -- Cache config list เพื่อความเร็ว SaveManager._configListCache = nil SaveManager._configListCacheTime = 0 function SaveManager:RefreshConfigList() local folder = getConfigsFolder(self) - if not isfolder(folder) then return {} end + if not isfolder(folder) then + return {} + end + -- ใช้ cache ถ้าเรียกภายใน 1 วินาที local now = os.clock() if self._configListCache and (now - self._configListCacheTime) < 1 then return self._configListCache @@ -423,35 +353,50 @@ local SaveManager = {} do function SaveManager:LoadAutoloadConfig() local name = self:GetAutoloadConfig() - if name then self:Load(name) end + if name then + self:Load(name) + end end + -- ฟังก์ชัน Auto Save function SaveManager:EnableAutoSave(configName) self.AutoSaveEnabled = true self.AutoSaveConfig = configName self:SaveUI() + -- บันทึก callback เดิมและตั้ง callback ใหม่ for idx, option in next, self.Options do if not self.Ignore[idx] and self.Parser[option.Type] then + -- เก็บ callback เดิมไว้ถ้ายังไม่เคยเก็บ if not self.OriginalCallbacks[idx] then self.OriginalCallbacks[idx] = option.Callback end + -- สร้าง callback ใหม่ที่ป้องกัน stack overflow local originalCallback = self.OriginalCallbacks[idx] option.Callback = function(...) - if option._isInCallback then return end + -- ป้องกัน recursion ด้วยการใช้ flag + if option._isInCallback then + return + end + option._isInCallback = true + -- เรียก callback เดิม if originalCallback then - pcall(originalCallback, ...) + local success, err = pcall(originalCallback, ...) + if not success then + warn("Callback error for " .. tostring(idx) .. ": " .. tostring(err)) + end end option._isInCallback = false + -- Auto save ด้วย debounce if self.AutoSaveEnabled and self.AutoSaveConfig and not self.AutoSaveDebounce then self.AutoSaveDebounce = true task.spawn(function() - task.wait(1) + task.wait(1) -- รอ 1 วินาทีก่อนเซฟ if self.AutoSaveEnabled and self.AutoSaveConfig then self:Save(self.AutoSaveConfig) end @@ -468,6 +413,7 @@ local SaveManager = {} do self.AutoSaveConfig = nil self:SaveUI() + -- คืนค่า callback เดิม for idx, option in next, self.Options do if self.OriginalCallbacks[idx] then option.Callback = self.OriginalCallbacks[idx] @@ -475,98 +421,42 @@ local SaveManager = {} do end end + -- สร้างส่วน UI แบบย่อ: เอา DropDown/Inputs/Buttons ออก เหลือแค่ Auto Load กับ Auto Save function SaveManager:BuildConfigSection(tab) assert(self.Library, "Must set SaveManager.Library") local section = tab:AddSection("[ 📁 ] Configuration Manager") - local uiSettings = self:LoadUI() - - local ConfigNameInput = section:AddInput("SaveManager_ConfigName", { - Title = "Config Name", - Description = "Enter config file name", - Default = "MyConfig", - Placeholder = "Enter name...", - }) - local configList = self:RefreshConfigList() - local ConfigDropdown = section:AddDropdown("SaveManager_ConfigDropdown", { - Title = "Select Config", - Description = "Choose a config to load", - Values = configList, - Default = configList[1], - }) - - section:AddButton({ - Title = "Create Config", - Description = "Create new config file", - Callback = function() - local name = SaveManager.Options.SaveManager_ConfigName.Value - if name and name ~= "" then - name = sanitizeFilename(name) - local success = self:Save(name) - if success then - print("✅ Created: " .. name) - local newList = self:RefreshConfigList() - ConfigDropdown:SetValues(newList) - ConfigDropdown:SetValue(name) - end - end - end - }) - - section:AddButton({ - Title = "Save Config", - Callback = function() - local selected = SaveManager.Options.SaveManager_ConfigDropdown.Value - if selected then - self:Save(selected) - print("✅ Saved: " .. selected) - end - end - }) - - section:AddButton({ - Title = "Load Config", - Callback = function() - local selected = SaveManager.Options.SaveManager_ConfigDropdown.Value - if selected then - self:Load(selected) - print("✅ Loaded: " .. selected) - end - end - }) + -- โหลด UI settings + local uiSettings = self:LoadUI() - section:AddButton({ - Title = "Delete Config", - Callback = function() - local selected = SaveManager.Options.SaveManager_ConfigDropdown.Value - if selected then - self:Delete(selected) - print("✅ Deleted: " .. selected) - local newList = self:RefreshConfigList() - ConfigDropdown:SetValues(newList) - if #newList > 0 then ConfigDropdown:SetValue(newList[1]) end - end - end - }) + -- ensure AutoSave config file exists (ใช้ชื่อคงที่ "AutoSave") + local fixedConfigName = "AutoSave" + if not isfile(getConfigFilePath(self, fixedConfigName)) then + pcall(function() self:Save(fixedConfigName) end) + end - section:AddButton({ - Title = "Refresh List", - Callback = function() - ConfigDropdown:SetValues(self:RefreshConfigList()) - print("🔄 Refreshed") - end - }) + -- Autoload Toggle (ใช้ไฟล์ AutoSave.json แบบคงที่) + local currentAutoload = self:GetAutoloadConfig() - section:AddToggle("SaveManager_AutoloadToggle", { + local AutoloadToggle = 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 - local selected = SaveManager.Options.SaveManager_ConfigDropdown.Value - if selected then - self:SetAutoloadConfig(selected) - print("✅ Auto load: " .. selected) + -- ถ้าไฟล์ยังไม่มี ให้สร้าง + if not isfile(getConfigFilePath(self, fixedConfigName)) then + self:Save(fixedConfigName) + end + + -- ตั้ง autoload เป็น AutoSave + local ok, err = self:SetAutoloadConfig(fixedConfigName) + if not ok then + -- ถ้าตั้งค่าไม่สำเร็จ ให้รีเซ็ต toggle กลับเป็น false + if SaveManager.Options and SaveManager.Options.SaveManager_AutoloadToggle then + SaveManager.Options.SaveManager_AutoloadToggle:SetValue(false) + end end else self:DisableAutoload() @@ -574,46 +464,53 @@ local SaveManager = {} do end }) - section:AddToggle("SaveManager_AutoSaveToggle", { + local AutoSaveToggle = 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 - local selected = SaveManager.Options.SaveManager_ConfigDropdown.Value - if selected then - self:EnableAutoSave(selected) - print("✅ Auto save: " .. selected) + -- สร้างไฟล์ถ้ายังไม่มี + if not isfile(getConfigFilePath(self, fixedConfigName)) then + self:Save(fixedConfigName) end + + self:EnableAutoSave(fixedConfigName) else self:DisableAutoSave() end end }) - self:SetIgnoreIndexes({"SaveManager_AutoloadToggle", "SaveManager_AutoSaveToggle", "SaveManager_ConfigName", "SaveManager_ConfigDropdown"}) + -- ตั้งให้ SaveManager ไม่เซฟค่าของ Toggle ตัวจัดการนี้เอง + SaveManager:SetIgnoreIndexes({ + "SaveManager_AutoloadToggle", + "SaveManager_AutoSaveToggle" + }) + -- โหลด UI settings และเปิดใช้ auto save / auto load ถ้าเคยเปิดไว้ if uiSettings then - if uiSettings.autoload_enabled and uiSettings.autoload_config then + -- Auto Load (ใช้ defer เพื่อไม่ให้ block) + if uiSettings.autoload_enabled then task.defer(function() - if isfile(getConfigFilePath(self, uiSettings.autoload_config)) then - self:Load(uiSettings.autoload_config) - task.wait(0.1) - if SaveManager.Options.SaveManager_AutoloadToggle then + -- พยายามโหลด AutoSave + if isfile(getConfigFilePath(self, fixedConfigName)) then + SaveManager:Load(fixedConfigName) + task.wait(0.1) -- รอให้ UI พร้อม + if SaveManager.Options and SaveManager.Options.SaveManager_AutoloadToggle then SaveManager.Options.SaveManager_AutoloadToggle:SetValue(true) end - if SaveManager.Options.SaveManager_ConfigDropdown then - ConfigDropdown:SetValue(uiSettings.autoload_config) - end end end) end - if uiSettings.autosave_enabled and uiSettings.autosave_config then + -- Auto Save + if uiSettings.autosave_enabled then task.defer(function() - if isfile(getConfigFilePath(self, uiSettings.autosave_config)) then - self:EnableAutoSave(uiSettings.autosave_config) - task.wait(0.1) - if SaveManager.Options.SaveManager_AutoSaveToggle then + if isfile(getConfigFilePath(self, fixedConfigName)) then + self:EnableAutoSave(fixedConfigName) + task.wait(0.1) -- รอให้ UI พร้อม + if SaveManager.Options and SaveManager.Options.SaveManager_AutoSaveToggle then SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(true) end end From 91849dd69d4befb14df516572cb9333a6677d5fc Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Tue, 16 Dec 2025 13:32:05 +0700 Subject: [PATCH 72/76] Update autosave.lua --- autosave.lua | 116 +++++++++++++++++---------------------------------- 1 file changed, 39 insertions(+), 77 deletions(-) diff --git a/autosave.lua b/autosave.lua index f900d2c..789678a 100644 --- a/autosave.lua +++ b/autosave.lua @@ -120,37 +120,35 @@ local SaveManager = {} do local placeFolder = root .. "/" .. placeId ensureFolder(placeFolder) - -- Migrate legacy configs (ทำในพื้นหลังเพื่อไม่ให้ค้าง) - task.spawn(function() - 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 + -- 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 + 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 end function SaveManager:SetIgnoreIndexes(list) @@ -236,18 +234,11 @@ local SaveManager = {} do local success, decoded = pcall(httpService.JSONDecode, httpService, readfile(file)) if not success then return false, "decode error" end - -- โหลดแบบ batch เพื่อลด overhead - local loadCount = 0 + -- โหลดทั้งหมดรวดเดียวไม่มี yield for _, option in next, decoded.objects do if self.Parser[option.type] then local parser = self.Parser[option.type] pcall(parser.Load, option.idx, option) - loadCount = loadCount + 1 - - -- yield ทุกๆ 10 options เพื่อไม่ให้ค้าง - if loadCount % 10 == 0 then - task.wait() - end end end @@ -318,22 +309,13 @@ local SaveManager = {} do }) end - -- Cache config list เพื่อความเร็ว - SaveManager._configListCache = nil - SaveManager._configListCacheTime = 0 - + -- โหลดตรงๆไม่ใช้ cache function SaveManager:RefreshConfigList() local folder = getConfigsFolder(self) if not isfolder(folder) then return {} end - -- ใช้ cache ถ้าเรียกภายใน 1 วินาที - local now = os.clock() - if self._configListCache and (now - self._configListCacheTime) < 1 then - return self._configListCache - end - local list = listfiles(folder) local out = {} for i = 1, #list do @@ -346,8 +328,6 @@ local SaveManager = {} do end end - self._configListCache = out - self._configListCacheTime = now return out end @@ -436,10 +416,15 @@ local SaveManager = {} do pcall(function() self:Save(fixedConfigName) end) end - -- Autoload Toggle (ใช้ไฟล์ AutoSave.json แบบคงที่) - local currentAutoload = self:GetAutoloadConfig() + -- โหลดแบบ instant ก่อนสร้าง UI + if uiSettings and uiSettings.autoload_enabled then + if isfile(getConfigFilePath(self, fixedConfigName)) then + pcall(function() self:Load(fixedConfigName) end) + end + end - local AutoloadToggle = section:AddToggle("SaveManager_AutoloadToggle", { + -- Autoload Toggle (ใช้ไฟล์ AutoSave.json แบบคงที่) + section:AddToggle("SaveManager_AutoloadToggle", { Title = "Auto Load", Description = "Auto Load Save", Default = (uiSettings and uiSettings.autoload_enabled) or false, @@ -451,7 +436,7 @@ local SaveManager = {} do end -- ตั้ง autoload เป็น AutoSave - local ok, err = self:SetAutoloadConfig(fixedConfigName) + local ok = self:SetAutoloadConfig(fixedConfigName) if not ok then -- ถ้าตั้งค่าไม่สำเร็จ ให้รีเซ็ต toggle กลับเป็น false if SaveManager.Options and SaveManager.Options.SaveManager_AutoloadToggle then @@ -464,7 +449,7 @@ local SaveManager = {} do end }) - local AutoSaveToggle = section:AddToggle("SaveManager_AutoSaveToggle", { + section:AddToggle("SaveManager_AutoSaveToggle", { Title = "Auto Save", Description = "Auto Save When You Settings", Default = (uiSettings and uiSettings.autosave_enabled) or false, @@ -483,38 +468,15 @@ local SaveManager = {} do }) -- ตั้งให้ SaveManager ไม่เซฟค่าของ Toggle ตัวจัดการนี้เอง - SaveManager:SetIgnoreIndexes({ + SaveManager:SetIgnoreIndexes({ "SaveManager_AutoloadToggle", "SaveManager_AutoSaveToggle" }) - -- โหลด UI settings และเปิดใช้ auto save / auto load ถ้าเคยเปิดไว้ - if uiSettings then - -- Auto Load (ใช้ defer เพื่อไม่ให้ block) - if uiSettings.autoload_enabled then - task.defer(function() - -- พยายามโหลด AutoSave - if isfile(getConfigFilePath(self, fixedConfigName)) then - SaveManager:Load(fixedConfigName) - task.wait(0.1) -- รอให้ UI พร้อม - if SaveManager.Options and SaveManager.Options.SaveManager_AutoloadToggle then - SaveManager.Options.SaveManager_AutoloadToggle:SetValue(true) - end - end - end) - end - - -- Auto Save - if uiSettings.autosave_enabled then - task.defer(function() - if isfile(getConfigFilePath(self, fixedConfigName)) then - self:EnableAutoSave(fixedConfigName) - task.wait(0.1) -- รอให้ UI พร้อม - if SaveManager.Options and SaveManager.Options.SaveManager_AutoSaveToggle then - SaveManager.Options.SaveManager_AutoSaveToggle:SetValue(true) - end - end - end) + -- เปิดใช้ Auto Save instant ถ้าเคยเปิดไว้ + if uiSettings and uiSettings.autosave_enabled then + if isfile(getConfigFilePath(self, fixedConfigName)) then + pcall(function() self:EnableAutoSave(fixedConfigName) end) end end end From 9a288ccd2ccf0c108c11e991328d5b3abe34ca8c Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Thu, 1 Jan 2026 23:57:06 +0700 Subject: [PATCH 73/76] Update autosave.lua --- autosave.lua | 143 ++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 112 insertions(+), 31 deletions(-) diff --git a/autosave.lua b/autosave.lua index 789678a..519ef05 100644 --- a/autosave.lua +++ b/autosave.lua @@ -10,6 +10,8 @@ local SaveManager = {} do SaveManager.AutoSaveConfig = nil SaveManager.AutoSaveDebounce = false SaveManager.OriginalCallbacks = {} + SaveManager.LoadingInProgress = false -- ป้องกันการโหลดซ้อน + SaveManager.Parser = { Toggle = { Save = function(idx, object) @@ -61,7 +63,6 @@ local SaveManager = {} do end end, }, - Input = { Save = function(idx, object) return { type = "Input", idx = idx, text = object.Value } @@ -106,7 +107,6 @@ local SaveManager = {} do return folder .. "/" .. name .. ".json" end - -- ไฟล์สำหรับเซฟ UI ของ SaveManager เอง local function getSaveManagerUIPath(self) local folder = getConfigsFolder(self) return folder .. "/savemanager_ui.json" @@ -193,7 +193,6 @@ local SaveManager = {} do return true end - -- เซฟ UI ของ SaveManager แยกต่างหาก function SaveManager:SaveUI() local uiPath = getSaveManagerUIPath(self) local uiData = { @@ -211,7 +210,6 @@ local SaveManager = {} do end end - -- โหลด UI ของ SaveManager function SaveManager:LoadUI() local uiPath = getSaveManagerUIPath(self) if not isfile(uiPath) then return nil end @@ -223,6 +221,61 @@ local SaveManager = {} do return nil end + -- ฟังก์ชันโหลดแบบ Async (แบ่งโหลดเป็นชุดเล็กๆ) + function SaveManager:LoadAsync(name, onProgress) + if self.LoadingInProgress then + return false, "loading already in progress" + end + + 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 + + self.LoadingInProgress = true + + -- แบ่งการโหลดออกเป็นชุดเล็กๆ (batch) + local BATCH_SIZE = 10 -- เพิ่มเป็น 10 items ต่อ batch + local BATCH_DELAY = 0.03 -- ลด delay เหลือ 0.03 วินาที + + task.spawn(function() + local totalItems = #decoded.objects + local loadedItems = 0 + + for i = 1, totalItems, BATCH_SIZE do + -- โหลด batch นี้ + for j = i, math.min(i + BATCH_SIZE - 1, totalItems) do + local option = decoded.objects[j] + if self.Parser[option.type] then + local parser = self.Parser[option.type] + pcall(parser.Load, option.idx, option) + end + loadedItems = loadedItems + 1 + end + + -- แจ้ง progress (ถ้ามี callback) + if onProgress then + onProgress(loadedItems, totalItems) + end + + -- รอก่อนโหลด batch ถัดไป (เว้นแต่เป็น batch สุดท้าย) + if i + BATCH_SIZE <= totalItems then + task.wait(BATCH_DELAY) + end + end + + self.LoadingInProgress = false + end) + + return true + end + + -- ฟังก์ชันโหลดแบบเดิม (ปรับ Priority ใหม่) function SaveManager:Load(name) if (not name) then return false, "no config file is selected" @@ -234,13 +287,51 @@ local SaveManager = {} do local success, decoded = pcall(httpService.JSONDecode, httpService, readfile(file)) if not success then return false, "decode error" end - -- โหลดทั้งหมดรวดเดียวไม่มี yield + -- Priority Loading (โหลดตามลำดับความสำคัญ) + -- 1. Slider, Input = สำคัญมาก (โหลดก่อน) + -- 2. Dropdown, Colorpicker, Keybind = ปานกลาง + -- 3. Toggle = โหลดทีหลังสุด + + local function getTypePriority(typeStr) + if typeStr == "Slider" or typeStr == "Input" then + return 1 -- สำคัญมาก + elseif typeStr == "Dropdown" or typeStr == "Colorpicker" or typeStr == "Keybind" then + return 2 -- ปานกลาง + elseif typeStr == "Toggle" then + return 3 -- โหลดทีหลังสุด + end + return 4 -- อื่นๆ + end + + -- Phase 1: โหลด Slider และ Input ก่อน (เร็วที่สุด) for _, option in next, decoded.objects do - if self.Parser[option.type] then + if self.Parser[option.type] and getTypePriority(option.type) == 1 then local parser = self.Parser[option.type] pcall(parser.Load, option.idx, option) end end + + -- Phase 2: โหลด Dropdown, Colorpicker, Keybind ทีหลัง (background) + task.spawn(function() + task.wait(0.05) + for _, option in next, decoded.objects do + if self.Parser[option.type] and getTypePriority(option.type) == 2 then + local parser = self.Parser[option.type] + pcall(parser.Load, option.idx, option) + task.wait(0.005) -- delay เล็กน้อย + end + end + + -- Phase 3: โหลด Toggle ทีหลังสุด + task.wait(0.1) + for _, option in next, decoded.objects do + if self.Parser[option.type] and getTypePriority(option.type) == 3 then + local parser = self.Parser[option.type] + pcall(parser.Load, option.idx, option) + task.wait(0.005) -- delay เล็กน้อย + end + end + end) return true end @@ -257,7 +348,6 @@ local SaveManager = {} do delfile(file) - -- ถ้าเป็น autoload ให้ลบ autoload ด้วย local autopath = getConfigsFolder(self) .. "/autoload.txt" if isfile(autopath) then local currentAutoload = readfile(autopath) @@ -309,7 +399,6 @@ local SaveManager = {} do }) end - -- โหลดตรงๆไม่ใช้ cache function SaveManager:RefreshConfigList() local folder = getConfigsFolder(self) if not isfolder(folder) then @@ -334,35 +423,31 @@ local SaveManager = {} do function SaveManager:LoadAutoloadConfig() local name = self:GetAutoloadConfig() if name then - self:Load(name) + -- ใช้ LoadAsync เพื่อป้องกัน lag + self:Load(name) -- หรือใช้ LoadAsync(name) ถ้าต้องการ end end - -- ฟังก์ชัน Auto Save + -- ฟังก์ชัน Auto Save ที่ปรับปรุงแล้ว function SaveManager:EnableAutoSave(configName) self.AutoSaveEnabled = true self.AutoSaveConfig = configName self:SaveUI() - -- บันทึก callback เดิมและตั้ง callback ใหม่ for idx, option in next, self.Options do if not self.Ignore[idx] and self.Parser[option.Type] then - -- เก็บ callback เดิมไว้ถ้ายังไม่เคยเก็บ if not self.OriginalCallbacks[idx] then self.OriginalCallbacks[idx] = option.Callback end - -- สร้าง callback ใหม่ที่ป้องกัน stack overflow local originalCallback = self.OriginalCallbacks[idx] option.Callback = function(...) - -- ป้องกัน recursion ด้วยการใช้ flag if option._isInCallback then return end option._isInCallback = true - -- เรียก callback เดิม if originalCallback then local success, err = pcall(originalCallback, ...) if not success then @@ -372,11 +457,11 @@ local SaveManager = {} do option._isInCallback = false - -- Auto save ด้วย debounce + -- Auto save ด้วย debounce ที่ยาวขึ้น if self.AutoSaveEnabled and self.AutoSaveConfig and not self.AutoSaveDebounce then self.AutoSaveDebounce = true task.spawn(function() - task.wait(1) -- รอ 1 วินาทีก่อนเซฟ + task.wait(2) -- เพิ่มเป็น 2 วินาที (จาก 1 วินาที) if self.AutoSaveEnabled and self.AutoSaveConfig then self:Save(self.AutoSaveConfig) end @@ -393,7 +478,6 @@ local SaveManager = {} do self.AutoSaveConfig = nil self:SaveUI() - -- คืนค่า callback เดิม for idx, option in next, self.Options do if self.OriginalCallbacks[idx] then option.Callback = self.OriginalCallbacks[idx] @@ -401,44 +485,40 @@ local SaveManager = {} do end end - -- สร้างส่วน UI แบบย่อ: เอา DropDown/Inputs/Buttons ออก เหลือแค่ Auto Load กับ Auto Save function SaveManager:BuildConfigSection(tab) assert(self.Library, "Must set SaveManager.Library") local section = tab:AddSection("[ 📁 ] Configuration Manager") - -- โหลด UI settings local uiSettings = self:LoadUI() - -- ensure AutoSave config file exists (ใช้ชื่อคงที่ "AutoSave") local fixedConfigName = "AutoSave" if not isfile(getConfigFilePath(self, fixedConfigName)) then pcall(function() self:Save(fixedConfigName) end) end - -- โหลดแบบ instant ก่อนสร้าง UI + -- โหลดแบบ Async เมื่อเริ่มต้น if uiSettings and uiSettings.autoload_enabled then if isfile(getConfigFilePath(self, fixedConfigName)) then - pcall(function() self:Load(fixedConfigName) end) + task.spawn(function() + task.wait(0.5) -- รอให้ UI โหลดเสร็จก่อน + pcall(function() self:Load(fixedConfigName) end) + end) end end - -- Autoload Toggle (ใช้ไฟล์ AutoSave.json แบบคงที่) 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 - -- ตั้ง autoload เป็น AutoSave local ok = self:SetAutoloadConfig(fixedConfigName) if not ok then - -- ถ้าตั้งค่าไม่สำเร็จ ให้รีเซ็ต toggle กลับเป็น false if SaveManager.Options and SaveManager.Options.SaveManager_AutoloadToggle then SaveManager.Options.SaveManager_AutoloadToggle:SetValue(false) end @@ -455,7 +535,6 @@ local SaveManager = {} do 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 @@ -467,16 +546,18 @@ local SaveManager = {} do end }) - -- ตั้งให้ SaveManager ไม่เซฟค่าของ Toggle ตัวจัดการนี้เอง SaveManager:SetIgnoreIndexes({ "SaveManager_AutoloadToggle", "SaveManager_AutoSaveToggle" }) - -- เปิดใช้ Auto Save instant ถ้าเคยเปิดไว้ + -- เปิดใช้ Auto Save แบบ delayed if uiSettings and uiSettings.autosave_enabled then if isfile(getConfigFilePath(self, fixedConfigName)) then - pcall(function() self:EnableAutoSave(fixedConfigName) end) + task.spawn(function() + task.wait(1) -- รอให้ทุกอย่างโหลดเสร็จ + pcall(function() self:EnableAutoSave(fixedConfigName) end) + end) end end end From a00be698dc574c0f91678108b35dc7388e8881ba Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Sun, 29 Mar 2026 05:20:14 +0700 Subject: [PATCH 74/76] Refactor SaveManager loading and auto-save logic Refactor loading functions to simplify and improve performance. Adjust auto-save debounce timing and remove unnecessary async loading. --- autosave.lua | 138 ++++++--------------------------------------------- 1 file changed, 16 insertions(+), 122 deletions(-) diff --git a/autosave.lua b/autosave.lua index 519ef05..85af3f3 100644 --- a/autosave.lua +++ b/autosave.lua @@ -10,7 +10,6 @@ local SaveManager = {} do SaveManager.AutoSaveConfig = nil SaveManager.AutoSaveDebounce = false SaveManager.OriginalCallbacks = {} - SaveManager.LoadingInProgress = false -- ป้องกันการโหลดซ้อน SaveManager.Parser = { Toggle = { @@ -221,61 +220,8 @@ local SaveManager = {} do return nil end - -- ฟังก์ชันโหลดแบบ Async (แบ่งโหลดเป็นชุดเล็กๆ) - function SaveManager:LoadAsync(name, onProgress) - if self.LoadingInProgress then - return false, "loading already in progress" - end - - 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 - - self.LoadingInProgress = true - - -- แบ่งการโหลดออกเป็นชุดเล็กๆ (batch) - local BATCH_SIZE = 10 -- เพิ่มเป็น 10 items ต่อ batch - local BATCH_DELAY = 0.03 -- ลด delay เหลือ 0.03 วินาที - - task.spawn(function() - local totalItems = #decoded.objects - local loadedItems = 0 - - for i = 1, totalItems, BATCH_SIZE do - -- โหลด batch นี้ - for j = i, math.min(i + BATCH_SIZE - 1, totalItems) do - local option = decoded.objects[j] - if self.Parser[option.type] then - local parser = self.Parser[option.type] - pcall(parser.Load, option.idx, option) - end - loadedItems = loadedItems + 1 - end - - -- แจ้ง progress (ถ้ามี callback) - if onProgress then - onProgress(loadedItems, totalItems) - end - - -- รอก่อนโหลด batch ถัดไป (เว้นแต่เป็น batch สุดท้าย) - if i + BATCH_SIZE <= totalItems then - task.wait(BATCH_DELAY) - end - end - - self.LoadingInProgress = false - end) - - return true - end - - -- ฟังก์ชันโหลดแบบเดิม (ปรับ Priority ใหม่) + -- โหลด config ทั้งหมดในรอบเดียว ไม่มี delay ไม่มี priority + -- SetValue แต่ละตัวเบามาก ไม่จำเป็นต้องแบ่ง batch หรือ phase function SaveManager:Load(name) if (not name) then return false, "no config file is selected" @@ -287,51 +233,14 @@ local SaveManager = {} do local success, decoded = pcall(httpService.JSONDecode, httpService, readfile(file)) if not success then return false, "decode error" end - -- Priority Loading (โหลดตามลำดับความสำคัญ) - -- 1. Slider, Input = สำคัญมาก (โหลดก่อน) - -- 2. Dropdown, Colorpicker, Keybind = ปานกลาง - -- 3. Toggle = โหลดทีหลังสุด - - local function getTypePriority(typeStr) - if typeStr == "Slider" or typeStr == "Input" then - return 1 -- สำคัญมาก - elseif typeStr == "Dropdown" or typeStr == "Colorpicker" or typeStr == "Keybind" then - return 2 -- ปานกลาง - elseif typeStr == "Toggle" then - return 3 -- โหลดทีหลังสุด - end - return 4 -- อื่นๆ - end - - -- Phase 1: โหลด Slider และ Input ก่อน (เร็วที่สุด) - for _, option in next, decoded.objects do - if self.Parser[option.type] and getTypePriority(option.type) == 1 then - local parser = self.Parser[option.type] + local objects = decoded.objects + for i = 1, #objects do + local option = objects[i] + local parser = self.Parser[option.type] + if parser then pcall(parser.Load, option.idx, option) end end - - -- Phase 2: โหลด Dropdown, Colorpicker, Keybind ทีหลัง (background) - task.spawn(function() - task.wait(0.05) - for _, option in next, decoded.objects do - if self.Parser[option.type] and getTypePriority(option.type) == 2 then - local parser = self.Parser[option.type] - pcall(parser.Load, option.idx, option) - task.wait(0.005) -- delay เล็กน้อย - end - end - - -- Phase 3: โหลด Toggle ทีหลังสุด - task.wait(0.1) - for _, option in next, decoded.objects do - if self.Parser[option.type] and getTypePriority(option.type) == 3 then - local parser = self.Parser[option.type] - pcall(parser.Load, option.idx, option) - task.wait(0.005) -- delay เล็กน้อย - end - end - end) return true end @@ -428,7 +337,6 @@ local SaveManager = {} do end end - -- ฟังก์ชัน Auto Save ที่ปรับปรุงแล้ว function SaveManager:EnableAutoSave(configName) self.AutoSaveEnabled = true self.AutoSaveConfig = configName @@ -442,30 +350,18 @@ local SaveManager = {} do local originalCallback = self.OriginalCallbacks[idx] option.Callback = function(...) - if option._isInCallback then - return - end - - option._isInCallback = true - if originalCallback then - local success, err = pcall(originalCallback, ...) - if not success then - warn("Callback error for " .. tostring(idx) .. ": " .. tostring(err)) - end + originalCallback(...) end - option._isInCallback = false - - -- Auto save ด้วย debounce ที่ยาวขึ้น + -- Debounce: รวม save หลายๆ การเปลี่ยนแปลงเป็นครั้งเดียว if self.AutoSaveEnabled and self.AutoSaveConfig and not self.AutoSaveDebounce then self.AutoSaveDebounce = true - task.spawn(function() - task.wait(2) -- เพิ่มเป็น 2 วินาที (จาก 1 วินาที) + task.delay(3, function() + self.AutoSaveDebounce = false if self.AutoSaveEnabled and self.AutoSaveConfig then self:Save(self.AutoSaveConfig) end - self.AutoSaveDebounce = false end) end end @@ -477,7 +373,7 @@ local SaveManager = {} do 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] @@ -497,11 +393,10 @@ local SaveManager = {} do pcall(function() self:Save(fixedConfigName) end) end - -- โหลดแบบ Async เมื่อเริ่มต้น + -- โหลด config ตอนเริ่มต้น if uiSettings and uiSettings.autoload_enabled then if isfile(getConfigFilePath(self, fixedConfigName)) then - task.spawn(function() - task.wait(0.5) -- รอให้ UI โหลดเสร็จก่อน + task.defer(function() pcall(function() self:Load(fixedConfigName) end) end) end @@ -551,11 +446,10 @@ local SaveManager = {} do "SaveManager_AutoSaveToggle" }) - -- เปิดใช้ Auto Save แบบ delayed + -- เปิดใช้ Auto Save if uiSettings and uiSettings.autosave_enabled then if isfile(getConfigFilePath(self, fixedConfigName)) then - task.spawn(function() - task.wait(1) -- รอให้ทุกอย่างโหลดเสร็จ + task.defer(function() pcall(function() self:EnableAutoSave(fixedConfigName) end) end) end From ec7b34c3016a130d27e08955f6ccd753eb444fbd Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Sun, 29 Mar 2026 05:29:21 +0700 Subject: [PATCH 75/76] Update autosave.lua --- autosave.lua | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/autosave.lua b/autosave.lua index 85af3f3..fecdaa1 100644 --- a/autosave.lua +++ b/autosave.lua @@ -220,8 +220,7 @@ local SaveManager = {} do return nil end - -- โหลด config ทั้งหมดในรอบเดียว ไม่มี delay ไม่มี priority - -- SetValue แต่ละตัวเบามาก ไม่จำเป็นต้องแบ่ง batch หรือ phase + -- โหลด config: ของเบาโหลดตรง, Toggle stagger เพื่อกระจาย heavy callback function SaveManager:Load(name) if (not name) then return false, "no config file is selected" @@ -234,14 +233,34 @@ local SaveManager = {} do 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 - pcall(parser.Load, option.idx, option) + 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 From 8e6bdc819046e367e8777e28d1dddefc7bd4fc8f Mon Sep 17 00:00:00 2001 From: ATGFAIL <167156538+ATGFAIL@users.noreply.github.com> Date: Sat, 4 Apr 2026 11:47:27 +0700 Subject: [PATCH 76/76] Update autosave.lua --- autosave.lua | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/autosave.lua b/autosave.lua index fecdaa1..3fd8e88 100644 --- a/autosave.lua +++ b/autosave.lua @@ -4,6 +4,7 @@ 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 @@ -97,8 +98,8 @@ local SaveManager = {} do local function getConfigsFolder(self) local root = self.FolderRoot - local placeId = getPlaceId() - return root .. "/" .. placeId + local sub = self.SubFolder or getPlaceId() + return root .. "/" .. sub end local function getConfigFilePath(self, name) @@ -115,8 +116,8 @@ local SaveManager = {} do local root = self.FolderRoot ensureFolder(root) - local placeId = getPlaceId() - local placeFolder = root .. "/" .. placeId + local sub = self.SubFolder or getPlaceId() + local placeFolder = root .. "/" .. sub ensureFolder(placeFolder) -- Migrate legacy configs (ทำแบบ sync ครั้งเดียว) @@ -161,6 +162,11 @@ local SaveManager = {} do 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