diff --git a/public/js/game.js b/public/js/game.js index 0b68f53..a151bed 100644 --- a/public/js/game.js +++ b/public/js/game.js @@ -1,600 +1 @@ -import * as THREE from 'three'; - -/* ============================================================ - Newbury Nights — client game logic - - Screen routing (title / scan / hunt / roster) - - QR scan via BarcodeDetector (manual code fallback) - - AR hunt: camera passthrough + DeviceOrientation gyro look, - color-wheel gloom detection, blaster with overheat, - procedural wisp meshes + animated-GIF billboards. - ============================================================ */ - -const $ = (s, r = document) => r.querySelector(s); -const $$ = (s, r = document) => [...r.querySelectorAll(s)]; - -const screens = { - title: $('#screen-title'), - scan: $('#screen-scan'), - hunt: $('#screen-hunt'), - roster: $('#screen-roster'), - about: $('#screen-about'), -}; -function show(name) { - Object.values(screens).forEach((s) => s.classList.add('hidden')); - screens[name].classList.remove('hidden'); -} - -/* ---------- Title nav ---------- */ -$('#btn-scan-set').addEventListener('click', () => startScan()); -$('#btn-freehunt').addEventListener('click', () => startHunt({ freeHunt: true })); -$('#btn-roster').addEventListener('click', () => openRoster()); -$('#btn-about').addEventListener('click', () => show('about')); -$$('[data-back]').forEach((b) => b.addEventListener('click', () => returnHome())); - -function returnHome() { - stopScan(); - hunt.stop(); - show('title'); -} - -/* ============================================================ - SCAN - ============================================================ */ -let scanStream = null; -let scanLoop = null; -let detector = null; - -async function startScan() { - show('scan'); - $('#scan-error').classList.add('hidden'); - const status = $('#scan-status'); - const video = $('#scan-video'); - - if ('BarcodeDetector' in window) { - try { - const formats = await window.BarcodeDetector.getSupportedFormats(); - if (formats.includes('qr_code')) { - detector = new window.BarcodeDetector({ formats: ['qr_code'] }); - } - } catch { detector = null; } - } - - try { - scanStream = await navigator.mediaDevices.getUserMedia({ - video: { facingMode: 'environment' }, audio: false, - }); - video.srcObject = scanStream; - await video.play(); - status.textContent = detector ? 'Looking for a set code…' : 'Camera on — enter code below'; - if (detector) tickScan(video); - } catch (e) { - status.textContent = 'Camera unavailable — enter the code below'; - } -} - -async function tickScan(video) { - if (!detector || !scanStream) return; - try { - const codes = await detector.detect(video); - if (codes.length) { - const raw = codes[0].rawValue?.trim(); - if (raw) { resolveCode(raw); return; } - } - } catch { /* keep trying */ } - scanLoop = requestAnimationFrame(() => tickScan(video)); -} - -function stopScan() { - if (scanLoop) cancelAnimationFrame(scanLoop), (scanLoop = null); - if (scanStream) { scanStream.getTracks().forEach((t) => t.stop()); scanStream = null; } -} - -$('#btn-manual-go').addEventListener('click', () => { - const code = $('#manual-code').value.trim(); - if (code) resolveCode(code); -}); -$('#manual-code').addEventListener('keydown', (e) => { - if (e.key === 'Enter') $('#btn-manual-go').click(); -}); - -async function resolveCode(code) { - // Accept either a bare code or a URL whose last path/query segment is the code. - const cleaned = code.includes('/') ? code.split(/[\/?=]/).filter(Boolean).pop() : code; - try { - const res = await fetch(`/api/scan/${encodeURIComponent(cleaned)}`); - if (!res.ok) throw new Error('Unknown set code'); - const data = await res.json(); - stopScan(); - startHunt({ setData: data }); - } catch (e) { - const err = $('#scan-error'); - err.textContent = `Couldn't find a set for "${cleaned}". Check the code and try again.`; - err.classList.remove('hidden'); - } -} - -/* ============================================================ - HUNT (AR) - ============================================================ */ -const TYPE_COLORS = { red: 0xff3b5c, yellow: 0xffc23b, blue: 0x3bb6ff }; - -const hunt = { - running: false, - scene: null, camera: null, renderer: null, raf: null, - stream: null, video: null, - ghosts: [], // active ghost objects in the scene - target: null, // currently locked ghost - armedType: null, // color-wheel armed type - battery: 100, - gloom: 0, - overheat: 0, - blasting: false, - orientation: { alpha: 0, beta: 0, gamma: 0, active: false }, - pool: [], // ghosts available to spawn (from set roster or freehunt) - boss: null, - remaining: 0, // spots left to clear (set mode) - freeHunt: false, - - async start({ setData, freeHunt }) { - show('hunt'); - this.running = true; - this.freeHunt = !!freeHunt; - this.battery = 100; this.gloom = 0; this.overheat = 0; this.target = null; this.armedType = null; - this.ghosts = []; - $('#result').classList.add('hidden'); - $('#lockon').classList.add('hidden'); - this.updateBattery(); this.updateGloom(); - - if (freeHunt) { - const res = await fetch('/api/freehunt?n=6'); - this.pool = (await res.json()).spawns; - this.boss = null; - this.remaining = Infinity; - } else { - this.pool = setData.roster.filter((g) => !g.isBoss); - this.boss = setData.boss; - this.remaining = Math.min(5, this.pool.length); // spots to clear before boss - } - - await this.initCamera(); - this.initScene(); - this.bindControls(); - this.maybePromptMotion(); - this.spawnNext(); - this.loop(); - }, - - // On iOS, surface an explicit "Enable Motion" button. Tapping it is the clean - // user gesture iOS needs to show its Motion & Orientation Access prompt. - maybePromptMotion() { - const overlay = $('#motion-gate'); - if (!this.gyroNeedsPermission()) { overlay.classList.add('hidden'); return; } - overlay.classList.remove('hidden'); - const btn = $('#motion-enable'); - const onTap = async () => { - const ok = await this.requestGyro(); - overlay.classList.add('hidden'); - if (!ok) this.toast('Motion blocked — enable it in Settings › Safari', 2200); - }; - btn.onclick = onTap; - this._motionEls = { overlay, btn }; - }, - - async initCamera() { - this.video = $('#ar-video'); - try { - this.stream = await navigator.mediaDevices.getUserMedia({ - video: { facingMode: 'environment' }, audio: false, - }); - this.video.srcObject = this.stream; - await this.video.play(); - } catch { /* no camera → dark backdrop, gyro still works */ } - }, - - initScene() { - const canvas = $('#ar-canvas'); - this.renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true }); - this.renderer.setPixelRatio(Math.min(devicePixelRatio, 2)); - this.renderer.setSize(innerWidth, innerHeight); - - this.scene = new THREE.Scene(); - this.camera = new THREE.PerspectiveCamera(70, innerWidth / innerHeight, 0.1, 100); - this.camera.position.set(0, 0, 0); - - this.scene.add(new THREE.AmbientLight(0xffffff, 0.8)); - const p = new THREE.PointLight(0x88aaff, 1.2, 50); - p.position.set(0, 2, 2); - this.scene.add(p); - - this._onResize = () => { - this.camera.aspect = innerWidth / innerHeight; - this.camera.updateProjectionMatrix(); - this.renderer.setSize(innerWidth, innerHeight); - }; - addEventListener('resize', this._onResize); - - // Gyro look. iOS Safari requires DeviceOrientationEvent.requestPermission() - // from an explicit user gesture (handled by the "Enable Motion" button in - // start()). Other browsers can attach the listener immediately. - this._onOrient = (e) => { - if (e.alpha == null) return; - this.orientation = { alpha: e.alpha, beta: e.beta, gamma: e.gamma, active: true }; - }; - if (!this.gyroNeedsPermission()) { - addEventListener('deviceorientation', this._onOrient); - this._gyroAttached = true; - } - }, - - gyroNeedsPermission() { - const DOE = window.DeviceOrientationEvent; - return !!(DOE && typeof DOE.requestPermission === 'function'); - }, - - // Called from a clean tap (the Enable Motion overlay). On iOS this triggers - // the system "Motion & Orientation Access" prompt; once granted we attach the - // listener. Safe to call more than once. - async requestGyro() { - if (this._gyroAttached) return true; - const DOE = window.DeviceOrientationEvent; - try { - if (DOE && typeof DOE.requestPermission === 'function') { - const res = await DOE.requestPermission(); - if (res !== 'granted') return false; - } - addEventListener('deviceorientation', this._onOrient); - this._gyroAttached = true; - return true; - } catch { - return false; - } - }, - - bindControls() { - // color wheel - $$('.wheel-seg').forEach((seg) => { - seg.onclick = () => { - const type = seg.dataset.type; - this.armedType = type; - $$('.wheel-seg').forEach((s) => s.classList.remove('armed')); - seg.classList.add('armed'); - $('#wheel-core').textContent = type.toUpperCase(); - this.scanGloom(type); - }; - }); - - const blast = $('#btn-blast'); - const startBlast = (e) => { - e.preventDefault(); - this.blasting = true; - }; - const endBlast = () => { this.blasting = false; }; - blast.addEventListener('touchstart', startBlast, { passive: false }); - blast.addEventListener('mousedown', startBlast); - addEventListener('touchend', endBlast); - addEventListener('mouseup', endBlast); - this._blastEls = { blast, startBlast, endBlast }; - }, - - /* spawn a wisp/billboard ghost at a random spot around the player */ - spawnNext() { - if (!this.pool.length) return; - const data = this.pool[Math.floor(Math.random() * this.pool.length)]; - this.addGhost(data); - }, - - addGhost(data, isBoss = false) { - const group = new THREE.Group(); - const color = TYPE_COLORS[data.type] ?? 0xffffff; - - let mesh; - let texture = null; - if (data.image) { - // animated GIF billboard — texture.needsUpdate pumped each frame - const img = document.createElement('img'); - img.crossOrigin = 'anonymous'; - img.src = data.image; - texture = new THREE.Texture(img); - img.onload = () => { texture.needsUpdate = true; }; - const mat = new THREE.MeshBasicMaterial({ map: texture, transparent: true, side: THREE.DoubleSide }); - mesh = new THREE.Mesh(new THREE.PlaneGeometry(1.2, 1.2), mat); - group.userData.gifImg = img; - group.userData.gifTex = texture; - } else { - // procedural wisp: glowing sphere + halo - const geo = new THREE.SphereGeometry(0.45, 24, 24); - const mat = new THREE.MeshStandardMaterial({ - color, emissive: color, emissiveIntensity: 1.4, roughness: 0.4, - transparent: true, opacity: 0.92, - }); - mesh = new THREE.Mesh(geo, mat); - const halo = new THREE.Mesh( - new THREE.SphereGeometry(0.62, 24, 24), - new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 0.16, side: THREE.BackSide }) - ); - group.add(halo); - } - group.add(mesh); - - // Place in a forward-facing arc so ghosts are findable without perfect - // aiming: yaw within ±60° of straight ahead, modest pitch, closer in. - const yaw = (Math.random() - 0.5) * (Math.PI * 2 / 3); - const pitch = (Math.random() - 0.5) * 0.5; - const dist = 3 + Math.random() * 1.5; - group.position.set( - Math.sin(yaw) * dist, - Math.sin(pitch) * dist, - -Math.cos(yaw) * dist - ); - - group.userData = { - ...group.userData, - data, isBoss, - hp: data.health, maxHp: data.health, - bobPhase: Math.random() * Math.PI * 2, - revealed: true, // visible on spawn; the wheel now aggros/tints rather than uncloaks - }; - this.scene.add(group); - this.ghosts.push(group); - }, - - scanGloom(type) { - // Ghosts are visible on spawn now, so the wheel acts as a "lure": scanning a - // colour pulls the nearest matching ghost toward your aim and charges gloom. - // Mismatched colour risks a jumpscare. - const matches = this.ghosts.filter((g) => g.userData.data.type === type); - if (matches.length) { - // pull the nearest match into a forward, easy-to-aim position - const g = matches[0]; - const dist = 3; - g.position.set((Math.random() - 0.5) * 1.2, (Math.random() - 0.3) * 0.8, -dist); - g.userData.lured = true; - this.gloom += 5; this.updateGloom(); - this.toast(`${type.toUpperCase()} ghost lured in`, 800); - } else { - // no ghost of that colour present — small chance of a jumpscare - if (Math.random() < 0.12) this.jumpscare(); - else { this.gloom += 2; this.updateGloom(); this.toast('+2 gloom', 600); } - } - }, - - loop() { - if (!this.running) return; - this.raf = requestAnimationFrame(() => this.loop()); - const t = performance.now() / 1000; - - // camera yaw/pitch from gyro - if (this.orientation.active) { - const { alpha, beta, gamma } = this.orientation; - // simple mapping; good enough for look-around aiming - this.camera.rotation.set( - THREE.MathUtils.degToRad((beta ?? 0) - 90), - THREE.MathUtils.degToRad(alpha ?? 0), - THREE.MathUtils.degToRad(-(gamma ?? 0)), - 'YXZ' - ); - } - - // animate ghosts: bob + face camera + pump GIF textures - for (const g of this.ghosts) { - g.position.y += Math.sin(t * 1.5 + g.userData.bobPhase) * 0.0025; - g.lookAt(this.camera.position); - if (g.userData.gifTex && g.userData.gifImg?.complete) g.userData.gifTex.needsUpdate = true; - } - - // targeting: ghost nearest screen-center (and revealed) becomes the lock-on - this.updateTarget(); - - // blaster - if (this.blasting && this.overheat < 100 && this.target) { - const dmg = 0.9; // per-frame chip damage - this.target.userData.hp -= dmg; - this.overheat = Math.min(100, this.overheat + 0.8); - this.updateGhostHp(); - if (this.target.userData.hp <= 0) this.captureTarget(); - } else if (!this.blasting && this.overheat > 0) { - this.overheat = Math.max(0, this.overheat - 1.2); // cool down - } - if (this.overheat >= 100) this.blasting = false; - this.updateOverheat(); - - // ghosts attack: drain battery if a revealed ghost is in front of you - this.maybeTakeDamage(); - - this.renderer.render(this.scene, this.camera); - }, - - updateTarget() { - const center = new THREE.Vector2(0, 0); - let best = null, bestDist = 0.5; // within ~half-screen of center - const v = new THREE.Vector3(); - for (const g of this.ghosts) { - if (!g.userData.revealed) continue; - v.copy(g.position).project(this.camera); - if (v.z > 1) continue; // behind camera - const d = Math.hypot(v.x - center.x, v.y - center.y); - if (d < bestDist) { bestDist = d; best = g; } - } - if (best !== this.target) { - this.target = best; - this.renderLockon(); - } - }, - - renderLockon() { - const lock = $('#lockon'); - if (!this.target) { lock.classList.add('hidden'); return; } - const d = this.target.userData.data; - lock.classList.remove('hidden'); - // IMPORTANT: label reads displayName, not a hardcoded tint name - $('#lockon-name').textContent = d.displayName || d.name; - $('#lockon-name').className = `lockon-name display type-${d.type}`; - $('#lockon-type').textContent = d.type; - $('#lockon-rarity').textContent = '★'.repeat(d.rarity); - $('#lockon-ability').textContent = d.ability || '—'; - this.updateGhostHp(); - }, - - updateGhostHp() { - if (!this.target) return; - const pct = Math.max(0, (this.target.userData.hp / this.target.userData.maxHp) * 100); - $('#ghost-hp-fill').style.width = `${pct}%`; - }, - - captureTarget() { - const g = this.target; - const d = g.userData.data; - this.scene.remove(g); - this.ghosts = this.ghosts.filter((x) => x !== g); - this.target = null; - $('#lockon').classList.add('hidden'); - this.gloom += 10 + d.rarity * 5; - this.updateGloom(); - this.toast(`Captured ${d.displayName || d.name}!`, 900); - - if (g.userData.isBoss) { this.win(d); return; } - - if (!this.freeHunt) { - this.remaining -= 1; - if (this.remaining <= 0 && this.boss) { - // soul artifact → boss appears - this.toast('A Soul Artifact pulses…', 1100); - setTimeout(() => this.addGhost(this.boss, true), 1100); - return; - } - } - // respawn another ghost to keep the hunt going - setTimeout(() => this.spawnNext(), 600); - }, - - maybeTakeDamage() { - // a revealed ghost roughly centered + we're not blasting → it glooms us - if (this.blasting) return; - if (!this.target) return; - if (Math.random() < 0.004) { - const dmg = this.target.userData.data.rarity * 1.5; // 4★ hurts most - this.battery = Math.max(0, this.battery - dmg); - this.updateBattery(); - this.flashDamage(); - if (this.battery <= 0) this.lose(); - } - }, - - /* ---- HUD updates ---- */ - updateBattery() { - const fill = $('#battery-fill'); - fill.style.width = `${this.battery}%`; - fill.classList.toggle('low', this.battery <= 30); - $('#battery-pct').textContent = `${Math.round(this.battery)}%`; - }, - updateGloom() { $('#gloom-count').textContent = this.gloom; }, - updateOverheat() { - $('#overheat-fill').style.width = `${this.overheat}%`; - $('#btn-blast').classList.toggle('overheated', this.overheat >= 100); - $('#btn-blast').textContent = this.overheat >= 100 ? 'OVERHEATED' : 'HOLD TO BLAST'; - }, - - toast(msg, ms = 800, jump = false) { - const t = $('#toast'); - t.textContent = msg; - t.className = `toast${jump ? ' jumpscare' : ''}`; - clearTimeout(this._toastT); - this._toastT = setTimeout(() => t.classList.add('hidden'), ms); - }, - jumpscare() { - this.toast('BOO!', 700, true); - this.battery = Math.max(0, this.battery - 4); - this.updateBattery(); - if (navigator.vibrate) navigator.vibrate(120); - }, - flashDamage() { - if (navigator.vibrate) navigator.vibrate(40); - document.body.animate( - [{ boxShadow: 'inset 0 0 0 9999px rgba(255,59,92,.25)' }, { boxShadow: 'inset 0 0 0 9999px rgba(255,59,92,0)' }], - { duration: 250 } - ); - }, - - win(boss) { - $('#result-title').textContent = 'Set Cleared'; - $('#result-body').textContent = `You captured ${boss.displayName || boss.name} and drove the gloom out. Gloom banked: ${this.gloom}.`; - $('#result').classList.remove('hidden'); - this.running = false; - }, - lose() { - $('#result-title').textContent = 'Battery Dead'; - $('#result-body').textContent = 'The detector went dark and the ghosts slipped away. Recharge and try again.'; - $('#result').classList.remove('hidden'); - this.running = false; - }, - - stop() { - this.running = false; - if (this.raf) cancelAnimationFrame(this.raf); - if (this.stream) { this.stream.getTracks().forEach((t) => t.stop()); this.stream = null; } - if (this._onResize) removeEventListener('resize', this._onResize); - if (this._onOrient && this._gyroAttached) { - removeEventListener('deviceorientation', this._onOrient); - this._gyroAttached = false; - } - if (this._motionEls) { this._motionEls.overlay.classList.add('hidden'); } - if (this._blastEls) { - removeEventListener('touchend', this._blastEls.endBlast); - removeEventListener('mouseup', this._blastEls.endBlast); - } - if (this.renderer) { this.renderer.dispose?.(); } - this.ghosts = []; this.target = null; - }, -}; - -function startHunt(opts) { hunt.start(opts); } - -/* ============================================================ - ROSTER / GHOST INDEX - ============================================================ */ -async function openRoster() { - show('roster'); - await loadRoster(); -} -['#filter-type', '#filter-rarity', '#filter-boss'].forEach((sel) => - $(sel).addEventListener('change', loadRoster) -); - -async function loadRoster() { - const type = $('#filter-type').value; - const rarity = $('#filter-rarity').value; - const boss = $('#filter-boss').checked ? '1' : ''; - const params = new URLSearchParams(); - if (type) params.set('type', type); - if (rarity) params.set('rarity', rarity); - if (boss) params.set('boss', boss); - const res = await fetch(`/api/ghosts?${params}`); - const { ghosts } = await res.json(); - const grid = $('#roster-grid'); - grid.innerHTML = ghosts.map(ghostCard).join('') || - '

No ghosts match those filters.

'; -} - -function ghostCard(g) { - const colorVar = `var(--${g.type})`; - const thumb = g.image - ? `${escapeHtml(g.displayName)}` - : ''; - const setRef = g.setNumber ? `
Set ${g.setNumber} · ${escapeHtml(g.setName || '')}
` : ''; - return ` -
-
- ${thumb} -
${escapeHtml(g.displayName)} ${g.isBoss ? 'BOSS' : ''}
-
${'★'.repeat(g.rarity)}
-
HP ${g.health}DMG ${g.damage}
-
SPD ${g.speed}RNG ${g.range}
- ${g.ability ? `
⚡ ${escapeHtml(g.ability)}
` : ''} - ${setRef} -
`; -} - -function escapeHtml(s) { - return String(s ?? '').replace(/[&<>"']/g, (c) => - ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); -} +aW1wb3J0ICogYXMgVEhSRUUgZnJvbSAndGhyZWUnOwoKLyogPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09CiAgIE5ld2J1cnkgTmlnaHRzIOKAlCBjbGllbnQgZ2FtZSBsb2dpYwogICAtIFNjcmVlbiByb3V0aW5nICh0aXRsZSAvIHNjYW4gLyBodW50IC8gcm9zdGVyKQogICAtIFFSIHNjYW4gdmlhIEJhcmNvZGVEZXRlY3RvciAobWFudWFsIGNvZGUgZmFsbGJhY2spCiAgIC0gQVIgaHVudDogY2FtZXJhIHBhc3N0aHJvdWdoICsgRGV2aWNlT3JpZW50YXRpb24gZ3lybyBsb29rLAogICAgIGNvbG9yLXdoZWVsIGdsb29tIGRldGVjdGlvbiwgYmxhc3RlciB3aXRoIG92ZXJoZWF0LAogICAgIHByb2NlZHVyYWwgd2lzcCBtZXNoZXMgKyBhbmltYXRlZC1HSUYgYmlsbGJvYXJkcy4KICAgPT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09ICovCgpjb25zdCAkID0gKHMsIHIgPSBkb2N1bWVudCkgPT4gci5xdWVyeVNlbGVjdG9yKHMpOwpjb25zdCAkJCA9IChzLCByID0gZG9jdW1lbnQpID0+IFsuLi5yLnF1ZXJ5U2VsZWN0b3JBbGwocyldOwoKY29uc3Qgc2NyZWVucyA9IHsKICB0aXRsZTogJCgnI3NjcmVlbi10aXRsZScpLAogIHNjYW46ICQoJyNzY3JlZW4tc2NhbicpLAogIGh1bnQ6ICQoJyNzY3JlZW4taHVudCcpLAogIHJvc3RlcjogJCgnI3NjcmVlbi1yb3N0ZXInKSwKICBhYm91dDogJCgnI3NjcmVlbi1hYm91dCcpLAp9OwpmdW5jdGlvbiBzaG93KG5hbWUpIHsKICBPYmplY3QudmFsdWVzKHNjcmVlbnMpLmZvckVhY2goKHMpID0+IHMuY2xhc3NMaXN0LmFkZCgnaGlkZGVuJykpOwogIHNjcmVlbnNbbmFtZV0uY2xhc3NMaXN0LnJlbW92ZSgnaGlkZGVuJyk7Cn0KCi8qIC0tLS0tLS0tLS0gVGl0bGUgbmF2IC0tLS0tLS0tLS0gKi8KJCgnI2J0bi1zY2FuLXNldCcpLmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgKCkgPT4gc3RhcnRTY2FuKCkpOwokKCcjYnRuLWZyZWVodW50JykuYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoKSA9PiBzdGFydEh1bnQoeyBmcmVlSHVudDogdHJ1ZSB9KSk7CiQoJyNidG4tcm9zdGVyJykuYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoKSA9PiBvcGVuUm9zdGVyKCkpOwokKCcjYnRuLWFib3V0JykuYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoKSA9PiBzaG93KCdhYm91dCcpKTsKJCQoJ1tkYXRhLWJhY2tdJykuZm9yRWFjaCgoYikgPT4gYi5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsICgpID0+IHJldHVybkhvbWUoKSkpOwoKZnVuY3Rpb24gcmV0dXJuSG9tZSgpIHsKICBzdG9wU2NhbigpOwogIGh1bnQuc3RvcCgpOwogIHNob3coJ3RpdGxlJyk7Cn0KCi8qID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQogICBTQ0FOCiAgID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PSAqLwpsZXQgc2NhblN0cmVhbSA9IG51bGw7CmxldCBzY2FuTG9vcCA9IG51bGw7CmxldCBkZXRlY3RvciA9IG51bGw7Cgphc3luYyBmdW5jdGlvbiBzdGFydFNjYW4oKSB7CiAgc2hvdygnc2NhbicpOwogICQoJyNzY2FuLWVycm9yJykuY2xhc3NMaXN0LmFkZCgnaGlkZGVuJyk7CiAgY29uc3Qgc3RhdHVzID0gJCgnI3NjYW4tc3RhdHVzJyk7CiAgY29uc3QgdmlkZW8gPSAkKCcjc2Nhbi12aWRlbycpOwoKICBpZiAoJ0JhcmNvZGVEZXRlY3RvcicgaW4gd2luZG93KSB7CiAgICB0cnkgewogICAgICBjb25zdCBmb3JtYXRzID0gYXdhaXQgd2luZG93LkJhcmNvZGVEZXRlY3Rvci5nZXRTdXBwb3J0ZWRGb3JtYXRzKCk7CiAgICAgIGlmIChmb3JtYXRzLmluY2x1ZGVzKCdxcl9jb2RlJykpIHsKICAgICAgICBkZXRlY3RvciA9IG5ldyB3aW5kb3cuQmFyY29kZURldGVjdG9yKHsgZm9ybWF0czogWydxcl9jb2RlJ10gfSk7CiAgICAgIH0KICAgIH0gY2F0Y2ggeyBkZXRlY3RvciA9IG51bGw7IH0KICB9CgogIHRyeSB7CiAgICBzY2FuU3RyZWFtID0gYXdhaXQgbmF2aWdhdG9yLm1lZGlhRGV2aWNlcy5nZXRVc2VyTWVkaWEoewogICAgICB2aWRlbzogeyBmYWNpbmdNb2RlOiAnZW52aXJvbm1lbnQnIH0sIGF1ZGlvOiBmYWxzZSwKICAgIH0pOwogICAgdmlkZW8uc3JjT2JqZWN0ID0gc2NhblN0cmVhbTsKICAgIGF3YWl0IHZpZGVvLnBsYXkoKTsKICAgIHN0YXR1cy50ZXh0Q29udGVudCA9IGRldGVjdG9yID8gJ0xvb2tpbmcgZm9yIGEgc2V0IGNvZGXigKYnIDogJ0NhbWVyYSBvbiDigJQgZW50ZXIgY29kZSBiZWxvdyc7CiAgICBpZiAoZGV0ZWN0b3IpIHRpY2tTY2FuKHZpZGVvKTsKICB9IGNhdGNoIChlKSB7CiAgICBzdGF0dXMudGV4dENvbnRlbnQgPSAnQ2FtZXJhIHVuYXZhaWxhYmxlIOKAlCBlbnRlciB0aGUgY29kZSBiZWxvdyc7CiAgfQp9Cgphc3luYyBmdW5jdGlvbiB0aWNrU2Nhbih2aWRlbykgewogIGlmICghZGV0ZWN0b3IgfHwgIXNjYW5TdHJlYW0pIHJldHVybjsKICB0cnkgewogICAgY29uc3QgY29kZXMgPSBhd2FpdCBkZXRlY3Rvci5kZXRlY3QodmlkZW8pOwogICAgaWYgKGNvZGVzLmxlbmd0aCkgewogICAgICBjb25zdCByYXcgPSBjb2Rlc1swXS5yYXdWYWx1ZT8udHJpbSgpOwogICAgICBpZiAocmF3KSB7IHJlc29sdmVDb2RlKHJhdyk7IHJldHVybjsgfQogICAgfQogIH0gY2F0Y2ggeyAvKiBrZWVwIHRyeWluZyAqLyB9CiAgc2Nhbkxvb3AgPSByZXF1ZXN0QW5pbWF0aW9uRnJhbWUoKCkgPT4gdGlja1NjYW4odmlkZW8pKTsKfQoKZnVuY3Rpb24gc3RvcFNjYW4oKSB7CiAgaWYgKHNjYW5Mb29wKSBjYW5jZWxBbmltYXRpb25GcmFtZShzY2FuTG9vcCksIChzY2FuTG9vcCA9IG51bGwpOwogIGlmIChzY2FuU3RyZWFtKSB7IHNjYW5TdHJlYW0uZ2V0VHJhY2tzKCkuZm9yRWFjaCgodCkgPT4gdC5zdG9wKCkpOyBzY2FuU3RyZWFtID0gbnVsbDsgfQp9CgokKCcjYnRuLW1hbnVhbC1nbycpLmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgKCkgPT4gewogIGNvbnN0IGNvZGUgPSAkKCcjbWFudWFsLWNvZGUnKS52YWx1ZS50cmltKCk7CiAgaWYgKGNvZGUpIHJlc29sdmVDb2RlKGNvZGUpOwp9KTsKJCgnI21hbnVhbC1jb2RlJykuYWRkRXZlbnRMaXN0ZW5lcigna2V5ZG93bicsIChlKSA9PiB7CiAgaWYgKGUua2V5ID09PSAnRW50ZXInKSAkKCcjYnRuLW1hbnVhbC1nbycpLmNsaWNrKCk7Cn0pOwoKYXN5bmMgZnVuY3Rpb24gcmVzb2x2ZUNvZGUoY29kZSkgewogIC8vIEFjY2VwdCBlaXRoZXIgYSBiYXJlIGNvZGUgb3IgYSBVUkwgd2hvc2UgbGFzdCBwYXRoL3F1ZXJ5IHNlZ21lbnQgaXMgdGhlIGNvZGUuCiAgY29uc3QgY2xlYW5lZCA9IGNvZGUuaW5jbHVkZXMoJy8nKSA/IGNvZGUuc3BsaXQoL1tcLz89XS8pLmZpbHRlcihCb29sZWFuKS5wb3AoKSA6IGNvZGU7CiAgdHJ5IHsKICAgIGNvbnN0IHJlcyA9IGF3YWl0IGZldGNoKGAvYXBpL3NjYW4vJHtlbmNvZGVVUklDb21wb25lbnQoY2xlYW5lZCl9YCk7CiAgICBpZiAoIXJlcy5vaykgdGhyb3cgbmV3IEVycm9yKCdVbmtub3duIHNldCBjb2RlJyk7CiAgICBjb25zdCBkYXRhID0gYXdhaXQgcmVzLmpzb24oKTsKICAgIHN0b3BTY2FuKCk7CiAgICBzdGFydEh1bnQoeyBzZXREYXRhOiBkYXRhIH0pOwogIH0gY2F0Y2ggKGUpIHsKICAgIGNvbnN0IGVyciA9ICQoJyNzY2FuLWVycm9yJyk7CiAgICBlcnIudGV4dENvbnRlbnQgPSBgQ291bGRuJ3QgZmluZCBhIHNldCBmb3IgIiR7Y2xlYW5lZH0iLiBDaGVjayB0aGUgY29kZSBhbmQgdHJ5IGFnYWluLmA7CiAgICBlcnIuY2xhc3NMaXN0LnJlbW92ZSgnaGlkZGVuJyk7CiAgfQp9CgovKiA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0KICAgSFVOVCAoQVIpCiAgID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PSAqLwpjb25zdCBUWVBFX0NPTE9SUyA9IHsgcmVkOiAweGZmM2I1YywgeWVsbG93OiAweGZmYzIzYiwgYmx1ZTogMHgzYmI2ZmYgfTsKCi8vIERldGVjdCBWUDktd2l0aC1hbHBoYSBXZWJNIHN1cHBvcnQgb25jZS4gaU9TIFNhZmFyaSBoaXN0b3JpY2FsbHkgbGFja3MKLy8gcmVsaWFibGUgVlA5LWFscGhhLCBzbyB0aG9zZSBkZXZpY2VzIGZhbGwgYmFjayB0byB0aGUgR0lGL1dlYlAgPGltZz4gcGF0aC4KY29uc3QgU1VQUE9SVFNfV0VCTV9BTFBIQSA9ICgoKSA9PiB7CiAgdHJ5IHsKICAgIGNvbnN0IHYgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCd2aWRlbycpOwogICAgcmV0dXJuICEhdi5jYW5QbGF5VHlwZSAmJiB2LmNhblBsYXlUeXBlKCd2aWRlby93ZWJtOyBjb2RlY3M9InZwOSInKSAhPT0gJyc7CiAgfSBjYXRjaCB7CiAgICByZXR1cm4gZmFsc2U7CiAgfQp9KSgpOwoKY29uc3QgaHVudCA9IHsKICBydW5uaW5nOiBmYWxzZSwKICBzY2VuZTogbnVsbCwgY2FtZXJhOiBudWxsLCByZW5kZXJlcjogbnVsbCwgcmFmOiBudWxsLAogIHN0cmVhbTogbnVsbCwgdmlkZW86IG51bGwsCiAgZ2hvc3RzOiBbXSwgICAgICAgICAgLy8gYWN0aXZlIGdob3N0IG9iamVjdHMgaW4gdGhlIHNjZW5lCiAgdGFyZ2V0OiBudWxsLCAgICAgICAgLy8gY3VycmVudGx5IGxvY2tlZCBnaG9zdAogIGFybWVkVHlwZTogbnVsbCwgICAgIC8vIGNvbG9yLXdoZWVsIGFybWVkIHR5cGUKICBiYXR0ZXJ5OiAxMDAsCiAgZ2xvb206IDAsCiAgb3ZlcmhlYXQ6IDAsCiAgYmxhc3Rpbmc6IGZhbHNlLAogIG9yaWVudGF0aW9uOiB7IGFscGhhOiAwLCBiZXRhOiAwLCBnYW1tYTogMCwgYWN0aXZlOiBmYWxzZSB9LAogIHBvb2w6IFtdLCAgICAgICAgICAgIC8vIGdob3N0cyBhdmFpbGFibGUgdG8gc3Bhd24gKGZyb20gc2V0IHJvc3RlciBvciBmcmVlaHVudCkKICBib3NzOiBudWxsLAogIHJlbWFpbmluZzogMCwgICAgICAgIC8vIHNwb3RzIGxlZnQgdG8gY2xlYXIgKHNldCBtb2RlKQogIGZyZWVIdW50OiBmYWxzZSwKCiAgYXN5bmMgc3RhcnQoeyBzZXREYXRhLCBmcmVlSHVudCB9KSB7CiAgICBzaG93KCdodW50Jyk7CiAgICB0aGlzLnJ1bm5pbmcgPSB0cnVlOwogICAgdGhpcy5mcmVlSHVudCA9ICEhZnJlZUh1bnQ7CiAgICB0aGlzLmJhdHRlcnkgPSAxMDA7IHRoaXMuZ2xvb20gPSAwOyB0aGlzLm92ZXJoZWF0ID0gMDsgdGhpcy50YXJnZXQgPSBudWxsOyB0aGlzLmFybWVkVHlwZSA9IG51bGw7CiAgICB0aGlzLmdob3N0cyA9IFtdOwogICAgJCgnI3Jlc3VsdCcpLmNsYXNzTGlzdC5hZGQoJ2hpZGRlbicpOwogICAgJCgnI2xvY2tvbicpLmNsYXNzTGlzdC5hZGQoJ2hpZGRlbicpOwogICAgdGhpcy51cGRhdGVCYXR0ZXJ5KCk7IHRoaXMudXBkYXRlR2xvb20oKTsKCiAgICBpZiAoZnJlZUh1bnQpIHsKICAgICAgY29uc3QgcmVzID0gYXdhaXQgZmV0Y2goJy9hcGkvZnJlZWh1bnQ/bj02Jyk7CiAgICAgIHRoaXMucG9vbCA9IChhd2FpdCByZXMuanNvbigpKS5zcGF3bnM7CiAgICAgIHRoaXMuYm9zcyA9IG51bGw7CiAgICAgIHRoaXMucmVtYWluaW5nID0gSW5maW5pdHk7CiAgICB9IGVsc2UgewogICAgICB0aGlzLnBvb2wgPSBzZXREYXRhLnJvc3Rlci5maWx0ZXIoKGcpID0+ICFnLmlzQm9zcyk7CiAgICAgIHRoaXMuYm9zcyA9IHNldERhdGEuYm9zczsKICAgICAgdGhpcy5yZW1haW5pbmcgPSBNYXRoLm1pbig1LCB0aGlzLnBvb2wubGVuZ3RoKTsgLy8gc3BvdHMgdG8gY2xlYXIgYmVmb3JlIGJvc3MKICAgIH0KCiAgICBhd2FpdCB0aGlzLmluaXRDYW1lcmEoKTsKICAgIHRoaXMuaW5pdFNjZW5lKCk7CiAgICB0aGlzLmJpbmRDb250cm9scygpOwogICAgdGhpcy5tYXliZVByb21wdE1vdGlvbigpOwogICAgdGhpcy5zcGF3bk5leHQoKTsKICAgIHRoaXMubG9vcCgpOwogIH0sCgogIC8vIE9uIGlPUywgc3VyZmFjZSBhbiBleHBsaWNpdCAiRW5hYmxlIE1vdGlvbiIgYnV0dG9uLiBUYXBwaW5nIGl0IGlzIHRoZSBjbGVhbgogIC8vIHVzZXIgZ2VzdHVyZSBpT1MgbmVlZHMgdG8gc2hvdyBpdHMgTW90aW9uICYgT3JpZW50YXRpb24gQWNjZXNzIHByb21wdC4KICBtYXliZVByb21wdE1vdGlvbigpIHsKICAgIGNvbnN0IG92ZXJsYXkgPSAkKCcjbW90aW9uLWdhdGUnKTsKICAgIGlmICghdGhpcy5neXJvTmVlZHNQZXJtaXNzaW9uKCkpIHsgb3ZlcmxheS5jbGFzc0xpc3QuYWRkKCdoaWRkZW4nKTsgcmV0dXJuOyB9CiAgICBvdmVybGF5LmNsYXNzTGlzdC5yZW1vdmUoJ2hpZGRlbicpOwogICAgY29uc3QgYnRuID0gJCgnI21vdGlvbi1lbmFibGUnKTsKICAgIGNvbnN0IG9uVGFwID0gYXN5bmMgKCkgPT4gewogICAgICBjb25zdCBvayA9IGF3YWl0IHRoaXMucmVxdWVzdEd5cm8oKTsKICAgICAgb3ZlcmxheS5jbGFzc0xpc3QuYWRkKCdoaWRkZW4nKTsKICAgICAgaWYgKCFvaykgdGhpcy50b2FzdCgnTW90aW9uIGJsb2NrZWQg4oCUIGVuYWJsZSBpdCBpbiBTZXR0aW5ncyDigLogU2FmYXJpJywgMjIwMCk7CiAgICB9OwogICAgYnRuLm9uY2xpY2sgPSBvblRhcDsKICAgIHRoaXMuX21vdGlvbkVscyA9IHsgb3ZlcmxheSwgYnRuIH07CiAgfSwKCiAgYXN5bmMgaW5pdENhbWVyYSgpIHsKICAgIHRoaXMudmlkZW8gPSAkKCcjYXItdmlkZW8nKTsKICAgIHRyeSB7CiAgICAgIHRoaXMuc3RyZWFtID0gYXdhaXQgbmF2aWdhdG9yLm1lZGlhRGV2aWNlcy5nZXRVc2VyTWVkaWEoewogICAgICAgIHZpZGVvOiB7IGZhY2luZ01vZGU6ICdlbnZpcm9ubWVudCcgfSwgYXVkaW86IGZhbHNlLAogICAgICB9KTsKICAgICAgdGhpcy52aWRlby5zcmNPYmplY3QgPSB0aGlzLnN0cmVhbTsKICAgICAgYXdhaXQgdGhpcy52aWRlby5wbGF5KCk7CiAgICB9IGNhdGNoIHsgLyogbm8gY2FtZXJhIOKGkiBkYXJrIGJhY2tkcm9wLCBneXJvIHN0aWxsIHdvcmtzICovIH0KICB9LAoKICBpbml0U2NlbmUoKSB7CiAgICBjb25zdCBjYW52YXMgPSAkKCcjYXItY2FudmFzJyk7CiAgICB0aGlzLnJlbmRlcmVyID0gbmV3IFRIUkVFLldlYkdMUmVuZGVyZXIoeyBjYW52YXMsIGFscGhhOiB0cnVlLCBhbnRpYWxpYXM6IHRydWUgfSk7CiAgICB0aGlzLnJlbmRlcmVyLnNldFBpeGVsUmF0aW8oTWF0aC5taW4oZGV2aWNlUGl4ZWxSYXRpbywgMikpOwogICAgdGhpcy5yZW5kZXJlci5zZXRTaXplKGlubmVyV2lkdGgsIGlubmVySGVpZ2h0KTsKCiAgICB0aGlzLnNjZW5lID0gbmV3IFRIUkVFLlNjZW5lKCk7CiAgICB0aGlzLmNhbWVyYSA9IG5ldyBUSFJFRS5QZXJzcGVjdGl2ZUNhbWVyYSg3MCwgaW5uZXJXaWR0aCAvIGlubmVySGVpZ2h0LCAwLjEsIDEwMCk7CiAgICB0aGlzLmNhbWVyYS5wb3NpdGlvbi5zZXQoMCwgMCwgMCk7CgogICAgdGhpcy5zY2VuZS5hZGQobmV3IFRIUkVFLkFtYmllbnRMaWdodCgweGZmZmZmZiwgMC44KSk7CiAgICBjb25zdCBwID0gbmV3IFRIUkVFLlBvaW50TGlnaHQoMHg4OGFhZmYsIDEuMiwgNTApOwogICAgcC5wb3NpdGlvbi5zZXQoMCwgMiwgMik7CiAgICB0aGlzLnNjZW5lLmFkZChwKTsKCiAgICB0aGlzLl9vblJlc2l6ZSA9ICgpID0+IHsKICAgICAgdGhpcy5jYW1lcmEuYXNwZWN0ID0gaW5uZXJXaWR0aCAvIGlubmVySGVpZ2h0OwogICAgICB0aGlzLmNhbWVyYS51cGRhdGVQcm9qZWN0aW9uTWF0cml4KCk7CiAgICAgIHRoaXMucmVuZGVyZXIuc2V0U2l6ZShpbm5lcldpZHRoLCBpbm5lckhlaWdodCk7CiAgICB9OwogICAgYWRkRXZlbnRMaXN0ZW5lcigncmVzaXplJywgdGhpcy5fb25SZXNpemUpOwoKICAgIC8vIEd5cm8gbG9vay4gaU9TIFNhZmFyaSByZXF1aXJlcyBEZXZpY2VPcmllbnRhdGlvbkV2ZW50LnJlcXVlc3RQZXJtaXNzaW9uKCkKICAgIC8vIGZyb20gYW4gZXhwbGljaXQgdXNlciBnZXN0dXJlIChoYW5kbGVkIGJ5IHRoZSAiRW5hYmxlIE1vdGlvbiIgYnV0dG9uIGluCiAgICAvLyBzdGFydCgpKS4gT3RoZXIgYnJvd3NlcnMgY2FuIGF0dGFjaCB0aGUgbGlzdGVuZXIgaW1tZWRpYXRlbHkuCiAgICB0aGlzLl9vbk9yaWVudCA9IChlKSA9PiB7CiAgICAgIGlmIChlLmFscGhhID09IG51bGwpIHJldHVybjsKICAgICAgdGhpcy5vcmllbnRhdGlvbiA9IHsgYWxwaGE6IGUuYWxwaGEsIGJldGE6IGUuYmV0YSwgZ2FtbWE6IGUuZ2FtbWEsIGFjdGl2ZTogdHJ1ZSB9OwogICAgfTsKICAgIGlmICghdGhpcy5neXJvTmVlZHNQZXJtaXNzaW9uKCkpIHsKICAgICAgYWRkRXZlbnRMaXN0ZW5lcignZGV2aWNlb3JpZW50YXRpb24nLCB0aGlzLl9vbk9yaWVudCk7CiAgICAgIHRoaXMuX2d5cm9BdHRhY2hlZCA9IHRydWU7CiAgICB9CiAgfSwKCiAgZ3lyb05lZWRzUGVybWlzc2lvbigpIHsKICAgIGNvbnN0IERPRSA9IHdpbmRvdy5EZXZpY2VPcmllbnRhdGlvbkV2ZW50OwogICAgcmV0dXJuICEhKERPRSAmJiB0eXBlb2YgRE9FLnJlcXVlc3RQZXJtaXNzaW9uID09PSAnZnVuY3Rpb24nKTsKICB9LAoKICAvLyBDYWxsZWQgZnJvbSBhIGNsZWFuIHRhcCAodGhlIEVuYWJsZSBNb3Rpb24gb3ZlcmxheSkuIE9uIGlPUyB0aGlzIHRyaWdnZXJzCiAgLy8gdGhlIHN5c3RlbSAiTW90aW9uICYgT3JpZW50YXRpb24gQWNjZXNzIiBwcm9tcHQ7IG9uY2UgZ3JhbnRlZCB3ZSBhdHRhY2ggdGhlCiAgLy8gbGlzdGVuZXIuIFNhZmUgdG8gY2FsbCBtb3JlIHRoYW4gb25jZS4KICBhc3luYyByZXF1ZXN0R3lybygpIHsKICAgIGlmICh0aGlzLl9neXJvQXR0YWNoZWQpIHJldHVybiB0cnVlOwogICAgY29uc3QgRE9FID0gd2luZG93LkRldmljZU9yaWVudGF0aW9uRXZlbnQ7CiAgICB0cnkgewogICAgICBpZiAoRE9FICYmIHR5cGVvZiBET0UucmVxdWVzdFBlcm1pc3Npb24gPT09ICdmdW5jdGlvbicpIHsKICAgICAgICBjb25zdCByZXMgPSBhd2FpdCBET0UucmVxdWVzdFBlcm1pc3Npb24oKTsKICAgICAgICBpZiAocmVzICE9PSAnZ3JhbnRlZCcpIHJldHVybiBmYWxzZTsKICAgICAgfQogICAgICBhZGRFdmVudExpc3RlbmVyKCdkZXZpY2VvcmllbnRhdGlvbicsIHRoaXMuX29uT3JpZW50KTsKICAgICAgdGhpcy5fZ3lyb0F0dGFjaGVkID0gdHJ1ZTsKICAgICAgcmV0dXJuIHRydWU7CiAgICB9IGNhdGNoIHsKICAgICAgcmV0dXJuIGZhbHNlOwogICAgfQogIH0sCgogIGJpbmRDb250cm9scygpIHsKICAgIC8vIGNvbG9yIHdoZWVsCiAgICAkJCgnLndoZWVsLXNlZycpLmZvckVhY2goKHNlZykgPT4gewogICAgICBzZWcub25jbGljayA9ICgpID0+IHsKICAgICAgICBjb25zdCB0eXBlID0gc2VnLmRhdGFzZXQudHlwZTsKICAgICAgICB0aGlzLmFybWVkVHlwZSA9IHR5cGU7CiAgICAgICAgJCQoJy53aGVlbC1zZWcnKS5mb3JFYWNoKChzKSA9PiBzLmNsYXNzTGlzdC5yZW1vdmUoJ2FybWVkJykpOwogICAgICAgIHNlZy5jbGFzc0xpc3QuYWRkKCdhcm1lZCcpOwogICAgICAgICQoJyN3aGVlbC1jb3JlJykudGV4dENvbnRlbnQgPSB0eXBlLnRvVXBwZXJDYXNlKCk7CiAgICAgICAgdGhpcy5zY2FuR2xvb20odHlwZSk7CiAgICAgIH07CiAgICB9KTsKCiAgICBjb25zdCBibGFzdCA9ICQoJyNidG4tYmxhc3QnKTsKICAgIGNvbnN0IHN0YXJ0Qmxhc3QgPSAoZSkgPT4gewogICAgICBlLnByZXZlbnREZWZhdWx0KCk7CiAgICAgIHRoaXMuYmxhc3RpbmcgPSB0cnVlOwogICAgfTsKICAgIGNvbnN0IGVuZEJsYXN0ID0gKCkgPT4geyB0aGlzLmJsYXN0aW5nID0gZmFsc2U7IH07CiAgICBibGFzdC5hZGRFdmVudExpc3RlbmVyKCd0b3VjaHN0YXJ0Jywgc3RhcnRCbGFzdCwgeyBwYXNzaXZlOiBmYWxzZSB9KTsKICAgIGJsYXN0LmFkZEV2ZW50TGlzdGVuZXIoJ21vdXNlZG93bicsIHN0YXJ0Qmxhc3QpOwogICAgYWRkRXZlbnRMaXN0ZW5lcigndG91Y2hlbmQnLCBlbmRCbGFzdCk7CiAgICBhZGRFdmVudExpc3RlbmVyKCdtb3VzZXVwJywgZW5kQmxhc3QpOwogICAgdGhpcy5fYmxhc3RFbHMgPSB7IGJsYXN0LCBzdGFydEJsYXN0LCBlbmRCbGFzdCB9OwogIH0sCgogIC8qIHNwYXduIGEgd2lzcC9iaWxsYm9hcmQgZ2hvc3QgYXQgYSByYW5kb20gc3BvdCBhcm91bmQgdGhlIHBsYXllciAqLwogIHNwYXduTmV4dCgpIHsKICAgIGlmICghdGhpcy5wb29sLmxlbmd0aCkgcmV0dXJuOwogICAgY29uc3QgZGF0YSA9IHRoaXMucG9vbFtNYXRoLmZsb29yKE1hdGgucmFuZG9tKCkgKiB0aGlzLnBvb2wubGVuZ3RoKV07CiAgICB0aGlzLmFkZEdob3N0KGRhdGEpOwogIH0sCgogIGFkZEdob3N0KGRhdGEsIGlzQm9zcyA9IGZhbHNlKSB7CiAgICBjb25zdCBncm91cCA9IG5ldyBUSFJFRS5Hcm91cCgpOwogICAgY29uc3QgY29sb3IgPSBUWVBFX0NPTE9SU1tkYXRhLnR5cGVdID8/IDB4ZmZmZmZmOwoKICAgIGxldCBtZXNoOwogICAgbGV0IHRleHR1cmUgPSBudWxsOwogICAgaWYgKGRhdGEud2VibSAmJiBTVVBQT1JUU19XRUJNX0FMUEhBKSB7CiAgICAgIC8vIFdlYk0gKFZQOSthbHBoYSkgYmlsbGJvYXJkIHZpYSBWaWRlb1RleHR1cmUuIFRoZSBicm93c2VyIGRlY29kZXMgYW5kCiAgICAgIC8vIHVwZGF0ZXMgdGhlIHRleHR1cmUgaXRzZWxmIOKAlCBubyBwZXItZnJhbWUgbmVlZHNVcGRhdGUgcHVtcGluZyBuZWVkZWQuCiAgICAgIGNvbnN0IHZpZCA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ3ZpZGVvJyk7CiAgICAgIHZpZC5jcm9zc09yaWdpbiA9ICdhbm9ueW1vdXMnOwogICAgICB2aWQubXV0ZWQgPSB0cnVlOyAgICAgICAgLy8gcmVxdWlyZWQgZm9yIGF1dG9wbGF5CiAgICAgIHZpZC5sb29wID0gdHJ1ZTsKICAgICAgdmlkLnBsYXlzSW5saW5lID0gdHJ1ZTsgIC8vIGlPUzogc3RheSBpbmxpbmUsIGRvbid0IGZ1bGxzY3JlZW4KICAgICAgdmlkLmF1dG9wbGF5ID0gdHJ1ZTsKICAgICAgdmlkLnByZWxvYWQgPSAnYXV0byc7CiAgICAgIHZpZC5zcmMgPSBkYXRhLndlYm07CiAgICAgIHRleHR1cmUgPSBuZXcgVEhSRUUuVmlkZW9UZXh0dXJlKHZpZCk7CiAgICAgIHRleHR1cmUuY29sb3JTcGFjZSA9IFRIUkVFLlNSR0JDb2xvclNwYWNlOwogICAgICB0ZXh0dXJlLm1pbkZpbHRlciA9IFRIUkVFLkxpbmVhckZpbHRlcjsKICAgICAgdGV4dHVyZS5tYWdGaWx0ZXIgPSBUSFJFRS5MaW5lYXJGaWx0ZXI7CiAgICAgIHRleHR1cmUuZ2VuZXJhdGVNaXBtYXBzID0gZmFsc2U7CiAgICAgIGNvbnN0IG1hdCA9IG5ldyBUSFJFRS5NZXNoQmFzaWNNYXRlcmlhbCh7IG1hcDogdGV4dHVyZSwgdHJhbnNwYXJlbnQ6IHRydWUsIHNpZGU6IFRIUkVFLkRvdWJsZVNpZGUsIGRlcHRoV3JpdGU6IGZhbHNlIH0pOwogICAgICBtZXNoID0gbmV3IFRIUkVFLk1lc2gobmV3IFRIUkVFLlBsYW5lR2VvbWV0cnkoMS4yLCAxLjIpLCBtYXQpOwogICAgICAvLyBJZiB0aGUgdmlkZW8gZXJyb3JzIChlLmcuIGFscGhhIHVuc3VwcG9ydGVkIGRlc3BpdGUgY2FuUGxheVR5cGUpLCBzd2FwCiAgICAgIC8vIHRvIHRoZSBHSUYvaW1hZ2UgYmlsbGJvYXJkIGlmIHdlIGhhdmUgb25lLgogICAgICB2aWQub25lcnJvciA9ICgpID0+IHsKICAgICAgICBpZiAoZ3JvdXAudXNlckRhdGEudmlkZW9GZWxsQmFjaykgcmV0dXJuOwogICAgICAgIGdyb3VwLnVzZXJEYXRhLnZpZGVvRmVsbEJhY2sgPSB0cnVlOwogICAgICAgIGlmIChkYXRhLmltYWdlKSB7CiAgICAgICAgICBjb25zdCBpbWcgPSBkb2N1bWVudC5jcmVhdGVFbGVtZW50KCdpbWcnKTsKICAgICAgICAgIGltZy5jcm9zc09yaWdpbiA9ICdhbm9ueW1vdXMnOwogICAgICAgICAgaW1nLnNyYyA9IGRhdGEuaW1hZ2U7CiAgICAgICAgICBjb25zdCB0MiA9IG5ldyBUSFJFRS5UZXh0dXJlKGltZyk7CiAgICAgICAgICBpbWcub25sb2FkID0gKCkgPT4geyB0Mi5uZWVkc1VwZGF0ZSA9IHRydWU7IH07CiAgICAgICAgICBtZXNoLm1hdGVyaWFsLm1hcCA9IHQyOwogICAgICAgICAgbWVzaC5tYXRlcmlhbC5uZWVkc1VwZGF0ZSA9IHRydWU7CiAgICAgICAgICBncm91cC51c2VyRGF0YS5naWZJbWcgPSBpbWc7CiAgICAgICAgICBncm91cC51c2VyRGF0YS5naWZUZXggPSB0MjsKICAgICAgICAgIGdyb3VwLnVzZXJEYXRhLnZpZEVsID0gbnVsbDsKICAgICAgICB9CiAgICAgIH07CiAgICAgIGNvbnN0IHByID0gdmlkLnBsYXkoKTsKICAgICAgaWYgKHByICYmIHByLmNhdGNoKSBwci5jYXRjaCgoKSA9PiB7fSk7CiAgICAgIGdyb3VwLnVzZXJEYXRhLnZpZEVsID0gdmlkOwogICAgfSBlbHNlIGlmIChkYXRhLmltYWdlKSB7CiAgICAgIC8vIGFuaW1hdGVkIEdJRiBiaWxsYm9hcmQg4oCUIHRleHR1cmUubmVlZHNVcGRhdGUgcHVtcGVkIGVhY2ggZnJhbWUKICAgICAgY29uc3QgaW1nID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnaW1nJyk7CiAgICAgIGltZy5jcm9zc09yaWdpbiA9ICdhbm9ueW1vdXMnOwogICAgICBpbWcuc3JjID0gZGF0YS5pbWFnZTsKICAgICAgdGV4dHVyZSA9IG5ldyBUSFJFRS5UZXh0dXJlKGltZyk7CiAgICAgIGltZy5vbmxvYWQgPSAoKSA9PiB7IHRleHR1cmUubmVlZHNVcGRhdGUgPSB0cnVlOyB9OwogICAgICBjb25zdCBtYXQgPSBuZXcgVEhSRUUuTWVzaEJhc2ljTWF0ZXJpYWwoeyBtYXA6IHRleHR1cmUsIHRyYW5zcGFyZW50OiB0cnVlLCBzaWRlOiBUSFJFRS5Eb3VibGVTaWRlIH0pOwogICAgICBtZXNoID0gbmV3IFRIUkVFLk1lc2gobmV3IFRIUkVFLlBsYW5lR2VvbWV0cnkoMS4yLCAxLjIpLCBtYXQpOwogICAgICBncm91cC51c2VyRGF0YS5naWZJbWcgPSBpbWc7CiAgICAgIGdyb3VwLnVzZXJEYXRhLmdpZlRleCA9IHRleHR1cmU7CiAgICB9IGVsc2UgewogICAgICAvLyBwcm9jZWR1cmFsIHdpc3A6IGdsb3dpbmcgc3BoZXJlICsgaGFsbwogICAgICBjb25zdCBnZW8gPSBuZXcgVEhSRUUuU3BoZXJlR2VvbWV0cnkoMC40NSwgMjQsIDI0KTsKICAgICAgY29uc3QgbWF0ID0gbmV3IFRIUkVFLk1lc2hTdGFuZGFyZE1hdGVyaWFsKHsKICAgICAgICBjb2xvciwgZW1pc3NpdmU6IGNvbG9yLCBlbWlzc2l2ZUludGVuc2l0eTogMS40LCByb3VnaG5lc3M6IDAuNCwKICAgICAgICB0cmFuc3BhcmVudDogdHJ1ZSwgb3BhY2l0eTogMC45MiwKICAgICAgfSk7CiAgICAgIG1lc2ggPSBuZXcgVEhSRUUuTWVzaChnZW8sIG1hdCk7CiAgICAgIGNvbnN0IGhhbG8gPSBuZXcgVEhSRUUuTWVzaCgKICAgICAgICBuZXcgVEhSRUUuU3BoZXJlR2VvbWV0cnkoMC42MiwgMjQsIDI0KSwKICAgICAgICBuZXcgVEhSRUUuTWVzaEJhc2ljTWF0ZXJpYWwoeyBjb2xvciwgdHJhbnNwYXJlbnQ6IHRydWUsIG9wYWNpdHk6IDAuMTYsIHNpZGU6IFRIUkVFLkJhY2tTaWRlIH0pCiAgICAgICk7CiAgICAgIGdyb3VwLmFkZChoYWxvKTsKICAgIH0KICAgIGdyb3VwLmFkZChtZXNoKTsKCiAgICAvLyBQbGFjZSBpbiBhIGZvcndhcmQtZmFjaW5nIGFyYyBzbyBnaG9zdHMgYXJlIGZpbmRhYmxlIHdpdGhvdXQgcGVyZmVjdAogICAgLy8gYWltaW5nOiB5YXcgd2l0aGluIMKxNjDCsCBvZiBzdHJhaWdodCBhaGVhZCwgbW9kZXN0IHBpdGNoLCBjbG9zZXIgaW4uCiAgICBjb25zdCB5YXcgPSAoTWF0aC5yYW5kb20oKSAtIDAuNSkgKiAoTWF0aC5QSSAqIDIgLyAzKTsKICAgIGNvbnN0IHBpdGNoID0gKE1hdGgucmFuZG9tKCkgLSAwLjUpICogMC41OwogICAgY29uc3QgZGlzdCA9IDMgKyBNYXRoLnJhbmRvbSgpICogMS41OwogICAgZ3JvdXAucG9zaXRpb24uc2V0KAogICAgICBNYXRoLnNpbih5YXcpICogZGlzdCwKICAgICAgTWF0aC5zaW4ocGl0Y2gpICogZGlzdCwKICAgICAgLU1hdGguY29zKHlhdykgKiBkaXN0CiAgICApOwoKICAgIGdyb3VwLnVzZXJEYXRhID0gewogICAgICAuLi5ncm91cC51c2VyRGF0YSwKICAgICAgZGF0YSwgaXNCb3NzLAogICAgICBocDogZGF0YS5oZWFsdGgsIG1heEhwOiBkYXRhLmhlYWx0aCwKICAgICAgYm9iUGhhc2U6IE1hdGgucmFuZG9tKCkgKiBNYXRoLlBJICogMiwKICAgICAgcmV2ZWFsZWQ6IHRydWUsIC8vIHZpc2libGUgb24gc3Bhd247IHRoZSB3aGVlbCBub3cgYWdncm9zL3RpbnRzIHJhdGhlciB0aGFuIHVuY2xvYWtzCiAgICB9OwogICAgdGhpcy5zY2VuZS5hZGQoZ3JvdXApOwogICAgdGhpcy5naG9zdHMucHVzaChncm91cCk7CiAgfSwKCiAgc2Nhbkdsb29tKHR5cGUpIHsKICAgIC8vIEdob3N0cyBhcmUgdmlzaWJsZSBvbiBzcGF3biBub3csIHNvIHRoZSB3aGVlbCBhY3RzIGFzIGEgImx1cmUiOiBzY2FubmluZyBhCiAgICAvLyBjb2xvdXIgcHVsbHMgdGhlIG5lYXJlc3QgbWF0Y2hpbmcgZ2hvc3QgdG93YXJkIHlvdXIgYWltIGFuZCBjaGFyZ2VzIGdsb29tLgogICAgLy8gTWlzbWF0Y2hlZCBjb2xvdXIgcmlza3MgYSBqdW1wc2NhcmUuCiAgICBjb25zdCBtYXRjaGVzID0gdGhpcy5naG9zdHMuZmlsdGVyKChnKSA9PiBnLnVzZXJEYXRhLmRhdGEudHlwZSA9PT0gdHlwZSk7CiAgICBpZiAobWF0Y2hlcy5sZW5ndGgpIHsKICAgICAgLy8gcHVsbCB0aGUgbmVhcmVzdCBtYXRjaCBpbnRvIGEgZm9yd2FyZCwgZWFzeS10by1haW0gcG9zaXRpb24KICAgICAgY29uc3QgZyA9IG1hdGNoZXNbMF07CiAgICAgIGNvbnN0IGRpc3QgPSAzOwogICAgICBnLnBvc2l0aW9uLnNldCgoTWF0aC5yYW5kb20oKSAtIDAuNSkgKiAxLjIsIChNYXRoLnJhbmRvbSgpIC0gMC4zKSAqIDAuOCwgLWRpc3QpOwogICAgICBnLnVzZXJEYXRhLmx1cmVkID0gdHJ1ZTsKICAgICAgdGhpcy5nbG9vbSArPSA1OyB0aGlzLnVwZGF0ZUdsb29tKCk7CiAgICAgIHRoaXMudG9hc3QoYCR7dHlwZS50b1VwcGVyQ2FzZSgpfSBnaG9zdCBsdXJlZCBpbmAsIDgwMCk7CiAgICB9IGVsc2UgewogICAgICAvLyBubyBnaG9zdCBvZiB0aGF0IGNvbG91ciBwcmVzZW50IOKAlCBzbWFsbCBjaGFuY2Ugb2YgYSBqdW1wc2NhcmUKICAgICAgaWYgKE1hdGgucmFuZG9tKCkgPCAwLjEyKSB0aGlzLmp1bXBzY2FyZSgpOwogICAgICBlbHNlIHsgdGhpcy5nbG9vbSArPSAyOyB0aGlzLnVwZGF0ZUdsb29tKCk7IHRoaXMudG9hc3QoJysyIGdsb29tJywgNjAwKTsgfQogICAgfQogIH0sCgogIGxvb3AoKSB7CiAgICBpZiAoIXRoaXMucnVubmluZykgcmV0dXJuOwogICAgdGhpcy5yYWYgPSByZXF1ZXN0QW5pbWF0aW9uRnJhbWUoKCkgPT4gdGhpcy5sb29wKCkpOwogICAgY29uc3QgdCA9IHBlcmZvcm1hbmNlLm5vdygpIC8gMTAwMDsKCiAgICAvLyBjYW1lcmEgeWF3L3BpdGNoIGZyb20gZ3lybwogICAgaWYgKHRoaXMub3JpZW50YXRpb24uYWN0aXZlKSB7CiAgICAgIGNvbnN0IHsgYWxwaGEsIGJldGEsIGdhbW1hIH0gPSB0aGlzLm9yaWVudGF0aW9uOwogICAgICAvLyBzaW1wbGUgbWFwcGluZzsgZ29vZCBlbm91Z2ggZm9yIGxvb2stYXJvdW5kIGFpbWluZwogICAgICB0aGlzLmNhbWVyYS5yb3RhdGlvbi5zZXQoCiAgICAgICAgVEhSRUUuTWF0aFV0aWxzLmRlZ1RvUmFkKChiZXRhID8/IDApIC0gOTApLAogICAgICAgIFRIUkVFLk1hdGhVdGlscy5kZWdUb1JhZChhbHBoYSA/PyAwKSwKICAgICAgICBUSFJFRS5NYXRoVXRpbHMuZGVnVG9SYWQoLShnYW1tYSA/PyAwKSksCiAgICAgICAgJ1lYWicKICAgICAgKTsKICAgIH0KCiAgICAvLyBhbmltYXRlIGdob3N0czogYm9iICsgZmFjZSBjYW1lcmEgKyBwdW1wIEdJRiB0ZXh0dXJlcwogICAgZm9yIChjb25zdCBnIG9mIHRoaXMuZ2hvc3RzKSB7CiAgICAgIGcucG9zaXRpb24ueSArPSBNYXRoLnNpbih0ICogMS41ICsgZy51c2VyRGF0YS5ib2JQaGFzZSkgKiAwLjAwMjU7CiAgICAgIGcubG9va0F0KHRoaXMuY2FtZXJhLnBvc2l0aW9uKTsKICAgICAgaWYgKGcudXNlckRhdGEuZ2lmVGV4ICYmIGcudXNlckRhdGEuZ2lmSW1nPy5jb21wbGV0ZSkgZy51c2VyRGF0YS5naWZUZXgubmVlZHNVcGRhdGUgPSB0cnVlOwogICAgfQoKICAgIC8vIHRhcmdldGluZzogZ2hvc3QgbmVhcmVzdCBzY3JlZW4tY2VudGVyIChhbmQgcmV2ZWFsZWQpIGJlY29tZXMgdGhlIGxvY2stb24KICAgIHRoaXMudXBkYXRlVGFyZ2V0KCk7CgogICAgLy8gYmxhc3RlcgogICAgaWYgKHRoaXMuYmxhc3RpbmcgJiYgdGhpcy5vdmVyaGVhdCA8IDEwMCAmJiB0aGlzLnRhcmdldCkgewogICAgICBjb25zdCBkbWcgPSAwLjk7IC8vIHBlci1mcmFtZSBjaGlwIGRhbWFnZQogICAgICB0aGlzLnRhcmdldC51c2VyRGF0YS5ocCAtPSBkbWc7CiAgICAgIHRoaXMub3ZlcmhlYXQgPSBNYXRoLm1pbigxMDAsIHRoaXMub3ZlcmhlYXQgKyAwLjgpOwogICAgICB0aGlzLnVwZGF0ZUdob3N0SHAoKTsKICAgICAgaWYgKHRoaXMudGFyZ2V0LnVzZXJEYXRhLmhwIDw9IDApIHRoaXMuY2FwdHVyZVRhcmdldCgpOwogICAgfSBlbHNlIGlmICghdGhpcy5ibGFzdGluZyAmJiB0aGlzLm92ZXJoZWF0ID4gMCkgewogICAgICB0aGlzLm92ZXJoZWF0ID0gTWF0aC5tYXgoMCwgdGhpcy5vdmVyaGVhdCAtIDEuMik7IC8vIGNvb2wgZG93bgogICAgfQogICAgaWYgKHRoaXMub3ZlcmhlYXQgPj0gMTAwKSB0aGlzLmJsYXN0aW5nID0gZmFsc2U7CiAgICB0aGlzLnVwZGF0ZU92ZXJoZWF0KCk7CgogICAgLy8gZ2hvc3RzIGF0dGFjazogZHJhaW4gYmF0dGVyeSBpZiBhIHJldmVhbGVkIGdob3N0IGlzIGluIGZyb250IG9mIHlvdQogICAgdGhpcy5tYXliZVRha2VEYW1hZ2UoKTsKCiAgICB0aGlzLnJlbmRlcmVyLnJlbmRlcih0aGlzLnNjZW5lLCB0aGlzLmNhbWVyYSk7CiAgfSwKCiAgdXBkYXRlVGFyZ2V0KCkgewogICAgY29uc3QgY2VudGVyID0gbmV3IFRIUkVFLlZlY3RvcjIoMCwgMCk7CiAgICBsZXQgYmVzdCA9IG51bGwsIGJlc3REaXN0ID0gMC41OyAvLyB3aXRoaW4gfmhhbGYtc2NyZWVuIG9mIGNlbnRlcgogICAgY29uc3QgdiA9IG5ldyBUSFJFRS5WZWN0b3IzKCk7CiAgICBmb3IgKGNvbnN0IGcgb2YgdGhpcy5naG9zdHMpIHsKICAgICAgaWYgKCFnLnVzZXJEYXRhLnJldmVhbGVkKSBjb250aW51ZTsKICAgICAgdi5jb3B5KGcucG9zaXRpb24pLnByb2plY3QodGhpcy5jYW1lcmEpOwogICAgICBpZiAodi56ID4gMSkgY29udGludWU7IC8vIGJlaGluZCBjYW1lcmEKICAgICAgY29uc3QgZCA9IE1hdGguaHlwb3Qodi54IC0gY2VudGVyLngsIHYueSAtIGNlbnRlci55KTsKICAgICAgaWYgKGQgPCBiZXN0RGlzdCkgeyBiZXN0RGlzdCA9IGQ7IGJlc3QgPSBnOyB9CiAgICB9CiAgICBpZiAoYmVzdCAhPT0gdGhpcy50YXJnZXQpIHsKICAgICAgdGhpcy50YXJnZXQgPSBiZXN0OwogICAgICB0aGlzLnJlbmRlckxvY2tvbigpOwogICAgfQogIH0sCgogIHJlbmRlckxvY2tvbigpIHsKICAgIGNvbnN0IGxvY2sgPSAkKCcjbG9ja29uJyk7CiAgICBpZiAoIXRoaXMudGFyZ2V0KSB7IGxvY2suY2xhc3NMaXN0LmFkZCgnaGlkZGVuJyk7IHJldHVybjsgfQogICAgY29uc3QgZCA9IHRoaXMudGFyZ2V0LnVzZXJEYXRhLmRhdGE7CiAgICBsb2NrLmNsYXNzTGlzdC5yZW1vdmUoJ2hpZGRlbicpOwogICAgLy8gSU1QT1JUQU5UOiBsYWJlbCByZWFkcyBkaXNwbGF5TmFtZSwgbm90IGEgaGFyZGNvZGVkIHRpbnQgbmFtZQogICAgJCgnI2xvY2tvbi1uYW1lJykudGV4dENvbnRlbnQgPSBkLmRpc3BsYXlOYW1lIHx8IGQubmFtZTsKICAgICQoJyNsb2Nrb24tbmFtZScpLmNsYXNzTmFtZSA9IGBsb2Nrb24tbmFtZSBkaXNwbGF5IHR5cGUtJHtkLnR5cGV9YDsKICAgICQoJyNsb2Nrb24tdHlwZScpLnRleHRDb250ZW50ID0gZC50eXBlOwogICAgJCgnI2xvY2tvbi1yYXJpdHknKS50ZXh0Q29udGVudCA9ICfimIUnLnJlcGVhdChkLnJhcml0eSk7CiAgICAkKCcjbG9ja29uLWFiaWxpdHknKS50ZXh0Q29udGVudCA9IGQuYWJpbGl0eSB8fCAn4oCUJzsKICAgIHRoaXMudXBkYXRlR2hvc3RIcCgpOwogIH0sCgogIHVwZGF0ZUdob3N0SHAoKSB7CiAgICBpZiAoIXRoaXMudGFyZ2V0KSByZXR1cm47CiAgICBjb25zdCBwY3QgPSBNYXRoLm1heCgwLCAodGhpcy50YXJnZXQudXNlckRhdGEuaHAgLyB0aGlzLnRhcmdldC51c2VyRGF0YS5tYXhIcCkgKiAxMDApOwogICAgJCgnI2dob3N0LWhwLWZpbGwnKS5zdHlsZS53aWR0aCA9IGAke3BjdH0lYDsKICB9LAoKICBjYXB0dXJlVGFyZ2V0KCkgewogICAgY29uc3QgZyA9IHRoaXMudGFyZ2V0OwogICAgY29uc3QgZCA9IGcudXNlckRhdGEuZGF0YTsKICAgIGlmIChnLnVzZXJEYXRhLnZpZEVsKSB7IHRyeSB7IGcudXNlckRhdGEudmlkRWwucGF1c2UoKTsgZy51c2VyRGF0YS52aWRFbC5zcmMgPSAnJzsgfSBjYXRjaCB7fSBnLnVzZXJEYXRhLnZpZEVsID0gbnVsbDsgfQogICAgdGhpcy5zY2VuZS5yZW1vdmUoZyk7CiAgICB0aGlzLmdob3N0cyA9IHRoaXMuZ2hvc3RzLmZpbHRlcigoeCkgPT4geCAhPT0gZyk7CiAgICB0aGlzLnRhcmdldCA9IG51bGw7CiAgICAkKCcjbG9ja29uJykuY2xhc3NMaXN0LmFkZCgnaGlkZGVuJyk7CiAgICB0aGlzLmdsb29tICs9IDEwICsgZC5yYXJpdHkgKiA1OwogICAgdGhpcy51cGRhdGVHbG9vbSgpOwogICAgdGhpcy50b2FzdChgQ2FwdHVyZWQgJHtkLmRpc3BsYXlOYW1lIHx8IGQubmFtZX0hYCwgOTAwKTsKCiAgICBpZiAoZy51c2VyRGF0YS5pc0Jvc3MpIHsgdGhpcy53aW4oZCk7IHJldHVybjsgfQoKICAgIGlmICghdGhpcy5mcmVlSHVudCkgewogICAgICB0aGlzLnJlbWFpbmluZyAtPSAxOwogICAgICBpZiAodGhpcy5yZW1haW5pbmcgPD0gMCAmJiB0aGlzLmJvc3MpIHsKICAgICAgICAvLyBzb3VsIGFydGlmYWN0IOKGkiBib3NzIGFwcGVhcnMKICAgICAgICB0aGlzLnRvYXN0KCdBIFNvdWwgQXJ0aWZhY3QgcHVsc2Vz4oCmJywgMTEwMCk7CiAgICAgICAgc2V0VGltZW91dCgoKSA9PiB0aGlzLmFkZEdob3N0KHRoaXMuYm9zcywgdHJ1ZSksIDExMDApOwogICAgICAgIHJldHVybjsKICAgICAgfQogICAgfQogICAgLy8gcmVzcGF3biBhbm90aGVyIGdob3N0IHRvIGtlZXAgdGhlIGh1bnQgZ29pbmcKICAgIHNldFRpbWVvdXQoKCkgPT4gdGhpcy5zcGF3bk5leHQoKSwgNjAwKTsKICB9LAoKICBtYXliZVRha2VEYW1hZ2UoKSB7CiAgICAvLyBhIHJldmVhbGVkIGdob3N0IHJvdWdobHkgY2VudGVyZWQgKyB3ZSdyZSBub3QgYmxhc3Rpbmcg4oaSIGl0IGdsb29tcyB1cwogICAgaWYgKHRoaXMuYmxhc3RpbmcpIHJldHVybjsKICAgIGlmICghdGhpcy50YXJnZXQpIHJldHVybjsKICAgIGlmIChNYXRoLnJhbmRvbSgpIDwgMC4wMDQpIHsKICAgICAgY29uc3QgZG1nID0gdGhpcy50YXJnZXQudXNlckRhdGEuZGF0YS5yYXJpdHkgKiAxLjU7IC8vIDTimIUgaHVydHMgbW9zdAogICAgICB0aGlzLmJhdHRlcnkgPSBNYXRoLm1heCgwLCB0aGlzLmJhdHRlcnkgLSBkbWcpOwogICAgICB0aGlzLnVwZGF0ZUJhdHRlcnkoKTsKICAgICAgdGhpcy5mbGFzaERhbWFnZSgpOwogICAgICBpZiAodGhpcy5iYXR0ZXJ5IDw9IDApIHRoaXMubG9zZSgpOwogICAgfQogIH0sCgogIC8qIC0tLS0gSFVEIHVwZGF0ZXMgLS0tLSAqLwogIHVwZGF0ZUJhdHRlcnkoKSB7CiAgICBjb25zdCBmaWxsID0gJCgnI2JhdHRlcnktZmlsbCcpOwogICAgZmlsbC5zdHlsZS53aWR0aCA9IGAke3RoaXMuYmF0dGVyeX0lYDsKICAgIGZpbGwuY2xhc3NMaXN0LnRvZ2dsZSgnbG93JywgdGhpcy5iYXR0ZXJ5IDw9IDMwKTsKICAgICQoJyNiYXR0ZXJ5LXBjdCcpLnRleHRDb250ZW50ID0gYCR7TWF0aC5yb3VuZCh0aGlzLmJhdHRlcnkpfSVgOwogIH0sCiAgdXBkYXRlR2xvb20oKSB7ICQoJyNnbG9vbS1jb3VudCcpLnRleHRDb250ZW50ID0gdGhpcy5nbG9vbTsgfSwKICB1cGRhdGVPdmVyaGVhdCgpIHsKICAgICQoJyNvdmVyaGVhdC1maWxsJykuc3R5bGUud2lkdGggPSBgJHt0aGlzLm92ZXJoZWF0fSVgOwogICAgJCgnI2J0bi1ibGFzdCcpLmNsYXNzTGlzdC50b2dnbGUoJ292ZXJoZWF0ZWQnLCB0aGlzLm92ZXJoZWF0ID49IDEwMCk7CiAgICAkKCcjYnRuLWJsYXN0JykudGV4dENvbnRlbnQgPSB0aGlzLm92ZXJoZWF0ID49IDEwMCA/ICdPVkVSSEVBVEVEJyA6ICdIT0xEIFRPIEJMQVNUJzsKICB9LAoKICB0b2FzdChtc2csIG1zID0gODAwLCBqdW1wID0gZmFsc2UpIHsKICAgIGNvbnN0IHQgPSAkKCcjdG9hc3QnKTsKICAgIHQudGV4dENvbnRlbnQgPSBtc2c7CiAgICB0LmNsYXNzTmFtZSA9IGB0b2FzdCR7anVtcCA/ICcganVtcHNjYXJlJyA6ICcnfWA7CiAgICBjbGVhclRpbWVvdXQodGhpcy5fdG9hc3RUKTsKICAgIHRoaXMuX3RvYXN0VCA9IHNldFRpbWVvdXQoKCkgPT4gdC5jbGFzc0xpc3QuYWRkKCdoaWRkZW4nKSwgbXMpOwogIH0sCiAganVtcHNjYXJlKCkgewogICAgdGhpcy50b2FzdCgnQk9PIScsIDcwMCwgdHJ1ZSk7CiAgICB0aGlzLmJhdHRlcnkgPSBNYXRoLm1heCgwLCB0aGlzLmJhdHRlcnkgLSA0KTsKICAgIHRoaXMudXBkYXRlQmF0dGVyeSgpOwogICAgaWYgKG5hdmlnYXRvci52aWJyYXRlKSBuYXZpZ2F0b3IudmlicmF0ZSgxMjApOwogIH0sCiAgZmxhc2hEYW1hZ2UoKSB7CiAgICBpZiAobmF2aWdhdG9yLnZpYnJhdGUpIG5hdmlnYXRvci52aWJyYXRlKDQwKTsKICAgIGRvY3VtZW50LmJvZHkuYW5pbWF0ZSgKICAgICAgW3sgYm94U2hhZG93OiAnaW5zZXQgMCAwIDAgOTk5OXB4IHJnYmEoMjU1LDU5LDkyLC4yNSknIH0sIHsgYm94U2hhZG93OiAnaW5zZXQgMCAwIDAgOTk5OXB4IHJnYmEoMjU1LDU5LDkyLDApJyB9XSwKICAgICAgeyBkdXJhdGlvbjogMjUwIH0KICAgICk7CiAgfSwKCiAgd2luKGJvc3MpIHsKICAgICQoJyNyZXN1bHQtdGl0bGUnKS50ZXh0Q29udGVudCA9ICdTZXQgQ2xlYXJlZCc7CiAgICAkKCcjcmVzdWx0LWJvZHknKS50ZXh0Q29udGVudCA9IGBZb3UgY2FwdHVyZWQgJHtib3NzLmRpc3BsYXlOYW1lIHx8IGJvc3MubmFtZX0gYW5kIGRyb3ZlIHRoZSBnbG9vbSBvdXQuIEdsb29tIGJhbmtlZDogJHt0aGlzLmdsb29tfS5gOwogICAgJCgnI3Jlc3VsdCcpLmNsYXNzTGlzdC5yZW1vdmUoJ2hpZGRlbicpOwogICAgdGhpcy5ydW5uaW5nID0gZmFsc2U7CiAgfSwKICBsb3NlKCkgewogICAgJCgnI3Jlc3VsdC10aXRsZScpLnRleHRDb250ZW50ID0gJ0JhdHRlcnkgRGVhZCc7CiAgICAkKCcjcmVzdWx0LWJvZHknKS50ZXh0Q29udGVudCA9ICdUaGUgZGV0ZWN0b3Igd2VudCBkYXJrIGFuZCB0aGUgZ2hvc3RzIHNsaXBwZWQgYXdheS4gUmVjaGFyZ2UgYW5kIHRyeSBhZ2Fpbi4nOwogICAgJCgnI3Jlc3VsdCcpLmNsYXNzTGlzdC5yZW1vdmUoJ2hpZGRlbicpOwogICAgdGhpcy5ydW5uaW5nID0gZmFsc2U7CiAgfSwKCiAgc3RvcCgpIHsKICAgIHRoaXMucnVubmluZyA9IGZhbHNlOwogICAgaWYgKHRoaXMucmFmKSBjYW5jZWxBbmltYXRpb25GcmFtZSh0aGlzLnJhZik7CiAgICBpZiAodGhpcy5zdHJlYW0pIHsgdGhpcy5zdHJlYW0uZ2V0VHJhY2tzKCkuZm9yRWFjaCgodCkgPT4gdC5zdG9wKCkpOyB0aGlzLnN0cmVhbSA9IG51bGw7IH0KICAgIGlmICh0aGlzLl9vblJlc2l6ZSkgcmVtb3ZlRXZlbnRMaXN0ZW5lcigncmVzaXplJywgdGhpcy5fb25SZXNpemUpOwogICAgaWYgKHRoaXMuX29uT3JpZW50ICYmIHRoaXMuX2d5cm9BdHRhY2hlZCkgewogICAgICByZW1vdmVFdmVudExpc3RlbmVyKCdkZXZpY2VvcmllbnRhdGlvbicsIHRoaXMuX29uT3JpZW50KTsKICAgICAgdGhpcy5fZ3lyb0F0dGFjaGVkID0gZmFsc2U7CiAgICB9CiAgICBpZiAodGhpcy5fbW90aW9uRWxzKSB7IHRoaXMuX21vdGlvbkVscy5vdmVybGF5LmNsYXNzTGlzdC5hZGQoJ2hpZGRlbicpOyB9CiAgICBpZiAodGhpcy5fYmxhc3RFbHMpIHsKICAgICAgcmVtb3ZlRXZlbnRMaXN0ZW5lcigndG91Y2hlbmQnLCB0aGlzLl9ibGFzdEVscy5lbmRCbGFzdCk7CiAgICAgIHJlbW92ZUV2ZW50TGlzdGVuZXIoJ21vdXNldXAnLCB0aGlzLl9ibGFzdEVscy5lbmRCbGFzdCk7CiAgICB9CiAgICBpZiAodGhpcy5yZW5kZXJlcikgeyB0aGlzLnJlbmRlcmVyLmRpc3Bvc2U/LigpOyB9CiAgICBmb3IgKGNvbnN0IGcgb2YgdGhpcy5naG9zdHMpIHsKICAgICAgaWYgKGcudXNlckRhdGEudmlkRWwpIHsgdHJ5IHsgZy51c2VyRGF0YS52aWRFbC5wYXVzZSgpOyBnLnVzZXJEYXRhLnZpZEVsLnNyYyA9ICcnOyB9IGNhdGNoIHt9IGcudXNlckRhdGEudmlkRWwgPSBudWxsOyB9CiAgICB9CiAgICB0aGlzLmdob3N0cyA9IFtdOyB0aGlzLnRhcmdldCA9IG51bGw7CiAgfSwKfTsKCmZ1bmN0aW9uIHN0YXJ0SHVudChvcHRzKSB7IGh1bnQuc3RhcnQob3B0cyk7IH0KCi8qID09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PQogICBST1NURVIgLyBHSE9TVCBJTkRFWAogICA9PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT0gKi8KYXN5bmMgZnVuY3Rpb24gb3BlblJvc3RlcigpIHsKICBzaG93KCdyb3N0ZXInKTsKICBhd2FpdCBsb2FkUm9zdGVyKCk7Cn0KWycjZmlsdGVyLXR5cGUnLCAnI2ZpbHRlci1yYXJpdHknLCAnI2ZpbHRlci1ib3NzJ10uZm9yRWFjaCgoc2VsKSA9PgogICQoc2VsKS5hZGRFdmVudExpc3RlbmVyKCdjaGFuZ2UnLCBsb2FkUm9zdGVyKQopOwoKYXN5bmMgZnVuY3Rpb24gbG9hZFJvc3RlcigpIHsKICBjb25zdCB0eXBlID0gJCgnI2ZpbHRlci10eXBlJykudmFsdWU7CiAgY29uc3QgcmFyaXR5ID0gJCgnI2ZpbHRlci1yYXJpdHknKS52YWx1ZTsKICBjb25zdCBib3NzID0gJCgnI2ZpbHRlci1ib3NzJykuY2hlY2tlZCA/ICcxJyA6ICcnOwogIGNvbnN0IHBhcmFtcyA9IG5ldyBVUkxTZWFyY2hQYXJhbXMoKTsKICBpZiAodHlwZSkgcGFyYW1zLnNldCgndHlwZScsIHR5cGUpOwogIGlmIChyYXJpdHkpIHBhcmFtcy5zZXQoJ3Jhcml0eScsIHJhcml0eSk7CiAgaWYgKGJvc3MpIHBhcmFtcy5zZXQoJ2Jvc3MnLCBib3NzKTsKICBjb25zdCByZXMgPSBhd2FpdCBmZXRjaChgL2FwaS9naG9zdHM/JHtwYXJhbXN9YCk7CiAgY29uc3QgeyBnaG9zdHMgfSA9IGF3YWl0IHJlcy5qc29uKCk7CiAgY29uc3QgZ3JpZCA9ICQoJyNyb3N0ZXItZ3JpZCcpOwogIGdyaWQuaW5uZXJIVE1MID0gZ2hvc3RzLm1hcChnaG9zdENhcmQpLmpvaW4oJycpIHx8CiAgICAnPHAgY2xhc3M9Im11dGVkIj5ObyBnaG9zdHMgbWF0Y2ggdGhvc2UgZmlsdGVycy48L3A+JzsKfQoKZnVuY3Rpb24gZ2hvc3RDYXJkKGcpIHsKICBjb25zdCBjb2xvclZhciA9IGB2YXIoLS0ke2cudHlwZX0pYDsKICBjb25zdCB0aHVtYiA9IGcuaW1hZ2UKICAgID8gYDxpbWcgY2xhc3M9Imdob3N0LXRodW1iIiBzcmM9IiR7Zy5pbWFnZX0iIGFsdD0iJHtlc2NhcGVIdG1sKGcuZGlzcGxheU5hbWUpfSIgbG9hZGluZz0ibGF6eSI+YAogICAgOiAnJzsKICBjb25zdCBzZXRSZWYgPSBnLnNldE51bWJlciA/IGA8ZGl2IGNsYXNzPSJzZXQtcmVmIj5TZXQgJHtnLnNldE51bWJlcn0gwrcgJHtlc2NhcGVIdG1sKGcuc2V0TmFtZSB8fCAnJyl9PC9kaXY+YCA6ICcnOwogIHJldHVybiBgCiAgICA8ZGl2IGNsYXNzPSJnaG9zdC1jYXJkICR7Zy5pc0Jvc3MgPyAnYm9zcycgOiAnJ30iPgogICAgICA8ZGl2IGNsYXNzPSJhY2NlbnQiIHN0eWxlPSJiYWNrZ3JvdW5kOiR7Y29sb3JWYXJ9Ij48L2Rpdj4KICAgICAgJHt0aHVtYn0KICAgICAgPGRpdiBjbGFzcz0iZ25hbWUiPiR7ZXNjYXBlSHRtbChnLmRpc3BsYXlOYW1lKX0gJHtnLmlzQm9zcyA/ICc8c3BhbiBjbGFzcz0iYm9zcy1iYWRnZSI+Qk9TUzwvc3Bhbj4nIDogJyd9PC9kaXY+CiAgICAgIDxkaXYgY2xhc3M9Im1ldGEiPjxzcGFuIGNsYXNzPSJkb3QgJHtnLnR5cGV9Ij48L3NwYW4+IDxzcGFuIGNsYXNzPSJzdGFycyI+JHsn4piFJy5yZXBlYXQoZy5yYXJpdHkpfTwvc3Bhbj48L2Rpdj4KICAgICAgPGRpdiBjbGFzcz0ic3RhdGxpbmUiPjxzcGFuPkhQICR7Zy5oZWFsdGh9PC9zcGFuPjxzcGFuPkRNRyAke2cuZGFtYWdlfTwvc3Bhbj48L2Rpdj4KICAgICAgPGRpdiBjbGFzcz0ic3RhdGxpbmUiPjxzcGFuPlNQRCAke2cuc3BlZWR9PC9zcGFuPjxzcGFuPlJORyAke2cucmFuZ2V9PC9zcGFuPjwvZGl2PgogICAgICAke2cuYWJpbGl0eSA/IGA8ZGl2IGNsYXNzPSJzZXQtcmVmIiBzdHlsZT0ibWFyZ2luLXRvcDo2cHgiPuKaoSAke2VzY2FwZUh0bWwoZy5hYmlsaXR5KX08L2Rpdj5gIDogJyd9CiAgICAgICR7c2V0UmVmfQogICAgPC9kaXY+YDsKfQoKZnVuY3Rpb24gZXNjYXBlSHRtbChzKSB7CiAgcmV0dXJuIFN0cmluZyhzID8/ICcnKS5yZXBsYWNlKC9bJjw+IiddL2csIChjKSA9PgogICAgKHsgJyYnOiAnJmFtcDsnLCAnPCc6ICcmbHQ7JywgJz4nOiAnJmd0OycsICciJzogJyZxdW90OycsICInIjogJyYjMzk7JyB9W2NdKSk7Cn0K \ No newline at end of file