local defaultCodes = require(script.DefaultCodes) local dataStore = nil local DataStoreService = game:GetService("DataStoreService") dataStoreKey = script.Parent.dataStoreKey.Value if dataStoreKey ~= "" then dataStore = DataStoreService:GetDataStore('securityCodes-' .. script.Parent.dataStoreKey.Value or "XP-99") else -- Make virtual datastore for offline use dataStore = { _data = {}, GetAsync = function(self, key) return self._data[key] end, SetAsync = function(self, key, value) self._data[key] = value end } end function setup(parentScript) -- dont touch local version = "v1.2.4" -- codes object, will be swapped for data store later codes = dataStore:GetAsync("codes") if not codes then codes = defaultCodes dataStore:SetAsync("codes", codes) end -- Make a settings object mapped to the values in parentScript.Parent.Settings settings = {} for i, e in pairs(parentScript.Parent.Settings:GetChildren()) do settings[e.Name] = e.Value end power = false chime = true armState = 0 delay = 0 instant = false faults = {} fault = false alarms = {} fires = {} zones = {} keypads = {} countdown = nil --[[ armState 0 is disarmed 1 is armed stay 2 is armed away 3 is alarm 4 is fire alarm 5 is armed instant (stay no entry delay) 6 is armed maximum (away no entry delay) delay 0 is inactive 1 is exit delay When active, use slow beep, show ARMED ***AWAY***\nYou may exit now or ARMED ***STAY***\nYou may exit now depending on state. During last 10 seconds of delay, switch to fast beep, at each second check if the system has been armed, if it has drop out of the loop 2 is entry delay. When active, use slow beep, show "DISARM SYSTEM\nOr alarm will occur", during last 10 seconds of delay, switch to fast beep. At each second check if the system has been disarmed, if it has drop out of the loop ]] ux = require(parentScript.Parent.ScriptModules:WaitForChild("UX")) -- Get keypads for i, e in pairs(parentScript.Parent.Keypads:GetChildren()) do if e:IsA("Model") then table.insert(keypads, e) end end -- Get zone for i, e in ipairs(parentScript.Parent.Zones:GetChildren()) do -- add a task that constantly checks e.tripped.Value, and set zones[i] to the value of e.tripped.Value zones[e.Name] = e end -- Funcs function rightAlignString(str) local strLen = string.len(str) local padding = 16 - strLen local paddingStr = "" for i = 1, padding do paddingStr = paddingStr .. " " end return paddingStr .. str end function playAud(sound) if not sound.Playing then sound:Play() end end function stopAud(sound) if sound.Playing then sound:Stop() end end local curTone = 0 local lastTone = 0 -- only really used to go back from button pulse to previous tone local beeping = false function changeTone(tone) curTone = tone end function beep(keypad) if not power then return end if not beeping then task.spawn(function() beeping = true lastTone = curTone playAud(keypad.Display.hi) wait(0.08) stopAud(keypad.Display.hi) wait(0.08) beeping = false end) end end function ding(count) -- Play a ding sound count times, each lasting .3 seconds with a .1 second pause between each task.spawn(function() for i = 1, count do if not power then return end for i, keypad in ipairs(keypads) do playAud(keypad.Display.ding) end wait(0.2) -- if its not the last ding, stop the sound if i ~= count then for i, keypad in ipairs(keypads) do stopAud(keypad.Display.ding) end end end end) end --[[ 0 no tone 1 constant 2 pulse (for number input) 3 slow pulse (entry/exit delay main) 4 fast pulse (end of exit/entry delay and trouble) ]] function switchKeypadAudio(enabled) for i,e in ipairs(script.KeypadAudio:GetChildren()) do if e.Name ~= enabled then e.Disabled = true else e.Enabled = true end end end function audioLoop() while true do wait() if not power then curTone = 0 end if curTone == 0 then -- Stop tone switchKeypadAudio("off") elseif curTone == 1 then -- Constant tone switchKeypadAudio("constant") elseif curTone == 2 then -- Slow pulse tone switchKeypadAudio("slowpulse") elseif curTone == 3 then -- Fast pulse tone switchKeypadAudio("fastpulse") elseif curTone == 4 then -- HiLo tone alarm switchKeypadAudio("hilo") elseif curTone == 5 then -- Fire alarm tone switchKeypadAudio("fire") end end end function alignText(leftText, rightText, totalWidth) local leftLength = #leftText local rightLength = #rightText local spaceBetween = totalWidth - (leftLength + rightLength) -- If the total length of both texts exceeds the totalWidth, return concatenated textif spaceBetween < 0thenreturn leftText .. rightText return leftText .. string.rep(" ", spaceBetween) .. rightText end function displayLoop() while true do wait() if not power then for i, keypad in ipairs(keypads) do ux.setDisplays(keypads, "", "") end else if armState == 0 then -- We are disarmed, check if there are any faults if #faults > 0 then -- for however many faults there are, display them one at a time based on faultPage (gets updated every 2 seconds, or when * is pressed) --ux.setDisplays(keypads, "FAULT " .. table.concat(faults, ", ", faultPage, faultPage + 1)) ux.setLEDs(keypads, "ready", false) fault = true ux.setDisplays(keypads, "FAULT " .. faults[1]) else ux.setLEDs(keypads, "ready", true) ux.setLEDs(keypads, "armed", false) if not chime then ux.setDisplays(keypads, "****DISARMED****", " READY TO ARM ") else ux.setDisplays(keypads, " DISARMED CHIME ", " READY TO ARM ") end end elseif armState == 1 then -- We are armed stay ux.setLEDs(keypads, "armed", true) ux.setLEDs(keypads, "ready", false) if delay == 1 then ux.setDisplays(keypads, "ARMED ***STAY***", "You may exit now") elseif delay == 2 then ux.setDisplays(keypads, "DISARM SYSTEM", "Or alarm occurs.") else ux.setDisplays(keypads, "ARMED ***STAY***") end elseif armState == 2 then -- We are armed away ux.setLEDs(keypads, "armed", true) ux.setLEDs(keypads, "ready", false) if delay == 1 then if settings.showCountdown then ln2 = alignText("May Exit Now", tostring(settings.exitDelay - countdown), 16) else ln2 = "You may exit now" end ux.setDisplays(keypads, "ARMED ***AWAY***", ln2) elseif delay == 2 then ux.setDisplays(keypads, "DISARM SYSTEM", "Or alarm occurs.") else ux.setDisplays(keypads, "ARMED ***AWAY***", "***ALL SECURE***") end elseif armState == 3 then -- We are in alarm ux.setLEDs(keypads, "ready", false) if curTone ~= 4 then changeTone(4) end -- show the zone number of the alarmed zone ux.setDisplays(keypads, "ALARM " .. tostring(alarms[1] or "")) elseif armState == 4 then -- We are in fire alarm ux.setLEDs(keypads, "ready", false) -- show the zone number of the alarmed zone ux.setDisplays(keypads, "FIRE " .. tostring(fires[1] or "")) elseif armState == 5 then -- Armed instant ux.setLEDs(keypads, "armed", true) ux.setLEDs(keypads, "ready", false) if delay == 1 then ux.setDisplays(keypads, "ARMED *INSTANT*", "You may exit now") else ux.setDisplays(keypads, "ARMED *INSTANT*") end elseif armState == 6 then -- Armed max ux.setLEDs(keypads, "armed", true) ux.setLEDs(keypads, "ready", false) if delay == 1 then if settings.showCountdown then ln2 = alignText("May Exit Now", tostring(settings.exitDelay - countdown), 16) else ln2 = "You may exit now" end ux.setDisplays(keypads, "ARMED *MAXIMUM*", ln2) else ux.setDisplays(keypads, "ARMED *MAXIMUM*", "***ALL SECURE***") end end end end end local input = {} local keyClear = nil local authenticated = false local inputCount = 0 local inputMode = 0 local authedUser = nil lockout = false authAttempts = 0 function handleInput(key) if not power then return end if lockout then task.spawn(function() curTone = 1 wait(2) curTone = 0 end) return end -- eventually do long beep 2 seconds here if key == "*" then deauth() return end keyClear = math.random(1, 10000) task.spawn(function() inputTimeout(keyClear) end) if not authenticated then -- Should work like a honeywell vista, for every number entered 4 or more in buffer, check if the last 4 numbers are a valid code. For every 4 numbers entered, if its not a valid code add one to the attempts table.insert(input, key) if #input >= 4 then local code = table.concat(input, "", #input - 3, #input) if table.find(codes, code) then authenticated = true inputMode = 1 authAttempts = 0 keyClear = nil input = {} authedUser = code else authAttempts = authAttempts + 1 if authAttempts >= 3 then task.spawn(lockoutFunc) end keyClear = nil input = {} end end elseif authenticated and inputMode == 1 then wait(math.random(.5, 1.5)) if key == "1" then disarm() deauth() elseif key == "2" then if armState ~= 0 then return end armAway() deauth() elseif key == "3" then if armState ~= 0 then return end armStay() deauth() elseif key == "4" then -- Arm Max if armState ~= 0 then return end armMax() deauth() elseif key == "5" then -- Test (TODO) elseif key == "6" then -- Bypass (TODO) elseif key == "7" then -- Instant if armState ~= 0 then return end armInstant() deauth() elseif key == "8" then -- If the code used was code 1, and the system is disarmed, Set inputMode to 2 (code change) -- See what index the code is at, if its index 1, its the right code, set mode to 2. do NOT hardcode the code here print(codes[1] .. " : " .. authedUser) if codes[1] == authedUser then inputMode = 2 else ding(1) deauth() end elseif key == "9" then toggleChime() deauth() end elseif authenticated and inputMode == 2 then -- Keep adding inputs together until we have 2+4+4 in the buffer, if both codes match, and arent being used, set the new code to the code at index of the first 2 digits -- input data is: 2 digits: user id/index, 4 digits: new code, 4 digits: new code verify table.insert(input, key) -- check if digit 3 is a hash and 4 a 0, if so delete the user if #input >= 4 and input[3] == "#" and input[4] == "0" then local uid = table.concat(input, "", 1, 2) if tonumber(uid) == 1 then ding(4) return else codes[tonumber(uid)] = nil dataStore:SetAsync("codes", codes) ding(1) end deauth() else if #input >= 10 then local uid = table.concat(input, "", 1, 2) local newCode = table.concat(input, "", 3, 6) local newCodeVerify = table.concat(input, "", 7, 10) if newCode == newCodeVerify then if not table.find(codes, newCode) then codes[tonumber(uid)] = newCode dataStore:SetAsync("codes", codes) ding(1) else ding(4) end else ding(4) end deauth() end end end end function deauth() keyClear = nil authenticated = false input = {} inputMode = 0 end function toggleChime() if chime then ding(2) else ding(1) end chime = not chime deauth() end function disarm() deauth() task.spawn(function() -- Reset any smoke zones for i,e in pairs(zones) do if e:FindFirstChild("Reset") then e.Reset.Value = true wait(5) e.Reset.Value = false end end end) wait(.1) armState = 0 changeTone(0) alarms = {} fires = {} faults = {} ding(1) end function armStay() if #faults > 0 then ding(1) return end armState = 1 delay = 1 task.spawn(function() ding(3) for i = 1, settings.exitDelay do if armState ~= 1 then delay = 0 countdown = 0 return end countdown = i wait(1) if i >= settings.exitDelay then delay = 0 countdown = 0 end end end) end function armAway() if #faults > 0 then ding(1) return end armState = 2 delay = 1 task.spawn(function() changeTone(2) for i = 1, settings.exitDelay do if armState ~= 2 then changeTone(0) delay = 0 countdown = 0 return end countdown = i wait(1) if i >= settings.exitDelay - 10 then changeTone(3) end if i >= settings.exitDelay then changeTone(0) delay = 0 countdown = 0 end end end) end function armMax() if #faults > 0 then ding(1) return end armState = 6 delay = 1 task.spawn(function() changeTone(2) for i = 1, settings.exitDelay do if armState ~= 6 then changeTone(0) delay = 0 countdown = 0 return end countdown = i wait(1) if i >= settings.exitDelay - 10 then changeTone(3) end if i >= settings.exitDelay then changeTone(0) delay = 0 countdown = 0 end end end) end function armInstant() if #faults > 0 then ding(1) return end armState = 5 delay = 1 task.spawn(function() ding(3) for i = 1, settings.exitDelay do if armState ~= 5 then delay = 0 countdown = 0 return end countdown = i wait(1) if i >= settings.exitDelay then delay = 0 countdown = 0 end end end) end function inputTimeout(myNum) wait(10) if keyClear ~= myNum then return end authenticated = false input = {} end function lockoutFunc() lockout = true changeTone(1) wait(2) changeTone(0) wait(60) lockout = false end -- Button Click Handler (each keypad has Buttons folder with parts that have a clickdetector nested within, add an event handler for each that adds the button to the input buffer) function buttonClickHandler(button) button.MouseClick:Connect(function() if not power then return end task.spawn(function() beep(button.Parent.Parent.Parent) end) wait(0.05) handleInput(button.Parent.Name) end) end for i, keypad in ipairs(keypads) do for j, button in ipairs(keypad.Buttons:GetChildren()) do buttonClickHandler(button.ClickDetector) end end -- Arming Functions function boot() task.spawn(audioLoop) power = true wait() ux.displayPower(keypads, true) ux.setLEDs(keypads, "*", true) ux.setDisplays(keypads, "################", "################") changeTone(1) wait(1) changeTone(0) ux.setLEDs(keypads, "*", false) ux.setLEDs(keypads, "power", true) ux.setDisplays(keypads, "Busy - Standby", rightAlignString(version)) wait(5) task.spawn(displayLoop) task.spawn(handleZones) task.spawn(sirenLoop) task.spawn(stateLoop) end function stateLoop() while true do if #alarms > 0 then script.Parent.Reporting.alarmZone.Value = alarms[1] else script.Parent.Reporting.alarmZone.Value = -1 end if #fires > 0 then script.Parent.Reporting.fireZone.Value = fires[1] else script.Parent.Reporting.fireZone.Value = -1 end script.Parent.Reporting.armState.Value = armState script.Parent.Reporting.delay.Value = delay script.Parent.Reporting.curTone.Value = curTone wait(0) end end function handleZones() while true do wait() -- Always: If a zone is tripped, add it to the faults table if its not already there -- If the zone is not tripped remove it from the faults table if its there -- If the zone is tripped and the system is armed, check if its an instant zone, if it is, set the alarm state to 3, otherwise trigger entry delay -- If the system is in alarm, check if the zone is tripped, if it is, add it to the alarms table if its not already there for i, zone in pairs(zones) do -- if the zone is tripped add it to the faults table if its not already there if zone.tripped.Value then -- if the zone is already in faults, continue if not table.find(faults, zone.Name) then if not table.find(faults, zone.Name) then table.insert(faults, zone.Name) end if zone.isFire.Value then armState = 4 changeTone(5) table.insert(fires, zone.Name) end if zone.alwaysOn.Value then -- This zone is a 24/7 audible zone, go straight to alarm armState = 3 changeTone(4) table.insert(alarms, zone.Name) end if armState == 0 then if zone:FindFirstChild("chime") then if chime and zone.chime.Value then ding(3) end end elseif armState == 1 and delay == 0 then -- if the zone is interior, ignore if zone.interior.Value then -- ignore else -- Check if the zone is instant or not -- If it is, set the alarm state to 3 -- If it is not, trigger entry delay if zone.instant.Value then -- Add to alarms table.insert(alarms, zone.Name) armState = 3 changeTone(4) else delay = 2 task.spawn(function() table.insert(alarms, zone.Name) changeTone(2) for i = 1, settings.entryDelay do if armState ~= 1 then changeTone(0) delay = 0 return end wait(1) if i >= settings.entryDelay - 10 then changeTone(3) end if i >= settings.entryDelay then -- at this point the system has not been disarmed, go to alarm changeTone(4) delay = 0 armState = 3 end end end) end end elseif armState == 2 and delay == 0 then -- Check if the zone is instant or not -- If it is, set the alarm state to 3 -- If it is not, trigger entry delay if zone.instant.Value then -- Add to alarms table.insert(alarms, zone.Name) armState = 3 changeTone(4) else table.insert(alarms, zone.Name) delay = 2 task.spawn(function() changeTone(2) for i = 1, settings.entryDelay do if armState ~= 2 then changeTone(0) delay = 0 return end wait(1) if i >= settings.entryDelay - 10 then changeTone(3) end if i >= settings.entryDelay then -- at this point the system has not been disarmed, go to alarm changeTone(4) delay = 0 armState = 3 end end end) end elseif armState == 3 then -- if the zone is already in alarms, continue if not table.find(alarms, zone.Name) then table.insert(alarms, zone.Name) changeTone(4) end elseif armState == 5 and delay == 0 then -- Armed instant, if not zone.interior.Value then table.insert(alarms, zone.Name) armState = 3 changeTone(4) end elseif armState == 6 and delay == 0 then table.insert(alarms, zone.Name) armState = 3 changeTone(4) end end else if table.find(faults, zone.Name) then table.remove(faults, table.find(faults, zone.Name)) end end end end end function sirenLoop() -- For external sirens while true do wait() if armState == 3 then -- Turn on siren for i,e in ipairs(parentScript.Parent.Sirens:GetChildren()) do if not e.Speaker.siren.playing then e.Speaker.siren:Play() end end elseif armState == 4 then -- Fire alarm -- Handle dedicated fire sirens for i,e in ipairs(parentScript.Parent.FireSirens:GetChildren()) do if not e.Speaker.siren.playing then e.Speaker.siren:Play() end end -- Make this do temporal 3 for i = 1, 3 do for i,e in ipairs(parentScript.Parent.Sirens:GetChildren()) do if not e.Speaker.siren.playing then e.Speaker.siren:Play() end end wait(.5) for i,e in ipairs(parentScript.Parent.Sirens:GetChildren()) do if e.Speaker.siren.playing then e.Speaker.siren:Stop() end end wait(.5) end wait(1) else -- Turn off siren for i,e in ipairs(parentScript.Parent.Sirens:GetChildren()) do if e.Speaker.siren.playing then e.Speaker.siren:Stop() end end for i,e in ipairs(parentScript.Parent.FireSirens:GetChildren()) do if e.Speaker.siren.playing then e.Speaker.siren:Stop() end end end end end boot() end setup(script)