Главная » Хабрахабр » Как я визуальную новеллу препарировал

Как я визуальную новеллу препарировал

Привет, Хабр!

На днях я захотел достать ресурсы одной визуальной новеллы, созданной с помощью Ren'Py (Да, да, того самого "Бесконечного Лета"). Опытным путем было установлено, что все они хранятся в файле archive.rpa. Я нашел готовые скрипты для распаковки на Github, но решил достать их сам, Haskell в помощь...

archive.rpa

Во-первых, давайте разберемся с чем конкретно мы имеем дело, то есть как ресурсы хранятся в нашем "архиве". Откроем его в dhex или в аналогичной программе:

Что мы видим? Сначала данные о Ren'Py, потом еще что-то, а потом — начало PNG файла! Вот оно!
Долистаем до конца PNG:

Про начало и конец PNG

Тип бинарного файла можно определить по его первым байтам (т. н. "магическому числу"). У PNG "магические" байты таковы: 89 50 4e 47 0d 0a 1a 0a. Конец же PNG файла тоже всегда одинаков: 49 45 4e 44 ae 42 60 82
Список "магических чисел" других форматов можно найти здесь

Ага! PNG кончился, после него байты 4d 61 64 65 20 77 69 74 68 20 52 65 6e 27 50 79 2e (в ASCII: "Made with Ren'Py."), после чего — начало нового PNG. Вывод: данные ресурсов в .rpa архивах разделяются этой последовательностью байтов. Время подумать о программе.

Пишем код

Итак, что же конкретно должна делать программа? Вот это:

  1. Считать файл
  2. Разбить его на файлы ресурсов
  3. Записать файлы ресурсов в папку, определенную пользователем
  4. Приделать каждому файлу соответствующее расширение

Разбиваем на части

Начнем, пожалуй, с функции разбиения на файлы:

Extractor.hs:

module Extractor where import qualified Data.ByteString.Lazy as B import Data.List.Split extractRes :: B.ByteString -> [B.ByteString]
extractRes bs = map B.pack $ splitOn magicSep $ B.unpack bs magicSep = [0x4d, 0x61, 0x64, 0x65, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x52, 0x65, 0x6e, 0x27, 0x50, 0x79, 0x2e]

Здесь magicSep — это те самые "магические" байты, разделяющие файлы ресурсов. Функция extractRes берет строку байтов, преобразует ее в список, разбивает его на списки байтов файлов, и каждый из этих списков преобразуется обратно в строку байтов. Остается один вопрос: зачем я использовал ленивую разновидность строк байтов? Ответ на него мы вскоре получим.

Устанавливаем расширение

ExtensionId.hs:

module ExtensionId where import qualified Data.ByteString.Lazy as B import Data.List (isPrefixOf) import System.Directory type FileType = String readExtension :: FilePath -> IO (Maybe FileType)
readExtension path = B.readFile path >>= (return . getExtension) writeExtension :: FilePath -> Maybe FileType -> IO ()
writeExtension _ Nothing = return ()
writeExtension path (Just ext) = renameFile path $ path ++ ext getExtension :: B.ByteString -> Maybe FileType
getExtension = magicLookup magicMap magicLookup :: [(B.ByteString, FileType)] -> B.ByteString -> Maybe FileType
magicLookup [] _ = Nothing
magicLookup ((magic, fileType) : rest) bytes | (B.unpack magic) `isPrefixOf` (B.unpack bytes) = Just fileType | otherwise = magicLookup rest bytes magicMap = [(pngMagic, ".png"), (jpgMagic, ".jpg"), (oggMagic, ".ogg")] pngMagic = B.pack [0x89, 0x50, 0x4e, 0x47]
jpgMagic = B.pack [0xff, 0xd8, 0xff]
oggMagic = B.pack [0x4f, 0x67, 0x67, 0x53]

Здесь мы видим две главные функции: readExtension и writeExtension. Несложно догадаться, что одна считывает первые байты файла и на их основе устанавливает расширение, а вторая — переименовывает файл в соответствии с ним. Всю грязную работу по определению расширения на себя берет функция magicLookup, которая просто проходит по списку известных "магических чисел" и сравнивает их с началом файла. magicMap представляет из себя список соответствий байтов и расширений, pngMagic, jpgMagic и oggMagic — "магические числа" форматов файлов. Их список может быть расширен читателем, но я точно знал, что кроме этих форматов в архиве ничего не встретится.

Уффф...

Остался наш дорогой Main.hs:

module Main where import qualified Data.ByteString.Lazy as B import System.Environment
import System.Directory
import System.FilePath
import System.IO import ExtensionId
import Extractor main :: IO ()
main = do args <- getArgs if (length args) /= 2 then usage else extractToFolder (args !! 0) (args !! 1) extractToFolder :: FilePath -> FilePath -> IO ()
extractToFolder input output = do b <- doesFileExist input if b then do b <- doesDirectoryExist output if b then do c <- confirm $ "Folder " ++ output ++ " already exists, overwrite it? (y/n): " if c then do removeDirectoryRecursive output createDirectory output extractToFolder' input output else return () else extractToFolder' input output else putStrLn $ "Input file " ++ input ++ " does not exist" extractToFolder' :: FilePath -> FilePath -> IO ()
extractToFolder' input output = do bytes <- B.readFile input let bs = extractRes bytes doNastyWork bs 0 where doNastyWork [] _ = putStrLn "Done." doNastyWork (bs : rest) n = do let path = output </> ("extraction_" ++ (show n)) B.writeFile path bs ext <- readExtension path writeExtension path ext putStrLn $ (show $ n + 1) ++ " files processed..." doNastyWork rest (n + 1) confirm :: String -> IO Bool
confirm q = do putStr q hFlush stdout line <- getLine case head line of 'y' -> return True 'n' -> return False _ -> confirm q usage :: IO ()
usage = putStrLn "Usage: extractrpa [ARCHIVE] [OUTPUT FOLDER]"

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

В extractToFolder творится примерно то же самое: проверка корректности ввода (защита от дурака), и если папка, в которую надо выводить, не существует или пользователь дал согласие на уничтожение ее содержимого, обращается за помощью к extractToFolder'

В других языках способ обработки данных примерно такой: читаем все данные, после чего разбиваем их на части, после чего каждую записываем в файл и даем расширение. Haskell же предлагает несколько другой подход: обрабатывать данные по мере их поступления. Это возможно благодаря ленивым вычислениям (вот почему я использовал ленивые строки байтов). Такой подход позволяет пользователю лучше видеть процесс обработки, что уже неплохо. extractToFolder' — своеобразный конвейер, на котором и происходит обработка архива: читаем файл, разбиваем его на части, пока части не кончатся записываем их в файлы с уникальными именами.

Ну вот и все, набираем в терминале заветные команды:

$ ghc --make Main.hs -o extractrpa
...
$ extractrpa archive.rpa archive
...
Done.

И наслаждаемся ловко стыренным искусно добытым контентом!
Вопросы, жалобы, предложения — прошу в комментарии!


x

Ещё Hi-Tech Интересное!

OpenSceneGraph: Групповые узлы, узлы трансформации и узлы-переключатели

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

«Монстры в играх или 15 см достаточно для атаки»

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