BUILD200 (2026.11.05.1) — закрепить плашку удаления и снимать штраф после гола по ACK vMix

This commit is contained in:
2026-11-05 11:48:31 +03:00
parent 011e84485e
commit 2a243e3441
30 changed files with 3044 additions and 207 deletions

View File

@@ -13,6 +13,30 @@ function extract(name) {
assert.ok(end > start, `missing function end ${name}`);
return source.slice(start, end + 4);
}
// Load new transport dependencies from production; existing I/O/DOM edges stay local.
function productionClosure(context, roots) {
const functions = new Map(), pattern = /^( {0,2})(?:async )?function (\w+)\(/gm;
let match;
while ((match = pattern.exec(source))) {
const start = match.index, lineEnd = source.indexOf('\n', start);
const singleLine = /\}\s*;?\s*$/.test(source.slice(start, lineEnd));
const end = singleLine ? lineEnd : source.indexOf('\n' + match[1] + '}', start) + match[1].length + 2;
assert.ok(end > start, 'Unclosed production helper ' + match[2]);
functions.set(match[2], source.slice(start, end));
}
const needed = new Set(roots);
function visit(name) {
assert.ok(functions.has(name), 'Missing actual production helper ' + name);
for (const candidate of functions.keys()) {
if (needed.has(candidate) || typeof context[candidate] === 'function') continue;
if (new RegExp('\\b' + candidate + '\\s*\\(').test(functions.get(name))) {
needed.add(candidate); visit(candidate);
}
}
}
roots.forEach(visit);
return [...needed].map(name => functions.get(name)).join('\n');
}
function runtime(extra = []) {
const noop = () => {};
const game = { action_id:'hockey_game_timer', type:'timer', props:{mode:'count_down',startTime:'20:00'} };
@@ -34,8 +58,18 @@ function runtime(extra = []) {
runningShortcutSequences:new Set(),pendingShortcutSequenceRuns:new Map(),data:{}};
let allowed = true;
const compact = raw => Object.fromEntries(Object.entries(raw).map(([key,value])=>[key,typeof value==='function'?value():value]));
const context = {console,performance:{now:()=>1000},state,boot:{mode:'runtime'},setTimeout,clearTimeout,
window:{HockeyMatchAccess:{storage:{getItem:()=> 'account-a'}}},
const context = {console,Date,AbortController,URLSearchParams,performance:{now:()=>1000},state,boot:{mode:'runtime'},setTimeout,clearTimeout,
window:{setTimeout,clearTimeout,HockeyMatchAccess:{storage:{getItem:()=> 'account-a'}}},
refreshQuickPanelOnAirClasses:noop,hockeyRuntimeVmixInputTitle:async input=>String(input),
async fetch(url,options={}) {
assert.equal(options.method,'POST');
if(url.startsWith('/api/hockey/agents/devices/OUTPUT-A/apply-mapping?')) return {ok:true,status:200,json:async()=>({ok:true,complete:true,mapping_ack_waited:true,confirmation:'ack',applied:1,
text_fields_total:1,text_fields_confirmed:1,missing:[],rule_missing:[],errors:[],warnings:[]})};
assert.equal(url,'/api/hockey/vmix/sequence');
const body=JSON.parse(options.body);order.push('transport');sent.push(body.commands.map(compact));
return {ok:true,status:200,json:async()=>({ok:true,confirmation:'ack',applied:body.commands.length,
results:body.commands.map(command=>({ok:true,function:command.Function,ack_waited:true}))})};
},
padTimer:value=>String(value).padStart(2,'0'),clamp:(n,l,h)=>Math.max(l,Math.min(h,n)),
hockeyCanControl:()=>allowed,componentByActionId:id=>id===game.action_id?game:id===boardComponent.action_id?boardComponent:null,
isTimerComponent:c=>c===game,ensureTimerState:()=>main,
@@ -76,9 +110,10 @@ function runtime(extra = []) {
'hockeyScoreboardDelaySeconds','hockeyScoreboardPresentationContext','hockeyObserveScoreboardPresentation','hockeyScoreboardAuxiliaryReady',
'hockeyScoreboardAuxiliaryDelay','hockeyScoreboardAuxiliaryTargetReady','hockeyMatchFlags',
'hockeyScoreboardTeamStateSettings','hockeyTeamStateSetting','hockeyGameControlLanguage','shortcutVmixStepCommands',
'hockeyAuxiliaryGateContext','hockeyScoreboardAuxiliariesEnabled',...extra];
'hockeyAuxiliaryGateContext','hockeyScoreboardAuxiliariesEnabled','hockeyPenaltyEntryDeliveryPending', 'hockeyPenaltyDisplayTargetAllowed', 'hockeyPenaltyCaptionTargetAllowed',
'hockeyPenaltyTargetDeliveryPending', 'hockeyPenaltyTransportSampler', 'sendRuntimeVmixSequence',...extra];
vm.createContext(context);
vm.runInContext([...new Set(names)].map(extract).join('\n'),context);
vm.runInContext(productionClosure(context,[...new Set(names)]),context);
return {context,game,main,board,boardComponent,step,sequence,target,state,sent,events,order,
allow:value=>{allowed=value;},clear:()=>{sent.length=events.length=order.length=0;},
functions:()=>sent.flat().map(command=>command.Function)};