bokpass.tcl 91 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306
  1. #!/bin/sh
  2. #\
  3. for x in 8.7 8.6 8.5 ""; do \
  4. command -v "tclsh$x" >/dev/null && exec "tclsh$x" "$0" "$@"; \
  5. command -v "wish$x" >/dev/null && exec "wish$x" "$0" -- "$@"; \
  6. done
  7. # Permission to use, copy, modify, and/or distribute this software for
  8. # any purpose with or without fee is hereby granted.
  9. #
  10. # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
  11. # WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
  12. # OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
  13. # FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
  14. # DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
  15. # AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
  16. # OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  17. # NAME
  18. # bokpass - decode/encode passwords from/for boktai games
  19. #
  20. # SYNOPSIS
  21. # bokpass [-gui] [-kbd] [-invalid] [-columns columns] [-fontsize size]
  22. # bokpass [-cli] [-verbose] -game game password ...
  23. #
  24. # DESCRIPTION
  25. # bokpass is a tool to decode and encode passwords from/for the Boktai
  26. # series of games, including Lunar Knights.
  27. #
  28. # OPTIONS
  29. # -gui Open the GUI, this is the default when there are no
  30. # arguments provided.
  31. #
  32. # -columns columns
  33. # Arrange the gui in specified number of columns,
  34. # the default is 2.
  35. #
  36. # -fontsize size
  37. # Set the default font size to `size', the password entry and
  38. # virtual keyboard font sizes are an additional 20% bigger.
  39. # The default is 10.
  40. #
  41. # -cli Decode the provided passwords, this is the default when
  42. # arguments are provided.
  43. #
  44. # -verbose
  45. # If a value has a single string representation, print that
  46. # next to its numerical representation.
  47. #
  48. # -invalid
  49. # Do not validate the data fields.
  50. #
  51. # -game game
  52. # The game that the provided password(s) are for, `game' can
  53. # be a number (1-4), the title of the game, or the short-name
  54. # for the game (zoktai/shinbok).
  55. # This option is required for decoding passwords.
  56. #
  57. # GUI
  58. # The tabs at the top select the game.
  59. #
  60. # Under that is the encode button "↱", the password text box, the
  61. # cycle button "↻", and the decode button "↴".
  62. #
  63. # The cycle button "↻" edits the password to change the `offset'
  64. # field and change how it would be decoded. For if one of the first
  65. # letters was a typo. Though note that the `offset' is not included in
  66. # Boktai 1's checksum.
  67. #
  68. # Then columns (changed by the `-columns' option) of labeled dropdown
  69. # menus, number-boxes, checkboxes, and text fields that correspond to
  70. # the various fields of the selected game's password.
  71. #
  72. # Then there is an error line, usually containing "✓", that will show
  73. # an error preceded by "⚠" if there was an encoding error, a decoding
  74. # error, or if the password may be considered invalid. For example,
  75. # Boktai 2 (US) will not accept Boktai 1 passwords that have 0 playtime
  76. # or clears, and will not accept Japanese characters in the player's
  77. # name.
  78. #
  79. # If the `-kbd' flag is given then at the bottom there are tabs
  80. # containing grids of buttons that function as a virtual keyboard.
  81. # When clicking a text-entry field it will automatically switch to
  82. # the appropriate keyboard tab, and lock it out of any invalid tabs.
  83. #
  84. # BOKTAI PASSWORD
  85. # JP EN
  86. # u3 u3 region
  87. # u16 u16 checksum
  88. # u2 u2 offset [0x0203C800, ([0x03004620]+1 & 0x3FF)*2] & 3
  89. # u9 u9 sol/4 [0x0203D8BC] >> 12
  90. # u6 u7 timezone [0x0203D82C]
  91. # u6 u6 hours [0x0203D8FC] / 60 / 60 / 60
  92. # u6 u6 minutes [0x0203D8FC] / 60 / 60 % 60
  93. # u3 u3 difficulty [0x0203D838]
  94. # u5 u5 dungeons [0x0203D8A6]h
  95. # u3 u3 clears [0x0203D8B6]h
  96. # u4 u4 continues [0x0203D902]h
  97. # u5 u5 caught [0x0203DB0C]h
  98. # u7 u7 kills [0x0203D8B0]h
  99. # u4 u4 rank [0x0203D8C6]h
  100. # u5 u5 title [0x0203D828]h
  101. # u8[5] u8[9] name [0x0203D830]...
  102. # u6 u6 link-battles [0x0203DD2E]
  103. # u6 u6 link-trades [0x0203DD32]
  104. # u8 u8 loan [0x0203D878]
  105. # u0 u15 padding 0
  106. #
  107. # The checksum offset is 24-bits and uses the constant 0x1021.
  108. # The encoding offset is 24-bits and uses the initializer 0x8C159.
  109. #
  110. # BOKTAI 2 PASSWORD
  111. # JP EN
  112. # u16 u16 checksum
  113. # u2 u2 padding 0
  114. # u3 u3 region
  115. # u3 u3 offset [0x0203B400, ([0x030046B8]+1 & 0x3FF)*2] & 7
  116. # u6 u7 timezone [[0x030047A8], 0x20]
  117. # u3 u3 side [[0x030046A0], 0x2F8]h
  118. # u4 u4 style [[0x030046A0], 0x2FA]h
  119. # s8 s8 kills/8 [[0x030046A0], 0x1F0]h / 8
  120. # u7 u7 forge [[0x030046A0], 0x2FC]h
  121. # u6 u6 link-battles [[0x030046A0], 0x914]h
  122. # u6 u6 link-shops [[0x030046A0], 0x918]h + [[0x030047A8], 0x91A]h
  123. # s10 s10 sol/4096 [[0x030046A0], 0x1D8] / 4096
  124. # s8 s8 loan/256 [[0x030046A0], 0x1D4] / 256
  125. # u6 u6 hours [[0x030046A0], 0x2B4] / 60 / 60 / 60
  126. # u16 u16 titles [[0x030046A0], 0x1EE]h
  127. # u8[5] u8[9] name [[0x030046A0], 0x3C0]b...
  128. # u0 u15 padding 0
  129. # [0x030046A0] is heap allocated, typically 0x0203C400.
  130. #
  131. # The checksum offset is 18-bits and uses the constant 0x1021.
  132. # The encoding offset is 24-bits and uses the initializer 0xB8E6E.
  133. #
  134. # BOKTAI 3 PASSWORD
  135. # u16 checksum
  136. # u2 padding 0
  137. # u3 region
  138. # u3 offset [0x203B400, ([0x03005308] + 1 & 0x3FF) * 2] / 256 & 7
  139. # u6 timezone [[0x30053F8], 0x18]
  140. # u8 kills/8 [[0x2000710], 0x538]h / 8
  141. # u7 forges [[0x2000710], 0x664]h
  142. # u9 races [[0x2000710], 0x75A]h
  143. # u7 link-races [[0x2000710], 0x7B4]h
  144. # u1 cross-linked [[0x2000710], 0x75F]b
  145. # u6 endings [[0x2000710], 0x75E]b
  146. # u10 sol/4096 [[0x2000710], 0x520]
  147. # u8 loan/256 [[0x2000710], 0x51C]
  148. # u6 hours [[0x2000710], 0x614] / 60 / 60 / 60
  149. # u12 titles [[0x2000710], 0x536]h
  150. # u8[5] name [[0x2000710], 0x781]b...
  151. # u0 padding 0
  152. # [0x02000710] is heap allocated, typically 0x0203C400.
  153. #
  154. # The checksum offset is 18-bits and uses the constant 0x8005.
  155. # The encoding offset is 24-bits and uses the initializer 0x8C159.
  156. #
  157. # CAVEATS
  158. # I was unable to find a Shinbok password with what I labeled as the
  159. # "Cross-Linked" flag set. I labeled it that way because it seems to
  160. # only be modified in bytecode function 1657 at file offset 0xDF2D81
  161. # which references a string 9797 that translates as a wireless-adapter
  162. # communication error, and it seems as though the wireless-adapter is
  163. # only used for the crossover battles.
  164. #
  165. # DISASSEMBLY NOTES
  166. # Password generation functions:
  167. # Boktai 1 JP put 1: 0x12D2E4
  168. # Boktai 1 NA put 1: 0x12E7D4
  169. # Boktai 1 EU put 1: 0x12B244
  170. # Boktai 2 JP put 2: 0x12DC0
  171. # Boktai 2 NA put 2: 0x12C14
  172. # Boktai 2 EU put 2: 0x12ABC
  173. # Boktai 3 JP put 3: 0x44E6C
  174. # Boktai 3 JP put 2: 0x451BC
  175. # Password reading functions:
  176. # Boktai 2 JP get 1: 0x112B4
  177. # Boktai 2 NA get 1: 0x111E0
  178. # Boktai 2 EU get 1: 0x10FC8
  179. # Boktai 3 JP get 3: 0x450D8
  180. # Boktai 3 JP get 2: 0x453DC
  181. #
  182. # Boktai 1 uses a static random number table of 1024 numbers at 0x203C800;
  183. # the index stored at 0x3004620 with an inital value of 837. The index
  184. # is only incremented when used (particle effects and enemies, mostly),
  185. # and is not stored in the save file.
  186. #
  187. # This means, for example, you can just choose what emblem you want at the
  188. # Azure Sky Tower. Entering the outside screen while there is still the
  189. # ice/fire increments the RNG by 1, entering Azure Sky Tower itself
  190. # increments it by two, and the steam/smoke increments it by 85.
  191. # So before collecting an emblem repeatedly running between the outside
  192. # of the tower and the 'Byroad of the Beasts' will increment the RNG by 1,
  193. # and repeatedly exiting/entering the tower will increment the RNG by 3
  194. # each cycle. The following table tells you what actions after loading a
  195. # file saved on the 'Byroad of the Beasts' will get which emblem:
  196. # RNG Index Byroad in/outs AZT in/outs Emblem
  197. # 925 0 0 Earth
  198. # 928 0 1 Cloud
  199. # 930 2 1 Frost
  200. # 931 0 2 Cloud
  201. # 932 1 2 Frost
  202. # 934 0 3 Cloud
  203. # 937 0 4 Frost
  204. # 939 2 4 Flame
  205. # 940 0 5 Earth
  206. # 943 0 6 Earth
  207. # 946 0 7 Flame
  208. #
  209. # Boktai 1's titles are checked in bytecode function 37607 in this order:
  210. # 0 Trigger of Sol S Rank and all 42 parts
  211. # 1 Gun Master Level 3 lenses and all 42 parts
  212. # 6 Death 100 or more Continues
  213. # 5 Berserker Caught 400 or more times
  214. # 3 Bishop 500 or more Kills
  215. # 4 Queen 100 or less Kills
  216. # 2 Gladiator A win rate of 70% or more with at least 50 Link Battles
  217. # 10 Running Boy Punished 8 or more times
  218. # 9 Solar Merchant 50 or more Link Trades
  219. # 7 Solar Boy 600 or more Sol
  220. # 8 Dark Boy 100 or less Sol
  221. # 11 King S Rank
  222. # 12 Rook A-/A/A+ Rank
  223. # 13 Knight B-/B/B+ Rank
  224. # 14 Pawn C-/C/C+ Rank
  225. # 14 Pawn Default
  226. #
  227. # Boktai 2's "side" is calculated as follows:
  228. # 128*r/max(r+b, 1) > 70 ? 0 : 128*r/max(r+b, 1) > 58 ? 2 : 1
  229. # Where `r' is the number of frames of "red" Django action and `b' is
  230. # the number of frames of "black" Django action.
  231. #
  232. # Boktai 2's "style" is based on which weapon had the highest number of
  233. # frames in use rounded down to the nearest 15 minutes, or "No Style"
  234. # if there was a tie.
  235. #
  236. # Boktai 3's endings should be:
  237. # 1 Otenko lives.
  238. # 2 Both Otenko and Sabata live.
  239. # 4 Neither Otenko or Sabata live.
  240. # 8 Sabata lives.
  241. #
  242. # The checksums are CRC-16s initialized to 0xFFFF and with the result
  243. # inverted. The polynomial 0x1021 is used in the first two games, 0x8005
  244. # is used in the third, and 0x180D is used in the fourth game.
  245. #
  246. # The encoding algorithm is to initialize a value depending on the game,
  247. # and initialize the an accumulator variable to 0xFFFF. For each iteration
  248. # the current value by 0x6262C05D and increment it by 1, then xor the result
  249. # into the xor accumulator. This iteration in run a number of times
  250. # depending on the offset and 4 additional times, then before each iteration
  251. # starting at offset 4 until the end of the list the last 6-bits of the xor
  252. # accumulator are xored with the current 6-bit value.
  253. # Because of this truncation only the last 6 bits of any constant matters.
  254. # The initialization constants are 0x8C159 for games 1 and 3, 0xB8E6E for
  255. # game 2, and 0x5BB15 for game 4.
  256. #
  257. # BYTECODE NOTES
  258. # In Boktai 3 most things about the bytecode are the same as described
  259. # in github.com/Prof9/SolDec, though there are more 16-bit (control)
  260. # operators. The Boktai 3 function that executes them is at 0x21AC6C.
  261. #
  262. # The following are tables some useful addresses and where they are
  263. # found in RAM.
  264. #
  265. # note: Addresses starting with 0x08 indicate a location in the ROM.
  266. # RAM-Address Func-Table Func-Count BC-Offset BC-Start
  267. # JP1 0x03000648 0x08E1D738 0x13F6 0x08E52328 0x08EE9F17
  268. # NA1 0x03000648 0x08E0DA88 0x140A 0x08E41D4C 0x08EDA36F
  269. # EU1 0x03000650 0x08DE2FBC 0x140F 0x08EC1350 0x08F5D001
  270. # JP2 0x03000748 0x08CBF1A0 0x2D13 0x08D1337C 0x08DA9DB4
  271. # NA2 0x03000748 0x08C8B56C 0x390A 0x08CE4B9C 0x08D7BBF5
  272. # EU2 0x03000748 0x08CA1E8C 0x3F48 0x08E2F0EC 0x08ECC13C
  273. # JP3 0x02000438 0x08D5A860 0x4260 0x08DD6BA0 0x08E5F048
  274. #
  275. # In Boktai 1 the Func-Table is indexed like:
  276. # for (i = 0; i < Func-Count; i++)
  277. # if (Func-Table[i*2] == index)
  278. # return BC-Offset + (Func-Table[i*2+1] & 0x00FFFFFF)
  279. #
  280. # In others the Func-Table is indexed like:
  281. # BC-Offset + (Func-Table[(index & 0x7FFFFFFF) - 1] & 0x00FFFFFF)
  282. #
  283. # RAM-Address ??? String-Table String-Data
  284. # JP1 0x03000658 0x08E276F0 0x08E27700 0x08E2B72C
  285. # NA1 0x03000658 0x08E17AE0 0x08E17AF0 0x08E1BE1C
  286. # EU1 0x03000660 0x08DED03C 0x08DED04C 0x08E00E64
  287. # JP2 0x03000758 0x08CCA5F0 0x08CCA600 0x08CD1594
  288. # NA2 0x03000758 0x08C99998 0x08C999A8 0x08CA0A80
  289. # EU2 0x03000758 0x08CB1BB0 0x08CB1BC0 0x08CD3A58
  290. # JP3 0x02000448 0x08D6B1E4 0x08D6B1F8 0x08D74DDC
  291. #
  292. # The String-Table is indexed like:
  293. # String-Data + (String-Table[index] & 0x7FFFFFFF)
  294. # The strings are nul-terminated.
  295. #
  296. # The current floor of the Azure Sky Tower is stored at memory location
  297. # 0x203E8DD, and the number of clears at 0x203E8E0. So building it up
  298. # can be skipped by changing 0x203E8E0 to 29+, and most of the climb up
  299. # can be skipped by changing 0x203E8DD to 99 after entering floor 1.
  300. #
  301. # Boktai 1 -> 2 password effects (byte-code function JP #25 and NA/EU #28):
  302. # Trigger of Sol Start with Stats +1, a Healer, and a Magic Potion
  303. # Gun Master Start with Stats +1, a Healer, and a Magic Potion
  304. # Gladiator Start with Stats +1, and 2 Healers
  305. # Bishop Start with Vitl +2, Strg +2, an Antidote, and an Elixer
  306. # Queen Start with Sprt +2, Agil +2, a Speed, and Tiptoe Nut
  307. # Berserker Start with Strg +2, a Tasty, and Rotten Meat
  308. # Death Start with Vitl +2, a Solar, and Rotten Nut
  309. # Solar Boy Start with Vitl +2, Sprt +2, a Solar Nut, and a Drop of Sun
  310. # Dark Boy Start with Strg +2, Agil +2, a Power Nut, and a Bearnut
  311. # Solar Merchant Start with Sprt +2, Strg +2, a Redshroom, and a Blueshroom
  312. # Running Boy Start with Vitl +2, Agil +2, and 2 Magic Potions
  313. # King Start with Stats +1, an Elixer, and a Healer
  314. # Rook Start with Vitl +1, Sprt +1, a Healer, and an Antidote
  315. # Knight Start with Strg +1, Agil +1, an Earthly, and Solar Nut
  316. # Pawn Start with an Earthly and Solar Nut
  317. # Invalid Start with 2 Earthly Nuts
  318. #
  319. # Boktai 2 -> 3 password effects (byte-code function #546):
  320. # Sword Master Starting Strength + 1
  321. # Spear Master Starting Spirit + 1
  322. # Hammer Master Starting Vitality + 1
  323. # Fist Master Starting Strength + 1
  324. # Gun Master Starting Spirit + 1
  325. # Adept Starting Vitality + 1
  326. # Day Walker Start with a Sun card
  327. # Adventurer Start with a Speed Nut
  328. # Agent Start with a Tiptoe Nut
  329. # Collector Start with a See-All Nut
  330. # Dark Hunter Start with a Tasty Meat
  331. # Grand Master Start with a Moon card
  332. # Red (Side) Solar Trance time + 5000 frames
  333. # Black (Side) Dark Trance time + 5000 frames
  334. # No Password Start with 2 extra Earthly Nuts
  335. #
  336. # ACKNOWLEDGMENTS
  337. # Most of the Lunar Knight / Boktai DS information, and the character
  338. # tables were copied from github.com/Prof9/LKPassDecode.
  339. #
  340. # github.com/Prof9/SolDec was very useful in understanding bytecode.
  341. #
  342. # The Boktai 3 translations and Japanese timezone names are from
  343. # github.com/moozilla/boktai3trans.
  344. # lmap shim for Tcl 8.5
  345. if {[info commands lmap] == ""} {
  346. proc lmap {args} {
  347. if {[llength $args] < 3 || [llength $args] % 2 != 1} {
  348. return -code error \
  349. {wrong # args: should be "lmap vars list ?vars list ...? body"}
  350. }
  351. set i 0
  352. for {set i 0} {$i < [llength $args] - 1} {incr i 2} {
  353. for {set j 0; set l {}} {$j < [llength [lindex $args $i]]} {incr j} {
  354. upvar [lindex $args $i $j] var$i,$j
  355. lappend l var$i,$j
  356. }
  357. lset args $i $l
  358. }
  359. set r {}
  360. foreach {*}[lrange $args 0 end-1] {
  361. lappend r [uplevel 1 [lindex $args end]]
  362. }
  363. return $r
  364. }
  365. }
  366. # dict map shim for Tcl 8.5
  367. if {[namespace which -command ::tcl::dict::map] == ""} {
  368. proc ::tcl::dict::map {vars dict body} {
  369. if {[llength $vars] != 2} {
  370. return -code error {must have exactly two variable names}
  371. }
  372. upvar 1 [lindex $vars 0] key [lindex $vars 1] value
  373. ::set r {}
  374. dict for {key value} $dict {
  375. ::set value [uplevel 1 $body]
  376. set r $key $value
  377. }
  378. return $r
  379. }
  380. namespace ensemble configure ::dict -map [dict replace \
  381. [namespace ensemble configure ::dict -map] map ::tcl::dict::map]
  382. }
  383. namespace eval bok {namespace export *}
  384. set bok::strings {en {
  385. games {
  386. jp1 {Bokura no Taiyō}
  387. jp2 {Zoku Bokura no Taiyō: Taiyō Shōnen Jango}
  388. jp3 {Shin Bokura no Taiyō: Gyakushū no Sabata}
  389. jp4 {Bokura no Taiyō: Django & Sabata}
  390. en1 {Boktai: The Sun Is in Your Hand}
  391. en2 {Boktai 2: Solar Boy Django}
  392. en3 {Boktai 3: Sabata's Counterattack}
  393. en4 {Lunar Knights}
  394. }
  395. labels {
  396. game-tabs {
  397. 1 Boktai 2 {Boktai 2 / Zoktai}
  398. 3 {Boktai 3 / Shinbok} 4 {Lunar Knights / Boktai DS}
  399. }
  400. keyboard-tabs {
  401. en64 Base64 lk64 {Base64 LK} jp64 {Base64 JP}
  402. en English ext {Latin Ext.}
  403. hiri Hirigana kata Katakana
  404. }
  405. padding Padding header-padding Padding
  406. region Region region-name Region
  407. checksum Checksum offset Offset
  408. sol Sol sol/4 Sol sol/4096 Sol
  409. loan Loan loan/256 Loan
  410. timezone Timezone timezone-name Timezone
  411. hours Hours minutes Minutes
  412. difficulty Difficulty difficulty-name Difficulty
  413. dungeons Dungeons clears Clears
  414. continues Continues caught Caught
  415. kills Kills kills/8 Kills
  416. rank Rank rank-name Rank
  417. forges Forges races Races
  418. link-battles Link-Battles link-trades Link-Trades
  419. link-shopping Link-Shopping
  420. link-races Link-Races cross-linked Cross-Linked
  421. title Title title-name Title
  422. titles Titles title-list Titles
  423. side Side side-name Side
  424. style Style style-name Style
  425. sword Sword sword-name Sword
  426. gun Gun gun-name Gun
  427. terrennial Terrennial terrennial-name Terrennial
  428. climate Climate climate-name Climate
  429. endings Endings ending-list Survivors
  430. name-dark Name(dark) name-solar Name(sol) name Name
  431. }
  432. regions {{Unknown Region #0} Japan {North America} Europe}
  433. timezones {jp {
  434. {Unknown Timezone #0}
  435. Ibaraki Tochigi Gunma Saitama
  436. Chiba Tokyo Kanagawa Ogasawara
  437. Niigata Toyama Ishikawa Fukui
  438. Yamanashi Nagano Gifu Shizuoka
  439. Aichi Mie Shiga Kyoto
  440. Osaka Hyougo Nara Wakayama
  441. Tottori Shimane Okayama Hiroshima
  442. Yamaguchi Tokushima Kagawa Ehime
  443. Kouchi Fukuoka Saga Nagasaki
  444. Kumamoto Ooita Miyazaki Kagoshima
  445. Okinawa Ishigakijima Sapporo Hakodate
  446. Asahikawa Kushiro Obihiro Aomori
  447. Iwate Miyagi Akita Yamagata
  448. Fukushima
  449. } na {
  450. {Unknown Timezone #0}
  451. {St. John's} {Labrador City} Halifax Quebec
  452. Montreal Ottawa Toronto Timmins
  453. Boston Albany Syracuse {New York}
  454. Philadelphia Pittsburgh {Washington D.C.} Norfolk
  455. Raleigh Charlotte Atlanta Jacksonville
  456. Tampa Miami Detroit Cleveland
  457. Columbus Lexington {Thunder Bay} Winnipeg
  458. Regina Thompson Indianapolis Chicago
  459. Milwaukee Minneapolis Bismark {St. Louis}
  460. Nashville Memphis Montgomery Jackson
  461. {New Orleans} {Des Moines} Lincoln {Kansas City}
  462. Topeka Springfield {Little Rock} {Oklahoma City}
  463. Dallas Houston Edmonton Calgary
  464. Yellowknife Denver Albuquerque Phoenix
  465. Boise {Salt Lake City} Vancouver Whitehorse
  466. Spokane Seattle Salem Reno
  467. {Las Vegas} {Los Angeles} {San Diego} {San Francisco}
  468. Anchorage Fairbanks Ketchikan Honolulu
  469. } eu {
  470. {Unknown Timezone #0}
  471. Reykjavik Dublin Cork London
  472. Cardiff Edinburgh Belfast Liverpool
  473. Lisbon Porto Valletta Madrid
  474. Barcelona Valencia {La Coruna} Seville
  475. Paris Brest Lyons Bordeaux
  476. Marseilles Brussels Bastogne Amsterdam
  477. Rotterdam Luxembourg Berlin Hamburg
  478. Essen Frankfurt Munich Bern
  479. Geneve Vaduz Wien Innsbruck
  480. Rome Genova Venezia Palermo
  481. Sassari Oslo Bergen Trondheim
  482. Copenhagen Odense Stockholm Gothenburg
  483. Helsinki Turku Mikkeli Warszawa
  484. Gdansk Poznan Wroclaw Krakow
  485. Praha Bratislava Kosice Budapest
  486. Bucuresti Ljubljana Zagreb Sarajevo
  487. Beograd Skopje Tirane Sofiya
  488. Athinai Thessaloniki Iraklion Ankara
  489. Istanbul Izmir Konya Adana
  490. Jerusalem Pretoria {Cape Town} Durban
  491. Wellington Auckland Dunedin Sydney
  492. Melbourne Adelaide Perth Brisbane
  493. }}
  494. ranks {S A+ A A- B+ B B- C+ C C- D+ D D- F+ F F-}
  495. difficulties {
  496. 1 {Easy {Normal 1} {Normal 2} Hard}
  497. 2 {}
  498. 3 {}
  499. 4 {Normal Hard Nightmare}
  500. }
  501. titles {1 {
  502. {Trigger of Sol} {Gun Master} Gladiator Bishop
  503. Queen Berserker Death {Solar Boy}
  504. {Dark Boy} {Solar Merchant} {Running Boy} King
  505. Rook Knight Pawn
  506. } 2 {
  507. {Sword Master} {Spear Master} {Hammer Master} {Fist Master}
  508. {Gun Master} Adept {Day Walker} {Adventurer}
  509. Agent Collector {Dark Hunter} {Grand Master}
  510. } 3 {
  511. Adept Gladiator {SP Agent} Champion
  512. {Dark Hunter} Alchemist Collector {Doll Master}
  513. Storyteller Grandmaster
  514. } 4 {
  515. {Dark Knight} {Sol Gunner} {Sword Master} {Gun Master}
  516. Guardian {Treasure Hunter} Collector Huntmaster
  517. {Shooting Star} Gladiator {Special Agent} Wanderer
  518. Adventurer {Grand Master}
  519. }}
  520. sides {Red Black Grey}
  521. styles {Sword Spear Hammer Gun Fists {No Style}}
  522. endings {Otenko Everybody Nobody Sabata}
  523. swords {Vanargand Jormungandr Hel}
  524. guns {Knight Dragoon Bomber Witch Ninja}
  525. terrennials {Toasty Nero Ursula Ezra Alexander Tove {War Rock}}
  526. climates {
  527. {Balmy Sub-Tropical} {Arid Desert} {Tropical Rainforest}
  528. {Humind-Continental} {Frigid Arctic}
  529. }
  530. }}
  531. # bok::@ is a shortcut to lookup a string in bok::strings. Arguments starting
  532. # with "#" are used as list indices, other arguments are used as dict keys.
  533. proc bok::@ {locale args} {
  534. variable strings
  535. set current [dict get $strings $locale]
  536. foreach arg $args {
  537. set current [expr {[string index $arg 0] == "#" ?
  538. [lindex $current [string range $arg 1 end]] : [dict get $current $arg]
  539. }]
  540. }
  541. return $current
  542. }
  543. # The character table is stored as a dictionary of lists indexed by the table's
  544. # number formatted as "0x%X". {0x0 0x1F 0x80 0x81 0x81}
  545. set bok::ctable {0x0 {
  546. � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � � �
  547. { } ! \" # ÷ % & ' ( ) * + , - . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ?
  548. @ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ × ] ^ _
  549. ` a b c d e f g h i j k l m n o p q r s t u v w x y z \{ | \} ¯ ⋅
  550. } 0x1F {
  551. � Ä � Ç É Ñ Ö Ü á à â ä � å ç é è ê ë í ì î ï ñ ó ò ô ö � ú ù û
  552. ü � ° � � � � � ß � � � � � � � � � � � � � � � � � � � � � � �
  553. � ¿ ¡ � � � � � « » � � À � � Œ œ � � � � � � � ý ÿ Ý � � � � �
  554. � � � � � � Â Ê Á Ë È Í Î Ï Ì Ó Ô � Ò Ú Û Ù � � � � � � � � � �
  555. } 0x80 {
  556. � あ い う え お か き く け こ さ し す せ そ
  557. よ た ち つ て と な に ぬ ね の は ひ ふ へ ほ
  558. 下 ま み む め も や ゆ よ ら り る れ ろ わ を
  559. 左 ん ぁ ぃ ぅ ぇ ぉ っ ゃ ゅ ょ が ぎ ぐ げ ご
  560. 右 ざ じ ず ぜ ぞ だ ぢ づ で ど ば び ぶ べ ぼ
  561. 東 ぱ ぴ ぷ ぺ ぽ 。 、 ~ ー … � � � � �
  562. 西 ア イ ウ エ オ カ キ ク ケ コ サ シ ス セ ソ
  563. 南 タ チ ツ テ ト ナ ニ ヌ ネ ノ ハ ヒ フ ヘ ホ
  564. 北 マ ミ ム メ モ ラ リ ル レ ロ ヤ ユ ヨ ワ ヲ
  565. 大 ン ァ ィ ゥ ェ ォ ッ ャ ュ ョ ガ ギ グ ゲ ゴ
  566. 中 ザ ジ ズ ゼ ゾ ダ ヂ ヅ デ ド バ ビ ブ ベ ボ
  567. 小 パ ピ プ ペ ポ ・ : ; 「 」 + × ℃ ℉ �
  568. � ↑ ↓ → ← ★ ♥ ♪ ヴ Ⅰ Ⅱ Ⅲ � � � �
  569. 風 白 黒 赤 青 黄 緑 金 銀 紫 � 火 炎 災 水 氷
  570. 永 太 陽 年 月 日 時 分 秒 春 夏 秋 冬 之 ヶ 々
  571. = 丈 片 己 凶 歯 � � � � � � � � � �
  572. } 0x180 {
  573. � あ い う え お か き く け こ さ し す せ そ
  574. よ た ち つ て と な に ぬ ね の は ひ ふ へ ほ
  575. 下 ま み む め も や ゆ よ ら り る れ ろ わ を
  576. 左 ん ぁ ぃ ぅ ぇ ぉ っ ゃ ゅ ょ が ぎ ぐ げ ご
  577. 右 ざ じ ず ぜ ぞ だ ぢ づ で ど ば び ぶ べ ぼ
  578. 東 ぱ ぴ ぷ ぺ ぽ 。 、 ~ ー … � � � � �
  579. 西 ア イ ウ エ オ カ キ ク ケ コ サ シ ス セ ソ
  580. 南 タ チ ツ テ ト ナ ニ ヌ ネ ノ ハ ヒ フ ヘ ホ
  581. 北 マ ミ ム メ モ ラ リ ル レ ロ ヤ ユ ヨ ワ ヲ
  582. 大 ン ァ ィ ゥ ェ ォ ッ ャ ュ ョ ガ ギ グ ゲ ゴ
  583. 中 ザ ジ ズ ゼ ゾ ダ ヂ ヅ デ ド バ ビ ブ ベ ボ
  584. 小 パ ピ プ ペ ポ ・ : ; 「 」 + × ℃ ℉ �
  585. � ↑ ↓ → ← ★ ♥ ♪ ヴ Ⅰ Ⅱ Ⅲ � � � �
  586. 風 白 黒 赤 青 黄 緑 金 銀 紫 � 火 炎 災 水 氷
  587. 永 太 陽 年 月 日 時 分 秒 春 夏 秋 { } 之 ヶ 々
  588. = 丈 片 己 凶 歯 � � � � � � � � � �
  589. } 0x81 {
  590. � 地 均 坂 塔 境 塊 填 場 増 堀 堤 壊 塚 域 城
  591. � 現 理 球 環 � � � � 切 � 功 攻 項 崎 靖
  592. 端 化 代 付 何 仕 任 仗 仲 伯 件 作 伝 休 体 仮
  593. 住 佐 他 使 便 信 倍 借 価 値 低 侮 個 保 係 供
  594. 侵 依 偉 備 偽 似 俊 傷 像 優 候 修 例 側 倒 働
  595. 健 併 佳 倫 停 傲 儀 � � � � � � � � �
  596. 衝 行 往 彼 役 徐 復 後 待 得 徳 術 街 御 徴 徹
  597. 衛 打 払 押 択 技 抜 投 抗 持 担 指 捨 排 抵 挑
  598. 推 提 携 授 接 掘 操 揮 捕 探 換 振 掛 援 拠 損
  599. 拡 把 握 掃 撤 � � � � � � � � � � �
  600. 状 牧 物 特 犠 牲 独 狙 猛 狂 狩 猫 狐 狼 獲 猟
  601. 獄 性 快 悦 怪 悟 怖 情 慎 慢 燐 憶 � � � �
  602. � 粒 料 粗 精 � � � � 灯 灼 焼 煙 燥 燃 爆
  603. 燼 札 材 林 杯 村 析 相 枚 板 松 根 格 槍 横 株
  604. 様 棺 桶 桿 植 橋 構 機 械 � 欄 樹 樋 椛 椿 模
  605. 根 標 � � � 和 利 科 称 程 種 移 秘 積 稼 稲
  606. } 0x82 {
  607. � 礼 祈 社 祝 神 視 福 � � � � 初 裕 複 被
  608. 捕 紅 紀 約 紋 紙 細 組 統 終 純 練 級 緒 経 絵
  609. 給 絃 納 紹 絡 結 続 継 絶 編 縁 博 織 総 縦 綾
  610. 締 績 網 縮 絆 幻 郷 � � 次 冷 凍 � � 議 論
  611. 訳 計 討 記 許 訓 詳 説 話 証 読 設 語 談 試 調
  612. 誤 課 誠 誘 護 認 謙 誕 識 謝 泡 汚 浪 液 涙 汰
  613. 沙 江 況 沢 泊 河 注 洋 泣 治 活 浴 浩 池 波 洗
  614. 流 法 決 油 消 温 浮 海 � � � � � � � �
  615. }}
  616. # bok::decstr converts a list of 8-bit integers to a string, mapping to
  617. # bok::ctable. If $tab is specified then the decoding only uses table $tab,
  618. # essentially assuming that each 8-bit integer is preceded by $tab.
  619. proc bok::decstr {src {tab ""}} {
  620. variable ctable
  621. if {$tab != ""} {
  622. set tab [format 0x%X $tab]
  623. if {![dict exists $ctable $tab]} {
  624. return -code error "invalid table $tab"
  625. }
  626. }
  627. while {[lindex $src end] == 0} {
  628. set src [lreplace $src end end]
  629. }
  630. set r ""
  631. set state ""
  632. foreach c $src {
  633. if {$state == ""} {
  634. if {$tab == ""} {
  635. set state [format 0x%X $c]
  636. if {[dict exists $ctable $state]} {continue}
  637. set state [format 0x%X [expr {$c >> 8}]]
  638. } else {
  639. set state $tab
  640. }
  641. }
  642. if {[dict exists $ctable $state]} {
  643. append r [lindex [dict get $ctable $state] [expr {$c & 0xFF}]]
  644. } else {
  645. append r �
  646. }
  647. set state ""
  648. }
  649. return $r
  650. }
  651. # bok::encstr converts a string to a list of 8-bit integers, mapping from
  652. # bok::ctable. If $tab is specified the encoder only uses the $tab table,
  653. # essentially omitting the table-change characters.
  654. proc bok::encstr {src {tab ""}} {
  655. variable ctable
  656. if {$tab != ""} {
  657. set tab [format 0x%X $tab]
  658. if {![dict exists $ctable $tab]} {
  659. return -code error "invalid table $tab"
  660. }
  661. }
  662. set k 0
  663. set r {}
  664. foreach c [split $src {}] {
  665. if {$c == "�"} {break}
  666. if {$tab != ""} {
  667. set k 0
  668. set t [lsearch -exact [dict get $ctable $tab] $c]
  669. } else {
  670. foreach {k s} $ctable {
  671. if {[set t [lsearch -exact $s $c]] >= 0} {break}
  672. }
  673. }
  674. if {$t < 0} {break}
  675. if {$k != "0"} {lappend r $k}
  676. lappend r $t
  677. }
  678. return $r
  679. }
  680. set bok::b64table {jp {
  681. あ い う え お か き く け こ さ し す せ そ た
  682. ち つ て と な に ぬ ね の は ひ ふ へ ほ ま み
  683. む め も や ゆ よ ら り る れ ろ わ を が ぎ ぐ
  684. げ ご ざ じ ず ぜ ぞ だ ぢ づ で ど ば び ぶ べ ぼ
  685. } en {
  686. B C D F G H J K L M N P Q R S T V W X Y Z
  687. b c d f g h j k l m n p q r s t v w x y z
  688. 0 1 2 3 4 5 6 7 8 9 ? ! @ # = ^ > / - _ + : .
  689. } lk {
  690. A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
  691. a b c d e f g h i j k l m n o p q r s t u v w x y z
  692. 1 2 3 4 5 6 7 8 9 0 ? = .
  693. }}
  694. # bok::decb64 decodes the base64 string $pass using the above tables into a list
  695. # of 6-bit integers, ignoring any [:space:] characters. If the $game is 4 the
  696. # "jp" & "lk" tables are used, otherwise the "jp" & "en" tables are used.
  697. proc bok::decb64 {pass {game 1} {lang ""}} {
  698. variable b64table
  699. set tables [list [dict get $b64table jp] \
  700. [dict get $b64table [expr {$game < 4 ? "en" : "lk"}]]]
  701. return [lmap c [split $pass {}] {
  702. foreach table $tables {
  703. if {[set j [lsearch -exact $table $c]] >= 0} {break}
  704. }
  705. if {[string is space $c] || $j >= 64} {continue}
  706. if {$j < 0} {return -code error "Invalid character: $c"}
  707. set j
  708. }]
  709. }
  710. # bok::encb64 encodes a base64 string from list, which is list of 6-bit integers
  711. # using the appropriate alphabet for the $game / $lang pair. $lang is set based
  712. # on the region if it is set to an empty string.
  713. proc bok::encb64 {list {game 1} {lang ""}} {
  714. variable b64table
  715. if {$lang == ""} {
  716. set lang [getlang $list $game 6]
  717. }
  718. set table [dict get $b64table [expr {
  719. $lang == "jp" ? "jp" : $game < 3 ? "en" : "lk"}]]
  720. return [join [lmap c $list {lindex $table $c}] {}]
  721. }
  722. # bok::spaceout inserts spaces every few characters for readability. It uses
  723. # the common amount for a particular $game and $lang combination. 5 if $game
  724. # is 4, 6 if $lang is "jp", and finally 8 if $lang is "en". Otherwise the
  725. # number guessed based on the length of $str.
  726. proc bok::spaceout {str {game ""} {lang ""}} {
  727. if {$game == "" && $lang == ""} {
  728. variable passwordlengths
  729. set k [lindex [dict keys [dict filter $passwordlengths value \
  730. [string length $str]]] 0]
  731. } else {
  732. set k $lang$game
  733. }
  734. switch -exact -- $k {
  735. 4 - en4 - jp4 {set n 5}
  736. jp - jp1 - jp2 - jp3 {set n 6}
  737. en - en1 - en2 - en3 {set n 8}
  738. default {return $str}
  739. }
  740. for {set i $n} {$i < [string length $str]} {incr i; incr i $n} {
  741. set str "[string range $str 0 $i-1] [string range $str $i end]"
  742. }
  743. return $str
  744. }
  745. # bok::getint gets an integer of width $count at bit-offset $offset from the
  746. # list $list of $width wide integers, interpreting it as a two's complement
  747. # signed integer if $sign is true. The bits are read and written LSb first.
  748. proc bok::getint {list offset count {sign 0} {width 6}} {
  749. set count [expr {min($count, [llength $list] * $width - $offset)}]
  750. for {set v 0; set i 0} {$i < $count} {incr i; incr offset} {
  751. set j [expr {$offset / $width}]
  752. set k [expr {$offset % $width}]
  753. set v [expr {$v | ([lindex $list $j] >> $k & 1) << $i}]
  754. }
  755. if {$sign && $count > 0 && ($v >> $count - 1 & 1)} {
  756. set v [expr {-(~$v + 1 & (1 << $count) - 1)}]
  757. }
  758. return $v
  759. }
  760. # bok::putint puts the integer $v of width $count into list $listvar at
  761. # bit-offset $offset. $listvar is a list of $width wide integers. Zeros are
  762. # added to the list as needed. $v is clamped to [0,2^$count) for unsigned
  763. # integers ($sign is false), and [-2^($count-1),2^($count-1)) for signed
  764. # integers ($sign is true). Signed numbers are converted to two's-complement
  765. # formatted $width wide unsigned integers before writing. The bits are written
  766. # and read LSb first.
  767. proc bok::putint {listvar v offset count {sign 0} {width 6}} {
  768. if {$listvar != ""} {upvar $listvar list} {set list {}}
  769. set m [expr {(1 << $count - ($sign && $count > 0 ? 1 : 0)) - 1}]
  770. set v [expr {$sign ? min(max($v, -($m + 1)), $m) + $m + 1 ^ $m + 1 :
  771. min(max($v, 0), $m)}]
  772. for {set i 0} {$i < $count} {incr i; incr offset} {
  773. set j [expr {$offset / $width}]
  774. set k [expr {$offset % $width}]
  775. while {[llength $list] <= $j} {lappend list 0}
  776. lset list $j [expr {
  777. [lindex $list $j] & ~(1 << $k) | ($v >> $i & 1) << $k}]
  778. }
  779. return $list
  780. }
  781. # bok::getstr gets the string converted from $count octets from the $list of
  782. # $width wide integers starting at bit-offset $offset. $table is passed to
  783. # bok::decstr along with the octets.
  784. proc bok::getstr {list offset count {table ""} {width 6}} {
  785. if {$table == "-"} {set table ""}
  786. if {$offset + $count * 8 > [llength $list] * $width} {
  787. set count [expr {[llength $list] * $width - $offset >> 3}]
  788. }
  789. for {set r {}; set i 0} {$i < $count} {incr i} {
  790. lappend r [getint $list [expr {$offset + $i * 8}] 8 0 $width]
  791. }
  792. return [decstr $r $table]
  793. }
  794. # bok::putstr puts $count octets from the encoded $str into $listvar at
  795. # bit-offset $offset. $listvar is a list of $width wide integers and $table
  796. # is passed to bok::encstr along with $str.
  797. proc bok::putstr {listvar str offset count {table ""} {width 6}} {
  798. if {$listvar != ""} {upvar $listvar list} {set list {}}
  799. if {$table == "-"} {set table ""}
  800. set l [encstr $str $table]
  801. for {set i 0} {$i < $count} {incr i} {
  802. set c [expr {$i < [llength $l] ? [lindex $l $i] : 0}]
  803. putint list $c [expr {$offset + $i * 8}] 8 0 $width
  804. }
  805. return $list
  806. }
  807. # bok::bitmaps is a dictionary of bitmaps with the keys being the password
  808. # language and the game number: {jp1 jp2 jp3 jp4 en1 en2 en3 en4}
  809. # each bitmap is a dictionary with list values: {type offset count modifier}
  810. # The type is either "int" or "str". The offset is the bit that the value
  811. # starts at. The count how long the value is in either bits (for type "int")
  812. # or octets (for type "str"). The modifier for type "int" is a boolean meaning
  813. # unsigned for false values or signed for true values; for type "str" it is the
  814. # character table to use with "-" indicating the default (""). The list items
  815. # after the type can be used directly as arguments to bok::getint/bok::putint
  816. # for type "int", or bok::getstr/bok::putstr for type "str".
  817. set bok::bitmaps {
  818. jp1 {
  819. region {int 0 3 0} checksum {int 3 16 0}
  820. offset {int 19 2 0} sol/4 {int 21 9 0}
  821. timezone {int 30 6 0} hours {int 36 6 0}
  822. minutes {int 42 6 0} difficulty {int 48 3 0}
  823. dungeons {int 51 5 0} clears {int 56 3 0}
  824. continues {int 59 4 0} caught {int 63 5 0}
  825. kills {int 68 7 0} rank {int 75 4 0}
  826. title {int 79 5 0} name {str 84 5 0x180}
  827. link-battles {int 124 6 0} link-trades {int 130 6 0}
  828. loan {int 136 8 0} padding {int 144 0 0}
  829. } jp2 {
  830. checksum {int 0 16 0} header-padding {int 16 2 0}
  831. region {int 18 3 0} offset {int 21 3 0}
  832. timezone {int 24 6 0} side {int 30 3 0}
  833. style {int 33 4 0} kills/8 {int 37 8 0}
  834. forges {int 45 7 0} link-battles {int 52 6 0}
  835. link-shopping {int 58 6 0} sol/4096 {int 64 10 1}
  836. loan/256 {int 74 8 1} hours {int 82 6 0}
  837. titles {int 88 16 0} name {str 104 5 0x180}
  838. padding {int 144 0 0}
  839. } jp3 {
  840. checksum {int 0 16 0} header-padding {int 16 2 0}
  841. region {int 18 3 0} offset {int 21 3 0}
  842. timezone {int 24 6 0} kills/8 {int 30 8 0}
  843. forges {int 38 7 0} races {int 45 9 0}
  844. link-races {int 54 7 0} cross-linked {int 61 1 0}
  845. endings {int 62 6 0} sol/4096 {int 68 10 0}
  846. loan/256 {int 78 8 0} hours {int 86 6 0}
  847. titles {int 92 12 0} name {str 104 5 0x180}
  848. padding {int 144 0 0}
  849. } jp4 {
  850. checksum {int 0 16 0} header-padding {int 16 2 0}
  851. region {int 18 3 0} offset {int 21 3 0}
  852. titles {int 24 14 0} difficulty {int 38 2 0}
  853. hours {int 40 7 0} sol/4096 {int 47 15 0}
  854. sword {int 62 3 0} gun {int 65 3 0}
  855. terrennial {int 68 3 0} climate {int 71 3 0}
  856. name-dark {str 74 10 -} name-solar {str 154 10 -}
  857. padding {int 234 6 0}
  858. } en1 {
  859. region {int 0 3 0} checksum {int 3 16 0}
  860. offset {int 19 2 0} sol/4 {int 21 9 0}
  861. timezone {int 30 7 0} hours {int 37 6 0}
  862. minutes {int 43 6 0} difficulty {int 49 3 0}
  863. dungeons {int 52 5 0} clears {int 57 3 0}
  864. continues {int 60 4 0} caught {int 64 5 0}
  865. kills {int 69 7 0} rank {int 76 4 0}
  866. title {int 80 5 0} name {str 85 9 0}
  867. link-battles {int 157 6 0} link-trades {int 163 6 0}
  868. loan {int 169 8 0} padding {int 177 15 0}
  869. } en2 {
  870. checksum {int 0 16 0} header-padding {int 16 2 0}
  871. region {int 18 3 0} offset {int 21 3 0}
  872. timezone {int 24 7 0} side {int 31 3 0}
  873. style {int 34 4 0} kills/8 {int 38 8 0}
  874. forges {int 46 7 0} link-battles {int 53 6 0}
  875. link-shopping {int 59 6 0} sol/4096 {int 65 10 1}
  876. loan/256 {int 75 8 1} hours {int 83 6 0}
  877. titles {int 89 16 0} name {str 105 9 0}
  878. padding {int 177 15 0}
  879. } en3 {
  880. checksum {int 0 16 0} header-padding {int 16 2 0}
  881. region {int 18 3 0} offset {int 21 3 0}
  882. timezone {int 24 7 0} kills/8 {int 31 8 0}
  883. forges {int 39 7 0} races {int 46 9 0}
  884. link-races {int 55 7 0} cross-linked {int 62 1 0}
  885. endings {int 63 6 0} sol/4096 {int 69 10 0}
  886. loan/256 {int 79 8 0} hours {int 87 6 0}
  887. titles {int 93 12 0} name {str 105 9 0}
  888. padding {int 177 15 0}
  889. } en4 {
  890. checksum {int 0 16 0} header-padding {int 16 2 0}
  891. region {int 18 3 0} offset {int 21 3 0}
  892. titles {int 24 14 0} difficulty {int 38 2 0}
  893. hours {int 40 7 0} sol/4096 {int 47 15 0}
  894. sword {int 62 3 0} gun {int 65 3 0}
  895. terrennial {int 68 3 0} climate {int 71 3 0}
  896. name-dark {str 74 10 -} name-solar {str 154 10 -}
  897. padding {int 234 6 0}
  898. }
  899. }
  900. # bok::getdict returns a dictionary created by reading values from the $list of
  901. # $width wide integers for each key in $args, or every key if $args is empty,
  902. # as specified in bok::bitmaps for the $game / $lang pair.
  903. proc bok::getdict {list game lang width args} {
  904. variable bitmaps
  905. if {[llength $args] == 0} {
  906. set args [dict keys [dict get $bitmaps $lang$game]]
  907. }
  908. set dict {}
  909. foreach key $args {
  910. if {![dict exists $bitmaps $lang$game $key]} {continue}
  911. lassign [dict get $bitmaps $lang$game $key] type offset count mod
  912. switch -exact -- $type str {
  913. dict set dict $key [getstr $list $offset $count $mod $width]
  914. } int {
  915. dict set dict $key [getint $list $offset $count $mod $width]
  916. }
  917. }
  918. return $dict
  919. }
  920. # bok::getkeys returns the list of values from calling bok::getdict.
  921. proc bok::getkeys {list game lang width args} {
  922. return [dict values [getdict $list $game $lang $width {*}$args]]
  923. }
  924. # bok::getkey returns only the value from calling bok::getdict with $key.
  925. proc bok::getkey {list game lang width key} {
  926. return [lindex [getkeys $list $game $lang $width $key] 0]
  927. }
  928. # bok::putdict inserts the values from each key/value pair into the $listvar of
  929. # $width wide integers as specified with the corresponding key in bok::bitmaps
  930. # for the $game / $lang pair.
  931. proc bok::putdict {listvar game lang width args} {
  932. variable bitmaps
  933. if {$listvar != ""} {upvar $listvar list} {set list {}}
  934. if {[llength $args] == 1} {set args [concat {*}$args]}
  935. if {[llength $args] % 2 != 0} {
  936. set usage {putdict listvar game lang width ?dict | key value ...?}
  937. return -code error "wrong # args: should be \"$usage\""
  938. }
  939. foreach {key value} $args {
  940. if {![dict exists $bitmaps $lang$game $key]} {continue}
  941. lassign [dict get $bitmaps $lang$game $key] type offset count mod
  942. switch -exact -- $type str {
  943. putstr list $value $offset $count $mod $width
  944. } int {
  945. putint list $value $offset $count $mod $width
  946. }
  947. }
  948. return $list
  949. }
  950. # bok::modkeys executes the last argument for each key given in $args, with
  951. # the key, value, and count values set to variables specified in $varnames.
  952. # If $varnames only contains one member then it is used for the storing the
  953. # value. The key and count values are from the bitmap specified in bok::bitmaps
  954. # for the $game / $lang pair, and the value itself is extracted from the list of
  955. # $width wide integers in $listvar. The result of the executed body is then
  956. # put back into $listvar in place of what was extracted. Non-existent or out
  957. # of range keys are ignored, continues skip overwriting the value, and breaks
  958. # exit early. This proc returns the final value of $listvar.
  959. proc bok::modkeys {listvar game lang width varnames args} {
  960. if {$listvar == "" || $varnames == "" || [llength $args] < 1} {
  961. set usage {modify listvar game lang width varnames ?key ...? body}
  962. return -code error "wrong # args: should be \"$usage\""
  963. }
  964. variable bitmaps
  965. upvar $listvar list
  966. if {[llength $varnames] > 1} {
  967. lassign $varnames keyname varname countvar
  968. if {$keyname != ""} {upvar $keyname key}
  969. if {$varname != ""} {upvar $varname var}
  970. if {$countvar != ""} {upvar $countvar count}
  971. } elseif {$varnames != ""} {
  972. upvar $varnames var
  973. }
  974. set body [lindex $args end]
  975. if {[llength $args] == 1} {
  976. set args [dict keys [dict get $bitmaps $lang$game]]
  977. } else {
  978. set args [lrange $args 0 end-1]
  979. }
  980. set max [expr {[llength $list] * $width}]
  981. foreach key $args {
  982. if {![dict exists $bitmaps $lang$game $key]} {continue}
  983. lassign [dict get $bitmaps $lang$game $key] type offset count mod
  984. if {$offset >= $max} {continue}
  985. switch -exact -- $type int {
  986. set var [getint $list $offset $count $mod $width]
  987. putint list [uplevel 1 $body] $offset $count $mod $width
  988. } str {
  989. set var [getstr $list $offset $count $mod $width]
  990. putstr list [uplevel 1 $body] $offset $count $mod $width
  991. }
  992. }
  993. return $list
  994. }
  995. # bok::getregion reads the region bits from the $list of $width wide integers
  996. # and returns a short region code, or an error.
  997. proc bok::getregion {list {game 1} {width 6}} {
  998. switch -exact -- [set t [getkey $list $game jp $width region]] {
  999. 1 {return jp} 2 {return na} 3 {return eu}
  1000. }
  1001. return -code error "Invalid region #$t"
  1002. }
  1003. # bok::getlang reads the region bits from the $list of $width wide integers and
  1004. # returns a short language code or an error.
  1005. proc bok::getlang {list {game 1} {width 6}} {
  1006. switch -exact -- [set t [getkey $list $game jp $width region]] {
  1007. 1 {return jp} 2 - 3 {return en}
  1008. }
  1009. return -code error "Invalid region #$t"
  1010. }
  1011. # bok::whichgame searches $dict for keys specific to a particular game, and
  1012. # returns the game number of the first one found.
  1013. proc bok::whichgame {dict} {
  1014. foreach {key game} {
  1015. dungeons 1 clears 1 caught 1 rank 1 title 1 link-trades 1
  1016. rank-name 1 title-name 1
  1017. side 2 style 2 link-shopping 2
  1018. side-name 2 style-name 2
  1019. races 3 link-races 3 cross-linked 3 endings 3
  1020. sword 4 gun 4 terrennial 4 climate 4 name-dark 4 name-solar 4
  1021. sword-name 4 gun-name 4 terrennial-name 4 climate-name 4
  1022. } {
  1023. if {[dict exists $dict $key]} {return $game}
  1024. }
  1025. return -code error "Unknown game"
  1026. }
  1027. # bok::checksum calculates the CRC-16 for the $list of 6-bit integers with an
  1028. # initializer of 0xFFFF, the $game appropriate polynomial from bok::sumconsts,
  1029. # and the output inverted, and starting at the offset from bok::sumoffsets.
  1030. set bok::sumoffsets {4 3 3 3}
  1031. set bok::sumconsts {0x1021 0x1021 0x8005 0x180D}
  1032. proc bok::checksum {list {game 1}} {
  1033. variable sumoffsets
  1034. variable sumconsts
  1035. for {
  1036. set c [lindex $sumconsts $game-1]
  1037. set v 0xFFFF
  1038. set i [lindex $sumoffsets $game-1]
  1039. } {$i < [llength $list]} {incr i} {
  1040. set v [expr {$v ^ [lindex $list $i] << 8}]
  1041. for {set j 0} {$j < 8} {incr j} {
  1042. set v [expr {($v << 1 ^ (($v & 0x8000) ? $c : 0)) & 0xFFFF}]
  1043. }
  1044. }
  1045. return [expr {~$v & 0xFFFF}]
  1046. }
  1047. # bok::xor encodes/decodes the password bits from the $list of 6-bit integers
  1048. # using bok::xorconst and the $game appropriate initalizer from bok::xorconsts. # $m is the initialized value, $t is initialized to ~0, and the offset is
  1049. # read from the $list. For each iteration $m is multiplied by bok::xorconst
  1050. # then incremented by 1, and $t is xored by the result. After $offset plus 4
  1051. # iterations, starting at index 4 until the end of the $list, each $list item
  1052. # is xored by the last 6 bits of $t before the next iteration.
  1053. # The encoded list is returned.
  1054. # Using the full sized constants can casue unnecessary integer promotion,
  1055. # and only the last 6 bits are used, so smaller constants are used here.
  1056. #set bok::xorconsts {0x8C159 0xB8E6E 0x8C159 0x5BB15}
  1057. set bok::xorconsts {0x19 0x2E 0x19 0x15}
  1058. #set bok::xorconst 0x6262C05D
  1059. set bok::xorconst 0x1D
  1060. proc bok::xor {list {game 1}} {
  1061. variable xorconsts
  1062. variable xorconst
  1063. for {
  1064. set m [lindex $xorconsts $game-1]
  1065. set t 0x3F
  1066. set i -[getkey $list $game jp 6 offset]
  1067. } {$i < [llength $list]} {incr i} {
  1068. if {$i >= 4} {
  1069. lset list $i [expr {[lindex $list $i] ^ $t}]
  1070. }
  1071. set m [expr {$xorconst * $m + 1 & 0x3F}]
  1072. set t [expr {$t ^ $m}]
  1073. }
  1074. return $list
  1075. }
  1076. set bok::passwordlengths {
  1077. jp1 24 jp2 24 jp3 24 jp4 40 en1 32 en2 32 en3 32 en4 40
  1078. }
  1079. # bok::decpass decodes the base64 string $password into a dictionary.
  1080. proc bok::decpass {password {game 1} {lang ""}} {
  1081. variable passwordlengths
  1082. set l [decb64 $password $game]
  1083. if {$lang == ""} {
  1084. set lang [getlang $l $game]
  1085. }
  1086. set passlen [dict get $passwordlengths $lang$game]
  1087. if {[llength $l] != $passlen} {
  1088. return -code error "Invalid length: [llength $l] != $passlen"
  1089. }
  1090. set sum [checksum $l $game]
  1091. set l [xor $l $game]
  1092. set dict [getdict $l $game $lang 6]
  1093. if {[dict get $dict checksum] != $sum} {
  1094. dict append dict checksum "!=$sum"
  1095. }
  1096. return $dict
  1097. }
  1098. # bok::encpass encodes $dict into a base64 password.
  1099. proc bok::encpass {dict {lang ""}} {
  1100. if {$lang == ""} {
  1101. set lang [lindex {jp jp en en} [dict get $dict region]]
  1102. }
  1103. set game [whichgame $dict]
  1104. set l [xor [putdict {} $game $lang 6 $dict] $game]
  1105. putdict l $game $lang 6 checksum [checksum $l $game]
  1106. return [encb64 $l $game $lang]
  1107. }
  1108. # bok::normalizelist sets {*}$skey in $dictvar based on the value of {*}$vkey
  1109. # based on the strings in $list, or vice versa. $name is used for error
  1110. # messages, and defaults to the last element of $vkey. If $robust is set then
  1111. # no errors are returned for invalid values, or known invalid strings.
  1112. proc bok::normalizelist {dictvar list vkey skey {name ""} {robust 0}} {
  1113. upvar $dictvar dict
  1114. if {$name == ""} {
  1115. set name [lindex $vkey end]
  1116. }
  1117. set inval "Invalid $name #"
  1118. set unknown "Unknown $name #"
  1119. if {[dict exists $dict {*}$vkey]} {
  1120. set v [dict get $dict {*}$vkey]
  1121. set s "$unknown$v"
  1122. if {$v >= 0 && $v < [llength $list]} {
  1123. set s [lindex $list $v]
  1124. }
  1125. if {!$robust && $s == "$unknown$v"} {
  1126. return -code error $s
  1127. }
  1128. dict set dict {*}$skey $s
  1129. } elseif {[dict exists $dict {*}$skey]} {
  1130. set s [dict get $dict {*}$skey]
  1131. set v [lsearch -exact -nocase $list $s]
  1132. if {$v < 0 && [string first $inval $s] == 0} {
  1133. set v [string range $s [string length $inval] end]
  1134. if {![string is integer $v]} {set v -1}
  1135. if {!$robust} {
  1136. return -code error $s
  1137. }
  1138. } elseif {$v < 0 && [string first $unknown $s] == 0} {
  1139. set v [string range $s [string length $unknown] end]
  1140. if {![string is integer $v]} {set v -1}
  1141. if {!$robust} {
  1142. return -code error $s
  1143. }
  1144. }
  1145. if {$v < 0} {
  1146. return -code error "Invalid [lindex $skey end]: $s"
  1147. }
  1148. dict set dict {*}$vkey $v
  1149. }
  1150. }
  1151. # bok::normalizebits sets {*}$lkey in $dictvar based on the value of {*}$vkey
  1152. # based on the strings in $list, or vice versa. $name is used for error
  1153. # messages, and defaults to the last element of $vkey. If $robust is set then
  1154. # invalid values and items are ignored.
  1155. proc bok::normalizebits {dictvar list vkey lkey {name ""} {robust 0}} {
  1156. upvar $dictvar dict
  1157. if {$name == ""} {
  1158. set name [lindex $vkey end]
  1159. }
  1160. if {[dict exists $dict {*}$vkey]} {
  1161. dict set dict {*}$vkey [format 0x%0[expr {
  1162. [llength $list] + 3 >> 2
  1163. }]X [dict get $dict {*}$vkey]]
  1164. set v [dict get $dict {*}$vkey]
  1165. for {set i 0; set l {}} {$i < [llength $list]} {incr i} {
  1166. if {$v & 1 << $i} {
  1167. set v [expr {$v ^ 1 << $i}]
  1168. lappend l [lindex $list $i]
  1169. }
  1170. }
  1171. if {!$robust && $v != 0} {
  1172. for {} {!($v & 1 << $i)} {incr i} {}
  1173. return -code error "Unknown $name #$i"
  1174. }
  1175. dict set dict {*}$lkey $l
  1176. } elseif {[dict exists $dict {*}$lkey]} {
  1177. set v 0
  1178. foreach item [dict get $dict {*}$lkey] {
  1179. set i [lsearch -exact -nocase $list $item]
  1180. if {!$robust && $i < 0} {
  1181. return -code error "Unknown $name: $item"
  1182. }
  1183. set v [expr {$v | ($i >= 0 ? 1 << $i : 0)}]
  1184. }
  1185. dict set dict {*}$vkey $v
  1186. }
  1187. }
  1188. # bok::normalize normalizes the provided dictionary using $locale strings
  1189. # such that if "sol/4096" or "timezone-name" exists then "sol" and "timezone"
  1190. # will contain the equivalent value in the result. If $robust is set then no
  1191. # ignorable errors are returned.
  1192. proc bok::normalize {dict {locale en} {robust 0}} {
  1193. # Identify game, language, and region
  1194. set game [whichgame $dict]
  1195. normalizelist dict [@ $locale regions] region region-name \
  1196. [@ $locale labels region] $robust
  1197. set region [expr {[dict exists $dict region] ? [dict get $dict region] : -1}]
  1198. if {!$robust && ($region < 1 || $region > 3)} {
  1199. return -code error "Unknown region"
  1200. }
  1201. set lang [lindex {?? jp en en ?? ?? ?? ??} $region]
  1202. set region [lindex {?? jp na eu ?? ?? ?? ??} $region]
  1203. # Normalize
  1204. dict set dict game $game
  1205. dict set dict game-name [@ $locale games $lang$game]
  1206. if {![dict exists $dict header-padding]} {dict set dict header-padding 0}
  1207. if {![dict exists $dict padding]} {dict set dict padding 0}
  1208. if {![dict exists $dict offset]} {dict set dict offset 0}
  1209. # Sol
  1210. if {[dict exists $dict sol]} {
  1211. dict set dict sol/4 [expr {int([dict get $dict sol] / 4)}]
  1212. dict set dict sol/4096 [expr {int([dict get $dict sol] / 4096)}]
  1213. } elseif {[dict exists $dict sol/4]} {
  1214. dict set dict sol [expr {[dict get $dict sol/4] * 4}]
  1215. dict set dict sol/4096 [expr {int([dict get $dict sol/4] / 1024)}]
  1216. } elseif {[dict exists $dict sol/4096]} {
  1217. dict set dict sol [expr {[dict get $dict sol/4096] * 4096}]
  1218. dict set dict sol/4 [expr {[dict get $dict sol/4096] * 1024}]
  1219. }
  1220. # Kills
  1221. if {[dict exists $dict kills]} {
  1222. dict set dict kills/8 [expr {int([dict get $dict kills] / 8)}]
  1223. } elseif {[dict exists $dict kills/8]} {
  1224. dict set dict kills [expr {[dict get $dict kills/8] * 8}]
  1225. }
  1226. # Loan
  1227. if {[dict exists $dict loan]} {
  1228. dict set dict loan/256 [expr {int([dict get $dict loan] / 256)}]
  1229. } elseif {[dict exists $dict loan/256]} {
  1230. dict set dict loan [expr {[dict get $dict loan/256] * 256}]
  1231. }
  1232. # Timezone
  1233. set l [@ $locale timezones]
  1234. if {[dict exists $l $region]} {
  1235. normalizelist dict [dict get $l $region] timezone timezone-name \
  1236. [@ $locale labels timezone] $robust
  1237. }
  1238. # Boktai 1
  1239. normalizelist dict [@ $locale ranks] rank rank-name [@ $locale labels rank] \
  1240. $robust
  1241. normalizelist dict [@ $locale titles 1] title title-name \
  1242. [@ $locale labels title] $robust
  1243. normalizelist dict [@ $locale difficulties $game] difficulty difficulty-name \
  1244. [@ $locale labels difficulty] $robust
  1245. # Boktai 2
  1246. normalizelist dict [@ $locale sides] side side-name [@ $locale labels side] \
  1247. $robust
  1248. normalizelist dict [@ $locale styles] style style-name \
  1249. [@ $locale labels style] $robust
  1250. # Boktai 2, Boktai 3, and Lunar Knight's Titles
  1251. normalizebits dict [@ $locale titles $game] titles title-list \
  1252. [@ $locale labels titles] $robust
  1253. # Boktai 3's Endings
  1254. normalizebits dict [@ $locale endings] endings ending-list \
  1255. [@ $locale labels endings] $robust
  1256. # Lunar Knights favorites
  1257. normalizelist dict [@ $locale swords] sword sword-name \
  1258. [@ $locale labels sword] $robust
  1259. normalizelist dict [@ $locale guns] gun gun-name [@ $locale labels gun] \
  1260. $robust
  1261. normalizelist dict [@ $locale terrennials] terrennial terrennial-name \
  1262. [@ $locale labels terrennial] $robust
  1263. normalizelist dict [@ $locale climates] climate climate-name \
  1264. [@ $locale labels climate] $robust
  1265. return $dict
  1266. }
  1267. namespace eval bok::ui {
  1268. namespace export *
  1269. namespace import [namespace parent]::*
  1270. }
  1271. # bok::ui::info returns a list of ui element types, values, and defaults
  1272. # depending on the values of $game, $region, and $locale. Most values and
  1273. # defaults can be dictionaries keyed by region. A key of "-" makes the next
  1274. # widget always start on a new row.
  1275. # key {int {min ?max?} ?defaults? ?step?}
  1276. # key {string {max ?maxbytes?} ?defaults? ?restrict?}
  1277. # key {list ?values? ?defaults?}
  1278. # key {boolean ?default?}
  1279. # key {bits values ?default?}
  1280. # - {}
  1281. proc bok::ui::info {game {region na} {locale en}} {
  1282. set region [expr {max([region $region $locale num] - 1, 0)}]
  1283. set timezone [expr {
  1284. $region == 0 ? 5 : $region == 2 ? 21 : $region == 1 ? 14 : 0
  1285. }]
  1286. set timezonelists [dict map {key value} [@ $locale timezones] {
  1287. lrange $value 1 end
  1288. }]
  1289. set regions [lrange [@ $locale regions] 1 end]
  1290. switch -exact -- $game 1 {
  1291. return [dict create \
  1292. region-name [list list $regions $region] \
  1293. offset {int {0 3}} \
  1294. sol {int {0 0x7FC 4}} \
  1295. timezone-name [list regionlist $timezonelists {jp 5 na 14 eu 21}] \
  1296. - {} hours {int {0 63}} minutes {int {0 59} 1} \
  1297. difficulty-name [list list [@ $locale difficulties 1] 2] \
  1298. dungeons {int {0 29}} \
  1299. clears {int {1 7}} continues {int {0 15}} \
  1300. caught {int {0 31}} kills {int {0 127}} \
  1301. rank-name [list list [@ $locale ranks]] \
  1302. title-name [list list [@ $locale titles 1]] \
  1303. name {string {jp 5 na 9 eu 9} {jp ジャンゴ na Django eu Django} 1} \
  1304. link-battles {int {0 63}} link-trades {int {0 63}} \
  1305. loan {int {0 255}} \
  1306. padding hidden \
  1307. ]
  1308. } 2 {
  1309. return [dict create \
  1310. region-name [list list $regions $region] \
  1311. offset {int {0 7}} \
  1312. timezone-name [list regionlist $timezonelists {jp 5 na 14 eu 21}] \
  1313. side-name [list list [@ $locale sides]] \
  1314. style-name [list list [@ $locale styles]] \
  1315. kills {int {0 0x7F8 8}} \
  1316. forges {int {0 127}} \
  1317. link-battles {int {0 63}} link-shopping {int {0 63}} \
  1318. sol {int {0 0x1FF000 0x1000}} loan {int {0 0x7F00 0x100}} \
  1319. hours {int {0 63}} \
  1320. name {string {jp 5 na 9 eu 9} {jp ジャンゴ na Django eu Django} 1} \
  1321. title-list [list bits [@ $locale titles 2] 0x40] \
  1322. header-padding hidden padding hidden \
  1323. ]
  1324. } 3 {
  1325. return [dict create \
  1326. region-name [list list [lindex $regions 0] 0] \
  1327. offset {int {0 7}} \
  1328. timezone-name [list regionlist $timezonelists {jp 5 na 14 eu 21}] \
  1329. kills {int {0 0x7F8 8}} forges {int {0 127}} races {int {0 511}} \
  1330. link-races {int {0 127}} cross-linked {boolean} \
  1331. sol {int {0 0x3FF000 0x1000}} loan {int {0 0xFF00 0x100}} \
  1332. hours {int {0 63}} \
  1333. name {string {jp 5 na 9 eu 9} {jp ジャンゴ na Django eu Django} 1} \
  1334. ending-list [list bits [@ $locale endings] 1] \
  1335. title-list [list bits [@ $locale titles 3]] \
  1336. header-padding hidden padding hidden \
  1337. ]
  1338. } 4 {
  1339. return [dict create \
  1340. region-name [list list $regions $region] \
  1341. offset {int {0 7}} \
  1342. difficulty-name [list list [@ $locale difficulties 4]] \
  1343. hours {int {0 127}} sol {int {0 0x7FFF000 0x1000}} \
  1344. - {} \
  1345. sword-name [list list [@ $locale swords]] \
  1346. gun-name [list list [@ $locale guns]] \
  1347. terrennial-name [list list [@ $locale terrennials]] \
  1348. climate-name [list list [@ $locale climates]] \
  1349. - {} \
  1350. name-dark {string {jp {5 10} na 10 eu 10}
  1351. {jp サバタ na Lucian eu Lucian}} \
  1352. name-solar {string {jp {5 10} na 10 eu 10}
  1353. {jp ジャンゴ na Aaron eu Aaron}} \
  1354. title-list [list bits [@ $locale titles 4]] \
  1355. header-padding hidden padding hidden \
  1356. ]
  1357. }
  1358. }
  1359. # bok::ui::region translates the $str to the region "number", two-letter "code",
  1360. # "name", or a list of all three depending on $type. $locale selects which
  1361. # region list from bok::strings to use.
  1362. proc bok::ui::region {str {locale en} {type code}} {
  1363. set dict {}
  1364. switch -glob -nocase -- $str {
  1365. jp - japan - japanese {dict set dict region 1}
  1366. na - north?america - northamerica - america {dict set dict region 2}
  1367. us - usa - united?states - unitedstates {dict set dict region 2}
  1368. en - english {dict set dict region 2}
  1369. eu - europe {dict set dict region 3}
  1370. default {dict set dict region-name $str}
  1371. }
  1372. normalizelist dict [@ $locale regions] region region-name \
  1373. [@ $locale labels region] 1
  1374. set region [dict get $dict region]
  1375. set str [dict get $dict region-name]
  1376. set code [lindex {?? jp na eu ?? ?? ?? ??} $region]
  1377. switch -exact -- $type {
  1378. short - code {
  1379. return $code
  1380. } string - str - name {
  1381. return $str
  1382. } number - num {
  1383. return $region
  1384. } default {
  1385. return [list $region $code $str]
  1386. }}
  1387. }
  1388. # bok::ui::init initializes a grid of widgets in frame $window based on the
  1389. # values from bok::ui::info for region $region. Valid options are -locale to
  1390. # set the locale to use, -columns for the number of columns of label/entry pairs
  1391. # to use, -bind for a list of bindings for matching elements, -style for a list
  1392. # of styles for matching elements, -fontsize for the base font size, and
  1393. # -validate to pass when creating widgets.
  1394. # -bind and -style keys can match either the widget type, the widget subtype,
  1395. # the widget path, or "all". For example button $widget.password.cycleoffset
  1396. # matches the widget path, button, password.button, or "all".
  1397. proc bok::ui::init {window region game args} {
  1398. set usage {init path {jp|na|eu} {1|2|3|4} ?-option value ...?}
  1399. if {[llength $args] % 2 != 0} {
  1400. return -code error "wrong # args: should be \"$usage\""
  1401. }
  1402. set options [dict create \
  1403. -locale en -columns 2 -bind {} -style {} -fontsize 10 -validate all]
  1404. foreach {opt arg} $args {
  1405. switch -exact -- $opt -locale - -columns - -fontsize - -validate {
  1406. dict set options $opt $arg
  1407. } -bind - -style {
  1408. dict lappend options $opt {*}$arg
  1409. } default {
  1410. return -code error "bad option: $opt"
  1411. }
  1412. }
  1413. set columns [expr {[dict get $options -columns] * 2}]
  1414. set locale [dict get $options -locale]
  1415. set region [region $region $locale]
  1416. if {$region == "??" || $game < 1 || $game > 4} {
  1417. return -code error "wrong args: should be \"$usage\""
  1418. }
  1419. if {![winfo exists $window]} {
  1420. ttk::frame $window
  1421. }
  1422. set row 0
  1423. set col $columns
  1424. grid [ttk::frame $window.password] \
  1425. -row $row -column 0 -columnspan $columns -sticky nsew
  1426. pack [ttk::button $window.password.encode -text ↱ -width 2 -takefocus 0 \
  1427. -command [list [namespace current]::encode $window $game $locale]
  1428. ] -padx 2p -side left
  1429. pack [ttk::entry $window.password.entry \
  1430. -font "mono [expr {int([dict get $options -fontsize]*1.2)}]"] \
  1431. -expand 1 -side left -fill x
  1432. pack [ttk::button $window.password.cycleoffset -text ↻ -width 2 -takefocus 0 \
  1433. -command [list [namespace current]::cycleoffset $window $game]
  1434. ] -padx 2p -side left
  1435. pack [ttk::button $window.password.decode -text ↴ -width 2 -takefocus 0 \
  1436. -command [list [namespace current]::decode $window $game $locale]
  1437. ] -padx 2p -side left
  1438. set elements [list \
  1439. frame password $window.password \
  1440. button password.button $window.password.encode \
  1441. entry password.entry $window.password.entry \
  1442. button password.button $window.password.cycleoffset \
  1443. button password.button $window.password.decode
  1444. ]
  1445. set sublabellengths {}
  1446. foreach {key list} [info $game $locale] {
  1447. if {$list == ""} {
  1448. set col $columns
  1449. continue
  1450. }
  1451. if {[lindex $list 0] == "hidden"} {
  1452. if {[llength $list] < 2} {set list {hidden 0}}
  1453. set default [lindex $list 1]
  1454. if {[llength $default] > 1} {set default [dict get $defalut $region]}
  1455. set ::$window.$key $default
  1456. continue
  1457. }
  1458. if {[lindex $list 0] == "bits"} {
  1459. set col $columns
  1460. }
  1461. if {[incr col] >= $columns} {
  1462. set col 0
  1463. incr row
  1464. }
  1465. grid [ttk::label $window.$key\-label -anchor e \
  1466. -text [@ $locale labels $key]:
  1467. ] -row $row -column $col -sticky new
  1468. if {[lindex $list 0] != "bits"} {
  1469. bind $window.$key\-label <ButtonPress> [list focus $window.$key]
  1470. }
  1471. incr col
  1472. lappend elements label $key.label $window.$key\-label
  1473. switch -exact -- [lindex $list 0] string {
  1474. lassign [lrange $list 1 end] l default type
  1475. grid [ttk::entry $window.$key -validate [dict get $options -validate] \
  1476. -validatecommand [list [namespace current]::validatestring \
  1477. $window %W %V %s %P $l $default $locale $type] \
  1478. -font "sans [dict get $options -fontsize]"
  1479. ] -row $row -column $col -sticky new
  1480. if {[llength $default] > 1} {
  1481. set default [dict get $default $region]
  1482. }
  1483. $window.$key insert 0 $default
  1484. lappend elements entry string $window.$key
  1485. } int {
  1486. lassign [lrange $list 1 end] l default
  1487. if {$default == ""} {set default [lindex $l 0]}
  1488. if {[llength $l] == 1} {lappend l $l}
  1489. lappend l 1
  1490. grid [ttk::spinbox $window.$key \
  1491. -font "sans [dict get $options -fontsize]" \
  1492. -from [lindex $l 0] -to [lindex $l 1] -increment [lindex $l 2] \
  1493. -validate [dict get $options -validate] \
  1494. -validatecommand [list \
  1495. [namespace current]::validateint $window %W %V %s %P $default]
  1496. ] -row $row -column $col -sticky new
  1497. if {[llength $default] > 1} {set default [dict get $default $region]}
  1498. $window.$key set $default
  1499. lappend elements spinbox int $window.$key
  1500. } boolean {
  1501. set default [lindex $list 1]
  1502. grid [ttk::checkbutton $window.$key] -row $row -column $col -sticky new
  1503. $window.$key state !alternate
  1504. if {[llength $default] > 1} {set default [dict get $default $region]}
  1505. if {$default != "" && !!$default} {
  1506. $window.$key state selected
  1507. }
  1508. bind $window.$key\-label <ButtonPress> [list $window.$key invoke]
  1509. lappend elements checkbutton boolean $window.$key
  1510. } regionlist - list {
  1511. lassign [lrange $list 1 end] l default
  1512. if {[lindex $list 0] == "regionlist"} {set l [dict get $l $region]}
  1513. grid [ttk::combobox $window.$key -state readonly -values $l \
  1514. -font "sans [dict get $options -fontsize]" \
  1515. ] -row $row -column $col -sticky new
  1516. if {[llength $default] > 1} {set default [dict get $default $region]}
  1517. $window.$key current [expr {$default == "" ? 0 : $default}]
  1518. lappend elements combobox [lindex $list 0] $window.$key
  1519. } bits {
  1520. lassign [lrange $list 1 end] l default
  1521. if {[llength $default] > 1} {set default [dict get $default $region]}
  1522. grid [ttk::frame $window.$key -borderwidth 1] \
  1523. -column $col -row $row -columnspan [expr {$columns - $col}] -sticky new
  1524. set i 0
  1525. set x 0
  1526. set y 0
  1527. foreach k $l {
  1528. lappend elements label bits.label $window.$key.l$i
  1529. grid [ttk::label $window.$key.l$i -text $k: -anchor e \
  1530. -font "sans [dict get $options -fontsize]"] \
  1531. -column $x -row $y -sticky new
  1532. bind $window.$key.l$i <ButtonPress> [list $window.$key.$i invoke]
  1533. if {![dict exists $sublabellengths $x] ||
  1534. [dict get $sublabellengths $x] < [string length $k:]} {
  1535. dict set sublabellengths $x [string length $k:]
  1536. }
  1537. incr x
  1538. grid [ttk::checkbutton $window.$key.$i] \
  1539. -column $x -row $y -sticky new
  1540. $window.$key.$i state !alternate
  1541. if {$default != "" && ($default & 1 << $i) != 0} {
  1542. $window.$key.$i state selected
  1543. }
  1544. if {[incr x] >= $columns * 2} {
  1545. set x 0
  1546. incr y
  1547. }
  1548. lappend elements checkbutton bits.checkbutton $window.$key.$i
  1549. incr i
  1550. }
  1551. if {$x > 0} {
  1552. set x 0
  1553. incr y
  1554. }
  1555. grid $window.$key -rowspan $y
  1556. set col $columns
  1557. incr row [expr {$y - 1}]
  1558. lappend elements frame bits $window.$key
  1559. }
  1560. }
  1561. if {$col > 0} {
  1562. incr row
  1563. }
  1564. grid [ttk::label $window.error -text ✓] -column 0 -row $row -sticky sew \
  1565. -columnspan $columns
  1566. for {set col 1} {$col < $columns} {incr col 2} {
  1567. grid columnconfigure $window $col -weight 1
  1568. }
  1569. for {set col 0} {$col < $columns} {incr col} {
  1570. grid columnconfigure $window $col -pad 4p
  1571. }
  1572. for {set y 0} {$y < $row} {incr y} {
  1573. grid rowconfigure $window $y -pad 2p
  1574. }
  1575. bind $window.region-name <<ComboboxSelected>> \
  1576. [list [namespace current]::updateregion $window $game $locale]
  1577. foreach {type key w} $elements {
  1578. switch -exact -- $key bits {
  1579. dict for {col width} $sublabellengths {
  1580. foreach label [grid slaves $w -column $col] {
  1581. $label configure -width $width
  1582. }
  1583. }
  1584. }
  1585. foreach {k binding} [dict get $options -bind] {
  1586. if {[string equal -nocase $k all] ||
  1587. [string equal -nocase $k $type] ||
  1588. [string equal -nocase $k $key]} {
  1589. foreach {pat body} $binding {
  1590. bind $w $pat $body
  1591. }
  1592. }
  1593. }
  1594. foreach {k style} [dict get $options -style] {
  1595. if {[string equal -nocase $k all] ||
  1596. [string equal -nocase $k $type] ||
  1597. [string equal -nocase $k $key]} {
  1598. $w configure -style $style
  1599. }
  1600. }
  1601. }
  1602. return $window
  1603. }
  1604. # bok::ui::updateregion updates any region dependant entries/lists in $window
  1605. # based on the value of $window.region-name and $locale.
  1606. proc bok::ui::updateregion {window game {locale en}} {
  1607. set region [region [$window.region-name get] $locale]
  1608. if {$region == "??"} {
  1609. $window.timezone-name set [@ $locale timezones \#0]
  1610. $window.timezone-name configure state disabled
  1611. return
  1612. }
  1613. set lang [expr {$region == "jp" ? "jp" : "en"}]
  1614. set pass [decb64 [$window.password.entry get] $game]
  1615. modkeys pass $game $lang 6 {k v} region {
  1616. expr {$region == "jp" ? 1 : $region == "na" ? 2 : 3}
  1617. }
  1618. foreach {key list} [info $game $region $locale] {
  1619. switch -exact -- [lindex $list 0] regionlist {
  1620. $window.$key configure -values [dict get [lindex $list 1] $region]
  1621. if {[llength [lindex $list 2]] > 1} {
  1622. $window.$key current [dict get [lindex $list 2] $region]
  1623. } elseif {[lindex $list 2] != ""} {
  1624. $window.$key current [lindex $list 2]
  1625. } else {
  1626. $window.$key current 0
  1627. }
  1628. } string {
  1629. $window.$key validate
  1630. }
  1631. }
  1632. if {[llength $pass] > 0} {
  1633. $window.password.entry delete 0 end
  1634. $window.password.entry insert 0 [spaceout [
  1635. encb64 $pass $game $lang] $game $lang]
  1636. }
  1637. }
  1638. # bok::ui::validatestring validates the string in entry $window.
  1639. # $root is the frame initialized by bok::ui::init, $window is the entry, $type
  1640. # is the event type, $old is the old value, $new is the new value, $max is
  1641. # {max ?maxbytes?} which limits the length of the string, default is either a
  1642. # default string or a dictionary of default strings where the region in $root
  1643. # is used as the key, $locale is the locale to use for the region name, and
  1644. # $restrict if true restricts the character table to either 0x180 if the region
  1645. # is Japan, or 0 otherwise.
  1646. proc bok::ui::validatestring {root window type old {new {}} {max 0} {default {}}
  1647. {locale en} {restrict 0}} {
  1648. set region [region [$root.region-name get] $locale]
  1649. set tab [expr {
  1650. $restrict == "" || !$restrict ? "" : $region == "jp" ? 0x180 : 0
  1651. }]
  1652. if {[llength $max] > 2} {set max [dict get $max $region]}
  1653. if {[llength $max] == 1} {lappend max $max}
  1654. lassign $max max bytemax
  1655. switch -exact -- $type key {
  1656. set list [encstr $new $tab]
  1657. return [expr {
  1658. ($max <= 0 || [string length $new] <= $max) &&
  1659. ($bytemax <= 0 || [llength $list] <= $bytemax) &&
  1660. [decstr $list $tab] == $new
  1661. }]
  1662. } default {
  1663. if {$old != ""} {
  1664. set list [encstr $new $tab]
  1665. if {$bytemax > 0 && [llength $list] > $bytemax} {
  1666. set list [lrange $list 0 $bytemax-1]
  1667. }
  1668. set new [decstr $list $tab]
  1669. if {$max > 0 && [string length $new] > $max} {
  1670. set new [string range $new 0 $max-1]
  1671. }
  1672. if {$new == $old} {return 1}
  1673. $window delete 0 end
  1674. if {$new != ""} {
  1675. $window insert 0 $new
  1676. return 0
  1677. }
  1678. }
  1679. if {$default != ""} {
  1680. if {[llength $default] > 1} {set default [dict get $default $region]}
  1681. $window insert 0 $default
  1682. }
  1683. return 0
  1684. }
  1685. }
  1686. # bok::ui::validateint validates the value of spinbox $window is an integer.
  1687. # $root is the frame initialized by bok::ui::init $window is the spinbox, $type
  1688. # is the event type, $old is the old value, $new is the new value, and $default
  1689. # is the default value.
  1690. proc bok::ui::validateint {root window type old {new {}} {default {}}} {
  1691. switch -exact -- $type key {
  1692. if {![string is integer $new]} {
  1693. return 0
  1694. } elseif {$new != "" && $new < [$window cget -from]} {
  1695. $window set [expr {int([$window cget -from])}]
  1696. return 0
  1697. } elseif {$new != "" && $new > [$window cget -to]} {
  1698. $window set [expr {int([$window cget -to])}]
  1699. return 0
  1700. }
  1701. return 1
  1702. } default {
  1703. if {$old == "" || ![string is integer $old]} {
  1704. if {![string equal $old $default]} {
  1705. $window set $default
  1706. }
  1707. return 0
  1708. }
  1709. return 1
  1710. }
  1711. }
  1712. # bok::ui::encode encodes the widget values into the password entry.
  1713. # $window is the window initialized with bok::ui::init, $game is the game that
  1714. # $window is for, and $locale is the locale of bok::strings to use.
  1715. proc bok::ui::encode {window game {locale en}} {
  1716. if {$game < 1 || $game > 4} {
  1717. return -code error \
  1718. {wrong args: should be "encode path game ?locale?"}
  1719. }
  1720. set region [region [$window.region-name get] $locale]
  1721. set info [info $game $region $locale]
  1722. set dict {}
  1723. foreach {key list} $info {
  1724. switch -exact -- [lindex $list 0] {
  1725. {} {continue}
  1726. string - int - regionlist - list {
  1727. dict set dict $key [$window.$key get]
  1728. } boolean {
  1729. dict set dict $key [expr {"selected" in [$window.$key state]}]
  1730. } bits {
  1731. dict set dict $key {}
  1732. for {set i 0} {$i < [llength [lindex $list 1]]} {incr i} {
  1733. if {"selected" in [$window.$key.$i state]} {
  1734. dict lappend dict $key [lindex $list 1 $i]
  1735. }
  1736. }
  1737. } hidden {
  1738. dict set dict $key [set ::$window.$key]
  1739. }}
  1740. }
  1741. $window.error configure -text ✓
  1742. if {[catch {normalize $dict $locale} error]} {
  1743. $window.error configure -text "⚠ $error"
  1744. }
  1745. set dict [normalize $dict $locale 1]
  1746. if {[catch {set pass [encpass $dict]} error]} {
  1747. $window.error configure -text "⚠ $error"
  1748. return
  1749. }
  1750. switch -exact -- [dict get $dict game] 1 {
  1751. if {[dict get $dict minutes] == 0 && [dict get $dict hours] == 0} {
  1752. $window.error configure \
  1753. -text "⚠ [@ $locale labels hours] or [
  1754. @ $locale labels minutes] should be set"
  1755. } elseif {[dict get $dict minutes] >= 60} {
  1756. $window.error configure \
  1757. -text "⚠ [@ $locale labels minutes] should be less than 60"
  1758. } elseif {[dict get $dict clears] == 0} {
  1759. $window.error configure \
  1760. -text "⚠ [@ $locale labels clears] should be greater than 0"
  1761. }
  1762. } 2 {
  1763. if {([dict get $dict titles] & (1 << 6)) == 0} {
  1764. $window.error configure \
  1765. -text "⚠ [@ $locale titles 2 \#6] should be set"
  1766. }
  1767. } 3 {
  1768. } 4 {
  1769. if {[dict get $dict name-solar] == ""} {
  1770. $window.error configure \
  1771. -text "⚠ [@ $locale labels name-solar] should be set"
  1772. } elseif {[dict get $dict name-dark] == ""} {
  1773. $window.error configure \
  1774. -text "⚠ [@ $locale labels name-dark] should be set"
  1775. }
  1776. }
  1777. if {[dict exists $dict name] && [dict get $dict name] == ""} {
  1778. $window.error configure -text "⚠ [@ $locale labels name] should be set"
  1779. }
  1780. $window.password.entry delete 0 end
  1781. $window.password.entry insert 0 [spaceout $pass]
  1782. }
  1783. # bok::ui::decode decodes the current password into the appropriate widgets.
  1784. # $window is the window initialized with bok::ui::init, $game is the game that
  1785. # $window is for, and $locale is the locale of bok::strings to use.
  1786. proc bok::ui::decode {window game {locale en}} {
  1787. set region [region [$window.region-name get] $locale]
  1788. set lang [expr {$region == "jp" ? "jp" : "en"}]
  1789. if {[catch {
  1790. set dict [decpass [$window.password.entry get] $game $lang]
  1791. } error]} {
  1792. $window.error configure -text "⚠ $error"
  1793. return
  1794. }
  1795. if {[string match *!=* [dict get $dict checksum]]} {
  1796. $window.error configure -text "⚠ checksum [dict get $dict checksum]"
  1797. } elseif {[catch {normalize $dict $locale} error]} {
  1798. $window.error configure -text "⚠ $error"
  1799. } else {
  1800. $window.error configure -text ✓
  1801. }
  1802. set dict [normalize $dict $locale 1]
  1803. foreach {key list} [info $game $region $locale] {
  1804. switch -exact -- [lindex $list 0] {} {continue} string {
  1805. $window.$key delete 0 end
  1806. $window.$key insert 0 [dict get $dict $key]
  1807. } int - regionlist - list {
  1808. $window.$key set [dict get $dict $key]
  1809. } boolean {
  1810. $window.$key state "[expr {[dict get $dict $key] ? "" : "!"}]selected"
  1811. } bits {
  1812. for {set i 0} {$i < [llength [lindex $list 1]]} {incr i} {
  1813. $window.$key.$i state "[expr {
  1814. [lindex $list 1 $i] in [dict get $dict $key] ? "" : "!"
  1815. }]selected"
  1816. }
  1817. } hidden {
  1818. set ::$window.$key [dict get $dict $key]
  1819. }
  1820. }
  1821. }
  1822. # bok::ui::cycleoffset increments the offset bit in the current password by
  1823. # $count, if it is present. $window is the window initialized with
  1824. # bok::ui::init and $game is the game that $window is for.
  1825. proc bok::ui::cycleoffset {window game {count 1}} {
  1826. set list [decb64 [$window.password.entry get] $game]
  1827. set ok 0
  1828. modkeys list $game jp 6 {k v n} offset region {
  1829. if {$k == "region"} {set ok 1; return}
  1830. expr {$v + $count & (1 << $n) - 1}
  1831. }
  1832. if {!$ok} {return}
  1833. $window.password.entry delete 0 end
  1834. $window.password.entry insert 0 [spaceout [encb64 $list $game]]
  1835. }
  1836. # bok::ui::keyboards is a dictionary of lists. The lists contain lists of rows
  1837. # of keys, with the keys being in the form {display ?value? ?expand? ?width?}.
  1838. # The value defaults to the same as the display value, if the display is empty
  1839. # then that button is skipped, if "expand" is present then that key is expanded
  1840. # to take up horizontal space, and if "expand" has a width then that key is
  1841. # expanded to the specified width.
  1842. set bok::ui::keyboards {en64 {
  1843. {B C D F G H J @ # ^ {⌫ <<DeletePrevChar>>} {⌦ <<DeleteNextChar>>}}
  1844. {K L M N P Q R 0 1 2 3 4}
  1845. {S T V W X Y Z 5 6 7 8 9}
  1846. {b c d f g h j + - / .}
  1847. {k l m n p q r = _ ? {← <<PrevChar>>} {→ <<NextChar>>}}
  1848. {s t v w x y z : > ! {␣ { } expand}}
  1849. } lk64 {
  1850. {1 2 3 4 5 6 7 8 9 0 . {⌫ <<DeletePrevChar>>} {⌦ <<DeleteNextChar>>}}
  1851. {A B C D E F G H I J K L M}
  1852. {N O P Q R S T U V W X Y Z}
  1853. {a b c d e f g h i j k l m}
  1854. {n o p q r s t u v w x y z}
  1855. {? = {␣ { } expand} {← <<PrevChar>>} {→ <<NextChar>>}}
  1856. } jp64 {
  1857. {あ い う え お ま み む め も {⌫ <<DeletePrevChar>>} {⌦ <<DeleteNextChar>>}}
  1858. {か き く け こ や ゆ よ わ を ば び}
  1859. {さ し す せ そ ら り る れ ろ ぶ べ}
  1860. {た ち つ て と が ぎ ぐ げ ご ぼ}
  1861. {な に ぬ ね の ざ じ ず ぜ ぞ {← <<PrevChar>>} {→ <<NextChar>>}}
  1862. {は ひ ふ へ ほ だ ぢ づ で ど {␣ { } expand}}
  1863. } en {
  1864. {A B C D E F G H I {⌫ <<DeletePrevChar>>} {⌦ <<DeleteNextChar>>}}
  1865. {J K L M N O P Q R . ,}
  1866. {S T U V W X Y Z {} ' -}
  1867. {a b c d e f g h i /}
  1868. {j k l m n o p q r {← <<PrevChar>>} {→ <<NextChar>>}}
  1869. {s t u v w x y z {} {␣ { } expand}}
  1870. } ext {
  1871. {& * ; + - × ÷ = {⌫ <<DeletePrevChar>>} {⌦ <<DeleteNextChar}}
  1872. {à á â ä è é ê ë ( )}
  1873. {ì í î ï ò ó ô ö [ ]}
  1874. {ù ú û ü ç ñ œ ß ! ?}
  1875. {À Á Â Ä È É Ê Ë ¡ ¿}
  1876. {Ì Í Î Ï Ò Ó Ô Ö {← <<PrevChar>>} {→ <<NextChar>>}}
  1877. {Ù Ú Û Ü Ç Ñ Œ {⋅ ⋅} {␣ { } expand}}
  1878. } hiri {
  1879. {あ い う え お ら り る れ ろ {⌫ <<DeletePrevChar>>} {⌦ <<DeleteNextChar>>}}
  1880. {か き く け こ が ぎ ぐ げ ご ん ・}
  1881. {さ し す せ そ ざ じ ず ぜ ぞ ー ~}
  1882. {た ち つ て と だ ぢ づ で ど}
  1883. {な に ぬ ね の ば び ぶ べ ぼ}
  1884. {は ひ ふ へ ほ ぱ ぴ ぷ ぺ ぽ}
  1885. {ま み む め も ぁ ぃ ぅ ぇ ぉ {← <<PrevChar>>} {→ <<NextChar>>}}
  1886. {や ゆ よ わ を っ ゃ ゅ ょ {} {␣ { } expand}}
  1887. } kata {
  1888. {ア イ ウ エ オ ラ リ ル レ ロ {⌫ <<DeletePrevChar>>} {⌦ <<DeleteNextChar>>}}
  1889. {カ キ ク ケ コ ガ ギ グ ゲ ゴ ン ・}
  1890. {サ シ ス セ ソ ザ ジ ズ ゼ ゾ ー ~}
  1891. {タ チ ツ テ ト ダ ヂ ヅ デ ド}
  1892. {ナ ニ ヌ ネ ノ バ ビ ブ ベ ボ}
  1893. {ハ ヒ フ ヘ ホ パ ピ プ ペ ポ}
  1894. {マ ミ ム メ モ ァ ィ ゥ ェ ォ {← <<PrevChar>>} {→ <<NextChar>>}}
  1895. {ヤ ユ ヨ ワ ヲ ッ ャ ュ ョ ヴ {␣ { } expand}}
  1896. }}
  1897. # bok::ui::keyboardinit initializes a grid of buttons that send
  1898. # <<VirtualKeyPress>> events to the frame $window with %d set to the value.
  1899. # The default binding is to call bok::ui::keypress with %d. Supported options
  1900. # are -style and -command. -style sets the style to use for the buttons, and
  1901. # -command replaces the default binding for <<VirtualKeyPress>>.
  1902. proc bok::ui::keyboardinit {window keyboard args} {
  1903. variable keyboards
  1904. set usage {keyboardinit path keyboard ?option value ...?}
  1905. set options [dict create -style {} \
  1906. -command [list [namespace current]::keypress %W %d]
  1907. ]
  1908. if {[llength $args] % 2 != 0} {
  1909. return -code error "wrong # args: should be \"$usage\""
  1910. }
  1911. if {![winfo exists $window]} {
  1912. ttk::frame $window
  1913. } elseif {[winfo exists $window.0]} {
  1914. return
  1915. }
  1916. foreach {opt arg} $args {
  1917. switch -exact -- $opt -keys - -command {
  1918. dict set options $opt $arg
  1919. } -style {
  1920. dict lappend options $opt {*}$arg
  1921. } default {
  1922. return -code error "Invalid option: $opt"
  1923. }
  1924. }
  1925. if {[dict exists $keyboards $keyboard]} {
  1926. set keyboard [dict get $keyboards $keyboard]
  1927. }
  1928. bind $window <<VirtualKeyPress>> [dict get $options -command]
  1929. set einfo {}
  1930. set winfo {}
  1931. foreach list $keyboard {
  1932. set e 0
  1933. set j 0
  1934. foreach key $list {
  1935. set width 1
  1936. foreach opt [lrange $key 2 end] {
  1937. if {[lindex $opt 0] != "expand"} {continue}
  1938. set width [lindex $opt 1]
  1939. }
  1940. if {$width == ""} {
  1941. set width 1
  1942. incr e
  1943. }
  1944. incr j $width
  1945. }
  1946. lappend einfo $e
  1947. lappend winfo $j
  1948. }
  1949. set max [::tcl::mathfunc::max {*}$winfo]
  1950. set button 0
  1951. for {set row 0} {$row < [llength $keyboard]} {incr row} {
  1952. set list [lindex $keyboard $row]
  1953. set n [lindex $einfo $row]
  1954. set e [expr {max($max - [lindex $winfo $row] + $n - 1, 0)}]
  1955. set elist {}
  1956. if {$n > 0} {
  1957. for {set i 0; set j [expr {int(($n - $e % $n) / 2)}]} {$i < $n} {incr i} {
  1958. lappend elist [expr {
  1959. int($e / $n) + ($i >= $j && $i < $n - $j ? 1 : 0)
  1960. }]
  1961. }
  1962. }
  1963. for {set e 0; set i 0; set col 0} {$i < [llength $list]} {incr i} {
  1964. set key [lindex $list $i]
  1965. if {$key == ""} {
  1966. incr col
  1967. continue
  1968. }
  1969. if {[llength $key] < 2} {
  1970. set text $key
  1971. set value $key
  1972. } else {
  1973. lassign $key text value
  1974. }
  1975. set width 1
  1976. foreach opt [lrange $key 2 end] {
  1977. switch -nocase -exact -- [lindex $opt 0] expand {
  1978. set width [lindex $opt 1]
  1979. }
  1980. }
  1981. if {$width == ""} {
  1982. set width [lindex $elist $e]
  1983. incr e
  1984. }
  1985. grid [ttk::button $window.$button -text $text -command \
  1986. [list event generate $window <<VirtualKeyPress>> -data $value \
  1987. -root [winfo toplevel $window] -subwindow $window.$button] \
  1988. -style [dict get $options -style] -width 2 -takefocus 0
  1989. ] -ipadx 0 -row $row -column $col -columnspan $width -sticky nsew
  1990. incr button
  1991. incr col $width
  1992. }
  1993. }
  1994. for {set row 0} {$row < [llength $keyboard]} {incr row} {
  1995. grid rowconfigure $window $row -weight 1
  1996. }
  1997. for {set col 0} {$col < $max} {incr col} {
  1998. grid columnconfigure $window $col -weight 1
  1999. }
  2000. return $window
  2001. }
  2002. # bok::ui::keypress inserts $value into Entry/TEntry currently in focus for the
  2003. # toplevel window for $window. If value is a virtual event then that action is
  2004. # performed instead. Supported virtual events are PrevChar, NextChar,
  2005. # DeletePrevChar, and DeleteNextChar.
  2006. proc bok::ui::keypress {window value} {
  2007. set window [focus -lastfor [winfo toplevel $window]]
  2008. switch -glob -nocase -- [winfo class $window] *Entry {
  2009. switch -glob -nocase -- $value {
  2010. <<PrevChar>> {$window icursor [expr {[$window index i] - 1}]}
  2011. <<NextChar>> {$window icursor [expr {[$window index i] + 1}]}
  2012. <<DeletePrevChar>> {$window delete [expr {[$window index i] - 1}]}
  2013. <<DeleteNextChar>> {$window delete i}
  2014. <<*>> {return -code error "Unsupported event: $value"}
  2015. default {
  2016. $window insert i $value
  2017. }}
  2018. }
  2019. }
  2020. # bok::ui::keyboardallow disables/enables all keyboards in $window depending on
  2021. # if they are in $keyboards. The tab is set to the default (or first valid
  2022. # keyboard) if the current keyboard is not in $keyboards.
  2023. proc bok::ui::keyboardallow {window keyboards {default ""}} {
  2024. if {$default == "" || $default ni $keyboards} {
  2025. set default [lindex $keyboards 0]
  2026. }
  2027. set current [winfo name [$window select]]
  2028. foreach keyboard [$window tabs] {
  2029. set name [winfo name $keyboard]
  2030. $window tab $keyboard \
  2031. -state [expr {$name in $keyboards ? "normal" : "disabled"}]
  2032. if {$name == $default && $current ni $keyboards} {
  2033. $window select $keyboard
  2034. }
  2035. }
  2036. }
  2037. proc usage {} {
  2038. puts stderr [join {
  2039. {usage: bokpass [-gui] [-kbd] [-invalid] [-columns columns]}
  2040. { [-fontsize size]}
  2041. { bokpass [-cli] [-verbose] -game game password ...}
  2042. } "\n"]
  2043. exit 1
  2044. }
  2045. set options {
  2046. -locale en
  2047. -mode cli -verbose 0
  2048. -columns 2 -fontsize 10 -kbd 0 -validate all
  2049. }
  2050. for {set optind 0} {$optind < [llength $argv]} {incr optind} {
  2051. switch -glob -- [lindex $argv $optind] -game - -columns - -fontsize {
  2052. if {$optind + 1 >= [llength $argv]} {usage}
  2053. dict set options {*}[lrange $argv $optind $optind+1]
  2054. incr optind
  2055. } -cli - -gui {
  2056. dict set options -mode [string range [lindex $argv $optind] 1 end]
  2057. } -verbose - -kbd {
  2058. dict set options [lindex $argv $optind] 1
  2059. } -invalid {
  2060. dict set options -validate none
  2061. } -- {
  2062. incr optind
  2063. break
  2064. } -?* {
  2065. usage
  2066. } default {
  2067. break
  2068. }
  2069. }
  2070. if {$optind == [llength $argv]} {
  2071. dict set options -mode gui
  2072. }
  2073. if {[dict get $options -mode] == "cli" && ![dict exists $options -game]} {
  2074. usage
  2075. }
  2076. switch -exact -- [dict get $options -mode] gui {
  2077. package require Tk
  2078. wm title . bokpass
  2079. ttk::style configure Keyboard.TButton \
  2080. -font "mono [expr {int([dict get $options -fontsize] * 1.2)}]"
  2081. ttk::style configure TLabel \
  2082. -font "sans [dict get $options -fontsize]"
  2083. ttk::style configure TNotebook.Tab \
  2084. -font "sans [dict get $options -fontsize]"
  2085. ttk::style configure TSpinBox -font "sans [dict get $options -fontsize]"
  2086. ttk::style configure TComboBox -font "sans [dict get $options -fontsize]"
  2087. option add *TCombobox*Listbox.font "sans [dict get $options -fontsize]"
  2088. # Adds ^W and ^U functionality to Entry and TEntry widgets.
  2089. proc betterediting {args} {
  2090. foreach arg $args {
  2091. bind $arg <Control-KeyPress-w> {
  2092. apply {old {
  2093. event generate %W <<PrevWord>>
  2094. %W delete [%W index i] $old
  2095. }} [%W index i]
  2096. }
  2097. bind $arg <Control-KeyPress-u> {
  2098. %W delete 0 end
  2099. }
  2100. bind $arg <KeyPress-Up> {%W icursor 0}
  2101. bind $arg <KeyPress-Down> {%W icursor e}
  2102. }
  2103. }
  2104. betterediting Entry TEntry
  2105. # Lazily initializes game tabs.
  2106. proc loadgametab {window} {
  2107. if {[dict get $::options -kbd]} {
  2108. foreach keyboard [.keyboard tabs] {
  2109. .keyboard tab $keyboard -state normal
  2110. }
  2111. }
  2112. if {[winfo exists $window.password]} {return}
  2113. if {![string match {*.*[1234]} $window]} {return}
  2114. set game [string index $window end]
  2115. set bindings {}
  2116. if {[dict get $::options -kbd]} {
  2117. lappend bindings password.entry {<FocusIn> {+
  2118. if {[bok::ui::region \
  2119. [[winfo parent [winfo parent %W]].region-name get] \
  2120. [dict get $::options -locale]] == "jp"} {
  2121. bok::ui::keyboardallow .keyboard jp64
  2122. } elseif {[string index [.games select] end] == "4"} {
  2123. bok::ui::keyboardallow .keyboard lk64
  2124. } else {
  2125. bok::ui::keyboardallow .keyboard en64
  2126. }
  2127. }} string {<FocusIn> {+
  2128. switch -glob -- "[bok::ui::region [[winfo parent %W].region-name get] \
  2129. [dict get $::options -locale]][string index [.games select] end]" {
  2130. jp[123] {
  2131. bok::ui::keyboardallow .keyboard {hiri kata} hiri
  2132. } jp4 {
  2133. bok::ui::keyboardallow .keyboard {en ext hiri kata} hiri
  2134. } na[123] - eu[123] {
  2135. bok::ui::keyboardallow .keyboard {en}
  2136. } na4 - eu4 {
  2137. bok::ui::keyboardallow .keyboard {en ext hiri kata} en
  2138. }}
  2139. }} entry {<FocusOut> {
  2140. bok::ui::keyboardallow .keyboard {en64 lk64 jp64 en ext kata hiri}
  2141. }}
  2142. }
  2143. if {$game != 3} {
  2144. switch -exact -- [dict get $::options -locale] {
  2145. en {set region na}
  2146. jp {set region jp}
  2147. default {set region eu}
  2148. }
  2149. } else {
  2150. set region jp
  2151. }
  2152. set options [dict map {key value} $::options {
  2153. if {$key ni {-fontsize -columns -validate}} {continue}
  2154. set value
  2155. }]
  2156. bok::ui::init $window $region $game -bind $bindings {*}$options
  2157. }
  2158. pack [ttk::notebook .games] -expand 1 -fill both
  2159. bind .games <<NotebookTabChanged>> {loadgametab [%W select]}
  2160. foreach {tab name} [bok::@ [dict get $options -locale] labels game-tabs] {
  2161. .games add [ttk::frame .games.$tab] -text $name
  2162. }
  2163. if {[dict get $options -kbd]} {
  2164. # Lazily initializes keyboard tabs.
  2165. pack [ttk::notebook .keyboard -takefocus 0] -fill both
  2166. bind .keyboard <<NotebookTabChanged>> {
  2167. if {![winfo exists [%W select].0]} {
  2168. bok::ui::keyboardinit [%W select] [winfo name [%W select]] \
  2169. -style Keyboard.TButton
  2170. }
  2171. }
  2172. bind Entry <FocusIn> {+ set ::currententry %W}
  2173. bind TEntry <FocusIn> {+ set ::currententry %W}
  2174. bind .games <<NotebookTabChanged>> {+ set ::currententry {}}
  2175. bind .keyboard <<NotebookTabChanged>> {+
  2176. if {[winfo exists $::currententry]} {focus $::currententry}
  2177. }
  2178. foreach {tab name} \
  2179. [bok::@ [dict get $options -locale] labels keyboard-tabs] {
  2180. .keyboard add [ttk::frame .keyboard.$tab] -text $name
  2181. }
  2182. }
  2183. } cli {
  2184. # Forgivingly match -game.
  2185. switch -exact -nocase -- [regsub {[^[:alnum:]]+} \
  2186. [dict get $options -game] {}] {
  2187. boktai - tsiiyh - boktai1 - boktaitsiiyh - boktaithesunisinyourhand -
  2188. bokura - bokura1 - bokuranotaiyou - bokuranotaiyou1 -
  2189. jp1 - en1 - na1 - eu1 - us1 - 1 {
  2190. dict set options -game 1
  2191. }
  2192. zoktai - boktai2 - boktai2solarboydjango - solarboydjango -
  2193. zokubokura - zokubokuranotaiyou - bokura2 - bokuranotaiyou2 -
  2194. jp2 - en2 - na2 - eu2 - us2 - 2 {
  2195. dict set options -game 2
  2196. }
  2197. shinbok - boktai3 - boktai3sabatascounterattack - sabatascounterattack -
  2198. shinbokura - shinbokuranotaiyou - bokura3 - bokuranotaiyou3 -
  2199. jp3 - en3 - na3 - eu3 - us3 - 3 {
  2200. dict set options -game 3
  2201. }
  2202. lk - lunarknigths - boktai4 - boktaids - bokura4 - bokurads -
  2203. bokuranotaiyouds - bokuranotaiyou4 - boktaidjangosabata - bokuradjangosabata -
  2204. bokuranotaiyoudjangosabata - djangosabata - ds -
  2205. jp4 - en4 - na4 - eu4 - us4 - 4 {
  2206. dict set options -game 4
  2207. } default {
  2208. puts stderr "Unknown game: [dict get $options -game]"
  2209. exit 1
  2210. }}
  2211. # Decodes passwords for -game and print them in columns.
  2212. set dict [dict create]
  2213. for {} {$optind < [llength $argv]} {incr optind} {
  2214. if {[catch {
  2215. set width [::tcl::mathfunc::max 0 {*}[lmap value [dict values $dict] {
  2216. string length $value
  2217. }]]
  2218. set tdict [bok::decpass [lindex $argv $optind] \
  2219. [dict get $options -game]]
  2220. set vdict {}
  2221. catch {set vdict [bok::normalize $tdict [dict get $options -locale] 1]}
  2222. dict for {key value} $tdict {
  2223. set pad {}
  2224. if {[dict exists $dict $key]} {
  2225. set pad [string repeat { } [expr {
  2226. $width - [string length [dict get $dict $key]]
  2227. }]]
  2228. }
  2229. if {[dict exists $vdict $key]} {
  2230. set value [dict get $vdict $key]
  2231. }
  2232. if {[dict get $options -verbose] && [dict exists $vdict $key\-name]} {
  2233. append value " \"[dict get $vdict $key\-name]\""
  2234. }
  2235. dict append dict $key " $pad$value"
  2236. }
  2237. } error]} {
  2238. puts stderr "bokpass: $error"
  2239. }
  2240. }
  2241. set width [::tcl::mathfunc::max 0 {*}[lmap key [dict keys $dict] {
  2242. string length $key
  2243. }]]
  2244. dict for {key value} $dict {
  2245. set pad [string repeat { } [expr {$width - [string length $key]}]]
  2246. puts $pad$key:$value
  2247. }
  2248. }