diff --git a/lua/autorun/client/cl_disconnect_interface.lua b/lua/autorun/client/cl_disconnect_interface.lua deleted file mode 100644 index 27d2345..0000000 --- a/lua/autorun/client/cl_disconnect_interface.lua +++ /dev/null @@ -1 +0,0 @@ -RunConsoleCommand("cl_timeout '240'") diff --git a/lua/autorun/client/cl_init.lua b/lua/autorun/client/cl_init.lua new file mode 100644 index 0000000..9f9fdec --- /dev/null +++ b/lua/autorun/client/cl_init.lua @@ -0,0 +1,3 @@ +RunConsoleCommand("cl_timeout '240'") +include("cfc_disconnect_interface/client/cl_ponger.lua") +include("cfc_disconnect_interface/client/cl_interface.lua") \ No newline at end of file diff --git a/lua/autorun/server/sv_init.lua b/lua/autorun/server/sv_init.lua new file mode 100644 index 0000000..69fa389 --- /dev/null +++ b/lua/autorun/server/sv_init.lua @@ -0,0 +1,9 @@ +util.AddNetworkString("cfc_di_ping") +util.AddNetworkString("cfc_di_loaded") +util.AddNetworkString("cfc_di_shutdown") + +AddCSLuaFile("cfc_disconnect_interface/client/cl_ponger.lua") +AddCSLuaFile("cfc_disconnect_interface/client/cl_api.lua") +AddCSLuaFile("cfc_disconnect_interface/client/cl_interface.lua") + +include("cfc_disconnect_interface/server/sv_pinger.lua") \ No newline at end of file diff --git a/lua/cfc_disconnect_interface/client/cl_api.lua b/lua/cfc_disconnect_interface/client/cl_api.lua new file mode 100644 index 0000000..12828f7 --- /dev/null +++ b/lua/cfc_disconnect_interface/client/cl_api.lua @@ -0,0 +1,86 @@ +crashApi = {} + +local cfc_endpoint = "https://scripting.cfcservers.org/cfc3-ping" +local global_endpoint = "https://www.google.com" + +local api = crashApi + +api.INACTIVE = 0 +api.PINGING_API = 1 +api.NO_INTERNET = 2 +api.SERVER_DOWN = 3 +api.SERVER_UP = 4 + +local responses = {cfc = nil, global = nil} -- Does nothing but helps with clarity + +local state = api.INACTIVE + +local pingCancelled = false + +local function getState() + return state +end + +local function handleResponses() + if pingCancelled then -- Ignore responses if ping was cancelled + return + end + if responses.cfc == nil or responses.global == nil then -- Not all responses arrived yet + return + end + if responses.cfc then + -- Server is up + state = api.SERVER_UP + elseif not responses.cfc and responses.global then + -- Server is down + state = api.SERVER_DOWN + else + -- Internet is down + state = api.NO_INTERNET + end +end + +local function triggerPing() + pingCancelled = false + state = api.PINGING_API + responses = {cfc = nil, global = nil} + + http.Fetch(cfc_endpoint, + function(body, size, headers, code) + local data = util.JSONToTable( body ) + if not data or data["server-is-up"] == nil then -- Can't use dot notation cuz api field has dashes >:( + responses.cfc = false + handleResponses() + else + responses.cfc = data["server-is-up"] + handleResponses() + end + end, + function(err) + responses.cfc = false + handleResponses() + end + ) + + http.Fetch(global_endpoint, + function(body, size, headers, code) + responses.global = true + handleResponses() + end, + function(err) + responses.global = false + handleResponses() + end + ) + +end + +local function cancelPing() + state = api.INACTIVE + pingCancelled = true +end + + +api.getState = getState +api.triggerPing = triggerPing +api.cancelPing = cancelPing diff --git a/lua/cfc_disconnect_interface/client/cl_interface.lua b/lua/cfc_disconnect_interface/client/cl_interface.lua new file mode 100644 index 0000000..07c639e --- /dev/null +++ b/lua/cfc_disconnect_interface/client/cl_interface.lua @@ -0,0 +1,327 @@ +include("cfc_disconnect_interface/client/cl_api.lua") + +surface.CreateFont( "CFC_Normal", + { + font = "arial", + size = 18, + weight = 500 + } +) + +surface.CreateFont( "CFC_Special", + { + font = "coolvetica", + size = 26, + weight = 500 + } +) + +surface.CreateFont( "CFC_Button", + { + font = "arial", + size = 18, + weight = 1500 + } +) + +local interfaceDerma = false + +local TIME_TO_RESTART = 10 +local timeDown +local apiState +local previouslyShown = false + +-- Colors +primaryCol = Color( 36, 41, 67 ) +secondaryCol = Color( 42, 47, 74 ) +accentCol = Color( 84, 84, 150 ) + +local function lerpColor(fraction, from, to) + return Color(from.r + (to.r - from.r) * fraction, + from.g + (to.g - from.g) * fraction, + from.b + (to.b - from.b) * fraction) +end + +local function secondsAsTime(s) + return string.FormattedTime( s, "%02i:%02i" ) +end + +-- Delay Function +local delayId = 0 +local function delaycall(time, callback) + local wait = RealTime() + time + delayId = delayId + 1 + local hookName = "cfc_di_delay_" .. delayId + hook.Add("Tick", hookName, function() + if RealTime() > wait then + hook.Remove("Tick", hookName) + callback() + end + end) +end + +local function rejoin() + delaycall(1, function() -- gm_crashsys does this, I don't feel like going through finding out why, so I'm just gonna do the same :) + RunConsoleCommand( "snd_restart" ) + RunConsoleCommand( "retry" ) + end) +end + +local function leave() + delaycall(1, function() + RunConsoleCommand( "disconnect" ) + end) +end + +local function addTitleBar(frame) + local frameW, frameH = frame:GetSize() + local titleBarHeight = 32 + local titleBar = vgui.Create( "DPanel", frame ) + titleBar:SetSize( frameW, titleBarHeight ) + titleBar:SetPos( 0, 0 ) + function titleBar:Paint(w, h) + surface.SetDrawColor( secondaryCol ) + surface.DrawRect( 0, 0, w, h ) + end + + local closeBtnPadding = (titleBarHeight - 16) / 2 + + local closeBtn = vgui.Create( "DImageButton", titleBar ) + closeBtn:SetSize( 16, 16 ) + closeBtn:SetPos( frameW - 16 - closeBtnPadding, closeBtnPadding) + closeBtn:SetImage( "icon16/cross.png" ) + function closeBtn:DoClick() + frame:Close() + end + + local titleLabelPadding = (titleBarHeight - 26) / 2 + + local titleLabel = vgui.Create( "DLabel", titleBar ) + titleLabel:SetFont( "CFC_Special" ) + titleLabel:SetText( "Oops! Looks like the server crashed..." ) + titleLabel:SizeToContents() + titleLabel:SetPos( 0, titleLabelPadding + 2 ) + titleLabel:CenterHorizontal() + + return titleBar +end + +local function makeButton(frame, text, xFraction, doClick, outlineCol, fillCol, hoverOutlineCol, hoverFillCol) + outlineCol = outlineCol or Color( 255, 255, 255 ) + fillCol = fillCol or primaryCol + hoverOutlineCol = hoverOutlineCol or Color(155,241,255) + hoverFillCol = hoverFillCol or primaryCol + + local frameW, frameH = frame:GetSize() + local btn = vgui.Create( "DButton", frame ) + btn:SetText( text ) + btn:SetTextColor( Color( 255, 255, 255 ) ) + btn:SetFont( "CFC_Button" ) + btn:SetSize( frameW * 0.4, frameH * 0.6 ) + btn:CenterHorizontal( xFraction ) + btn:CenterVertical() + btn.DoClick = doClick + + btn.fadeState = 0 + btn.prevTime = CurTime() + + local btnAnimSpeed = 0.05 * 60 + + function btn:Think() + -- Make anim same speed for all framerates + local dt = CurTime() - self.prevTime + self.prevTime = CurTime() + if dt > 1 then dt = 0 end + + if self:IsHovered() and self.fadeState < 1 then + self.fadeState = math.Clamp(self.fadeState + btnAnimSpeed * dt, 0, 1) + elseif not self:IsHovered() and self.fadeState > 0 then + self.fadeState = math.Clamp(self.fadeState - btnAnimSpeed * dt, 0, 1) + end + end + + local btnBorderWeight = 2 + function btn:Paint(w, h) + local lineCol + local bgCol + if self:GetDisabled() then + lineCol = Color( 74, 74, 74 ) + bgCol = fillCol + self:SetCursor( "no" ) + else + lineCol = lerpColor(self.fadeState, outlineCol, hoverOutlineCol) + bgCol = lerpColor(self.fadeState, fillCol, hoverFillCol) + self:SetCursor( "hand" ) + end + + self:SetTextColor( lineCol ) + + surface.SetDrawColor( lineCol ) + surface.DrawRect( 0, 0, w, h ) + surface.SetDrawColor( bgCol ) + surface.DrawRect( btnBorderWeight, btnBorderWeight, + w - (btnBorderWeight*2), h - (btnBorderWeight*2) ) + end + + return btn +end + +local function addButtonsBar(frame) + local frameW, frameH = frame:GetSize() + + local buttonBarHeight = 64 + + local barPanel = vgui.Create( "DPanel", frame ) + barPanel:SetSize( frameW, buttonBarHeight ) + barPanel:SetPos( 0, frameH - buttonBarHeight ) + function barPanel:Paint(w, h) + surface.SetDrawColor( accentCol ) + surface.DrawLine( 16, 0, w - 16, 0 ) + end + + barPanel.reconBtn = makeButton(barPanel, "RECONNECT", 0.25, rejoin, + Color( 74, 251, 191 ), nil, Color( 74, 251, 191 ), Color( 64, 141, 131 )) + barPanel.reconBtn:SetDisabled( true ) + barPanel.disconBtn = makeButton(barPanel, "DISCONNECT", 0.75, leave) + + return barPanel +end + +local function makeLabel(frame, text, top, col, xFraction) + col = col or Color( 255, 255, 255 ) + local label = vgui.Create( "DLabel", frame ) + label:SetText( text ) + label:SetFont( "CFC_Special" ) + label:SizeToContents() + label:SetPos( 0, top ) + label:SetTextColor( col ) + label:CenterHorizontal( xFraction ) + return label +end + +local function populateBodyInternetDown(body) + local label1 = makeLabel(body, "Looks like your internet has gone down!", 20) + local label2 = makeLabel(body, "Stick around for when it comes back", 64) +end + +local function populateBodyServerDown(body) + + local frameW, frameH = body:GetSize() + local restartTimeStr = "The server normally takes about " .. secondsAsTime(TIME_TO_RESTART) .. " to restart!" + local restartTimeLabel = makeLabel(body, restartTimeStr, 0) + local curTimePreLabel = makeLabel(body, "It has been down for", 32) + function curTimePreLabel:Think() + if apiState == crashApi.SERVER_UP and not self.backUp then + self:SetText( "It was down for" ) + self:SizeToContents() + self:CenterHorizontal() + self.backUp = true + end + end + + local tooLongLabel = makeLabel(body, "Uh oh, seems it's taking a little longer than usual!", 70, Color( 251, 191, 83 ), 0.8) + tooLongLabel:SetAlpha(0) + tooLongLabel:Hide() + + local curTimeLabel = makeLabel(body, secondsAsTime(math.floor(timeDown)), 70, Color( 251, 191, 83 )) + function curTimeLabel:Think() + if apiState ~= crashApi.SERVER_UP then + self:SetText(secondsAsTime(math.floor(timeDown))) + if timeDown > TIME_TO_RESTART then + self:SetTextColor(Color(255, 0, 0)) + if not tooLongLabel:IsVisible() then + tooLongLabel:Show() + tooLongLabel:AlphaTo(255, 1) + end + end + else + self:SetTextColor(Color(0, 255, 0)) + end + end +end + +local function populateBody(body) + body.Paint = nil + if apiState == crashApi.NO_INTERNET then + populateBodyInternetDown(body) + else -- Server down or up via api, and down via net + populateBodyServerDown(body) + end + + local frameW, frameH = 0.8 * ScrW(), 0.8 * ScrH() + + local playGameLabel = makeLabel(body, "Why not play a game while you wait? (Press space)", 108) + + local gamePanel = vgui.Create( "DPanel", body ) + gamePanel:SetSize( frameW - 20, frameH - 134 - 15 ) + gamePanel:SetPos( -6, 134 + 10 ) + gamePanel.Paint = nil + + local gameHtml = vgui.Create( "DHTML", gamePanel ) + gameHtml:Dock( FILL ) + gameHtml:OpenURL("https://cdn.cfcservers.org/media/dinosaur/index.html") + function gameHtml:Think() + if not gameHtml:HasFocus() then gameHtml:RequestFocus() end + end +end + +local function createInterface() + + local frameW, frameH = 0.8 * ScrW(), 0.8 * ScrH() + + local startTime = SysTime() + + local frame = vgui.Create( "DFrame" ) + interfaceDerma = frame + frame:SetSize( frameW, frameH ) + frame:Center() + frame:SetTitle("") + frame:SetDraggable( false ) + frame:MakePopup() + frame:ShowCloseButton( false ) + + function frame:Paint(w, h) + Derma_DrawBackgroundBlur(self, startTime) + surface.SetDrawColor( primaryCol ) + surface.DrawRect( 0, 0, w, h ) + end + + local titlePanel = addTitleBar(frame) + local btnsPanel = addButtonsBar(frame) + + local body = vgui.Create( "DPanel", frame ) + body:SetSize(frameW - 32, frameH - 32 - titlePanel:GetTall() - btnsPanel:GetTall()) + body:SetPos(16, titlePanel:GetTall() + 16) + populateBody(body) + + function frame:Think() + if apiState == crashApi.INACTIVE then + frame:Close() -- Server recovered without ever closing + elseif apiState == crashApi.SERVER_UP then + if btnsPanel.reconBtn:GetDisabled() == true then + btnsPanel.reconBtn:SetDisabled( false ) -- Server back up + -- Maybe show a "The server is back up, click here to reconnect?" + end + end + end + + function frame:OnClose() + interfaceDerma = nil + end +end + +hook.Add("cfc_di_crashTick", "cfc_di_interfaceUpdate", function(isCrashing, _timeDown, _apiState) + timeDown = _timeDown or 0 + apiState = _apiState + if isCrashing and apiState ~= crashApi.PINGING_API and not interfaceDerma and not previouslyShown then + createInterface() + previouslyShown = true + end + if not isCrashing then + previouslyShown = false + if interfaceDerma then + interfaceDerma:Close() + end + end +end) + diff --git a/lua/cfc_disconnect_interface/client/cl_ponger.lua b/lua/cfc_disconnect_interface/client/cl_ponger.lua new file mode 100644 index 0000000..6ff1702 --- /dev/null +++ b/lua/cfc_disconnect_interface/client/cl_ponger.lua @@ -0,0 +1,65 @@ +include("cfc_disconnect_interface/client/cl_api.lua") + +local GRACE_TIME = 3.5 -- How many seconds of lag should we have before showing the panel? +local PING_MISS = 2 -- How many pings can we miss on join? + +local API_TIMEOUT = 15 -- How often to call the api + +local lastPing +local lastApiCall + +net.Receive("cfc_di_ping", function() + if PING_MISS > 0 then -- Allow some pings before actually starting crash systems. (Avoid bugs on join stutter.) + PING_MISS = PING_MISS - 1 + else + lastPong = RealTime() + end +end) + +local function shutdown() + timer.Remove("cfc_di_startup") + hook.Remove("Tick", "cfc_di_tick") +end + +net.Receive("cfc_di_shutdown", shutdown) +hook.Add("ShutDown", "crashsys", shutdown) + +local function crashTick(timedown) + local apiState = crashApi.getState(); + if (apiState == crashApi.INACTIVE) or -- No ping sent + (apiState ~= crashApi.SERVER_UP and RealTime() - lastApiCall > API_TIMEOUT) then -- Previous ping failed, and API_TIMEOUT has passed + crashApi.triggerPing(); + lastApiCall = RealTime(); + + apiState = crashApi.getState(); + end + hook.Run("cfc_di_crashTick", true, timedown, apiState); +end + +local function checkCrashTick() + if not lastPong then return end + if not LocalPlayer():IsValid() then return end -- disconnected or connecting + + local timeout = RealTime() - lastPong + + if timeout > GRACE_TIME then + crashTick(timeout) + else + if crashApi.getState() ~= crashApi.INACTIVE then + crashApi.cancelPing(); + end + hook.Run("cfc_di_crashTick", false); + end +end + +-- Ping the server when the client is ready. +timer.Create("cfc_di_startup", 0.01, 0, function() + local ply = LocalPlayer() + if ply:IsValid() then + net.Start("cfc_di_loaded") + net.SendToServer() + timer.Remove("cfc_di_startup") + print("cfc_disconnect_interface loaded.") + hook.Add("Tick", "cfc_di_tick", checkCrashTick) + end +end) diff --git a/lua/cfc_disconnect_interface/server/sv_pinger.lua b/lua/cfc_disconnect_interface/server/sv_pinger.lua new file mode 100644 index 0000000..8eb8547 --- /dev/null +++ b/lua/cfc_disconnect_interface/server/sv_pinger.lua @@ -0,0 +1,30 @@ +local PING_TIME = 3 + +local players = {} +local table = table + +local function ping(ply) + net.Start("cfc_di_ping") + net.Send(ply or players) +end + +net.Receive("cfc_di_loaded", function(len, ply) + if not IsValid(ply) then return end + if not table.HasValue(players, ply) then + table.insert(players, ply) + end +end) + +hook.Add("PlayerDisconnected", "crashsys", function(ply) + ping(ply) + table.RemoveByValue(players, ply) +end) + +timer.Create("cfc_di_pingTimer", PING_TIME, 0, function() + ping() +end) + +hook.Add("ShutDown", "cfc_di", function() + net.Start("cfc_di_shutdown") + net.Send(players) +end) \ No newline at end of file diff --git a/resource/fonts/coolvetica.ttf b/resource/fonts/coolvetica.ttf new file mode 100644 index 0000000..efe72cc Binary files /dev/null and b/resource/fonts/coolvetica.ttf differ