Skip to content

Commit 451156f

Browse files
committed
Reconnection handling
1 parent 4196d94 commit 451156f

3 files changed

Lines changed: 147 additions & 4 deletions

File tree

networking/action_handlers.lua

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,17 +67,72 @@ local function action_rejoinedLobby(code, type, token)
6767
-- Update reconnect token
6868
reconnectToken = token
6969
lastLobbyCode = code
70+
MP.self_reconnect_countdown = nil
7071
MP.ACTIONS.sync_client()
7172
MP.ACTIONS.lobby_info()
7273
MP.UI.update_connection_status()
74+
sendWarnMessage("Reconnected to lobby!", "MULTIPLAYER")
75+
G.FUNCS.exit_overlay_menu()
76+
MP.UI.UTILS.overlay_message("Reconnected to lobby!")
7377
end
7478

75-
local function action_enemyDisconnected()
79+
-- Countdown state for disconnect overlays
80+
MP.enemy_disconnect_countdown = nil
81+
MP.self_reconnect_countdown = nil
82+
83+
-- Shared timeout handler for both countdowns
84+
local function handle_reconnect_timeout(message)
85+
G.FUNCS.exit_overlay_menu()
86+
MP.LOBBY.connected = false
87+
if MP.LOBBY.code then MP.LOBBY.code = nil end
88+
reconnectToken = nil
89+
lastLobbyCode = nil
90+
MP.UI.update_connection_status()
91+
if G.STAGE ~= G.STAGES.MAIN_MENU then
92+
MP.reset_game_states()
93+
G.FUNCS.go_to_menu()
94+
end
95+
MP.UI.UTILS.overlay_message(message)
96+
end
97+
98+
-- Hook into Game.update to tick countdown displays
99+
local _disconnect_gupdate = Game.update
100+
function Game:update(dt)
101+
if MP.enemy_disconnect_countdown then
102+
local remaining = math.max(0, math.ceil(MP.enemy_disconnect_countdown.end_time - love.timer.getTime()))
103+
MP.enemy_disconnect_countdown.display = remaining .. "s remaining"
104+
-- No client-side timeout needed: the server sends stopGame
105+
-- when the grace period expires, which handles the cleanup
106+
end
107+
if MP.self_reconnect_countdown then
108+
local remaining = math.max(0, math.ceil(MP.self_reconnect_countdown.end_time - love.timer.getTime()))
109+
MP.self_reconnect_countdown.display = remaining .. "s remaining"
110+
if remaining <= 0 then
111+
MP.self_reconnect_countdown = nil
112+
handle_reconnect_timeout("Reconnection failed.\nReturning to main menu.")
113+
end
114+
end
115+
return _disconnect_gupdate(self, dt)
116+
end
117+
118+
local function action_enemyDisconnected(timeout)
119+
timeout = timeout or 60
76120
sendWarnMessage("Opponent disconnected, waiting for reconnection...", "MULTIPLAYER")
77-
MP.UI.UTILS.overlay_message("Opponent disconnected,\nwaiting for reconnection...", true)
121+
122+
MP.enemy_disconnect_countdown = {
123+
end_time = love.timer.getTime() + timeout,
124+
display = timeout .. "s remaining",
125+
}
126+
127+
MP.UI.UTILS.overlay_message_countdown(
128+
"Opponent disconnected,\nwaiting for reconnection...",
129+
MP.enemy_disconnect_countdown,
130+
true
131+
)
78132
end
79133

80134
local function action_enemyReconnected()
135+
MP.enemy_disconnect_countdown = nil
81136
sendWarnMessage("Opponent reconnected!", "MULTIPLAYER")
82137
G.FUNCS.exit_overlay_menu()
83138
MP.UI.UTILS.overlay_message("Opponent reconnected!")
@@ -142,13 +197,34 @@ end
142197

143198
local function action_disconnected()
144199
MP.LOBBY.connected = false
200+
MP.self_reconnect_countdown = nil
145201
if MP.LOBBY.code then MP.LOBBY.code = nil end
146202
-- Clear reconnect state since all reconnection attempts failed
147203
reconnectToken = nil
148204
lastLobbyCode = nil
149205
MP.UI.update_connection_status()
150206
end
151207

208+
local function action_reconnecting()
209+
-- Only show if we were in a lobby and don't already have a countdown running
210+
if reconnectToken and lastLobbyCode and not MP.self_reconnect_countdown then
211+
MP.LOBBY.connected = false
212+
MP.UI.update_connection_status()
213+
sendWarnMessage("Connection lost, attempting to reconnect...", "MULTIPLAYER")
214+
215+
MP.self_reconnect_countdown = {
216+
end_time = love.timer.getTime() + 60,
217+
display = "60s remaining",
218+
}
219+
220+
MP.UI.UTILS.overlay_message_countdown(
221+
"Connection lost,\nattempting to reconnect...",
222+
MP.self_reconnect_countdown,
223+
true
224+
)
225+
end
226+
end
227+
152228
---@param seed string
153229
---@param stake_str string
154230
local function action_start_game(seed, stake_str)
@@ -251,6 +327,7 @@ local function action_enemy_info(score_str, hands_left_str, skips_str, lives_str
251327
end
252328

253329
local function action_stop_game()
330+
MP.enemy_disconnect_countdown = nil
254331
if G.STAGE ~= G.STAGES.MAIN_MENU then
255332
G.FUNCS.go_to_menu()
256333
MP.UI.update_connection_status()
@@ -1128,12 +1205,14 @@ function Game:update(dt)
11281205
action_version()
11291206
elseif parsedAction.action == "disconnected" then
11301207
action_disconnected()
1208+
elseif parsedAction.action == "reconnecting" then
1209+
action_reconnecting()
11311210
elseif parsedAction.action == "joinedLobby" then
11321211
action_joinedLobby(parsedAction.code, parsedAction.type, parsedAction.reconnectToken)
11331212
elseif parsedAction.action == "rejoinedLobby" then
11341213
action_rejoinedLobby(parsedAction.code, parsedAction.type, parsedAction.reconnectToken)
11351214
elseif parsedAction.action == "enemyDisconnected" then
1136-
action_enemyDisconnected()
1215+
action_enemyDisconnected(parsedAction.timeout)
11371216
elseif parsedAction.action == "enemyReconnected" then
11381217
action_enemyReconnected()
11391218
elseif parsedAction.action == "lobbyInfo" then

networking/socket.lua

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ end
3131
3232
Networking = {}
3333
local isSocketClosed = true
34+
local hasGivenUp = false -- true after all reconnect attempts failed
3435
local networkToUiChannel = love.thread.getChannel("networkToUi")
3536
local uiToNetworkChannel = love.thread.getChannel("uiToNetwork")
3637
@@ -62,6 +63,7 @@ function Networking.connect()
6263
return false
6364
else
6465
isSocketClosed = false
66+
hasGivenUp = false
6567
end
6668
6769
Networking.Client:settimeout(0)
@@ -98,6 +100,7 @@ local mainThreadMessageQueue = function()
98100
local msg = uiToNetworkChannel:pop()
99101
if msg then
100102
if msg == "{\"action\":\"connect\"}" then
103+
hasGivenUp = false
101104
Networking.connect()
102105
else
103106
Networking.Client:send(msg .. "\n")
@@ -135,7 +138,7 @@ local retryCount = 0
135138
local networkPacketQueue = function()
136139
local packetsPerCycle = 25
137140
while true do
138-
if Networking.Client then
141+
if Networking.Client and not hasGivenUp then
139142
-- Tries to fetch a packet a max of packetsPerCycle times
140143
-- and then yields
141144
for _ = 1, packetsPerCycle do
@@ -162,9 +165,12 @@ local networkPacketQueue = function()
162165
isRetry = false
163166
timerCoroutine = coroutine.create(timer)
164167
168+
networkToUiChannel:push("{\"action\":\"reconnecting\"}")
165169
if not Networking.tryReconnect() then
170+
hasGivenUp = true
166171
networkToUiChannel:push("{\"action\":\"disconnected\"}")
167172
end
173+
break
168174
else
169175
-- If there are no more packets, yield
170176
coroutine.yield()
@@ -203,7 +209,9 @@ while true do
203209
isRetry = false
204210
timerCoroutine = coroutine.create(timer)
205211
212+
networkToUiChannel:push("{\"action\":\"reconnecting\"}")
206213
if not Networking.tryReconnect() then
214+
hasGivenUp = true
207215
networkToUiChannel:push("{\"action\":\"disconnected\"}")
208216
end
209217
end

ui/utils.lua

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,62 @@ function MP.UI.UTILS.create_object_node(object, config)
3838
return { n = G.UIT.O, config = config }
3939
end
4040

41+
--- Overlay with a DynaText countdown timer
42+
--- @param message string Static message lines (newline-separated)
43+
--- @param countdown_table table Table with a "display" key that gets updated externally
44+
--- @param no_back boolean If true, disables back/esc buttons
45+
function MP.UI.UTILS.overlay_message_countdown(message, countdown_table, no_back)
46+
G.SETTINGS.paused = true
47+
local message_table = MP.UTILS.string_split(message, "\n")
48+
local message_ui = {
49+
MP.UI.UTILS.create_row({ align = "cm", padding = 0.2 }, {
50+
MP.UI.UTILS.create_text_node("MULTIPLAYER", {
51+
scale = 0.8,
52+
colour = G.C.UI.TEXT_LIGHT,
53+
}),
54+
}),
55+
}
56+
57+
for _, v in ipairs(message_table) do
58+
table.insert(
59+
message_ui,
60+
MP.UI.UTILS.create_row({ align = "cm", padding = 0.1 }, {
61+
MP.UI.UTILS.create_text_node(v, {
62+
scale = 0.6,
63+
colour = G.C.UI.TEXT_LIGHT,
64+
}),
65+
})
66+
)
67+
end
68+
69+
-- Countdown row using DynaText with ref_table for live updates
70+
table.insert(
71+
message_ui,
72+
MP.UI.UTILS.create_row({ align = "cm", padding = 0.2 }, {
73+
MP.UI.UTILS.create_object_node(
74+
DynaText({
75+
string = {{ ref_table = countdown_table, ref_value = "display" }},
76+
colours = { G.C.UI.TEXT_LIGHT },
77+
shadow = true,
78+
silent = true,
79+
scale = 0.7,
80+
pop_in = 0,
81+
})
82+
),
83+
})
84+
)
85+
86+
G.FUNCS.overlay_menu({
87+
definition = create_UIBox_generic_options({
88+
no_back = no_back,
89+
no_esc = no_back,
90+
contents = {
91+
MP.UI.UTILS.create_column({ align = "cm", padding = 0.2 }, message_ui),
92+
},
93+
}),
94+
})
95+
end
96+
4197
-- Overlay message function (moved from misc/utils.lua)
4298
function MP.UI.UTILS.overlay_message(message, no_back)
4399
G.SETTINGS.paused = true

0 commit comments

Comments
 (0)