Старые программы конца 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 = p55t2p4cpu_family = pentium_p55ccpu_speed = 200000000cpu_multi = 3cpu_use_dynarec = 1mem_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, idehdd_01_fn = win98.imghdd_01_ide_channel = 0:0[Floppy and CD-ROM drives]fdd_01_type = 35_2hdfdd_02_type = nonefdd_01_fn = ../../iso/win98se_boot_nojo.imgcdrom_01_parameters = 1, atapicdrom_01_ide_channel = 1:0cdrom_01_speed = 24cdrom_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 offstart "" "%~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.img
param([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_FILES
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)[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 1New-Item -ItemType Directory -Force "$root\_dl" | Out-NullInvoke-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" -UseBasicParsingExpand-Archive "$root\_dl\roms.zip" "$root\_dl\roms_tmp" -ForceMove-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:```powershellStart-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 offstart "" "%~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 = p55t2p4cpu_family = pentium_p55ccpu_speed = 200000000cpu_multi = 3cpu_use_dynarec = 1mem_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, idehdd_01_fn = win98.imghdd_01_ide_channel = 0:0[Floppy and CD-ROM drives]fdd_01_type = 35_2hdfdd_02_type = nonefdd_01_fn = ../../iso/win98se_boot_nojo.imgcdrom_01_parameters = 1, atapicdrom_01_ide_channel = 1:0cdrom_01_speed = 24cdrom_01_image_path = ../../iso/win98se.iso```### А.3. `scripts\snap.ps1`Снимок окна 86Box в PNG через PrintWindow. Параметры: -Out путь, -Handle (необязательно, снять конкретное окно).<!-- file: scripts\snap.ps1 -->```powershellparam([string]$Out)Add-Type -AssemblyName System.DrawingAdd-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 -->```powershellparam([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 -->```powershellparam([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 -->```powershellAdd-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 -->```powershellAdd-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 -->```powershellAdd-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 -->```powershellparam([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 -->```powershellparam([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"```
ссылка на оригинал статьи https://habr.com/ru/articles/1099030/