Files

258 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* ihx2wmf.js — Конвертер SDCC .ihx (Intel HEX) + патч заголовка WC → .wmf
*
* Использование:
* node scripts/ihx2wmf.js <input.ihx> <output.wmf> [--extra <data.bin>]
*
* Алгоритм:
* 1. Парсить Intel HEX, получить бинарные данные (относительные адреса).
* 2. Определить реальный размер кода (до последнего не-нулевого байта).
* 3. Вычислить PAGES = ceil(code_size / 16384).
* 4. Вычислить BLKSIZE = ceil(code_size / 512).
* 5. Подготовить заголовок плагина (512 байт) с правильными PAGES/BLKSIZE.
* 6. Если задан --extra: добавить доп. страницы данных (LUT таблицы и т.п.).
* 7. Записать: [header 512 байт][код][extra data pages].
*
* Формат WMF-заголовка (Wild Commander Plugin):
* +0 16 байт: зарезервировано
* +16 16 байт: "WildCommanderMDL"
* +32 1 байт: версия системы плагинов (#10)
* +33 1 байт: зарезервировано
* +34 1 байт: число 16KB страниц (PAGES)
* +35 1 байт: начальная страница (0)
* +36 12 байт: 6 × 2 байта блоков данных (blockN: [page, blkcount])
* +48 15 байт: зарезервировано
* +63 1 байт: флаги (0 = активировать по Enter и F3)
* +64 96 байт: расширения (32 × 3 байта, "VGM" + нули)
* +160 1 байт: маркер конца расширений (0)
* +161 4 байта: макс. размер файла (0xFFFFFFFF = без ограничений)
* +165 32 байта: имя плагина (дополнить пробелами)
* +197 1 байт: тип активации (0 = по расширению)
* +198 6 байт: текст кнопок F2/F4
* +204 24 байта: текст меню
* +228 32 байта: зарезервировано
* +260 1 байт: флаги INI
* +261 ... паддинг до 512 байт
*/
'use strict';
const fs = require('fs');
const path = require('path');
/* ── Константы заголовка ─────────────────────────────────────────────── */
const HEADER_SIZE = 512;
const PLUGIN_ORG = 0x8000; /* виртуальный адрес загрузки WC */
const PLUGIN_NAME = 'VGM Player OPL3 v2.0 SDCC '; /* 32 байта */
/* ── Парсер Intel HEX ────────────────────────────────────────────────── */
function parseIHX(src) {
const buf = Buffer.alloc(0x10000, 0x00);
let minA = 0x10000;
let maxA = 0;
for (const rawLine of src.split('\n')) {
const line = rawLine.trim();
if (!line.startsWith(':')) continue;
const byteCount = parseInt(line.slice(1, 3), 16);
const address = parseInt(line.slice(3, 7), 16);
const recordType = parseInt(line.slice(7, 9), 16);
if (recordType === 0x00) { /* Data */
for (let i = 0; i < byteCount; i++) {
const b = parseInt(line.slice(9 + i * 2, 11 + i * 2), 16);
const a = address + i;
buf[a] = b;
if (a < minA) minA = a;
if (a > maxA) maxA = a;
}
}
/* recordType 01 = EOF, игнорируем остальное */
}
return { buf, minAddr: minA, maxAddr: maxA };
}
/* ── Построить заголовок WC ──────────────────────────────────────────── */
/**
* @param {number} totalPages — общее число 16KB страниц
* @param {Array<{page: number, blocks: number}>} blockEntries — блоки
*/
function buildHeader(totalPages, blockEntries) {
const hdr = Buffer.alloc(HEADER_SIZE, 0x00);
/* +16: сигнатура */
Buffer.from('WildCommanderMDL').copy(hdr, 16);
/* +32: версия системы плагинов */
hdr[32] = 0x10;
/* +34: число страниц */
hdr[34] = totalPages;
/* +35: начальная страница */
hdr[35] = 0;
/* +36: block table — до 6 записей × 2 байта */
for (let i = 0; i < Math.min(blockEntries.length, 6); i++) {
hdr[36 + i * 2] = blockEntries[i].page;
hdr[36 + i * 2 + 1] = blockEntries[i].blocks;
}
/* +63: флаги */
hdr[63] = 0;
/* +64: расширения: "VGM", "VGZ" */
hdr[64] = 0x56; hdr[65] = 0x47; hdr[66] = 0x4D; /* V G M */
hdr[67] = 0x56; hdr[68] = 0x47; hdr[69] = 0x5A; /* V G Z */
/* остальные 30 × 3 = 90 нулей уже есть */
/* +160: маркер конца */
hdr[160] = 0;
/* +161: макс. размер файла = 0xFFFFFFFF */
hdr[161] = 0xFF; hdr[162] = 0xFF; hdr[163] = 0xFF; hdr[164] = 0xFF;
/* +165: имя плагина (32 байта, пробелами) */
const name = PLUGIN_NAME.slice(0, 32).padEnd(32, ' ');
Buffer.from(name, 'ascii').copy(hdr, 165);
/* +197: тип активации */
hdr[197] = 0;
return hdr;
}
/* ── Основная логика ─────────────────────────────────────────────────── */
function main() {
const args = process.argv.slice(2);
let inFile = null, outFile = null, extraFile = null;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--extra' && args[i + 1]) {
extraFile = args[++i];
} else if (!inFile) {
inFile = args[i];
} else if (!outFile) {
outFile = args[i];
}
}
if (!inFile || !outFile) {
console.error('Usage: node ihx2wmf.js <input.ihx> <output.wmf> [--extra <data.bin>]');
process.exit(1);
}
const src = fs.readFileSync(inFile, 'utf8');
const { buf, minAddr, maxAddr } = parseIHX(src);
if (minAddr > maxAddr) {
console.error('ERROR: empty IHX');
process.exit(1);
}
/* Код начинается с PLUGIN_ORG = 0x8000 */
if (minAddr < PLUGIN_ORG) {
console.error(`ERROR: code starts at 0x${minAddr.toString(16)}, expected >= 0x${PLUGIN_ORG.toString(16)}`);
process.exit(1);
}
const codeSize = maxAddr - PLUGIN_ORG + 1;
const codePages = Math.max(1, Math.ceil(codeSize / 16384));
const codeBlocks = Math.max(1, Math.ceil(codeSize / 512));
console.log(`Code: 0x${PLUGIN_ORG.toString(16)} - 0x${maxAddr.toString(16)} (${codeSize} bytes)`);
console.log(`Code pages: ${codePages}, Code blocks: ${codeBlocks}`);
/* Блоки: каждая 16KB-страница = один block entry (max 32 секторов).
*
* WC читает WMF последовательно и загружает каждый block в свою страницу.
* Максимум 32 сектора на запись (= 16384 байт = 1 страница).
* Если код занимает >1 страницы — разбиваем на отдельные block entry.
*
* При наличии extra: код паддится до границы 16KB перед extra,
* поэтому последняя страница кода всегда = 32 сектора (кроме случая
* когда это единственная страница и extra нет).
*/
let extraBuf = null;
if (extraFile) {
extraBuf = fs.readFileSync(extraFile);
}
/* ── Block entries для кода: по одной на страницу ────────────── */
const blockEntries = [];
for (let p = 0; p < codePages; p++) {
if (p < codePages - 1) {
/* Промежуточные страницы всегда полные */
blockEntries.push({ page: p, blocks: 32 });
} else {
/* Последняя (или единственная) страница кода */
if (extraBuf) {
/* С extra: код паддится до 16KB → полная страница */
blockEntries.push({ page: p, blocks: 32 });
} else {
/* Без extra: реальный размер */
const pageStart = p * 16384;
const remaining = codeSize - pageStart;
blockEntries.push({ page: p, blocks: Math.ceil(remaining / 512) });
}
}
}
/* ── Block entries для extra data: по одной на страницу ──────── */
if (extraBuf) {
const extraPages = Math.ceil(extraBuf.length / 16384);
console.log(`Extra: ${extraFile} (${extraBuf.length} bytes, ${extraPages} pages)`);
for (let p = 0; p < extraPages; p++) {
const pageStart = p * 16384;
const remaining = extraBuf.length - pageStart;
const pageSize = Math.min(remaining, 16384);
const blk = Math.ceil(pageSize / 512);
blockEntries.push({ page: codePages + p, blocks: blk });
}
}
const totalPages = blockEntries.reduce((sum, e) => Math.max(sum, e.page + 1), 0);
if (blockEntries.length > 6) {
console.error(`ERROR: too many block entries (${blockEntries.length}, max 6)`);
process.exit(1);
}
const hdr = buildHeader(totalPages, blockEntries);
const codeSlice = buf.slice(PLUGIN_ORG, PLUGIN_ORG + codeSize);
/* Итоговый файл: [512 header][код][padding до 16KB boundary][extra pages] */
const parts = [hdr, codeSlice];
if (extraBuf) {
/* Паддинг кода до границы 16KB-страницы
* (WC загружает каждый блок в свою страницу) */
const codePadded = codePages * 16384;
const pad = codePadded - codeSize;
if (pad > 0) {
parts.push(Buffer.alloc(pad, 0x00));
}
parts.push(extraBuf);
/* Паддинг extra до конца последней страницы */
const extraPadded = Math.ceil(extraBuf.length / 16384) * 16384;
const extraPad = extraPadded - extraBuf.length;
if (extraPad > 0) {
parts.push(Buffer.alloc(extraPad, 0x00));
}
}
const out = Buffer.concat(parts);
fs.writeFileSync(outFile, out);
console.log(`Pages: ${totalPages}, Block entries: ${blockEntries.length}`);
for (let i = 0; i < blockEntries.length; i++) {
console.log(` block[${i}]: page=${blockEntries[i].page}, sectors=${blockEntries[i].blocks}`);
}
console.log(`Written: ${outFile} (${out.length} bytes)`);
}
main();