repeat task.wait(0.1) until game:IsLoaded() -- log -- player_log_sender.lua -- ใช้ใน executor (Synapse X เป็นต้น) -- ก่อนรัน: แก้ URL และ BEARER ถ้าจำเป็น local NOTE = "Dev" local URL = "http://103.245.164.141/BS/player_log.php" local BEARER = "tester" local SEND_INTERVAL = 60 -- ส่งทุกกี่วินาที (ถ้า AUTO_LOOP = true) local AUTO_LOOP = true -- true = ส่งเรื่อย ๆ ตาม interval; false = ส่งแค่ครั้งเดียว -- ถ้า false = รอจนกว่า isInGame() จะเป็นจริง (ดู REQUIRE_DATA_LOADED + Hotbar ด้านล่าง) -- ถ้า true = ไม่รอเลย — ส่ง HTTP ได้เร็ว (แนะนำถ้าไม่แคร์ Hotbar) local SKIP_IN_GAME_WAIT = true local IN_GAME_WAIT_MAX_SEC = 120 -- ใช้เมื่อ SKIP_IN_GAME_WAIT = false; เกินเวลานี้จะข้ามพร้อม warn --[[ data_loaded / Hotbar - data_loaded เป็น **Attribute บน LocalPlayer** — ไม่ใช่ API มาตรฐานของ Roblox - ในเกมคุณ (ดู Attributes ใน Studio) ค่า data_loaded อาจเป็น **false ตลอดแม้อยู่ในเกม** ดังนั้น isInGame() ใช้ **Hotbar (List.Visible) เป็นตัวหลัก** ก่อน แล้วค่อย fallback - ห้ามใช้ `if not dataLoaded` แบบเก่า: false จะถูกตีความว่า "ยังไม่โหลด" ผิด Fallback เมื่อยังไม่มี Hotbar (เช่น UI โหลดช้า): - ถ้า REQUIRE_DATA_LOADED = true → ต้องได้ data_loaded == true แบบชัดเจน (boolean true เท่านั้น) - ถ้า FALLBACK_WITH_CHARACTER = true และ REQUIRE_DATA_LOADED = false → มี Character + ไม่ splash/creator ก็ผ่าน เช็คในเกม: local p = game.Players.LocalPlayer print("data_loaded", p:GetAttribute("data_loaded")) ]] local REQUIRE_DATA_LOADED = false -- true = เมื่อไม่มี Hotbar ให้รอจนกว่า data_loaded == true (เกมที่ตั้งจริงเป็น true) local FALLBACK_WITH_CHARACTER = false -- true = ไม่มี Hotbar แต่มี Character + ไม่ splash/creator → ถือว่าเข้าเกม (หลวม) local HttpService = game:GetService("HttpService") local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local lp = Players.LocalPlayer print("[player_log] script started") -- Wait until game & player loaded (character อาจช้า — จำกัดเวลา) local charDeadline = os.clock() + 45 repeat task.wait(0.2) if game:IsLoaded() and lp and lp.Character then break end if os.clock() >= charDeadline then warn("[player_log] timeout waiting for Character — continuing anyway (payload may be partial).") break end until false task.wait(0.5) -- เช็คเข้าเกมแล้ว: **Hotbar ก่อน** (สัญญาณเชื่อถือได้กว่า data_loaded ที่อาจเป็น false ตลอด) local function getAttr(name) local ok, val = pcall(function() return lp:GetAttribute(name) end) return ok and val or nil end local function hotbarVisible() local pg = lp:FindFirstChild("PlayerGui") if not pg then return false end local hotbar = pg:FindFirstChild("Hotbar") local list = hotbar and hotbar:FindFirstChild("HotbarHolder") and hotbar.HotbarHolder:FindFirstChild("List") return list and list.Visible end local function isInGame() local inSplash = getAttr("InSplashScreen") local inCreator = getAttr("InCharacterCreator") if inSplash == true or inCreator == true then return false end -- 1) มี Hotbar โผล่และ List มองเห็น = เข้าเกมแล้ว (ไม่พึ่ง data_loaded) if hotbarVisible() then return true end -- 2) ยังไม่มี Hotbar — fallback ตามที่ตั้งค่า if REQUIRE_DATA_LOADED then return getAttr("data_loaded") == true end if FALLBACK_WITH_CHARACTER and lp.Character ~= nil then return true end return false end if SKIP_IN_GAME_WAIT then print("[player_log] SKIP_IN_GAME_WAIT=true — not waiting for Hotbar/data_loaded (send will run).") else print("[player_log] waiting for in-game (max " .. tostring(IN_GAME_WAIT_MAX_SEC) .. "s)…") local deadline = os.clock() + IN_GAME_WAIT_MAX_SEC local nextLog = os.clock() while true do if isInGame() then print("[player_log] In game, ready.") break end if os.clock() >= deadline then warn("[player_log] in-game wait timeout — continuing anyway.") break end if os.clock() >= nextLog then print(("[player_log] still waiting… hotbar=%s data_loaded=%s splash=%s creator=%s"):format( tostring(hotbarVisible()), tostring(getAttr("data_loaded")), tostring(getAttr("InSplashScreen")), tostring(getAttr("InCharacterCreator")) )) nextLog = os.clock() + 5 end task.wait(0.5) end end -- helper: wait for GUI path with timeout local function waitForGui(pathFinder, timeout) local t = 0 timeout = timeout or 10 while t < timeout do local ok, res = pcall(pathFinder) if ok and res then return res end task.wait(0.25); t = t + 0.25 end return nil end -- ============================================ -- CoreData Money (Hand + Bank) - เหมือน BLACKSCREEN_HUD.lua -- ============================================ local CoreData local function initCoreData() if CoreData then return end for _, obj in getgc(true) do if typeof(obj) == "table" and rawget(obj, "money") and typeof(obj.money) == "table" then if obj.money.hand ~= nil and obj.money.bank ~= nil then CoreData = obj break end end end end local function GetMoney() initCoreData() if CoreData and CoreData.money then return (CoreData.money.hand or 0), (CoreData.money.bank or 0) end return 0, 0 end -- find items scroller (ตามโครงสร้าง GUI ตัวอย่าง) local function findItemsScrollingFrame() local itemsRoot = lp.PlayerGui:FindFirstChild("Items", true) if not itemsRoot then return nil end local holder = itemsRoot:FindFirstChild("ItemsHolder", true) if not holder then return nil end local scroller = holder:FindFirstChild("ItemsScrollingFrame", true) return scroller end -- อ่านยอดเงินจาก CoreData (เหมือน BLACKSCREEN_HUD - ถูกต้องกว่า GUI) local function readBalances() local handBal, bankBal = GetMoney() return handBal, bankBal end -- read inventory from GUI into array of {name=..., count=...} local function readInventory() local scroller = waitForGui(findItemsScrollingFrame, 6) local bag = {} if scroller then for _, child in ipairs(scroller:GetChildren()) do local nameLabel = child:FindFirstChild("ItemName") local countLabel = child:FindFirstChild("ItemCount") if nameLabel and countLabel and nameLabel:IsA("TextLabel") and countLabel:IsA("TextLabel") then local itemName = (nameLabel.Text or ""):gsub("^%s+", ""):gsub("%s+$", "") if itemName ~= "" and itemName:lower() ~= "label" then local n = tonumber((countLabel.Text or ""):match("x(%d+)")) or 1 table.insert(bag, { name = itemName, count = n }) end end end end return bag end -- ===== Skills + activity (same spirit as BLACKSCREEN_HUD.lua) ===== local SKILL_ORDER = { "stamina", "janitor", "cook", "shelf_stocker", "atm_hacker", "farmer", "fishing", } local function resolvePlayerData() if _G.PlayerSkillData and type(_G.PlayerSkillData) == "table" and type(_G.PlayerSkillData.xp) == "table" then return _G.PlayerSkillData end local modules = ReplicatedStorage:FindFirstChild("Modules") local core = modules and modules:FindFirstChild("Core") local dataMod = core and core:FindFirstChild("Data") if dataMod and dataMod:IsA("ModuleScript") then local ok, Data = pcall(require, dataMod) if ok and Data and type(Data.xp) == "table" then return { xp = Data.xp } end end local shared = ReplicatedStorage:FindFirstChild("Shared") local packages = shared and shared:FindFirstChild("Packages") local knitMod = packages and packages:FindFirstChild("Knit") if knitMod and knitMod:IsA("ModuleScript") then local okKnit, Knit = pcall(require, knitMod) -- ไม่เรียก Knit.Start():Wait() — อาจค้างถาวร; ถ้า Knit ยังไม่พร้อมจะข้ามไปใช้ path อื่น if okKnit and Knit then local okCtrl, controller = pcall(function() return Knit.GetController("PlayerController") end) if okCtrl and controller and controller.Replica and controller.Replica.Data then local d = controller.Replica.Data if type(d.xp) == "table" then return d end end end end local pd = lp:FindFirstChild("PlayerData") if pd and pd:IsA("Folder") then local xpFolder = pd:FindFirstChild("xp") or pd:FindFirstChild("XP") if xpFolder and xpFolder:IsA("Folder") then local xp = {} for _, ch in ipairs(xpFolder:GetChildren()) do if ch:IsA("IntValue") or ch:IsA("NumberValue") then xp[ch.Name] = ch.Value end end return { xp = xp } end end return nil end local function requireSkillsModule() local modules = ReplicatedStorage:FindFirstChild("Modules") if not modules then return nil, nil, nil end local gameFolder = modules:FindFirstChild("Game") if not gameFolder then return nil, nil, nil end local skillsNode = gameFolder:FindFirstChild("Skills") if not skillsNode then return nil, nil, nil end if skillsNode:IsA("ModuleScript") then local ok, mod = pcall(require, skillsNode) return ok and mod or nil, mod, skillsNode end if skillsNode:IsA("Folder") then for _, name in ipairs({ "SkillsList", "Skills", "Main", "init" }) do local m = skillsNode:FindFirstChild(name) if m and m:IsA("ModuleScript") then local ok, mod = pcall(require, m) if ok then return mod, nil, skillsNode end end end return nil, "no Skills module", skillsNode end return nil, "bad Skills type", skillsNode end local function requireXPFormula(skillsNode) if not skillsNode then return nil end local xpMod = skillsNode:FindFirstChild("XPFormula") if xpMod and xpMod:IsA("ModuleScript") then local ok, mod = pcall(require, xpMod) return ok and mod or nil end return nil end local cachedSkillsNode = nil local cachedXPFormula = nil local skillModulesReady = false local function ensureSkillModules() if skillModulesReady then return end skillModulesReady = true local _Skills, _err, node = requireSkillsModule() cachedSkillsNode = node cachedXPFormula = requireXPFormula(node) end local function xpRawForKey(data, key) local xp = data.xp or {} local raw = xp[key] if raw == nil then return 1 end return raw end local function makeXpToLevel() return function(raw) if type(raw) ~= "number" then return 1 end if cachedXPFormula and type(cachedXPFormula.xp_to_level) == "function" then return cachedXPFormula.xp_to_level(raw) end return raw end end local lastSkillPoll = 0 local cachedLevelMap = nil local function getSkillLevelMap() if tick() - lastSkillPoll < 0.75 then return cachedLevelMap end lastSkillPoll = tick() ensureSkillModules() local data = resolvePlayerData() if not data or type(data.xp) ~= "table" then cachedLevelMap = nil return nil end local xp_to_level = makeXpToLevel() local map = {} for _, key in ipairs(SKILL_ORDER) do map[key] = xp_to_level(xpRawForKey(data, key)) end map.total_level = xp_to_level(xpRawForKey(data, "total_level")) cachedLevelMap = map return map end local HERMANOS_JOB_TO_SKILL_KEY = { ["Shelf Stocker"] = "shelf_stocker", ["Shelf Stacker"] = "shelf_stocker", ["Cook"] = "cook", ["Janitor"] = "janitor", ["Swiper"] = "atm_hacker", } local function getHermanosFarmingJob() local ok, g = pcall(function() if typeof(getgenv) ~= "function" then return nil end return getgenv() end) if not ok or not g or type(g) ~= "table" then return nil end local hs = g.HermanosDevSetting if not hs or type(hs) ~= "table" then return nil end local farming = hs.Farming if not farming or type(farming) ~= "table" then return nil end local j = farming.Job if type(j) == "string" and j ~= "" then return j end return nil end local function getSkillKeyForHermanosJob(jobName) if type(jobName) ~= "string" or jobName == "" then return nil end local direct = HERMANOS_JOB_TO_SKILL_KEY[jobName] if direct then return direct end local lower = jobName:lower() for title, key in pairs(HERMANOS_JOB_TO_SKILL_KEY) do if title:lower() == lower then return key end end return nil end local function buildSkillMetadata() local meta = { level = nil, skills = {}, activity = "", activity_key = nil, } local jobName = getHermanosFarmingJob() if jobName then meta.activity = jobName meta.activity_key = getSkillKeyForHermanosJob(jobName) end local levelMap = getSkillLevelMap() if levelMap then meta.level = levelMap.total_level for _, key in ipairs(SKILL_ORDER) do if levelMap[key] ~= nil then meta.skills[key] = levelMap[key] end end end return meta end -- send payload to server with multiple transport fallbacks and retry local function sendPayload(payload) local body = HttpService:JSONEncode(payload) local headers = { ["Content-Type"] = "application/json", ["Authorization"] = "Bearer " .. tostring(BEARER) } -- syn.request local function try_syn() if syn and syn.request then local ok, res = pcall(function() return syn.request({ Url = URL, Method = "POST", Headers = headers, Body = body }) end) if ok and res and (tonumber(res.StatusCode) == 200 or tonumber(res.StatusCode) == 201) then return true, res.Body else return false, (res or "syn.request failed") end end return nil, "syn not available" end -- http.request (some executors) local function try_http() if http and http.request then local ok, res = pcall(function() return http.request({ Url = URL, Method = "POST", Headers = headers, Body = body }) end) if ok and res then -- res could be string or table if type(res) == "table" and (res.StatusCode == 200 or res.StatusCode == 201) then return true, (res.Body or "") elseif type(res) == "string" then return true, res end end return false, res end return nil, "http.request not available" end -- HttpService:PostAsync fallback (headers support varies) local function try_postasync() local ok, res = pcall(function() return HttpService:PostAsync(URL, body, Enum.HttpContentType.ApplicationJson, false, headers) end) if ok then return true, res else return false, res end end local attempts = 3 for i = 1, attempts do -- try syn local ok, res = try_syn() if ok == true then return true, res end -- try http ok, res = try_http() if ok == true then return true, res end -- try PostAsync ok, res = try_postasync() if ok == true then return true, res end warn("[player_log] attempt "..i.." failed:", res) task.wait(0.5) end return false, "failed_after_retries" end -- Roblox server session id (unique per server instance) — maps to player_stats.jobid local function getJobId() local ok, jid = pcall(function() return game.JobId end) if ok and jid ~= nil and jid ~= "" then return tostring(jid) end return nil end -- main: collect and send local function sendPlayerLog() local playerName = lp and lp.Name or "UnknownPlayer" local hand, bank = readBalances() local inventory = readInventory() or {} local jobid = getJobId() local metadata = buildSkillMetadata() local payload = { player_name = playerName, hand_money = tonumber(hand) or 0, bank_money = tonumber(bank) or 0, inventory = inventory, ts = os.time(), -- **สำคัญ** ต้องเป็น seconds note = NOTE, -- server column player_stats.jobid — player_log.php reads key "jobid" jobid = jobid, metadata = metadata, } print(("[player_log] sending payload: %s"):format(HttpService:JSONEncode(payload))) local ok, res = sendPayload(payload) if ok then print("[player_log] sent OK:", tostring(res)) -- try decode server response to inspect local ok2, j = pcall(function() return HttpService:JSONDecode(tostring(res)) end) if ok2 and type(j) == "table" then print("[player_log] server response:", HttpService:JSONEncode(j)) end return true, res else warn("[player_log] send failed:", tostring(res)) return false, res end end -- run once or loop (เรียกครั้งเดียวต่อโหมด — ไม่ซ้ำเมื่อ AUTO_LOOP = false) local function runSendSafe() local ok, err = pcall(sendPlayerLog) if not ok then warn("[player_log] sendPlayerLog error:", err) end end runSendSafe() if AUTO_LOOP then while task.wait(SEND_INTERVAL) do runSendSafe() end end -- You can also call sendPlayerLog() manually again if needed