{"version":3,"file":"sound-lab-agent.bundle.js?hash=1e0bdab0075790131522","mappings":";;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","sources":["webpack://game-client-lerna/./build/internal-tools/sound-lab/agent.js"],"sourcesContent":["/**\n * Sound Lab runtime agent.\n *\n * Bundled into the instrumented game build as an extra webpack entry. It:\n *   - drains the `window.__SLQ` queue populated by the instrumentation and\n *     replaces it with a live handler (so script order never matters);\n *   - fetches the points manifest from the Sound Lab server and plays the\n *     assigned sound whenever an enabled point triggers; on a deployed build\n *     running WITHOUT the tool, a session published to the asset-pack host\n *     is read back instead (see loadPointsFromFtp) — never when the tool is\n *     in use, where the tool is the only source of truth;\n *   - records the game canvas (`#app-canvas`) with MediaRecorder and reports\n *     every triggered point with a timestamp relative to the recording start;\n *   - talks to the Sound Lab tool (parent frame) over postMessage;\n *   - exposes the game's own sound manager over a request/reply RPC, which is\n *     what the tool's Game Sounds screen drives (see GAME-SOUND RPC below).\n *\n * Parent -> agent messages: SL_START_RECORDING, SL_STOP_RECORDING,\n *                           SL_RELOAD_POINTS, SL_PING, SL_MUTE, SL_SOUNDS,\n *                           SL_GS_CALL\n * Agent -> parent messages: SL_READY, SL_POINTS_LOADED, SL_EVENT,\n *                           SL_RECORDING_STARTED, SL_RECORDING, SL_ERROR,\n *                           SL_PONG, SL_GS_REPLY\n *\n * The audio-stream switch mutes through SL_MUTE (see applyStreamMute). The\n * tool's same-origin `Howler.mute` reach-in (LabWorkbench) survives only as\n * a fallback for agents that predate SL_MUTE — it cannot work at all when\n * the game is framed cross-origin (straight from :9000).\n */\n(function () {\n    'use strict';\n\n    if (window.__SOUNDLAB_AGENT__) {\n        return;\n    }\n    window.__SOUNDLAB_AGENT__ = true;\n\n    var MAX_EVENTS = 8000;\n\n    function detectServerBase() {\n        try {\n            var fromQuery = new URLSearchParams(window.location.search).get('slServer');\n            if (fromQuery) {\n                return fromQuery.replace(/\\/+$/, '');\n            }\n        } catch (e) {\n            /* ignore */\n        }\n        // Served by the Sound Lab server itself under /game/.\n        if (window.location.pathname.indexOf('/game/') === 0) {\n            return window.location.origin;\n        }\n        // The NODE server (APIs), not the Vite dev server — a game on :9000\n        // framed by the tool reaches it directly. One value with the tool's\n        // config.json `port`.\n        return 'http://localhost:4610';\n    }\n\n    /**\n     * On a deployed build the default server base is localhost, and fetching\n     * it makes Chrome ask for local-network access the moment the game loads.\n     * So the Sound Lab server is only contacted on an explicit signal that the\n     * tool is in use: `?useAgents=true`, an explicit `?slServer=`, or the game\n     * being served by the Sound Lab server itself (under /game/).\n     */\n    function agentsEnabled() {\n        try {\n            var q = new URLSearchParams(window.location.search);\n            if (q.get('useAgents') === 'true' || q.get('slServer')) {\n                return true;\n            }\n        } catch (e) {\n            /* ignore */\n        }\n        return window.location.pathname.indexOf('/game/') === 0;\n    }\n\n    var serverBase = detectServerBase();\n    var points = {}; // id -> manifest entry\n    var audioCache = {}; // sound file name -> HTMLAudioElement (template)\n    var recording = null; // { recorder, events, startTs, chunks, mime }\n\n    function post(msg) {\n        try {\n            if (window.parent && window.parent !== window) {\n                window.parent.postMessage(msg, '*');\n            }\n        } catch (e) {\n            /* ignore */\n        }\n    }\n\n    // ------------------------------------------------------------------ //\n    // Points config + sound playback\n    // ------------------------------------------------------------------ //\n\n    function loadPoints() {\n        if (!agentsEnabled()) {\n            // No tool in use — never touch localhost (Chrome's local-network\n            // permission prompt). Published sessions still replay from FTP.\n            loadPointsFromFtp();\n            return;\n        }\n        fetch(serverBase + '/api/points', { cache: 'no-store' })\n            .then(function (r) {\n                return r.json();\n            })\n            .then(function (json) {\n                points = (json && json.points) || {};\n                preloadSounds();\n                var active = Object.keys(points).filter(function (id) {\n                    return points[id].enabled && points[id].sound;\n                });\n                post({ type: 'SL_POINTS_LOADED', total: Object.keys(points).length, active: active.length });\n                console.log('[SOUND-LAB agent]: points loaded -', Object.keys(points).length, 'total,', active.length, 'active');\n            })\n            .catch(function (e) {\n                // No FTP fallback here: the tool is in use (agentsEnabled), so\n                // a published session must never shadow the live one — playing\n                // STALE overrides while the tool shows something else is worse\n                // than playing nothing.\n                console.warn('[SOUND-LAB agent]: could not load points from', serverBase, e);\n            });\n    }\n\n    /**\n     * FTP fallback: the tool's \"Publish session\" (PackagePanel.tsx) puts the\n     * BASE points manifest (points.json), the published project's changes\n     * (overrides.json — one file, last publish wins), the sound files and the\n     * mixer/fade/idle settings (gamesounds.json) on the asset-pack host under\n     * `<CustomResourcesHost>/assets/<gameKey>/sounds/soundlab/`. When the\n     * game runs WITHOUT the tool (a deployed build, agentsEnabled false) and\n     * with `&replaceAssets=true` or `&replaceSoundAssets=true` (the same\n     * switches the game itself uses for FTP assets), the session is read back\n     * from there and reproduces with no tool running at all. Inside the tool\n     * this never runs — the tool strips them from the framed URL and the live\n     * manifest wins.\n     *\n     * One contract with the publish action (PackagePanel.tsx in the tool):\n     * the `soundlab/` layout and the `gamesounds.json`/`overrides.json`\n     * shapes change together.\n     */\n    function loadPointsFromFtp() {\n        var host = null;\n        try {\n            host = window.ssi && window.ssi.CustomResourcesHost ? String(window.ssi.CustomResourcesHost) : null;\n        } catch (e) {\n            /* ignore */\n        }\n        var wantsFtp = false;\n        try {\n            // Either switch: replaceSoundAssets is the sound-only one, which is\n            // exactly what a published session needs.\n            var q = new URLSearchParams(window.location.search);\n            wantsFtp = q.get('replaceAssets') === 'true' || q.get('replaceSoundAssets') === 'true';\n        } catch (e) {\n            /* ignore */\n        }\n        if (!host || !wantsFtp) {\n            return;\n        }\n        // gameKey (for the URL) and the sound manager (for the settings)\n        // both come from Core, which boots after this script - poll until\n        // they show up, then give up quietly.\n        var tries = 0;\n        var timer = setInterval(function () {\n            tries += 1;\n            if (!gameKey() || !sounds()) {\n                if (tries > 240) {\n                    clearInterval(timer);\n                }\n                return;\n            }\n            clearInterval(timer);\n            var base = host + '/assets/' + gameKey() + '/sounds/soundlab/';\n            // Two files: points.json is the BASE manifest (no setups), and\n            // overrides.json carries the published project's changes on top —\n            // a single file, whichever project published last. Older publishes\n            // have no overrides.json; the base alone then just plays nothing.\n            Promise.all([\n                fetch(base + 'points.json?time=' + Date.now()).then(function (r) {\n                    if (!r.ok) {\n                        throw new Error('points.json -> ' + r.status);\n                    }\n                    return r.json();\n                }),\n                fetch(base + 'overrides.json?time=' + Date.now())\n                    .then(function (r) {\n                        return r.ok ? r.json() : null;\n                    })\n                    .catch(function () {\n                        return null;\n                    }),\n            ])\n                .then(function (results) {\n                    var json = results[0];\n                    points = (json && json.points) || {};\n                    mergeOverrides(points, results[1]);\n                    soundBase = base;\n                    ftpSession = true;\n                    preloadSounds();\n                    var active = Object.keys(points).filter(function (id) {\n                        return points[id].enabled && points[id].sound;\n                    });\n                    post({ type: 'SL_POINTS_LOADED', total: Object.keys(points).length, active: active.length });\n                    console.log('[SOUND-LAB agent]: points loaded from FTP -', Object.keys(points).length, 'total,', active.length, 'active');\n                    applyPublishedGameSounds(base);\n                })\n                .catch(function (e) {\n                    console.warn('[SOUND-LAB agent]: no published session on the asset-pack host', e);\n                });\n        }, 500);\n    }\n\n    /**\n     * Applies a published overrides.json onto the base manifest — the same\n     * merge the Sound Lab server does for GET /api/points: known ids take the\n     * changed fields, variant ids (baseId#label) are synthesized from their\n     * base, entries whose base the build no longer has are skipped.\n     */\n    function mergeOverrides(pts, overlay) {\n        var patches = (overlay && overlay.points) || {};\n        Object.keys(patches).forEach(function (id) {\n            var patch = patches[id];\n            if (pts[id]) {\n                Object.assign(pts[id], patch);\n                return;\n            }\n            var hash = id.indexOf('#');\n            var basePt = hash > 0 ? pts[id.slice(0, hash)] : null;\n            if (!basePt) {\n                return;\n            }\n            pts[id] = Object.assign({}, basePt, {\n                id: id,\n                label: id.slice(hash + 1),\n                variantOf: id.slice(0, hash),\n                enabled: false,\n                sound: null,\n                volume: 1,\n                offsetMs: 0,\n            }, patch);\n        });\n    }\n\n    /**\n     * The published fade/idle settings; every part is optional.\n     *\n     * Volumes are deliberately NOT applied here (and the tool publishes an\n     * empty `volumes` map): a game slot's level ships BAKED in the rendered\n     * pack that `replaceAssets` loads, so a runtime attenuation on top would\n     * apply the level twice — 0.2 baked × 0.2 runtime left sounds tuned in\n     * the tool near-inaudible on the approval env.\n     */\n    function applyPublishedGameSounds(base) {\n        fetch(base + 'gamesounds.json?time=' + Date.now())\n            .then(function (r) {\n                return r.ok ? r.json() : null;\n            })\n            .then(function (cfg) {\n                var mgr = sounds();\n                if (!cfg || !mgr) {\n                    return;\n                }\n                try {\n                    if (cfg.fadeConfig) {\n                        mgr.updateBgSoundFadeDuration({\n                            backgroundMusicFadeOutDurationNew: cfg.fadeConfig.backgroundMusicFadeOutDuration,\n                            backgroundNoPlayFadeInDurationNew: cfg.fadeConfig.backgroundNoPlayFadeInDuration,\n                        });\n                    }\n                    if (cfg.idleConfig != null) {\n                        mgr.updateBGInactivityTimeConfiguration(cfg.idleConfig);\n                    }\n                } catch (e) {\n                    console.warn('[SOUND-LAB agent]: could not apply published game-sound settings', e);\n                }\n            })\n            .catch(function () {\n                /* no gamesounds.json published - fine */\n            });\n    }\n\n    // Where the sound files live. A live session plays them from the Sound\n    // Lab server; the published-to-FTP fallback repoints this at the\n    // asset-pack host (see loadPointsFromFtp).\n    var soundBase = serverBase + '/sounds/';\n\n    // True once the points were loaded from the asset-pack host (published\n    // session, no tool). Packaged points then play through the game's own\n    // replaced packs instead of the raw files - see playSound().\n    var ftpSession = false;\n\n    // Sound files pushed by the tool (SL_SOUNDS): the library lives in the\n    // tool's BROWSER storage (IndexedDB), not on the server, so the agent\n    // cannot fetch it - the tool transfers the blobs over postMessage and\n    // they play from object URLs created in this frame. The server path\n    // stays as a fallback for names never pushed (older tool, leftovers).\n    var soundData = {}; // sound file name -> object URL of a pushed blob\n\n    function soundUrl(file) {\n        if (soundData[file]) {\n            return soundData[file];\n        }\n        return soundBase + encodeURIComponent(file);\n    }\n\n    /** SL_SOUNDS: replaces the pushed library wholesale. */\n    function acceptSounds(map) {\n        try {\n            Object.keys(soundData).forEach(function (name) {\n                try {\n                    URL.revokeObjectURL(soundData[name]);\n                } catch (e) {\n                    /* ignore */\n                }\n            });\n            soundData = {};\n            Object.keys(map || {}).forEach(function (name) {\n                var blob = map[name];\n                if (blob && typeof Blob !== 'undefined' && blob instanceof Blob) {\n                    soundData[name] = URL.createObjectURL(blob);\n                }\n            });\n            // Cached templates may hold revoked URLs (or the server path for\n            // a file that just arrived) - drop them, they rebuild lazily.\n            audioCache = {};\n            preloadSounds();\n        } catch (e) {\n            /* never break the game */\n        }\n    }\n\n    function preloadSounds() {\n        Object.keys(points).forEach(function (id) {\n            var pt = points[id];\n            if (pt.enabled && pt.sound && !audioCache[pt.sound]) {\n                var a = new Audio(soundUrl(pt.sound));\n                a.preload = 'auto';\n                audioCache[pt.sound] = a;\n            }\n        });\n    }\n\n    /**\n     * The game's sprite window for a slot, but ONLY when the pack holding\n     * it was actually replaced from the asset-pack host (its URL points at\n     * CustomResourcesHost after replaceAssets rewrote it). A slot found in\n     * a pack the game still loads from its own bundle is NOT playable as a\n     * packaged point - the audio there is the original, not the render.\n     */\n    function gameSlotWindow(slot) {\n        var host = null;\n        try {\n            host = window.ssi && window.ssi.CustomResourcesHost ? String(window.ssi.CustomResourcesHost) : null;\n        } catch (e) {\n            /* ignore */\n        }\n        var mgr = sounds();\n        if (!host || !mgr || typeof mgr.getAllSounds !== 'function') {\n            return null;\n        }\n        var groups = mgr.getAllSounds() || [];\n        for (var i = 0; i < groups.length; i++) {\n            var sp = groups[i] && groups[i].sprite;\n            if (!sp || Array.isArray(sp) || !sp[slot]) {\n                continue;\n            }\n            var url = null;\n            try {\n                url = groups[i].howl && groups[i].howl._src\n                    ? (Array.isArray(groups[i].howl._src) ? groups[i].howl._src[0] : groups[i].howl._src)\n                    : (typeof groups[i].url === 'string' ? groups[i].url : null);\n            } catch (e) {\n                /* mid-load */\n            }\n            if (typeof url === 'string' && url.indexOf(host) === 0) {\n                return sp[slot];\n            }\n        }\n        return null;\n    }\n\n    // Guards agent-initiated SoundManager.play() calls: the instrumented\n    // play() calls __SL_SOUND, which must not treat the agent's own\n    // playback as a game trigger (points[baseId] could suppress it or\n    // recurse right back into playSound).\n    var selfPlay = false;\n\n    /** Set by __SL_SOUND; see the note there. */\n    var lastPlayPointId = null;\n\n    /**\n     * Point sounds play through plain Audio elements, which neither the game's\n     * own mute button (ON_SOUND_MUTED) nor the tool's off-stage Howler mute\n     * can silence — so the agent checks both itself before playing one.\n     */\n    function pointsMuted() {\n        if (streamMuted) {\n            return true;\n        }\n        try {\n            var mgr = sounds();\n            if (mgr && mgr.ON_SOUND_MUTED && mgr.ON_SOUND_MUTED.value) {\n                return true;\n            }\n        } catch (e) {\n            /* ignore */\n        }\n        try {\n            if (window.Howler && window.Howler._muted) {\n                return true;\n            }\n        } catch (e) {\n            /* ignore */\n        }\n        return false;\n    }\n\n    // Point sounds currently SOUNDING. pointsMuted() only gates new plays and\n    // Howler.mute never reaches these plain Audio nodes, so applyStreamMute\n    // (SL_MUTE) mutes the ones already going — muted, not stopped: a\n    // switch-back mid-sound lets the tail through, same as the rest of the\n    // game stream.\n    var livePointNodes = [];\n    var pointNodesMuted = false;\n    function mutePointNodes(flag) {\n        pointNodesMuted = !!flag;\n        for (var i = 0; i < livePointNodes.length; i++) {\n            try {\n                livePointNodes[i].muted = pointNodesMuted;\n            } catch (e) {\n                /* ignore */\n            }\n        }\n    }\n\n    function playSound(pt) {\n        try {\n            // A published session plays a PACKAGED point through the game's\n            // own (replaced) pack - the audition is the shipped audio, with\n            // the point volume already baked in at render time.\n            if (ftpSession && pt.slot && gameSlotWindow(pt.slot)) {\n                var mgr = sounds();\n                var slotDelay = Math.max(0, pt.offsetMs || 0);\n                var playSlot = function () {\n                    selfPlay = true;\n                    try {\n                        mgr.play(pt.slot);\n                    } catch (e) {\n                        /* never break the game */\n                    }\n                    selfPlay = false;\n                };\n                if (slotDelay > 0) {\n                    setTimeout(playSlot, slotDelay);\n                } else {\n                    playSlot();\n                }\n                return;\n            }\n            var base = audioCache[pt.sound];\n            if (!base) {\n                base = new Audio(soundUrl(pt.sound));\n                base.preload = 'auto';\n                audioCache[pt.sound] = base;\n            }\n            var node = base.cloneNode(true);\n            node.volume = pt.volume == null ? 1 : Math.max(0, Math.min(1, pt.volume));\n            var delay = Math.max(0, pt.offsetMs || 0);\n            var untrack = function () {\n                var at = livePointNodes.indexOf(node);\n                if (at !== -1) {\n                    livePointNodes.splice(at, 1);\n                }\n            };\n            var play = function () {\n                // Checked at PLAY time, not trigger time — an offset point\n                // must stay silent if the game was muted during the delay.\n                if (pointsMuted()) {\n                    return;\n                }\n                node.muted = pointNodesMuted;\n                livePointNodes.push(node);\n                node.addEventListener('ended', untrack);\n                node.play().catch(function () {\n                    untrack(); // autoplay policies etc.\n                });\n            };\n            if (delay > 0) {\n                setTimeout(play, delay);\n            } else {\n                play();\n            }\n        } catch (e) {\n            /* never break the game */\n        }\n    }\n\n    // ------------------------------------------------------------------ //\n    // Trigger handling (queue protocol)\n    // ------------------------------------------------------------------ //\n\n    function trigger(id, label, extra) {\n        // A variant entry (id#label, e.g. one per spine animation type)\n        // overrides the base point when it exists - including silencing it\n        // when the variant is disabled.\n        var pt = null;\n        if (label != null && label !== '') {\n            pt = points[id + '#' + label];\n        }\n        if (!pt) {\n            pt = points[id];\n        }\n        if (pt && pt.enabled && pt.sound && !pt.mute) {\n            playSound(pt);\n        }\n        if (recording) {\n            var t = performance.now() - recording.startTs;\n            var ev = { id: id, t: t, label: label || null };\n            if (extra && extra.d) {\n                ev.dur = extra.d;\n            }\n            if (extra && extra.u) {\n                ev.src = extra.u;\n            }\n            if (extra && extra.o) {\n                ev.off = extra.o; // offset (s) into the source file (audio sprites)\n            }\n            if (extra && extra.l) {\n                ev.loop = true; // plays looped until an explicit stop\n            }\n            if (extra && extra.ctl) {\n                ev.ctl = extra.ctl; // 'stop' | 'fadeout' | 'fadein'\n                ev.fd = extra.fd || 0; // fade duration (ms)\n                ev.fv = extra.fv || 0; // target volume\n            }\n            if (recording.events.length < MAX_EVENTS) {\n                recording.events.push(ev);\n            }\n            post({\n                type: 'SL_EVENT',\n                id: ev.id,\n                t: ev.t,\n                label: ev.label,\n                dur: ev.dur,\n                src: ev.src,\n                off: ev.off,\n                loop: ev.loop,\n                ctl: ev.ctl,\n                fd: ev.fd,\n                fv: ev.fv,\n            });\n        }\n    }\n\n    /**\n     * Sprite segment [offset, duration] (seconds) of `id` inside a Howl, or\n     * null when the howl plays the whole file. Audio-sprite games pack every\n     * sound into one big file - howl.duration() alone would report the full\n     * file length for each of them.\n     */\n    function spriteSeg(howl, id) {\n        try {\n            var sp = howl && howl._sprite && howl._sprite[id];\n            if (sp && sp.length >= 2 && sp[1] > 0) {\n                return { o: (sp[0] || 0) / 1000, d: sp[1] / 1000, l: !!sp[2] };\n            }\n        } catch (e) {\n            /* ignore */\n        }\n        return null;\n    }\n\n    /** Duration + offset + absolute source url of a game sound, best effort. */\n    function sndInfo(mgr, id) {\n        try {\n            // Howler-based SoundManager\n            if (mgr && typeof mgr.findHowl === 'function') {\n                var howl = mgr.findHowl(id);\n                if (howl) {\n                    var src = howl._src;\n                    if (Object.prototype.toString.call(src) === '[object Array]') {\n                        src = src[0];\n                    }\n                    var seg = spriteSeg(howl, id);\n                    return {\n                        d: seg ? seg.d : (typeof howl.duration === 'function' ? howl.duration() : 0) || 0,\n                        o: seg ? seg.o : 0,\n                        l: seg ? seg.l : !!howl._loop,\n                        u: src ? new URL(String(src), window.location.href).href : null,\n                    };\n                }\n            }\n            // ToneSoundManager\n            if (mgr && typeof mgr.getSound === 'function') {\n                var asset = mgr.getSound(id);\n                if (asset) {\n                    var d = 0;\n                    var o = 0;\n                    var l = false;\n                    var u = null;\n                    var seg2 = asset.howl ? spriteSeg(asset.howl, id) : null;\n                    if (seg2) {\n                        d = seg2.d;\n                        o = seg2.o;\n                        l = seg2.l;\n                    } else if (asset.buffer && asset.buffer.duration) {\n                        d = asset.buffer.duration;\n                    } else if (asset.howl && typeof asset.howl.duration === 'function') {\n                        d = asset.howl.duration();\n                        l = !!asset.howl._loop;\n                    }\n                    if (asset.url) {\n                        u = new URL(String(asset.url), window.location.href).href;\n                    }\n                    return { d: d, o: o, l: l, u: u };\n                }\n            }\n        } catch (e) {\n            /* ignore */\n        }\n        return null;\n    }\n\n    /**\n     * Slot overrides, applied at PLAY TIME: when the instrumented play() asks\n     * __SL_SOUND and this map holds the sound id, the agent plays its own Howl\n     * and suppresses the original. Owning the map here (instead of\n     * SoundManager._replacedSounds via mgr.replaceSound) is what makes\n     * replacement robust: it does not need the original howl to be loaded yet,\n     * it survives SoundManager.reset() clearing _replacedSounds, and the slot's\n     * own loop flag is honoured (a replaced background keeps looping - the\n     * _replacedSounds path never looped a file replacement at all).\n     *\n     * __SL_OVR is the loader-side half of the contract: the instrumented\n     * stop/fade/stopAll methods fire __SL_SNDCTL for overridden slots even\n     * though those never enter _playing (the original play() returned early).\n     */\n    var slotOverrides = {};\n    window.__SL_OVR = slotOverrides;\n\n    /** The override's Howl, recreated if Howler.unload() destroyed it. */\n    function ovrHowl(ovr) {\n        if (!ovr.howl || (typeof ovr.howl.state === 'function' && ovr.howl.state() === 'unloaded')) {\n            var Howl = howlCtor();\n            if (!Howl) {\n                return null;\n            }\n            ovr.howl = new Howl(ovr.spec);\n            // A finished one-shot is no longer \"playing\" - the instrumented\n            // isPlaying()/getNowPlayingSounds() read ovr.active. Looping howls\n            // fire 'end' every cycle but keep going, hence the loop() check.\n            (function (h, o) {\n                h.on('end', function () {\n                    if (!h.loop()) {\n                        o.active = false;\n                    }\n                });\n            })(ovr.howl, ovr);\n        }\n        return ovr.howl;\n    }\n\n    function ovrStop(id) {\n        var ovr = slotOverrides[id];\n        if (!ovr) {\n            return;\n        }\n        if (ovr.fadeTimer) {\n            clearTimeout(ovr.fadeTimer);\n            ovr.fadeTimer = null;\n        }\n        if (ovr.howl) {\n            try {\n                ovr.howl.stop();\n            } catch (e) {\n                /* ignore */\n            }\n        }\n        ovr.active = false;\n    }\n\n    /** exceptCsv mirrors stopAll(exceptions) - ids that keep playing. */\n    function ovrStopAll(exceptCsv) {\n        var except = (exceptCsv || '').split(',');\n        Object.keys(slotOverrides).forEach(function (id) {\n            if (except.indexOf(id) === -1) {\n                ovrStop(id);\n            }\n        });\n    }\n\n    /**\n     * Plays the override for the slot, mirroring SoundManager.play()'s rules:\n     * volume from changeSoundVolume/mgr.volume, volume 0 for a muted background\n     * sound (global mute is already global - Howler is shared), and the loop\n     * flag of the ORIGINAL slot - the slot's nature decides looping, not the\n     * assigned file. Returns false when it cannot play, so the caller lets the\n     * original through instead of leaving silence.\n     */\n    function ovrPlay(id, mgr, fade, fadeMs) {\n        var ovr = slotOverrides[id];\n        var howl = ovr && ovrHowl(ovr);\n        if (!howl) {\n            return false;\n        }\n        var bgGroup = mgr && mgr.config && mgr.config.backgroundSoundsGroup;\n        var isBg = !!(bgGroup && bgGroup.indexOf(id) !== -1);\n        if (isBg) {\n            try {\n                mgr._lastPlayedBackgroundSound = id;\n            } catch (e) {\n                /* bookkeeping only */\n            }\n        }\n        var vol =\n            mgr && mgr.changeSoundVolume && mgr.changeSoundVolume.has && mgr.changeSoundVolume.has(id)\n                ? mgr.changeSoundVolume.get(id)\n                : mgr && typeof mgr.volume === 'number'\n                    ? mgr.volume\n                    : 1;\n        if (\n            isBg &&\n            mgr &&\n            ((mgr.ON_SOUND_MUTED && mgr.ON_SOUND_MUTED.value) ||\n                (mgr.ON_BACKGROUND_SOUNDS_MUTED && mgr.ON_BACKGROUND_SOUNDS_MUTED.value))\n        ) {\n            vol = 0;\n        }\n        // A background override the game re-plays must NOT restart: the\n        // manager reuses its own live instance there, and that repeated\n        // play() is exactly how the game re-applies the current volume/mute\n        // (bg mute = volume 0, unmute = the real level). So mirror it -\n        // re-apply the level in place. Restarting instead made a replaced\n        // background stutter back to 0 on every controller tick; skipping the\n        // call altogether (an earlier attempt) broke bg mute until the next\n        // spin. Non-bg slots keep restarting: the game spawns a new instance\n        // there, overlapping is expected.\n        var sounding = false;\n        try {\n            sounding = !!(ovr.active && typeof howl.playing === 'function' && howl.playing());\n        } catch (e) {\n            sounding = !!ovr.active;\n        }\n        if (isBg && sounding) {\n            try {\n                // A fade-out was queued with a stop behind it - the game\n                // asking to play again cancels that stop.\n                if (ovr.fadeTimer) {\n                    clearTimeout(ovr.fadeTimer);\n                    ovr.fadeTimer = null;\n                }\n                if (fade && fadeMs) {\n                    howl.fade(howl.volume(), vol, fadeMs);\n                } else {\n                    howl.volume(vol);\n                }\n            } catch (e) {\n                return false;\n            }\n            return true;\n        }\n        ovrStop(id);\n        var loop = false;\n        try {\n            var all = (mgr && typeof mgr.getAllSounds === 'function' && mgr.getAllSounds()) || [];\n            for (var i = 0; i < all.length; i++) {\n                var sp = all[i] && all[i].sprite;\n                if (sp && !Array.isArray(sp) && sp[id]) {\n                    loop = !!sp[id][2];\n                    break;\n                }\n            }\n        } catch (e) {\n            /* one-shot fallback */\n        }\n        try {\n            howl.loop(loop);\n            if (fade && fadeMs) {\n                howl.volume(0);\n                howl.play();\n                howl.fade(0, vol, fadeMs);\n            } else {\n                howl.volume(vol);\n                howl.play();\n            }\n            ovr.active = true;\n        } catch (e) {\n            return false;\n        }\n        return true;\n    }\n\n    /**\n     * Hook called from the instrumented SoundManager/ToneSoundManager play()\n     * methods. Records the event (with waveform info) and plays the assigned\n     * sound via the normal variant lookup. Returns true when the original\n     * playback must be suppressed: the composer either REPLACED this game\n     * sound (replace + assigned sound, played by trigger()) or MUTED it\n     * (mute - nothing plays instead), or the slot carries an override, which\n     * the agent plays itself (see slotOverrides above).\n     */\n    window.__SL_SOUND = function (baseId, soundId, mgr, fade, fadeMs) {\n        if (selfPlay) {\n            // The agent playing a packaged point slot - not a game trigger.\n            return false;\n        }\n        // The point id of the manager's play() call site, remembered because\n        // snapshotSounding() has to mint events without a play() to read it\n        // from. Constant for the session - one call site per manager.\n        lastPlayPointId = baseId;\n        // Record the EVENT only when this play() starts something new: a bg\n        // sound already in _playing keeps its live instance (the manager\n        // hands the existing soundId back to Howler, which no-ops on a\n        // playing one), so the repeated play() calls the bg controllers make\n        // painted a wall of identical loop starts on the timeline. ONLY the\n        // event is skipped - the play() body itself must ALWAYS run, and an\n        // override must ALWAYS go through ovrPlay: even a \"reused\" play\n        // re-applies the current volume/mute state, which is exactly how the\n        // game's bg mute/unmute works. Suppressing it once broke mute until\n        // the next spin.\n        var bgGroup = mgr && mgr.config && mgr.config.backgroundSoundsGroup;\n        var isBg = !!(bgGroup && bgGroup.indexOf(soundId) !== -1);\n        // `_playing` is the MANAGER's map, and an overridden slot never enters\n        // it - the agent plays those itself. So a REPLACED background looked\n        // brand new on every controller tick and painted exactly the wall of\n        // loop starts this guard exists to prevent; the timeline preview then\n        // started one player per mark and the background stacked on itself.\n        // ovrPlay already refuses to restart a sounding bg override - this is\n        // the same fact, on the event side. The identical blind spot is why\n        // the loader patches isPlaying/getNowPlayingSounds.\n        var ovrLive = slotOverrides[soundId];\n        var ovrSounding = false;\n        if (ovrLive && ovrLive.active) {\n            try {\n                ovrSounding =\n                    ovrLive.howl && typeof ovrLive.howl.playing === 'function'\n                        ? !!ovrLive.howl.playing()\n                        : true;\n            } catch (e) {\n                ovrSounding = true;\n            }\n        }\n        var reused = !!(\n            isBg &&\n            (ovrSounding ||\n                (mgr &&\n                    mgr._playing &&\n                    typeof mgr._playing.has === 'function' &&\n                    mgr._playing.has(soundId) &&\n                    mgr._playing.get(soundId)))\n        );\n        if (!reused) {\n            trigger(baseId, soundId, sndInfo(mgr, soundId));\n        }\n        var cfg = points[baseId + '#' + soundId];\n        if (cfg && cfg.enabled && (cfg.mute || (cfg.sound && cfg.replace))) {\n            return true;\n        }\n        if (slotOverrides[soundId]) {\n            return ovrPlay(soundId, mgr, fade, fadeMs);\n        }\n        return false;\n    };\n\n    /**\n     * Hook called from the instrumented SoundManager stop/fade methods.\n     * Records a lifecycle event so the tool can truncate/fade the waveform\n     * of the matching play event on the timeline.\n     */\n    window.__SL_SNDCTL = function (baseId, soundId, ctl, fadeMs, targetVol) {\n        try {\n            // stopAll: internal only - overrides never enter _playing, so the\n            // game's own loop over it misses them. No timeline event: the\n            // per-sound stop() calls it makes fire their own.\n            if (ctl === 'stopall') {\n                ovrStopAll(soundId);\n                return;\n            }\n            var ovr = slotOverrides[soundId];\n            if (ovr && ovr.active && ovr.howl) {\n                // Mirror SoundManager's own mute rules: it refuses fades and\n                // volume changes on a muted background sound (fadeOutAndStop\n                // degrades to a plain stop), and the override must not become\n                // audible under mute because a fade raised it from 0.\n                var mgr = sounds();\n                var bgGroup = mgr && mgr.config && mgr.config.backgroundSoundsGroup;\n                var isBg = !!(bgGroup && bgGroup.indexOf(soundId) !== -1);\n                var muted = !!(mgr && mgr.ON_SOUND_MUTED && mgr.ON_SOUND_MUTED.value);\n                var bgMuted =\n                    muted ||\n                    !!(mgr && mgr.ON_BACKGROUND_SOUNDS_MUTED && mgr.ON_BACKGROUND_SOUNDS_MUTED.value);\n                if (ctl === 'volume') {\n                    // Instrumented setVolume; fadeMs carries its setIfMuted\n                    // flag. Internal only - no timeline event.\n                    var refuse = (bgMuted && isBg && !fadeMs) || (muted && !isBg);\n                    if (!refuse) {\n                        ovr.howl.volume(targetVol || 0);\n                    }\n                    return;\n                }\n                if (ctl === 'stop' && (!fadeMs || (bgMuted && isBg))) {\n                    ovrStop(soundId);\n                } else if (ctl === 'stop') {\n                    ovr.howl.fade(ovr.howl.volume(), 0, fadeMs);\n                    ovr.fadeTimer = setTimeout(function () {\n                        ovrStop(soundId);\n                    }, fadeMs);\n                } else if (!(bgMuted && isBg)) {\n                    ovr.howl.fade(ovr.howl.volume(), targetVol || 0, fadeMs || 0);\n                }\n            } else if (ctl === 'volume') {\n                return;\n            }\n            trigger(baseId, soundId, { ctl: ctl, fd: fadeMs || 0, fv: targetVol || 0 });\n        } catch (e) {\n            /* never break the game */\n        }\n    };\n\n    var listened = typeof WeakMap !== 'undefined' ? new WeakMap() : null;\n\n    /** How far up the display tree to look for a name. */\n    var LABEL_DEPTH = 5;\n\n    /** \"ui/atlas/spinNormal.png\" -> \"spinNormal\". */\n    function assetLabel(raw) {\n        if (!raw) {\n            return null;\n        }\n        // Unlike __SL_VPN this keeps a trailing number: \"line1\" and \"line2\" are\n        // different buttons, not two frames of one animation.\n        var s = String(raw).split(/[\\\\/]/).pop().replace(/\\.[a-z0-9]+$/i, '');\n        return s || null;\n    }\n\n    /** The texture a sprite draws, as a name. */\n    function textureLabel(obj) {\n        try {\n            var tex = obj && obj.texture;\n            if (!tex) {\n                return null;\n            }\n            var ids = tex.textureCacheIds;\n            if (ids && ids.length) {\n                // Several ids alias one texture (bare frame name and atlas\n                // path); the shortest is the bare name.\n                var best = null;\n                for (var i = 0; i < ids.length; i++) {\n                    var n = assetLabel(ids[i]);\n                    if (n && (!best || n.length < best.length)) {\n                        best = n;\n                    }\n                }\n                if (best) {\n                    return best;\n                }\n            }\n            if (tex.baseTexture && tex.baseTexture.resource) {\n                return assetLabel(tex.baseTexture.resource.url);\n            }\n        } catch (e) {\n            /* fall through */\n        }\n        return null;\n    }\n\n    /**\n     * The runtime identity of a clicked display object - what distinguishes one\n     * button from another when many register through the same line of code (the\n     * shared Button class means one point id for every button in the game).\n     *\n     * Names in this codebase are set on the view/container (`this.backgroundSoundButton\n     * .name = 'backgroundSoundButton'`), while the object that receives the event is\n     * the inner interactive Sprite - so the nearest *named ancestor* is the identity,\n     * not the hit object itself. The drawn texture is the fallback for the buttons\n     * that were never named.\n     */\n    function displayLabel(obj) {\n        var node = obj;\n        for (var depth = 0; node && depth < LABEL_DEPTH; depth++) {\n            try {\n                // PIXI <= 7 uses .name; PIXI 8 renamed it to .label.\n                var n = node.name || node.label;\n                if (typeof n === 'string' && n !== '') {\n                    return n;\n                }\n            } catch (e) {\n                /* keep climbing */\n            }\n            node = node.parent;\n        }\n        return textureLabel(obj);\n    }\n\n    /** A DOM target: honest about being the page, never a game button. */\n    function domLabel(el) {\n        try {\n            if (el === document || el === window || el.nodeType === 9) {\n                return 'document';\n            }\n            return el.id || String(el.tagName || '').toLowerCase() || null;\n        } catch (e) {\n            return null;\n        }\n    }\n\n    /**\n     * The label a click fires with.\n     *\n     * Read from the **hit object** (`InteractionEvent.target`), not from the object\n     * the listener sits on: the dominant pattern here registers on a global target\n     * (`Core.stage.stage.on('pointertap')`, `fromEvent(document, 'pointerdown')`),\n     * where the bound object identifies nothing and every click looks identical.\n     * The hit object is the button the composer actually pressed.\n     */\n    function clickLabel(bound, ev) {\n        try {\n            // A listener on the page itself (fromEvent(document, 'pointerdown'))\n            // cannot tell one button from another - every click lands on the same\n            // canvas. Say so, instead of naming the element under the cursor and\n            // implying a specific target.\n            if (bound === document || bound === window) {\n                return 'document';\n            }\n            var hit = ev && ev.target ? ev.target : null;\n            if (hit && typeof hit.nodeType === 'number') {\n                return domLabel(hit);\n            }\n            if (hit) {\n                return displayLabel(hit);\n            }\n            // No event target (an emitter may pass nothing) - the bound object is\n            // the best identity available.\n            if (bound && typeof bound.nodeType === 'number') {\n                return domLabel(bound);\n            }\n            return displayLabel(bound);\n        } catch (e) {\n            return null;\n        }\n    }\n\n    function listen(id, obj, ev) {\n        if (!obj) {\n            return;\n        }\n        var isEmitter = typeof obj.on === 'function';\n        var isDom = !isEmitter && typeof obj.addEventListener === 'function';\n        if (!isEmitter && !isDom) {\n            return;\n        }\n        if (listened) {\n            var set = listened.get(obj);\n            if (!set) {\n                set = {};\n                listened.set(obj, set);\n            }\n            var key = ev + '|' + id;\n            if (set[key]) {\n                return;\n            }\n            set[key] = true;\n        }\n        try {\n            var handler = function (domOrPixiEvent) {\n                // The label makes the per-button variant (id#name) resolvable in\n                // trigger(); unnamed targets keep the old base-only path.\n                trigger(id, clickLabel(obj, domOrPixiEvent));\n            };\n            if (isEmitter) {\n                obj.on(ev, handler);\n            } else {\n                obj.addEventListener(ev, handler);\n            }\n        } catch (e) {\n            /* ignore */\n        }\n    }\n\n    function handleEntry(entry) {\n        if (!entry || !entry.length) {\n            return;\n        }\n        if (entry[0] === 't') {\n            trigger(entry[1], entry[2]);\n        } else if (entry[0] === 'L') {\n            listen(entry[1], entry[2], entry[3]);\n        }\n    }\n\n    /**\n     * Derives a readable name for a VideoPlayer from its first frame's\n     * texture cache id (e.g. \"wildExplosion_0012.png\" -> \"wildExplosion\").\n     * Used by the video-point instrumentation when the player has no name.\n     */\n    window.__SL_VPN = function (vp) {\n        try {\n            var frame = vp && vp._frames && vp._frames[0];\n            var raw = '';\n            if (frame && frame.textureCacheIds && frame.textureCacheIds.length) {\n                raw = frame.textureCacheIds[0];\n            }\n            if (!raw && frame && frame.baseTexture && frame.baseTexture.resource && frame.baseTexture.resource.url) {\n                raw = frame.baseTexture.resource.url;\n            }\n            if (!raw) {\n                return 'unnamed';\n            }\n            raw = String(raw).split(/[\\\\/]/).pop();\n            raw = raw.replace(/\\.[a-z0-9]+$/i, ''); // extension\n            raw = raw.replace(/[_\\-. ]*\\d+$/, ''); // frame counter suffix\n            return raw || 'unnamed';\n        } catch (e) {\n            return 'unnamed';\n        }\n    };\n\n    var backlog = window.__SLQ;\n    window.__SLQ = { push: handleEntry };\n    if (Object.prototype.toString.call(backlog) === '[object Array]') {\n        backlog.forEach(handleEntry);\n    }\n\n    // ------------------------------------------------------------------ //\n    // Recording\n    // ------------------------------------------------------------------ //\n\n    function pickMime() {\n        var candidates = ['video/webm;codecs=vp9', 'video/webm;codecs=vp8', 'video/webm'];\n        for (var i = 0; i < candidates.length; i++) {\n            if (window.MediaRecorder && MediaRecorder.isTypeSupported(candidates[i])) {\n                return candidates[i];\n            }\n        }\n        return '';\n    }\n\n    /**\n     * How far into its slice a sounding slot currently is, in seconds.\n     *\n     * An override is played by the AGENT, so the manager knows nothing about\n     * its position - read the agent's own howl. Otherwise the manager's own\n     * helper already subtracts the sprite offset for us. Kept inside one\n     * iteration for a loop: the phase is all the timeline needs, and the real\n     * elapsed time (minutes, for background music) would stretch the drawn bar\n     * over a hundred thousand off-canvas pixels.\n     */\n    function soundingPos(mgr, id, info) {\n        var ovr = slotOverrides[id];\n        if (ovr && ovr.active && ovr.howl) {\n            try {\n                var s = ovr.howl.seek();\n                return typeof s === 'number' && isFinite(s) && s > 0 ? s : 0;\n            } catch (e) {\n                return 0;\n            }\n        }\n        try {\n            if (typeof mgr.getSoundCurrentTime === 'function') {\n                var p = mgr.getSoundCurrentTime(id);\n                if (typeof p === 'number' && isFinite(p) && p > 0) {\n                    if (info.l && info.d > 0) {\n                        return p % info.d;\n                    }\n                    return info.d > 0 ? Math.min(p, info.d) : p;\n                }\n            }\n        } catch (e) {\n            /* not this manager's kind of sound */\n        }\n        return 0;\n    }\n\n    /**\n     * Enters the sounds that were ALREADY SOUNDING when the recording started.\n     *\n     * Background music normally began long before Record was pressed, so its\n     * play() was never recorded: the timeline showed nothing for the music the\n     * footage plainly has, and the tool's playback preview was silent where the\n     * game was not. Each one is entered with a NEGATIVE `t` - how long ago the\n     * sounding iteration began - and every bit of downstream arithmetic then\n     * does the right thing on its own: the waveform bar starts off-canvas to\n     * the left (so it READS as older than the recording) and `syncAt` in\n     * useTimelineAudio starts the file at `pos % dur`, i.e. exactly where the\n     * loop had got to. No timeline event is posted for the assigned point\n     * sound - these are history, not a live trigger.\n     */\n    function snapshotSounding() {\n        if (!recording || !lastPlayPointId) {\n            return;\n        }\n        var mgr = sounds();\n        if (!mgr || typeof mgr.getNowPlayingSounds !== 'function') {\n            return;\n        }\n        var ids;\n        try {\n            ids = mgr.getNowPlayingSounds() || [];\n        } catch (e) {\n            return;\n        }\n        for (var i = 0; i < ids.length; i++) {\n            var id = ids[i];\n            var info = sndInfo(mgr, id);\n            if (!info || !info.d) {\n                continue;\n            }\n            var pos = soundingPos(mgr, id, info);\n            // Already at the very start: the ordinary play() event covers it.\n            if (!(pos > 0.02)) {\n                continue;\n            }\n            var ev = { id: lastPlayPointId, t: -Math.round(pos * 1000), label: id, dur: info.d };\n            if (info.u) {\n                ev.src = info.u;\n            }\n            if (info.o) {\n                ev.off = info.o;\n            }\n            if (info.l) {\n                ev.loop = true;\n            }\n            if (recording.events.length < MAX_EVENTS) {\n                recording.events.push(ev);\n            }\n            post({\n                type: 'SL_EVENT',\n                id: ev.id,\n                t: ev.t,\n                label: ev.label,\n                dur: ev.dur,\n                src: ev.src,\n                off: ev.off,\n                loop: ev.loop,\n            });\n        }\n    }\n\n    function startRecording() {\n        if (recording) {\n            return;\n        }\n        var canvas = document.getElementById('app-canvas') || document.querySelector('canvas');\n        if (!canvas || typeof canvas.captureStream !== 'function' || !window.MediaRecorder) {\n            post({ type: 'SL_ERROR', message: 'Canvas capture / MediaRecorder not available' });\n            return;\n        }\n        try {\n            var stream = canvas.captureStream(60);\n            var mime = pickMime();\n            var rec = new MediaRecorder(stream, mime ? { mimeType: mime, videoBitsPerSecond: 8000000 } : undefined);\n            var chunks = [];\n            recording = {\n                recorder: rec,\n                events: [],\n                chunks: chunks,\n                startTs: performance.now(),\n                mime: mime || 'video/webm',\n            };\n            rec.ondataavailable = function (e) {\n                if (e.data && e.data.size > 0) {\n                    chunks.push(e.data);\n                }\n            };\n            rec.onstart = function () {\n                recording.startTs = performance.now();\n                post({ type: 'SL_RECORDING_STARTED' });\n                // AFTER the STARTED post, which resets the tool's live event\n                // counter and is what puts it in the state where SL_EVENT is\n                // counted at all.\n                snapshotSounding();\n            };\n            rec.onstop = function () {\n                var rc = recording;\n                recording = null;\n                if (!rc) {\n                    return;\n                }\n                var blob = new Blob(rc.chunks, { type: rc.mime });\n                post({\n                    type: 'SL_RECORDING',\n                    blob: blob,\n                    mime: rc.mime,\n                    duration: performance.now() - rc.startTs,\n                    events: rc.events,\n                });\n            };\n            rec.onerror = function (e) {\n                post({ type: 'SL_ERROR', message: 'MediaRecorder error: ' + (e.error ? e.error.message : 'unknown') });\n            };\n            rec.start(250);\n        } catch (e) {\n            recording = null;\n            post({ type: 'SL_ERROR', message: 'Failed to start recording: ' + e.message });\n        }\n    }\n\n    function stopRecording() {\n        if (recording && recording.recorder.state !== 'inactive') {\n            recording.recorder.stop();\n        }\n    }\n\n    // ------------------------------------------------------------------ //\n    // GAME-SOUND RPC\n    //\n    // A request/reply channel over the same postMessage link, backing the\n    // tool's Game Sounds screen. It is a port of the game client's own\n    // `custom-react` Sound tab (packages/core/features/custom-react/src/egt/\n    // custom-sound/), which could reach `Core` directly because it rendered\n    // inside the game. From the parent frame the tool cannot, so every one of\n    // those actions becomes a method here.\n    //\n    //   parent -> agent  { type: 'SL_GS_CALL', rid, method, params }\n    //   agent  -> parent { type: 'SL_GS_REPLY', rid, ok, data }        on success\n    //                    { type: 'SL_GS_REPLY', rid, ok: false, error } on failure\n    //\n    // `window.__SL_CORE` is published by the loader (see its file header).\n    // Everything below tolerates its absence: an uninstrumented build, or a\n    // Core that has not finished initialising, answers `available: false`\n    // rather than throwing into the game.\n    // ------------------------------------------------------------------ //\n\n    function core() {\n        return window.__SL_CORE || null;\n    }\n\n    /** `Core.sounds` throws until Core is initialised - hence the catch. */\n    function sounds() {\n        try {\n            var c = core();\n            return c ? c.sounds : null;\n        } catch (e) {\n            return null;\n        }\n    }\n\n    function need(value, what) {\n        if (!value) {\n            throw new Error(what + ' is not available in this build');\n        }\n        return value;\n    }\n\n    function resolveDep(name) {\n        try {\n            var c = core();\n            if (c && c.gameDependencyContainer && c.gameDependencyContainer.isRegistered(name)) {\n                return c.gameDependencyContainer.resolve(name);\n            }\n        } catch (e) {\n            /* not registered for this game type */\n        }\n        return null;\n    }\n\n    /** Slot, scratch or multiplayer configuration - whichever this game has. */\n    function gameConfig() {\n        return (\n            resolveDep('ISlotConfiguration') ||\n            resolveDep('IScratchConfiguration') ||\n            resolveDep('IMultiplayerConfiguration')\n        );\n    }\n\n    function bigWinConfig() {\n        return resolveDep('BigWinFeatureConfiguration');\n    }\n\n    /** The task folder inside `window.ssi.CustomResourcesHost`; '' when absent. */\n    function ssiPath() {\n        try {\n            var parts = String(window.ssi.CustomResourcesHost).split('/');\n            return parts[parts.length - 2] || '';\n        } catch (e) {\n            return '';\n        }\n    }\n\n    function gameKey() {\n        var c = core();\n        return (c && c.configuration && c.configuration.gameKey) || '';\n    }\n\n    function loading(flag) {\n        try {\n            var c = core();\n            if (c && c.LOADING_ASSETS) {\n                c.LOADING_ASSETS.next(flag);\n            }\n        } catch (e) {\n            /* ignore */\n        }\n    }\n\n    function spriteNames(sprite) {\n        if (Object.prototype.toString.call(sprite) === '[object Array]') {\n            return sprite.slice();\n        }\n        if (sprite && typeof sprite === 'object') {\n            return Object.keys(sprite);\n        }\n        return [];\n    }\n\n    /** Loaded sound groups and the sound ids inside each, for the picker. */\n    function soundGroups() {\n        var mgr = sounds();\n        if (!mgr || typeof mgr.getAllSounds !== 'function') {\n            return [];\n        }\n        return (mgr.getAllSounds() || []).map(function (group) {\n            var src = null;\n            try {\n                src = group.howl && group.howl._src\n                    ? (Array.isArray(group.howl._src) ? group.howl._src[0] : group.howl._src)\n                    : (typeof group.url === 'string' ? group.url : null);\n            } catch (e) {\n                /* a group mid-load - no file yet */\n            }\n            return {\n                id: group.id,\n                sounds: spriteNames(group.sprite),\n                // The sprite file and its per-sound [offsetMs, durMs, loop?]\n                // windows - what lets the tool extract a game sound's audio.\n                url: src,\n                sprite: group.sprite && !Array.isArray(group.sprite) ? group.sprite : null,\n            };\n        });\n    }\n\n    /**\n     * Howler's constructor, for building a replacement sound out of an\n     * uploaded file. Taken off an already-loaded Howl rather than adding\n     * another loader hook for the `howler` import.\n     */\n    function howlCtor() {\n        if (window.Howl) {\n            return window.Howl;\n        }\n        try {\n            var all = (sounds() && sounds().getAllSounds()) || [];\n            for (var i = 0; i < all.length; i++) {\n                if (all[i] && all[i].howl && all[i].howl.constructor) {\n                    return all[i].howl.constructor;\n                }\n            }\n        } catch (e) {\n            /* ignore */\n        }\n        return null;\n    }\n\n    /** Sound names a win line can be keyed by, as the game's own tab derives them. */\n    function lineSoundNames() {\n        var cfg = gameConfig();\n        var seen = {};\n        try {\n            (cfg && cfg.symbolsConfig ? cfg.symbolsConfig : []).forEach(function (symbol) {\n                ['winSound', 'expandSound', 'fullLineSound'].forEach(function (key) {\n                    if (symbol[key]) {\n                        seen[symbol[key]] = true;\n                    }\n                });\n            });\n            (cfg && cfg.assets ? cfg.assets : []).forEach(function (asset) {\n                if (asset.id === 'winSounds') {\n                    spriteNames(asset.sprite).forEach(function (name) {\n                        seen[name] = true;\n                    });\n                }\n            });\n        } catch (e) {\n            /* ignore */\n        }\n        return Object.keys(seen).sort(function (a, b) {\n            return a.localeCompare(b);\n        });\n    }\n\n    /** Win-line sounds that the Big Win feature actually has an entry for. */\n    function bigWinLineNames() {\n        var bw = bigWinConfig();\n        var cfg = gameConfig();\n        if (!bw || !cfg || !cfg.symbolsConfig) {\n            return [];\n        }\n        var out = [];\n        try {\n            cfg.symbolsConfig.forEach(function (symbol) {\n                var names = [symbol.winSound, symbol.expandSound]\n                    .filter(function (s) {\n                        return s && bw.winLinesSounds.bigWin[s];\n                    })\n                    .concat(symbol.fullLineSound ? [symbol.fullLineSound] : []);\n                names.forEach(function (name) {\n                    if (out.indexOf(name) === -1) {\n                        out.push(name);\n                    }\n                });\n            });\n        } catch (e) {\n            /* ignore */\n        }\n        return out.sort(function (a, b) {\n            return a.localeCompare(b);\n        });\n    }\n\n    /** `Map<string, number>` -> plain object, so it survives structured clone. */\n    function volumeOverrides() {\n        var out = {};\n        try {\n            sounds().changeSoundVolume.forEach(function (volume, id) {\n                out[id] = volume;\n            });\n        } catch (e) {\n            /* ignore */\n        }\n        return out;\n    }\n\n    function safe(fn, fallback) {\n        try {\n            var value = fn();\n            return value === undefined ? fallback : value;\n        } catch (e) {\n            return fallback;\n        }\n    }\n\n    // ---- FTP / asset-pack host ---------------------------------------- //\n\n    /** The `customurl` every asset-pack command is addressed with. */\n    function soundsUrl(suffix) {\n        return '/sounds/' + ssiPath() + '/assets/' + gameKey() + '/sounds/' + (suffix || '');\n    }\n\n    /**\n     * The '/assets/.../' directory a sound group's own file loads from - the\n     * same path `replaceAssets` checks on the asset-pack host. Commons packs\n     * (e.g. the base `mainSounds`, '/assets/commons/sounds/') do not live\n     * under the game's key, so an upload must mirror the group's real path\n     * or the game never finds it. Null when the group (or its file) is\n     * unknown - the caller falls back to the game's own sounds dir.\n     */\n    function groupAssetDir(groupId) {\n        if (!groupId) {\n            return null;\n        }\n        var groups = soundGroups();\n        for (var i = 0; i < groups.length; i++) {\n            if (groups[i].id === groupId && typeof groups[i].url === 'string') {\n                var url = groups[i].url.split('?')[0];\n                var at = url.indexOf('/assets/');\n                var end = url.lastIndexOf('/');\n                if (at !== -1 && end > at) {\n                    return url.substring(at, end + 1);\n                }\n            }\n        }\n        return null;\n    }\n\n    function loopSoundsUrl() {\n        return '/sounds/' + ssiPath() + '/assets/loopedSounds/';\n    }\n\n    function ftpPost(command, data) {\n        var c = need(core(), 'Core');\n        var host = c.configuration && c.configuration.assetPackHost;\n        if (!host) {\n            throw new Error('assetPackHost is not configured for this game');\n        }\n        return Promise.resolve(c.requestManager.post(host + command, data)).then(function (res) {\n            var status = res ? res.status : 0;\n            if (!(status >= 200 && status < 300)) {\n                throw new Error('asset-pack host answered ' + status);\n            }\n            return res ? res.data : null;\n        });\n    }\n\n    /**\n     * Every sound in `group`, or - for the special `overrideSounds` group -\n     * every sound the game has, which is what an override pack may contain.\n     */\n    function soundsInGroup(group) {\n        var groups = soundGroups();\n        if (group !== 'overrideSounds') {\n            for (var i = 0; i < groups.length; i++) {\n                if (groups[i].id === group) {\n                    return groups[i].sounds;\n                }\n            }\n            return [];\n        }\n        var all = [];\n        groups.forEach(function (g) {\n            g.sounds.forEach(function (name) {\n                if (all.indexOf(name) === -1) {\n                    all.push(name);\n                }\n            });\n        });\n        return all;\n    }\n\n    // ---- Methods ------------------------------------------------------- //\n\n    var gsMethods = {\n        /**\n         * One snapshot for the whole screen. The tool re-reads it after any\n         * mutation rather than mirroring game state it does not own.\n         */\n        state: function () {\n            var mgr = sounds();\n            var bw = bigWinConfig();\n            return {\n                available: !!mgr,\n                gameKey: gameKey(),\n                ssiPath: ssiPath(),\n                hasAssetPackHost: !!safe(function () {\n                    return core().configuration.assetPackHost;\n                }, ''),\n                debug: !!(mgr && mgr.debug),\n                groups: soundGroups(),\n                volumes: volumeOverrides(),\n                assetGroups: safe(function () {\n                    return (gameConfig().assets || [])\n                        .filter(function (a) {\n                            return a.type === 'sound';\n                        })\n                        .map(function (a) {\n                            return a.id;\n                        });\n                }, []),\n                lineSounds: lineSoundNames(),\n                bgConfig: safe(function () {\n                    return mgr.getBGSoundConfiguration().positionData.defaults;\n                }, null),\n                fadeConfig: safe(function () {\n                    return mgr.getBGSoundFadeConfiguration();\n                }, null),\n                idleConfig: safe(function () {\n                    return mgr.getBGInactivityTimeConfiguration();\n                }, null),\n                // `winLinesSounds` is a DynamicPositionObject: `.bigWin` is the\n                // resolution for the current orientation - what the feature\n                // reads and what the editor edits - while `positionData.defaults`\n                // is the base, and the only shape `updateBigWinConfiguration`\n                // writes back. So the screen edits one and exports the other.\n                bigWin: bw\n                    ? {\n                          lines: bigWinLineNames(),\n                          config: safe(function () {\n                              return bw.winLinesSounds.bigWin;\n                          }, null),\n                          defaults: safe(function () {\n                              return bw.winLinesSounds.positionData.defaults;\n                          }, null),\n                      }\n                    : null,\n            };\n        },\n\n        /** Logs every played sound with its name to the game's console. */\n        setDebug: function (p) {\n            need(sounds(), 'sound manager').debug = !!p.value;\n            return { debug: !!p.value };\n        },\n\n        /** Auditioning one sound stops everything else, as the game tab did. */\n        play: function (p) {\n            var mgr = need(sounds(), 'sound manager');\n            mgr.stopAll();\n            ovrStopAll('');\n            if (slotOverrides[p.id]) {\n                ovrPlay(p.id, mgr);\n            } else {\n                mgr.play(p.id);\n            }\n            return { playing: p.id };\n        },\n\n        /** Stop also drops any replacement, returning the sound to the original. */\n        stop: function (p) {\n            var mgr = need(sounds(), 'sound manager');\n            ovrStop(p.id);\n            delete slotOverrides[p.id];\n            mgr.stop(p.id);\n            mgr.replaceSound(p.id, undefined);\n            return { stopped: p.id };\n        },\n\n        /**\n         * Stops the slot's PLAYBACK only — the audition stop. Unlike `stop`\n         * above (whose ported quirk also drops the replacement), this leaves\n         * the override map untouched, so a mixer play/stop round trip cannot\n         * silently return a replaced sound to the game's own audio.\n         */\n        stopAudition: function (p) {\n            var mgr = need(sounds(), 'sound manager');\n            ovrStop(p.id);\n            mgr.stop(p.id);\n            return { stopped: p.id };\n        },\n\n        /** `volume` is 0..1; the override is remembered for the volume config. */\n        setVolume: function (p) {\n            var mgr = need(sounds(), 'sound manager');\n            if (p.volume === 1) {\n                // Back to the default = no override. The ENTRY is what every\n                // \"changed\" marker keys on (mixer counters, pack borders, the\n                // published gamesounds.json, the exported volume config), so a\n                // reset must remove it — storing a 1 would keep the sound\n                // flagged as changed forever.\n                mgr.changeSoundVolume.delete(p.id);\n            } else {\n                mgr.changeSoundVolume.set(p.id, p.volume);\n            }\n            // NEVER with setIfMuted: bypassing the mgr's mute guard made a\n            // muted background sound audibly change volume - mute must keep\n            // its silence. While muted the level lives in changeSoundVolume\n            // and lands on unmute / the next play, like the game's own rules.\n            mgr.setVolume(p.id, p.volume);\n            var ovr = slotOverrides[p.id];\n            if (ovr && ovr.active && ovr.howl) {\n                try {\n                    ovr.howl.volume(p.volume);\n                } catch (e) {\n                    /* ignore */\n                }\n            }\n            return { id: p.id, volume: p.volume };\n        },\n\n        /** Swaps a game sound for an uploaded file, held only in this session. */\n        replaceSound: function (p) {\n            var mgr = need(sounds(), 'sound manager');\n            need(howlCtor(), 'the Howler constructor');\n            // ovrStop only silences a PREVIOUS override - the first replacement\n            // of a slot the game is still sounding needs the game's own stop,\n            // or the old audio keeps playing under the new one until Stop.\n            ovrStop(p.id);\n            mgr.stop(p.id);\n            var ovr = { spec: { src: p.dataUrl, format: p.format } };\n            slotOverrides[p.id] = ovr;\n            loading(true);\n            try {\n                ovrHowl(ovr); // eager decode; recreated later if unloaded\n            } finally {\n                loading(false);\n            }\n            return { id: p.id };\n        },\n\n        /** Swaps a slot for another game sound's segment of its sprite file. */\n        replaceWithGameSound: function (p) {\n            var mgr = need(sounds(), 'sound manager');\n            var Howl = need(howlCtor(), 'the Howler constructor');\n            var groups = mgr.getAllSounds() || [];\n            var src = null;\n            var entry = null;\n            for (var i = 0; i < groups.length; i++) {\n                var sp = groups[i] && groups[i].sprite;\n                if (sp && !Array.isArray(sp) && sp[p.sourceId]) {\n                    entry = sp[p.sourceId];\n                    src = groups[i].howl && groups[i].howl._src\n                        ? (Array.isArray(groups[i].howl._src) ? groups[i].howl._src[0] : groups[i].howl._src)\n                        : groups[i].url;\n                    break;\n                }\n            }\n            if (!src || !entry) {\n                throw new Error(p.sourceId + ' is not a loaded game sound');\n            }\n            ovrStop(p.id);\n            mgr.stop(p.id);\n            var ovr = {\n                spec: { src: src, sprite: { __default: [entry[0], entry[1], false] } },\n            };\n            slotOverrides[p.id] = ovr;\n            loading(true);\n            try {\n                ovrHowl(ovr);\n            } finally {\n                loading(false);\n            }\n            return { id: p.id, sourceId: p.sourceId };\n        },\n\n        /**\n         * Which sound groups have a rendered pack (`<id>.mp3`) on the\n         * asset-pack host. The tool's FTP dialog uses it to flag STALE packs:\n         * one uploaded earlier whose local settings went back to default\n         * reads as \"unchanged\" and would never be re-uploaded — yet\n         * `replaceAssets` keeps playing the old render (baked volume and\n         * all). Re-uploading the pristine render is how a default-again pack\n         * is \"deleted\" from the host.\n         */\n        ftpReplacedPacks: function () {\n            var mgr = need(sounds(), 'sound manager');\n            var host = null;\n            try {\n                host = window.ssi && window.ssi.CustomResourcesHost ? String(window.ssi.CustomResourcesHost) : null;\n            } catch (e) {\n                /* ignore */\n            }\n            if (!host) {\n                return { packs: [] };\n            }\n            var groups = (typeof mgr.getAllSounds === 'function' && mgr.getAllSounds()) || [];\n            var checks = groups.map(function (g) {\n                var dir = groupAssetDir(g.id) || '/assets/' + gameKey() + '/sounds/';\n                return fetch(host + dir + g.id + '.mp3?time=' + Date.now(), { method: 'HEAD' })\n                    .then(function (r) {\n                        return r.ok ? g.id : null;\n                    })\n                    .catch(function () {\n                        return null;\n                    });\n            });\n            return Promise.all(checks).then(function (ids) {\n                return {\n                    packs: ids.filter(function (id) {\n                        return id !== null;\n                    }),\n                };\n            });\n        },\n\n        /** The `<gameKey>VolumeConfig.txt` body; the tool writes the file. */\n        volumeConfig: function () {\n            var lines = [];\n            var overrides = volumeOverrides();\n            Object.keys(overrides).forEach(function (id) {\n                lines.push(id + ': ' + overrides[id] * 100);\n            });\n            return { name: gameKey() + 'VolumeConfig.txt', text: lines.join('\\n') + (lines.length ? '\\n' : '') };\n        },\n\n        // ---- Sound augmentations --------------------------------------- //\n\n        /** `lineSound === 'default'` writes every line of that background sound. */\n        setBgLineConfig: function (p) {\n            var mgr = need(sounds(), 'sound manager');\n            if (p.lineSound === 'default') {\n                mgr.updateBgAllLinesSoundConfiguration({ bgSound: p.bgSound, soundConfig: p.soundConfig });\n            } else {\n                mgr.updateBgSingleLineSoundConfiguration({\n                    bgSound: p.bgSound,\n                    lineSound: p.lineSound,\n                    soundConfig: p.soundConfig,\n                });\n            }\n            return { bgSound: p.bgSound, lineSound: p.lineSound };\n        },\n\n        setBgFade: function (p) {\n            need(sounds(), 'sound manager').updateBgSoundFadeDuration({\n                backgroundMusicFadeOutDurationNew: p.fadeOutDuration,\n                backgroundNoPlayFadeInDurationNew: p.fadeInDuration,\n            });\n            return { ok: true };\n        },\n\n        setBgInactivity: function (p) {\n            need(sounds(), 'sound manager').updateBGInactivityTimeConfiguration(p.duration);\n            return { ok: true };\n        },\n\n        /** Applies a whole background config file, one background sound at a time. */\n        importBgConfig: function (p) {\n            var mgr = need(sounds(), 'sound manager');\n            loading(true);\n            try {\n                Object.keys(p.config || {}).forEach(function (bgSound) {\n                    mgr.updateBGSoundConfiguration({ bgSound: bgSound, bgSoundConfig: p.config[bgSound] });\n                });\n            } finally {\n                loading(false);\n            }\n            return { applied: Object.keys(p.config || {}).length };\n        },\n\n        // ---- Big Win win-lines fade ------------------------------------ //\n\n        /**\n         * Mutates the resolved feature configuration in place, exactly as the\n         * game's own tab does - the Big Win feature reads it on each win.\n         */\n        setBigWinLineConfig: function (p) {\n            var bw = need(bigWinConfig(), 'the Big Win feature configuration');\n            var lines = bw.winLinesSounds.bigWin;\n            var entry = p.lineSound === 'default' ? lines.default : lines[p.lineSound];\n            if (!entry) {\n                throw new Error('no Big Win entry for ' + p.lineSound);\n            }\n            if (!entry.fadeOut) {\n                entry.fadeOut = {};\n            }\n            entry.fadeOut.duration = p.duration;\n            entry.fadeOut.volume = p.volume;\n            entry.delay = p.delay;\n            return { lineSound: p.lineSound };\n        },\n\n        importBigWinConfig: function (p) {\n            var mgr = need(sounds(), 'sound manager');\n            loading(true);\n            try {\n                Object.keys(p.config || {}).forEach(function (bigWinSound) {\n                    mgr.updateBigWinConfiguration({\n                        bigWinSound: bigWinSound,\n                        bigWinSoundConfig: p.config[bigWinSound],\n                    });\n                });\n            } finally {\n                loading(false);\n            }\n            return { applied: Object.keys(p.config || {}).length };\n        },\n\n        // ---- Asset pack / FTP ------------------------------------------ //\n\n        ftpVersions: function () {\n            return ftpPost('getAllSoundVersions', { customurl: soundsUrl('') }).then(function (data) {\n                return { versions: (data && data.versions) || [] };\n            });\n        },\n\n        ftpLoopSounds: function () {\n            return ftpPost('getLoopedSounds', { customurl: loopSoundsUrl() }).then(function (data) {\n                return { loopSounds: (data && data.loopedSounds) || [] };\n            });\n        },\n\n        ftpSetLoopSounds: function (p) {\n            return ftpPost('updateLoopSounds', {\n                customurl: loopSoundsUrl(),\n                loopSounds: JSON.stringify(p.loopSounds),\n            }).then(function () {\n                return { loopSounds: p.loopSounds };\n            });\n        },\n\n        /**\n         * Uploads files to the asset-pack host. Sound uploads are checked\n         * against the target group first: the host generates one audio sprite\n         * per group, so a stray file would silently land in the wrong pack.\n         */\n        ftpUpload: function (p) {\n            var files = p.files || [];\n            if (files.length === 0) {\n                throw new Error('no files selected');\n            }\n            var form = new FormData();\n            var allowed = p.isSoundAsset ? soundsInGroup(p.group) : null;\n            for (var i = 0; i < files.length; i++) {\n                var file = files[i];\n                if (allowed && allowed.indexOf(String(file.name).split('.')[0]) === -1) {\n                    throw new Error(file.name + ' is not a sound of group ' + p.group);\n                }\n                form.append(p.fileType + '[]', file, file.name);\n            }\n            // Route the upload to the SAME '/assets/.../' directory the target\n            // group's own file loads from (see groupAssetDir) - sprite uploads\n            // name the group in p.group, pack uploads in p.pack.\n            var assetDir = groupAssetDir(p.isSoundAsset ? p.group : p.pack);\n            form.append(\n                'customurl',\n                assetDir\n                    ? '/sounds/' + ssiPath() + assetDir + (p.customUrlPath || '')\n                    : soundsUrl(p.customUrlPath || ''),\n            );\n            if (p.isSoundAsset) {\n                form.append('filename', p.group);\n                form.append('loopSounds', JSON.stringify(p.loopSounds || []));\n                if (p.group === 'overrideSounds') {\n                    form.append('version', p.version ? p.version + '/' : '');\n                }\n            }\n            return ftpPost(p.command, form).then(function () {\n                return {\n                    uploaded: files.map(function (f) {\n                        return f.name;\n                    }),\n                };\n            });\n        },\n\n        /** Uploads a config file the tool generated (background / Big Win JSON). */\n        ftpUploadConfig: function (p) {\n            var form = new FormData();\n            form.append('files[]', new Blob([p.text], { type: 'text/plain' }), p.name);\n            form.append('customurl', soundsUrl('configuration/'));\n            return ftpPost('uploadToFTP', form).then(function () {\n                return { name: p.name };\n            });\n        },\n\n        ftpClear: function (p) {\n            return ftpPost('resetAllAudioSprites', {\n                customurl: soundsUrl(''),\n                specificDir: p.specificDir || '',\n                excludingDir: p.excludingDir || '',\n            }).then(function () {\n                return { cleared: true };\n            });\n        },\n    };\n\n    function handleCall(msg) {\n        var reply = function (payload) {\n            payload.type = 'SL_GS_REPLY';\n            payload.rid = msg.rid;\n            post(payload);\n        };\n        var method = gsMethods[msg.method];\n        if (!method) {\n            reply({ ok: false, error: 'unknown method: ' + msg.method });\n            return;\n        }\n        try {\n            Promise.resolve(method(msg.params || {})).then(\n                function (data) {\n                    reply({ ok: true, data: data });\n                },\n                function (err) {\n                    reply({ ok: false, error: (err && err.message) || String(err) });\n                },\n            );\n        } catch (e) {\n            reply({ ok: false, error: (e && e.message) || String(e) });\n        }\n    }\n\n    // ------------------------------------------------------------------ //\n    // Audio-stream mute (the tool's Game / Sound Lab switch)\n    // ------------------------------------------------------------------ //\n\n    // True while the tool's Sound Lab stream is active. Checked by\n    // pointsMuted() so new point plays stay silent even in builds where\n    // window.Howler never got a global.\n    var streamMuted = false;\n\n    /**\n     * Mutes/unmutes everything audible from this frame without STOPPING any of\n     * it: the game's Howls, the agent's own override Howls, and the point\n     * Audio nodes already sounding (mutePointNodes).\n     *\n     * The mute is PER-HOWL (howl.mute over getAllSounds), NOT only the global\n     * Howler.mute: a build can carry TWO copies of howler (one per chunk), and\n     * `window.Howler` is whichever assigned the global last — muting it zeroes\n     * the WRONG master gain while the game keeps playing through its own copy\n     * (seen live: window.Howler._muted true, masterGain 0, game audible).\n     * Muting the howls themselves needs no global at all. Howler.mute is still\n     * asserted on top for whatever does route through it.\n     *\n     * Unmute sets every group to the game's CURRENT mute wish (ON_SOUND_MUTED),\n     * never a stale snapshot — the game itself never mutes at group level, so\n     * there is no group flag to preserve. Its bg-mute DOES set per-sound flags,\n     * which a group-level unmute wipes; those are re-asserted below from\n     * ON_BACKGROUND_SOUNDS_MUTED rather than lost. The tool re-sends SL_MUTE\n     * once a second while muted, which also catches groups loaded after the\n     * switch, and LabWorkbench's same-origin fallback applies the same values,\n     * so the two paths cannot fight.\n     */\n    function applyStreamMute(muted) {\n        streamMuted = !!muted;\n        mutePointNodes(streamMuted);\n        var mgr = sounds();\n        var gameWantsMute = !!(mgr && mgr.ON_SOUND_MUTED && mgr.ON_SOUND_MUTED.value);\n        var target = streamMuted || gameWantsMute;\n        var bgTarget =\n            target ||\n            !!(mgr && mgr.ON_BACKGROUND_SOUNDS_MUTED && mgr.ON_BACKGROUND_SOUNDS_MUTED.value);\n        try {\n            if (window.Howler && typeof window.Howler.mute === 'function') {\n                window.Howler.mute(target);\n            }\n        } catch (e) {\n            /* never break the game */\n        }\n        try {\n            if (mgr && typeof mgr.getAllSounds === 'function') {\n                (mgr.getAllSounds() || []).forEach(function (group) {\n                    var howl = group && group.howl;\n                    if (howl && typeof howl.mute === 'function') {\n                        howl.mute(target);\n                    }\n                });\n            }\n        } catch (e) {\n            /* never break the game */\n        }\n        // A group-level mute(false) clears the per-INSTANCE _muted flags the\n        // manager's bg-mute sets (onBackgroundSoundMuted), which exist so a\n        // howler 'resume' cannot bring a muted background back at volume 1.\n        // Re-assert them, or unmuting the sound stream un-mutes a background\n        // the player muted separately.\n        try {\n            var bgGroup = mgr && mgr.config && mgr.config.backgroundSoundsGroup;\n            if (bgGroup && typeof mgr.getAllSounds === 'function') {\n                (mgr.getAllSounds() || []).forEach(function (group) {\n                    var howl = group && group.howl;\n                    if (!howl || !howl._sounds) {\n                        return;\n                    }\n                    howl._sounds.forEach(function (snd) {\n                        if (snd && bgGroup.indexOf(snd._sprite) !== -1) {\n                            snd._muted = bgTarget;\n                        }\n                    });\n                });\n            }\n        } catch (e) {\n            /* never break the game */\n        }\n        Object.keys(slotOverrides).forEach(function (id) {\n            var ovr = slotOverrides[id];\n            if (ovr && ovr.howl) {\n                try {\n                    // The game's own mute must reach an override too: the\n                    // manager silences its backgrounds by walking _playing,\n                    // which an override never enters, so pressing mute during\n                    // playback left the replacement audible.\n                    var bgGrp = mgr && mgr.config && mgr.config.backgroundSoundsGroup;\n                    var isBg = !!(bgGrp && bgGrp.indexOf(id) !== -1);\n                    ovr.howl.mute(isBg ? bgTarget : target);\n                } catch (e) {\n                    /* ignore */\n                }\n            }\n        });\n    }\n\n    /**\n     * Re-asserts the mute whenever the GAME changes its own mind.\n     *\n     * The manager's mute buttons only flip their subjects and call the global\n     * `Howler.mute` — which, per the note above, is not what silences this\n     * build. Until now nothing re-applied the per-howl mute on that signal, so\n     * an in-game mute (or unmute) became audible only when the tool happened to\n     * re-assert it: switching stream or stage. That was the whole bug.\n     *\n     * Subscribed AFTER the manager's own subscribers, so the game has already\n     * done its side by the time this runs. `applyStreamMute` never writes to a\n     * subject, so there is no feedback loop.\n     */\n    function watchGameMute() {\n        var tries = 0;\n        // Core boots after this script — poll for the manager, then give up.\n        var timer = setInterval(function () {\n            tries += 1;\n            var mgr = sounds();\n            if (!mgr) {\n                if (tries > 240) {\n                    clearInterval(timer);\n                }\n                return;\n            }\n            clearInterval(timer);\n            ['ON_SOUND_MUTED', 'ON_BACKGROUND_SOUNDS_MUTED'].forEach(function (name) {\n                try {\n                    var subject = mgr[name];\n                    if (subject && typeof subject.subscribe === 'function') {\n                        subject.subscribe(function () {\n                            applyStreamMute(streamMuted);\n                        });\n                    }\n                } catch (e) {\n                    /* not an observable in this build */\n                }\n            });\n        }, 250);\n    }\n\n    // ------------------------------------------------------------------ //\n    // Parent frame protocol\n    // ------------------------------------------------------------------ //\n\n    window.addEventListener('message', function (e) {\n        var msg = e.data;\n        if (!msg || typeof msg.type !== 'string') {\n            return;\n        }\n        switch (msg.type) {\n            case 'SL_START_RECORDING':\n                startRecording();\n                break;\n            case 'SL_STOP_RECORDING':\n                stopRecording();\n                break;\n            case 'SL_RELOAD_POINTS':\n                loadPoints();\n                break;\n            case 'SL_PING':\n                post({ type: 'SL_PONG' });\n                break;\n            case 'SL_MUTE':\n                applyStreamMute(!!msg.muted);\n                break;\n            case 'SL_SOUNDS':\n                acceptSounds(msg.sounds);\n                break;\n            case 'SL_GS_CALL':\n                handleCall(msg);\n                break;\n            default:\n                break;\n        }\n    });\n\n    loadPoints();\n    watchGameMute();\n    // Injected by apply.js (DefinePlugin) at build time; an older build that\n    // predates the stamp reports null and the tool skips the staleness check.\n    var buildStamp = typeof __SL_BUILD_STAMP__ !== 'undefined' ? __SL_BUILD_STAMP__ : null;\n    post({ type: 'SL_READY', serverBase: serverBase, buildStamp: buildStamp });\n    console.log('[SOUND-LAB agent]: ready, server =', serverBase);\n})();\n"],"names":[],"sourceRoot":""}