Английский я теперь учу по 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 присылает из неё карточки.
Что внутри сохранения
Файл сохранения у меня весил 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 это не получится: SSH работает в отдельном сеансе Windows и до окна игры не достаёт. Codex завёл задачу в планировщике Windows, которая запускается в моём сеансе, и вспомогательный скрипт на Python. Скрипт переключает раскладку на английскую и нажимает клавиши кодами сканирования, как настоящая клавиатура. Команды такие:
prid 000A5BFBdisable
Решётка пропала, проход открылся. Команда 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('<II', data, 0)base = 8 + count * 8 # строки начинаются после оглавленияfor i in range(count): sid, off = struct.unpack_from('<II', data, 8 + i * 8) p = base + off if ext == 'STRINGS': raw = data[p:data.index(b'\0', p)] else: # DLSTRINGS и ILSTRINGS: сначала длина ln = struct.unpack_from('<I', data, p)[0] raw = data[p + 4:p + 4 + ln].rstrip(b'\0') out[sid] = raw.decode(enc, errors='replace')
Ещё одна мелочь — кодировки. Скрипт читает строки как 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 # голосовое длиннее не принимаем: для перевода хватит пары секунд
После знакомства и после ошибки слово возвращается назавтра. Каждый верный ответ подряд отодвигает его дальше: на день, три, неделю, две и месяц.
Как бот проверяет
Сначала приходит карточка-знакомство со строкой из игры, а на следующий день проверка: выбрать перевод из четырёх вариантов.
Быстро стало понятно, что варианты подсказывают. Перевод не помнишь, видишь его в списке и узнаёшь. Узнать и вспомнить — разные вещи.
Поэтому после первого верного ответа варианты пропадают: вспоминаю сам и отвечаю голосовым или текстом. Если ответ совпал с переводом из колоды или с его частью, бот засчитывает сразу. Иначе ответ уходит модели. Ей передаются слово, правильный перевод, строка из игры с переводом и мой ответ. Правила простые: засчитывать синонимы, другие формы слова, опечатки и ошибки распознавания речи, не засчитывать другой смысл и слишком общий ответ, переводить дословно, без официальной локализации. Отвечает модель двумя строками: «да» или «нет» и короткое пояснение. Если ответ не разобрать, решаю я сам кнопкой. Проверка занимает от 4 до 11 секунд.
Barrow я перевёл как «курган каких-то водопадов», и бот засчитал. На sinkhole ответил «разрыв в земле», и ИИ возразил: «„Разрыв в земле“ — это скорее трещина или разлом, а sinkhole — провал в земле». Я нажал «Я был прав». Последнее слово осталось за мной, с ИИ такое бывает нечасто.
После первого такого занятия всплыл баг: на текстовый ответ бот молчал, и казалось, что он завис. Итог проверки дописывался в карточку выше, где его не видно. Починили за час: теперь сразу под ответом появляется «⏳ Проверяю…», а следом итог с пояснением.
Почему слова читает не Макс фон Сюдов
Хотелось, чтобы слова звучали голосами из игры. Актёры там серьёзные: Макс фон Сюдов озвучил Эсберна, Кристофер Пламмер — Арнгейра, Джоан Аллен — Дельфину. Их реплики лежат в архивах игры, достать можно.
Только актёры читали диалоги. А в колоде почти всё — текст из меню: задания в журнале, названия вещей, описания перков. Вслух этого в игре никто не произносит. Для части слов можно найти диалог, где они звучат, ward наверняка говорят учителя магии. Но у многих слов таких реплик нет совсем. Агент прикинул, что раскопки займут день-два, и я решил, что с оригинальными голосами можно не запариваться.
Под каждой карточкой есть кнопка 🔊: бот присылает голосовое со словом и строкой из игры, озвучивает нейросеть ElevenLabs. Сделал специально, чтобы слышать произношение, и пользоваться прям супер удобно. Голос — Harry с пометкой «яростный воин», к Скайриму подходит. Каждая озвучка делается один раз, примерно за секунду, и хранится, повторные нажатия ничего не стоят. Если поменять строку в колоде, карточка озвучится заново. Родные голоса оставил на вторую версию.
Итоги двух недель
Сколько слов в день, решаю сам, остальное делает расписание повторов.
Пилот длился две недели. Занимался не каждый день: за первые двенадцать дней — четыре раза. Но бот мне нравится, и захожу всё чаще. На первом занятии с голосовыми ответами все 13 проверок прошли, одна — через «Я был прав». Самое приятное: захожу в игру и узнаю слова из бота, ради этого всё и делалось. Жду, когда бот пришлёт knee, тогда можно и в стражники.
Репетитора бот не заменит: он учит слова, разговору с ним не научишься. Зато слова мои, из моей игры. «London is the capital of Great Britain» там не встретишь.
Как делаю проекты с ИИ-агентами, пишу в телеграм-канале «ИИ лаба»: https://t.me/sobrano_d
ссылка на оригинал статьи https://habr.com/ru/articles/1099838/