Stack bytecode. Heavily modified based on someone else's BGI script tools. See also BGI (old)
1 #!/usr/bin/env python
2
3 from sys import argv, exit, stdout
4 from mmap import mmap
5 from struct import unpack, iter_unpack
6
7 def print(s, end="\n"):
8 stdout.buffer.write((str(s) + end).encode('utf-8'))
9
10 #stdout = open(stdout.name, 'w', newline='\n', encoding='utf-8')
11
12 #documented (poorly) by bgiop.py
13 opcodes = {
14 #-1 = unknown
15 # push : number of elements pushed to the stack
16 # pull: number of elements pulled from the stack
17 # all numbers for push/pull are currently guesses
18 0x0000:{"arguments":1, "istext": 0, "islabel":-1, "push": 1, "pull": 0},
19 0x0001:{"arguments":1, "istext": 0, "islabel":-1, "push": 1, "pull": 0},
20 0x0002:{"arguments":1, "istext": 0, "islabel":-1, "push": 1, "pull": 0},
21 0x0003:{"arguments":1, "istext": 1, "islabel": 0, "push": 1, "pull": 0},
22 #0x0004:{"arguments":1, "istext":-1, "islabel":-1, "push":-1, "pull":-1}, # new operation
23 #0x0005:{"arguments":1, "istext":-1, "islabel":-1, "push":-1, "pull":-1}, # new operation
24 #0x0006:{"arguments":1, "istext":-1, "islabel":-1, "push":-1, "pull":-1}, # new operation
25 #0x0007:{"arguments":1, "istext":-1, "islabel":-1, "push":-1, "pull":-1}, # new operation
26 0x0008:{"arguments":1, "istext": 0, "islabel": 0, "push": 0, "pull": 0},
27 0x0009:{"arguments":1, "istext": 0, "islabel": 0, "push": 0, "pull": 0},
28 0x000A:{"arguments":1, "istext": 0, "islabel": 0, "push": 0, "pull": 0},
29 #0x000B:{"arguments":1, "istext":-1, "islabel":-1, "push":-1, "pull":-1}, # new operation
30 #0x000C:{"arguments":1, "istext":-1, "islabel":-1, "push":-1, "pull":-1}, # new operation
31 #0x000D:{"arguments":1, "istext":-1, "islabel":-1, "push":-1, "pull":-1}, # new operation
32 #0x000E:{"arguments":1, "istext":-1, "islabel":-1, "push":-1, "pull":-1}, # new operation
33 #0x000F:{"arguments":1, "istext":-1, "islabel":-1, "push":-1, "pull":-1}, # new operation
34 0x0010:{"arguments":0, "istext": 0, "islabel": 0, "push": 0, "pull": 0},
35 0x0011:{"arguments":0, "istext": 0, "islabel": 0, "push": 0, "pull": 0},
36 #0x0012:{"arguments":1, "istext":-1, "islabel":-1, "push":-1, "pull":-1}, # new operation
37 #0x0013:{"arguments":1, "istext":-1, "islabel":-1, "push":-1, "pull":-1}, # new operation
38 #0x0014:{"arguments":1, "istext":-1, "islabel":-1, "push":-1, "pull":-1}, # new operation
39 #0x0015:{"arguments":1, "istext":-1, "islabel":-1, "push":-1, "pull":-1}, # new operation
40 #0x0016:{"arguments":1, "istext":-1, "islabel":-1, "push":-1, "pull":-1}, # new operation
41 #0x0017:{"arguments":1, "istext":-1, "islabel":-1, "push":-1, "pull":-1}, # new operation
42 0x0018:{"arguments":0, "istext": 0, "islabel": 1, "push": 0, "pull": 1},
43 0x0019:{"arguments":1, "istext": 0, "islabel":-1, "push":-1, "pull": 0},
44 0x001A:{"arguments":0, "istext": 0, "islabel": 1, "push":-1, "pull": 1},
45 0x001B:{"arguments":0, "istext": 0, "islabel": 1, "push":-1, "pull":-1},
46 #0x001C:{"arguments":0, "istext":-1, "islabel":-1, "push":-1, "pull":-1}, # executes something from the stack
47 # 001D
48 0x001E:{"arguments":0, "istext": 0, "islabel":-1, "push":-1, "pull":-1},
49 0x001F:{"arguments":0, "istext": 0, "islabel":-1, "push":-1, "pull":-1},
50 0x0020:{"arguments":0, "istext": 0, "islabel":-1, "push":-1, "pull":-1},
51 0x0021:{"arguments":0, "istext": 0, "islabel":-1, "push":-1, "pull":-1},
52 0x0022:{"arguments":0, "istext": 0, "islabel":-1, "push":-1, "pull":-1},
53 0x0023:{"arguments":0, "istext": 0, "islabel":-1, "push":-1, "pull":-1},
54 0x0024:{"arguments":0, "istext": 0, "islabel":-1, "push":-1, "pull":-1},
55 0x0025:{"arguments":0, "istext": 0, "islabel":-1, "push":-1, "pull":-1},
56 0x0026:{"arguments":0, "istext": 0, "islabel":-1, "push":-1, "pull":-1},
57 0x0027:{"arguments":0, "istext": 0, "islabel":-1, "push":-1, "pull":-1},
58 0x0028:{"arguments":0, "istext": 0, "islabel":-1, "push":-1, "pull":-1},
59 0x0029:{"arguments":0, "istext": 0, "islabel":-1, "push":-1, "pull":-1},
60 0x002A:{"arguments":0, "istext": 0, "islabel":-1, "push":-1, "pull":-1},
61 0x002B:{"arguments":0, "istext": 0, "islabel":-1, "push":-1, "pull":-1},
62 #002C...002F
63 0x0030:{"arguments":0, "istext": 0, "islabel":-1, "push":-1, "pull":-1},
64 0x0031:{"arguments":0, "istext": 0, "islabel":-1, "push":-1, "pull":-1},
65 0x0032:{"arguments":0, "istext": 0, "islabel":-1, "push":-1, "pull":-1},
66 0x0033:{"arguments":0, "istext": 0, "islabel":-1, "push":-1, "pull":-1},
67 0x0034:{"arguments":0, "istext": 0, "islabel":-1, "push":-1, "pull":-1},
68 0x0035:{"arguments":0, "istext": 0, "islabel":-1, "push":-1, "pull":-1},
69 0x0038:{"arguments":0, "istext": 0, "islabel":-1, "push":-1, "pull":-1},
70 0x0039:{"arguments":0, "istext": 0, "islabel":-1, "push":-1, "pull":-1},
71 0x003A:{"arguments":0, "istext": 0, "islabel":-1, "push":-1, "pull":-1},
72 #003B...003E
73 0x003F:{"arguments":1, "istext": 0, "islabel":-1, "push":-1, "pull":-1},
74 0x007E:{"arguments":1, "istext": 1, "islabel": 0, "push": 1, "pull":-1},
75 0x007F:{"arguments":2, "istext": 1, "islabel": 0, "push":-1, "pull":-1},
76
77 # unsorted new
78 #0x0140:{"arguments":0, "istext":-1, "islabel":-1, "push":-1, "pull":-1}, # command for dialogue, narration
79 #0x01A9:{"arguments":0, "istext":-1, "islabel":-1, "push":-1, "pull":-1}, # command for voiceover
80
81 # used in CardSelect.dat after a 0x0002 operation, definitely real operations, number of arguments uncertain
82 #0x0098:{"arguments":0, "istext":-1, "islabel":-1, "push":-1, "pull":-1},
83 #0x0090:{"arguments":0, "istext":-1, "islabel":-1, "push":-1, "pull":-1},
84 #0x0091:{"arguments":0, "istext":-1, "islabel":-1, "push":-1, "pull":-1},
85
86 # bogus?
87 0x007B:{"arguments":3, "istext":-1, "islabel":-1, "push":-1, "pull":-1}, # certainly wrong but fixes a lot of problems without adding shitloads of operators
88 #0x00FE:{"arguments":2, "istext":-1, "islabel":-1, "push":-1, "pull":-1}, # new operation
89 #0x0090:{"arguments":0, "istext":-1, "islabel":-1, "push":-1, "pull":-1},
90 #0x0091:{"arguments":0, "istext":-1, "islabel":-1, "push":-1, "pull":-1},
91 #0x0098:{"arguments":0, "istext":-1, "islabel":-1, "push":-1, "pull":-1},
92 #0x011B:{"arguments":0, "istext":-1, "islabel":-1, "push":-1, "pull":-1},
93 #0x0127:{"arguments":0, "istext":-1, "islabel":-1, "push":-1, "pull":-1},
94 0xDBDB:{} # bogus ending operation to keep the list clean
95 }
96
97 # clipped from bgiop.py
98 def make_ops():
99 for op in range(0x800):
100 if op not in opcodes:
101 opcodes[op] = {"arguments":0, "istext":-1, "islabel":-1, "push":-1, "pull":-1}
102 # if op < 0x18:
103 # opcodes[op]["arguments"] = 1
104
105 make_ops()
106
107 def uint(x):
108 return unpack("<L", x)[0]
109
110 def short(x):
111 return "0x%04X" % x
112 def long(x):
113 return "0x%08X" % x
114
115 if len(argv) < 2:
116 print("No input given")
117 exit()
118
119 #print(argv[1])
120
121 first = True
122 for fn in argv:
123 if first:
124 first = False
125 continue
126 else:
127 #print("\nFile:")
128 #print(fn)
129 with open(fn, "r+b") as f:
130 memory = mmap(f.fileno(), 0)
131
132 data = f.read(0x1C)
133 if data.decode("sjis") != "BurikoCompiledScriptVer1.00\0":
134 print("Error: input is not a valid supported script file")
135 exit()
136 data = f.read(0x04)
137
138 header_length = uint(data)-0x04 # ?
139
140 num_defines = uint(f.read(0x04))
141
142 header = f.read(header_length-0x04)
143 defines = []
144
145 last_end = 0
146 for i in range(num_defines):
147 end = header.find(b'\x00', last_end)
148 string = header[last_end:end].decode("sjis")
149 defines.append(string)
150 last_end = end+1
151
152 real_beginning = header_length+0x04+0x1C # beginning in terms of address indexing of string constants
153 #real_beginning = 0
154
155 earliest_string = -1
156 string_addresses = set()
157 strings = {}
158 stack = []
159
160 newstack = []
161
162 debug = 0
163 is_spoken = False
164
165 stack.append(0)
166
167 if debug:
168 print("Header length: " + hex(header_length));
169 print("Real beginning: " + hex(real_beginning));
170
171 while 1:
172 if debug: print(long(f.tell()), end=" ")
173 temp = f.read(0x04)
174 if temp == b'': break
175 operation = uint(temp)
176 if operation <= 0xFFFF and operation not in opcodes:
177 print(short(operation) + " unknown operation")
178 print("Location: "+long(f.tell()))
179 print("First string: " + long(earliest_string))
180 exit()
181 elif operation > 0xFFFF:
182 #print("looks like we hit the string table")
183 break
184 else:
185 op = opcodes[operation]
186 arguments = []
187 for i in range(op["arguments"]):
188 arguments.append(uint(f.read(0x04)))
189 if debug:
190 print(short(operation), end=': ')
191 for i in arguments:
192 print(long(i), end=', ')
193 print("")
194 #if op["pull"] > 0:
195 # stack.pop()
196 #if operation == 0x0018: # jump
197 #f.seek(stack.pop())
198 #print("Jumped")
199 #if operation == 0x00F4: # return from script
200 #print("returning from script")
201 #print(long(f.tell()))
202 #break
203 #if operation == 0x003F:
204 #numargs = arguments[0]
205 #newstack = []
206 #for i in range(numargs):
207 # newstack.append(stack.pop())
208 #print(newstack)
209 if operation == 0x0000:
210 stack.append(arguments[0])
211 if operation == 0x0001:
212 stack.append(arguments[0])
213 if operation == 0x0002:
214 stack.append(arguments[0])
215 if operation == 0x0003:
216 string_addr = arguments[0]+real_beginning
217 if string_addr >= memory.size():
218 print("Bogus string address (too large)")
219 exit()
220 if string_addr < f.tell():
221 print("Bogus string address (before us)")
222 exit()
223 if memory[string_addr-1] != 0:
224 print("Probably a bogus string address (comes before a non-null character)")
225 exit()
226 #print("found real op")
227 if earliest_string < 0:
228 earliest_string = string_addr
229 else:
230 earliest_string = min(earliest_string, string_addr)
231 startpos = string_addr
232 endpos = startpos
233 while 1:
234 if memory[endpos] == 0: break
235 else: endpos += 1
236 string = memory[startpos:endpos].decode("sjis", errors="ignore")
237 if string_addr not in string_addresses:
238 string_addresses.add(string_addr)
239 strings[string_addr] = string
240
241 if debug: print(string + "(" + long(string_addr) + ")")
242
243 stack.append(string)
244
245 if operation == 0x007E:
246 string_addr = arguments[0]+real_beginning
247
248 if earliest_string < 0:
249 earliest_string = string_addr
250 else:
251 earliest_string = min(earliest_string, string_addr)
252
253 startpos = string_addr
254 endpos = startpos
255 while 1:
256 if memory[endpos] == 0: break
257 else: endpos += 1
258 string = memory[startpos:endpos].decode("sjis", errors="ignore")
259
260 if string_addr not in string_addresses:
261 string_addresses.add(string_addr)
262 strings[string_addr] = string
263
264 if operation == 0x0140:
265 if debug: print(stack)
266 string1 = stack.pop()
267 string2 = stack.pop()
268 flag1 = stack.pop()
269 flag2 = stack.pop()
270 flag3 = stack.pop() # following newline
271
272 line = string1.replace("《", "«").replace("》", "»")
273
274 #if speaker > real_beginning:
275 #print("> " + strings[speaker], end=": ")
276
277 #if speaker > real_beginning:
278 # print("> " + strings[speaker] + ": " + strings[line])
279 #else:
280 # print(strings[line])
281 #print(strings[line])strings[line])
282 if flag3 == 1:
283 print(line, end="\n")
284 else:
285 print(line, end="")
286 #is_spoken = False
287 if operation == 0x0141:
288 print("\n\n\n", end="")
289 if operation == 0x0142:
290 print("\n", end="")
291 # 0x014B - furigana
292 #elif operation > 0x0140 and operation < 0x0160:
293 #print("OPERATOR")
294
295 #if operation == 0x0141:
296 #print(stringstack)
297 #line = stringstack.pop()+real_beginning
298 #speaker = stringstack.pop()+real_beginning
299 #if speaker > real_beginning:
300 #print("> " + strings[speaker], end=": ")
301
302 #if is_spoken and speaker > real_beginning:
303 #print("> " + strings[speaker] + ": " +
304 #is_spoken = False
305 #if operation == 0x01A9:
306 #line = stack.pop()+real_beginning
307 #print("♪" + strings[line])
308 #is_spoken = True
309
310
311 # count number of available strings and compare to number of found strings
312 i = earliest_string
313 available_strings = 0
314 while i < memory.size():
315 if memory[i] == 0:
316 available_strings += 1
317 i += 1
318
319 if debug:
320 print("First: " + long(earliest_string))
321 print("Available: " + str(available_strings))
322 print("Found: " + str(len(string_addresses)))
323 if available_strings > len(string_addresses):
324 print("Did not find all strings")
325 exit()
326
327 print("Finished cleanly")
328
329