Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,33 @@
All notable changes to bizzymod-stats are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); we use SemVer.

## [0.6.1] — UNRELEASED

### Fixed

- **Query buffer truncation in player_stats upsert.** The session-flush
transaction's main `INSERT INTO player_stats … ON DUPLICATE KEY UPDATE …`
exceeded its 2048-byte `FormatEx` buffer after the schema grew through
migration 010, silently chopping the statement mid-keyword. Surfaced as
MySQL errors like `Unknown column 'damage_to_ta'`, `'VALUES'` → `'VALUE'`
→ `'VALU'` → `'V'`, and `syntax error … near ''`. Whole transactions
were rejected, so session counters never persisted. Bumped the buffer
to 4096. Long-term fix tracked separately: switch the wide upserts to
parameterized queries. (#5)
- **`tank_records.fk_tr_map` foreign-key violation on tank/witch spawn.**
`g_CurrentMapId` is resolved through an async `INSERT IGNORE INTO maps
→ SELECT id` chain that only kicks off in `OnMapStart`. On a plugin
hot-reload or DB-late-connect mid-map, `g_CurrentMapId` stayed at 0 and
the first boss spawn after produced a foreign-key error against
`maps.id`. Three-part fix:
- The DB-ready callback (`OnServerLookup`) now triggers
`Bizzy_Identity_ResolveMap()` itself, so a late connect catches up.
- `Bizzy_Identity_ResolveMap()` refreshes `g_CurrentMap` from the
engine on every call (no longer relies on `OnMapStart` having
already populated it).
- Tank- and witch-spawn inserts now bail with `g_CurrentMapId == 0`
instead of attempting the FK-violating insert. (#5)

## [0.6.0] — 2026-05-25

**First public release.** Built from scratch over a 2026-Q2 sprint as a
Expand Down
18 changes: 17 additions & 1 deletion plugin/scripting/bizzymod_stats/identity.sp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ static void OnServerLookup(Database db, DBResultSet rs, const char[] error, any
g_ServerId = rs.FetchInt(0);
LogMessage("[bizzymod-stats] online as server_id=%d (key=%s)", g_ServerId, g_ServerKey);

// Resolve map_id now in case the DB connected mid-map (server boot
// with a map already loaded, or plugin hot-reload). OnMapStart only
// fires on actual transitions, so without this hook g_CurrentMapId
// would stay 0 until the next map change — and any tank / witch /
// crescendo / wave insert in the meantime would FK-violate against
// maps.id. See bogware/bizzymod-stats#5.
Bizzy_Identity_ResolveMap();

// Now safe to backfill any sessions for players already connected
// (covers hot-reload).
for (int i = 1; i <= MaxClients; i++)
Expand Down Expand Up @@ -177,7 +185,15 @@ static void OnPlayerLookup(Database db, DBResultSet rs, const char[] error, Data

stock void Bizzy_Identity_ResolveMap()
{
if (g_DB == null || g_CurrentMap[0] == '\0') return;
if (g_DB == null) return;
// Refresh from the engine every call. OnMapStart sets g_CurrentMap on
// every map change, but if this is called from the DB-ready callback
// (handling hot reload / late connect) OnMapStart may not have fired
// yet on this plugin instance — in which case g_CurrentMap is empty
// and we'd no-op. Reading from GetCurrentMap directly is safe and
// idempotent. See bogware/bizzymod-stats#5.
GetCurrentMap(g_CurrentMap, sizeof g_CurrentMap);
if (g_CurrentMap[0] == '\0') return;
char esc[260];
Bizzy_DB_Escape(g_CurrentMap, esc, sizeof esc);
char sql[512];
Expand Down
12 changes: 11 additions & 1 deletion plugin/scripting/bizzymod_stats/session.sp
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,17 @@ stock void Bizzy_Session_Flush(int client, int duration)
g_Clients[client].killStreakMax = g_Clients[client].killStreak;

// 1) Upsert rollup row in player_stats.
char sql[2048];
//
// Buffer sized for the worst-case post-substitution length of every
// FormatEx call in this function. The biggest one is this player_stats
// upsert with ~35 columns + matching ON DUPLICATE KEY UPDATE clause; at
// 2048 it silently truncated mid-statement (errors like
// `Unknown column 'damage_to_ta'` / `'VALUES'` cut to `'VALUE'`/`'V'`),
// producing MySQL syntax errors and dropping the whole transaction.
// See bogware/bizzymod-stats#5. Long-term fix is parameterized queries
// (no string-build). For now: large fixed buffer + the truncation
// guard in Bizzy_DB_AssertQueryLen below.
char sql[4096];
FormatEx(sql, sizeof sql,
"INSERT INTO player_stats "
... "(player_id, gamemode_id, difficulty_id, server_id, "
Expand Down
13 changes: 11 additions & 2 deletions plugin/scripting/bizzymod_stats/tank_witch.sp
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,15 @@ stock void Bizzy_TankWitch_TankSpawn(int tankClient)
g_Tanks[slot].lastY = RoundToFloor(pos[1]);
g_Tanks[slot].lastZ = RoundToFloor(pos[2]);

if (g_DB == null || g_ServerId == 0) return;
// Guard against tank_records.fk_tr_map FK violation: the maps row this
// map_id references must exist. g_CurrentMapId is resolved async (map
// upsert + id lookup) so there's a window — fresh server boot, plugin
// hot-reload mid-map, or first tank of a new map before resolution
// completes — when g_CurrentMapId is 0 or stale. The identity module
// also re-resolves on OnServerLookup (covers DB-late-connect), but this
// guard makes the failure mode "lose this one record" instead of
// "FK error in errors_*.log". See bogware/bizzymod-stats#5.
if (g_DB == null || g_ServerId == 0 || g_CurrentMapId == 0) return;
char sql[384];
FormatEx(sql, sizeof sql,
"INSERT INTO tank_records (server_id, match_round_id, map_id, spawned_at) "
Expand Down Expand Up @@ -285,7 +293,8 @@ stock void Bizzy_TankWitch_WitchSpawn(int witchEnt)
g_Witches[slot].incappedUid = 0;
g_Witches[slot].state = 0;

if (g_DB == null || g_ServerId == 0) return;
// Same FK guard as tank spawn — witch_records also FKs to maps.id.
if (g_DB == null || g_ServerId == 0 || g_CurrentMapId == 0) return;
char sql[384];
FormatEx(sql, sizeof sql,
"INSERT INTO witch_records (server_id, match_round_id, map_id, seen_at, outcome) "
Expand Down
Loading