mkv

dual language for rabi-ribi v1.75

mkv
Feb 19th, 2017
284
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 15.72 KB | None | 0 0
  1. #!/usr/bin/env python2
  2. # -*- coding: utf-8 -*-
  3. from __future__ import print_function
  4. README="""
  5.  
  6.    dual language display v1.0
  7.    for rabi-ribi v1.75
  8.    MIT-licensed 2017/02/19 ed@rizon
  9.  
  10.    first install this http://taku910.github.io/mecab/#download
  11.    then run this with py2/3 and visit http://127.0.0.1:8086/the.html
  12.    it should look like this https://my.mixtape.moe/sljnjk.png
  13.    video of an older version https://my.mixtape.moe/lgjrje.mp4
  14.  
  15.    if there's a new version of rabi-ribi and this script has died,
  16.    open up the following file for reference along with cheatengine:
  17.    C:\Program Files (x86)\Steam\steamapps\common\Rabi-Ribi\localize\event\story_en.rbrb
  18.    
  19.    enter a new dialogue scene ingame (do not advance past first line),
  20.    look for the dialogue text in story_en.rbrb, you'll see its block ID
  21.    right above the line you've got on-screen ingame.
  22.    do a 4byte search for that ID, you should get 2 results (either is fine)
  23.    if more than 1 result then progress through that dialogue scene until
  24.    you enter a new one, then do another search for the new block ID,
  25.    repeat until just 2 results.
  26.    do a pointerscan on one of the hits, grab the lowest result with 0 offset,
  27.    this is ADR_BLOCK_ID
  28.    
  29.    still without advancing the dialogue do another 4byte search for 0,
  30.    then move one line ahead ingame and search for 1, then one more line
  31.    and search for 2 etc until there's just one result, pointerscan that
  32.    and grab the lowest result with 0 offset, this is ADR_BLOCK_POS
  33.    
  34.    note that if the dialogue box goes away for a camera pan or whatever
  35.    then you have to start over since that makes it go back to 0, so
  36.    do this with a dialogue scene that goes on for a bit uninterrupted
  37.    
  38.    ADR_STRING_TAB is unimportant, just set it None
  39.  
  40. """
  41. ADR_BLOCK_ID = 0xd7d64
  42. ADR_BLOCK_POS = 0xd7d50
  43. ADR_STRING_TAB = 0x27ab64
  44.  
  45.  
  46.  
  47. from ctypes.wintypes import \
  48.     WORD, DWORD, HMODULE, HANDLE, MAX_PATH
  49. from ctypes import \
  50.     POINTER, byref, cast, addressof, string_at, \
  51.     create_unicode_buffer, sizeof, windll, \
  52.     c_ubyte, c_ulong, c_void_p
  53. from builtins import chr
  54. import subprocess
  55. import threading
  56. import socket
  57. import struct
  58. import codecs
  59. import base64
  60. import json
  61. import zlib
  62. import time
  63. import sys
  64. import re
  65.  
  66. ##
  67. ## codepage stuff
  68. ##
  69.  
  70. # chcp 65001
  71.  
  72. enc_py26 = 'eJxdkttyhCAMht/F615wBp+l02GUusrqHkZ0ZtunLwvJmu0N//eHhIDxs+lSiLH5aPo4apBpTiFlDncmbVElVFXwmrGiFry1uqjTDFSAYtyA1nxnIM9wUMg3ElSBQr2BetNWtbAPfVtZ61tV91u4B2fMAAgArmBL6BdwBIEgERSCRjAIFsFlGPbgz3eEmLxgTB32wQSXYOc1w9gLyUWBuazcMfm80PSbl5hugglRTzyM5+9W/LO1JYnIdzs8NhIo91i6LV7xXOd0i4cWloQVYU3YELaEHeGWMGfU0A6ctuC0B382Od+mrs8636LzK8Ke4dIFH37WuCwxgB3XYZiBYxiW7voNrrxYgFlvl+4KvO3rHNOU3X3Lcy2jT1M8bc8BUsYP/QrgdPft5MufWMH3w8ELcnlLhSMh8yvBgjpUn+LYfP0Bd0QChw=='
  73.  
  74. enc_py27 = 'eJxdkt1ygyAQRt/F617wDz5Lp+MoNUo0MSM6k/bpi7AbN73xnA9wF8TPqo0+hOqj6sKgAeMUfUzuH0zaTCVUIWTNWKYVQBi3Vmc6zYACiOMGaIGu0MB6w4HwnpFABYQ6BuqYutDCPPSvZXm/VmW+hv1wxgyIAOEKpoR+CUcRKBJFoWgUg2JRjjP1u2+uD5QQG8GYOuOTCS4hTmuSoROSiyxTfnLH5LGh8Tc9QlwEE6JUPEPD36P4F0tLMiLfY//cyEDex9xu4Y51ndM1Fs0uiSvimrghbok74jVxzmignTltx2k/Thvyo+N1GdsucVqCa1aUPcmt9Y3/WcM8Bw9xWPt+Ag++n9v7N6R8fAFhXW7tHXzb1ynEMaXHli45/wdxDJftuE3q+NVfA3jV+3Zp8m9ZpOn602f0fJYi54LkrwUW6JBNDEP19Qcj/gml'
  75.  
  76. def zenc(lst):
  77.     return base64.b64encode(zlib.compress(json.dumps(lst, separators=(',',':'))))
  78.  
  79. def zdec(b64):
  80.     return json.loads(zlib.decompress(base64.b64decode(b64)).decode())
  81.  
  82. if sys.version_info.major == 2 and sys.version_info.minor == 6:
  83.     tcodecs = zdec(enc_py26)
  84. else:
  85.     tcodecs = zdec(enc_py27)
  86.  
  87. # kinda fixes py2.7
  88. def cp65001(name):
  89.     if name.lower() == 'cp65001':
  90.         return codecs.lookup('utf-8')
  91. codecs.register(cp65001)
  92.  
  93. ##
  94. ## process interaction declarations
  95. ##
  96.  
  97. OpenProcess = windll.kernel32.OpenProcess
  98. ReadProcessMemory = windll.kernel32.ReadProcessMemory
  99. CloseHandle = windll.kernel32.CloseHandle
  100. PROCESS_ALL_ACCESS = 0x1f0fff
  101. PROCESS_QUERY_INFORMATION = 0x400
  102. PROCESS_VM_READ = 0x10
  103. LIST_MODULES_ALL = 0x3
  104.  
  105. ##
  106. ## list processes and modules within
  107. ##
  108.  
  109. #def findproc(name):
  110. #    cmd = 'wmic process get description,processid'
  111. #    for line in run(cmd.split(' ')):
  112. #        line = line.strip()
  113. #        if line[-3:].lower() == 'exe':
  114. #            ofs = line.find(' ')
  115. #            pid = line[:ofs].strip()
  116. #            line = line[ofs:].strip()
  117. #        else:
  118. #            ofs = line.rfind(' ')
  119. #            pid = line[ofs+1:].strip()
  120. #            line = line[:ofs].strip()
  121. #        if line == name:
  122. #            return pid
  123. #    return None
  124.  
  125. def EnumProcesses():
  126.     buf_count = 256
  127.     while True:
  128.         buf = (DWORD * buf_count)()
  129.         buf_size = sizeof(buf)
  130.         res_size = DWORD()
  131.         if not windll.Psapi.EnumProcesses(byref(buf), buf_size, byref(res_size)):
  132.             raise OSError('EnumProcesses failed')
  133.         if res_size.value >= buf_size:
  134.             buf_count *= 2
  135.             continue
  136.         count = res_size.value // (buf_size // buf_count)
  137.         return buf[:count]
  138.  
  139. def EnumProcessModulesEx(hProcess):
  140.     buf_count = 256
  141.     while True:
  142.         buf = (HMODULE * buf_count)()
  143.         buf_size = sizeof(buf)
  144.         needed = DWORD()
  145.         if not windll.Psapi.EnumProcessModulesEx(
  146.                 hProcess, byref(buf), buf_size,
  147.                 byref(needed), LIST_MODULES_ALL):
  148.             raise OSError('EnumProcessModulesEx failed')
  149.         if buf_size < needed.value:
  150.             buf_count = needed.value // (buf_size // buf_count)
  151.             continue
  152.         count = needed.value // (buf_size // buf_count)
  153.         return map(HMODULE, buf[:count])
  154.  
  155. def GetModuleFileNameEx(hProcess, hModule):
  156.     buf = create_unicode_buffer(MAX_PATH)
  157.     nSize = DWORD()
  158.     if not windll.Psapi.GetModuleFileNameExW(
  159.             hProcess, hModule, byref(buf), byref(nSize)):
  160.         raise OSError('GetModuleFileNameEx failed')
  161.     return buf.value
  162.  
  163. def get_process_modules(pid):
  164.     hProcess = windll.Kernel32.OpenProcess(
  165.             PROCESS_QUERY_INFORMATION | PROCESS_VM_READ,
  166.             False, pid)
  167.     if not hProcess:
  168.         raise OSError('Could not open PID %s' % pid)
  169.     try:
  170.         return [
  171.             [ hModule.value, GetModuleFileNameEx(hProcess, hModule) ]
  172.             for hModule in EnumProcessModulesEx(hProcess)]
  173.     finally:
  174.         windll.Kernel32.CloseHandle(hProcess)
  175.  
  176. def getmodule(procname, modname):
  177.     procname = '\\' + procname
  178.     modname = '\\' + modname
  179.     for pid in EnumProcesses():
  180.         try:
  181.             dll_list = get_process_modules(pid)
  182.             if dll_list[0][1].endswith(procname):
  183.                 for pair in dll_list:
  184.                     if pair[1].endswith(modname):
  185.                         return [pid, pair[0]]
  186.         except OSError as ose:
  187.             #print(str(ose))
  188.             pass
  189.  
  190. ##
  191. ## figure out what character encoding is used for japanese
  192. ##
  193.  
  194. def unfuck(input, expect):
  195.     for src in tcodecs:
  196.         try:
  197.             tmp3 = input.decode(src)
  198.         except:
  199.             continue
  200.         for via in tcodecs:
  201.             try:
  202.                 tmp = tmp3.encode(via)
  203.             except:
  204.                 continue
  205.             for enc in tcodecs:
  206.                 try:
  207.                     tmp2 = tmp.decode(enc).encode('utf-8')
  208.                     if tmp2.startswith(expect):
  209.                         print(src, via, enc)
  210.                 except:
  211.                     continue
  212.  
  213. def mecab(txt):
  214.     cmd = [ 'c:\\Program Files (x86)\\MeCab\\bin\\mecab.exe' ]
  215.     proc = subprocess.Popen(cmd,
  216.         stdin=subprocess.PIPE,
  217.         stdout=subprocess.PIPE,
  218.         stderr=subprocess.PIPE,
  219.         universal_newlines=False)
  220.     txt = txt.encode('utf-8')
  221.     stdout = proc.communicate(input=txt)[0]
  222.     kana = ''
  223.     html = ''
  224.     for line in iter(stdout.decode('utf-8').splitlines()):
  225.         if line == 'EOS':
  226.             kana += '\n'
  227.             html += '\n'
  228.             continue
  229.         conv = line.split(',')[-2]
  230.         orig = line.split('\t')[0]
  231.         if conv == '*':
  232.             conv = orig
  233.         yomi = ''
  234.         for ch in conv:
  235.             co = ord(ch)
  236.             # hiragana >= 0x3041 and <= 0x3096
  237.             # katakana >= 0x30a1 and <= 0x30f6
  238.             if co >= 0x30a1 and co <= 0x30f6:
  239.                 yomi += chr(co - (0x30a1 - 0x3041))
  240.             else:
  241.                 yomi += ch
  242.         if yomi == orig:
  243.             kana += orig
  244.             html += orig
  245.         else:
  246.             kana += yomi
  247.             html += (
  248.                 '<table><tr><td class="sup">{0}</td></tr>' +
  249.                 '<tr><td>{1}</td></tr></table>').format(
  250.                     yomi, orig)
  251.     return [ kana, html ]
  252.  
  253. ##
  254. ## change terminal color
  255. ##
  256.  
  257. def color(c):
  258.     # w: black blue green cyan red magenta yellow grey
  259.     # u: black red green yellow blue magenta cyan grey
  260.     cmap = [ 0, 4, 2, 6, 1, 5, 3, 7 ]
  261.    
  262.     # normal=0 bright_fg=8 bright_bg=128
  263.     # fg + bg * 16 + (style | light)
  264.     attrs = cmap[c % 8] + int(c / 8) * 8
  265.     attrs = int(attrs)
  266.    
  267.     # stdout=-11 stderr=-12
  268.     handle = -11
  269.    
  270.     mc = windll.kernel32.GetStdHandle
  271.     mc.argtypes = [ DWORD ]
  272.     handle = mc(handle)
  273.    
  274.     mc = windll.kernel32.SetConsoleTextAttribute
  275.     mc.argtypes = [ HANDLE, WORD ]
  276.     mc(handle, attrs)
  277.  
  278. def printc(c, *args, **kwargs):
  279.     color(c)
  280.     print(*args, **kwargs, flush=True)
  281.     color(7)
  282.  
  283. ##
  284. ## httpd
  285. ##
  286.  
  287. current_text = None
  288. class Httpd(object):
  289.     def __init__(self):
  290.         self.text = ''
  291.         thr = threading.Thread(target=self.run, args=())
  292.         thr.daemon = True
  293.         thr.start()
  294.    
  295.     def set(self, text):
  296.         self.text = text
  297.    
  298.     def run(self):
  299.         old_text = self.text
  300.         s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  301.         s.bind(('0.0.0.0', 8086))
  302.         s.listen(1)
  303.         while True:
  304.             try:
  305.                 conn, addr = s.accept()
  306.                 data = conn.recv(4096)
  307.                 if not data:
  308.                     conn.close()
  309.                     continue
  310.                 data = data.decode('utf-8')
  311.                 if not data.startswith('GET /the.html HTTP/1.1'):
  312.                     msg = 'HTTP/1.1 404 Not Found\r\nContent-Length: 4\r\nConnection: close\r\n\r\nnope'
  313.                     conn.sendall(msg.encode('utf-8'))
  314.                     time.sleep(0.1)
  315.                     conn.close()
  316.                     continue
  317.                 response = """HTTP/1.1 200 OK\r
  318. Content-Type: text/html\r
  319. Connection: close\r
  320. \r
  321. <!DOCTYPE html><html lang="en"><head>
  322.    <meta charset="utf-8">
  323.    <title>rabi-ribi</title>
  324.    <meta http-equiv="X-UA-Compatible" content="IE=edge">
  325.    <meta name="viewport" content="width=device-width, initial-scale=0.6">
  326.    <style>
  327. html,body,#t,#t>tr,#t>tr>td {
  328.    width: 100%;
  329.    height: 100%;
  330.    overflow: hidden;
  331.    font-family: sans-serif;
  332.    background: #fff;
  333.    color: #000;
  334. }
  335. #t2 {
  336.    font-size: 2em;
  337.    text-align: center;
  338.    line-height: 1.3em;
  339. }
  340. td table {
  341.    display: inline-block;
  342.    border-bottom: 1px solid #aaa;
  343.    border-radius: .3em;
  344. }
  345. td table,
  346. td table * {
  347.    margin: 0;
  348.    padding: 0;
  349.    border-collapse: collapse;
  350.    line-height: 1.1em;
  351.    margin-bottom: -.15em;
  352. }
  353. .sup {
  354.    font-size: .7em;
  355. }
  356. #hidden {
  357.    display: none;
  358. }
  359. #ja {
  360.    line-height: 2em;
  361. }
  362.  
  363. </style></head><body>
  364.    <table id="t"><tr><td id="t2">
  365. """ + self.text.replace('\n', '<br />\r\n') + """
  366.    </td></tr></table>
  367.    <div id="hidden">
  368. """
  369.                 conn.sendall(response.encode('utf-8'))
  370.                 while self.text == old_text:
  371.                     conn.sendall('.'.encode('utf-8'))
  372.                     for n in range(10):
  373.                         if self.text != old_text:
  374.                             break
  375.                         time.sleep(0.1)
  376.                 old_text = self.text
  377.                 response = """
  378. </div><script>
  379.    window.location.reload(true);
  380. </script></body></html>"""
  381.                 conn.sendall(response.encode('utf-8'))
  382.                 time.sleep(0.1)
  383.                 conn.close()
  384.             except:
  385.                 pass
  386.  
  387. httpd = Httpd()
  388.  
  389. ##
  390. ## parse game files
  391. ##
  392.  
  393. def parse_rbrb(path, enc):
  394.     rbrb = []
  395.     scene_id = None
  396.     scene_lines = None
  397.     with open(path, 'rb') as f:
  398.         f.readline()
  399.         for n, line in enumerate(f):
  400.             line = line.decode(enc).strip()
  401.             if line.startswith('//'):
  402.                 continue
  403.             if not line:
  404.                 if scene_id is not None:
  405.                     rbrb.append([
  406.                         scene_id,
  407.                         scene_lines
  408.                     ])
  409.                 scene_id = None
  410.                 scene_lines = None
  411.                 continue
  412.             if scene_id is None:
  413.                 try:
  414.                     scene_id = int(line)
  415.                     scene_lines = []
  416.                 except:
  417.                     printc(8+1, 'fuck {0} line {1}'.format(path, n+2))
  418.                 continue
  419.             scene_lines.append(line)
  420.     return rbrb
  421.  
  422. path = 'C:\\Program Files (x86)\\Steam\\steamapps\\common\\Rabi-Ribi\\localize\\event\\'
  423. rb_en = parse_rbrb(path + 'story_en.rbrb', 'utf-8')
  424. rb_ja = parse_rbrb(path + 'story_jp.rbrb', 'cp932')
  425. printc(8+3, 'rb_en entries:', len(rb_en))
  426. printc(8+3, 'rb_ja entries:', len(rb_ja))
  427.  
  428. ##
  429. ## it begins
  430. ##
  431.  
  432. pid, mod = getmodule('rabiribi.exe', 'rabiribi.exe')
  433. proc = OpenProcess(PROCESS_ALL_ACCESS, False, pid)
  434.  
  435. buflen = 512
  436. numread = c_ulong(0)
  437. buf = (c_ubyte * buflen)()
  438. #ptr = cast(addressof(buf), POINTER(c_ubyte))
  439.  
  440. def read_mem(adr, len):
  441.     global proc, buf, numread
  442.     if not ReadProcessMemory(proc, adr, buf, len, byref(numread)):
  443.         print('read err')
  444.         exit(1)
  445.  
  446. def read_int(adr):
  447.     global buf
  448.     #print('reading int32 @ {0:x}'.format(adr))
  449.     read_mem(adr, 4)
  450.     return struct.unpack('<L', string_at(addressof(buf), 4))[0]
  451.  
  452. if ADR_STRING_TAB:
  453.     adr = read_int(mod + ADR_STRING_TAB)
  454.     read_mem(adr, buflen)
  455.     first_line = string_at(buf)
  456.     remaining_lines = string_at(addressof(buf)+len(first_line)+1)
  457.  
  458.     # repair nullbytes (they used to be newlines,
  459.     # game engine swaps out with \x00 for display)
  460.     for x in range(numread.value):
  461.         if buf[x] == 0:
  462.             buf[x] = 0xd
  463.     str = string_at(buf)
  464.     # trim at end of dialogue block
  465.     ofs = str.find(b'\r\n\r\n')
  466.     if ofs > 0:
  467.         str = str[:ofs]
  468.  
  469.     #unfuck(str[:8], 'ったく')
  470.     str = str.decode('cp932', 'replace')
  471.     printc(8+5, 'memory dump of current scene table:')
  472.     print(str)
  473.  
  474. last_block_id = None
  475. last_block_pos = None
  476. while True:
  477.     block_id = read_int(read_int(mod + ADR_BLOCK_ID))
  478.     block_pos = read_int(read_int(mod + ADR_BLOCK_POS))
  479.     if block_id == last_block_id and block_pos == last_block_pos:
  480.         time.sleep(0.5)
  481.         continue
  482.     last_block_id = block_id
  483.     last_block_pos = block_pos
  484.     printc(8+3, 'scene {0} line {1}'.format(
  485.         block_id, block_pos))
  486.     print()
  487.    
  488.     html_text = ''
  489.     if block_id > 0:
  490.         new_text = ''
  491.         en = next(x for x in rb_en if x[0] == block_id)[1][block_pos].strip()
  492.         ja = next(x for x in rb_ja if x[0] == block_id)[1][block_pos].strip()
  493.        
  494.         printc(8+3, 'ka: ', end='')
  495.         lines = ja.replace('@', '\n    ')
  496.         print(lines.strip(), '\n')
  497.        
  498.         printc(8+3, 'hi: ', end='')
  499.         lines = ja.replace('@', '\n')
  500.         kana, html = mecab(lines)
  501.         html_text += '<div id="ja">{0}</div>\n'.format(html.strip())
  502.         print(kana.strip().replace('\n', '\n    '), '\n')
  503.        
  504.         printc(8+3, 'en: ', end='')
  505.         lines = en.replace('@', '\n    ')
  506.         html_text += lines.strip()
  507.         print(lines.strip(), '\n')
  508.    
  509.     current_text = new_text
  510.     httpd.set(html_text)
  511.  
  512. CloseHandle(proc)
Add Comment
Please, Sign In to add comment