Animal Crossing. Прославившаяся своей обаятельной, но в конечном счёте до боли повторяющейся болтовнёй. Вновь запустив этот классический хит для GameCube, я ужаснулся... жители по‑прежнему твердят те же самые фразы, что и двадцать три года назад. Ну уж нет, пора это менять.

Проблема? Игра работает на Nintendo GameCube — консоли 24-летней давности с процессором PowerPC на 485 МГц, 24 мегабайтами оперативки и полным отсутствием интернета. Она была создана — и технически, и концептуально — как замкнутый остров в офлайне.

И вот история о том, как я протянул кабель от 2001-го в сегодняшний день, заставив винтажную приставку разговаривать с облачным ИИ, не изменив ни строчки кода оригинальной игры.

Делегируйте рутинные задачи вместе с BotHub! По ссылке вы можете получить 100 000 бесплатных капсов и приступить к работе с нейросетями прямо сейчас.

Куки: «О боже, Джош :)! Мне приснился такой странный сон, будто всё, что мы делаем, — это игра! Арфер!»
Куки: «О боже, Джош :)! Мне приснился такой странный сон, будто всё, что мы делаем, — это игра! Арфер!»

Первая преграда: как заговорить с игрой ?️

Мой старт оказался удивительно удачным. В ту самую неделю, когда я взялся за проект, сообщество энтузиастов завершило гигантский труд — полную декомпиляцию Animal Crossing. Вместо того чтобы таращиться в ассемблер PowerPC, я получил доступ к вполне читаемому коду на C.

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

Взгляд на декомпилированную систему диалогов (C):

// Фрагмент исходного кода Animal Crossing после декомпиляции
// Функция, которая меняет внутренности сообщения в диалоговой системе.
// С этого места я и начал угонять текст.

extern int mMsg_ChangeMsgData(mMsg_Window_c* msg_p, int index) {
    if (index >= 0 && index < MSG_MAX && mMsg_LoadMsgData(msg_p->msg_data, index, FALSE)) {
        msg_p->end_text_cursor_idx = 0;
        mMsg_Clear_CursolIndex(msg_p);
        mMsg_SetTimer(msg_p, 20.0f);
        return TRUE;
    }

    return FALSE;
}

Просто победа, правда? Но заменить статический текст — одно дело. А вот как вживую подсунуть в игру данные от внешнего ИИ?

Первая мысль: добавить сетевой вызов. Но это означало бы писать с нуля целый сетевой стек для GameCube (TCP/IP, сокеты, HTTP) и вживлять его в движок, который для сети никогда не предназначался. Даже не вариант.

Вторая идея: воспользоваться функциями эмулятора Dolphin, чтобы писать файл на хост‑машине. Игра бы оставляла там «запрос», мой Python‑скрипт читал бы его, дергал LLM и возвращал «ответ». Увы, запертая в песочнице среда GameCube не смогла получить доступ к файловой системе хоста. Очередной тупик.

Прорыв: «почтовый ящик» в памяти ?

Решение пришло из классики моддинга: межпроцессное взаимодействие (IPC) через общую память. Смысл был прост — выделить кусок RAM GameCube в роли «почтового ящика». В этот ящик мой внешний Python‑скрипт может напрямую записывать данные, а игра — считывать их.

Сердце интерфейса «почтового ящика» (Python):

# Это мост. Функции читают и пишут данные в RAM GameCube через Dolphin.
GAMECUBE_MEMORY_BASE = 0x80000000
 
def read_from_game(gc_address: int, size: int) -> bytes:
    """Читает блок памяти по виртуальному адресу GameCube."""
    real_address = GAMECUBE_MEMORY_BASE + (gc_address - 0x80000000)
    return dolphin_process.read(real_address, size)
 
def write_to_game(gc_address: int, data: bytes) -> bool:
    """Записывает блок данных по виртуальному адресу GameCube."""
    real_address = GAMECUBE_MEMORY_BASE + (gc_address - 0x80000000)
    return dolphin_process.write(real_address, data)

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

Для этого я написал собственный сканер памяти на Python. Процесс превратился в однообразный ритуал:

  • Разговаривал с жителем. В тот миг, когда появлялось окно диалога, я замораживал эмулятор.

  • Сканировал. Скрипт прогонял все 24 миллиона байт RAM GameCube в поисках строки текста с экрана (например, Hey, how's it going?).

  • Сверял. Часто находилось несколько адресов. Тогда я размораживал игру, заводил беседу с другим жителем и искал уже его имя, чтобы понять, какой блок памяти отвечает за активного собеседника.

После многих часов разговоров, заморозок и сканирования я наконец вычислил ключевые адреса: 0x8129A3EA для имени говорящего и 0x81298360 для буфера диалога. Теперь я мог надёжно определять, кто именно говорит, и — что важнее — вписывать собственный текст прямо в окно диалога.

А что насчёт сетевого адаптера GameCube? ?

Да, у GameCube был официальный Broadband Adapter (BBA). Но Animal Crossing поставлялась без каких‑либо сетевых примитивов: ни сокетов, ни протоколов на уровне движка. Чтобы использовать BBA здесь, пришлось бы колхозить крошечный сетевой стек и патчить игру так, чтобы она его вызывала. Это значило: внедрять хуки в движок, планировать асинхронный ввод‑вывод, обрабатывать таймауты и повторы — всё внутри кода, который даже не подозревал о существовании сети.

  • Хуки в движке: перехватывать безопасные точки в цикле сообщений, чтобы отправлять и принимать пакеты.

  • Драйвер/протокол: реализовать минимальный интерфейс UDP/RPC поверх BBA.

  • Надёжность: справляться с таймаутами, повторами и неполными чтениями, не зависая при этом в анимации и UI.

Я выбрал «почтовый ящик» RAM, потому что он детерминированный, не требует ни ядра, ни драйверов и остаётся полностью внутри границ эмулятора — без какого‑либо бинарного сетевого стека. Хотя справедливости ради: схема с BBA абсолютно реальна. Более того, это была бы классная задача для будущего — например, для запуска на реальном железе через Swiss + homebrew.

Минимальная RPC‑обёртка для гипотетического BBA‑шима (C):

#include <stdint.h>

/* Минимальная RPC-структура для гипотетического BBA-шима */
typedef struct {
    uint32_t magic;    // 'ACRP'
    uint16_t type;     // 1=запрос, 2=ответ
    uint16_t length;   // длина полезной нагрузки
    uint8_t  payload[512];
} RpcMsg;

int ac_net_send(const RpcMsg* msg);          // отправка через BBA
int ac_net_recv(RpcMsg* out, int timeoutMs); // опрос с таймаутом

Хостовый UDP‑мост, упрощённая версия (Python):

import socket, json
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("0.0.0.0", 19135))
while True:
    data, addr = sock.recvfrom(2048)
    msg = json.loads(data.decode("utf-8", "ignore"))
    # ... вызов Writer/Director LLM ...
    reply = json.dumps({"ok": True, "text": " Hi from the cloud!"}).encode() // "Привет из облака!"
    sock.sendto(reply, addr)

Разговор на тайном языке игры ?

С воодушевлением я попробовал записать в память Hello, world — и… игра зависла. Персонажи продолжали анимированно шевелиться, но диалог не двигался дальше. Так близко — и так далеко.

Оказалось, что я отправлял обычный текст. А Animal Crossing на таком не говорит. У неё свой собственный язык — закодированный, с особыми управляющими символами.

Представьте себе HTML: браузер не просто показывает слова, а распознаёт теги вроде <b>, чтобы сделать текст жирным. Так же и тут. Специальный байт‑префикс CHAR_CONTROL_CODE сообщает движку игры: «Следующий байт — это не буква, а команда».

Эти команды управляют всем: цветом текста, паузами, звуковыми эффектами, эмоциями персонажей и даже завершением разговора. Если не передать код <End Conversation>, игра будет вечно ждать команду, которая так и не придёт. Вот почему всё зависало.

И снова сообщество декомпиляторов выручило меня: они уже задокументировали большую часть этих кодов. Мне оставалось лишь собрать инструменты, чтобы ими пользоваться.

Я написал энкодер и декодер на Python. Декодер умел читать сырой участок памяти и переводить его в удобочитаемый вид, а энкодер — брать мой текст с самодельными тегами и превращать его обратно в точную последовательность байтов, понятных GameCube.

Небольшая часть управляющих кодов, которые пришлось кодировать/декодировать (Python):

# Пример набора управляющих кодов для Animal Crossing
CONTROL_CODES = {
    0x00: "<End Conversation>",
    0x03: "<Pause [{:02X}]>",        # напр., <Pause [0A]> — короткая пауза
    0x05: "<Color Line [{:06X}]>",  # напр., <Color Line [FF0000]> — красный
    0x09: "<NPC Expression [Cat:{:02X}] [{}]>", # вызвать эмоцию
    0x59: "<Play Sound Effect [{}]>",  # напр., <Play Sound Effect [Happy]>
    0x1A: "<Player Name>",
    0x1C: "<Catchphrase>",
}

# Магический байт, сигнализирующий: дальше идёт команда
PREFIX_BYTE = 0x7F

С новым энкодером я попробовал снова. И на этот раз я говорил на языке самой игры. И это сработало. Самая трудная часть хака была позади.

Создание мозга ИИ ?

Когда канал связи заработал, настал момент для самой весёлой части — построения ИИ.

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

Решение оказалось простым: я разделил процесс на два модуля — Писателя и Режиссёра.

  • ИИ‑писатель. Его единственная задача — креатив. Он получает подробное досье на персонажа (я собрал их, скрапнув фанатскую вики Animal Crossing) и пишет диалог, который смешной, соответствует его характеру и уместен в текущем контексте.

  • ИИ‑режиссёр. Он берёт чистый текст от Писателя и работает чисто технически: решает, как поставить сцену. Добавляет паузы для драматизма, выделяет слова цветом, подбирает эмоцию на лице или звуковой эффект в точный момент.

Это разделение сработало идеально.

Эффекты «саморазвития» ?

Сначала я подключил лёгкую новостную ленту. И буквально через несколько минут жители начали вплетать заголовки в свою болтовню — без промптов, просто на основе контекста.

Митци: «Новости? Европейские лидеры собираются встретиться с Трампом и Зеленским!»
Митци: «Новости? Европейские лидеры собираются встретиться с Трампом и Зеленским!»

Потом я дал им крошечный общий блок памяти для сплетен: кто что сказал, кому и как это восприняли. Предсказуемо всё вылилось в движение против Тома Нука.

Куки: «В городе всё идёт прекрасно, но иногда мне кажется, что Том Нук типа забирает все колокольчики!»
Куки: «В городе всё идёт прекрасно, но иногда мне кажется, что Том Нук типа забирает все колокольчики!»

А потом я вспомнил, что источником новостей у меня была Fox News.

Куки: «Женщина была убита во время ограбления в „синем городе“!»
Куки: «Женщина была убита во время ограбления в „синем городе“!»

Теперь игра превратилась во что‑то странное, уморительное и слегка тревожное :)

Весь код проекта — от интерфейса работы с памятью до энкодера диалогов и логики подсказок для ИИ — лежит на GitHub. Это, пожалуй, один из самых трудных и при этом самых захватывающих экспериментов, которые я когда‑либо делал: сплав реверс‑инжиниринга, ИИ и пристрастия к классике.

Комментарии (0)


  1. Artyomich
    15.09.2025 12:28

    https://habr.com/ru/articles/946194/