Windows 98 на Windows 11 в 2026 году: пошаговый гайд по 86Box (и как отдать установку ИИ‑агенту)

Старые программы конца 90-х на Windows 11 то зависают до нажатия клавиши, то молчат, то не запускаются вовсе. Песочница Windows тут не помогает, VirtualBox с Windows 98 дружит плохо. Рабочим вариантом оказался эмулятор 86Box: за полтора‑два часа он даёт настоящую Windows 98 SE в окне, со звуком, мышью и CD‑приводом. Руками из этого времени — минут 20, остальное ожидание.
Ниже — пошаговый гайд, который я проверил на живой установке 21.10.2026 (Windows 11 Pro, 86Box 6.0 build 9001, Windows 98 SE English 4.10.2222A), со всеми граблями, на которые наступили. В конце — бонус: инструкция для ИИ‑агента, по которой Claude Code или похожий агент проходит всю установку сам.
Зачем это вообще нужно
Многие программы 1995–2000 годов на Windows 10/11 не запускаются или работают с ошибками. Чаще всего им нужен 16-битный код, а 64-битная Windows не умеет выполнять его в принципе. В Windows 98 они работают как задумано.
Почему 86Box, а не что‑то привычное:
Песочница Windows — это та же Windows 11, только одноразовая. Ограничения у неё те же, что у основной системы.
VirtualBox запускает Windows 98 плохо: на современных процессорах бывают сбои, драйверов под неё почти нет. К тому же рядом со встроенной виртуализацией Windows 11 он работает медленнее.
86Box не виртуализирует, а полностью эмулирует ПК конца 90-х: материнскую плату, Pentium, видеокарту S3, Sound Blaster 16. Windows 98 видит знакомое железо и сама ставит драйверы. Эмулятор бесплатный, с открытым кодом и работает из одной папки без установки.
Что понадобится
Что | Зачем | Где взять | Размер |
|---|---|---|---|
86Box 6.0 для Windows (64-bit) | Эмулятор старого ПК | 60 МБ | |
ROM‑файлы 86Box той же версии | Прошивки BIOS плат и видеокарт | 75 МБ | |
Образ Windows 98 SE (ISO) | Установочный диск | Ваш диск или образ (см. ниже) | 450–660 МБ |
Ключ продукта | Вводится при установке | Наклейка на коробке или конверте диска | 25 символов |
Свободное место | Программы, образы, виртуальный диск | — | около 4 ГБ |
Про лицензию. Windows 98 до сих пор принадлежит Microsoft, официально её не продают и не раздают. Легальный путь — свой старый диск с ключом: коробочные Windows 98 SE недорого находятся на Авито и eBay, а ISO с диска снимает любая программа для записи дисков. Образы из архивов в интернете лицензии не дают. Но можно не заморачиваться и зайти на http://old‑dos.ru/ — там есть все необходимое.
Какой образ подойдёт. Копия оригинального диска Windows 98 SE (4.10.2222) или первой Windows 98 (4.10.1998) размером 450–660 МБ; язык неважен. Файлы на 60–200 МБ — дискетные или урезанные варианты, они не подойдут. Сборки с пометками «ключ вшит» или «с дополнениями» лучше обходить: такой образ кто‑то изменил, и неизвестно как.
Шаг 1. Скачиваем 86Box и ROM‑файлы
86Box не устанавливается: два архива распаковываются в одну папку, в систему и реестр ничего не пишется. Удалить всё — значит стереть папку.
Создайте папку проекта, например
C:\Users\<вы>\Desktop\win98.Скачайте с последнего релиза 86Box архив
86Box-Windows-64-b….zip(для 6.0 —86Box-Windows-64-b9001.zip) и распакуйте вwin98\86box\.В релизах ROM‑файлов скачайте архив той же версии (для 6.0 — тег
v6.0, «Source code (zip)»). Теги сpreиbetaне берите.Папку
roms-6.0из архива переименуйте вromsи положите рядом с86Box.exe. Внутри должны бытьmachines,video,soundи другие.
Итоговая структура (vm и iso появятся дальше):
win98\
├─ 86box\
│ ├─ 86Box.exe
│ └─ roms\ ← machines, video, sound, …
├─ vm\win98\ ← настройки и диск виртуальной машины
└─ iso\ ← образы дисков и дискетПроверка (по желанию). Сравните SHA-256 архива с указанной на странице релиза:
Get-FileHash .\86Box-Windows-64-b9001.zip -Algorithm SHA256Для 6.0 должно получиться B86722ED547089F095E8875805411F83C76AAFD7D318F67286A5A69AECDD7644. Сам 86Box.exe подписан ведущим разработчиком (Miran Grča): «Свойства» → «Цифровые подписи». Если при первом запуске появится SmartScreen — «Подробнее» → «Выполнить в любом случае», это обычное дело для редко скачиваемых программ.
Шаг 2. Собираем виртуальный компьютер
Собираем типичный домашний ПК 1997 года — всё это железо Windows 98 знает сама.
Компонент | Что выбрать | Почему |
|---|---|---|
Материнская плата | [i430HX] ASUS P/I‑P55T2P4 | Надёжная плата под Pentium, BIOS грузится с CD |
Процессор | Intel Pentium MMX, 200 МГц | Быстро для Windows 98 и тянется на 100% скорости |
Память | 64 МБ | Больше 512 МБ Windows 98 не любит |
Видеокарта | S3 ViRGE (Diamond Stealth 3D 2000) PCI | Драйвер есть на диске Windows 98 |
Звуковая карта | Sound Blaster 16 PnP | Определяется сама, драйвер встроен |
Мышь | PS/2 | — |
Жёсткий диск | IDE, 2 ГБ (63/16/4161), Primary Master | Хватает с запасом |
CD‑привод | ATAPI, Secondary Master, 24 x | — |
Дисковод | 3,5" 1,44 МБ | Нужен для загрузочной дискеты |
Сеть | Нет | Интернет Windows 98 не нужен и небезопасен |
Проще всего не прокликивать это в настройках, а положить готовый конфиг. Создайте папку win98\vm\win98\ и в ней файл 86box.cfg:
[General]
vid_renderer = qt_software
[Machine]
machine = p55t2p4
cpu_family = pentium_p55c
cpu_speed = 200000000
cpu_multi = 3
cpu_use_dynarec = 1
mem_size = 65536
[Video]
gfxcard = virge_pci
[S3 ViRGE PCI]
bios = stealth3d_2000_pci
[Input devices]
mouse_type = ps2
[Sound]
sndcard = sb16_pnp
[Hard disks]
hdd_01_parameters = 63, 16, 4161, 0, ide
hdd_01_fn = win98.img
hdd_01_ide_channel = 0:0
[Floppy and CD-ROM drives]
fdd_01_type = 35_2hd
fdd_02_type = none
fdd_01_fn = ../../iso/win98se_boot_nojo.img
cdrom_01_parameters = 1, atapi
cdrom_01_ide_channel = 1:0
cdrom_01_speed = 24
cdrom_01_image_path = ../../iso/win98se.isoПути относительные, так что папку win98 можно переносить целиком.
Пустой жёсткий диск — это файл из нулей ровно на 63 × 16 × 4161 × 512 = 2 147 475 456 байт. Создайте его в PowerShell из папки vm\win98:
fsutil file createnew win98.img 2147475456Или в самом 86Box: «Инструменты» → «Настройки» → «Жёсткие диски» → «Новый…».
Запуск. Создайте в папке win98 файл start_win98.bat:
@echo off
start "" "%~dp086box\86Box.exe" -P "%~dp0vm\win98"Ключ -P говорит 86Box, где лежат настройки машины. При первом запуске он допишет в конфиг служебные строки — это нормально. Важно: правьте 86box.cfg только при закрытом 86Box, при выходе он перезаписывает файл.
Шаг 3. Самое неочевидное: загрузочная дискета без JO.SYS
Установочный диск Windows 98 SE в 86Box зависает при загрузке. Внутри диска записана обычная загрузочная дискета Windows 98, а перед ней стоит маленький загрузчик JO.SYS с меню «Boot from Hard Disk / Boot from CD‑ROM». Пункт «Boot from CD‑ROM» в 86Box висит на чёрном экране с курсором — я ждал больше минуты, без результата.

Решение: достаём эту дискету из образа и удаляем из неё JO.SYS. Без него дискета сразу грузит DOS с меню установки, а файлы читает с CD через свои драйверы.
Положите образ в
win98\iso\под именемwin98se.iso. Файл с ключом (частоSerial.txt) сохраните.Сохраните скрипт ниже как
make_bootdisk.ps1в папкуwin98и запустите:
powershell -ExecutionPolicy Bypass -File .\make_bootdisk.ps1 -Iso .\iso\win98se.iso -Out .\iso\win98se_boot_nojo.imgparam([string]$Iso, [string]$Out)
$fs = [IO.File]::OpenRead((Resolve-Path $Iso)); $b = New-Object byte[] 2048
# Сектор 17 — запись El Torito, в ней ссылка на каталог загрузки
$fs.Position = 17 * 2048; [void]$fs.Read($b, 0, 2048)
if ([Text.Encoding]::ASCII.GetString($b, 7, 23) -ne 'EL TORITO SPECIFICATION') { throw 'Образ не загрузочный' }
$fs.Position = [int64][BitConverter]::ToUInt32($b, 0x47) * 2048; [void]$fs.Read($b, 0, 2048)
if ($b[33] -ne 2) { throw 'Загрузочная запись не является дискетой 1,44 МБ' }
$img = New-Object byte[] 1474560
$fs.Position = [int64][BitConverter]::ToUInt32($b, 40) * 2048; [void]$fs.Read($img, 0, $img.Length); $fs.Close()
# Корневой каталог FAT12: после зарезервированных секторов и таблиц FAT
$bps = [BitConverter]::ToUInt16($img, 11); $res = [BitConverter]::ToUInt16($img, 14)
$root = ($res + $img[16] * [BitConverter]::ToUInt16($img, 22)) * $bps; $entries = [BitConverter]::ToUInt16($img, 17)
for ($i = 0; $i -lt $entries; $i++) { $o = $root + 32 * $i
if ([Text.Encoding]::ASCII.GetString($img, $o, 11) -eq 'JO SYS') { $img[$o] = 0xE5; 'JO.SYS удалён' } }
[IO.File]::WriteAllBytes((Join-Path (Get-Location) $Out), $img); "Готово: $Out"Как это работает: в 17-м секторе ISO лежит запись El Torito со ссылкой на каталог загрузки, оттуда берём смещение образа дискеты 1,44 МБ, копируем его и в корневом каталоге FAT12 помечаем запись JO.SYS удалённой (байт 0xE5). Сам ISO скрипт не меняет. Работает с оригинальными дисками Windows 98 и 98 SE.
Альтернатива без скрипта — готовый образ «Windows 98 SE Boot Disk» (.img, 1,44 МБ), положенный как iso\win98se_boot_nojo.img. Этот вариант мы не проверяли, но такие дискеты устроены так же, только без JO.SYS.

Шаг 4. Первый запуск и BIOS
BIOS настраивается один раз: дискета должна стоять первой в порядке загрузки. Иначе после разметки диска машина попробует загрузиться с пустого винчестера и остановится.
Два правила работы с окном 86Box: клавиатура работает в машине, когда окно активно (раскладка внутри всегда английская), а чтобы мышь заработала внутри, щёлкните по экрану машины. Вернуть курсор в Windows 11 — Ctrl+End или средняя кнопка мыши.
Запустите
start_win98.bat. Появится POST: Pentium‑MMX 200 МГц, 65 536 КБ, Creative SB16 PnP, диск и CD. «CMOS checksum error — Defaults loaded» при первом запуске — норма.

Пока идёт проверка памяти, несколько раз нажмите Delete — откроется CMOS Setup Utility. Не успели — «Действие» → «Жёсткий сброс».

Выберите BIOS FEATURES SETUP, спуститесь на Boot Sequence и нажимайте Page Down, пока не появится A,CDROM,C. Теперь при вставленной дискете машина грузится с неё, без дискеты — с жёсткого диска.

Esc, F10, Y, Enter — настройки сохранятся, машина загрузится с дискеты.
Настройки BIOS лежат в vm\win98\nvr. Чтобы они не терялись, закрывайте 86Box крестиком с подтверждением, а не через диспетчер задач.
Шаг 5. Текстовая часть установки
Установщик сам размечает и форматирует диск, FDISK вручную не нужен. Около 10 минут, из них 5 — форматирование. Всё происходит внутри win98.img, реальный диск не затрагивается.
В Startup Menu выберите 1. Start Windows 98 Setup from CD‑ROM (через 10 секунд выберется сам). Строки Adaptec ASPI Manager — это грузятся драйверы CD.
Welcome to Setup → Enter.
Configure unallocated disk space (recommended) → Enter.
На вопрос про large disk support стрелкой вниз выберите Yes, enable large disk support — будет FAT32. Внимание: по умолчанию выделено «No».
Запрос на перезагрузку с дискетой в A: — дискета уже вставлена, Enter.
Снова Startup Menu → 1. Пойдёт форматирование C: (около 5 минут).
«Setup is now going to perform a routine check on your system» → Enter. ScanDisk за полминуты, затем графическая часть.
Шаг 6. Мастер установки
Везде подходят варианты по умолчанию, всё делается клавишей Enter.
Экран | Что сделать |
|---|---|
Welcome to Windows 98 Setup | Continue |
Select Directory | Оставить |
Setup Options | Typical, Next |
Windows Components | Install the most common components, Next |
Identification | Имя компьютера латиницей до 15 символов (например, |
Establishing Your Location | Любая страна, Next |
Start Copying Files | Next |
Страна влияет только на региональные ссылки в Internet Explorer. Если предложат создать Startup Disk — Cancel, дискета у вас уже есть. Дальше примерно 20 минут копирования со слайд‑шоу.
Пока идёт копирование, извлеките дискету: «Носители» → «Дисковод 1 (3.5» 1.44M)…» → «Извлечь». Если её оставить, после перезагрузки меню само запустит установку заново. Ничего не сломается, но придётся извлекать и перезагружаться ещё раз.
И не пытайтесь «испортить» файл дискеты, пока он вставлен: 86Box держит её в памяти и изменений файла не заметит. Мы проверили.
Шаг 7. Первая загрузка
После копирования машина сама перезагрузится в Windows 98 с жёсткого диска. Если вместо заставки «Getting ready to run Windows for the first time» снова появилось чёрное Startup Menu — вы забыли извлечь дискету.
User Information — любое имя, например
User.License Agreement — I accept the Agreement (Alt+A).
Windows Product Key — 25 символов без дефисов, курсор сам перескакивает. Ключ должен подходить к диску: полная версия, Upgrade и OEM не всегда взаимозаменяемы.
Start Wizard → Finish.
Hardware Detection — около 5 минут. Предупреждение про «may cause your computer to stop responding» пишется всегда; у нас ничего не зависло. Ничего не нажимайте.
Date/Time Properties — время уже взято с хоста, выберите часовой пояс и Close.
«Updating System Settings» — ещё около 5 минут и перезагрузка.
Enter Windows Password — оставьте пустым, OK. Больше пароль спрашивать не будут.
Add New Hardware Wizard для Plug and Play Monitor: Next → «Search for the best driver» → снимите галочку «Floppy disk drives» → драйвер найдётся в
C:\WINDOWS\INF\MONITOR.INF→ Finish.
Появится рабочий стол и окно Welcome to Windows 98!

Установка завершена. Выключайте машину так же, как настоящий компьютер тех лет: Start → Shut Down → Shut down → OK, дождитесь надписи «It's now safe to turn off your computer» и только потом закройте окно 86Box. Если закрыть окно с работающей Windows, при следующем запуске она устроит проверку диска ScanDisk.
После установки: перенос файлов и запись экрана
На этом Windows 98 установлена, и гайд, по сути, закончен. Дальше — не обязательные шаги, а мой опыт. Я переносил в машину старую запись экрана конца 90-х и записывали её воспроизведение в MP4, чтобы смотреть уже на современном компьютере. По дороге набралось несколько неочевидных вещей — возможно, они пригодятся и вам.
Как передать файлы внутрь
Общих папок у 86Box нет, и для безопасности это даже хорошо: старая система не видит ваших файлов. Проще всего передавать их через образ диска: собрать ISO из папки и «вставить» его в виртуальный привод. Мы собирали образ встроенным в Windows IMAPI2, без сторонних программ:
powershell -ExecutionPolicy Bypass -File .\make_iso.ps1 -Folder .\cd_files -Out .\iso\my_files.iso -Label MY_FILESparam([string]$Folder, [string]$Out, [string]$Label = 'FILES')
Add-Type -TypeDefinition @'
using System; using System.IO; using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes;
public static class IsoWriter { public static void Save(object stream, string path) {
IStream s = (IStream)stream; byte[] buf = new byte[65536]; IntPtr pRead = Marshal.AllocHGlobal(4);
using (FileStream f = File.Create(path)) { while (true) { s.Read(buf, buf.Length, pRead); int n = Marshal.ReadInt32(pRead); if (n <= 0) break; f.Write(buf, 0, n); } }
Marshal.FreeHGlobal(pRead); } }
'@
$fs = New-Object -ComObject IMAPI2FS.MsftFileSystemImage
$fs.FileSystemsToCreate = 3 # ISO 9660 + Joliet (длинные имена файлов)
$fs.VolumeName = $Label
$fs.Root.AddTree((Resolve-Path $Folder).Path, $false)
[IsoWriter]::Save($fs.CreateResultImage().ImageStream, (Join-Path (Get-Location) $Out))
"Готово: $Out"Подойдёт и AnyBurn или ImgBurn. Вставить диск можно на ходу («Носители» → «CD‑ROM 1 (ATAPI)…» → «Образ…») или при закрытом 86Box, поменяв в конфиге cdrom_01_image_path = ../../iso/my_files.iso. Внутри Windows 98 диск будет под буквой D:.
Как записать экран в MP4
Я записывал встроенным в Windows 11 Xbox Game Bar: он пишет окно 86Box вместе со звуком приложения. 22 минуты записи у нас заняли 164 МБ.
Щёлкните по заголовку окна 86Box и нажмите Win+Alt+R — появится индикатор с таймером.
Запустите видео или программу внутри Windows 98 и не сворачивайте окно до конца записи. Если картинка мелкая, перед записью увеличьте окно: «Вид» → масштаб ×2.
Остановите запись кнопкой ■ на индикаторе. Файл появится в
C:\Users\<вы>\Videos\Captures, лишнее в начале и конце можно обрезать в Clipchamp.
Нюансы Game Bar: пока идёт запись, в Captures лежит файл на пару сотен килобайт — видео дописывается целиком только при остановке, так что ориентируйтесь на таймер. Win+Alt+R — переключатель: «нажать ещё раз на всякий случай» начнёт новую запись (у нас так появились файлы на 4 и 18 секунд). Перечёркнутый микрофон значит только, что не пишется ваш микрофон, звук 86Box пишется.
С чем мы столкнулись по дороге
Экран почернел посреди записи. По умолчанию Windows 98 гасит монитор через 15 минут без движения мыши, и воспроизведение видео ей не мешает: в строке состояния 86Box появляется «Монитор в спящем режиме», а в записи — чёрный экран. Перед долгой записью отключите это: Start → Run… → control powercfg.cpl → Turn off monitor = Never. Заставку на всякий случай проверьте в control desk.cpl,,1 — там должно стоять (None).
Работает ли звук в самой Windows 98, проще всего проверить командой в Start → Run…: sndrec32 /play /close c:\windows\media\tada.wav.
Разрешение экрана меняется в control desk.cpl,,3, но мы оставили 640×480: старые программы и видео тех лет рассчитаны именно на него.
Грабли, на которые мы наступили
Симптом | Причина | Решение |
|---|---|---|
После «Boot from CD‑ROM» чёрный экран с курсором |
| Грузиться с дискеты без |
DISK BOOT FAILURE, INSERT SYSTEM DISK | Дискета не вставлена или BIOS её не пробует | Проверить |
Не удаётся попасть в BIOS | Delete нажата поздно или окно не активно | Щёлкнуть по заголовку 86Box, жёсткий сброс, сразу жать Delete |
Установка начинается заново после копирования | В приводе осталась дискета | «Носители» → «Дисковод 1» → «Извлечь», жёсткий сброс |
Изменения в | Правили при запущенном 86Box | Править только при закрытом |
Экран чёрный, «Монитор в спящем режиме» | Windows 98 погасила монитор | Shift в окне; насовсем — Turn off monitor = Never |
Играет музыка, хотя ничего не запущено | Открыто окно Welcome to Windows 98 | Закрыть и снять галочку |
Вторая копия программы падает с illegal operation | Две копии одной старой программы мешают друг другу | Закрыть первую копию или перезагрузить |
В Run не печатаются русские буквы | В английской Windows 98 нет русской раскладки | Пути латиницей; раскладка — Control Panel → Keyboard |
Мышь «застряла» в окне | Курсор захвачен машиной | Ctrl+End или средняя кнопка |
Бонус: пусть установку сделает ИИ‑агент
Всю эту установку можно отдать ИИ‑агенту с доступом к PowerShell, например Claude Code. Для этого у меня есть один самодостаточный файл AGENT_ALL_IN_ONE.md: в нём сценарий экран за экраном, конфиг машины, батник и вспомогательные скрипты (снимок только окна 86Box через PrintWindow, отправка нажатий с проверкой фокуса, клики, корректное закрытие VM, запуск записи Game Bar). Человеку остаётся положить свой ISO, дать ключ и пару раз подтвердить действия.
В сценарий заложены правила безопасности для агента: сам Windows 98 он не скачивает, снимает только окно эмулятора (а не весь экран с вашими переписками), не жмёт клавиши, если окно 86Box не в фокусе, и предупреждает перед тем, как взять управление мышью и клавиатурой.
Сохраните текст ниже в формате md и дайте агенту. Я бы его приложил к статье файлом, но тут так нельзя, поэтому вот:
AGENT_ALL_IN_ONE.md — инструкция для ИИ‑агента (целиком)
# Инструкция для ИИ-агента: Windows 98 SE в 86Box на Windows 11
Этот файл — сценарий для ИИ-агента с доступом к PowerShell на компьютере пользователя (например, Claude Code). По нему агент сам ставит Windows 98 SE в эмулятор 86Box, запускает в ней старую программу и записывает экран. Человеку остаётся скачать образ Windows 98 и пару раз подтвердить действия.
Всё здесь проверено на практике 21.10.2026: Windows 11 Pro 26200, PowerShell 5.1, 86Box 6.0 (build 9001), Windows 98 SE English (4.10.2222A). Тексты экранов и сочетания клавиш взяты из этой установки.
**Файл самодостаточный.** Все вспомогательные скрипты, конфигурация машины и батник лежат в **приложении А** в конце. Первым делом создай папку проекта и вынь их оттуда одной командой (раздел А.0). Других файлов, кроме этого `.md`, не нужно.
---
## 0. Правила для агента (обязательно)
1. **Перед любым перехватом клавиатуры или мыши предупреди пользователя** одной строкой: «Беру управление примерно на N минут, не трогайте мышь и клавиатуру». Когда закончишь, скажи «Готово, компьютер ваш». Нажатия уходят в активное окно: если пользователь работает в другой программе, нажатия попадут туда.
2. **Сам Windows 98 не скачивай.** Это проприетарная ОС, её копии в интернете распространяются без разрешения Microsoft. Попроси пользователя положить ISO в `iso\` (его собственный диск или образ, который он сам нашёл). Ключ продукта тоже должен дать пользователь.
3. **Снимай только окно 86Box** (`snap.ps1`, через PrintWindow). Никогда не снимай весь экран: на нём личные переписки и документы пользователя.
4. **Не отправляй нажатия без проверки фокуса.** `keys.ps1` сам отказывается работать, если окно 86Box не активно (`WARN: 86Box window is not in focus`). Не обходи это ключом `-NoFocus`. Попроси пользователя щёлкнуть по заголовку окна 86Box или получи разрешение и щёлкни сам через `click.ps1 -X 300 -Y 15` (заголовок окна).
5. **После каждого действия делай снимок и смотри на него**, прежде чем жать дальше. Не отправляй длинные цепочки нажатий вслепую.
6. **Долгие ожидания запускай в фоне** (Start-Sleep N; затем snap.ps1). Не блокируй сессию на минуты.
7. **Ничего не удаляй вне папки проекта** без явного разрешения. В том числе файлы в `Видео\Captures`.
8. Разговаривай с пользователем на его языке, коротко сообщай, на каком ты этапе.
---
## 1. Подготовка папки проекта
Структура (корень — любой, например `C:\Users\<user>\Desktop\win98`):
```
<root>\
├─ 86box\ 86Box.exe + roms\
├─ vm\win98\ 86box.cfg, win98.img, nvr\
├─ iso\ win98se.iso, win98se_boot_nojo.img, образы с файлами
├─ scripts\ скрипты из приложения А
├─ shots\ снимки окна, которые делает агент
└─ start_win98.bat
```
### 1.1. 86Box и ROM-файлы
```powershell
$root = "<root>"; $ProgressPreference = 'SilentlyContinue'
$r = Invoke-RestMethod "https://api.github.com/repos/86Box/86Box/releases/latest" -Headers @{ 'User-Agent'='ps' }
$a = $r.assets | ? { $_.name -like '86Box-Windows-64-*.zip' } | Select-Object -First 1
New-Item -ItemType Directory -Force "$root\_dl" | Out-Null
Invoke-WebRequest $a.browser_download_url -OutFile "$root\_dl\$($a.name)" -UseBasicParsing
# Сверить SHA-256 с полем digest релиза:
(Get-FileHash "$root\_dl\$($a.name)").Hash.ToLower() -eq $a.digest.Replace('sha256:','')
Expand-Archive "$root\_dl\$($a.name)" "$root\86box" -Force
# ROM-файлы той же версии (тег = $r.tag_name, например v6.0):
Invoke-WebRequest "https://github.com/86Box/roms/archive/refs/tags/$($r.tag\_name).zip" -OutFile "$root\_dl\roms.zip" -UseBasicParsing
Expand-Archive "$root\_dl\roms.zip" "$root\_dl\roms_tmp" -Force
Move-Item (Get-ChildItem "$root\_dl\roms_tmp" -Directory)[0].FullName "$root\86box\roms"
Get-AuthenticodeSignature "$root\86box\86Box.exe" # Valid, CN=Open Source Developer Miran Grča
```
Проверка: в `86box\roms\` есть папки `machines`, `video`, `sound`.
### 1.2. Машина
1. Убедись, что `<root>\vm\win98\86box.cfg` и `<root>\start_win98.bat` созданы из приложения А (раздел А.0).
2. Создай пустой диск ровно на 63×16×4161×512 байт:
```powershell
fsutil file createnew "<root>\vm\win98\win98.img" 2147475456
```
3. Перед использованием проверь, что внутренние имена из cfg существуют в установленной версии 86Box (в новых версиях они могут поменяться). Поищи строки `\0p55t2p4\0`, `\0pentium_p55c\0`, `\0sb16_pnp\0`, `\0stealth3d_2000_pci\0` в байтах `86Box.exe`. После первого запуска 86Box перепишет cfg: если устройство пропало или сменилось на другое, значит, имя не подошло.
### 1.3. Образ Windows 98 и загрузочная дискета
1. Попроси пользователя положить ISO в `iso\` (если он в ZIP, распакуй его в `iso\`, читая только нужную запись архива). Переименуй в `win98se.iso`. Ключ часто лежит рядом в `Serial.txt`.
2. Проверь образ, не монтируя его:
- сектор 16: `CD001`, метка тома (у SE — `WIN98 SE`, дата 1999-04-23);
- сектор 17: `EL TORITO SPECIFICATION` — образ загрузочный;
- в корне есть папка `WIN98` с ~77 CAB-файлами и `SETUP.EXE`.
3. Сделай загрузочную дискету без `JO.SYS`:
```powershell
cd <root>; powershell -ExecutionPolicy Bypass -File .\scripts\make_bootdisk.ps1 -Iso .\iso\win98se.iso -Out .\iso\win98se_boot_nojo.img
```
**Почему:** встроенная в диск дискета начинается с загрузчика `JO.SYS` (меню «Boot from Hard Disk / Boot from CD-ROM»). В 86Box пункт «Boot from CD-ROM» навсегда виснет на чёрном экране с курсором. Без `JO.SYS` дискета сразу показывает «Microsoft Windows 98 Startup Menu» и ставит Windows с CD через свои драйверы.
---
## 2. Вспомогательные скрипты (`scripts\`)
Тексты всех скриптов лежат в приложении А. Все скрипты ищут окно процесса `86Box`. Запускай их через `powershell -NoProfile -File` (snap.ps1, click.ps1) или напрямую через `&` (keys.ps1).
| Скрипт | Что делает | Пример |
| --- | --- | --- |
| `snap.ps1` | Снимок окна 86Box через PrintWindow (DPI-aware). Работает, даже если окно перекрыто, но не когда оно свёрнуто | `powershell -NoProfile -File .\scripts\snap.ps1 -Out .\shots\s01.png` |
| `keys.ps1` | Нажатия в машину сканкодами. Сам выводит 86Box на передний план, а если не вышло — отказывается | `& .\scripts\keys.ps1 -Keys 'DOWN','ENTER' -DelayMs 300` |
| `click.ps1` | Клик в координатах относительно окна (физические пиксели) | `powershell -NoProfile -File .\scripts\click.ps1 -X 300 -Y 15` |
| `closevm.ps1` | Штатно закрывает 86Box и подтверждает выход Enter в диалоге «86Box». Сохраняются nvr и cfg | `powershell -NoProfile -File .\scripts\closevm.ps1` |
| `enum.ps1` | Список видимых окон процесса 86Box (найти диалоги) | `powershell -NoProfile -File .\scripts\enum.ps1` |
| `make_bootdisk.ps1` | Дискета из ISO без `JO.SYS` | см. 1.3 |
| `make_iso.ps1` | ISO (ISO 9660 + Joliet) из папки встроенным IMAPI2 | `... -Folder .\cd_files -Out .\iso\files.iso -Label FILES` |
| `winaltr.ps1` | Нажимает Win+Alt+R (старт/стоп записи Xbox Game Bar) | см. раздел 6 |
**Токены `keys.ps1`:**
- отдельные клавиши: `ENTER ESC TAB BKSP F1 F2 F3 F4 F5 F8 F10 LSHIFT LALT UP DOWN LEFT RIGHT DEL NDEL PGUP PGDN`, буквы `A`–`Z`, цифры, `' '` (пробел);
- `TEXT:строка` — набрать текст. Поддерживаются латиница, цифры и `: . \ / , _ -`, пробел. Других символов в таблице нет: допиши их сканкоды в `$map`, прежде чем использовать;
- `ALT+X`, `CTRL+X` (например, `CTRL+ESC` открывает меню Start внутри Windows 98), `ALTTAB`;
- `WAIT:мс` — пауза.
**Проверенные ограничения:**
- `click.ps1` по меню и панели 86Box попадает неточно: однажды вместо «Жёсткий сброс» нажалась пауза, а вместо «Носители» открылся «Вид». Для меню 86Box не используй его. Надёжный путь — закрыть 86Box (`closevm.ps1`), поправить `86box.cfg` и запустить заново.
- Пока открыто меню 86Box, у процесса меняется MainWindowHandle, и `snap.ps1` снимает само меню.
---
## 3. Установка: экран за экраном
Время — ориентир для фоновых ожиданий. Номера снимков — эталонные скриншоты в `guide_screenshots\` (если они переданы вместе с гайдом).
### 3.1. Первый запуск и BIOS (нужно поймать Delete)
Запусти машину и сразу, примерно через 1,5 с, начни жать Delete:
```powershell
Start-Process "<root>\86box\86Box.exe" -ArgumentList '-P','"<root>\vm\win98"'
Start-Sleep -Milliseconds 1500
& .\scripts\keys.ps1 -Keys (@('NDEL','WAIT:150','DEL','WAIT:150') * 40) -DelayMs 30
```
Ожидаемо: синий «ROM PCI/ISA BIOS (PI55T2P4) CMOS SETUP UTILITY» (снимок 03). Если вместо него Startup Menu или POST уже прошёл — закрой 86Box (`closevm.ps1`) и повтори. «CMOS checksum error – Defaults loaded» при первом старте — норма.
В BIOS:
| Действие | Клавиши |
| --- | --- |
| Открыть BIOS FEATURES SETUP | `DOWN`, `ENTER` |
| Перейти на Boot Sequence (6-я строка) | `DOWN` ×5 |
| C,A → A,CDROM,C | `PGDN` (один раз; проверь снимком) |
| Сохранить и выйти | `ESC`, `F10`, `Y`, `ENTER` |
**Зачем:** после разметки диска установщик перезагружается, и с порядком C,A машина остановится на неотформатированном C:.
### 3.2. Текстовая часть установки (≈10 мин)
| Экран | Клавиши | Ждать |
| --- | --- | --- |
| Microsoft Windows 98 Startup Menu (снимок 06) | `ENTER` (пункт 1; через 10 с выберется сам) | ~40 с, мелькают строки Adaptec ASPI |
| Welcome to Setup | `ENTER` | |
| Configure unallocated disk space | `ENTER` | |
| Enable large disk support? (по умолчанию выделено No) | `DOWN`, `ENTER` | |
| Setup will restart… Boot Disk in drive A | `ENTER` | ~30 с |
| Startup Menu снова | `ENTER` | ~45 с |
| Formatting drive C | — | ~5 мин, фоновые проверки снимком раз в 2 мин |
| Setup is now going to perform a routine check | `ENTER` | ~40 с, затем графический мастер |
### 3.3. Графический мастер (≈25 мин, из них ~20 — копирование)
| Экран | Клавиши |
| --- | --- |
| Welcome to Windows 98 Setup | `ENTER` (Continue), ждать ~45 с |
| Select Directory (C:\WINDOWS) | `ENTER` |
| Setup Options (Typical) | `ENTER` |
| Windows Components | `ENTER` |
| Identification (имя выделено) | `TEXT:DENGI98`, `ENTER` (до 15 символов латиницей) |
| Establishing Your Location | `ENTER` (страна неважна) |
| Start Copying Files | `ENTER` |
| Копирование, «File copy progress» | ждать ~20 мин в фоне |
Если спросят про создание Startup Disk — `ESC` или Cancel (у нас вопрос не появился).
**Дискету нужно убрать до перезагрузки после копирования.** 86Box держит образ дискеты в памяти: если испортить или заменить файл на диске, он этого не заметит (проверено). Варианты:
- **попросить пользователя** во время копирования выбрать в 86Box «Носители» → «Дисковод 1 (3.5" 1.44M)…» → «Извлечь»;
- **или** дождаться перезагрузки. Если появился Startup Menu, не нажимай ничего: через 10 с пункт 1 выберется сам и откроется Welcome to Setup, который ждёт Enter и ничего не трогает. Затем `closevm.ps1` → удали строку `fdd_01_fn = …` из `86box.cfg` → запусти машину снова. BIOS перейдёт к CD, меню CD через 10 с само выберет «Boot from Hard Disk», и загрузится установленная Windows (снимок 21, «Getting ready to run Windows for the first time»).
### 3.4. Первая загрузка (≈20 мин)
| Экран | Клавиши |
| --- | --- |
| User Information | `TEXT:User`, `ENTER` |
| License Agreement | `ALT+A`, `ENTER` |
| Windows Product Key | `TEXT:<25 символов без дефисов>` → **снимок, сверить** → `ENTER` |
| Start Wizard | `ENTER` (Finish) |
| Hardware Detection → Setting up Hardware | ждать ~5–8 мин в фоне |
| Date/Time Properties | `ENTER` (Close; время уже взято с хоста) |
| Updating System Settings, перезагрузка | ждать ~5 мин |
| Enter Windows Password | `ENTER` (пустой пароль — больше не спросит) |
| Add New Hardware Wizard (Plug and Play Monitor) | `ENTER`, `ENTER`, `' '` (снять Floppy disk drives), `ENTER`, `ENTER`, `ENTER` (Finish) — снимок после каждого шага |
| Welcome to Windows 98 | `ALT+O` (снять «Show this screen…»), `ESC` |
Ключ — данные пользователя: не выводи его в чат без нужды и не оставляй на снимках, которые пойдут в публикацию.
---
## 4. Настройка после установки (≈3 мин)
```powershell
# Не гасить монитор (иначе через 15 минут запись станет чёрной: «Монитор в спящем режиме»)
& .\scripts\keys.ps1 -Keys 'CTRL+ESC','WAIT:1200','R','WAIT:1800','TEXT:control powercfg.cpl','WAIT:400','ENTER'
# снимок → поле Turn off monitor:
& .\scripts\keys.ps1 -Keys 'ALT+M','WAIT:400','N','WAIT:400','ENTER' -DelayMs 250
# Проверить заставку (должно быть (None)):
& .\scripts\keys.ps1 -Keys 'CTRL+ESC','WAIT:1500','R','WAIT:2000','TEXT:control desk.cpl,,1','WAIT:400','ENTER'
# снимок → ESC
```
Если меню Start не открылось, увеличь паузы: после закрытия окон панели управления нужна ~1,5 с.
**Выключение Windows 98:** на рабочем столе (сначала закрой окна) `ALT+F4` → диалог Shut Down Windows. Позиция по умолчанию бывает разной (Stand by / Shut down) — **сними снимок** и выбери Shut down стрелками → `ENTER` → «It's now safe to turn off your computer» → `closevm.ps1`.
**Звук:** проверка — Control Panel → Sounds → Preview. Окно Welcome to Windows 98 само играет фоновую музыку — не путай её со звуком программы.
---
## 5. Передача файлов и запуск программы
1. Сложи файлы в отдельную папку и собери ISO:
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\make_iso.ps1 -Folder .\cd_files -Out .\iso\files.iso -Label FILES
```
2. Выключи Windows 98 (раздел 4), выполни `closevm.ps1`, в `86box.cfg` замени строку на `cdrom_01_image_path = ../../iso/files.iso`, запусти машину. Образ без загрузки → BIOS загрузит C:.
3. Внутри Windows 98 диск — `D:`. Запуск: `CTRL+ESC`, `R`, `TEXT:d:\program.exe`, `ENTER`.
4. Не запускай второй экземпляр той же старой программы, пока открыт первый. Два плеера Lotus ScreenCam падали с «This program has performed an illegal operation». Если такое случилось — перезагрузи Windows 98.
Пример, на котором всё проверено: демо «1С:Деньги» 1998 года (Lotus ScreenCam 97, 22 мин, 640×480, **без звуковой дорожки** — так записан оригинал).
- `https://1c.ru/ftp/pub/demo/1c/d77.zip` — работает: плеер `SCPLAYER.EXE` и 7 разделов `D20_*.SCM`.
- `dengir.exe` (тот же ролик одним файлом) с 1c.ru отдаёт 404. Копия в веб-архиве: `https://web.archive.org/web/20010418081352if_/http://www.1c.ru:80/ftp/pub/demo/1c/dengir.exe`.
- Перед запуском любых скачанных `.exe` проверь их, не запуская: SHA-256, подпись, PE-заголовок, VersionInfo, метку источника (`Zone.Identifier`).
---
## 6. Запись экрана в MP4 (Xbox Game Bar)
1. Выведи 86Box на передний план (`keys.ps1 -Keys 'WAIT:200'`), затем запусти `powershell -NoProfile -File .\scripts\winaltr.ps1` — запись началась.
2. **Проверка, что запись идёт, — только по индикатору с таймером на экране** (попроси пользователя посмотреть). Файл в `Видео\Captures` во время записи не растёт (~800 КБ, без длительности): Game Bar пишет видео целиком при остановке.
3. Запусти программу внутри машины. Пока идёт запись, не сворачивай окно 86Box и ничего не нажимай в нём.
4. Остановка: попроси пользователя нажать ■ на индикаторе. Если будешь нажимать `winaltr.ps1` сам, помни: это **переключатель**. Если запись уже остановлена, он начнёт новую. Так у нас появились лишние файлы на 4 и 18 секунд.
5. Проверь результат по свойствам файла:
```powershell
$cap = Join-Path ([Environment]::GetFolderPath('MyVideos')) 'Captures'; $sh = New-Object -ComObject Shell.Application; $fo = $sh.Namespace($cap)
Get-ChildItem $cap -File | sort CreationTime -Desc | select -First 3 | % { "{0} | {1}" -f $_.Name, $fo.GetDetailsOf($fo.ParseName($_.Name), 27) }
```
У нас 22-минутный ролик дал файл 23:55, 164 МБ.
---
## 7. Известные грабли — коротко
| Симптом | Причина | Что делать |
| --- | --- | --- |
| Чёрный экран с курсором после «Boot from CD-ROM» | `JO.SYS` | Дискета без `JO.SYS` (1.3) |
| DISK BOOT FAILURE | Нет дискеты / порядок C,A | Проверить `fdd_01_fn`, Boot Sequence A,CDROM,C |
| После копирования снова Startup Menu | Дискета вставлена | 3.3: извлечь (меню) или убрать `fdd_01_fn` и перезапустить |
| Правки `86box.cfg` пропали | 86Box перезаписывает cfg при выходе | Править только при закрытом 86Box |
| Настройки BIOS пропали | 86Box убит, а не закрыт | Закрывать через `closevm.ps1` |
| «Монитор в спящем режиме» | Power Management Windows 98 | Turn off monitor = Never; разбудить — `LSHIFT` |
| Музыка, которой «не должно быть» | Окно Welcome to Windows 98 | Закрыть, снять галочку |
| `keys.ps1`: WARN not in focus | Пользователь работает в другом окне | Попросить щёлкнуть по 86Box или получить разрешение и щёлкнуть заголовок |
| Символ не напечатался в `TEXT:` | Нет в таблице сканкодов | Добавить в `$map` (например, `'_'=0x0C` с Shift, `'/'=0x35`, `','=0x33`) |
| Старая программа на Windows 11 / в Песочнице Windows виснет до нажатия клавиши, без звука | 16-битный код (flat thunk), которого нет в 64-битной Windows | Запускать в Windows 98 в 86Box |
| Game Bar «не пишет» (файл не растёт) | Так и задумано, файл пишется при остановке | Смотреть на таймер индикатора |
---
## 8. Итог для пользователя
В конце сообщи пользователю:
- где лежит машина и как её запускать (`start_win98.bat`);
- как передавать файлы (ISO + `cdrom_01_image_path` или «Носители» → «CD-ROM 1» → «Образ…»);
- что мышь захватывается щелчком и освобождается Ctrl+End;
- где лежит запись, если её делали;
- что можно удалить: временные загрузки в `_dl\`, снимки в `shots\`.
---
## Приложение А. Файлы проекта
Ниже вложены все файлы, которые нужны по сценарию. Перед каждым стоит пометка `<!-- file: относительный\путь -->`, а под ней — блок кода с содержимым. Все файлы в чистом ASCII.
### А.0. Распаковка одной командой
1. Создай папку проекта `<root>` и положи этот `.md` рядом или внутрь неё.
2. Выполни в PowerShell (подставь пути):
```powershell
$md = '<путь к этому файлу>.md'; $root = '<root>'
$text = [IO.File]::ReadAllText($md, [Text.Encoding]::UTF8)
$rx = [regex]'(?s)<!-- file: (?<path>[^>]+?) -->\r?\n```[a-z]*\r?\n(?<body>.*?)\r?\n```'
foreach ($m in $rx.Matches($text)) {
$dst = Join-Path $root $m.Groups['path'].Value.Trim()
New-Item -ItemType Directory -Force (Split-Path $dst) | Out-Null
[IO.File]::WriteAllText($dst, $m.Groups['body'].Value + "`r`n", [Text.Encoding]::ASCII)
"written: $dst"
}
```
Должно появиться 10 файлов: `start_win98.bat`, `vm\win98\86box.cfg` и 8 скриптов в `scripts\`. Если меньше — регулярное выражение не нашло блок: проверь, что при копировании этого файла не потерялись переводы строк.
Если команду выполнить нельзя, создай файлы вручную: имя файла указано в пометке над каждым блоком, а содержимое — это блок целиком.
### А.1. `start_win98.bat`
Запуск машины: 86Box с папкой настроек vm\win98.
<!-- file: start_win98.bat -->
```bat
@echo off
start "" "%~dp086box\86Box.exe" -P "%~dp0vm\win98"
```
### А.2. `vm\win98\86box.cfg`
Конфигурация машины (раздел 1.2). Пути к дискете и ISO — относительно папки машины.
<!-- file: vm\win98\86box.cfg -->
```ini
[General]
vid_renderer = qt_software
[Machine]
machine = p55t2p4
cpu_family = pentium_p55c
cpu_speed = 200000000
cpu_multi = 3
cpu_use_dynarec = 1
mem_size = 65536
[Video]
gfxcard = virge_pci
[S3 ViRGE PCI]
bios = stealth3d_2000_pci
[Input devices]
mouse_type = ps2
[Sound]
sndcard = sb16_pnp
[Hard disks]
hdd_01_parameters = 63, 16, 4161, 0, ide
hdd_01_fn = win98.img
hdd_01_ide_channel = 0:0
[Floppy and CD-ROM drives]
fdd_01_type = 35_2hd
fdd_02_type = none
fdd_01_fn = ../../iso/win98se_boot_nojo.img
cdrom_01_parameters = 1, atapi
cdrom_01_ide_channel = 1:0
cdrom_01_speed = 24
cdrom_01_image_path = ../../iso/win98se.iso
```
### А.3. `scripts\snap.ps1`
Снимок окна 86Box в PNG через PrintWindow. Параметры: -Out путь, -Handle (необязательно, снять конкретное окно).
<!-- file: scripts\snap.ps1 -->
```powershell
param([string]$Out)
Add-Type -AssemblyName System.Drawing
Add-Type @"
using System; using System.Runtime.InteropServices;
public static class W { [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
[DllImport("user32.dll")] public static extern bool PrintWindow(IntPtr h, IntPtr dc, uint f);
[DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int L, T, R, B; } }
"@
[void][W]::SetProcessDPIAware()
$p = Get-Process 86Box -ErrorAction Stop | ? { $_.MainWindowHandle -ne 0 } | Select-Object -First 1
$r = New-Object W+RECT; [void][W]::GetWindowRect($p.MainWindowHandle, [ref]$r)
$bmp = New-Object Drawing.Bitmap ($r.R-$r.L), ($r.B-$r.T); $g=[Drawing.Graphics]::FromImage($bmp); $dc=$g.GetHdc()
[void][W]::PrintWindow($p.MainWindowHandle, $dc, 2); $g.ReleaseHdc($dc); $bmp.Save($Out, [Drawing.Imaging.ImageFormat]::Png); $g.Dispose(); $bmp.Dispose()
"saved $Out ($($r.R-$r.L)x$($r.B-$r.T)) title='$($p.MainWindowTitle)'"
```
### А.4. `scripts\keys.ps1`
Нажатия в машину сканкодами (токены — раздел 2). Сам выводит 86Box на передний план, а если не вышло — пишет WARN и ничего не нажимает.
<!-- file: scripts\keys.ps1 -->
```powershell
param([string[]]$Keys, [int]$DelayMs = 150, [switch]$NoFocus)
Add-Type @"
using System; using System.Runtime.InteropServices;
public static class K {
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h); [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte sc, uint f, UIntPtr e);
[DllImport("user32.dll")] public static extern uint SendInput(uint n, INPUT[] i, int cb);
[StructLayout(LayoutKind.Sequential)] public struct KEYBDINPUT { public ushort wVk, wScan; public uint dwFlags, time; public IntPtr extra; }
[StructLayout(LayoutKind.Sequential)] public struct MOUSEINPUT { public int dx, dy; public uint data, flags, time; public IntPtr extra; }
[StructLayout(LayoutKind.Explicit)] public struct U { [FieldOffset(0)] public MOUSEINPUT mi; [FieldOffset(0)] public KEYBDINPUT ki; }
[StructLayout(LayoutKind.Sequential)] public struct INPUT { public uint type; public U u; }
public static void Scan(ushort sc, bool ext, bool up) { var i = new INPUT[1]; i[0].type = 1; i[0].u.ki.wScan = sc; i[0].u.ki.dwFlags = 8u | (ext ? 1u : 0u) | (up ? 2u : 0u); SendInput(1, i, Marshal.SizeOf(typeof(INPUT))); }
}
"@
$map = @{ '1'=0x02;'2'=0x03;'3'=0x04;'4'=0x05;'5'=0x06;'6'=0x07;'7'=0x08;'8'=0x09;'9'=0x0A;'0'=0x0B;'-'=0x0C;
'Q'=0x10;'W'=0x11;'E'=0x12;'R'=0x13;'T'=0x14;'Y'=0x15;'U'=0x16;'I'=0x17;'O'=0x18;'P'=0x19;'A'=0x1E;'S'=0x1F;'D'=0x20;'F'=0x21;'G'=0x22;'H'=0x23;'J'=0x24;'K'=0x25;'L'=0x26;
'Z'=0x2C;'X'=0x2D;'C'=0x2E;'V'=0x2F;'B'=0x30;'N'=0x31;'M'=0x32;':'=0x27;'.'=0x34;'\'=0x2B;'/'=0x35;','=0x33;'_'=0x0C;' '=0x39;
'ENTER'=0x1C;'ESC'=0x01;'TAB'=0x0F;'BKSP'=0x0E;'F1'=0x3B;'F2'=0x3C;'F3'=0x3D;'F4'=0x3E;'F5'=0x3F;'F8'=0x42;'F10'=0x44;'LSHIFT'=0x2A;'LALT'=0x38;'Y_'=0x15;'NDEL'=0x53 }
$ext = @{ 'UP'=0x48;'DOWN'=0x50;'LEFT'=0x4B;'RIGHT'=0x4D;'DEL'=0x53;'PGDN'=0x51;'PGUP'=0x49 }
$p = Get-Process 86Box | ? { $_.MainWindowHandle -ne 0 } | Select-Object -First 1; if (-not $NoFocus -and [K]::GetForegroundWindow() -ne $p.MainWindowHandle) { [K]::keybd_event(0x12,0,0,[UIntPtr]::Zero); [K]::keybd_event(0x12,0,2,[UIntPtr]::Zero); [void][K]::SetForegroundWindow($p.MainWindowHandle); Start-Sleep -Milliseconds 300 }; if (-not $NoFocus -and [K]::GetForegroundWindow() -ne $p.MainWindowHandle) { "WARN: 86Box window is not in focus"; return }
foreach ($k in $Keys) {
if ($k -like 'TEXT:*') { foreach ($ch in $k.Substring(5).ToCharArray()) { $c=[string]$ch; $up=$c.ToUpper(); $shift = ($c -cne $c.ToLower()) -or $c -eq ':' -or $c -eq '_'; if ($c -eq ':') { $up=':' }; if ($c -eq '_') { $up='_' }; if ($shift) { [K]::Scan(0x2A,$false,$false) }; [K]::Scan([uint16]$map[$up],$false,$false); Start-Sleep -Milliseconds 40; [K]::Scan([uint16]$map[$up],$false,$true); if ($shift) { [K]::Scan(0x2A,$false,$true) }; Start-Sleep -Milliseconds 60 } ; continue }
if ($k -like 'ALT+*') { $c=[uint16]$map[$k.Substring(4)]; [K]::Scan(0x38,$false,$false); Start-Sleep -Milliseconds 50; [K]::Scan($c,$false,$false); Start-Sleep -Milliseconds 50; [K]::Scan($c,$false,$true); [K]::Scan(0x38,$false,$true); Start-Sleep -Milliseconds $DelayMs; continue }
if ($k -like 'CTRL+*') { $n=$k.Substring(5); if ($ext.ContainsKey($n)) { $c=[uint16]$ext[$n] } else { $c=[uint16]$map[$n] }; [K]::Scan(0x1D,$false,$false); Start-Sleep -Milliseconds 50; [K]::Scan($c,$false,$false); Start-Sleep -Milliseconds 50; [K]::Scan($c,$false,$true); [K]::Scan(0x1D,$false,$true); Start-Sleep -Milliseconds $DelayMs; continue }
if ($k -eq 'ALTTAB') { [K]::Scan(0x38,$false,$false); Start-Sleep -Milliseconds 50; [K]::Scan(0x0F,$false,$false); Start-Sleep -Milliseconds 50; [K]::Scan(0x0F,$false,$true); Start-Sleep -Milliseconds 300; [K]::Scan(0x38,$false,$true); Start-Sleep -Milliseconds $DelayMs; continue }
if ($k -like 'WAIT:*') { Start-Sleep -Milliseconds ([int]$k.Substring(5)); continue }
if ($ext.ContainsKey($k)) { $sc=[uint16]$ext[$k]; $e=$true } else { $sc=[uint16]$map[$k]; $e=$false }
[K]::Scan($sc,$e,$false); Start-Sleep -Milliseconds 50; [K]::Scan($sc,$e,$true); Start-Sleep -Milliseconds $DelayMs
}
"sent: $($Keys -join ' ')"
```
### А.5. `scripts\click.ps1`
Клик левой кнопкой по координатам относительно окна 86Box (физические пиксели). По меню 86Box попадает неточно.
<!-- file: scripts\click.ps1 -->
```powershell
param([int]$X,[int]$Y)
Add-Type @"
using System; using System.Runtime.InteropServices;
public static class M { [DllImport("user32.dll")] public static extern bool SetProcessDPIAware(); [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
[DllImport("user32.dll")] public static extern bool SetCursorPos(int x,int y); [DllImport("user32.dll")] public static extern void mouse_event(uint f,int x,int y,uint d,UIntPtr e);
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h); [DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte sc, uint f, UIntPtr e);
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int L,T,R,B; } }
"@
[void][M]::SetProcessDPIAware(); $p=Get-Process 86Box | ? { $_.MainWindowHandle -ne 0 } | Select-Object -First 1
[M]::keybd_event(0x12,0,0,[UIntPtr]::Zero); [M]::keybd_event(0x12,0,2,[UIntPtr]::Zero); [void][M]::SetForegroundWindow($p.MainWindowHandle); Start-Sleep -Milliseconds 200
$r=New-Object M+RECT; [void][M]::GetWindowRect($p.MainWindowHandle,[ref]$r); [void][M]::SetCursorPos($r.L+$X,$r.T+$Y); Start-Sleep -Milliseconds 100
[M]::mouse_event(2,0,0,0,[UIntPtr]::Zero); Start-Sleep -Milliseconds 60; [M]::mouse_event(4,0,0,0,[UIntPtr]::Zero); "clicked $X,$Y"
```
### А.6. `scripts\enum.ps1`
Список видимых окон процесса 86Box (например, чтобы найти диалог подтверждения выхода).
<!-- file: scripts\enum.ps1 -->
```powershell
Add-Type @"
using System; using System.Text; using System.Runtime.InteropServices; using System.Collections.Generic;
public static class E { public delegate bool CB(IntPtr h, IntPtr l);
[DllImport("user32.dll")] public static extern bool EnumWindows(CB cb, IntPtr l); [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h); [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetWindowText(IntPtr h, StringBuilder s, int n);
public static List<string> List(uint pid) { var r = new List<string>(); EnumWindows((h,l)=>{ uint p; GetWindowThreadProcessId(h, out p); if (p==pid && IsWindowVisible(h)) { var sb=new StringBuilder(256); GetWindowText(h,sb,256); r.Add(h.ToString()+" '"+sb.ToString()+"'"); } return true; }, IntPtr.Zero); return r; } }
"@
$p=Get-Process 86Box | Select-Object -First 1; [E]::List([uint32]$p.Id)
```
### А.7. `scripts\closevm.ps1`
Штатное закрытие 86Box с подтверждением Enter в диалоге «86Box» (сохраняются nvr и cfg).
<!-- file: scripts\closevm.ps1 -->
```powershell
Add-Type @"
using System; using System.Text; using System.Runtime.InteropServices; using System.Collections.Generic;
public static class C { public delegate bool CB(IntPtr h, IntPtr l);
[DllImport("user32.dll")] public static extern bool EnumWindows(CB cb, IntPtr l); [DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h); [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetWindowText(IntPtr h, StringBuilder s, int n);
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h); [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte sc, uint f, UIntPtr e);
public static IntPtr FindTitled(uint pid, string title) { IntPtr found = IntPtr.Zero; EnumWindows((h,l)=>{ uint p; GetWindowThreadProcessId(h, out p); if (p==pid && IsWindowVisible(h)) { var sb=new StringBuilder(256); GetWindowText(h,sb,256); if (sb.ToString()==title) found=h; } return true; }, IntPtr.Zero); return found; } }
"@
$p=Get-Process 86Box -ErrorAction SilentlyContinue | Select-Object -First 1; if (-not $p) { "not running"; return }
[void]$p.CloseMainWindow(); Start-Sleep 2
$d=[C]::FindTitled([uint32]$p.Id,'86Box'); if ($d -ne [IntPtr]::Zero) { [C]::keybd_event(0x12,0,0,[UIntPtr]::Zero); [C]::keybd_event(0x12,0,2,[UIntPtr]::Zero); [void][C]::SetForegroundWindow($d); Start-Sleep -Milliseconds 400; if ([C]::GetForegroundWindow() -eq $d) { [C]::keybd_event(0x0D,0x1C,0,[UIntPtr]::Zero); [C]::keybd_event(0x0D,0x1C,2,[UIntPtr]::Zero) } else { "dialog not focused"; return } }
for ($i=0; $i -lt 10; $i++) { Start-Sleep 1; if (-not (Get-Process -Id $p.Id -ErrorAction SilentlyContinue)) { "86Box closed"; return } }; "still running"
```
### А.8. `scripts\winaltr.ps1`
Нажимает Win+Alt+R — старт/стоп записи Xbox Game Bar. Это переключатель.
<!-- file: scripts\winaltr.ps1 -->
```powershell
Add-Type @"
using System; using System.Runtime.InteropServices;
public static class GR { [DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte sc, uint f, UIntPtr e); }
"@
[GR]::keybd_event(0x5B,0x5B,1,[UIntPtr]::Zero); Start-Sleep -Milliseconds 60; [GR]::keybd_event(0x12,0x38,0,[UIntPtr]::Zero); Start-Sleep -Milliseconds 60
[GR]::keybd_event(0x52,0x13,0,[UIntPtr]::Zero); Start-Sleep -Milliseconds 80; [GR]::keybd_event(0x52,0x13,2,[UIntPtr]::Zero); Start-Sleep -Milliseconds 40
[GR]::keybd_event(0x12,0x38,2,[UIntPtr]::Zero); Start-Sleep -Milliseconds 40; [GR]::keybd_event(0x5B,0x5B,3,[UIntPtr]::Zero); "Win+Alt+R sent"
```
### А.9. `scripts\make_bootdisk.ps1`
Дискета 1,44 МБ из загрузочного ISO Windows 98 без JO.SYS. ISO не изменяет.
<!-- file: scripts\make_bootdisk.ps1 -->
```powershell
param([string]$Iso, [string]$Out)
$fs = [IO.File]::OpenRead((Resolve-Path $Iso)); $b = New-Object byte[] 2048
$fs.Position = 17 * 2048; [void]$fs.Read($b, 0, 2048)
if ([Text.Encoding]::ASCII.GetString($b, 7, 23) -ne 'EL TORITO SPECIFICATION') { throw 'not bootable' }
$fs.Position = [int64][BitConverter]::ToUInt32($b, 0x47) * 2048; [void]$fs.Read($b, 0, 2048)
if ($b[33] -ne 2) { throw 'not 1.44 floppy emulation' }
$img = New-Object byte[] 1474560
$fs.Position = [int64][BitConverter]::ToUInt32($b, 40) * 2048; [void]$fs.Read($img, 0, $img.Length); $fs.Close()
$bps = [BitConverter]::ToUInt16($img, 11); $res = [BitConverter]::ToUInt16($img, 14)
$root = ($res + $img[16] * [BitConverter]::ToUInt16($img, 22)) * $bps; $entries = [BitConverter]::ToUInt16($img, 17)
for ($i = 0; $i -lt $entries; $i++) { $o = $root + 32 * $i
if ([Text.Encoding]::ASCII.GetString($img, $o, 11) -eq 'JO SYS') { $img[$o] = 0xE5; 'JO.SYS removed' } }
[IO.File]::WriteAllBytes((Join-Path (Get-Location) $Out), $img); "Done: $Out"
```
### А.10. `scripts\make_iso.ps1`
ISO (ISO 9660 + Joliet) из папки встроенным IMAPI2. Параметры: -Folder, -Out, -Label.
<!-- file: scripts\make_iso.ps1 -->
```powershell
param([string]$Folder, [string]$Out, [string]$Label = 'FILES')
Add-Type -TypeDefinition @"
using System; using System.IO; using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes;
public static class IsoWriter { public static void Save(object stream, string path) {
IStream s = (IStream)stream; byte[] buf = new byte[65536]; IntPtr pRead = Marshal.AllocHGlobal(4);
using (FileStream f = File.Create(path)) { while (true) { s.Read(buf, buf.Length, pRead); int n = Marshal.ReadInt32(pRead); if (n <= 0) break; f.Write(buf, 0, n); } }
Marshal.FreeHGlobal(pRead); } }
"@
$fs = New-Object -ComObject IMAPI2FS.MsftFileSystemImage
$fs.FileSystemsToCreate = 3 # ISO 9660 + Joliet
$fs.VolumeName = $Label
$fs.Root.AddTree((Resolve-Path $Folder).Path, $false)
$outPath = if ([IO.Path]::IsPathRooted($Out)) { $Out } else { Join-Path (Get-Location) $Out }
[IsoWriter]::Save($fs.CreateResultImage().ImageStream, $outPath)
"Done: $outPath"
```
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.