Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
450b0b1
extend challengePlayer and requestLeaderboard with an indicator for t…
Endaris Jan 15, 2026
7da40fa
change protocol to use names already used for creating GameModes
Endaris Jan 15, 2026
8f3bec9
change game requests to use the established keywords for fetch GameMo…
Endaris Jan 15, 2026
0aa93e7
create a new lobbystate, fix publicIDs being pseudorandom in server t…
Endaris Jan 16, 2026
45d28b6
dumping progress because I forgot to merge in beta changes (oh no)
Endaris Feb 1, 2026
84f5cb7
Merge remote-tracking branch 'upstream/beta' into client2pTimeAttack
Endaris Feb 1, 2026
eef0b19
offer a submenu for picking the game mode you challenge a player to, …
Endaris Feb 11, 2026
06c9628
fix submenu navigation for lobby and add better visual indication
Endaris Feb 13, 2026
d94aa5d
smooth out presentation and add icons for lobbychallengebuttons
Endaris Feb 13, 2026
c39aec9
adjust lobby layout, add icons to rooms
Endaris Feb 18, 2026
6fa92a6
fix spectate and revert attempt at centered text for room names
Endaris Feb 19, 2026
7bf6571
add a room info text to allow players to glean the state of a room wi…
Endaris Feb 19, 2026
aaf2e7e
fix server tests
Endaris Feb 26, 2026
ef4afb5
bump network version and make server send serverTime on login approval
Endaris Feb 27, 2026
cfde278
remove references to old lobbydata and use enum notation for GameMode…
Endaris Feb 27, 2026
4eb412a
fix logintests failing
Endaris Feb 27, 2026
0956401
add pause to the network protocol and player/room state (single playe…
Endaris Feb 28, 2026
92d4fa2
resolve delta to server clock for game duration display
Endaris Feb 28, 2026
5dd6a5e
remove references to old lobby_state
Endaris Mar 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion client/assets/localization.csv
Original file line number Diff line number Diff line change
Expand Up @@ -663,4 +663,5 @@ Configure suficientes configuraciones de entrada e intente de nuevo.","Es gibt m
Bitte konfiguriere genügend Eingabemethoden und versuche es erneut.","Ci sono più giocatori locali che configurazioni di input configurate.
Si prega di configurare sufficienti configurazioni di input e riprovare.","มีผู้เล่นท้องถิ่นมากกว่าการกำหนดค่า input
โปรดกำหนดค่า input เพียงพอและลองอีกครั้ง"
translation_disclaimer,Disclaimer shown in language selection to inform about the quality / source of translation,"","La traduction initiale est effectuée à l'aide d'outils et peut être de qualité médiocre. La traduction peut être améliorée par la communauté.","A tradução inicial é feita com ferramentas e pode não estar à altura dos padrões. A tradução está aberta a melhorias da comunidade.","初期翻訳はツールによって行われ、水準に達していない可能性があります。翻訳はコミュニティからの改善を受け入れています。","La traducción inicial se realiza con herramientas y puede ser de calidad inferior. La traducción está abierta a mejoras por parte de la comunidad.","Die erste Übersetzung von neuem Text wird mit Tools durchgeführt und kann fehlerhaft sein. Übersetzungen sind offen für Verbesserungsvorschläge aus der Community.","La traduzione iniziale viene eseguita con strumenti e potrebbe non essere di qualità ottimale. La traduzione è aperta a miglioramenti da parte della comunità.",
translation_disclaimer,Disclaimer shown in language selection to inform about the quality / source of translation,"","La traduction initiale est effectuée à l'aide d'outils et peut être de qualité médiocre. La traduction peut être améliorée par la communauté.","A tradução inicial é feita com ferramentas e pode não estar à altura dos padrões. A tradução está aberta a melhorias da comunidade.","初期翻訳はツールによって行われ、水準に達していない可能性があります。翻訳はコミュニティからの改善を受け入れています。","La traducción inicial se realiza con herramientas y puede ser de calidad inferior. La traducción está abierta a mejoras por parte de la comunidad.","Die erste Übersetzung von neuem Text wird mit Tools durchgeführt und kann fehlerhaft sein. Übersetzungen sind offen für Verbesserungsvorschläge aus der Community.","La traduzione iniziale viene eseguita con strumenti e potrebbe non essere di qualità ottimale. La traduzione è aperta a miglioramenti da parte della comunità.",
gm_time_attack,Name of the score attack game mode with time limit,Time Attack,Contre la montre,Contra o tempo,スコアアタック,Contrareloj,Time Attack,A Tempo,Time Attack
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion client/src/ChallengeMode.lua
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ local ChallengeMode = class(
)

function ChallengeMode.create(difficulty, stageIndex)
return ChallengeMode(GameModes.getPreset("ONE_PLAYER_CHALLENGE"), Game1pChallenge, difficulty, stageIndex)
return ChallengeMode(GameModes.getPreset(GameModes.IDs.ONE_PLAYER_CHALLENGE), Game1pChallenge, difficulty, stageIndex)
end

ChallengeMode.numDifficulties = 8
Expand Down
25 changes: 25 additions & 0 deletions client/src/mods/Theme.lua
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,13 @@ function Theme:loadSelectionGraphics()
loadPlayerNumberIcons(self)
loadInputPromptIcons(self)
loadGridCursors(self)

self.images.IMG_checkBox = {}
self.images.IMG_checkBox[true] = self:load_theme_img("checkbox_checked")
self.images.IMG_checkBox[false] = self:load_theme_img("checkbox_unchecked")
self.images.fight = self:load_theme_img("fight")
self.images.stopWatch = self:load_theme_img("stopwatch")
self.images.endless = self:load_theme_img("endless")
end

function Theme:loadIngameGraphics()
Expand Down Expand Up @@ -1173,6 +1180,24 @@ function Theme:getTimePixelFont()
return self.fontMaps.time
end

---@param checked boolean
---@return love.Texture
function Theme:getCheckboxImage(checked)
return self.images.IMG_checkBox[checked]
end

function Theme:getFightImage()
return self.images.fight
end

function Theme:getStopwatchImage()
return self.images.stopWatch
end

function Theme:getEndlessImage()
return self.images.endless
end

---@param index integer
---@return IngameAssetPack
function Theme:getIngameAssetPack(index)
Expand Down
1 change: 1 addition & 0 deletions client/src/network/LoginRoutine.lua
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ local function login(tcpClient, ip, port)
if value.publicId then
GAME.localPlayer.publicId = value.publicId
end
result.serverTime = value.serverTime

return result
else --if result.login_denied then
Expand Down
117 changes: 77 additions & 40 deletions client/src/network/NetClient.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ local GameBase = require("client.src.scenes.GameBase")
local LoginRoutine = require("client.src.network.LoginRoutine")
local MessageTransition = require("client.src.scenes.Transitions.MessageTransition")
local LevelData = require("common.data.LevelData")
local GameModes = require("common.data.GameModes")

---@enum NetClientStates
local states = { OFFLINE = 1, LOGIN = 2, ONLINE = 3, ROOM = 4, INGAME = 5 }
Expand All @@ -22,45 +23,62 @@ local states = { OFFLINE = 1, LOGIN = 2, ONLINE = 3, ROOM = 4, INGAME = 5 }
-- that get automatically processed via NetClient:update

local function resetLobbyData(self)
self.lobbyData = {
---@class PersonalizedLobbyDataV2
self.lobbyDataV2 = {
---@type table<PublicPlayerID, LobbyPlayerV2>
players = {},
unpairedPlayers = {},
willingPlayers = {},
spectatableRooms = {},
sentRequests = {}
---@type LobbyPlayerV2[]
availablePlayers = {},
---@type table<PublicPlayerID, table<GameModeID, boolean>>
outgoingChallenges = {},
---@type table<PublicPlayerID, table<GameModeID, boolean>>
incomingChallenges = {},
---@type table<roomNumber, LobbyRoomV2>
rooms = {}
}
end

local function updateLobbyState(self, lobbyState)
if lobbyState.players then
self.lobbyData.players = lobbyState.players
---@param lobbyStateV2Message { content: LobbyStateV2 }
local function updateLobbyStateV2(self, lobbyStateV2Message)
local lobbyStateV2 = lobbyStateV2Message.content
if lobbyStateV2.players then
self.lobbyDataV2.players = lobbyStateV2.players
end

if lobbyState.unpaired then
self.lobbyData.unpairedPlayers = lobbyState.unpaired
-- players who leave the unpaired list no longer have standing invitations to us.\
-- we also no longer have a standing invitation to them, so we'll remove them from sentRequests
local newWillingPlayers = {}
local newSentRequests = {}
for _, player in ipairs(self.lobbyData.unpairedPlayers) do
newWillingPlayers[player] = self.lobbyData.willingPlayers[player]
newSentRequests[player] = self.lobbyData.sentRequests[player]
local availablePlayers = {}
for publicId, player in pairs(lobbyStateV2.players) do
if not player.roomNumber then
availablePlayers[#availablePlayers+1] = player
end
self.lobbyData.willingPlayers = newWillingPlayers
self.lobbyData.sentRequests = newSentRequests
end

if lobbyState.spectatable then
self.lobbyData.spectatableRooms = lobbyState.spectatable
-- if a player we challenged is not in lobby data or is in a room, they cannot accept our challenge anymore
for publicId, player in pairs(self.lobbyDataV2.outgoingChallenges) do
if not self.lobbyDataV2.players[publicId] then
self.lobbyDataV2.outgoingChallenges[publicId] = nil
elseif self.lobbyDataV2.players[publicId].roomNumber then
self.lobbyDataV2.outgoingChallenges[publicId] = nil
end
end

-- if a player that challenged us is not in lobby data or is in a room, we cannot accept their challenge anymore
for publicId, player in pairs(self.lobbyDataV2.incomingChallenges) do
if not self.lobbyDataV2.players[publicId] then
self.lobbyDataV2.incomingChallenges[publicId] = nil
elseif self.lobbyDataV2.players[publicId].roomNumber then
self.lobbyDataV2.incomingChallenges[publicId] = nil
end
end

self:emitSignal("lobbyStateUpdate", self.lobbyData)
self.lobbyDataV2.rooms = lobbyStateV2.rooms

self:emitSignal("lobbyStateV2Update", self.lobbyDataV2)
end

---@param room BattleRoom
local function getSceneFromRoom(room)
-- this is so hacky oh my god
if room.mode.name == "VS" then
if room.mode.name == "VS" or room.mode.name == "2p_timeattack" then
return CharacterSelect2p({battleRoom = room})
elseif room.mode.name == "endless" then
return require("client.src.scenes.EndlessMenu")({battleRoom = room})
Expand Down Expand Up @@ -274,13 +292,14 @@ local function processInputMessages(self)
end
end

local function processGameRequest(self, gameRequestMessage)
if gameRequestMessage.game_request then
self.lobbyData.willingPlayers[gameRequestMessage.game_request.sender] = true
love.window.requestAttention()
SoundController:playSfx(themes[config.theme].sounds.notification)
-- this might be moot if the server sends a lobby update to everyone after receiving the challenge
self:emitSignal("lobbyStateUpdate", self.lobbyData)
---@param self NetClient
local function processChallengeUpdate(self, challengeUpdateMessage)
if challengeUpdateMessage.challengeUpdate then
local challengeUpdate = challengeUpdateMessage.challengeUpdate
local challenges = self.lobbyDataV2.incomingChallenges[challengeUpdate.senderId] or {}
challenges[challengeUpdate.gameModeId] = challengeUpdate.challengeActive
self.lobbyDataV2.incomingChallenges[challengeUpdate.senderId] = challenges
self:emitSignal("lobbyStateV2Update", self.lobbyDataV2)
end
end

Expand Down Expand Up @@ -329,8 +348,8 @@ local function createListeners(self)
-- messageListener holds *all* available listeners
local messageListeners = {}
messageListeners.create_room = createListener(self, "create_room", start2pVsOnlineMatch)
messageListeners.players = createListener(self, "unpaired", updateLobbyState)
messageListeners.game_request = createListener(self, "game_request", processGameRequest)
messageListeners.lobbyStateV2 = createListener(self, "lobbyStateV2", updateLobbyStateV2)
messageListeners.challengeUpdate = createListener(self, "challengeUpdate", processChallengeUpdate)
messageListeners.menu_state = createListener(self, "menu_state", processMenuStateMessage)
messageListeners.ranked_match_approved = createListener(self, "ranked_match_approved", processRankedStatusMessage)
messageListeners.leave_room = createListener(self, "leave_room", processLeaveRoomMessage)
Expand All @@ -354,12 +373,15 @@ end
---@field messageListeners table
---@field room BattleRoom?
---@field lobbyData table
---@field lobbyDataV2 PersonalizedLobbyDataV2
---@field serverTimeDelta integer in seconds
---@overload fun(): NetClient
local NetClient = class(function(self)
self.tcpClient = TcpClient()
self.leaderboard = nil
self.pendingResponses = {}
self.state = states.OFFLINE
self.serverTimeDelta = 0

resetLobbyData(self)

Expand All @@ -368,8 +390,9 @@ local NetClient = class(function(self)
-- all listeners running while online but not in a room/match
self.lobbyListeners = {
players = messageListeners.players,
lobbyStateV2 = messageListeners.lobbyStateV2,
create_room = messageListeners.create_room,
game_request = messageListeners.game_request,
challengeUpdate = messageListeners.challengeUpdate,
}

-- all listeners running while in a room but not in a match
Expand Down Expand Up @@ -398,6 +421,7 @@ local NetClient = class(function(self)

Signal.turnIntoEmitter(self)
self:createSignal("lobbyStateUpdate")
self:createSignal("lobbyStateV2Update")
self:createSignal("leaderboardUpdate")
-- only fires for unintended disconnects
self:createSignal("clientDisconnected")
Expand Down Expand Up @@ -445,17 +469,29 @@ function NetClient:sendInput(input)
end
end

function NetClient:requestLeaderboard()
---@param gameModeId GameModeID?
function NetClient:requestLeaderboard(gameModeId)
if not self.pendingResponses.leaderboardUpdate then
self.pendingResponses.leaderboardUpdate = self.tcpClient:sendRequest(ClientMessages.requestLeaderboard())
gameModeId = gameModeId or GameModes.IDs.TWO_PLAYER_VS
self.pendingResponses.leaderboardUpdate = self.tcpClient:sendRequest(ClientMessages.requestLeaderboard(gameModeId))
end
end

function NetClient:challengePlayer(name)
if not self.lobbyData.sentRequests[name] then
self.tcpClient:sendRequest(ClientMessages.challengePlayer(config.name, name))
self.lobbyData.sentRequests[name] = true
self:emitSignal("lobbyStateUpdate", self.lobbyData)
---@param opponentId PublicPlayerID
---@param gameModeId GameModeID
function NetClient:challengePlayerById(opponentId, gameModeId)
self.lobbyDataV2.outgoingChallenges[opponentId] = self.lobbyDataV2.outgoingChallenges[opponentId] or {}
self.tcpClient:sendRequest(ClientMessages.updateChallengeStatus(GAME.localPlayer.publicId, opponentId, gameModeId, true))
self.lobbyDataV2.outgoingChallenges[opponentId][gameModeId] = true
self:emitSignal("lobbyStateV2Update", self.lobbyDataV2)
end

function NetClient:withdrawChallengeForId(opponentId, gameModeId)
if self.lobbyDataV2.outgoingChallenges[opponentId] then
self.tcpClient:sendRequest(ClientMessages.updateChallengeStatus(GAME.localPlayer.publicId, opponentId, gameModeId, false))
self.lobbyDataV2.outgoingChallenges[opponentId] = self.lobbyDataV2.outgoingChallenges[opponentId] or {}
self.lobbyDataV2.outgoingChallenges[opponentId][gameModeId] = false
self:emitSignal("lobbyStateV2Update", self.lobbyDataV2)
end
end

Expand Down Expand Up @@ -573,6 +609,7 @@ function NetClient:update()
self:setState(states.ONLINE)
self.loginState = result.message
self.loginTime = love.timer.getTime()
self.serverTimeDelta = os.difftime(to_UTC(os.time()), os.time(result.serverTime))
else
self.loginState = result.message
self:setState(states.OFFLINE)
Expand Down
14 changes: 8 additions & 6 deletions client/src/network/ServerMessages.lua
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ function ServerMessages.sanitizeServerMessage(message)
ban_duration = message.content.banDuration,
}
end
elseif message.type == "lobbyState" then
return message.content
elseif message.type == "lobbyStateV2" then
return { lobbyStateV2 = true, content = message.content }
elseif message.type == "leaderboardReport" then
return { leaderboard_report = message.content }
elseif message.type == "spectateRequestGranted" then
Expand Down Expand Up @@ -202,13 +202,15 @@ function ServerMessages.sanitizePlayerMessage(message)
index = content.index,
player_number = content.playerNumber,
}
elseif message.type == "challenge" then
elseif message.type == "challengeUpdate" then
return
{
game_request =
challengeUpdate =
{
sender = content.sender,
receiver = content.receiver,
senderId = content.senderId,
receiverId = content.receiverId,
gameModeId = content.gameModeId,
challengeActive = content.challengeActive,
}
}
end
Expand Down
2 changes: 1 addition & 1 deletion client/src/scenes/EndlessMenu.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ local ui = require("client.src.ui")
---@class EndlessMenu : CharacterSelect
local EndlessMenu = class(
function(self, sceneParams)
self.gameMode = GameModes.getPreset("ONE_PLAYER_ENDLESS")
self.gameMode = GameModes.getPreset(GameModes.IDs.ONE_PLAYER_ENDLESS)
self.gameScene = "EndlessGame"
end,
CharacterSelect
Expand Down
Loading