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

К счастью, сегодня большинство разработчиков не только не препятствуют изучению своих игр, но даже наоборот, делают всё, для того, чтобы игроки изменяли и дополняли игры сами. Но даже если официальной документации нет, то для 99% игр можно найти уже готовые программы для распаковки.

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

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

Завязка


Все началось с того, что будучи в детстве фанатом игр «SquareSoft», и маясь от безделья 1-го января, я заинтересовался информацией о ранних прототипах Final Fantasy и с удивлением узнал что у японской версии Final Fantasy VII для PlayStation был «бонусный» диск со всякими доп-материалами, и в том числе скетчами и скриншотами игры на ранних этапах разработки.
Немедленно скачав его образ с «торрентов» и запустив на эмуляторе я с некоторым разочарованием обнаружил, что во-первых диск жутко тормозит в эмуляторе(несмотря на новое железо), а во-вторых весь интерфейс на японском и черт ногу сломит в нем копаться через это меню.



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

Первичный investigation


Первым делом я решил изучить какие файлы вообще содержаться на диске.

Вот какой была его структура:
Структура папок тома SLPS_01060
E:.
¦ CG.FFD
¦ DUMMY3M.DA
¦ FFANM.APK
¦ FF_OPN.LPK
¦ FF_SYS.LPK
¦ NPK_ACC.NPK
¦ NPK_BOU.NPK
¦ NPK_DF.NPK
¦ NPK_DF2.NPK
¦ NPK_ETC.NPK
¦ NPK_MAIN.NPK
¦ NPK_MAKE.NPK
¦ NPK_MON.NPK
¦ NPK_WEP.NPK
¦ NPK_WM.NPK
¦ SLPS_010.60
¦ SYSTEM.CNF
¦
+---BG
¦ DATF001.LPK
¦ .....
¦ DATF009.LPK
¦
+---HERO
¦ HERO.FFD
¦
+---ITEM
¦ L---MATER
¦ MATER.FFD
¦
+---MAKINGCG
¦ MAKINGCG.NPK
¦
+---R3D
¦ RIDE_0.NPK
¦
+---S1G
¦ INST_M.S1G
¦ INST_P.S1G
¦ TITLE.S1G
¦
+---SM
¦ SM000.FFD
¦ SM001.FFD
¦
+---STR
¦ PP07FFA.STR
¦ PP07FFB.STR
¦ PP07FFC.STR
¦ PP07FFD.STR
¦ PP07FFE.STR
¦
+---TEXT
¦ MLIST.LPK
¦
L---TOWN
FF_TTOWN.NPK


Первое что бросилось в глаза — расширение файлов NPK и LPK, что косвенно указывало на то, что это архивы.

Поэтому одной из первых идей, которая пришла мне в голову, было воспользоваться всевозможными универсальными анализаторами и распаковщиками. Я скачал все что смог найти для PSX и на всякий случай, даже Dragon UnPACKer, для ПК.
Но улов был незначительным, файлы в папке STR оказались видеороликами в стандартном для PSX формате STR, а три файла в папке S1G, оказались картинками, в опять таки стандартном, как мне позже удалось нагуглить, для «соньки» формате TIM. только с другим расширением.
Архивы NPK и LPK, которые, как я предполагал, содержали основной контент диска, так и не удалось взять ни одной программе, в том числе, софту ориентированному на «Final Fantasy VII».

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



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

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

Qhimm(владелец форума и автор многих программ распаковщиков для FF) подорвался было изучать эти архивы, но закончилось все несколько внезапно…

Оказалось, что еще один разработчик софта для потрошения ресурсов PSX(snailrush) уже сделал утилиту для распаковки этих архивов. Казалось бы «Ура», переходим по ссылке и качаем утилиту, но если бы все было так хорошо, то этой статьи бы не было. В общем, ссылка была мертвой, и не помог даже archive.org. Сайт snailrush'а ушел в многолетний down практически через несколько дней после того, как он сделал эту чертову утилиту, и никаких снапшотов той страницы не сохранилось.



Таким образом, у меня был только пост Qhimm'а(который уже много лет не появлялся на своем форуме), о том, что ему удалось разобраться в устройстве этих архивов. А это значило, что ничего особо сложного в них не должно было быть.

Начинаем копать самостоятельно



Пришло время открыть файлы в HEX-редакторе. Сразу бросились в глаза какие-то числа в самом начале, которые указывали на то, что у файла есть что-то типа заголовка.
Тут я хотел бы сделать небольшое отступление. Не смотря на то, что в самом начале я сказал, что особых знаний для копания в ресурсах не нужно. Есть одно «небольшое» исключение. Нужно иметь хотя бы базовое представление о том, как именно в памяти или на диске могут хранится какие-нибудь данные, в самом простом виде — числа. Другими словами нужно знать что такое Big Endian и Little Endian.

Я попробую объяснить суть максимально кратко:
Очень часто числа или символы хранятся в памяти в обратном порядке. Например число «16830», которое в HEX выглядит как «41 BE», а будучи переменной типа int как «00 00 41 BE» в самой памяти может хранится как «BE 41 00 00», не буду объяснять почему так(вы можете погуглить сами), скажу просто что «компьютеру так удобнее». И соответственно, таким же образом, в обратном порядке, данные могут хранится в файле на диске.
Причем на первый взгляд, почти все файлы, независимо от расширения и LPK и ТЗЛ и FFD, имели очень схожую структуру в самом начале.

Несколько чисел типа int или short, потом последовательность «DF 10 00 00 00 09», потом уже какая-то мешанина байт, который были по всей видимостью данными.





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

Для более подробного изучения я решил сконцентрироваться на одном файле и выбрал NPK_WEP.NPK, так как из треда было ясно что во-первых именно его удалось распаковать, а во-вторых было известно, что в нем хранятся картинки в формате TIM.

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



Открываю заодно в HEX тем 3 файла, который уже были у меня в наличии в открытом виде.



Вау, тут во всех них „10 00 00 00 09“ в самом начале, прямо как в моем архиве.
Кажется это не может быть совпадением, вот оно! Tim файл прямо внутри, но почему его тогда не обнаружили автоматические анализаторы? и почему сразу после „10 00 00 00 09“ мешанина?
Пробую искать „10 00 00 00 09“ по файлу и обнаруживаю 127 совпадений. Более того все места с „10 00 00 00 09“ похожи друг на друга.

Вот например второе место в NPK_WEP.NPK где обнаружилась последовательность „10 00 00 00 09“:



После кучи нолей, я увидел точно такую же структуру как и в самом начале файла.

Так, теперь можно сделать кое-какие предварительные предположения и выводы:

  1. Если „10 00 00 00 09“ — действительно сигнатура говорящая о начале TIM файлы, то таких файлов в архиве 127, что в общем-то логично и похоже на правду.
  2. Архив NPK_WEP.NPK как таковой собственных заголовков(которые бы описывали именно его структуру) не имеет и скорее всего является нагромождением как-то структурированных данных.
  3. TIM файлы(если это все же они) в архиве как-то сжаты, так как данные после заголовка „10 00 00 00 09“ на реальные файлы ну никак не похожи.

Теперь я решил поподробнее изучить структуру NPK архива и выяснить что значат числа перед „10 00 00 00 09“.

Самым первым числом было:
37 00 00 00 > 00 00 00 37(HEX) > 55(DEC) Не похоже чтобы это было смещением, так как смещение 37 находится уже внутри того, что я пока считают сжатыми данными, так что пропустим его.

Второе число было short F7 03 > 03 F7(HEX) > 1015 (DEC)
Третье тоже short F6 04 > 04 F6(HEX) > 1270 (DEC)
Вот эти два уже похожи на смещения, попробуем найти что-то в районе этих мест.

Ищем 03 F7:



И бинго! Походу именно тут кончается блок данных, так как начиная с этого места идут нули.
А что тут строкой ниже? 36 00 00 00 FD 03 2F 08 — Не похоже на сжатые данные, зато очень похоже на такой же заголовок как и в начале.

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

Зато теперь появляется предположение, что означало самое первое число 37(HEX) или 55(DEC)
По всей видимости, -это количество суб-блоков до конца запакованного файла, у каждого следующего блока оно будет уменьшаться на 1. Проверка подтвердила это предположение.

Одно только осталось непонятным, если у нас есть смещения, говорящие о длине блока, то почему бы не начинать следующий блок сразу после конца предыдущего, зачем вся эта куча нолей? К тому, на начало второго блока 0х0400 ничего не указывает, как тогда понять, где начинается следующий блок? С другой стороны 0400 уж очень подозрительное смещение, слишком ровное. Интересно сколько это в DEC?



Ух ты! ровно 1024, вряд ли это совпадение. посмотрим что у нас через еще 1024 байта по смещению 0х800:



Теперь все ясно, размер каждого суб-блока — 1 килобайт.

Итак, структура NPK_WEP.NPK практически полностью расшифрована.
Весь файл разбит на блоки по 1024 байта.

Первые 4 байта каждого блока — число суб-блоков до конца одного содержащегося в архиве файла.

Следующие 2 байта — смещение от начала суб-блока до конца полезных данных
Еще два байта — скорее всего размер данных после распаковки(видимо для выделения места под них в памяти).

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

Разбираемся с сжатием


До этого я никогда не сталкивался с сжатием или шифрованием и не имел вообще никакого ни представления, ни опыта.

Поэтому я стал искать информацию сразу в двух направлениях:
Во-первых, как на глаз понять какое сжатие используется в файле, если это неизвестно.
И во-вторых, какое сжатие используется чаще всего для PlayStation в целом, или для игр SquareSoft и серии FF в частности. Зацепкой было то, что Qhimm довольно быстро разобрался с файлом, значит это было что-то знакомое для него.

Начав с общего поиска „как на глаз определить чем сжат файл“, самое полезное что я обнаружил — это вот этот список:
zenhax.com/viewtopic.php?t=27, в котором глаз зацепился за
lzss
Parts of the original data uncompressed.
, что в общем-то походило на то, что было у меня.
А во-вторых можно было исключить сразу всякие zip-related и прочие методы имеющие какие-то стандартные заголовки, так как ничего похожего на заголовок у сжатых данных не наблюдалось.

Поиск по сжатию используемому в Final Fantasy VII привел на упавший сайт wiki.qhimm.com, снапшоты которого к счастью были доступны через Wayback Machine.

Там говорилось что в FF VII к примеру используется два типа сжатия GZIP и LZS (какой-то специфический подвид LZSS). GZip я сразу отмел, так как у него должен был быть хидер, а вот на LZSS я решил остановится поподробнее, так как все указывало на него.
Но тут я совершил, можно сказать ошибку и полез в »википедию"(при редактировании статьи заметил, что это не википедия, а другой вики-сайт ) читать про LZSS. А проблема была в том, что одно дело — принцип сжатия 80х описанный в учебнике, а другое — его конкретное реализация. Я совсем не учел, что сохраняя принцип сжатия, реализовать его можно кучей разных способов, но в учебниках и в том числе в большинстве статей где оно описывается в интернете, оно описано следующим способом: читается 1 бит, если это 0, то читаются следующие 8 бит и заносятся как есть в буфер (вывод), а если это 1, то читаются следующие 7 бит, часть из которых офсет в буфере, а часть длина данных, которые нужно считать из буфера и занести в вывод.

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

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

В общем, потеряв впустую несколько часов на попытки сложить 2+2, я решил зайти с другой стороны и поискать какие-нить реализации «в коде» сжатия используемого именно в FF VII.
И сразу наткнулся на github.com/cebix/ff7tools — набор утилит на питоне для перевода ff7, в состав которых входили lzss и unlzss. Для проверки я решил натравить lzss на один из имеющихся у меня tim файлов. Получилось как-то так:



На первый взгляд — ничего хорошего, но я уцепился за 09 и последовательность за ней, которая уж больно напоминала то, как выглядели мои файлы в архивах. Тут я уже практически полностью уверился, что картинки сжаты какой-то из реализаций LZSS, осталось только разобраться какой именно.

Для этого я вернулся на wiki.qhimm.com и решил повнимательнее прочитать про «этот их LZS».
После внимательного прочтения и пере-прочтения — пазл сложился! БИНГО!

Формат устроен следующим образом:

Вся дата разбита на блоки, длина которых может варьироваться от 9 байт до 17 байт.
Первый байт блока указывает на то, чем будут все остальные байты блока. Закодировано это на уровне бит. 1 — значит что байт отправляется как есть в буфер и на вывод. 0 — означает что нужно прочитать 2 байта и они будут ссылкой на буфер. Причем чтение бит идет с конца.
Таким образом, если первый байт блока — FF, (11111111)- это значит, что следующие 8 байт нужно просто прочитать как есть и записать в вывод(буфер).
А, ставший для меня уже родным, байт DF, стоящий перед 10 00 00 00 09, в битовом виде выглядит как 11011111, что означает, что 5 идущих за ним байт нужно прочитать как есть(те самые 10 00 00 00 09), потом идет 2х-байтная ссылка(EF F0), и опять 2 байта нужно прочитать как есть (0C 02).
Дальше уже интереснее. Формат ссылки сильно зависит от размера буфера. В данной реализации буфер равен 4 килобайтам, а ссылка в битах выглядит следующим образом(опять гребаные биты, ненавижу уже их!)
OOOO OOOO OOOO LLLL (O = Offset, L = Length)
Ну или можно без бит, тогда так OO OL(на уровне байтов), причем первые 4 бита второго байта — это первый байт офсета, потому что читать опять нужно с конца. Но и это не все, к длине нужно прибавить три, просто такой вот хардкод. Потому что длины 0, 1 и 2 не имеют смысла, так как в таком случае ссылка будет занимать больше места чем считанные по ней данные. Так что 0 — это 3, 1 — это 4, и тд…

Понимаю что понять сложно, поэтому объясню на реальном примере:

Байт DF говорит нам о том, что 6-й и 7-й байт за ним — это офсет, у нас это EF F0.
Отсекаем последние 4 бита(длина) и переворачиваем, получаем 0хFEF — офсет в буфере, а оставшийся 0+3 = 3 — длина.

Таким образом, ссылка говорит нам прочитать 3 байта по офсету 0хFEF из буфера. Если вы проскролите выше и посмотрите как выглядит нормальный TIM файл, то там сразу после 09 — три нулевых байта, вот именно они и копируются из буфера.(по видимому это те 3 нулевых байта, которые были туда, буквально «только что», записаны предыдущими действиями).

Так же, в той статье, была описана сложная формула по вычислению реального офсета, по которому нужно читать из буфера. Объяснялось это тем что автор предполагал для записи и чтения использовать обычные потоки ввода-вывода, а формат сжатия подразумевал использование кольцевого буфера, и нужно было как-то хитро преобразовывать офсет.
Но я был отважен, и решил что «ну его нафиг», лучше я реализуют у себя кольцевой буфер, чем буду разбираться в этих странных формулах. Конечно я пожалел, но в тоже время, когда стал разбираться почему оно «не работает», нашел куда более красивое решение, чем автор той статьи.

Кроме того, в статье писалось о двух «фишках», которые нужно иметь ввиду, ссылки на буфер могут идти как в прошлое так и в будущее, обойти это предлагалось всякими хаками.
Как я потом выяснил при правильной реализации буфера, никакие «хаки» не нужны.

Пишем Код


Ну, а пока что, я расчехлил Eclipse(писать решил в JAVA, так как мне так удобнее)
Сначала надо определится с тем как реализовать кольцевой буфер. Дело в том что мне сразу было понятно, что реализовать его можно очень разными способами, так что я решил просто попытать счастье и выбрал подобную реализацию из более менее стандартной библиотеки CircularFifoQueue из apache.commons.collections. Это был FIFO буфер позволяющий читать по офсету внутри себя. Другие реализации из коробки чаще всего по офсету читать не позволяли.
В итоге код получился примерно такой(я выкинул всякий мусор для упрощения чтения и сокращения):

	static CircularFifoQueue<Byte> circularBuffer = new CircularFifoQueue<>(4096);

	private static void processNPK() {
		
		FileInputStream fis = new FileInputStream(new File("NPK_WEP.NPK"));
		ByteBuffer b1kb = ByteBuffer.allocate(1024);
		while (fis.read(b1kb.array()) != -1) {
			int blocksInMegaBlockCount = EndianUtils.swapInteger(b1kb.getInt());
			int compressedDataLenght = EndianUtils.swapShort(b1kb.getShort());
			int decompressedDataLenght = EndianUtils.swapShort(b1kb.getShort());
			while (b1kb.position()<compressedDataLenght) {
				readLZSBlock(b1kb);
			}
			if (blocksInMegaBlockCount == 1) {dumpFile(result);}
		}
 	}
	
	
	private static void readLZSBlock(ByteBuffer bb){
		byte controlByte = bb.get();
		for (int i = 0; i < 8; i++) {
			if (isLiteral(controlByte,i)) {
				byte b = bb.get();
				circularBuffer.add(b);
				result.write(b);
			}
			else result.write(getFromCBuffer(bb));
		}

	}
	
	private static byte[] getFromCBuffer(ByteBuffer bb) {
		byte first = bb.get();
		byte second = bb.get();
		Integer lowNibble = Integer.valueOf(second & 0x0f); 
		Integer highNibble = Integer.valueOf(((second & 0xf0) >> 4));
		int offset = EndianUtils.readSwappedShort(new byte[] {first, highNibble.byteValue()}, 0);
		byte[] bytesFromBuffer = new byte[lowNibble+3];
		for (int i = 0; i < (lowNibble+3); i++) {
			byte b = circularBuffer.get(offset);
			circularBuffer.add(b);
			bytesFromBuffer[i]=b;
		}
		return bytesFromBuffer;
	}

	private static boolean isLiteral(byte controlByte, int position) {
		if (((controlByte >> position) & 1) == 1) return true;
		else return false;
	}

Описать его работу можно следующим образом:

1) Файл нарезается на куски по 1024 байта,
2) Для каждого считывается int, short, short
3) Остаток передается в цикл с распаковщиком(readLZSBlock)
4) Он читает первый байт, выясняет что делать с остальными, и либо записывает их как есть, либо, если это ссылка, то отдает его в обработчик ссылок на буфер(getFromCBuffer), который читает из буфера.
5) Когда один файл внутри архива прочтен — он сливается на диск

Результат выполнения был такой:



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

Я не сильно расстроился, так как ожидал, что буфер придется допиливать. На всякий случай прогнал через программу все остальные архивы на диске и с удивлением обнаружил, что первый файл в архиве NPK_WEP.NPK был единственным который открылся в просмоторщике tim файлов. Так что мне очень повезло, что я начал именно с него.

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

Заметная ошибка обнаружилась по офсету 0х214. в нормальном TIM файле там должен был int соответствующий размеру данных в нем, причем учитывая, что файлы TIM не сжаты по умолчанию и размер зависит от разрешения, а разрешение в свою очередь у картинок растянутых на весь экран телевизора у playstation — одно и тоже, я даже наверняка знал, какое именно число там должно быть.

Вот пример нормального файла:



Размер данных 00 01 E0 0C.

А вот мой файл:



вместо нормального числа у меня там какой-то мусор.

Теперь изменив немного код, я получил данные относящиеся к обработке данного места.
Офсет считанный из файла был 4092, длина — 3 байта.

Вот как выглядел мой буфер в момент, когда из него нужно было получить 00 01 E0



(На самом деле это конец, так как весь буфер не влазил даже в мой экран, но в начале там просто еще больше нолей чем на картинке.)

Найдя в буфере нужные данные, я оказался в замешательстве.
В буфер было загнано 527 байт «полезной» даты. нужные данный находились по офсету 15 с начала ввода данных, и примерно по офсету 3569 с начала буфера. Каким образом их можно было считать по офсету 4092, выданному мне, я не мог понять. Еще, вспомнив что в статье упоминалось число 18, я попытался прибавлять и отнимать его от разных офсетов, но так ничего осмысленного и не получил.

В итого, почесав репу, и поняв что оригинальный буфер был устроен ну как-то совсем иначе, чем мой, я вернулся к статье и описанию того, как получить «реальный офсет» при использовании чтения из потоков. Формула выглядела настолько ужасно, что я просто скопировал ее как есть, не пытаясь вникнуть и еще прицепил к ней пару вагонов дополнений чтобы натянуть ее на мой буфер, в итоге получился вот такой ужас:

		// чтение нормального офсета из файла
		int offset = EndianUtils.readSwappedShort(new byte[] {first, highNibble.byteValue()}, 0);
		// формула из статьи(где OUTPOS - количество записанных в буфер данных)
		int realoffset = OUTPOS - ((OUTPOS + 0xfee - offset) & 0xfff);
		//натягивания формулы из статьи на мой "кольцевой буфер"
		int realoffset2 = 4096 - OUTPOS + realoffset;

(realoffset и realoffset2 можно было конечно сократить, но так как я не был уверен что оно заработает, я оставил так как есть для дебага.)

И как ни странно, оно заработало!



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

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

В итоге получилось как-то так:

public class ByteRingBuffer {

	private byte[] rBuf; // ring buffer data
	public int rBufSize = 4096; // ring buffer size
	public int writeReadOffsetCorrection = 18;
	public int lenghtCorrectionLZS = 3;
	private int rBufWrPos = rBufSize - writeReadOffsetCorrection; // position of first (oldest) data byte within the ring buffer

	public ByteRingBuffer() {
		rBuf = new byte[rBufSize];
	}

	public byte write(byte bt) {
		rBuf[rBufWrPos++] = bt;
		if (rBufWrPos == rBufSize)
			rBufWrPos = 0;
		return bt;
	}

	public byte read(int offset) {
		return rBuf[offset & 0xfff];
	}
}

Проще говоря, буфером был обычный массив байтов. Чтение происходило с начала массива, и маркер чтения никогда не перемещался. Единственным хаком было то, что изначальной точкой начала записи в массив было не 0, а размер массива минус 18. И все заработало как нужно!
Т.е достаточно было сместить на 18 точку записи в самом начале и после этого можно было спокойно читать и записывать без всяких хаков. при таком раскладе 15-й записанный в массив байт, действительно оказывался по офсету 4092.

Ну и кроме того не нужны были никакие хаки связанные с чтением из «прошлого» или «будущего».

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

1) Данные предназначенные для сжатия ищутся в буфере.
2) Обнаруживаются там.
3) Создается ссылка на них, и они записываются в буфер.
4) Но они перезаписывают, по сути, сами себя, причем частично
5) И в результате правильная распаковка теперь становится невозможной.

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

Помните те блоки по 1024 байт, данные в которых занимали чуть меньше места? У меня был код, который проверял остаток блока и выдавал уведомление, если там были не только нули. На первом файле — NPK_WEP.NPK, этот участок кода ни разу не сработал, но уже на следующем и почти на всех остальных выскакивали постоянные предупреждения. Так как файлы при этом распаковывались корректно, я решил посмотреть что там за «лишние» данные в хвостах. И немного офигел. Не долго думая, я добавил код, которые весь «мусор» из архива дампит в отдельный файл и вот что среди этого мусора я обнаружил:

Целую кучу кусков исходного кода с комментариями на японском
<libetc.h>
#include <libgte.h>
#include <libgpu.h>
#include <libsnd.h>
#include <libgs.h>
#include <KERNEL.H>
#include <libsn.h>
#include <libcd.h>

*************************************************************************/
#include "ff.h"
//#include "ff7list.h"
#include "winsys.h"
#include "cursol.h"
#include "datafanm.inc"
#include "dataflpk.inc"
#include "lpk_sys.inc"
#include "ffaku.h"
#include "clickm.h"
#include "npk_item.inc"

/*****************************************************************************/
/*  ???       */     
Z_CLUT, (long *)LPKBuf,   (long *)ComnBuf
#define DEFSET LZ_TIM | LZ_CLUT, (long *)MODELBuf, (long *)ComnBuf
#define DEFSYS LZ_TIM | LZ_CLUT, (long *)SYSBuf,   (long *)ComnBuf
#define SUBBERMAX 3
#define SUBBERMAX 3
#define ZENGAMEN_X 168                  /*  ???X??                     */
#define ZIGAMEN_X  282                  /*  ???X??                     */
#define ZIGAMEN_Y   16,  17};

_BOXFTAG EXsubbox[SUBBERMAX];           /*  ?????????               */

static int wait;                        /*  ????                         */
static int oldx;                        /*  ???X???????           */
static int oldy;                        /*  ???Y???????           */
static int oldpad;                      /*  ?????????                           
                                           /*  ??                             */
static int mode;                        /*  ???????????           */

static int Shop_Sel;                    /*  ????????????         */
int EXdatflag;                          /*  ???????????           */
int EXdatnum;                           /*  ??                             */

static char 							/*???BG???????       */
extern _BOXFTAG EXbox01;                /*  ????                         */

/*****************************************************************************/
/*  ????????                                                         */
/*****************************************************************************/
void Item_?                    /*  */
void ItemSelLoadEX(void);               /*???????????????????(?????)??*/
void ShopList(void);                    /*  ???????????           */
void ShopSel(void);                     /*  ?????????            */
/*                                                                           */
/*                                             1997/05/07 G.T.V H.Mizukami   */
/****************************        */
		LLbox.r = 1;
        LLbox.g = 1;
        LLbox.b = 1;

        LLbox.x = 16-256;
        LLbox.y = 20-120;
        LLbox.w = 332;
        LLbox.h = 16;

        GsSort i = i - LLtbl[Q1];
                        if(i < 0)
                        {
                                ListPage[Q1] = n + 0x30;
                                i = i + LLtbl[Q1];
                                                                           */
/*                                             1997/03/26 G.T.V H.Mizukami   */
/********************/

strcat(mlistbuf, akuselist[LLtop + Q1]);
                        break;
                  case 4:               /*  ???                           */
                        strcat(mli                            
						/**/
/*                                             1997/04/22 G.T.V H.Mizukami   */
/*****************************************************************************/
void Item_SelMenuInit(void)
{
  WinFill LLwin;

      A/* ????????               */
          case 4:                       /*  ??????                     */
                ListInit(item_menu);
                WinSetPos(wid[2], 128, 44, 16*20, 14*10);
                WinSetRGB(wid[2], 0, 0, 255, RGB_BUL);
                break;
/* H.Mizukami   */
/*****************************************************************************/
void Item_SelMenu(void)
{
  int LLnum;

        MjsLoop(&EXtprm01);
        if((LLnum = MjsMenuStat(&EXtprm01)) == -1)
                return;
        /*se                        */
/*                                             1997/04/25 G.T.V H.Mizukami   */
/*****************************************************************************/
void Item_Mat_SelInit(void)
{
  WinFill LLwin;

        command = 2;
        EXsubbox[0].mode = 0;           /*  ????OFF               */  
		
		
		/*  ??????                     */
                char_pt = mat01_list;
                break;
          case 2:                       /*  ????????                 */
                char_pt = mat02_list;
                  break;
          case 3:                       /*  ??????                     */
                char_pt = mat03_list;
                break;
               1997/04/25 G.T.V H.Mizukami   */
/*****************************************************************************/
void Item_Mat_Sel(void)
{
  int LLpad;
  int LLnum = item_sel_mat;

#ifdef DEBUG
FntPrint("item_sel_mat = %d\n", item_sel_mat);
#endif

        LLpad = Item_MatMouse();

        if(oldpad == PADLup && item_sel_mat)
                item_sel_mat--;
        if(oldpad == PA if(LLpad == 0 || (oldpad & PADnext))
        {
                command     = 99;
                nextcommand = 2;
        }
        else
        {
                if((TGPad1 & PADRdown) || Char_cansel())
                {
                        command     = 127;
                        nextcommand = 0;
                        TGPad1 |= PADnext;
                        StopPreLoad();
  /*******/
void ItemMat(void)
{
  int LLpad = 1;
  GsBOXF LLbox;
  int LLnum = ItemListMatNum[item_sel_mat];

#ifdef DEBUG
FntPrint("sel = %d max = %d\n", ItemListMatNum[item_sel_mat],
                ItemListMatMax[item_sel_mat]);
#endif

        LLpad = Item_subMatMouse();

        if(oldpad == PADLup && ItemListMatNum[item_sel_mat] > 1)
                ItemListMatNum[item_sel_mat] -= 2;     ItemListMatNum[item_sel_mat] < ItemListMatMax[item_sel_mat] - 1)
                ItemListMatNum[item_sel_mat]++;

        if(LLnum != ItemListMatNum[item_sel_mat])
        {
                _mt_sel_snd();
                StopPreLoad();
        }

        if(PreLoad == PL_NOUSE)
                ItemSelLoad((long *)(LPKBuf + BG_SZ));

        EXsubbox[1].mode = 1;
        EXsubbox[1].b & PADRdown) || Char_cansel())
        {
                command     = 127;
                nextcommand = 2;
                TGPad1 |= PADnext;
                StopPreLoad();
        }
        else
        {
                if((TGPad1 & PADnext) && LLpad == 0 || (oldpad & PADnext))
                {
                        command     = 99;
                        nextcommand = 3;
       /
void Item_Char_SelInit(void)
{
  WinFill LLwin;

        command = 2;
        EXsubbox[0].mode = 0;           /*  ????OFF                      */
        WinNotDisp(wid[2]);
        memset(&LLwin, 0x00, sizeof(WinFill));
        LLwin.flag  = WINf_GRO;
        LLwin.wflag = WINw_001;
        LLwin.prm   = 5;
        wid[3]      = WinOpen(&LLwin);
        WinSetPos(wid[3], 128, 44, 1         */
/*                                             1997/03/31 G.T.V H.Mizukami   */
/*********************************************************************************************************************************/
static void _init5(SCREEN_JOB *ps, int n)
{
  WinFill LLwin;

        if(n == -1)
        {
                ShopList();             /*  ?????????               */

                init_CursolEx(&EXmouse, 0, 256, 120, 32, 22,  3, 3);
                memset(&LLwin, 0x00, sizeof(WinFill));
                LLwin.flag                                                          */
/*                                                                           */
/*                                                                           */
/*                                             1997/03/31 G.T.V H.Mizukami   */
/*****************************************************************************/
static void _loop5(S             */
                MjsLoop(&EXtprm00);
                break;
          case 1:                       /*  ????????                 */
                if(How_Kind_PadCon(0) == 1)
                        SjsSel = 1;
                else
                        SjsSel = 0;
                ic_flag = 0;
                break;
          case 99:
                if(anmwait ==       FsOpen(TOWNFILE);
                        FsSeek(tmpcmp.lcv_offset+tmpcmp.bg_offset);
                        FsReadLzs((long*)LPKBuf,tmpcmp.bg_size);
/* Miz*/
                        FsOpen(tmpcmp.lpkname);
                        FsSeek(tmpcmp.bg_offset);
                        FsReadLzs((long*)LPKBuf,tmpcmp.bg_size);
/**/
                        PreLoad=PL_USENOW;
                                                   
/*                                             1997/03/31 G.T.V H.Mizukami   */
/*****************************************************************************/
static int _before5(SCREEN_JOB *ps, int n)
{

        if(command == 0)
                return(1);

        command--;
        return(0);
}

/***********************/
raw();
        }
}

/*****************************************************************************/
/*  ????                                                                 */
/*                                                                **********/
/*  ????????????                                                 */
/*                                                                           */
/*                              ***************************************************************/
/*  ??????????                                                     */
/**************************************/
CR_NOTPUSH,
   _sinit5,
   _init5,
   _loop5,
   NULL,
   NULL,
   NULL,
   _draw5,
   _end5,
   wp,
   {NULL,},
   512, 240,
   NULL, 0,
   NULL,
   0,
   1,
};

/**************                                                  */
/*                                                                           */
command   = 0;
                Shop_Sel  = 0;
                SjsSel    = 0;
                SjsSetBeforeKey(NULL);
        }

        anmwait = 0;
}

/**************************************************/
;
#endif

        oldx   = EXmouse.cx;
        oldy   = EXmouse.cy;
        oldpad = TGPad1;
        if(How_Kind_PadCon(0) == 1)
                read_Mouse_Data(&EXmouse, 1);

        SjsSel = 1;
        switch(command)
        {
             StopPreLoad();
                        SetTim(LPKBuf, 512, 0, 0, 480);
                        _mt_ok_snd();
                        anmwait = 0;
                        SjsSel=0;
                        SjsSetMode(&scr_list);
                        return;
                }
      
                }
                else
                {
                        command--;
                        _t_cancel_snd();
                }
        }
}

/*****************************************************************************/
/*******************************************************************/
static void _init5(SCREEN_JOB *ps, int n)
{
  WinFill LLwin;

        if(n == -1)
        {
                ShopList();             /*  ?????????               */

                init_CursolEx(&EXmouse, 0, 256, 120, 32, 22,  3, 3);
                memset(&LLwin, 0x00, sizeof(WinFill));
                LLwin.flag                       */
/*                                                                           */
/*                                             1997/03/31 G.T.V H.Mizukami   */
/*****************************************************************************/
static void _loop5(Swait ==       
 
h>
/**************************************************************************/
#include "ff.h"
//#include "ff7list.h"
#include "winsys.h"
#include "cursol.h"
#include "datafanm.inc"
#include "dataflpk.inc"
#include "lpk_sys.inc"
#include "ffaku.h"
#include "clickm.h"
#include "npk_item.inc"

/*****************************************************************************/
/*  ???            GAMEN_X 168                  /*  ???X??                     */
#define ZIGAMEN_X  282                  /*  ???X??                     */
#define ZIGAMEN_Y   1O????           */
static int oldpad;                      /*  ?????????      ??                             */

static char ************************************/
void Item_opSel(void);                     /*  ?????????7 G.T.V H.Mizukami   */
/**************************** 
*/
= 1;
        LLbox.g = 1;
        LLbox.b = 1;

        LLbox.x = 16-256;
        LLbox.y = 20-120;
        LLbox.w = 332;
        LLbox.h = 16;

        GsSortLLtbl[Q1];
                        if(i < 0)
                        {
                                ListPage[Q1] = n + 0x30;
                                i = i + LLtbl[Q1];
/*                                                                           */
/*                                             1997/03/26 G.T.V H.Mizukami   */
/********************       /*  ??????     */                */
                        strcat(mlistbuf, akuselist[LLtop + Q1]);
                        break;
                  case 4:               /*  ???                           */
                        strcat(mli                                                                         */
/*                                             1997/04/22 G.T.V H.Mizukami   */
/*****************************************************************************/
void Item_SelMenuInit(void)
{
  WinFill LLwin;

        ?????????               */
          case 4:                       /*  ??????                     */
                ListInit(item_menu);
                WinSetPos(wid[2], 128, 44, 16*20, 14*10);
                WinSetRGB(wid[2], 0, 0, 255, RGB_BUL);
                break;
**********************************************************************/
void Item_SelMenu(void)
{
  int LLnum;

        MjsLoop(&EXtprm01);
        if((LLnum = MjsMenuStat(&EXtprm01)) == -1)
                return;
        /*se       1997/04/25 G.T.V H.Mizukami   */
/*****************************************************************************/
void Item_Mat_SelInit(void)
{
  WinFill LLwin;

        command = 2;
        EXsubbox[0].mode = 0;           /*  ????OFF              at01_list;
                break;
          case 2:                       /*  ????????                 */
                char_pt = mat02_list;
                  break;
          case 3:                       /*  ??????                     */
                char_pt = mat03_list;
                break;
           /*    1997/04/25 G.T.V H.Mizukami   */
/*****************************************************************************/
void Item_Mat_Sel(void)
{
  int LLpad;
  int LLnum = item_sel_mat;

#ifdef DEBUG
FntPrint("item_sel_mat = %d\n", item_sel_mat);
#endif

        LLpad = Item_MatMouse();

        if(oldpad == PADLup && item_sel_mat)
                item_sel_mat--;
        if(oldpad == PA if(LLpad == 0 || (oldpad & PADnext))
        {
                command     = 99;
                nextcommand = 2;
        }
        else
        {
                if((TGPad1 & PADRdown) || Char_cansel())
                {
                        command     = 127;
                        nextcommand = 0;
                        TGPad1 |= PADnext;
                        StopPreLoad();
  
void ItemMat(void)
{
  int LLpad = 1;
  GsBOXF LLbox;
  int LLnum = ItemListMatNum[item_sel_mat];

#ifdef DEBUG
FntPrint("sel = %d max = %d\n", ItemListMatNum[item_sel_mat],
                ItemListMatMax[item_sel_mat]);
#endif

        LLpad = Item_subMatMouse();

        if(oldpad == PADLup && ItemListMatNum[item_sel_mat] > 1)
                ItemListMatNum[item_sel_mat] -= 2;     ItemListMatNum[item_sel_mat] < ItemListMatMax[item_sel_mat] - 1)
                ItemListMatNum[item_sel_mat]++;

        if(LLnum != ItemListMatNum[item_sel_mat])
        {
                _mt_sel_snd();
                StopPreLoad();
        }

        if(PreLoad == PL_NOUSE)
                ItemSelLoad((long *)(LPKBuf + BG_SZ));

        EXsubbox[1].mode = 1;
        EXsubbox[1].b & PADRdown) || Char_cansel())
        {
                command     = 127;
                nextcommand = 2;
                TGPad1 |= PADnext;
                StopPreLoad();
        }
        else
        {
                if((TGPad1 & PADnext) && LLpad == 0 || (oldpad & PADnext))
                {
                        command     = 99;
                        nextcommand = 3;
       /
void Item_Char_SelInit(void)
{
  WinFill LLwin;

        command = 2;
        EXsubbox[0].mode = 0;           /*  ????OFF                      */
        WinNotDisp(wid[2]);
        memset(&LLwin, 0x00, sizeof(WinFill));
        LLwin.flag  = WINf_GRO;
        LLwin.wflag = WINw_001;
        LLwin.prm   = 5;
        wid[3]      = WinOpen(&LLwin);
        WinSetPos(wid[3], 128, 44, 1      case 3:                       /*  ???13???                 */
                char_pt = char03_list;
                break;
          case 4:                       /*  ???????                   */
                char_pt = char04_list;
                break;
          case 5:                       /*  ?????                       */
                char_pt = char05_listNum[item+ 1 >=
                           BAEMax[item_menu])
                                bak = 1;
                        else
                        {
                                bak = BAENum[item_menu];
                                BAENum[item_menu]++;
                                ItemSelLoad((long *)MODELBuf);
                                BAENum[item_menu] = bak;
                           bak = ItemListNum[item_sel_hero];
                        break;
                  case 1:               /*  ??                             */
             
                        SjsSel = 0;

                switch(item_menu)
                           dispcom = 99;
                                         dispcom     = 99;
              DEBUG
FntPrint("mat sel = %d\n", ItemListMatNum[item_sode(&scr_item_disp);
                                oid cu_l(ANM2D *a2, ANMOBJ *ao)
{

        ao->wait = 1;
        if(dispcom == 99 || wait)
                return;
        if(item_menu != 0 && item_menu != 3)

/*  ???????????????                                           */
/*                                                                           */
         if(ItemListMatNum[item_sel_mat] <
                                ItemListMatMax[item_sel_mat] - 1)
                        {
                                ao->wait = 0;
                                ao->Move(ao, 1);
/*****************************************************************************/
/*  ??????????                                                     */
/*****************************************************************************/
static ANM2D anm2[L,
   wp2,
   {NULL,},
   512, 240,
   NULL, 0,
   NULL,
   0,
   1,
};

/*****************************************************************************/
/*  ???????????????????????????                   */
/*                                                  
};

/*****************************************************************************/
/*  ???????                                                           */
/*****************************************************************************/
static WIPE_PRM wp3[] = {
    {&WjCinCout,16,itch(EXdatflag)
        {
          case 0:                       /*  ??                             */
                *AGoffset = BukSZTBL[EXdatnum].offset;
                *AGsize   = BukSZTBL[EXdatnum].size;
              :                       /*  ????                         */
                *AGoffset = MatSZTBL[EXdatnum].offset;
                *AGsize   = MatSZTBL[EXdatnum].size;
                strcpy(LLbuf, "ITEM\\MATER\\MATER.FFD");
                break;

/*                                             1997/03/28 G.T.V              */
/*****************************************************************************/
static void _sinit3(SCREEN_JOB *ps, int n)
{
  char LLbuf[64];
  int  i, Q1;
  int  LLnum, LLoffset, LLsize;

        if(n == -py(LLbuf, Item_Image_TBL01[i]);
                        break;
                  case 2:               //  ??????
                        LLoffset = AkuSZTBL[EXdatnum].offset;
                        LLsize   = AkuSZTBL[EXdatnum].size;
                        strcpy(LLbuf, Item_Image_TBL02[i]);
                        break;
                  case 3:               //  ????
          dSync(0);
                        break;
                  case PL_DONE:
                        break;
                  case PL_NOUSE:
                  case PL_MUSTNOT:
                  default:
                        select_gfile(LLbuf, &LLoffset, &LLsize);
                        FsOpen(LLbuf);
                        FsSeek(LLoffset);
                        FsReadLzsB((long*)LPKBuf,                                                      */
/*                                                                           */
/*                                             1997/03/28 G.T.V              */
/*****************************************************************************/
static void _init3(SCREEN_JOB *ps, int n)
{

        SjsSel  = 0;
        wait    = 4;
        Sjs            */
/*****************************************************************************/
static void _loop3(SCREEN_JOB *ps)
{
  ClickMap tmpcmp;                      //  ????TEMP?????????

        if(!PreLoad)
        {
                StopPreLoad();
                tmpcmp=town_cmp[j_sel];
                FsOpen(TOWNFILE);
                FsSeek(tmpcmp.lcv_offset+tmpcmp.b
				
/*				*****************************************************************/
/*  ????                                                                 */
/*                                                                           */
/*                                                                           */
/*                                                                           */


56|SCR_NOTPUSH,
   _sinit3,
   _init3,
   _loop3,
   NULL,
   NULL,
   NULL,
   NULL,
   _s_end,
   wp3,
   {&WjCinCout,64,0,0,0},
   512, 240,
   NULL, 0,
   NULL,
   0,
   1,
};

/*****************************************************************************/
/*  ????(????????)??                                           */
                                     
el < list_tbl_num - 1)
                Shop_Sel++;

        if(LLnum != Shop_Sel)
        {
                _mt_sel_snd();
                StopPreLoad();
        }

        if(PreLoad == PL_NOUSE)
        {
                switch(list_tbl[Shop_Sel].f)
                {
                  case 0:
                        for(Q1 = 0, i = 0; Q1 < list_tbl[Shop_Sel].n0; Q1++)
							
						
						*************************************************************************/
#include "ff.h"
#include "ff7list.h"
#include "winsys.h"
#include "cursol.h"
#include "datafanm.inc"
#include "dataflpk.inc"
#include "lpk_sys.inc"
#include "ffaku.h"
#include "clickm.h"
#include "npk_item.inc"

/*****************************************************************************/
/*  ???             */ 
Buf, (long *)ComnBuf
#define DEFSYS LZ_TIM | LZ_CLUT, (long *)SYSBuf,   (long *)ComnBuf
#define SUBBERMAX 3
#define SUBBERMAX 3
#define ZENGAMEN_X 168                  /*  ???X??                     */
#define ZIGAMEN_X  282                  /*  ???X??                     */
#define ZIGAMEN_Y   15                     /*  ????                         */
static int oldx;                        /*  ???X???????           */
static int oldy;                        /*  ???Y???????           */
static int oldpad;                      /*  ?????????      ????           */
int EXdatnum;                           /*  ??                             */

static char EXe????**********tBo[Q1];
     *********cat(mlist           set_          c== PADLmat] -= 2;
rt;
                if(How_Kind_PadCon(0) == 4)     /*  ????????                */
        {
                set_Cursol_Point                                                                        */
/*                                             1997/03/26 G.T.V H.Mizukami   */
/****/
 if(oldpad == PADLup && LLsel > 1)
                BAENum[item_menu] -= 2;
        if(oldpad == PADLdown && LLsel < LLend - 2)
                BAENum[item_menu] += 2;

        if(oldpad == PADLleft && LLsel % 2)
                BAENum[item_menu]--;
        if(oldpad == PADLright && (LLsel % 2) == 0 && LLsel < LLend - 1)
                BAENum[item_menu]++;

        if(LLnum != BAENum[item_me                  set_Cursol_Point(&EXmouse, ZENGAMEN_X, ZIGAMEN_Y);
                }
                StopPreLoad();
                return;
        }
        if((((EXmouse.cx > 250 && EXmouse.cy > 10  &&
              EXmouse.cx < 300 && EXmouse.cy < 22) &&
             (TGPad1 & PADnext)) || (oldpad == PADR1))  && LLend == 20 &&
              LLtop + 20 != BAEMax[item_menu])
        {
   1;
        EXsubbox[0].ber.attribute = 0;
        EXsubbox[0].ber.r =   0;
        EXsubbox[0].ber.g = 128;
        EXsubbox[0].ber.b =   0;
        EXsubbox[0].ber.x = LLsel % 2 * 161 + 122+8 - 256;
        EXsubbox[0].ber.y = LLsel / 2 *  14 +  44 - 120;
        EXsubbox[0].ber.w = 156;
        EXsubbox[0].ber.h = 14;

        if(How_Kind_PadCon(0) == 4)
        {
                set_Curtprm03);
}

/*****************************************************************************/
/*  ???????                                                           */
/*                                                                           */
/*                                                                           */
/*                                                              */
                ItemListMax[item_sel_hero] - 2)
        {
                ItemListNum[item_sel_hero] += 2;
                if(ItemListNum[item_sel_hero] >= ItemListMax[item_sel_hero])
                        ItemListNum[item_sel_hero] =
                                ItemListMax[item_sel_hero] - 1;
        }

        if(oldpad == PADLleft && ItemListNum[item_sel_hero] % 2)
                                   */
/*                                                                           */
/*                                                                           */
/*                                             1997/03/31 G.T.V H.Mizukami   */
/*********************************************************************** */1;
                else
                        SjsSel = 0;
                ic_flag = 0;
                break;
          case 99:
 _;
       /
/* ******
          ******/
  /*  ???          */
  MjsLown && ItemLa      20char05_list;
     t_Cursol_Point(&***u
},
                           {_O_AKU04_FFD, _S_AKU04_FFD},
                           {_O_AKU05_FFD, _S_AKU05_FFD},
                           {_O_AKU06_FFD, _S_AKU06_FFD},
};

PFileDat Npk_BouSZTBL[] = {{_O_BOUGU00_FFD, _S_BOUGU00_FFD},
                           {_O_BOUGU01_FFD, _S_BOUGU01_FFD},
                        UKI09_FFD, _S_BUKI09_FFD},
                           {_O_BUKI10_FFD, _S_BUKI00_FFD},
                           {_O_BUKI11_FFD, _S_BUKI11_FFD},
#define  _S_GUA_021_TIM	56566
#define  _O_GUA_022_TIM	0xe000
#define  _S_GUA_022_TIM	67549
#define  _O_GUA_023_TIM	0x1e800
#define  _S_GUA_023_TIM	61491
#define  _O_GUA_024_TIM	0x2e000
#define  _S_GUA_024_TIM	67618
#define  _O_GUA_025_TIM	0x3f000
#define  _S_GUA_025_TIM	68968

#define  _BOUGU04_FFD_SIZE	327680

/*** [EOF] ***/
/*** NPK Header File ***/

#define  _O_GUA_02


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

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

На всякий случай ссылка на полную версию моего «говнокода».

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

P.S.: прошу прощение за тонну орфографических и пунктуационных ошибок, если заметите что-то уж совсем вопиющее, то пишите и я исправлю.

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


  1. VBKesha
    23.01.2018 18:33

    Спасибо за такую статью!
    PS. Хотелось бы в конце статьи ссылки на источники с форматами и прочим.


    1. Hedzin Автор
      23.01.2018 21:07

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


      1. VBKesha
        23.01.2018 22:00

        Да в общем только ссылки на википедию с описанием формата нет. А в остальном да всё есть.


        1. Hedzin Автор
          23.01.2018 22:10
          +1

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


  1. Javian
    23.01.2018 21:02

    изучить его контент напрямую

    Так там были только картинки?


    1. Hedzin Автор
      23.01.2018 21:06

      95% диска — картинки, так как это просто диск с доп-материалами. были еще модели(около 5 штук примерно) и буквально пара файлов фоновой музыки в формате типа midi. но так как принцип хранения всего этого был один и тот же, и сам я все это начал именно из-за картинок, то я не стал разбирать вообще все файлы диска, вроде как технически — интересных моментов там не было, а статья и так вышла слишком большой.


  1. 9660
    24.01.2018 04:28

    Спасибо. Интересно.

    Первое что бросилось в глаза — расширение файлов NPK и LPK, что косвенно указывало на то, что это архивы.

    Но вот этот момент как-то непонятен.


    1. AotD
      24.01.2018 06:29

      — Скорее всего файлы достаточно тяжелые
      — Файлов меньше чем предполагаемых изображений на диске
      — *PK — *package — пакет данных
      Вывод: это собранные в один файл данные.


    1. exformat
      24.01.2018 13:37

      NPK… N??? PK = package = упаковка и т.д.…
      Плюс минус такая логика


    1. plus_stick
      24.01.2018 13:37

      PK — package.
      Также файлы с ресами у многих игр имеют похожие расширения. Например .mpq у близзардовских.


    1. Hoksmur
      24.01.2018 13:37

      ?PK ->? + 'pack', 'packed'


    1. Neikist
      24.01.2018 14:05

      Хех, массовое одобрение комментариев выше автором?


      1. Hedzin Автор
        24.01.2018 14:24

        да, прошу прощения, до сих пор не могу понять, как надо было в этой ситуации поступить))


  1. KVL01
    24.01.2018 11:23

    можно было исключить сразу всякие zip-related и прочие методы имеющие какие-то стандартные заголовки, так как ничего похожего на заголовок у сжатых данных не наблюдалось.

    Встретились мне как-то обычные zip-файлы, но заголовки были оторваны и хранились скопом в отдельном файле. К счастью, это было на PC, и хитрецы не додумались спрятать unzip.dll, которая валялась в папке с приложением и наводила на определённые мысли.


  1. serpentcross
    24.01.2018 12:38

    Ууууууу!!! Ути мой родной!!!)))) Я так давно не прикладывал к этому руку)) А ведь когда то в далёкие уже нулевые занимался почти тем-же самым))) рипал архивы игрушек и пытался переделать под своё) Иногда даже оч круто получалось)

    Спасибо за статью!!!