This commit is contained in:
lifestorm
2024-08-05 18:40:29 +03:00
parent 9f505a0646
commit c6d9b6f580
8044 changed files with 1853472 additions and 21 deletions

View File

@@ -0,0 +1,198 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
ix.phone = ix.phone or {}
ix.phone.switch = ix.phone.switch or {}
-- a volatile table for caching ongoing connections
ix.phone.switch.connections = ix.phone.switch.connections or {}
function ix.phone.switch:ConnectionValid(connID)
if (self.connections[connID] == false) then
return false
end
return istable(self.connections[connID])
end
function ix.phone.switch:buildNewConnectionNode(connID, extID, extNum)
-- helper function to create source requests for connections
-- constructs a table that can be used by ix.phone.switch:connect()
if (!self:ConnectionValid(connID)) then
return
end
if (!self.connections[connID].nodes) then
self.connections[connID].nodes = {}
end
local nodeID = #self.connections[connID].nodes + 1
self.connections[connID].nodes[nodeID] = {}
self.connections[connID].nodes[nodeID]["exchange"] = extID
self.connections[connID].nodes[nodeID]["extension"] = extNum
end
function ix.phone.switch:buildNewConnection()
-- helper function to that creates a new connection
-- attempt to reuse a freshly terminated connection
for id, conn in ipairs(self.connections) do
if (conn == false) then
self.connections[id] = {}
return id
end
end
-- no terminated connections
connectionID = #self.connections + 1
self.connections[connectionID] = {}
return connectionID
end
function ix.phone.switch:Disconnect(connID, noNotify)
-- disconnects provided connection at connID
-- if notify is set, then it wont notify listeners that the connection is terminated
if (!istable(self.connections[connID])) then
return
end
local _nodes = self.connections[connID].nodes
for _, node in ipairs(_nodes) do
local recievers = self:getReceivers(node.exchange, node.extension)
if (!istable(recievers) or table.Count(recievers) < 1) then
continue
end
for _, reciever in ipairs(recievers) do
local ent = self.endpoints:GetEndpoint(reciever.endID)
if (ent and ent.HangUp and ent.isRinging) then
ent:HangUp()
end
end
local listeners = self:getListenersFromRecvs(recievers)
if (!listeners or table.Count(listeners) < 1) then
continue
end
for _, listener in ipairs(listeners) do
self:DisconnectClient(listener)
end
end
self.connections[connID] = false
end
-- disconnects the client's status and notifies them of said status change
-- IMPORTANT: Make sure you destroy the connection too!
function ix.phone.switch:DisconnectClient(client)
if (!client or !IsValid(client)) then
return ErrorNoHaltWithStack("Attempt to disconnect invalid client "..tostring(client))
end
local character = client:GetCharacter()
if (!istable(character)) then
return ErrorNoHaltWithStack("Attempt to disconnect client with invalid character "..tostring(character))
end
self:ResetCharVars(character)
self:NotifyStatusChange(client, false, false)
end
function ix.phone.switch:NotifyStatusChange(client, bCallActive, bInCall)
net.Start("ixConnectedCallStatusChange")
net.WriteBool(bCallActive)
net.WriteBool(bInCall)
net.Send(client)
end
function ix.phone.switch:NotifyAllListenersOfStatusChange(endID, bCallActive, bInCall)
local listeners = self.endpoints:GetListeners(endID)
if (!listeners or table.Count(listeners) < 1) then
return -- no need to do anything; no listeners
end
for _, listener in ipairs(listeners) do
-- notify all listeners of their call status change
self:NotifyStatusChange(listener, bCallActive, bInCall)
end
end
function ix.phone.switch:getListenersFromRecvs(recievers)
local listeners = {}
for k, recv in ipairs(recievers) do
-- there will almost always be one reciever.. but treating this as a list in case we ever do 'conference calls'
local _listeners = self.endpoints:GetListeners(recv.endID)
if (istable(_listeners)) then
for _, listener in ipairs(_listeners) do
listeners[table.Count(listeners) + 1] = listener
end
end
end
return listeners
end
function ix.phone.switch:getReceivers(extID, extNum)
local conn = self:GetActiveConnection(extID, extNum)
if (!istable(conn)) then
return
end
return self:getSourceRecieversFromConnection(conn.targetConnID,
conn.sourceNodeID)
end
function ix.phone.switch:GetListeners(extID, extNum)
return self:getListenersFromRecvs(self:getReceivers(extID, extNum))
end
function ix.phone.switch:GetReceivers(extID, extNum)
local conn = self:GetActiveConnection(extID, extNum)
if (!istable(conn)) then
return
end
return self:getSourceRecieversFromConnection(conn.targetConnID,
conn.sourceNodeID)
end
-- returns the active connection in the form of {"targetConnID", "sourceNodeID"} if one is present
function ix.phone.switch:GetActiveConnection(extID, extNum)
for connID, conn in ipairs(self.connections) do
if (conn == false) then
continue
end
for nodeID, node in ipairs(conn.nodes) do
if (node["exchange"] == extID and node["extension"] == extNum) then
-- source is present in this connection
return {targetConnID = connID, sourceNodeID = nodeID}
end
end
end
end
-- returns the actively connected (except for the source) recievers for a given connection
function ix.phone.switch:getSourceRecieversFromConnection(connID, sourceNodeID)
local res = {}
for nodeID, node in ipairs(self.connections[connID].nodes) do
if (nodeID != sourceNodeID) then
-- we want to return this as it exists in the exchange as that will give us
-- extra details the node tree does not contain such as name and endID
res[#res + 1] = self:GetDest(node["exchange"], node["extension"])
end
end
return res
end

View File

@@ -0,0 +1,123 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
ix.phone = ix.phone or {}
ix.phone.switch = ix.phone.switch or {}
-- a flat list of all the entities. each entity has an associated 'listeners' table which contains all of the players actively connected to that entity
ix.phone.switch.endpoints = ix.phone.switch.endpoints or {}
function ix.phone.switch.endpoints:exists(id)
return self[id] != nil
end
function ix.phone.switch.endpoints:entExists(entIdx)
for id, ent in ipairs(self) do
if (entIdx == ent:EntIndex()) then
return id
end
end
return nil
end
function ix.phone.switch.endpoints:Register(ent)
-- assigns an ID. if the ent already exists then it will return the existing id
-- we need to have our own ID here rather than the index because the entity index might change but
-- in that case the id shouldn't
local entExists = self:entExists(ent:EntIndex())
if (entExists != nil) then
return nil
end
local newID = math.random(1000, 9999)
if (self:exists(newID)) then
return nil
end
self[newID] = ent
return newID
end
function ix.phone.switch.endpoints:DeRegister(id)
self[id] = nil
end
function ix.phone.switch.endpoints:GetEndpoint(id)
-- returns the associated entity table
if (self:exists(id)) then
return self[id]
end
end
function ix.phone.switch.endpoints:AddListener(id, client)
if (!istable(self[id].listeners)) then
self[id].listeners = {}
end
for _, listener in ipairs(self[id].listeners) do
if (listener == client) then
return
end
end
self[id].listeners[table.Count(self[id].listeners) + 1] = client
end
function ix.phone.switch.endpoints:RmListener(id, client)
if (!istable(self[id].listeners)) then
return
end
for k, listener in ipairs(self[id].listeners) do
if (listener == client) then
self[id].listeners[k] = nil
end
end
end
function ix.phone.switch.endpoints:GetListeners(id)
return self[id].listeners
end
function ix.phone.switch.endpoints:RingEndpoint(id, callback)
-- rings and endpoint and, if the phone is picked up, it will call callback as true. otherwise false
-- if the destination is unavailable or busy then it will return nil
local ent = self:GetEndpoint(id)
if (ent.inUse or ent.isRinging) then
return nil
end
ent:EnterRing(callback)
end
function ix.phone.switch.endpoints:GetPlayersInRadiusFromPos(pos, radius)
local entsInside = ents.FindInSphere(pos, radius)
local res = {}
for _, _ent in ipairs(entsInside) do
if (_ent:IsPlayer() and _ent.GetCharacter and _ent:GetCharacter()) then
res[_ent:SteamID64()] = _ent
end
end
return res
end
-- returns a list of players within X radius of the endpoint
function ix.phone.switch.endpoints:GetPlayersInRadius(id, radius)
local ent = self:GetEndpoint(id)
if (!ent) then
return
end
return self:GetPlayersInRadiusFromPos(ent:GetPos(), radius)
end

View File

@@ -0,0 +1,100 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
ix.phone = ix.phone or {}
ix.phone.switch = ix.phone.switch or {}
-- a table used for managing pbxs and their members
ix.phone.switch.exchanges = ix.phone.switch.exchanges or {}
--[[ The following is some boiler plate code for creating and managing extensions and exchanges
In practice these things are just keys in a table and in the DB but it is probably best
if you use the functions here and not directly modify ix.phone.switch.exchanges directly!
This is necessary to keep the switching code readable
]]
function ix.phone.switch:AddExchange(exID)
if (self:ExchangeExists(exID)) then
return false
end
self.exchanges[exID] = {}
return true
end
function ix.phone.switch:RmExchange(exID)
if (!self:ExchangeExists(exID)) then
return false
end
self.exchanges[exID] = nil
return true
end
function ix.phone.switch:ExchangeExists(exID)
return self.exchanges[exID] != nil
end
function ix.phone.switch:DestExists(exID, extNum)
if (self.exchanges[exID] != nil) then
return self.exchanges[exID][extNum] != nil
else
return false
end
end
function ix.phone.switch:AddDest(exID, extNum, extName, endID)
-- returns false if destination exists or exchange doesn't
-- set noDB if you do not wish to store this destination to the database
if (self:DestExists(exID, extNum)) then
return false
end
self.exchanges[exID][extNum] = {}
self.exchanges[exID][extNum]["name"] = extName or ""
self.exchanges[exID][extNum]["endID"] = endID
return true
end
function ix.phone.switch:GetDest(exID, extNum)
if (!self:DestExists(exID, extNum)) then
return false
end
return self.exchanges[exID][extNum]
end
function ix.phone.switch:RmDest(exID, extNum)
-- returns false if destination does not exist
if (!self:DestExists(exID, extNum)) then
return false
end
self.exchanges[exID][extNum] = nil
return true
end
function ix.phone.switch:MvDest(fromExID, fromExtNum, toExID, toExtNum)
if (!self:RmDest(fromExID, fromExtNum)) then
return false
end
return self:AddDest(toExID, toExtNum)
end
function ix.phone.switch:SetName(exID, extNum, name)
if (!self:DestExists(exID, extNum)) then
return false
end
self.endpoints[self.exchanges[exID][extNum]["endID"]].currentName = name
return true
end

View File

@@ -0,0 +1,402 @@
--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]
-- this is a loose implementation of a virtual private business exchange (vPBX)
ix.phone = ix.phone or {}
ix.phone.switch = ix.phone.switch or {}
ix.phone.switch.lineTestPBX = 9
ix.phone.switch.lineTestExt = 999 -- reserved for testing
do
-- set up ent to use for 2 way call testing
timer.Simple(10, function()
local lineTestEnt = ents.Create("landline_phone")
lineTestEnt:SetPos(Vector(0, 0, 0))
lineTestEnt:Spawn()
lineTestEnt:SetNoDraw(true)
-- if we dont do this then our landline point ent will fall through the world
-- and get deleted
local _phys = lineTestEnt:GetPhysicsObject()
_phys:EnableGravity(false)
_phys:EnableMotion(false)
_phys:Sleep()
lineTestEnt.currentName = "Line Test"
lineTestEnt.currentPBX = ix.phone.switch.lineTestPBX
lineTestEnt.currentExtension = ix.phone.switch.lineTestExt
-- yes we need a real entity for this. doesn't have to be a landline but might as well
ix.phone.switch.lineTestEnt = lineTestEnt
end)
end
-- (optional)
-- takes in a dial sequence in the format of (exchange)(extension)
-- ex: 1 234
-- it returns it into a table as {"exchange", "extension"}
function ix.phone.switch:decodeSeq(dialSeq)
-- dial sequences must be strings, but must be a real number as well and must be 4 digits
if (type(dialSeq) != "string" or tonumber(dialSeq) == nil) then
return nil
elseif (string.len(dialSeq) > 4 or string.len(dialSeq) < 3) then
return nil
end
local exchange = nil
if (string.len(dialSeq) != 3) then -- otherwise it is a local dial (to endpoint in own exchange)
exchange = tonumber(string.sub(dialSeq, 0, 1))
if (exchange == nil or exchange < 1) then
return nil
end
end
-- the remaining digits should be the extension
local ext = tonumber(string.sub(dialSeq, 2, 4))
if (ext == nil or ext < 1) then
return nil
end
return {["exchange"] = exchange, ["extension"] = ext}
end
function ix.phone.switch:BuildNewTwoWayConnection(sourceExc, sourceExt, destExc, destExt)
local connID = self:buildNewConnection()
self:buildNewConnectionNode(connID, sourceExc, sourceExt)
self:buildNewConnectionNode(connID, destExc, destExt)
return connID
end
function ix.phone.switch:Dial(sourceExc, sourceExt, dialSeq)
if (!self:DestExists(sourceExc, sourceExt)) then
return self.DialStatus.SourceNotExist
end
--[[
Decode the user provided dial sequence into a switchable pair of of pbx and ext
]]
if (!istable(dialSeq) and #dialSeq < 1) then
return self.DialStatus.NoDialSeq
end
local decodedDest = self:decodeSeq(dialSeq)
if (!istable(decodedDest)) then
return self.DialStatus.CannotDecodeDial
end
if (decodedDest.exchange == self.lineTestPBX and decodedDest.extension == self.lineTestExt) then
self:StartLineTest(sourceExc, sourceExt)
return self.DialStatus.DebugMode
end
if (decodedDest.exchange == nil) then
decodedDest.exchange = sourceExc
end
if (!self:DestExists(decodedDest.exchange, decodedDest.extension)) then
return self.DialStatus.NumberNotFound
end
--[[
Get the endpoint IDs and corresponding entities for source & destination
]]
local destination = self:GetDest(decodedDest.exchange, decodedDest.extension)
local source = self:GetDest(sourceExc, sourceExt)
if (!destination.endID or !source.endID) then
return self.DialStatus.EndpointNotFound
end
local destEnt = self.endpoints:GetEndpoint(tonumber(destination.endID))
local sourceEnt = self.endpoints:GetEndpoint(tonumber(source.endID))
--[[
Check if the line is busy
]]
local _conn = self:GetActiveConnection(decodedDest.exchange, decodedDest.extension)
if (_conn and istable(_conn) or destEnt.offHook) then
return self.DialStatus.LineBusy
end
--[[
Build new connection for source & dest with the pbxs and exts.
]]
local connID = self:BuildNewTwoWayConnection(sourceExc, sourceExt,
decodedDest.exchange, decodedDest.extension)
--[[
Setup the callbacks and start the call
]]
local ringCallback = function(status)
if (!status) then -- call did not go through so we need to clean up
self:Disconnect(connID)
end
self:NotifyAllListenersOfStatusChange(tonumber(destination.endID), status, status)
self:NotifyAllListenersOfStatusChange(tonumber(source.endID), status, status)
end
destEnt:EnterRing(ringCallback) -- set the target as ringing
local hangUpCallback = function(status)
-- cleanup
self:Disconnect(connID)
end
destEnt.hangUpCallback = hangUpCallback
sourceEnt.hangUpCallback = hangUpCallback
return self.DialStatus.Success
end
-- returns back a list of player entities that are listening to the phone this character is speaking into
function ix.phone.switch:GetCharacterActiveListeners(character)
if (!istable(character)) then
return
end
local connMD = character:GetLandlineConnection()
if (!connMD) then
return
end
return self:GetListeners(connMD.exchange, connMD.extension)
end
function ix.phone.switch:GetPlayerActiveListeners(client)
local character = client:GetCharacter()
if (!istable(character)) then
return nil
end
return self:GetCharacterActiveListeners(character)
end
-- rudely hangs up every single active call related to this character
-- typically used when the player disconnects or switches chars mid call
function ix.phone.switch:DisconnectActiveCallIfPresentOnClient(client)
local character = client:GetCharacter()
if (!istable(character)) then
return
end
local connMD = character:GetLandlineConnection()
if (!istable(connMD) and !connMD["active"]) then
-- probably ran hangup on a phone someone else was speaking on
-- we should allow this in the future (maybe?) but for now we exit
client:NotifyLocalized("You are not speaking on the phone.")
return
end
-- terminate any existing connections here
local conn = self:GetActiveConnection(connMD["exchange"], connMD["extension"])
if (!istable(conn)) then
client:NotifyLocalized("Error: AttemptedHangupOnActivePhoneNoConn")
-- This shouldn't be possible but if it happens then there is some lingering issue with
-- this character's var being active when they are not in an active connection
self:ResetCharVars(character)
return
end
self:Disconnect(conn["targetConnID"])
end
-- returns whether or not the 'listener' is in an active phone call with 'speaker'
function ix.phone.switch:ListenerCanHearSpeaker(speaker, listener)
local speakerChar = speaker:GetCharacter()
local listeners = self:GetCharacterActiveListeners(speakerChar)
if (!istable(listeners)) then
-- doubly make sure that the call activity is set correctly on the caller
speaker:NotifyLocalized("You are not currently on a phone call!")
ErrorNoHaltWithStack("Speaker ("..tostring(speaker:GetName())..") has invalid listener list!")
self:ResetCharVars(speakerChar)
return false
end
for _, _listener in ipairs(listeners) do
if (_listener == listener) then
return true
end
end
return false
end
--[[
Some helpers for setting the correct things in the correct order
]]
-- Reset ix.character.landlineConnection variables to default state
function ix.phone.switch:ResetCharVars(character)
character:SetLandlineConnection({
active = false,
exchange = nil,
extension = nil
})
end
-- Set ix.character.landlineConnection variables
function ix.phone.switch:SetCharVars(character, bActive, exc, ext)
character:SetLandlineConnection({
active = bActive,
exchange = exc,
extension = ext
})
end
-- Create a fake destination for testing purposes
-- Do nothing if one exists already
function ix.phone.switch:initLineTestNumber()
if (!self:ExchangeExists(self.lineTestPBX)) then
self:AddExchange(self.lineTestPBX)
end
if (!self.lineTestEnt.endpointID) then
self.lineTestEnt.endpointID = self.endpoints:Register(self.lineTestEnt)
print("Line Test Initalized! EndID: "..tostring(self.lineTestEnt.endpointID))
end
local destination = self:GetDest(self.lineTestPBX, self.lineTestExt)
if (!destination) then
self:AddDest(self.lineTestPBX, self.lineTestExt, "Line Test", self.lineTestEnt.endpointID)
end
end
-- used for debugging purposes (by doing lua_run in rcon)
-- calls a specific landline
function ix.phone.switch:DebugSinglePartyCall(exchange, ext)
if (!self:DestExists(exchange, ext)) then
ErrorNoHalt("Destination does not exist!")
return -- source does not exist or is not valid
end
self:initLineTestNumber()
local connID = self:buildNewConnection()
self:buildNewConnectionNode(connID, exchange, ext)
self:buildNewConnectionNode(connID, self.lineTestPBX, self.lineTestExt)
local dest = self:GetDest(exchange, ext)
if (!istable(dest)) then
self:Disconnect(connID) -- source dissapeared for some reason
ErrorNoHalt("Destination does not exist, or is invalid!")
return
end
print("Destination Endpoint Found!: "..table.ToString(dest, "Destination Endpoint", true))
local destEnt = self.endpoints:GetEndpoint(tonumber(dest.endID))
print("Destination Entity Found! ID: "..tostring(destEnt:EntIndex()))
destEnt:EnterRing(function(status)
if (!status) then -- call did not go through so we need to clean up
self:Disconnect(connID)
end
self:NotifyAllListenersOfStatusChange(tonumber(dest.endID), status, status)
local client = destEnt.inUseBy
net.Start("LineTestChat")
net.WriteString("This is a test to determine connection stability. It will automatically disconnect in 10 seconds.")
net.Send(client)
net.Start("LineStatusUpdate")
net.WriteString(self.DialStatus.DebugMode)
net.Send(client)
timer.Simple(15, function()
net.Start("LineTestChat")
net.WriteString("Line test has completed. Disconnecting now.")
net.Send(client)
self:Disconnect(connID)
end)
end)
local hangUpCallback = function()
-- cleanup
self:Disconnect(connID)
end
destEnt.hangUpCallback = hangUpCallback
end
-- used for debugging purposes
-- allows landline to make a call out to a test entity placed at map root
function ix.phone.switch:StartLineTest(exchange, ext)
if (!self:DestExists(exchange, ext)) then
ErrorNoHalt("Source does not exist!")
return -- source does not exist or is not valid
end
self:initLineTestNumber()
local connID = self:BuildNewTwoWayConnection(exchange, ext, self.lineTestPBX, self.lineTestExt)
local conn = self:GetActiveConnection(exchange, ext)
if (!istable(conn)) then
ErrorNoHalt("Failed to construct connection nodes! ", tostring(connID))
return
end
print("Connection nodes constructed!: "..table.ToString(conn, "ConnID: "..tostring(connID), true))
local source = self:GetDest(exchange, ext)
if (!istable(source)) then
self:Disconnect(connID) -- source dissapeared for some reason
ErrorNoHalt("Source does not exist, or is invalid!")
return
end
print("Source Endpoint Found!: "..table.ToString(source, "Source Endpoint", true))
local sourceEnt = self.endpoints:GetEndpoint(tonumber(source["endID"]))
print("Source Entity Found! ID: "..tostring(sourceEnt:EntIndex()))
local listeners = self:GetListeners(self.lineTestPBX, self.lineTestExt)
local client = listeners[1]
if (!client) then
self:Disconnect(connID)
print("Destination listener pool: "..table.ToString(listeners, "Listener Pool", true))
ErrorNoHalt("Destination has no listeners!")
return
end
local hangUpCallback = function()
-- cleanup
self:Disconnect(connID)
end
sourceEnt.hangUpCallback = hangUpCallback
print("Line Test Listener Found! ID: "..tostring(client))
timer.Simple(2, function()
net.Start("ixConnectedCallStatusChange")
net.WriteBool(true)
net.WriteBool(true)
net.Send(client)
net.Start("LineTestChat")
net.WriteString("This is a test to determine connection stability. It will automatically disconnect in 10 seconds.")
net.Send(client)
end)
timer.Simple(15, function()
net.Start("LineTestChat")
net.WriteString("Line test has completed. Disconnecting now.")
net.Send(client)
net.Start("ixConnectedCallStatusChange")
net.WriteBool(false)
net.WriteBool(false)
net.Send(client)
self:Disconnect(connID)
end)
end