Путь длиной в 5 лет. Как я перевёл две огромные культовые JRPG на PS1, и не сошёл с ума. [УЛЬТРАЛОНГ]

Привет. Я автор переводов дилогии Persona 2: Innocent Sin и Persona 2: Eternal Punishment для PlayStation 1, и хочу рассказать о своём пути.

Как вы, наверное, знаете, Innocent Sin для PS1 не получила официальной английской локализации (про причины не будем), а вторая часть истории, Eternal Punishment, почему-то была переведена, и смутила своим сюжетом западных игроков. «О чём говорят эти люди в сюжете?! Какого хрена тут творится?!». Игра отличная, но сюжет для мира, не знающего японский, начинался с середины. Официальная западная локализация IS вышла уже для ремейка на PSP, а EP для PSP так и осталась на японском. Силами фанатов были выпущены переводы, как IS для PS1, так и EP для PSP, хотя бы на английский язык. Я же в своё время решился на сомнительный шаг по переводу этих игр на великий и могучий. Хотя, что значит, «решился»? Я просто не сразу осознал во что ввязался, но было уже поздно.

Так как мы приближаемся к релизу второго перевода (EP), я хотел бы поделиться увлекательной историей их создания. Так сказать, подвести итог в конце этого невероятного «переводческого» путешествия.

Надеюсь, будет интересно почитать, как вообще создаются фанатские переводы в деталях и изнутри. А может и сподвигну кого-нибудь на подобное (не делайте этого!). И вообще, хочется просто поделиться накопленными знаниями и рассказать через какие адские японские жернова я прошёл.

Лонг не претендует на роль энциклопедии или туториала, но кое-какие технические вопросы он закрыть может.

Предупреждаю заранее — технических деталей будет очень много. Если не боитесь — добро пожаловать!

Закапывая семена идей

Так уж вышло, что с давних времён, почти вся моя деятельность связана с компьютерами и программированием. А как хобби, я всегда интересовался видеоиграми и их устройством. В своё время мы с другом даже пилили большой и серьёзный инди-проект, который, к сожалению, заглох, из-за огромных объёмов работы, недостатка компетенций и ресурсов, и из-за различных жизненных ситуаций. Да и ладно — это был довольно ценный опыт (но мы всё ещё теплим надежду что-нибудь создать).

Я неспеша наслаждался жизнью, даже не думая ни о каких переводах. Да, ковырял какие-то старые игры на досуге ради спортивного интереса, изучал форматы хранения данных, и всё это без всякого смысла и цели.

И вот…

Однажды ко мне приехала только что вышедшая портативка Retroid Pocket 2. Давно хотел что-нибудь портативное — лишь бы тянула SMD/SNES/PS1/PSP. Залив туда кучу игр детства, я переиграл за несколько месяцев во все тайтлы, которые хотел освежить в памяти (FF7-8/ChronoTrigger/-Cross/MGS и прочие SH/RE).

Параллельно на YT мне попались видео Филинова (не кидайтесь какашками) про некую серию Shin Megami Tensei: Persona, которая брала своё начало как раз на PS1. Это удивительно, но я впервые слышал об этой серии игр (хотя на ps3 уже вышла Persona5, а в обсуждениях на ПДФ частенько встречались фанаты этой вашей Персоны). Ещё более удивительно, но меня жутко зацепила атмосфера, и то, как это непривычно выглядело и игралось.

Тут же навалив на портативку Persona2: Innocent Sin с английским патчем от неких Tom&Gemini, я включил её и….. пропал. Я буквально провалился с первой же секунды первого данжа. Не буду расписывать свои эмоции, но игра мне зашла так, как не заходило ничего очень давно. Что ж…

Сколько народу полегло…

«А что там с переводом этой игры на русский язык? Ведь все крупные тайтлы для ps1 уже давно переведены» – думал я, мучая Гугл запросами. А Гугл лишь отмахивался, подсовывая вместо ссылок на скачивание какой-то форум megaten.ru. Зайдя на него, и обнаружив тему о переводе на 25 страниц, я знатно офигел, узнав, что перевод так и не увидел свет, спустя >10 лет разработки. За перевод бралась неплохая команда переводчиков, затем ещё один парень на полном серьёзе обещал перевести игру “засев на пару недель”, но всё это заканчивалось эпичным “ничем” — то переводчик руку сломал, то «хакеръ пропалъ». Позже мне писал парень из ещё одной команды и говорил, что тоже занимался переводом.

Вот же… Или эта игра проклята, или адски сложна в разборе и переводе. А, скорее всего, и то, и другое.

Как это вообще работает?

Ночью мне не спалось. На часах 3 ночи, а меня мучает жажда и мысль «почему на образе перевода IS от Tom & Gemini нет ни одного файла, кроме исполняемого?!». Встав попить, я решил «на секундочку» сесть за компьютер и скачать японский образ игры. К моему удивлению, на образе оригинального диска имелось 1000+ файлов, которые отлично открывались и просматривались.

«А как же ты тогда работаешь, если тебе пофиг на таблицу файлов на диске?! Ты явно хочешь поиграться со мной, кусок хитрого японского кода!».

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

То, что со мной происходило дальше — уже не важно. Погружаемся в техническую часть!

Так вот, решив действовать решительно, я начал копать инструменты, чтобы узнать, как эта шарманка работает и читает данные. До этого я лишь краем уха что-то слышал о дизассемблировании кода с приставок. Знал, что кажется, есть такая штука, как IDA. Гугл вывел меня на Хабр, где парень с помощью какой-то Ghidra и плагина для загрузки исполняемых файлов PSX, потрошил Twisted Metal. Ресурсы меня пока что интересовали мало, а вот как выглядит код было интересно. Усмехнувшись тому, что Ghidra была разработана АНБ США, я, скачав её, + плагин для PSX (того парня с Хабра), и на всякий случай включив фаервол (АНБ умрут от кринжа, увидев мой двач-пак), загрузил туда исполняемый файл японского оригинала Persona2 IS под названием SLPS_021.00.

Импортировав файл, Ghidra пытается раскомпилировать и разбить весь код на функции, сделать ссылки на другие функции, привязать данные, распарсить входные параметры на основе регистров, и самое главное, пытается определить функции стандартной библиотеки Psy-Q и степень их энтропии (отклонения от стандарта). По сути, мы получаем сырые “недоисходники”, разбитые на функции, с возможностью просмотра псевдокода на C. То, что надо!

Пара слов о библиотеке Psy-Q.

С давних времён, игры для приставок разрабатывали с помощью DevKit-ов.

Так вот, для PS1 с какого-то момента стандартом разработки стало решение от Psygnosis — Psy-Q. По сути, это 2 огромные ISA-платы для ПК, эмулирующие PlayStation, и несколько дисков с компиляторами, демками и библиотекой. Сия библиотека здорово упрощала жизнь разработчикам — на борту имелись готовые функции работы со всем железом PS1, сотни готовых решений на все случаи жизни. Эдакий framework из 90-ых. Библиотеки компилировались прямо в игровой код и сейчас на 70-80% определяются Ghidr-ой (вместе с названиями!), позволяя увидеть, что и когда использует игра.

Любителям нырнуть поглубже, рекомендую почитать официальную документацию, например вот тут.

Более того — на Github есть обучающие проекты, использующие Psy-Q, например, вот этот. Последовав инструкции, вы даже сможете скомпилировать нечто, что будет работать на PS1 и эмуляторах (очередной Хэллоу Ворлд, но сам факт!). А в будущем, чем чёрт не шутит, написать свою игрульку как это делали предки. Все функции Psy-Q определяются Ghidr-ой и даже работают (я проверял).

Итак, я загрузил код исполняемого файла в Ghidra.

Немного побродив по вызовам основной функции main(), я обнаружил прямую загрузку какого-то файла с указанием конкретного сектора на диске (нейминг функций уже мой, Ghidra ещё не настолько молодец). Да, это оказалась таблица всех ресурсных файлов, которая грузится прямо из сектора 0x17. И на этот же сектор указывает файл с названием “FILEPOS.DAT”. Сама файловая таблица представляет собой массив структур uint32 sector, uint32 size. То есть, простейшее указание на секторы и размеры файлов.

Что же это значит?

Ну… При таком подходе не получится работать с диском классическим методом “распаковываем образ, изменяем файлы, собираем образ”. При запаковке нужно учитывать положение всех файлов, да и сама таблица файлов должна встать точно в читаемый сектор. Да и неизвестно, в какие сектора лягут файлы после сборки образа, чтобы заранее прописать их в FILEPOS.DAT. Пу-пу-пу…

На самом деле, плюсы данного подхода работы с файлами неочевидны. Зачем и почему Atlus отказались от работы через стандартные процедуры Psy-Q? Ответ прост. Подавляющее большинство игр для PS1 используют функции по типу dsSearchFile, которые, чтобы загрузить файл, перемещают головку в начало диска, ищут имя файла, запоминают сектор, передают управление обратно и т.д. А если нужно загрузить 10-15 файлов? А если файл нужно читать не сначала? Время загрузки одной сцены может растянуться на секунды, что, в их случае, было недопустимо. Что сделали Атлус? Они написали свои процедуры чтения файлов. Раз файловая система уже в памяти, значит мы экономим уйму времени на поиск нужных секторов. Номер сектора просто передаётся процедуре чтения из файловой таблицы в RAM. Таким образом, в игре отсутствуют загрузки как таковые. Нет, они, конечно, есть, но происходят молниеносно.

Это всё замечательно, но мне-то от этого не легче! Даже наоборот — как это ломать? Здесь я принял стратегическое (и верное) решение работать с образом напрямую, написать слой абстракции для декодирования образа и доступа к файлам, основываясь на внутренней таблице файлов игры, без распаковки-запаковки, просто меняя данные прямо в файле образа.

Логика основных функций чтения с диска (P2EP). Прямое чтение (overlay) и чтение ресурсных файлов с распаковкой. Psy-Q здесь используется только для прямого чтения сектора в RAM. Параллельно с чтением производится распаковка да
Логика основных функций чтения с диска (P2EP). Прямое чтение (overlay) и чтение ресурсных файлов с распаковкой. Psy-Q здесь используется только для прямого чтения сектора в RAM. Параллельно с чтением производится распаковка да

Думаете, найдя ответ на главную файловую загадку Innocent Sin, я смог спокойно спать? Ха! Очевидно, что я решил копнуть чуть глубже…

Погружаемся в архитектуру консоли

А что там с изображением? В конечном итоге, нам ведь нужно будет его менять. Как формируется картинка? Какая магия заставляет пиксели на экране менять цвет?

Запасайтесь попкорном.

PS1 использует несколько способов передачи данных в GPU, но нас интересует самый производительный из всех — передача начала связанного списка видеокоманд в DMA-контроллер. Не пугайтесь! Сейчас всё объясню.

У PS1 есть DMA(Direct Memory Access)-контроллер — специальный “диспетчер”, который умеет читать данные напрямую из памяти и передавать их разным устройствам и наоборот. Звуковой чип, декодер видео, CD-ROM, GPU — все они работают через DMA. Центральный процессор лишь готовит данные, устанавливает регистры DMA и активирует нужный канал DMA. Например, для чтения с диска, процессор отдаёт задание “прочитай такие-то секторы, потом передай управление такой-то подпрограмме” и идёт заниматься своими делами, а в памяти чудесным образом появляются данные.

В случае с видеочипом, процессор готовит связанные списки видеокоманд в памяти, которые собираются в OrderingTable (нам ведь надо как-то сортировать, что отрисовывать раньше, а что позже). OrderingTable, по сути, это массив указателей на цепочки видеокоманд. DMA проходит по ним и вкидывает цепочки в GPU, который их исполняет.

И мы приближаемся к самому интересному — как выглядит цепочка видеокоманд и что в ней прописывается?

Итак, раз уж у нас тут 32-битная консоль, то и все команды четырёхбайтовые (32 бита = 4 байта, неожиданно!). Сама команда выглядит проще некуда. Взгляните:

041C4108 - заголовок
64808080 - команда "Rect" (копирование спрайта на экран)
00250027 - координаты на экране
7FD3F0C0 - исходные координаты в VRAM
000C0020 - размер

Давайте разберём подробнее:

041C4108 в начале — это всегда заголовок. 04 — количество "слов" видеокоманды (4 команды по 4 байта). Следующие 3 байта — линкующий адрес следующей видеокоманды в цепочке. В данном случае 1C4108. Если прикрутить в начало 80 (префикс основной RAM PS1), то получим 801C4108. Если мы перейдём по этому адресу, то попадём на заголовок следующей видеокоманды (к слову, команда может находиться в другом конце RAM). Далее команда 64808080. 64 — это команда копирования спрайта из любого места VRAM, 80-80-80 — RGB-шейдинг. С помощью этих цифр делаются фейды, смена цвета текста и т.д. Затем идут технические данные команды: 25х27 — это конечные координаты на активном буфере. С 7FD3F0C0 чуть интереснее — здесь битовым смещением зашифрована текстурная страница VRAM, координаты внутри страницы и CLUT (палитра). В общем, откуда из видеопамяти мы будем копировать данные спрайта на экран. Ну а последняя строка 0C0020 — банально размер спрайта. 12х32 пикселя.

На скрине ниже мы видим VRAM — всю видеопамять на данный момент, размером 1024x512 в режиме 16bit. Справа - цепочка команд текущего кадра, чуть ниже описание команды, ещё ниже текстурная страница в режиме 4bit, с которой копируется спрайт (зелёным я отметил положение спрайта на общей карте). Остальное — это текстуры и спрайты, которые точно так же копируются на экран такими же видеокомандами. Если брать 3D-графику, то координаты текстур на экране высчитываются CPU+GTE (Graphic Transformation Engine), записываются в цепочку, а в GPU идут уже готовые команды “отрисуй-ка, друг, треугольник/квадрат с такой-то текстурой из VRAM там-то на экране”. Вот так просто работает графика на PS1. Если хотите изучить видеокоманды подробнее, то рекомендую вот этот исчерпывающий документ.

Видеопамять (VRAM) PlayStation 1. 1024х512 в режиме 16 бит. Целый 1Мб!
Видеопамять (VRAM) PlayStation 1. 1024х512 в режиме 16 бит. Целый 1Мб!

Помимо команды копирования спрайта на экран Rect (64), нам стоит знать о команде CpuToVRAM (A0). Команда отвечает за отрисовку в видеопамяти любых изображений и применяется для загрузки текстур, формирования текста и прочего, только подавай данные в 4-х битном формате.

По сути, это 2 основные команды, которые формируют текст на экране. Сначала символ формируется в видеопамяти (CpuToVRAM), затем копируется на экран (Rect).

И вот… Прочитав гору информации на тему архитектуры PS1 и вооружившись дебаггером и эмулятором no$psx, я сел ковырять японский образ Persona2: Innocent Sin. Собирался ли я тогда что-то переводить? Конечно, нет. Просто было интересно — как это работает?

План был простой — для начала найдём, поковыряем эти ваши видеокоманды напрямую в памяти, и посмотрим, что получится.

Видеокоманды в естественной среде обитания. Справа DMA Link, указывающий на память. В памяти хорошо видно заголовки со ссылкой на следующую команду и сами команды. Обратите внимание, что байтовый порядок (в каждом word-е/4 байтах) обратный (little-endian)
Видеокоманды в естественной среде обитания. Справа DMA Link, указывающий на память. В памяти хорошо видно заголовки со ссылкой на следующую команду и сами команды. Обратите внимание, что байтовый порядок (в каждом word-е/4 байтах) обратный (little-endian)

Если запустить эмулятор no$psx, остановить игру (Esc), открыть окно VRAM (F5) и внимательно посмотреть на список команд, то можно заметить повторяющийся паттерн (он же есть на скрине выше)

CpuToVram3x6, CpuToVram3x6, Rect12x12, CpuToVram3x6, CpuToVram3x6, Rect12x12…

Это, друзья мои, ничто иное, как повторяющиеся 2 команды формирования символа в VRAM и 1 одна команда копирования сформированного спрайта на экран. Можно даже заметить иероглиф в окне текстурной страницы внизу справа — именно там формируется символ. Почему CpuToVram размером 3x6 и почему повторяется 2 раза? Всё просто — иероглиф формируется двумя полусимволами 3х6 в 16-битном пространстве, но так как спрайт формируется в 4-битном режиме, 12 пикселей по ширине превращаются в 3 (в 16 битах). По высоте никаких изменений нет 6+6 = 12. Итого готовый игровой символ занимает 12х12 пикселей на экране.

Я же, посмотрев на данные, которые передаются в CpuToVram, решил изменить их в реальном времени прямо в RAM эмулятора. Сделать это оказалось просто - так как у нас 4-ёх битный режим отображения, то в каждом полубайте хранится цвет пикселя (то есть 16 возможных значений на пиксель). Например, на скрине выше видно BC 00 CB 00 BB - это B синий цвет и C - его тёмно-синяя тень. Палитра выглядит как 00 - прозрачный, 01 - белый, 02 - серый (тень белого), 03 — жёлтый, 04 — грязно-тёмно-жёлтый и т.д. Каждый цвет - нечётный индекс. +1 от него — цвет его тени. Таким образом очень просто добавлять тень, просто прибавляя единицу к цвету.

Нарисовав примерный шрифт и перенеся символы в видеопамять, я получил первые результаты на работающей игре.

Да, это первый скрин перевода. Ну что ещё можно было написать?
Да, это первый скрин перевода. Ну что ещё можно было написать?

И тут начинается главное веселье — шрифт должен попасть в игру, а скрипты показывать нужные буквы. Для этого нам нужно сделать очень много манипуляций.

Я тебя победю, скотина!

Добро пожаловать в самый замороченный и интересный раздел. Здесь начинается работа над переводом.

Итак.

До этого момента у нас было 0 написанного софта, но, похоже, что настало время достать главную имбу — vb.net! Его инструментарий тогда полностью меня устраивал, да и можно было просто работать с большими файловыми массивами (даже с целым образом на 700Мб), не особо заморачиваясь со сложным синтаксисом. Да, топорно. Да, некрасиво. Да, код как из одного места, но это работает! Кастомные классы собираются, списки/картинки рисуются, кнопки/элементы управления перехватываются, что ещё нужно для быстрого прототипирования?!

Готовый интерфейс Innocent Sin CD Toolkit
Готовый интерфейс Innocent Sin CD Toolkit

Первым делом я написал процедуры показа списка файлов из таблицы с 0x17 сектора, функции замены данных в секторах напрямую в образе, выгрузку файла из нужных секторов.

Следующим важным шагом было найти где и чем формируются видеокоманды текста в RAM? С дебаггером это оказалось несложно — мы просто смотрим, куда пишутся видеокоманды (см. выше), включаем “машину времени” — делаем сейвстейт, берём адреса видеокоманд, ставим брейкпоинт на изменение байта по адресу видеопамяти с формированием спрайта (например, брейкпоинт вида [8001B9370]!, который остановит программу при записи по этому адресу), загружаем сейвстейт и … вуаля! Мы останавливаемся в функции формирования символов. Зайдя по этому адресу в Ghidra, мы видим огромную процедуру, которая рисует буквы. По первой ни черта вообще непонятно, но было очень важно разобраться в ней досконально в будущем, чтобы понять, что она делает. Ну а сейчас нам нужно хотя бы уменьшить расстояние между буквами, найдя в цикле +12 к координате X каждого следующего символа, чтобы сделать шрифт поУже.

Подпрограмма печати текста. Отмечена константа ширины символа (12 пикселей)
Подпрограмма печати текста. Отмечена константа ширины символа (12 пикселей)

К слову, эта техника поиска подпрограмм через перехват изменений памяти была основной механикой перевода и использовалась мной очень много. Другое дело, что зачастую я натыкался на обычные подпрограммы копирования памяти, а оттуда в свою очередь, ещё какое-нибудь копирование, а там сбор данных через 3 подпрограммы. И эти цепочки поиска были довольно сложны, но по-другому источник записи данных не определить… Отлов цепочки вызовов — один из важнейших навыков реверсинга PS1, так что берите на вооружение сразу. В более совершенных системах, например, PSP, уже имеется стек вызовов, который сильно упрощает дебаг.

Что ж, перед нами стоят задачи:

  • Изменение ширины букв (подпрограмму в Ghidra уже нашли, в псевдокоде нашли где это прибавляется и тут же посмотрели адрес значения в самом ASM-e)
  • Поиск и изменение шрифта
  • Поиск и изменение текста в скрипте
  • Обратная запаковка этого добра на диск

Мдямс.

Первым делом мы находим x += 12 при формировании иероглифов в исполняемом файле и пишем патчер-кнопку из интерфейса CDToolkit. Но это дерьмовый подход — “писать кнопку-патчер для каждого изменения”, потому я сделал простой экспорт-импорт исполняемого файла из образа и менял его в HEX-редакторе (нужно лишь учитывать смещениe 0xF800 — это адрес, по которому он грузится в RAM). Затем написал экспорт-импорт любого файла по внутренней таблице файлов, что сэкономило мне сотни часов на распаковку-запаковку всего образа.

Найс. Осталось только сделать каждую букву в 2 раза уже и поменять расчёты координат.
Найс. Осталось только сделать каждую букву в 2 раза уже и поменять расчёты координат.

Так. Ширину пропатчили! Есть первые результаты “хакерства”… Что же дальше? Дальше начинаем сильнее курить процедуру формирования символов, чтобы понять — откуда же читается шрифт. Провалившись в одну из подпрограмм, вызывающихся каждый символ, мы обнаруживаем чтение из какой-то области памяти куда-то, какие-то расчёты, копирование…

Подпрограмма формирования символа
Подпрограмма формирования символа

Если присмотреться, видно, что шрифт просто копируется из 801ef480 через таблицу трансляции 1bpp → 4bpp (по адресу 1f8002e0) в области ScratchPad, ультрабыстрой временной памяти PS1, размером 1Kb, находящейся по адресу 1f800000. Именно там “черно-белый” шрифт превращается в пригодный для копирования в видеокоманду четырёхбитный формат. И тут же применяется цвет. Как? Простым умножением на код цвета (curColorIndex). То есть, имея символ из 0010111010101, умножая на A (синий), получаем 00A0AAA0A0A0A. Следом в коде идёт опциональная непростая обработка тенью, которая добавляет цвет+1.

Ну что ж, теперь мы знаем откуда копируется шрифт. Лезем смотреть данные по этим адресам… Там мы обнаруживаем шрифт в формате 1bpp. А что, если поискать в образе по этим данным? В каком файле на диске он лежит? А его нет! Ничего не находится! Возвращаемся к тактике установки breakpoint на адресе нахождения шрифта, перезапускаем игру и… АГА! Копирование из какой-то процедуры, лезем в Ghidra и видим… Процедуру распаковки…

Процедура распаковки LZ77
Процедура распаковки LZ77

Хе, приехали — данные запакованы. Продебажив процедуру, мы находим реальные сырые данные, из которых распаковывается шрифт. И да, теперь эти данные ищутся на диске! Ура? Как бы не так. Шрифт находится в некоем “пакете” данных. Не буду вас утомлять как я это всё искал и дебажил, скажу лишь, что большинство файлов на диске (а это 1100 с чем-то) — это “пакеты” файлов. Каждый файл там содержит мини-заголовок, тип файла, размер расжатый, размер сжатый, тип сжатия, а для изображений и текстур сразу указаны координаты загрузки в VRAM (что весьма практично).

Всего в игре существует 3 типа архивов, “без сжатия” — обычно так хранятся данные, которые нужно загрузить максимально быстро — разговоры с демонами, иногда код. Тип сжатия LZ77 для бинарных данных и банальный RLE для изображений.

LZ77 здесь имеет свой несложный формат — идём побайтово, если у нас readByte ≥0x80, то размер чанка для чтения = readByte - 0x7D (ведь нет смысла кодировать 1 или 2 байта), а следующий байт — это смещение окна назад из уже прочитанного. Если readByte<0x80, то просто читаем следующие readByte байт. Вот и вся компрессия.

RLE для изображений ещё проще — там вообще нет окна смещения и указывается нужное количество повтора байта.

В PSP-версиях пакеты абсолютно такие же, но используется более модное нативное deflate-сжатие, что упрощает работу

Что ж, спустя много времени, я разобрал файловые пакеты, написал их просмотр и распаковку.

Выковыряв, и открыв шрифт программой 7yuv, я обнаружил, что первая часть — это обычный шрифт 8х12, которым пишутся арканы, цифры и прочие АНГЛИЙСКИЕ надписи. А вот дальше было посложнее, потому что шрифт у нас 12х12, то есть мы имеем 8 первых бит в первом байте, во втором 4 бита от предыдущей строки, 4 бита следующей строки и дальше 8 бит остатка второй строки. То есть, в 3 байтах у нас шифруется 2 строки шрифта — итого 3*6 (12/2) = 18 байт на символ. В общем, пришлось писать конвертер в 16x16, чтобы без проблем отредактировать шрифт в YY-CHR, и обратный в 12x12 для импорта.

Рисуем шрифт
Рисуем шрифт

Дальше была работа со скриптами — смотрим ОТКУДА читаются символы в скриптах процедурой показа текста и таким же образом перехватываем запись распаковки, находя оригинальные сжатые данные. Таким образом обнаруживаем, что первый скрипт начала игры находится в файле 0181. Пак содержит в себе всю сцену целиком — текстуры сцены, 3d-данные, коллизии, ещё какую-то муть и, главное, последним всегда идёт скрипт для скриптовой виртуальной машины, который и содержит в себе текст.

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

Структура скрипта оказалась не так уж сложна:

  • смещения на области скрипта: подпрограммы, команды, параметры команд, текстовые данные.
  • Подпрограммы и их смещения в командах
  • Команды и смещение на параметр для каждой
  • Блок параметров каждой команды
  • Блок текстовых данных

Это действительно крутая структура, потому что скриптовая ВМ игры поддерживает паузы, многопоточность, проверку переменных, переходы (условные/безусловные), выборы, изменение переменных, запуск данжей/видео, перемещение персов, показ текста и прочее-прочее…

Самописный парсер скриптов Persona2.
Самописный парсер скриптов Persona2.

В тот момент мне нужно было понять, что команда 0x13 — это запустить команду показа текста, следом за ней идёт смещение в таблице параметров, в которой идёт смещение на начало текста внутри текстового блока. (Команда 0x13 → Смещение в таблице параметров → Параметр смещение в тексте → Текстовый блок).

То есть, для перевода мне нужно было постоянно пересобирать все текстовые окна и смещения на них. Причём текст показывался не обычной командой “показать текст”, а потоком для printCommandText, который обрабатывался до конца текстового окна. В потоке были команды смены цвета, перевода строки, и в конце почти всегда команды ожидания нажатия, вайпа текста и завершения текстового потока. Были и команды вставки фамилии/имени ГГ, выборов и прочего.

Были написаны специальные конвертеры в txt, который содержал в себе весь скрипт (в виде чисел) и текст для редактирования (то есть самодостаточный скриптовый txt) + обратный конвертер в формат скрипта игры. Всё это постоянно улучшалось, дорабатывалось и прошло несколько десятков итераций, прежде чем игра поверила, что это ЕЁ СКРИПТ.

Путь длиной в 5 лет. Как я перевёл две огромные культовые JRPG на PS1, и не сошёл с ума. [УЛЬТРАЛОНГ]

После того как я нарисовал шрифты, и сделал один скрипт с нашим РУССКИМ текстом, встал вопрос обратного импортирования этого добра.

Вроде как, всё просто — запаковывай, да импортируй в образ, но …ага, ага. На деле кастомный запаковщик LZ77 знатно поломал мне голову (его-то в исходниках не было, хы). Этих ваших дипсиков ещё не существовало (но, кстати, он не особо мне помог — я потом пробовал) и мне пришлось сооружать свой запаковщик на основе деревьев, подбора лучшей стратегии от каждого байта и подгонять это всё так, чтобы распаковщик игры не заподозрил подмены.

Наконец, собрав всё в кучу, пересобрав пакеты скриптов, шрифта и прочего… Что-то заработало! Игра загружала мой текст. Но ивенты и скрипты — это лишь 30% всей игры. Меню и прочее я нашёл в новых для себя списках, для которых не было нормального обработчика (а там свой очередной формат с таблицей смещений). Дальше написал черезпопные конвертеры, лишь бы хоть как-нибудь это всё залезло в игру и показывалось. Перевёл первые локации и сцены, иногда что-то софтлочилось, иногда что-то не лезло между файлов (там надо было уповать на гэп до конца сектора), но это всё было неважно — игра работала! Дополнительно были дописаны пересчёты длин текста для расчётов (текст посередине экрана, переносы при переключении текстовых строк).

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

Это работает, но… никуда не годится

Если вы были внимательны, то помните, что я писал о рендеринге иероглифов. Это 2 команды формирования полусимволов 6х12 и одна команда копирования спрайта. При большом количестве русского текста на экране графическая подсистема нагружалась куда сильнее. Русский символ 6x12 зачем-то рисовался двумя полусимволами, которые были в 2 раза уже по ширине. А учитывая, что русских символов, в среднем, в 2 раза больше, чем японских иероглифов, то картина вырисовывалась довольно мрачная.

Следующим камнем преткновения был формат хранения текста — в 99% случаев русским ASCII-символам не требовалось больше одного байта. У нас же весь текст выглядел как FA 00 FB 00 FC 00 FD 00 и т.д. Двухбайтовая японская система хранения никуда не годилась, текст просто занимал бы в 2 раза больше места, половина из которого — бесполезные нули.

Дальше моё любимое — память под цепочки символов в RAM. Которая весьма ограничена. В разных локациях этой памяти было разное количество и оно было далеко не бесконечно. Например, в данжах и боёвке это был очень ценный ресурс. Нельзя было просто взять и отрисовать больше, например, 100 символов, иначе игра крашилась. 100 символов, на секундочку, это 300 команд (2 на символ + копировать спрайт). И даже если использовать память экономно, то не всегда старый текст успевал освободить цепочки под команды, пока рисуется новый текст и, например, в боёвке были абсолютно рандомные софтлоки.

Но ведь можно подсмотреть как решил эту проблему Tom в английском патче? Можно. А решил он эту проблему на корню —

  • переместил весь английский шрифт в текстуру интерфейса (всегда в VRAM)
  • текст рендерится просто — 1 команда копирования спрайта на букву — найс, экономия до 300% RAM! Да и скорость отображения высоченная (потому не тормозит меню персонажей на PS2, например, а на японке тормозит).
  • заменил всё хранение текста на однобайтовую систему и поменял всю обработку управляющих кодов текста — вместо двухбайтовой системы кодов была однобайтовая, что добавило ему геморроя, о чём он писал, кстати. Плюсы очевидны — весь текст и управляющие коды ужались в 2 раза, НО было большущее НО — если раньше были иероглифы 0x3201, 0x1500, 0x1300, далее — управляющие коды 0x0611, 0x0211, 0x0311 (определялись по единице второго байта, то есть, если & 0x0010, то это управляющий код), это легко считывалось, второй полубайт указывал на количество управляющих байт после (0x2D12 — это смена цвета, а &000F = 2, то есть скачем на 2*2 после команды, 0x0311, скачем только через этот код после выполнения). Тому же пришлось городить костыли и какие-то свои таблицы перемещений и прочего.

Можно ли было перенять его опыт? К сожалению, нет. В текстуру интерфейса не поместить 2 шрифта (а у нас русский + английский). Либо нужно отказываться от латиницы. А может и от спецсимволов. Найс. Да и слишком мелким мне кажется его шрифт, хоть и хорошо помещается где угодно. И на CRT выглядит очень не очень.

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

  • Нужно писать своё чтение и формирование цепочек текста. Да...
  • Появилась идея МОЕГО управляющего кода для МОЕГО однобайтного текста
  • То есть, все вызовы отрисовки текста всей игры перенаправляются на МОЮ процедуру, которая смотрит — есть ли мой управляющий бит в команде? Если нет, то вызывается оригинальная японская процедура. Если да, то рисуется МОЙ ОДНОБАЙТОВЫЙ ТЕКСТ.
  • Таким образом совмещаются и иероглифы, и спецсимволы (всякие оригинальные сердечки!), и мой текст, найс!
  • Сделать управляющим кодом 0xXX20. Где XX — длина строки, позволяет ускорить расчёты длины текста без перебора текста (чтением заголовка), а 0x0020 не детектилась оригинальным детектором управляющих кодов (там проверялось & 0010), а я проверял >>0x0D, то есть наличие 13-ого бита смещением.

Что ж. Всё придумано, но… Ведь это… надо писать? Но как, Холмс? Накидаем примерный план.

Помните? — у нас ведь есть имеется оригинальная процедура формирования английского шрифта 8x12 (часы, числа в магазинах и т.д.), а она, на секундочку, использует всего 1 команду на формирование символа + 1 команду копирования на экран на символ. Итого — вместо трёх (в иероглифах) команд две. На треть команд меньше. Да и в ширину всего 8 пикселей, а не 12. Допустимо.

  • У нас есть наш родной нарисованный шрифт 6х12. Хорошо бы его вживить.
  • Туда же прикрутить однобайтное хранение и чтение.
  • Курим исходники формирования японских иероглифов.
  • Понимаем всё до байта (ох, сразу скажу, это было... тяжко.)
  • Выгружаем обе процедуры из Ghidra… Думаем как их совместить…

Глубоко вдыхаем и достаём ассемблер…

Расчехляем компилятор и пишем сами (рендер текста v2.0!)

Да, ребята, мы добрались до самого непростого — написания своего кода. Но, как показала практика, без этого никуда.

Сам ассемблер PlayStation1 с процессором MIPS не то, чтобы сложен. Я бы сказал, что он чрезвычайно прост, но имеет свои нюансы. Благо, у нас много документации и прекрасный компилятор ARMIPS, который умеет загружать бинарники, ставить метки, подгружать одни бинарники в другие, из других файлов, выставлять текстовые ссылки как адреса, ограничивать “зоны” и т.д.

Краткий курс по ассемблеру MIPS PS1:

Процессор использует набор из 32 32-х битных регистров (по простому, переменных). Их можно посмотреть, например, в дебаггере no$psx.

Некоторые регистры трогать можно, некоторые лучше не стоит.

Регистр pc — он отвечает за адрес текущей инструкции.

Регистр sp — указатель стека. Это область где-то в конце памяти, в которую временно сбрасываются значения регистров перед выполнением программы, если нужно задействовать много регистров. Обычно это происходит в начале подпрограммы - сместили стек вниз, закидали временные данные, поделали что-то, вернули временные данные из стека, вернули указатель стека обратно. Можно вызвать ещё одну подпрограмму, она в свою очередь сделает то же самое. Таким образом стек и перемещается в зависимости от глубины вызова. Также через стек любят перебрасывать данные компиляторы, но вручную это тот ещё ад.

Регистры v0, v1 (r2,r3) — используются для возврата результата подпрограммы

a0-a3 (r4-r7) — для входящих в процедуру данных (адреса, параметры)

t0-t9 (temp) — временные, можно перезаписывать как хотите

s0-s7 (save) — данные, которые нельзя потерять. Обычно их сохраняют в стеке

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

Есть одна мелочь — при переходах и вызовах ВСЕГДА выполняется следующая после перехода команда. Это и плюс и минус. Просто имейте ввиду, что после jr ra (возврат), можно поставить mov v0, a0 — устанавливает значение a0 в v0 вместе с выходом. Выглядит странно, но не забывайте.

Для изучения команд можете посмотреть код в Гидре, он довольно прост, либо покурите документацию к RISC-процессорам

Итак, где же разместить свой код?

Чтобы понять, где есть немного всегда доступной памяти, я сохранил дампы RAM в разные моменты игры — в данжах, в ивентах, в бою, в меню и т.д. Затем открыл эти дампы в 7yuv как изображения, визуально попереключал файлы и нашёл свободное пространство. Это очень красивый адрес 0x80090000. На самом деле, мне очень повезло, что и в IS и в EP это место свободно и там есть пустой десяток килобайт под код, хотя в EP к нему уже подбиралась “куча” (данные, генерируемые игрой).

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

Здесь можно увидеть и инициализацию и проверку 13-ого бита.
Здесь можно увидеть и инициализацию и проверку 13-ого бита.

Параллельно с этим, я доработал запаковщик скриптов, и научил его добавлять мой управляющий код во все русские строки (и считать, и добавлять количество символов). Затем я заменил вызовы оригинальной процедуры в обработчике текстового потока окон printCommandText и………… Спустя пару сотен часов дебага оно заработало!

Команды рендера текста IS (1 команда формирования + 1 команда копирования спрайта). Зелёным отмечен счётчик свободных видеокоманд по адресу 8007b6d4 (0x54a).
Команды рендера текста IS (1 команда формирования + 1 команда копирования спрайта). Зелёным отмечен счётчик свободных видеокоманд по адресу 8007b6d4 (0x54a).

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

С этого момента начался полноценный цикл перевода. Я переводил сотни файлов сценария, тестировал и дебажил, дебажил, дебажил…

Особенности национальной японской разработки

За всё время разработки и перевода (а прошло около года) я столкнулся с огромным количеством проблем в самых разных местах. Перечислять всё не буду, отмечу лишь самое “весёлое”:

  • Те самые “пакеты” файлов практически всегда имеют файловые таблицы, расположенные где-то в коде. Если файл большой (например, скрипты из данжей лежат в одном пакете), то нам для загрузки нужно знать на какой сектор внутри файла нужно переместиться и откуда начать распаковку после чтения. Вычислять эти таблицы, их форматы и делать процедуры пересчёта при импорте — отдельный челлендж.
  • В игре огромное количество форматов хранения текста. Есть так называемые “списки” с таблицей поинтеров, есть списки описаний, которые могут копипаститься в разный подгружаемый код (меню, боёвка, магазины и т.д.) до 7-8 раз (в EP я уже применял более продвинутый подход). И списков этих — десятки. Нужно не только перевести их, сохранив формат и не переборщить с общим размером данных, но ещё и разместить везде, где они нужны по всему диску. Особенно классно вносить правки в кучу разных копипастных мест. Или редактировать список на 800 строк, а в конце… у тебя вылетела программа или отключили свет. После всех перечисленных случаев была создана кнопка временного сохранения/загрузки, а все положения и смещения в файлах хранятся в специальных файлах настроек. Данные списков могут храниться как в простом формате, так и в “сложном” — составном (когда нужно кодировать коды переносов строк и т.д.). А иногда это всё необходимо совмещать. Плюсом были написаны обработчики, которые предварительно считают все данные и не дают перезаписать ничего за пределами массивов (и налезть на другие списки).
Старая версия редактора списков.
Старая версия редактора списков.
  • Форматы скриптов могут быть разным. Например, скрипты призыва Персон отличаются от обычных. Есть скрипты, встроенные прямо в код данжа (найдено/получено). Их нужно экспортировать-импортировать буквально в код через HEX-редактор. И у всего этого есть свои таблицы поинтеров.
  • Существуют так называемые “составные строки”. Это когда к предмету или имени прикручивается подпись или наоборот (“ХХХХ: изучено!”, “Тацуя и ребята — сбежали!”). В оригинале всё решалось прикручиванием иероглифов друг к другу с управляющим кодом в конце. После этого вызывалась обычная процедура печати текста (не потока текста). В нашем же случае пришлось городить ультра-мега-супер-процедуру конкатенации, которая соединяла несколько строк с заголовком в моём формате, рассчитывала длину и т.д.
  • Экспорт-импорт графики пришлось изобретать самостоятельно. Кое-где, конечно, используются стандартные .tim-ы (иконки магазинов или интерфейс), но для 95% случаев я писал свой собственный экспорт в png с поддержкой игровых палитр. Ну и как челлендж, я написал импорт png в игру с поддержкой всех видов фильтрации строк, основываясь на исходниках libpng. В итоге всё прекрасно кушается при сохранении из любого редактора и конвертируются в формат игры.
Просмотр графики и успешный экспорт png
Просмотр графики и успешный экспорт png
  • Каждый подгружаемый модуль игры (а их там несколько десятков) был импортирован в Ghidra, изучен и доработан. Карта города (плашки районов и зданий, значки у людей и на выходах из зон и т.д.), данжи (отображение локаций, карта, графика, сообщения), боевая система (кучи меню), обычные меню (которых в JRPG по традиции очень много)… Всё было скрупулёзно доработано, везде была введена поддержка русского текста, подогнаны координаты, чтобы выглядело красиво или хотя бы нормально.
  • Скрипты общения с демонами — это вообще отдельный мир. Во-первых, у них свой формат. Опять разбираем, изучаем заново, пишем распаковку-запаковку. Во-вторых, там и фразы персонажей, и фразы Персон, и фразы демонов. Объёмы текста сравнимы с объёмами со всеми скриптами сюжета — это всё должно правильно загружаться и работать. Были свои ошибки и ограничения, которые приходилось буквально подгонять по байтам.
  • Был создан кастомный обработчик отображения арканов. Все арканы пришлось переносить в свободную память, — они хранились в однобайтном формате, и русские арканы туда просто не помещались. Пришлось переписывать все вызовы отображения арканов, в списках, карточках и т.д.
  • Было перерисовано казино и переписан его код (4 игры). Тоже отдельная история, очень много графики и переписывания кода.
  • Починены некоторые баги японки, например баг при сохранении в данжах или городе, при котором видеопамять превращалась в кашу. Все текстуры, соответственно, тоже. Но игра при этом работала, хоть и нужно было делать хард-ресет. Фиксом была на порядок снижена вероятность встретить этот баг (раньше это было раз в ~20 сохранений, стало раз в ~200 сохранений).
  • И прочее, прочее, более мелкое.

И в каждом из этих пунктов десятки нюансов…

Перевод текста, софтлоки, оптимизации и миллиарды правок

После завершения перевода всего сюжета, доп. сценариев, данжей, списков, города и большей части технической работы мы добрались до первого технического билда, который тестировали 3 человека (IDN, Илья и Lefan — тот самый, который начал тему на megaten.ru за 13 лет до моего перевода). Про объёмы текста скажу просто — это JRPG, а следовательно, здесь сотни текстовых файлов для перевода, которые в разы превышает всю техническую работу. Мало того, что всё надо перевести, так ещё и всё должно работать на ЛОР и соответствовать ему. Не будучи переводчиком, я просто работал как умел, стараясь не загоняться и не перенапрягаться. Да, приходилось несколько раз всё переписывать, но полугодовая полировка текстов принесла свои плоды, повысив как качество продукта, так и мой скилл.

Тестирование проходило сложно. ОГРОМНОЕ спасибо нашим тестерам, потому как я никогда в жизни бы не выловил все баги и неточности. Параллельно я добивал файлы общения с демонами (а это 17 файлов по 800-900 реплик) и делал бесчисленное количество фиксов.

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

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

Наконец, перевод стал выглядеть и играться хорошо, я закончил всю перерисовку графики, перевёл видеоролики, поправил титры, сделал патчер-трейнер, нарисовал “полиграфию”, и принял решение — хватит мучать перевод, пора релизиться!

Релиз! (Innocent Sin v1.0 и v1.1)

Релиз перевода Persona2: Innocent Sin состоялся в августе 2022 года. После сего “знаменательного” события я сделал самое логичное, что мог — укатил в отпуск и отключился от всего. Но одним глазом поглядывал в наш канал и мониторил стримы. Естественно, вскрылись множественные опечатки, неточности и софтлоки. Доработка перевода до стабильного состояния и правка текста заняли ещё 4 месяца, и к новому году была выпущена версия 1.1, которая является самой актуальной на данный момент. Там ещё имеются проблемки, в первую очередь, с зависаниями (но их ОЧЕНЬ мало), уже готовится 1.2, в которой будет новый рендер текста (из EP) и десятки других исправлений. Релиз 1.2… Очень надеюсь, что скоро.

Перевод Eternal Punishment. Начинаем заново?

После релиза Innocent Sin я засел проходить Eternal Punishment. Это совсем другая по атмосфере игра. Да, это продолжение старой истории, но игра более взрослая и жёсткая. Персонажи прописаны куда лучше, а сюжет менее “рваный”.

Здесь я, если честно, колебался. Браться за перевод или нет? С одной стороны, понятен весь цикл и подводные камни. С другой — объёмы текстов, количество механик и общая сложность проекта ещё выше, чем в Innocent Sin. В очередной раз спасибо нашему комьюнити, что в своё время поддержали меня добрыми словами!

Ладно! Уговорили!

За основу перевода EP был взят японский образ Eternal Punishment. Во-первых — в начале прохождения можно перенести сохранения из IS (переносится много интересного), во-вторых — озвучка персонажей в бою и роликах, управление, и прочее-прочее, остались теми же. Так что перевести обе японские P2 — вполне себе логичное решение. Существует также американская версия EP. По слухам, она сложнее, но технически отличается не особо. Разве что им пришлось полностью перестроить структуру RAM, потому что даже японская версия работала на пределе свободной памяти. Кстати, это было огромным камнем преткновения для моего грядущего перевода. Я буквально не знал что делать с рендером текста, имея на руках уже переведённый сюжет.

Переиспользуем наработки? Нет.

Казалось бы, что проще? “Переиспользуй наработки и костыли из IS!”. Нет, друзья. В этот раз игра не оставила мне шанса и крашилась значительно чаще, памяти под видеокоманды было меньше, потому что в игру добавили гору нового контента, локации стали пышнее и сочнее, а мне уже не хотелось просто рубить текст, если он не лез в видеопамять. Разработчики тоже это понимали — например, рендерили имена в боёвке прямо в текстуры, чтобы они не ели драгоценные свободные цепочки. Сами цепочки превратились в 2 независимых буфера — формирования символов и копирования спрайтов (для которых нужно намного меньше места). В общем, тоже ужимались как могли. Я, переведя весь сценарий и общение в городе (внушительный объём), потихоньку дописывал софт: прокачивал наш EP_CDToolkit, сделал поддержку именования внутренних файлов, глубже изучал скрипты, графику, писал конвертеры, редакторы данжей и города. Теперь списки поддерживают клонирование в любые заранее заданные файлы, сделал поддержку легендарного перевода от Кудоса, добавил экспорт хитрой японской кодировки Атлусов для всех текстов и прочие сотни мелочей.

Обновлённый и прокачанный CDToolkit для EP
Обновлённый и прокачанный CDToolkit для EP

И, наконец, я придумал и написал ЕГО. Новый способ рендеринга текста. Он основан на групповом рендеринге до 10 символов сразу и единовременном копировании этого текста на экран одним спрайтом. Единственный минус — пришлось отказаться от посимвольного вывода. Текст выводится по 10 символов, но к этому быстро привыкаешь. Экономия? Текст начал занимать меньше видеопамяти, чем японский. Этот рендер уже внедрён в IS1.2 и в грядущий перевод EP. После нескольких месяцев тестирования обеих игр, могу с уверенностью сказать — все софтлоки, связанные с отображением текста и свободной памятью, побеждены.

Мощный рывок

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

Примерный объём работ по переводу одной игры. Здесь не видно, сколько переведено текста и написано софта, но зато понятно, чего ждать, если вдруг решите что-нибудь перевести.
Примерный объём работ по переводу одной игры. Здесь не видно, сколько переведено текста и написано софта, но зато понятно, чего ждать, если вдруг решите что-нибудь перевести.

Релиз ожидается в самое ближайшее время. Когда мне наконец дадут добро наши тестировщики.

Но ещё есть чем заняться до релиза. Нужно доперевести второй бонус-диск EP, озвучить ролики, сделать сабы, дорисовать графику. Ещё меня ждёт доработка IS с новым рендером текста и прочими мелочами. Что ж. Работаем!

Подводя итог последних пяти лет, хочется сказать, что я получил огромное удовольствие, работая с этими гениальными, и в то же время невероятно костыльными, в плане кода, играми. Вся работа производилась в качестве хобби, потому спасибо всем, кто донатит или поддерживает мои переводы добрым словом.

Что нас ждёт дальше? Пока не знаю. Есть планы сделать порты переводов для PSP-версий -- у PSP более совершенное железо и код будет писаться куда проще, но, с другой стороны, я немного устал от Персоны 2. Хотя кого я обманываю, я продолжу для неё что-нибудь пилить с новой силой уже через неделю после релиза EP.

Наверное, стоит упомянуть, что у меня есть кое-какие наработки в Unity -- плееры скриптов, загрузчики оригинальных локаций, разобрано много игровой логики. Можно даже замахнуться на проект ремейка, в котором будет поддержка оригинальных ассетов, которые можно заменить современными.

Посмотрите сами:

Проект "ремейка" P2 Innocent Sin. Загружаем оригинальные локации и воспроизводим оригинальные скрипты. Разве это не прекрасно? Нет. Работы ещё невероятно много.

Но все эти "ремейки" пока что на уровне очень ранних прототипов и идей. Посмотрим, куда заведёт меня эта тропа.

Итак.

Хочу поблагодарить всех, кто дочитал до этого момента. Не знаю как вы это выдержали, но вы молодцы.

Отдельное спасибо нашему маленькому комьюнити за поддержку и всем, всем, всем, кто проходил мимо. Надеюсь, вам было интересно и полезно.

Счастья вам, ребята и... Играйте в Persona 2!

Ну и, конечно, подписывайтесь на канал xD

1199KK
886677
112244
4411
1111
66
44
33
22
22
22
11
11
11
11