mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52:50 +08:00
Follow-up to the auto-pause fix: resume never fired when someone came back. Root cause (verified live against a TS3 server): the full-client library's command/response channel is dead whenever >=2 clients are connected anywhere on the server — clientlist, channellist and channelclientlist ALL time out (confirmed even with the two clients in different channels). So the moment a listener returns is exactly the moment occupancy can no longer be queried, and the query-based refreshOccupancy() can never observe the return -> no resume. Event channelID is also unusable (library reads notify `cid` but enter-view carries `ctid`, so it's always 0), so per-channel membership can't be derived from events either. Fix (minimal, asymmetric): keep PAUSE on the authoritative clientlist path (reliable precisely because it only succeeds when the bot is alone on the server — the only state pause should fire), and arm RESUME directly from the clientEnter push event. Because the bot only auto-pauses while alone, the sole way occupancy can return while autoPaused is set is a fresh connection, which arrives reliably as clientEnter. New pure predicate shouldResumeOnReturn() + _resumeIfReturning() resume iff autoPaused && paused; the resume branch routes through handleOccupancy(1) and NEVER pauses (userCount>0), so a spurious enter can only harmlessly resume. The bot's own enter at connect is a no-op (autoPaused is already false). This deliberately does NOT adopt a full event-tracked peer set: events don't reliably seed clients already present when the bot joins, so a count-from-events ==0 would reintroduce the false-pause bug we just fixed, and reconcile can't heal it (clientlist only works when alone). Pause must trust only the authoritative query; resume can trust the event. Net semantics: pause when the server is empty (bot alone), resume when someone connects. Channel granularity is impossible with this library. UI copy updated to say "服务器" instead of "频道", and the Settings toggle default corrected to false to match the backend default. cmdVote intentionally left as-is. Verified live: auto-paused bot + a real client connecting -> resume fires with no clientlist call in the path; bot's own enter and not-auto-paused enters do not resume. 311 unit tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
68 lines
3.0 KiB
TypeScript
68 lines
3.0 KiB
TypeScript
export type PlayerStateName = "idle" | "playing" | "paused";
|
|
export type OccupancyAction = "pause" | "resume" | "none";
|
|
|
|
/**
|
|
* Convert a channel client-list length into the number of *other* users, or
|
|
* `null` when occupancy can't be determined.
|
|
*
|
|
* A connected bot is always a member of its own channel, so a valid query
|
|
* returns at least 1 (the bot itself). A length of 0 therefore does NOT mean
|
|
* "empty channel" — it means the underlying `clientlist` query failed (e.g. the
|
|
* full-client `clientlist` command times out when other clients are present,
|
|
* and `getClientsInChannel()` returns `[]` on error). Treating that failure as
|
|
* "empty" is what caused playback to auto-pause within seconds whenever a
|
|
* listener was actually in the channel. When the result is indeterminate we
|
|
* return `null` so callers skip the auto-pause/idle decision entirely rather
|
|
* than mis-reading an unknown state as empty.
|
|
*/
|
|
export function occupancyFromClientList(clientCount: number): number | null {
|
|
if (clientCount <= 0) return null; // query failed → occupancy unknown
|
|
return clientCount - 1; // exclude the bot itself
|
|
}
|
|
|
|
/**
|
|
* Decide what auto-pause should do given channel occupancy.
|
|
* - empty (userCount <= 0): pause iff enabled and currently playing.
|
|
* - re-populated (userCount > 0): resume iff we previously auto-paused and are still paused.
|
|
* `autoPaused` distinguishes our auto-pause from a user pause, so user pauses are never resumed.
|
|
*/
|
|
export function decideOccupancyAction(
|
|
playerState: PlayerStateName,
|
|
autoPaused: boolean,
|
|
enabled: boolean,
|
|
userCount: number,
|
|
): OccupancyAction {
|
|
const empty = userCount <= 0;
|
|
if (empty) {
|
|
if (enabled && playerState === "playing") return "pause";
|
|
return "none";
|
|
}
|
|
if (autoPaused && playerState === "paused") return "resume";
|
|
return "none";
|
|
}
|
|
|
|
/**
|
|
* Whether a client-presence push event (a `clientEnter`) should trigger an
|
|
* auto-resume, WITHOUT consulting a clientlist query.
|
|
*
|
|
* Why event-driven: the full-client `clientlist`/`channellist` commands time
|
|
* out whenever ≥2 clients are connected to the server (a library limitation) —
|
|
* which is exactly the moment a listener returns. So occupancy cannot be
|
|
* re-queried to confirm the return; we must act on the push event itself.
|
|
* This is sound because the bot only ever auto-pauses while it is alone on the
|
|
* server (the sole state in which the occupancy query succeeds and pause
|
|
* fires). Therefore, while `autoPaused` is true, the only way occupancy can
|
|
* return is a fresh connection — delivered reliably as `clientEnter`.
|
|
*
|
|
* Gating on `autoPaused` (not merely "paused") guarantees we never revive a
|
|
* track the user paused by hand, and makes the bot's own `clientEnter` at
|
|
* connect a no-op (autoPaused is cleared to false on connect). This predicate
|
|
* NEVER pauses — pause stays on the authoritative clientlist path.
|
|
*/
|
|
export function shouldResumeOnReturn(
|
|
autoPaused: boolean,
|
|
playerState: PlayerStateName,
|
|
): boolean {
|
|
return autoPaused && playerState === "paused";
|
|
}
|