99 lines
4.2 KiB
JavaScript
99 lines
4.2 KiB
JavaScript
// server/match/match/messaging.js — Match methods: messaging — unicast, broadcast, toasts, the ticker lines
|
|
// (config.broadcasts by type; the remake's CUSTOM lines), the dirty marks and flush: m.private per player only when it
|
|
// changed (plus the prep scouts of that player's board), m.public throttled (DELAYS.PUBLIC_THROTTLE) and deduplicated.
|
|
// Installed on Match.prototype by server/match/Match.js (a method container: never instantiated; `this` is the match).
|
|
|
|
import { DELAYS } from './common.js';
|
|
|
|
export class MatchMessaging {
|
|
sendTo(playerId, msg) {
|
|
if (this.disposed) return false;
|
|
const ps = this.players.get(playerId) || this.spectators.get(playerId);
|
|
if (!ps || ps.isBot || ps.left) return false;
|
|
try { return !!this.sendFn(playerId, msg); } catch (e) { this.reportError('send', e); return false; }
|
|
}
|
|
|
|
broadcast(msg) {
|
|
if (this.disposed) return;
|
|
try { this.broadcastFn(msg); } catch (e) { this.reportError('broadcast', e); }
|
|
}
|
|
|
|
toast(ps, kind, text) {
|
|
if (!ps || ps.isBot || ps.left || !ps.connected) return;
|
|
this.sendTo(ps.playerId, { t: 'm.toast', kind, text });
|
|
}
|
|
|
|
/** Broadcast ticker from config.broadcasts by type; `param` picks the variant (SHOP_LEVEL level, BOSS_HIT share…). */
|
|
tickerFor(type, args = [], { playerId = null, to = null, param = null } = {}) {
|
|
const list = Array.isArray(this.gd.config.broadcasts) ? this.gd.config.broadcasts.filter((b) => b && b.type === type) : [];
|
|
let b = list[0];
|
|
if (param != null) b = list.find((x) => Array.isArray(x.params) && x.params.includes(String(param))) || b;
|
|
const tpl = b && typeof b.text === 'string' ? b.text : null;
|
|
if (!tpl) return;
|
|
const text = tpl.replace(/\{(\d)\}/g, (_, i) => (args[Number(i)] != null ? String(args[Number(i)]) : ''));
|
|
const msg = { t: 'm.ticker', text, id: b.id, type, priority: Number(b.priority) || 0, playerId };
|
|
if (to) this.sendTo(to, msg);
|
|
else this.broadcast(msg);
|
|
}
|
|
|
|
/**
|
|
* A ticker line of the remake's own (type CUSTOM). `priority`: the match-flow notices (隐秘核心已解锁, 联防阶段, a player out
|
|
* or gone) take FLOW_TICKER_PRIORITY so the strip does not hold them behind shop-level lines; other lines 0.
|
|
*/
|
|
tickerText(text, priority = 0) {
|
|
if (!text) return;
|
|
this.broadcast({ t: 'm.ticker', text: String(text).slice(0, 200), id: null, type: 'CUSTOM', priority: Number(priority) || 0, playerId: null });
|
|
}
|
|
|
|
markPublic() { this._pubDirty = true; }
|
|
/** A player's state changed: its m.private and (throttled, deduplicated) m.public (level, board, bonds…). */
|
|
markPrivate(ps) { if (ps) { this._privDirty.add(ps); this._pubDirty = true; } }
|
|
|
|
/** Send pending m.private (per player, only when changed) and m.public (throttled ≤ 10/s). */
|
|
flush(forcePublic = false) {
|
|
if (this.disposed) return;
|
|
if (this._privDirty.size) {
|
|
const list = [...this._privDirty];
|
|
this._privDirty.clear();
|
|
for (const ps of list) {
|
|
if (!(ps.isBot || ps.left || !ps.connected)) this._sendPrivate(ps, false);
|
|
this._notifyPrepScouts(ps);
|
|
}
|
|
}
|
|
if (this._pubDirty || forcePublic) this._maybeSendPublic(forcePublic);
|
|
}
|
|
|
|
_sendPrivate(ps, force) {
|
|
let view;
|
|
try { view = ps.privateView(); } catch (e) { this.reportError('privateView', e); return; }
|
|
const json = JSON.stringify(view);
|
|
if (!force && json === ps._lastPriv) return;
|
|
ps._lastPriv = json;
|
|
this.sendTo(ps.playerId, view);
|
|
}
|
|
|
|
_maybeSendPublic(force) {
|
|
const now = this.sched.now();
|
|
if (!force && now - this._lastPubAt < DELAYS.PUBLIC_THROTTLE) {
|
|
if (!this._pubTimer) {
|
|
const wait = Math.max(1, DELAYS.PUBLIC_THROTTLE - (now - this._lastPubAt));
|
|
this._pubTimer = this.sched.setTimeout(() => {
|
|
this._pubTimer = null;
|
|
if (this.disposed) return;
|
|
try { this._maybeSendPublic(false); } catch (e) { this.reportError('public', e); }
|
|
}, wait);
|
|
}
|
|
return;
|
|
}
|
|
this._pubDirty = false;
|
|
let view;
|
|
try { view = this.publicView(); } catch (e) { this.reportError('publicView', e); return; }
|
|
const { serverNow, ...rest } = view;
|
|
const json = JSON.stringify(rest);
|
|
if (!force && json === this._lastPubJson) return;
|
|
this._lastPubJson = json;
|
|
this._lastPubAt = now;
|
|
this.broadcast(view);
|
|
}
|
|
}
|