local houseOwners = {} local houseLocks = {} local speakerOccupants = {} local acStates = {} local speakerStates = {} local activeAppliances = {} local applianceTimers = {} local carWashState = nil local carWashTimer = 0 local serverInitialized = false local playerClaimCooldown = {} local VALID_APPLIANCES = { microwave = true, coffee = true, washingmachine = true, dishwasher = true, oven = true, shower = true, sink = true, bathtub = true } local function jsonEncode(t) local ok, res = pcall(function() return Util.JsonEncode(t) end) if ok and res then return res end local ok2, res2 = pcall(function() return json.encode(t) end) if ok2 and res2 then return res2 end return "{}" end local function safeDecode(str, maxLen) if type(str) ~= "string" or str == "" or #str > (maxLen or 2048) then return nil end local ok, res = pcall(function() return Util.JsonDecode(str) end) if ok and type(res) == "table" then return res end local ok2, res2 = pcall(function() return json.decode(str) end) if ok2 and type(res2) == "table" then return res2 end return nil end local function extractHouseNumber(str) if type(str) ~= "string" or str == "" then return nil end local lower = string.lower(str) local stage, hNum = string.match(lower, "^bathtubwater(%d)_(%d+)") if hNum then local n = tonumber(hNum) if n and n >= 1 and n <= 70 then return tostring(n) end end local firstNum = string.match(lower, "(%d+)") if firstNum then local n = tonumber(firstNum) if n and n >= 1 and n <= 70 then return tostring(n) end end return nil end local function sendClientEvent(playerID, eventName, dataTable) local str = jsonEncode(dataTable) if MP and type(MP.TriggerClientEvent) == "function" then MP.TriggerClientEvent(playerID, eventName, str) elseif type(TriggerClientEvent) == "function" then TriggerClientEvent(playerID, eventName, str) end end local function getOwnedHouseNumber(playerID) local sPid = tostring(playerID) for num, owner in pairs(houseOwners) do if tostring(owner.playerID) == sPid then return tostring(num) end end return nil end local function clearPlayerOwnership(playerID) local sPid = tostring(playerID) for num, owner in pairs(houseOwners) do if tostring(owner.playerID) == sPid then local sNum = tostring(num) houseOwners[sNum] = nil houseLocks[sNum] = nil acStates[sNum] = nil speakerStates[sNum] = nil for spkID, occupant in pairs(speakerOccupants) do if tostring(occupant.playerID) == sPid then speakerOccupants[spkID] = nil end end for key, appData in pairs(activeAppliances) do local appHouse = appData.houseNum or extractHouseNumber(appData.id) or extractHouseNumber(key) if appHouse == sNum then activeAppliances[key] = nil applianceTimers[key] = nil local stopState = (appData.applianceType == "bathtub" and "empty" or false) sendClientEvent(-1, "SmartHome_SyncAppliance", { applianceType = appData.applianceType, id = appData.id, action = "stop", state = stopState, houseNum = sNum }) end end sendClientEvent(-1, "SmartHome_SyncUnclaim", { houseNum = sNum }) sendClientEvent(-1, "SmartHome_SyncAC", { houseNum = sNum, isOn = false }) sendClientEvent(-1, "SmartHome_SyncSpeaker", { houseNum = sNum, isOn = false }) end end for spkID, occupant in pairs(speakerOccupants) do if tostring(occupant.playerID) == sPid then speakerOccupants[spkID] = nil end end playerClaimCooldown[playerID] = nil if carWashState and tostring(carWashState.playerID) == sPid then carWashState = nil carWashTimer = 0 sendClientEvent(-1, "SmartHome_SyncCarWash", { action = "reset" }) end end function onSmartHomeRequestFullSync(playerID, dataJson) for num, owner in pairs(houseOwners) do local sNum = tostring(num) local locks = houseLocks[sNum] or { houseLocked = false, garageLocked = false } sendClientEvent(playerID, "SmartHome_SyncClaim", { houseNum = sNum, houseName = "House " .. sNum, ownerID = owner.playerID, ownerName = owner.playerName, houseLocked = locks.houseLocked, garageLocked = locks.garageLocked }) end for num, ac in pairs(acStates) do local sNum = tostring(num) if ac.isOn then sendClientEvent(playerID, "SmartHome_SyncAC", { houseNum = sNum, isOn = true, fan = ac.fan, temp = ac.temp }) end end for num, spk in pairs(speakerStates) do local sNum = tostring(num) if spk.isOn then sendClientEvent(playerID, "SmartHome_SyncSpeaker", { houseNum = sNum, speakerID = spk.speakerID, isOn = true, isPaused = spk.isPaused, trackIdx = spk.trackIdx, volume = spk.volume }) end end local now = os.time() for _, appData in pairs(activeAppliances) do if appData.applianceType == "bathtub" and appData.state == "filling" and appData.serverStartTime then local elapsed = now - appData.serverStartTime local curFill = math.min(90, (appData.fillTime or 0) + elapsed) local isDone = (curFill >= 90) local curStage = isDone and 3 or (curFill >= 60 and 2 or (curFill >= 30 and 1 or 0)) sendClientEvent(playerID, "SmartHome_SyncAppliance", { applianceType = "bathtub", id = appData.id, action = isDone and "full" or "filling", state = isDone and "full" or "filling", stage = curStage, fillTime = curFill, houseNum = appData.houseNum }) else sendClientEvent(playerID, "SmartHome_SyncAppliance", appData) end end if carWashState then sendClientEvent(playerID, "SmartHome_SyncCarWash", carWashState) end end function onSmartHomeRequestClaim(playerID, dataJson) local data = safeDecode(dataJson, 512) if not data or type(data.triggerName) ~= "string" then return end local now = os.time() if now - (playerClaimCooldown[playerID] or 0) < 1 then return end playerClaimCooldown[playerID] = now local num = extractHouseNumber(data.triggerName) if not num then return end local currentOwnedNum = getOwnedHouseNumber(playerID) if currentOwnedNum and currentOwnedNum ~= num then sendClientEvent(playerID, "SmartHome_ClaimDenied", { reason = "You already own House " .. currentOwnedNum .. ". Unclaim it in Phone Settings first." }) return end local currentOwner = houseOwners[num] if currentOwner and tostring(currentOwner.playerID) ~= tostring(playerID) then sendClientEvent(playerID, "SmartHome_ClaimDenied", { reason = "House " .. num .. " is already owned by " .. currentOwner.playerName .. "." }) return end local pName = type(data.playerName) == "string" and string.sub(data.playerName, 1, 32) or "Player" houseOwners[num] = { playerID = playerID, playerName = pName } houseLocks[num] = houseLocks[num] or { houseLocked = false, garageLocked = false } sendClientEvent(playerID, "SmartHome_ClaimApproved", { houseNum = num, houseName = "House " .. num, playerName = pName }) sendClientEvent(-1, "SmartHome_SyncClaim", { houseNum = num, houseName = "House " .. num, ownerID = playerID, ownerName = pName, houseLocked = houseLocks[num].houseLocked, garageLocked = houseLocks[num].garageLocked }) end function onSmartHomeRequestUnclaim(playerID, dataJson) local now = os.time() if now - (playerClaimCooldown[playerID] or 0) < 1 then return end playerClaimCooldown[playerID] = now clearPlayerOwnership(playerID) end function onSmartHomeRequestUnclaimedCleanup(playerID, dataJson) local data = safeDecode(dataJson, 512) if not data or not data.houseNum then return end local sNum = tostring(data.houseNum) if houseOwners[sNum] then return end for key, appData in pairs(activeAppliances) do local appHouse = appData.houseNum or extractHouseNumber(appData.id) or extractHouseNumber(key) if appHouse == sNum then activeAppliances[key] = nil applianceTimers[key] = nil local stopState = (appData.applianceType == "bathtub" and "empty" or false) sendClientEvent(-1, "SmartHome_SyncAppliance", { applianceType = appData.applianceType, id = appData.id, action = "stop", state = stopState, houseNum = sNum }) end end speakerStates[sNum] = nil for spkID, _ in pairs(speakerOccupants) do local spkHouse = extractHouseNumber(spkID) if spkHouse == sNum then speakerOccupants[spkID] = nil end end sendClientEvent(-1, "SmartHome_SyncSpeaker", { houseNum = sNum, isOn = false }) sendClientEvent(-1, "SmartHome_SyncAC", { houseNum = sNum, isOn = false }) end function onSmartHomeRequestLockToggle(playerID, dataJson) local data = safeDecode(dataJson, 512) if not data then return end local num = extractHouseNumber(tostring(data.houseNum or data.houseName or "")) if not num then return end local owner = houseOwners[num] if not owner or tostring(owner.playerID) ~= tostring(playerID) then return end local target = data.target if target ~= "house" and target ~= "garage" then return end local locked = (data.locked == true) houseLocks[num] = houseLocks[num] or { houseLocked = false, garageLocked = false } if target == "house" then houseLocks[num].houseLocked = locked elseif target == "garage" then houseLocks[num].garageLocked = locked end sendClientEvent(-1, "SmartHome_SyncLock", { houseNum = num, houseLocked = houseLocks[num].houseLocked, garageLocked = houseLocks[num].garageLocked }) end function onSmartHomeRequestTeleport(playerID, dataJson) local data = safeDecode(dataJson, 512) if not data or type(data.triggerName) ~= "string" then return end local triggerName = data.triggerName local num = extractHouseNumber(triggerName) if not num then sendClientEvent(playerID, "SmartHome_TeleportApproved", { triggerName = triggerName }) return end local lowerName = string.lower(triggerName) local isEnter = string.find(lowerName, "enter") ~= nil local isGarage = string.find(lowerName, "garage") ~= nil or string.find(lowerName, "house%d+_enter") ~= nil or string.find(lowerName, "house%d+_exit") ~= nil local locks = houseLocks[num] or { houseLocked = false, garageLocked = false } local isLocked = false if isEnter then isLocked = isGarage and (locks.garageLocked == true) or (locks.houseLocked == true) end if isLocked then local msg = isGarage and "The garage is locked." or "The house is locked." sendClientEvent(playerID, "SmartHome_TeleportDenied", { triggerName = triggerName, reason = msg }) else sendClientEvent(playerID, "SmartHome_TeleportApproved", { triggerName = triggerName }) end end function onSmartHomeRequestACToggle(playerID, dataJson) local data = safeDecode(dataJson, 512) if not data then return end local num = extractHouseNumber(tostring(data.houseNum or data.houseName or "")) if not num then return end local owner = houseOwners[num] if not owner then sendClientEvent(playerID, "SmartHome_ACDenied", { reason = "You must claim House " .. num .. " to use climate control." }) return end if tostring(owner.playerID) ~= tostring(playerID) then sendClientEvent(playerID, "SmartHome_ACDenied", { ownerName = owner.playerName }) return end acStates[num] = acStates[num] or {} acStates[num].isOn = (data.isOn == true) if data.fan == "Low" or data.fan == "Med" or data.fan == "High" then acStates[num].fan = data.fan else acStates[num].fan = "Med" end local tempVal = tonumber(data.temp) if tempVal and tempVal >= 10 and tempVal <= 100 then acStates[num].temp = tempVal else acStates[num].temp = 72 end sendClientEvent(-1, "SmartHome_SyncAC", { houseNum = num, isOn = acStates[num].isOn, fan = acStates[num].fan, temp = acStates[num].temp }) end function onSmartHomeRequestSpeakerConnect(playerID, dataJson) local data = safeDecode(dataJson, 512) if not data or type(data.speakerID) ~= "string" then return end local speakerID = string.sub(data.speakerID, 1, 32) local occupant = speakerOccupants[speakerID] local sPid = tostring(playerID) if occupant and tostring(occupant.playerID) ~= sPid then sendClientEvent(playerID, "SmartHome_SpeakerDenied", { occupiedBy = occupant.playerName }) return end local pName = type(data.playerName) == "string" and string.sub(data.playerName, 1, 32) or "Player" speakerOccupants[speakerID] = { playerID = playerID, playerName = pName } sendClientEvent(playerID, "SmartHome_SpeakerConnected", { speakerID = speakerID }) end function onSmartHomeRequestSpeakerDisconnect(playerID, dataJson) local data = safeDecode(dataJson, 512) if not data or type(data.speakerID) ~= "string" then return end local speakerID = string.sub(data.speakerID, 1, 32) local occupant = speakerOccupants[speakerID] if occupant and tostring(occupant.playerID) == tostring(playerID) then speakerOccupants[speakerID] = nil end end function onSmartHomeRequestSpeakerState(playerID, dataJson) local data = safeDecode(dataJson, 1024) if not data then return end local num = extractHouseNumber(tostring(data.houseNum or data.speakerID or "")) if not num then return end local spkID = type(data.speakerID) == "string" and string.sub(data.speakerID, 1, 32) or ("bluetooth_" .. num) local occupant = speakerOccupants[spkID] local sPid = tostring(playerID) local isOwner = houseOwners[num] and tostring(houseOwners[num].playerID) == sPid local isOccupant = occupant and tostring(occupant.playerID) == sPid if occupant and not isOccupant and not isOwner then return end local isOn = (data.isOn == true) if not isOn then speakerOccupants[spkID] = nil end local vol = tonumber(data.volume) if vol then vol = math.max(0, math.min(1.0, vol)) else vol = 1.0 end local tIdx = tonumber(data.trackIdx) if tIdx then tIdx = math.max(1, math.floor(tIdx)) else tIdx = 1 end speakerStates[num] = { speakerID = spkID, isOn = isOn, isPaused = (data.isPaused == true), trackIdx = tIdx, volume = vol } sendClientEvent(-1, "SmartHome_SyncSpeaker", { houseNum = num, speakerID = spkID, isOn = isOn, isPaused = speakerStates[num].isPaused, trackIdx = tIdx, volume = vol }) end function onSmartHomeRequestAppliance(playerID, dataJson) local data = safeDecode(dataJson, 1024) if not data or type(data.applianceType) ~= "string" or type(data.id) ~= "string" then return end local appType = data.applianceType if not VALID_APPLIANCES[appType] then return end local sanitizedId = string.sub(data.id, 1, 48) local key = appType .. "_" .. sanitizedId local isStop = (data.action == "stop" or data.action == "take" or data.state == "empty" or data.state == false or data.state == "false") local parsedHouse = data.houseNum and tostring(data.houseNum) or extractHouseNumber(sanitizedId) if isStop then activeAppliances[key] = nil applianceTimers[key] = nil else local durationSec = 600 if appType == "microwave" then durationSec = (tonumber(data.seconds) or 30) + 5 elseif appType == "coffee" then durationSec = 10 elseif appType == "oven" then local val = tonumber(data.time) or 0 if val <= 0 then return end durationSec = math.max(10, val) + 10 elseif appType == "washingmachine" then local val = tonumber(data.minutes) or 45 durationSec = math.max(10, val) + 5 elseif appType == "dishwasher" then local val = tonumber(data.minutes) or 90 durationSec = math.max(10, val) + 5 elseif appType == "bathtub" then durationSec = 600 elseif appType == "shower" or appType == "sink" then durationSec = 600 end applianceTimers[key] = os.time() + durationSec local serverStart = (appType == "bathtub" and data.state == "filling") and os.time() or nil activeAppliances[key] = { applianceType = appType, id = sanitizedId, houseNum = parsedHouse, action = type(data.action) == "string" and string.sub(data.action, 1, 16) or nil, state = data.state, stage = tonumber(data.stage), fillTime = tonumber(data.fillTime), serverStartTime = serverStart, seconds = tonumber(data.seconds), power = tonumber(data.power), type = type(data.type) == "string" and string.sub(data.type, 1, 24) or nil, strength = type(data.strength) == "string" and string.sub(data.strength, 1, 24) or nil, mode = type(data.mode) == "string" and string.sub(data.mode, 1, 24) or nil, temp = tonumber(data.temp) or tostring(data.temp or ""), spin = type(data.spin) == "string" and string.sub(data.spin, 1, 24) or nil, minutes = tonumber(data.minutes), time = tonumber(data.time), sanitize = (data.sanitize == true), heatedDry = (data.heatedDry == true) } end data.houseNum = parsedHouse sendClientEvent(-1, "SmartHome_SyncAppliance", data) end function onSmartHomeRequestCarWash(playerID, dataJson) local data = safeDecode(dataJson, 512) if not data or type(data.action) ~= "string" then return end local action = data.action if action ~= "enter" and action ~= "start" and action ~= "exit" and action ~= "reset" then return end if action == "exit" or action == "reset" then carWashState = nil carWashTimer = 0 elseif action == "enter" then carWashState = { action = "enter", vehId = tonumber(data.vehId), playerID = playerID, triggerName = type(data.triggerName) == "string" and string.sub(data.triggerName, 1, 32) or nil } carWashTimer = os.time() elseif action == "start" then carWashState = { action = "start", vehId = tonumber(data.vehId), playerID = playerID, triggerName = type(data.triggerName) == "string" and string.sub(data.triggerName, 1, 32) or nil } carWashTimer = 0 end sendClientEvent(-1, "SmartHome_SyncCarWash", data) end function onSmartHomeRequestDriveThruOrder(playerID, dataJson) local data = safeDecode(dataJson, 4096) if not data or type(data.vehId) ~= "number" or type(data.order) ~= "table" then return end if #data.order > 25 then return end sendClientEvent(-1, "SmartHome_SyncDriveThruOrder", data) end function onSmartHomeRequestDriveThruPickup(playerID, dataJson) local data = safeDecode(dataJson, 512) if not data or type(data.vehId) ~= "number" then return end sendClientEvent(-1, "SmartHome_SyncDriveThruPickup", data) end function onSmartHomeRequestDriveThruVoice(playerID, dataJson) local data = safeDecode(dataJson, 512) if not data or type(data.soundFile) ~= "string" then return end if string.find(data.soundFile, "%.%.") then return end sendClientEvent(-1, "SmartHome_SyncDriveThruVoice", data) end local function handleServerTick(dt) local now = os.time() for key, expireTime in pairs(applianceTimers) do if now >= expireTime then local appData = activeAppliances[key] activeAppliances[key] = nil applianceTimers[key] = nil if appData then local stopState = (appData.applianceType == "bathtub" and "empty" or false) sendClientEvent(-1, "SmartHome_SyncAppliance", { applianceType = appData.applianceType, id = appData.id, action = "stop", state = stopState, houseNum = appData.houseNum }) end end end for key, appData in pairs(activeAppliances) do if appData.applianceType == "bathtub" and appData.state == "filling" and appData.serverStartTime then local elapsed = now - appData.serverStartTime if (appData.fillTime or 0) + elapsed >= 90 then appData.state = "full" appData.stage = 3 appData.fillTime = 90 appData.serverStartTime = nil sendClientEvent(-1, "SmartHome_SyncAppliance", { applianceType = "bathtub", id = appData.id, action = "full", state = "full", stage = 3, fillTime = 90, houseNum = appData.houseNum }) end end end if carWashState and carWashState.action == "enter" and carWashTimer > 0 then if now - carWashTimer >= 10 then carWashState = nil carWashTimer = 0 sendClientEvent(-1, "SmartHome_SyncCarWash", { action = "reset" }) end end end function onTick(dt) handleServerTick(dt) end function onPlayerJoin(playerID) clearPlayerOwnership(playerID) end function onPlayerDisconnect(playerID) clearPlayerOwnership(playerID) end function onInit() if serverInitialized then return end serverInitialized = true local regEvent = (MP and type(MP.RegisterEvent) == "function" and MP.RegisterEvent) or (type(RegisterEvent) == "function" and RegisterEvent) if regEvent then regEvent("SmartHome_RequestClaim", "onSmartHomeRequestClaim") regEvent("SmartHome_RequestUnclaim", "onSmartHomeRequestUnclaim") regEvent("SmartHome_RequestUnclaimedCleanup", "onSmartHomeRequestUnclaimedCleanup") regEvent("SmartHome_RequestLockToggle", "onSmartHomeRequestLockToggle") regEvent("SmartHome_RequestTeleport", "onSmartHomeRequestTeleport") regEvent("SmartHome_RequestACToggle", "onSmartHomeRequestACToggle") regEvent("SmartHome_RequestSpeakerConnect", "onSmartHomeRequestSpeakerConnect") regEvent("SmartHome_RequestSpeakerDisconnect", "onSmartHomeRequestSpeakerDisconnect") regEvent("SmartHome_RequestSpeakerState", "onSmartHomeRequestSpeakerState") regEvent("SmartHome_RequestAppliance", "onSmartHomeRequestAppliance") regEvent("SmartHome_RequestCarWash", "onSmartHomeRequestCarWash") regEvent("SmartHome_RequestDriveThruOrder", "onSmartHomeRequestDriveThruOrder") regEvent("SmartHome_RequestDriveThruPickup", "onSmartHomeRequestDriveThruPickup") regEvent("SmartHome_RequestDriveThruVoice", "onSmartHomeRequestDriveThruVoice") regEvent("SmartHome_RequestFullSync", "onSmartHomeRequestFullSync") regEvent("onPlayerJoin", "onPlayerJoin") regEvent("onPlayerDisconnect", "onPlayerDisconnect") end end