test 4
This commit is contained in:
@@ -4153,33 +4153,39 @@ function startCustomTooltips() {
|
||||
}
|
||||
|
||||
function vmixCountdownSyncCommands(input, selectedName, millisecondsProvider, action = "start") {
|
||||
// BUILD101: keep timer transport intentionally small and deterministic.
|
||||
// Runtime is authoritative. vMix is only reseeded from the current Runtime
|
||||
// value when the operator changes state; no ACK is required to change the web timer.
|
||||
// BUILD106: Runtime is the ONLY source of truth for game/penalty time.
|
||||
// Important vMix detail: SetCountdown changes the countdown Duration; it does not
|
||||
// reliably replace the current position of a countdown that was previously started
|
||||
// or suspended. Therefore every operator state change performs a hard reseed:
|
||||
// freeze title rendering -> reset native countdown -> set Runtime value -> unfreeze.
|
||||
// Start/Resume then starts from that freshly seeded Runtime value.
|
||||
const target = { Input: String(input || "").trim(), SelectedName: String(selectedName || "").trim() };
|
||||
const renderTarget = { Input: target.Input };
|
||||
const currentValue = () => {
|
||||
const milliseconds = typeof millisecondsProvider === "function" ? millisecondsProvider() : millisecondsProvider;
|
||||
return vmixCountdownValue(milliseconds);
|
||||
};
|
||||
if (!target.Input || !target.SelectedName) return [];
|
||||
if (action === "stop" || action === "pause") {
|
||||
// BUILD105: vMix StopCountdown means STOP + RESET TO BEGINNING. It must never
|
||||
// be used for an operator pause/stop where the current sports time has to freeze.
|
||||
// SuspendCountdown is the vMix pause-only command and preserves the exact native
|
||||
// countdown value until the next SetCountdown + StartCountdown.
|
||||
return [
|
||||
{ Function: "SuspendCountdown", ...target },
|
||||
];
|
||||
}
|
||||
if (action === "set") {
|
||||
// Explicit edits/reset still need to write the Runtime value while stopped.
|
||||
return [
|
||||
{ Function: "StopCountdown", ...target },
|
||||
{ Function: "SetCountdown", ...target, Value: currentValue },
|
||||
];
|
||||
}
|
||||
return [
|
||||
|
||||
const reseedStopped = [
|
||||
{ Function: "PauseRender", ...renderTarget },
|
||||
{ Function: "StopCountdown", ...target },
|
||||
{ Function: "SetCountdown", ...target, Value: currentValue },
|
||||
{ Function: "ResumeRender", ...renderTarget },
|
||||
];
|
||||
|
||||
if (action === "stop" || action === "pause" || action === "set") {
|
||||
// Do not trust the native vMix current position on Pause. Freeze at the exact
|
||||
// Runtime value and leave the native countdown stopped. The next Resume will
|
||||
// hard-reseed again before StartCountdown.
|
||||
return reseedStopped;
|
||||
}
|
||||
|
||||
return [
|
||||
{ Function: "PauseRender", ...renderTarget },
|
||||
{ Function: "StopCountdown", ...target },
|
||||
{ Function: "SetCountdown", ...target, Value: currentValue },
|
||||
{ Function: "ResumeRender", ...renderTarget },
|
||||
{ Function: "StartCountdown", ...target },
|
||||
];
|
||||
}
|
||||
@@ -4655,8 +4661,9 @@ function startCustomTooltips() {
|
||||
function sendRuntimeVmixTimerSequence(commands) {
|
||||
// BUILD103: timer transport is deliberately independent from the generic
|
||||
// shortcut/title/Mapping ACK queue. The web timer has already changed state before
|
||||
// this function is called. Server delivery_mode=timer-fast sends the tiny native
|
||||
// Countdown pair to Agent immediately without waiting for ACK, preferably as one batch.
|
||||
// this function is called. Server delivery_mode=timer-fast-ordered sends native Countdown commands to Agent
|
||||
// immediately without ACK, but as separate ordered WebSocket frames. This avoids any
|
||||
// ambiguity about command ordering inside Agent vmix.batch handling.
|
||||
const rawCommands = typeof commands === "function" ? commands() : commands;
|
||||
const clean = (rawCommands || []).map(compactVmixCommand).filter((command) => command.Function);
|
||||
if (!clean.length) return Promise.resolve({ ok: true, applied: 0, requested: 0, results: [] });
|
||||
@@ -4667,14 +4674,13 @@ function startCustomTooltips() {
|
||||
credentials: "same-origin",
|
||||
keepalive: true,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
// Intentionally omit sequence_id/button_id. Agent 1.4+ can then send the
|
||||
// tiny SetCountdown+Start/Stop pair as one vmix.batch instead of waiting
|
||||
// for several interactive ACK round-trips.
|
||||
// Intentionally omit sequence_id/button_id. Timer transport is fire-and-forget,
|
||||
// but BUILD106 uses ordered single WebSocket frames rather than vmix.batch.
|
||||
body: JSON.stringify({
|
||||
commands: clean,
|
||||
device_id: currentRuntimeVmixDeviceId(),
|
||||
session_token: currentRuntimeHockeySessionToken(),
|
||||
delivery_mode: "timer-fast",
|
||||
delivery_mode: "timer-fast-ordered",
|
||||
}),
|
||||
}).then(async (response) => {
|
||||
let payload = {};
|
||||
@@ -4768,6 +4774,45 @@ function startCustomTooltips() {
|
||||
return true;
|
||||
}
|
||||
|
||||
function configuredHockeyVmixGameCountdownTargets() {
|
||||
const targets = [];
|
||||
const seen = new Set();
|
||||
(state.config.shortcut_sequences || []).forEach((sequence) => {
|
||||
if (sequence?.enabled === false) return;
|
||||
(sequence.steps || []).forEach((step) => {
|
||||
if (!step || step.enabled === false || step.type !== "hockey_vmix_timers_start") return;
|
||||
if (!step.sync_vmix_game || step.game_vmix_mode !== "countdown") return;
|
||||
const input = String(step.game_vmix_input || "").trim();
|
||||
const selectedName = String(step.game_vmix_selected_name || "").trim();
|
||||
if (!input || !selectedName) return;
|
||||
const timer = componentByActionId(step.game_timer_action_id || "hockey_game_timer");
|
||||
if (!timer || !isTimerComponent(timer)) return;
|
||||
const key = `${input}\u0000${selectedName}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
targets.push({ input, selectedName, timer, timerState: ensureTimerState(timer) });
|
||||
});
|
||||
});
|
||||
return targets;
|
||||
}
|
||||
|
||||
async function syncConfiguredScoreboardCountdownsToRuntime() {
|
||||
const commands = [];
|
||||
configuredHockeyVmixGameCountdownTargets().forEach(({ input, selectedName, timerState }) => {
|
||||
commands.push(...vmixCountdownSyncCommands(
|
||||
input,
|
||||
selectedName,
|
||||
() => timerState.currentMs,
|
||||
timerState.running ? "start" : "set"
|
||||
));
|
||||
});
|
||||
if (!commands.length) return { ok: true, applied: 0, requested: 0 };
|
||||
// Await only WebSocket dispatch (never vMix ACK) so the countdown reseed reaches the
|
||||
// Agent before the scoreboard OverlayIn command. This fixes stale timer values when F1
|
||||
// shows a scoreboard before Space has ever been pressed.
|
||||
return await sendRuntimeVmixTimerSequence(commands);
|
||||
}
|
||||
|
||||
function penaltyMirrorKey(component, event) {
|
||||
return `${String(component?.action_id || "hockey_penalty_dashboard")}:${String(event?.id || "")}`;
|
||||
}
|
||||
@@ -5078,26 +5123,21 @@ function startCustomTooltips() {
|
||||
&& (force || assignmentChanged || previous?.running !== true);
|
||||
if (entry.event.running) {
|
||||
if (force || assignmentChanged || startingCountdown) {
|
||||
// BUILD101: while the web penalty is running, only reseed then run.
|
||||
// Do not inject an extra Stop before every Start; Runtime already owns
|
||||
// the state and the short SetCountdown+Start pair is enough.
|
||||
// BUILD106: penalty countdowns follow the same Runtime-authoritative rule
|
||||
// as the game clock. Never continue an old native position: Stop first,
|
||||
// then seed the exact web remaining time, then Start.
|
||||
stopCommands.push({ Function: "StopCountdown", Input: target.input, SelectedName: target.selected_name });
|
||||
setCommands.push({ Function: "SetCountdown", Input: target.input, SelectedName: target.selected_name, Value: () => vmixCountdownValue(entry.event.remainingMs) });
|
||||
}
|
||||
if (startingCountdown) {
|
||||
runCommands.push({ Function: "StartCountdown", Input: target.input, SelectedName: target.selected_name });
|
||||
}
|
||||
} else if (force || assignmentChanged || previous?.running !== false) {
|
||||
// BUILD105: an unchanged prepared/paused penalty must use SuspendCountdown.
|
||||
// StopCountdown resets a native vMix countdown to its beginning. For a
|
||||
// reassigned/reset target we may still deliberately Stop + Set below.
|
||||
if (preservePausedCountdown && !assignmentChanged) {
|
||||
stopCommands.push({ Function: "SuspendCountdown", Input: target.input, SelectedName: target.selected_name });
|
||||
} else {
|
||||
stopCommands.push({ Function: "StopCountdown", Input: target.input, SelectedName: target.selected_name });
|
||||
}
|
||||
if (!preservePausedCountdown || assignmentChanged) {
|
||||
setCommands.push({ Function: "SetCountdown", Input: target.input, SelectedName: target.selected_name, Value: () => vmixCountdownValue(entry.event.remainingMs) });
|
||||
}
|
||||
// prepared/paused penalty: reseed from Runtime. preservePausedCountdown
|
||||
// remains in the public call signature for compatibility, but native vMix time is
|
||||
// never trusted as the authoritative value anymore.
|
||||
stopCommands.push({ Function: "StopCountdown", Input: target.input, SelectedName: target.selected_name });
|
||||
setCommands.push({ Function: "SetCountdown", Input: target.input, SelectedName: target.selected_name, Value: () => vmixCountdownValue(entry.event.remainingMs) });
|
||||
}
|
||||
} else {
|
||||
assignedMirrorKeys.add(eventKey);
|
||||
@@ -5419,6 +5459,9 @@ function startCustomTooltips() {
|
||||
else if (String(meta.source || "").startsWith("keyboard")) toast(`Шорткат выполнен: ${sequence.name}`);
|
||||
return true;
|
||||
}
|
||||
if (sequence.is_scoreboard_sequence) {
|
||||
await syncConfiguredScoreboardCountdownsToRuntime();
|
||||
}
|
||||
const stepErrors = [];
|
||||
for (const [stepIndex, step] of (sequence.steps || []).entries()) {
|
||||
try {
|
||||
@@ -13872,7 +13915,7 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
|
||||
<div class="shortcut-finish-action-list">${timerFinishActionRows(step)}</div>
|
||||
|
||||
<div class="shortcut-vmix-inventory-note"><span>${escapeHtml(shortcutInventoryLabel())}</span><small>Для каждого countdown теперь обязательно выбирается конкретный Text / SelectedName. Это исключает отправку времени в первый текстовый элемент по умолчанию.</small></div>
|
||||
<p class="shortcut-step-note">Режим <b>Countdown vMix</b>: веб-таймер является главным. Start сразу меняет состояние Runtime и независимо отправляет <code>SetCountdown → StartCountdown</code> в vMix; Pause/Stop сразу останавливают Runtime и независимо отправляют только <code>SuspendCountdown</code> (пауза без сброса), без повторной установки времени. Ошибка Agent не блокирует управление таймером. Каждую секунду значение в vMix не передаётся. <b>Text mirror</b> оставлен только для совместимости со старыми титрами. Для верхнего счёта используется одна penalty-плашка: при реальном большинстве она показывает ближайшее изменение численного состава; при чистом равном обоюдном удалении сама по себе не появляется.</p>
|
||||
<p class="shortcut-step-note">Режим <b>Countdown vMix</b>: веб-таймер является единственным источником времени. Перед Start/Resume vMix выполняет <code>StopCountdown → SetCountdown(время Runtime) → StartCountdown</code>; при Pause/Stop vMix выполняет <code>StopCountdown → SetCountdown(время Runtime)</code> и остаётся остановленным на точном веб-времени. Команды идут в Agent без ACK отдельными кадрами строго по порядку. Каждую секунду значение в vMix не передаётся. <b>Text mirror</b> оставлен только для совместимости со старыми титрами. Для верхнего счёта используется одна penalty-плашка: при реальном большинстве она показывает ближайшее изменение численного состава; при чистом равном обоюдном удалении сама по себе не появляется.</p>
|
||||
</div>`;
|
||||
} else if (step.type === "delay") {
|
||||
body.innerHTML = `<div class="shortcut-step-grid"><label>Задержка, мс<input type="number" min="0" max="10000" step="10" data-step-field="milliseconds" value="${Number(step.milliseconds) || 0}"></label></div>`;
|
||||
|
||||
Reference in New Issue
Block a user