Repository navigation
Conversation
- Pass the userid (not the client index) through the spoof-check query. - Keep the connection across maps; re-check players authorized while the DB was not connected. - Retry failed connects; only treat real connection errors as lost. - SetCharset once per connection (SET NAMES as fallback), drop dead retry branches, db.Format for queries, refresh MySQL timestamp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WareZ57
left a comment
There was a problem hiding this comment.
Reviewed against master (merges cleanly, CI green). It compiles with spcomp 1.12.0.7259 and the same deps as CI (MultiColors, connect.inc, ripext, ProxyKiller, utilshelper), and _connect_included is defined so the SQL code is actually compiled. No warnings.
What I checked:
- The spoof-check pack now carries the userid, and
Query_SelectConnectionbails out on!client(PlayerManager.sp:865-875) before it can reach theKickClientat:902. The insert path just skips that cell. That's correct. - Reconnect state machine:
DB_ScheduleReconnectis only reached fromGotDatabasefailure (only whileConnectingand only for the current lock) and fromDB_Conn_Lost(only when not alreadyWait/Connecting). So there's at most oneTimerDB_Reconnectin flight, and withoutTIMER_FLAG_NO_MAPCHANGEtheWaitstate can't get stuck across a map change anymore.OnConfigsExecuted->DB_Connect()is a no-op while connected, so keeping the handle across maps doesn't open new connections. SetCharsetruns before the handle is published and beforeDB_SetNames/DB_CreateTablequeue anything (:662-671).- Ordering in
GotDatabase: the table creation is queued at normal priority andDB_CheckPendingClientsqueues its SELECTs atDBPrio_Low, so on a fresh DB the table exists first. DB_CheckPendingClientsusesAuthId_Engine, which is what theOnClientAuthorizedforward receives, soSteamClientGotValidateAuthTicketResponsematches the same key. Late-load players are picked up too, since their flag starts at false.g_hDatabase.Formatescaping and thetimestamp=CURRENT_TIMESTAMPon the MySQL upsert both look fine.
One small gap, not blocking: a check that was sent but failed on a lost connection isn't re-run. OnClientAuthorized sets g_bConnectionChecked[client] = true (:212) before the SELECT. If that SELECT comes back with "gone away"/"Lost connection", Query_SelectConnection logs and returns (:877-882), the handle is dropped and a reconnect is scheduled. But DB_CheckPendingClients skips that player after the reconnect because the flag is already true. So a NoSteam spoofer who joins right as MySQL restarts isn't checked until the next map. Setting g_bConnectionChecked[client] = false; in that error branch (you already have client there) would let the reconnect pick them up.
LGTM otherwise.
Summary
DataPackcarried the raw client index; when the asyncSELECTcame back after the player left,KickClient(client)could hit whoever took the slot. The pack now carriesGetClientUserId(client)andQuery_SelectConnectionresolves it withGetClientOfUserId(+ 0 check) (PlayerManager.sp).OnMapEnddeleted the connection andOnConfigsExecutedreconnected, so every player authorizing before the new connection landed hit the!g_hDatabaseearly return and was never checked. The connection is now kept across maps (SourceMod keeps it; the MySQL driver auto-reconnects), and once a connection is ready,DB_CheckPendingClientsre-runs the check for connected, authorized players that missed it (tracked withg_bConnectionChecked, reset inOnClientConnected). This also covers late-load players, who were checked before the DB connected.GotDatabasereturned on failure with the state stuck atConnecting, soDB_Connectnever tried again. It now schedules a retry (DB_ScheduleReconnect, same 15 s delay as a lost connection). The retry timer no longer usesTIMER_FLAG_NO_MAPCHANGE: a map change killing it would leave the state stuck inWait.DB_Conn_Lostdeleted the handle on anyresults == null. It now only does so when the error text is a real connection error ("Lost connection", "gone away", "Can't connect", "Broken pipe").db.SetCharset("utf8mb4")is called once per new MySQL connection before any query is queued (survives the driver's auto-reconnect); the asyncSET NAMESis only sent as fallback ifSetCharsetfails.DB_SetNames/DB_CreateTablehad retry branches that could never run (DB_Connect()is always true right after connecting) and would have passed a possibly-deleted raw handle to a timer. Removed along with thesm_manager_query_retryconvar, which only fed those branches.INSERT/SELECTnow useg_hDatabase.Formatinstead of plainFormat(escapes the auth/address strings). The MySQL upsert now also setstimestamp=CURRENT_TIMESTAMPon duplicate key: the column had noON UPDATE, so it kept the first-seen time while the SQLiteINSERT OR REPLACEpath refreshes it.Not included
LogPotentialSpooferpasses the raw client index to an async HTTP request (OnPlayerSummaryReceived/APIWebResponse); same class of bug but not SQL, left for a separate change.Test plan
connectionrow inserted/updated after the connecttimestampinconnectionupdates on every rejoinsm_manager_query_retrycan be removed fromcfg/sourcemod/PlayerManager.cfg🤖 Generated with Claude Code