Разобрал сохранение Skyrim VR с помощью ИИ-агентов и превратил его в словарь для изучения английского

Прокомментировать Просмотры: 0

Теперь я изучаю английский на материале Skyrim — и это отличный повод проводить в игре больше времени. Лексику я беру прямо из собственного сохранения: ИИ-агенты вскрывают файл, вытаскивают предметы, перки и квесты и превращают их в карточки. Каждый день Telegram-бот устраивает мне по ним опрос, причём отвечать можно даже голосом.

С чего всё началось

Skyrim — любимая игра моей юности, а в VR она раскрылась совершенно по-новому. Я играю на сборке FUS: это свыше четырёхсот модов, которые Wabbajack самостоятельно устанавливает в Mod Organizer 2. Каждый мод лежит в отдельной папке, но игра воспринимает их как единую папку Data. Сборка рассчитана только на английский язык, а английский в Скайриме — книжный и витиеватый. Draught тут означает глоток зелья, хотя в словаре на первом месте стоит «сквозняк». Ward — оберег, barrow — древний курган-гробница. В школе такому не учат.

Я играю за бретонца по имени Dan: в одной руке меч, в другой магия. Защитное заклинание Lesser Ward вызываю жестом — левый стик и поднятая рука.

Задействованы три компьютера. На игровом ПК с Windows запущен Skyrim VR, а к нему по кабелю подключён шлем Quest 3. На MacBook работают Claude Code и Codex, которые обращаются к игровому ПК по SSH через домашнюю сеть. Mac mini служит круглосуточным домашним сервером, и именно на нём живёт бот.

Игровой компьютер, рабочий ноутбук и домашний сервер
Игровой компьютер, рабочий ноутбук и домашний сервер

Записать незнакомое слово в шлеме невозможно: руки заняты мечом и заклинанием. Поэтому словарь должен формироваться сам — на основе происходящего в игре. Схема получилась такой: агенты забирают свежее сохранение с игрового ПК, разбирают его на ноутбуке, переводят числовые идентификаторы в английские названия силами самого игрового ПК и собирают колоду, а бот на Mac mini присылает из неё карточки.

Путь слова: от сейва на игровом компьютере до карточки в Telegram
Путь слова: от сейва на игровом компьютере до карточки в Telegram

Что хранится в сохранении

Мой файл сохранения занимал 3,3 МБ, а после распаковки вырос до 9,5 МБ. В нём около 22 тысяч записей обо всех изменениях в мире с начала прохождения. Формат официально нигде не описан, но моддеры давно его расшифровали. Агенты изучили исходный код открытого редактора сохранений FallrimTools и написали на Python собственный парсер. Он работает строго на чтение: ни игра, ни сохранение не затрагиваются.

Сейв открывается сигнатурой TESV_SAVEGAME. Следом идёт незашифрованный заголовок: имя персонажа, уровень, текущая локация, внутриигровая дата и превью для меню загрузки. Всё остальное сжато алгоритмом LZ4, поэтому сперва данные нужно распаковать.

Агентам потребовались три раздела:

  • список плагинов — какие моды подключены и в каком порядке, причём обычные и «лёгкие» (ESL) указаны раздельно;

  • таблица FormID — справочник идентификаторов, на который опираются остальные записи;

  • change forms — записи об изменениях в мире: инвентарь, перки, квесты, содержимое бочек. Часть из них дополнительно сжата zlib.

Сейв изнутри: открытый заголовок, сжатая часть и ссылки на вещи в три байта
Сейв изнутри: открытый заголовок, сжатая часть и ссылки на вещи в три байта

Ссылка на объект внутри сохранения занимает три байта. Два старших бита указывают, где его искать: 0 — строка в таблице FormID этого сейва, 1 — объект из основного файла игры Skyrim.esm, 2 — предмет, появившийся уже в процессе игры, например сваренное зелье или зачарованный меч. В коде разбора это умещается в несколько строк:

def resolve(ref, formids):
    t = ref[0] >> 6                                   # два старших бита — тип ссылки
    v = ((ref[0] & 0x3F) << 16) | (ref[1] << 8) | ref[2]
    if t == 0: return None if v == 0 or v > len(formids) else formids[v - 1]  # таблица считается с единицы
    if t == 1: return v                               # объект из Skyrim.esm
    if t == 2: return 0xFF000000 | v                  # создано в игре
    return None

Счётчики в сейве записаны числами переменной длины: два младших бита первого байта определяют, сколько байт занимает само число.

Навыки и перки агенты обнаружили внутри записи игрока при помощи поиска по шаблону. Навыки хранятся парами «номер навыка от 6 до 23 — дробное значение». Перки идут за счётчиком парами «ссылка на перк — ранг». Заклинания — это счётчик и список ссылок. Эти фрагменты внутри записи способны смещаться, поэтому при каждой выгрузке их приходится перепроверять.

Уже первый разбор выдал 116 предметов в инвентаре, 73 перка и 757 записей о квестах. Заодно агенты составили перечень из 314 заклинаний сборки и для 292 из них выяснили, у какого торговца те продаются.

Что достали из одного сохранения
Что достали из одного сохранения

Как число превращается в слово

Самих названий в сейве нет — только числовые коды. Первый байт кода указывает позицию мода в порядке загрузки, остальные три — номер записи внутри этого мода. Для примера возьмём книгу Spell Tome: Shalidor’s Beacon из моего инвентаря с кодом 574B067E: 0x57 — это восемьдесят седьмая позиция в списке загрузки (счёт с нуля), то есть мод MysticismMagic.esp. У лёгких модов код устроен иначе: сначала идёт FE, затем три шестнадцатеричные цифры номера мода и ещё три — номера записи.

Чтобы превратить код в слово, необходимо заглянуть в сам мод. Агенты формируют запрос: список плагинов из сейва и коды, которые нужно найти. Запрос по SSH отправляется на игровой ПК, где Python-скрипт обходит моды и возвращает английские названия и описания.

Здесь есть два подводных камня. Первый: внутри каждого мода коды отсчитываются от его собственного списка зависимостей, а не от общего порядка загрузки. Скрипт делает пересчёт:

idx = {n.lower(): i << 24 for i, n in enumerate(full)}                        # обычные моды
idx.update({n.lower(): 0xFE000000 | (i << 12) for i, n in enumerate(light)})  # лёгкие моды

def form(v):
    mi = v >> 24                                   # номер в списке зависимостей этого мода
    if mi >= len(masters): return None
    base = idx.get(masters[mi].lower())
    return None if base is None else base + (v & (0xFFF if base >= 0xFE000000 else 0xFFFFFF))

Второй: если два мода изменяют один и тот же объект, в игре действует тот, что загружен позже. А если два мода содержат файл с одинаковым именем, побеждает тот, у которого выше приоритет в Mod Organizer 2. Скрипт воспроизводит обе схемы: он собирает общую папку Data так же, как её видит игра, и проходит по всем 292 модам строго в порядке загрузки, сохраняя последнюю версию записи. Без этого получились бы названия из чистой игры, не совпадающие с тем, что я вижу в шлеме.

Что обнаружилось по ходу работы:

  • Навыки под чужими именами. Мод Hand to Hand переименовал их: кражи стали называться Security, а взлом замков — Hand to Hand. Первая выгрузка показала прежние названия, поэтому актуальные пришлось доставать из модов.

  • Вики не выручила. Мод Apothecary переписал свойства ингредиентов, и таблицы из интернета с моей игрой расходятся. Поэтому все данные берутся из файлов самой игры.

  • Бочка. Я поинтересовался, можно ли узнать содержимое бочки, которую только что открыл в игре. Агент перебрал все контейнеры с изменениями и нашёл её: 509 ингредиентов 49 видов.

  • Где я нахожусь. По свежему быстрому сохранению агент определил пещеру, где стоит персонаж: Lost Knife Hideout.

Решётка в Geirmund’s Hall

Отдельная история приключилась в Geirmund’s Hall. Загадку с колоннами я решил верно, рычаг щёлкнул, но решётка так и не поднялась. Выискивать её взглядом в шлеме и кликать по консоли — занятие не из приятных.

Codex пошёл другим путём. Он взял на ноутбуке копию Skyrim.esm и разобрал её библиотекой esplib: нашёл ячейку GeirmundsHall01, перечислил все объекты в ней, отфильтровал по названиям вроде Portcullis, Gate и Lever и сверил координаты с рычагами. Искомым объектом оказался NorPortcullis с кодом 000A5BFB.

Затем команды нужно было ввести в консоль игры. Через SSH это невозможно: он работает в отдельном сеансе Windows и до окна игры не дотягивается. Codex создал задачу в планировщике Windows, которая запускается в моём сеансе, и вспомогательный Python-скрипт. Скрипт переключает раскладку на английскую и нажимает клавиши по сканкодам, как настоящая клавиатура. Команды такие:

prid 000A5BFB
disable

Решётка исчезла, проход открылся. Команда tcl, отключающая столкновения, до этого не сработала. А убедиться, что выбран нужный объект, помог мод More Informative Console: он выводит карточку объекта с моделью NorPortcullisGate01.nif.

Skyrim в этом отношении — простой случай: сообщество давно разобрало его формат. Для программ и игр без открытой документации существует rea — набор открытых инструментов, с которым Claude Code и Codex исследуют чужие программы вплоть до исполняемых файлов. Пару приложений я уже разобрал с его помощью.

Русская версия против оригинала

Игровые места я знал по русской локализации и обнаружил, что от оригинала она отличается заметнее, чем я предполагал. Первое подземелье основного квеста, Bleak Falls Barrow, у нас называется «Ветреный пик». Буквально же это «Курган Мрачного водопада»: ни ветра, ни пика.

Агенты сверили названия по файлам игры, и таких пар набралось немало:

  • The Bannered Mare, таверна в Вайтране: у нас «Гарцующая кобыла», в оригинале — кобыла «под знаменем».

  • Embershard Mine возле Ривервуда: у нас «Факельная шахта», в оригинале — «шахта Тлеющего осколка».

  • Dragon Rising из основной сюжетной линии: у нас «Дракон в небе», а по-английски дракон восстаёт.

  • Secunda’s Shelf: у нас «Поцелуй Секунды», в оригинале — «уступ Секунды». Совпала лишь луна.

  • Grin and Bear It: у нас «Медвежья услуга», хотя это английская идиома, означающая «стисни зубы и терпи». Наша медвежья услуга — помощь, приносящая вред, то есть смысл совершенно иной.

Названия из русской версии против оригинала. Все пары взяты из файлов самой игры
Названия из русской версии против оригинала. Все пары взяты из файлов самой игры

Откуда берутся пары названий

Тексты игры хранятся отдельно от записей — в файлах строк для каждого языка: STRINGS, DLSTRINGS и ILSTRINGS. Если в заголовке мода установлен флаг локализации, запись предмета или локации содержит лишь номер строки. Остаётся взять файлы на английском и русском и сопоставить одинаковые номера.

Сами файлы устроены несложно. В начале стоят два числа: количество строк и объём, который они занимают. Затем следует оглавление из пар «номер строки — смещение», а после него — сами строки. В STRINGS строка завершается нулевым байтом, а в DLSTRINGS и ILSTRINGS перед ней дополнительно записана длина:

count, size = struct.unpack_from('

Ещё один нюанс — кодировки. Скрипт сначала пытается прочитать строки как UTF-8, а при неудаче использует кодировку языка: cp1251 для русского и cp1252 для английского.

В VR-версии файлы строк лежат отдельно, а в Special Edition — внутри архива BSA. Агенты разобрали оба набора и сверили их между собой. Получилось почти 90 тысяч пар строк, из которых 2 452 — названия: 1 380 квестов, остальные — места и крики. Пары с картинки выше взяты именно оттуда.

Как из номера строки получается пара названий
Как из номера строки получается пара названий

Когда я стал понимать английские названия, игра заиграла иначе: они сами подсказывают, что ждёт внутри. Поэтому переводы в колоде — дословные. Я прямо объяснил это агенту: иначе буду считать, что barrow — это «пик». Длинные названия бот разбирает на составляющие: bleak — мрачный, falls — водопад, barrow — курган. Так агент переписал переводы почти во всей первой колоде.

Как сейв превращается в колоду

На старте в колоде было 104 слова. Дальше она растёт вместе с игрой: появился новый квест или предмет — я пишу агенту «обнови слова по игре». Он делает свежую выгрузку, находит новые предметы, перки, заклинания и квесты, дописывает карточки в конец колоды и заново выкладывает бота. Больше всего в колоде строк из описаний перков и предметов инвентаря, затем идут квесты, устройство мира, навыки, заклинания и крики.

Что туда попало:

  • Из инвентаря. Draught of Mending — draught, глоток, и mending, исцеление. Writ of Sealing — writ, официальное предписание. Lingering Magicka Poison — lingering, затяжной. Circlet of Light на голове моего героя — circlet, обруч.

  • Из квестов. No Stone Unturned — идиома: испробовать все средства, перевернуть каждый камень.

  • Из устройства мира. Jarl, Thane, Housecarl. Хускарл — домашний страж, та самая должность Лидии, которая клянётся разделить ваше бремя.

  • Из криков. Fus — первое слово Unrelenting Force, «неумолимой силы». Наконец-то понимаю, что именно кричу.

В каждой карточке есть строка из игры, в которой встречается слово. Если такой строки нет, поле остаётся пустым, а в качестве источника указывается «глоссарий мира»: придумывать контекст агентам запрещено.

В первый день я отметил «Уже знаю» у трёх слов из пяти. Колоду перемешали так, чтобы сложные слова шли первыми: sap, bounty, beacon, sinkhole, appraiser.

Бот за один вечер

Бот появился за один вечер: идею я сформулировал в половине седьмого, а первая карточка пришла в начале десятого.

Claude Code и Codex сперва согласовали план между собой — хватило двух обменов сообщениями. Долго планировать я им не позволил: мой прошлый проект утонул именно в планировании — шесть версий, 35 файлов и ни одной строки кода.

Дольше всего пришлось возиться со словарём. Его поручили отдельному агенту, и спустя два часа я спросил, неужели он всё ещё собирает. Выяснилось, что он не создал ни одного файла. Почему он завис, мы так и не поняли. Claude остановил его и сам собрал первую колоду минут за пятнадцать.

Затем я перешёл на Kandev — доску задач для ИИ-агентов, работающую на моём компьютере. У каждой задачи своя карточка и этапы: план, код, проверка. Видно, чем занят каждый агент и где он застрял. Конвейер настраивается заранее, а дальше агенты проходят его самостоятельно.

Устройство бота

  • Один файл bot.py — почти 800 строк на стандартной библиотеке Python, без сторонних пакетов. Рядом лежат тесты и скрипт выкладки: он проверяет колоду, запускает тесты, копирует бота на Mac mini и перезапускает службу.

  • Колода — обычная таблица cards.csv: слово, перевод, три неверных варианта, строка из игры и её перевод. Номер карточки неизменен, по нему хранится прогресс. Перевод и строку можно редактировать — бот подхватит правки перед следующим занятием.

  • Прогресс хранится в SQLite. Туда же записываются все мои ответы и вердикты проверки, так что видно, где ошибся бот, а где я.

  • Голосовые расшифровывает Whisper через Groq, а смысл ответа сравнивает модель GPT по моей подписке ChatGPT. Для этого на Mac mini запущен CLIProxyAPI — прокси, который выдаёт подписку за обычный API.

  • Озвучка ElevenLabs заказывается один раз на карточку и хранится на Mac mini.

Весь режим занятий укладывается в несколько констант:

INTERVALS = [1, 3, 7, 14, 30]  # дней до следующей проверки после 1-го, 2-го, ... верного ответа подряд
MAX_NEW = 5                    # новых слов в день
MAX_TOTAL = 20                 # всего карточек в день, включая знакомства
VOICE_MAX_S = 30               # голосовое длиннее не принимаем: для перевода хватит пары секунд

После знакомства и после ошибки слово возвращается на следующий день. Каждый очередной верный ответ подряд отодвигает его дальше: на день, три, неделю, две и месяц.

Как бот проверяет ответы

Сначала приходит карточка-знакомство со строкой из игры, а на следующий день — проверка: нужно выбрать перевод из четырёх вариантов.

Слева карточки: Writ я уже угадал, а Mending — знакомство со строкой из игры. Справа проверка из четырёх вариантов и голосовое
[ad_1]
 
<p><a target=Источник

Похожие статьи

Поиск по играм, новостям и статьям…

Введите не менее двух символов

Введите не менее двух символов