makemessage.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. from __future__ import with_statement
  4. import argparse
  5. import os
  6. import traceback
  7. import struct
  8. def main():
  9. parser = argparse.ArgumentParser(description = 'Generate a translatable language file that can be used by SDLPAL.')
  10. parser.add_argument('gamepath', help = 'Game path where SSS.MKF & M.MSG & WORD.DAT are located.')
  11. parser.add_argument('outputfile', help = 'Path of the output message file.')
  12. parser.add_argument('encoding', choices = ['gbk', 'big5'], help = 'Text encoding name, should be either gbk or big5.')
  13. parser.add_argument('-w', '--width', dest = 'wordwidth', default = 10, type = int, help = 'Word width in bytes, default is 10')
  14. parser.add_argument("-c", "--comment", action = 'store_true', help = 'Automatically generate comments')
  15. options = parser.parse_args()
  16. if options.gamepath[-1] != '/' and options.gamepath[-1] != '\\':
  17. options.gamepath += '/'
  18. script_bytes = []
  19. index_bytes = []
  20. msg_bytes = []
  21. word_bytes = []
  22. is_msg_group = 0 #是否正在处理文字组的标示。
  23. msg_count = 0
  24. last_index = -1
  25. temp = ""
  26. comment = ""
  27. message = ""
  28. for file_ in os.listdir(options.gamepath):
  29. if file_.lower() == 'sss.mkf':
  30. try:
  31. with open(options.gamepath + file_, 'rb') as f:
  32. f.seek(12, os.SEEK_SET)
  33. offset_begin, script_begin, file_end = struct.unpack('<III', f.read(12))
  34. f.seek(offset_begin, os.SEEK_SET)
  35. index_bytes = f.read(script_begin - offset_begin)
  36. script_bytes = f.read(file_end - script_begin)
  37. except:
  38. traceback.print_exc()
  39. return
  40. elif file_.lower() == 'm.msg':
  41. try:
  42. with open(options.gamepath + file_, 'rb') as f:
  43. msg_bytes = f.read()
  44. except:
  45. traceback.print_exc()
  46. return
  47. elif file_.lower() == 'word.dat':
  48. try:
  49. with open(options.gamepath + file_, 'rb') as f:
  50. data_bytes=f.read()
  51. except:
  52. traceback.print_exc()
  53. return
  54. if len(data_bytes) % options.wordwidth != 0:
  55. data_bytes += [0x20 for i in range(0, options.wordwidth - len(data_bytes) % options.wordwidth)]
  56. output = "# All lines, except those inside [BEIGN MESSAGE] and [END MESSAGE], can be commented by adding the sharp '#' mark at the first of the line.\n\n"
  57. output += "# This section contains the information that will be displayed when a user finishes the game.\n"
  58. output += "# Only the keys listed here are valid. Other keys will be ignored.\n"
  59. output += "[BEGIN CREDITS]\n"
  60. output += "# Place the translated text of 'Classical special build' here in no more than 24 half-wide characters.\n"
  61. output += "1= Classical special build\n"
  62. output += "# Place the translated porting information template at the following two lines. Be aware that each replaced line will be truncated into at most 40 half-wide characters.\n"
  63. output += "6= ${platform} port by ${author}, ${year}.\n"
  64. output += "7=\n"
  65. output += "# Place the translated GNU licensing information at the following three lines. Be aware that each line will be truncated into at most 40 half-wide characters.\n"
  66. output += "8= This is a free software and it is\n"
  67. output += "9= published under GNU General Public\n"
  68. output += "10= License v3.\n"
  69. output += "# Place the translated text at the following line. Be aware that each line will be truncated into at most 40 half-wide characters.\n"
  70. output += "11= ...Press Enter to continue\n"
  71. output += "[END CREDITS]\n\n"
  72. output += "# Each line controls one position value.\n"
  73. output += "# For each line, the format is 'idx=x,y,flag', while the flag is optional\n"
  74. output += "# If bit 0 of flag is 1, then use 8x8 font; and while bit 1 of flag is 1, then disable shadow\n"
  75. output += "# Lines from 1 to 26 are for equipping screen, lines from 27 to 80 are for status screen.\n"
  76. output += "[BEGIN LAYOUT]\n"
  77. output += "# 1 is the position of image box in equipping screen\n"
  78. output += "1=8,8\n"
  79. output += "# 2 is the position of role list box in equipping screen\n"
  80. output += "2=2,95\n"
  81. output += "# 3 is the position of current equipment's name in equipping screen\n"
  82. output += "3=5,70\n"
  83. output += "# 4 is the position of current equipment's amount in equipping screen\n"
  84. output += "4=51,57\n"
  85. output += "# 5 .. 10 are the positions of words 600 ... 605 in equipping screen\n"
  86. output += "5=92,11\n"
  87. output += "6=92,33\n"
  88. output += "7=92,55\n"
  89. output += "8=92,77\n"
  90. output += "9=92,99\n"
  91. output += "10=92,121\n"
  92. output += "# 11 .. 16 are the positions of equipped equipments in equipping screen\n"
  93. output += "11=130,11\n"
  94. output += "12=130,33\n"
  95. output += "13=130,55\n"
  96. output += "14=130,77\n"
  97. output += "15=130,99\n"
  98. output += "16=130,121\n"
  99. output += "# 17 .. 21 are the positions of words 51 ... 55 in equipping screen\n"
  100. output += "17=226,10\n"
  101. output += "18=226,32\n"
  102. output += "19=226,54\n"
  103. output += "20=226,76\n"
  104. output += "21=226,98\n"
  105. output += "# 22 .. 26 are the positions of status values in equipping screen\n"
  106. output += "22=260,14\n"
  107. output += "23=260,36\n"
  108. output += "24=260,58\n"
  109. output += "25=260,80\n"
  110. output += "26=260,102\n"
  111. output += "# 27 is the position of role name in status screen\n"
  112. output += "27=110,8\n"
  113. output += "# 28 is the position of role image in status screen\n"
  114. output += "28=110,30\n"
  115. output += "# 29 is the position of EXP label in status screen\n"
  116. output += "29=6,6\n"
  117. output += "# 30 is the position of LEVEL label in status screen\n"
  118. output += "30=6,32\n"
  119. output += "# 31 is the position of HP label in status screen\n"
  120. output += "31=6,54\n"
  121. output += "# 32 is the position of MP label in status screen\n"
  122. output += "32=6,76\n"
  123. output += "# 33 .. 37 are the positions of words 51 ... 55 in equipping screen\n"
  124. output += "33=6,98\n"
  125. output += "34=6,118\n"
  126. output += "35=6,138\n"
  127. output += "36=6,158\n"
  128. output += "37=6,178\n"
  129. output += "# 38 is the position of current EXP in status screen\n"
  130. output += "38=58,6\n"
  131. output += "# 39 is the position of required EXP to level up in status screen\n"
  132. output += "39=58,15\n"
  133. output += "# 40 is the position of slash between EXPs in status screen, if set to (0,0), then do not show the slash\n"
  134. output += "40=0,0\n"
  135. output += "# 41 is the position of LEVEL in status screen\n"
  136. output += "41=54,35\n"
  137. output += "# 42 is the position of current HP in status screen\n"
  138. output += "42=42,56\n"
  139. output += "# 43 is the position of max HP in status screen\n"
  140. output += "43=63,61\n"
  141. output += "# 44 is the position of slash between cur & max HPs in status screen, if set to (0,0), then do not show the slash\n"
  142. output += "44=65,58\n"
  143. output += "# 45 is the position of current MP in status screen\n"
  144. output += "45=42,78\n"
  145. output += "# 46 is the position of max MP in status screen\n"
  146. output += "46=63,83\n"
  147. output += "# 47 is the position of slash between cur & max MPs in status screen, if set to (0,0), then do not show the slash\n"
  148. output += "47=65,80\n"
  149. output += "# 48 .. 52 are the positions of status values in status screen\n"
  150. output += "48=42,102\n"
  151. output += "49=42,122\n"
  152. output += "50=42,142\n"
  153. output += "51=42,162\n"
  154. output += "52=42,182\n"
  155. output += "# 53 .. 58 are the positions of image boxes for equipped equipments in status screen\n"
  156. output += "53=189,-1\n"
  157. output += "54=247,39\n"
  158. output += "55=251,101\n"
  159. output += "56=201,133\n"
  160. output += "57=141,141\n"
  161. output += "58=81,125\n"
  162. output += "# 59 .. 64 are the positions of names for equipped equipments in status screen\n"
  163. output += "59=195,38\n"
  164. output += "60=253,78\n"
  165. output += "61=257,140\n"
  166. output += "62=207,172\n"
  167. output += "63=147,180\n"
  168. output += "64=87,164\n"
  169. output += "# 65 .. 80 are the positions of poison names in status screen, note that currently there are no more than 8 poisons can be simulatenously displayed\n"
  170. output += "65=185,58\n"
  171. output += "66=185,76\n"
  172. output += "67=185,94\n"
  173. output += "68=185,112\n"
  174. output += "69=185,130\n"
  175. output += "70=185,148\n"
  176. output += "71=185,166\n"
  177. output += "72=185,184\n"
  178. output += "73=185,184\n"
  179. output += "74=185,184\n"
  180. output += "75=185,184\n"
  181. output += "76=185,184\n"
  182. output += "77=185,184\n"
  183. output += "78=185,184\n"
  184. output += "79=185,184\n"
  185. output += "80=185,184\n"
  186. output += "# 81 .. 82 are extra description lines in the item (81) & magic (82) menu, where the first value specifies the lines and the second value should be zero\n"
  187. output += "81=2,0\n"
  188. output += "82=1,0\n"
  189. output += "[END LAYOUT]\n\n"
  190. output += "# This section contains the words used by the game.\n"
  191. output += "[BEGIN WORDS]\n"
  192. output += "# Each line is a pattern of 'key=value', where key is an integer and value is a string.\n"
  193. for i in range(0, len(data_bytes) / options.wordwidth):
  194. temp = data_bytes[i * options.wordwidth: (i + 1) * options.wordwidth].rstrip('\x20\x00').decode(options.encoding).encode('utf-8')
  195. if options.comment: output += "# Original word: %d=%s\n" % (i, temp)
  196. output += "%d=%s\n" % (i, temp)
  197. output += "600=Headgear\n"
  198. output += "601=Body Gear\n"
  199. output += "602=Clothing\n"
  200. output += "603=Weapon\n"
  201. output += "604=Footwear\n"
  202. output += "605=Accessory\n"
  203. output += "# The following six words are for ATB only. It is not used in classical mode.\n"
  204. output += "606=Battle Speed\n"
  205. output += "# The following five words are for ATB battle speed. It is not used in classical mode.\n"
  206. output += "607=1\n"
  207. output += "608=2\n"
  208. output += "609=3\n"
  209. output += "610=4\n"
  210. output += "611=5\n"
  211. output += "# The following word is used to ask user whether to launch setting interface on next game start.\n"
  212. output += "612=Setting\n"
  213. output += "[END WORDS]\n\n"
  214. output += "# The following sections contain dialog/description texts used by the game.\n\n"
  215. print "Now Processing. Please wait..."
  216. for i in range(0, len(script_bytes) / 8):
  217. op, w1, w2, w3 = struct.unpack('<HHHH', script_bytes[i * 8 : (i + 1) * 8])
  218. if op == 0xFFFF:
  219. if is_msg_group == 1 and last_index + 1 != w1:
  220. is_msg_group = 0
  221. temp = "%s %d\n\n" % ('[END MESSAGE]', last_index)
  222. message += temp
  223. output += comment + message
  224. if is_msg_group == 0:
  225. is_msg_group = 1
  226. message = "%s %d\n" % ('[BEGIN MESSAGE]', w1)
  227. if options.comment: comment = "# Original message: %d\n" % w1
  228. last_index = w1
  229. msg_count += 1
  230. msg_begin, msg_end = struct.unpack("<II",index_bytes[w1 * 4 : (w1 + 2) * 4])
  231. try:
  232. temp = "%s\n" % (msg_bytes[msg_begin : msg_end].decode(options.encoding, 'replace').encode('utf-8'))
  233. message += temp
  234. if options.comment: comment += "# " + temp
  235. except:
  236. traceback.print_exc()
  237. elif op == 0x008E:
  238. if is_msg_group == 1:
  239. temp = "%s\n" % ('[CLEAR MESSAGE]')
  240. message += temp
  241. if options.comment: comment += "# " + temp
  242. else:
  243. if is_msg_group == 1:
  244. is_msg_group = 0
  245. temp = "%s %d\n\n" % ('[END MESSAGE]', last_index)
  246. message += temp
  247. output += comment + message
  248. try:
  249. with open(options.outputfile, "wt") as f:
  250. f.write(output)
  251. except:
  252. traceback.print_exc()
  253. print "OK! Extraction finished!"
  254. print "Original Dialog script count: " + str(msg_count)
  255. if __name__ == '__main__':
  256. main()