Что скрыто в файлах персонажей Doki Doki Literature Club

от
Soft    gamedev, визуальные новеллы, renpy, python, linux, ffmpeg, ffplay

Визуальная новелла Doki Doki Literature Club! привлекла меня тем, что содержит множество загадок, которые при первом прочтении заметить невозможно. Рассказывать обо всех я не стану, лучше сами скачайте — игра бесплатна. А я, постараясь без спойлеров, расскажу о том, что скрыто в файлах персонажей из папки characters/ и как самому сделать нечто подобное. В конце вас ждёт небольшой квест.

Кому лень читать, есть видео:


Декодируем
В папке characters/ лежат четыре файла, по одному на каждую героиню. В них зашифрована некоторая информация.
monika.chr natsuki.chr sayori.chr yuri.chr

Счастливые обладатели Linux получают подсказку сразу при открытии папки:
2018-01-14_18-35-47.png


Юри
Начнём с текстового файла yuri.chr. Выведем первые и последние 50 байт.
  1. $ head -c 50 yuri.chr
  2. SWYgeW91IGZvdW5kIHRoaXMgbm90ZSBpbiBhIHNtYWxsIHdvb2
  3. $ tail -c 50 yuri.chr
  4. FsbHkgcmVhbGx5LCByZWFsbHkgaG9wZSBzby4NCg0KfuKZpQ==

Два знака равно в конце файла говорят о том, что перед нами текст в base64. Раскодируем его:
  1. $ cat yuri.chr | base64 -d > yuri.txt
  2. $ head -c 50 yuri.chr
  3. If you found this note in a small wooden box with
  4. $ tail -c 50 yuri.txt
  5. is you. I actually really, really hope so.
  6.  
  7. ~♥
yuri.txt


Сайори
sayori.chr — аудио в формате ogg. В этом можно убедиться, выполнив:
  1. $ file sayori.chr
  2. sayori.chr: Ogg data, Vorbis audio, mono, 44100 Hz, ~110000 bps, created by: Xiph.Org libVorbis I

sayori.ogg

Если прослушать аудио в ffplay, будет видна спектрограмма:
sayori_ffplay.png

Это напомнило мне времена ZX Spectrum, когда программы записывались на магнитную ленту в виде звука.
Очевидно, что перед нами qr-код. Осталось только достать изображение. В этом поможет ffmpeg с фильтром showspectrumpic:
  1. ffmpeg -i sayori.chr -lavfi showspectrumpic='s=300x510:saturation=0:scale=4thrt:legend=0',negate -y sayori_qr.png

С параметрами фильтра можно ознакомиться в документации. Основная проблема была с выбором нужного размера (параметр s). Если пропорции не совпадали, то qr-код мог обрезаться или быть слишком сжатым. saturation=0 делает картинку чёрно-белой, scale=4thrt добавляет контраста в изображение, а legend=0 убирает легенду из результирующего изображения. После showspectrumpic применяется инверсия цвета — фильтр negate. В итоге получаем:
sayori_qr1.png

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

2018-01-14-20-10-01.jpg
http://projectlibitina.com


Нацуки
Файл natsuki.chr — изображение в формате jpeg.
natsuki1.jpg

Можно сразу открывать в редакторе, а можно предварительно сделать инверсию цвета и отражение по-вертикали:
  1. ffmpeg -i natsuki.chr -vf negate,vflip natsuki.jpg
natsuki2.jpg

Остаётся только преобразовать изображение в полярные координаты. В GIMP это Filters -> Distorts -> Polar Coordinates:
2018-01-15_14-19-58.png

natsuki3.jpg


Моника
Наконец, monika.chr. Это png-изображение размером 800x800.
monika.png

В центре изображения расположен чёрно-белый рисунок. Вырезаем его:
  1. ffmpeg -i monika.chr -vf crop=140:140:330:330 monika_code.png
monika_code.png
Теперь каждому чёрному пикселю ставим в соответствие бит 0, а каждому белому — 1. Получаем последовательность из нулей и единиц длиной 140*140 = 19600.
  1. 01010001001100100100011001110101010010010100100001101100011...
  2. ...
  3. ...
  4. ...11001110011110100000000000000000000000000000000000000000000000000000000000000000000000000000000

Преобразуем биты в символы. Получится 2450 байт текста:
Открыть спойлер

Скрипт, который делает всю работу:
  1. #!/usr/bin/python
  2. import getopt
  3. import sys
  4. from PIL import Image
  5.  
  6. def usage():
  7.   print('Usage: monika_decode.py [-v] [FILE]')
  8.   sys.exit(2)
  9.  
  10. def main(argv):
  11.   if len(argv) == 0:
  12.     usage()
  13.   verbose = False
  14.   input = ''
  15.   for arg in argv:
  16.     if arg == '-v':
  17.       verbose = True
  18.     else:
  19.       input = arg
  20.   if len(input) == 0:
  21.     usage()
  22.   image = Image.open(input)
  23.   width, height = image.size
  24.  
  25.   i = 0
  26.   value = 0
  27.   bits = ''
  28.   text = ''
  29.   for y in range(height):
  30.     for x in range(width):
  31.       pixel = image.getpixel((x, y))
  32.       mean = (pixel[0] + pixel[1] + pixel[2]) / 3
  33.       bit = 1 if mean >= 128 else 0
  34.       bits += str(bit)
  35.       value = value | (bit << (7 - i))
  36.       i += 1
  37.       if i >= 8:
  38.         text += chr(value)
  39.         value = 0
  40.         i = 0
  41.     bits += '\n'
  42.   if verbose:
  43.     print('Input image: {}x{}'.format(width, height))
  44.     print('Bits:')
  45.     print(bits)
  46.     print('Result string:')
  47.   print(text)
  48.  
  49. if __name__ == '__main__':
  50.   main(sys.argv[1:])

Символ = в конце полученного текста снова говорит, что перед нами base64. Декодируем:
  1. python3 monika_decode.py monika_code.png | base64 -d > monika.txt
monika.txt


Кодируем
Теперь поставим обратную задачу — закодировать необходимые данные, чтобы получить такие же .chr файлы.

Юри
Здесь всё просто. Берём текст, кодируем его в base64 без переносов строки (-w 0) и сохраняем в .chr файл:
  1. cat yuri.txt | base64 -w 0 > yuri.chr

Сайори
Для начала нужно подготовить картинку с qr-кодом. Можно воспользоваться генератором от Google:
  1. wget -O qr.png "https://chart.googleapis.com/chart?cht=qr&chs=512x512&choe=UTF-8&chl=https://annimon.com/"
Далее в графическом редакторе можно её немного исказить.
en_qr.png

Теперь кодируем изображение в аудиофайл, воспользовавшись https://github.com/alexadam/img-encode
Там есть онлайн версия, но я воспользуюсь скриптом на Python:
  1. $ ./imgencode.py -i qr.png -o qr.wav -t 8
  2. $ ffmpeg -i qr.wav -b:a 128k sayori.ogg
  3. $ mv sayori.ogg sayori.chr

en_sayori.ogg


Нацуки
Фильтр Polar Coordinates работает в обе стороны. На сей раз нам потребуется круглое или овальное изображение и такие параметры:
2018-01-16_13-40-09.png
Дальше делаем инверсию цвета, сохраняем картинку в jpg и меняем расширение на .chr.

Моника
Для начала закодируем текст в base64:
  1. cat monika.txt | base64 -w 0 > monika_base64.txt
Дальше нужно взять полученный текст и каждый ASCII код символа перевести в двоичное число и на месте 0 бита закрашивать картинку чёрным, а на месте 1 — белым цветом. Вот скрипт:
  1. #!/usr/bin/python
  2. import getopt
  3. import sys
  4. from math import ceil, sqrt
  5. from PIL import Image
  6.  
  7. def main(argv):
  8.   if len(argv) != 2:
  9.     print('Usage: python monika_encode.py [INPUT_FILE] [OUTPUT_FILE]')
  10.     sys.exit(2)
  11.  
  12.   with open(argv[0], 'r') as f:
  13.     input = f.read()
  14.  
  15.   bits_count = len(input) * 8
  16.   size = int(ceil(sqrt(bits_count)))
  17.   image = Image.new("RGB", (size, size), "black")
  18.   pixels = image.load()
  19.  
  20.   x = 0
  21.   y = 0
  22.   white = (255, 255, 255)
  23.   for ch in input:
  24.     value = ord(ch)
  25.     for i in range(8):
  26.       bit = (value >> (7-i)) & 1
  27.       if bit == 1:
  28.         pixels[x, y] = white
  29.       x += 1
  30.       if x >= size:
  31.         x = 0
  32.         y += 1
  33.   image.save(argv[1], 'png')
  34.  
  35. if __name__ == '__main__':
  36.   main(sys.argv[1:])

Конвертируем текст в изображение:
  1. python monika_encode.py monika_base64.txt code.png
monika_code.png

Теперь осталось только наложить это изображение в центр другого изображения.


Квест
Напоследок, предлагаю пройти небольшой квест и декодировать файл персонажа, назовём её Лизой: lisa.chr

JUST MONIKA
  • +7
  • views 44920