Stack bytecode. Heavily modified based on someone else's BGI script tools. See also BGI (new)
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 0x007F:{"arguments":2, "istext": 1, "islabel": 0, "push":-1, "pull":-1},
75
76 # unsorted new
77 #0x0140:{"arguments":0, "istext":-1, "islabel":-1, "push":-1, "pull":-1}, # command for dialogue, narration
78 #0x01A9:{"arguments":0, "istext":-1, "islabel":-1, "push":-1, "pull":-1}, # command for voiceover
79
80 # used in CardSelect.dat after a 0x0002 operation, definitely real operations, number of arguments uncertain
81 #0x0098:{"arguments":0, "istext":-1, "islabel":-1, "push":-1, "pull":-1},
82 #0x0090:{"arguments":0, "istext":-1, "islabel":-1, "push":-1, "pull":-1},
83 #0x0091:{"arguments":0, "istext":-1, "islabel":-1, "push":-1, "pull":-1},
84
85 # bogus?
86 0x007B:{"arguments":3, "istext":-1, "islabel":-1, "push":-1, "pull":-1}, # certainly wrong but fixes a lot of problems without adding shitloads of operators
87 #0x00FE:{"arguments":2, "istext":-1, "islabel":-1, "push":-1, "pull":-1}, # new operation
88 #0x0090:{"arguments":0, "istext":-1, "islabel":-1, "push":-1, "pull":-1},
89 #0x0091:{"arguments":0, "istext":-1, "islabel":-1, "push":-1, "pull":-1},
90 #0x0098:{"arguments":0, "istext":-1, "islabel":-1, "push":-1, "pull":-1},
91 #0x011B:{"arguments":0, "istext":-1, "islabel":-1, "push":-1, "pull":-1},
92 #0x0127:{"arguments":0, "istext":-1, "islabel":-1, "push":-1, "pull":-1},
93 0xDBDB:{} # bogus ending operation to keep the list clean
94 }
95
96 # clipped from bgiop.py
97 def make_ops():
98 for op in range(0x800):
99 if op not in opcodes:
100 opcodes[op] = {"arguments":0, "istext":-1, "islabel":-1, "push":-1, "pull":-1}
101 # if op < 0x18:
102 # opcodes[op]["arguments"] = 1
103
104 make_ops()
105
106 def uint(x):
107 return unpack("<L", x)[0]
108
109 def short(x):
110 return "0x%04X" % x
111 def long(x):
112 return "0x%08X" % x
113
114 if len(argv) < 2:
115 print("No input given")
116 exit()
117
118 #print(argv[1])
119
120 first = True
121 for fn in argv:
122 if first:
123 first = False
124 continue
125 else:
126 #print("\nFile:")
127 #print(fn)
128 with open(fn, "r+b") as f:
129 memory = mmap(f.fileno(), 0)
130
131 #data = f.read(0x1C)
132 #if data.decode("sjis") != "BurikoCompiledScriptVer1.00\0":
133 # print("Error: input is not a valid supported script file")
134 # exit()
135 #data = f.read(0x04)
136
137 #header_length = uint(data)-0x04 # ?
138
139 #num_defines = uint(f.read(0x04))
140
141 #header = f.read(header_length-0x04)
142 #defines = []
143
144 #last_end = 0
145 #for i in range(num_defines):
146 # end = header.find(b'\x00', last_end)
147 # string = header[last_end:end].decode("sjis")
148 # defines.append(string)
149 # last_end = end+1
150
151 #real_beginning = header_length+0x04+0x1C # beginning in terms of address indexing of string constants
152 real_beginning = 0
153
154 earliest_string = -1
155 string_addresses = set()
156 strings = {}
157 stack = []
158
159 newstack = []
160
161 debug = 0
162 is_spoken = False
163
164 stack.append(0)
165
166 while 1:
167 if debug: print(long(f.tell()), end=" ")
168 temp = f.read(0x04)
169 if temp == b'': break
170 operation = uint(temp)
171 if operation <= 0xFFFF and operation not in opcodes:
172 print(short(operation) + " unknown operation")
173 print("Location: "+long(f.tell()))
174 print("First string: " + long(earliest_string))
175 exit()
176 elif operation > 0xFFFF:
177 #print("looks like we hit the string table")
178 break
179 else:
180 op = opcodes[operation]
181 arguments = []
182 for i in range(op["arguments"]):
183 arguments.append(uint(f.read(0x04)))
184 if debug:
185 print(short(operation), end=': ')
186 for i in arguments:
187 print(long(i), end=', ')
188 print("")
189 #if op["pull"] > 0:
190 # stack.pop()
191 #if operation == 0x0018: # jump
192 #f.seek(stack.pop())
193 #print("Jumped")
194 #if operation == 0x00F4: # return from script
195 #print("returning from script")
196 #print(long(f.tell()))
197 #break
198 if operation == 0x003F:
199 numargs = arguments[0]
200 newstack = []
201 for i in range(numargs):
202 newstack.append(stack.pop())
203 #print(newstack)
204 if operation == 0x0000:
205 stack.append(arguments[0])
206 if operation == 0x0001:
207 stack.append(arguments[0])
208 if operation == 0x0002:
209 stack.append(arguments[0])
210 if operation == 0x0003:
211 string_addr = arguments[0]+real_beginning
212 if string_addr >= memory.size():
213 print("Bogus string address (too large)")
214 exit()
215 if string_addr < f.tell():
216 print("Bogus string address (before us)")
217 exit()
218 if memory[string_addr-1] != 0:
219 print("Probably a bogus string address (comes before a non-null character)")
220 exit()
221 #print("found real op")
222 if earliest_string < 0:
223 earliest_string = string_addr
224 else:
225 earliest_string = min(earliest_string, string_addr)
226 startpos = string_addr
227 endpos = startpos
228 while 1:
229 if memory[endpos] == 0: break
230 else: endpos += 1
231 string = memory[startpos:endpos].decode("sjis", errors="ignore")
232 if string_addr not in string_addresses:
233 string_addresses.add(string_addr)
234 strings[string_addr] = string
235
236 if debug: print(string + "(" + long(string_addr) + ")")
237
238 stack.append(string)
239 if operation == 0x0140:
240 #if debug: print(newstack)
241 string1 = newstack.pop()
242 string2 = newstack.pop()
243 flag1 = newstack.pop()
244 flag2 = newstack.pop()
245 flag3 = newstack.pop() # following newline
246
247 line = string1.replace("《", "«").replace("》", "»")
248
249 #if speaker > real_beginning:
250 #print("> " + strings[speaker], end=": ")
251
252 #if speaker > real_beginning:
253 # print("> " + strings[speaker] + ": " + strings[line])
254 #else:
255 # print(strings[line])
256 #print(strings[line])strings[line])
257 if flag3 == 1:
258 print(line, end="\n")
259 else:
260 print(line, end="")
261 #is_spoken = False
262 if operation == 0x0141:
263 print("\n\n\n", end="")
264 if operation == 0x0142:
265 print("\n", end="")
266 # 0x014B - furigana
267 #elif operation > 0x0140 and operation < 0x0160:
268 #print("OPERATOR")
269
270 #if operation == 0x0141:
271 #print(stringstack)
272 #line = stringstack.pop()+real_beginning
273 #speaker = stringstack.pop()+real_beginning
274 #if speaker > real_beginning:
275 #print("> " + strings[speaker], end=": ")
276
277 #if is_spoken and speaker > real_beginning:
278 #print("> " + strings[speaker] + ": " +
279 #is_spoken = False
280 #if operation == 0x01A9:
281 #line = stack.pop()+real_beginning
282 #print("♪" + strings[line])
283 #is_spoken = True
284
285
286 # count number of available strings and compare to number of found strings
287 i = earliest_string
288 available_strings = 0
289 while i < memory.size():
290 if memory[i] == 0:
291 available_strings += 1
292 i += 1
293
294 #print("First: " + long(earliest_string))
295 #print("Available: " + str(available_strings))
296 #print("Found: " + str(len(string_addresses)))
297 #if available_strings > len(string_addresses):
298 # print("Did not find all strings")
299 # exit()
300 #
301 #print("Finished cleanly")
302
303