// Downlink viewer: hex dump of the ship's transmit buffer, with download. import { api } from './api.js'; import { h, clear, toast, download } from './dom.js'; import { hexView } from './hexview.js'; export function downlinkView(ctx) { const ship = ctx.ship; const body = h('div', { class: 'panel-body' }, h('p', { class: 'dim' }, ship ? 'ACQUIRING SIGNAL...' : 'YOU HAVE NO SHIP.')); const meta = h('span', { class: 'dim' }); const dl = h('button', { class: 'btn', type: 'button', disabled: true }, 'DOWNLOAD .BIN'); const refresh = h('button', { class: 'btn', type: 'button', disabled: !ship }, 'REFRESH'); // Earlier downlinks: pick a day, or step with the arrows. `days` is oldest first. const older = h('button', { class: 'btn', type: 'button', disabled: true, 'aria-label': 'Previous downlink' }, '<'); const newer = h('button', { class: 'btn', type: 'button', disabled: true, 'aria-label': 'Next downlink' }, '>'); const pick = h('select', { class: 'daypick', 'aria-label': 'Downlink day', disabled: true }); let days = []; let shown = null; // the day on screen; null = the latest function syncPicker() { clear(pick).append(...days.slice().reverse().map((d, i) => h('option', { value: String(d) }, `DAY ${d}${i === 0 ? ' (LATEST)' : ''}`))); const at = shown == null ? days.length - 1 : days.indexOf(shown); if (at >= 0) pick.value = String(days[at]); pick.disabled = days.length < 2; older.disabled = at <= 0; newer.disabled = at < 0 || at >= days.length - 1; } function go(step) { const at = shown == null ? days.length - 1 : days.indexOf(shown); const to = days[at + step]; if (to !== undefined) fetchIt(to); } older.addEventListener('click', () => go(-1)); newer.addEventListener('click', () => go(1)); pick.addEventListener('change', () => fetchIt(Number(pick.value))); let current = null; async function fetchIt(day) { if (!ship) return; refresh.disabled = true; try { days = await api.downlinkDays(ship.id); // Refreshing (no day given) jumps to the latest; a stale choice falls back to it too. shown = day != null && days.includes(day) ? day : null; const got = await api.downlink(ship.id, shown); const bytes = got.bytes; shown = got.day === days[days.length - 1] ? null : got.day; syncPicker(); current = { bytes, day: got.day }; const allZero = bytes.every((b) => b === 0); meta.textContent = `SHIP #${ship.id} // FROM THE RUN OF DAY ${got.day} // ${bytes.length} BYTES`; dl.disabled = false; clear(body).append( ...(allZero ? [h('p', { class: 'warn' }, 'BUFFER IS ALL ZEROS: THE SHIP HAS NOT WRITTEN ANYTHING TO ITS DOWNLINK BUFFER.')] : []), hexView(bytes).el, h('p', { class: 'hint' }, 'OFFSETS ARE INTO THE 1 KB DOWNLINK BUFFER (SHIP RAM 1024-2047). MULTI-BYTE VALUES ARE LITTLE-ENDIAN.'), ); } catch (e) { current = null; days = []; shown = null; syncPicker(); dl.disabled = true; meta.textContent = ''; clear(body).append( e.status === 404 ? h('div', { class: 'nosignal' }, h('p', { class: 'big' }, 'NO SIGNAL'), h('p', { class: 'dim' }, 'THIS SHIP HAS NOT TRANSMITTED YET. A SHIP\'S FIRST DOWNLINK ARRIVES AFTER ITS FIRST DAY IN THE BELT.')) : h('p', { class: 'bad' }, `ERROR: ${e.message}`), ); } finally { refresh.disabled = false; } } // Reloading the account too keeps the ship's status and the day current. refresh.addEventListener('click', () => ctx.refresh().catch((e) => toast(e.message, 'err'))); dl.addEventListener('click', () => { if (!current) return; download(`ship-${ship.id}-day-${current.day}-downlink.bin`, current.bytes); toast(`SAVED ${current.bytes.length} BYTES`); }); fetchIt(); return h('section', { class: 'panel' }, h('div', { class: 'panel-title' }, 'DOWNLINK RECEIVER', h('span', { class: 'spacer' }), meta), h('div', { class: 'toolbar pad' }, refresh, older, pick, newer, h('span', { class: 'spacer' }), dl), body, ); }