/
code.go
461 lines (402 loc) · 12.5 KB
/
code.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
package randomizer
import (
"fmt"
"io"
"os"
"regexp"
"sort"
"strconv"
"strings"
"gopkg.in/yaml.v2"
)
// loaded from yaml, then converted to asm.
type asmData struct {
filename string
Common yaml.MapSlice
Floating yaml.MapSlice
Seasons yaml.MapSlice
Ages yaml.MapSlice
}
// designates a position at which the translated asm will overwrite whatever
// else is there, and associates it with a given label (or a generated label if
// the given one is blank). if the replacement extends beyond the end of the
// bank, the EOB point is moved to the end of the replacement. if the bank
// offset of `addr` is zero, the replacement will start at the existing EOB
// point.
//
// returns the final label of the replacement.
func (rom *romState) replaceAsm(addr address, label, asm string) string {
if data, err := rom.assembler.compile(asm); err == nil {
return rom.replaceRaw(addr, label, data)
} else {
panic(fmt.Sprintf("assembler error in %s:\n%v\n", label, err))
}
}
// as replaceAsm, but interprets the data as a literal byte string.
func (rom *romState) replaceRaw(addr address, label, data string) string {
if addr.offset == 0 {
addr.offset = rom.bankEnds[addr.bank]
}
if label == "" {
label = fmt.Sprintf("replacement at %02x:%04x", addr.bank, addr.offset)
} else if strings.HasPrefix(label, "dma_") && addr.offset%0x10 != 0 {
addr.offset += 0x10 - (addr.offset % 0x10) // align to $xxx0
}
end := addr.offset + uint16(len(data))
if end > rom.bankEnds[addr.bank] {
if end > 0x8000 || (end > 0x4000 && addr.bank == 0) {
panic(fmt.Sprintf("not enough space for %s in bank %02x",
label, addr.bank))
}
rom.bankEnds[addr.bank] = end
}
rom.codeMutables[label] = &mutableRange{
addr: addr,
new: []byte(data),
}
rom.assembler.define(label, addr.offset)
return label
}
// returns a byte table of (group, room, collect mode, player) entries for
// randomized items. a mode >7f means to use &7f as an index to a jump table
// for special cases.
func makeCollectPropertiesTable(game, player int, itemSlots map[string]*itemSlot) string {
b := new(strings.Builder)
for _, key := range orderedKeys(itemSlots) {
slot := itemSlots[key]
// use no pickup animation for falling small keys
mode := slot.collectMode
if mode == 0x29 && slot.treasure != nil && slot.treasure.id == 0x30 {
mode &= 0xf8
}
if _, err := b.Write([]byte{slot.group, slot.room, mode, slot.player}); err != nil {
panic(err)
}
for _, groupRoom := range slot.moreRooms {
group, room := byte(groupRoom>>8), byte(groupRoom)
if _, err := b.Write([]byte{group, room, mode, slot.player}); err != nil {
panic(err)
}
}
}
// linked hero's cave
if game == gameSeasons {
// don't play linked multiworld please
if _, err := b.Write([]byte{0x05, 0x2c, collectModes["chest"], byte(player)}); err != nil {
panic(err)
}
}
b.Write([]byte{0xff})
return b.String()
}
// returns a byte table (group, room, id, subid) entries for randomized small
// key drops (and other falling items, but those entries won't be used).
func makeRoomTreasureTable(game int, itemSlots map[string]*itemSlot) string {
b := new(strings.Builder)
for _, key := range orderedKeys(itemSlots) {
slot := itemSlots[key]
if key != "maku path basement" &&
slot.collectMode != collectModes["drop"] &&
(game == gameAges || slot.collectMode != collectModes["d4 pool"]) {
continue
}
// accommodate nil treasures when creating the dummy table before
// treasures have actually been assigned.
var err error
if slot.treasure == nil {
_, err = b.Write([]byte{slot.group, slot.room, 0x00, 0x00})
} else if slot.treasure.id == 0x30 {
// make small keys the normal falling variety, with no text box.
_, err = b.Write([]byte{slot.group, slot.room, 0x30, 0x01})
} else {
_, err = b.Write([]byte{slot.group, slot.room,
slot.treasure.id, slot.treasure.subid})
}
if err != nil {
panic(err)
}
}
b.Write([]byte{0xff})
return b.String()
}
// that's correct
type eobThing struct {
addr address
label, thing string
}
// applies the labels and EOB declarations in the asm data sets.
// returns a slice of added labels.
func (rom *romState) applyAsmData(asmFiles []*asmData) []string {
// preprocess map slices (keys = labels, values = asm blocks)
slices := make([]yaml.MapSlice, 0)
for _, asmFile := range asmFiles {
if rom.game == gameSeasons {
slices = append(slices, asmFile.Common, asmFile.Seasons)
} else {
slices = append(slices, asmFile.Common, asmFile.Ages)
}
}
// include free code
freeCode := make(map[string]string)
for _, asmFile := range asmFiles {
for _, item := range asmFile.Floating {
k, v := item.Key.(string), item.Value.(string)
freeCode[k] = v
}
}
for _, slice := range slices {
for name, item := range slice {
v := item.Value.(string)
if strings.HasPrefix(v, "/include") {
funcName := strings.Split(v, " ")[1]
slice[name].Value = freeCode[funcName]
}
}
}
// save original EOB boundaries
originalBankEnds := make([]uint16, 0x40)
copy(originalBankEnds, rom.bankEnds)
// make placeholders for labels and accumulate EOB items
allEobThings := make([]eobThing, 0, 3000) // 3000 is probably fine
for _, slice := range slices {
for _, item := range slice {
k, v := item.Key.(string), item.Value.(string)
addr, label := parseMetalabel(k)
if label != "" {
rom.assembler.define(label, 0)
}
if addr.offset == 0 {
allEobThings = append(allEobThings,
eobThing{address{addr.bank, 0}, label, v})
}
}
}
// defines (which have no labels, by convention) must be processed first
sort.Slice(allEobThings, func(i, j int) bool {
return allEobThings[i].label == ""
})
// owl text must go last
for i, thing := range allEobThings {
if thing.label == "owlText" {
allEobThings = append(allEobThings[:i], allEobThings[i+1:]...)
allEobThings = append(allEobThings, thing)
break
}
}
// write EOB asm using placeholders for labels, in order to get real addrs
for _, thing := range allEobThings {
rom.replaceAsm(thing.addr, thing.label, thing.thing)
}
// also get labels for labeled replacements
for _, slice := range slices {
for _, item := range slice {
addr, label := parseMetalabel(item.Key.(string))
if addr.offset != 0 && label != "" {
rom.assembler.define(label, addr.offset)
}
}
}
// reset EOB boundaries
copy(rom.bankEnds, originalBankEnds)
labels := make([]string, 0, 3000) // 3000 probably still fine
// rewrite EOB asm, using real addresses for labels
for _, thing := range allEobThings {
labels = append(labels,
rom.replaceAsm(thing.addr, thing.label, thing.thing))
}
// make non-EOB asm replacements
for _, slice := range slices {
for _, item := range slice {
k, v := item.Key.(string), item.Value.(string)
if addr, label := parseMetalabel(k); addr.offset != 0 {
labels = append(labels, rom.replaceAsm(addr, label, v))
}
}
}
return labels
}
// applies the labels and EOB declarations in the given asm data files.
func (rom *romState) applyAsmFiles(infos []os.FileInfo) {
asmFiles := make([]*asmData, len(infos))
// standard files are embedded
for i, info := range infos {
asmFiles[i] = new(asmData)
asmFiles[i].filename = info.Name()
// readme etc
if !strings.HasSuffix(info.Name(), ".yaml") {
continue
}
path := "/asm/" + info.Name()
if err := yaml.Unmarshal(
FSMustByte(false, path), asmFiles[i]); err != nil {
panic(err)
}
}
rom.applyAsmData(asmFiles)
}
// showAsm writes the disassembly of the specified symbol to the given
// io.Writer.
func (rom *romState) showAsm(symbol string, w io.Writer) error {
mut := rom.codeMutables[symbol]
if mut == nil {
return fmt.Errorf("no such label: %s", symbol)
}
s, err := rom.assembler.decompile(string(mut.new))
if err != nil {
return err
}
fmt.Fprintf(os.Stderr, "%02x:%04x: %s\n",
mut.addr.bank, mut.addr.offset, symbol)
_, err = fmt.Fprintln(w, s)
return err
}
// returns the address and label components of a meta-label such as
// "02/openRingList" or "02/56a1/". see asm/README.md for details.
func parseMetalabel(ml string) (addr address, label string) {
switch tokens := strings.Split(ml, "/"); len(tokens) {
case 1:
fmt.Sscanf(ml, "%s", &label)
case 2:
fmt.Sscanf(ml, "%x/%s", &addr.bank, &label)
case 3:
fmt.Sscanf(ml, "%x/%x/%s", &addr.bank, &addr.offset, &label)
default:
panic("invalid metalabel: " + ml)
}
return
}
// returns a $40-entry slice of addresses of the ends of rom banks for the
// given game.
func loadBankEnds(game string) []uint16 {
eobs := make(map[string][]uint16)
if err := yaml.Unmarshal(
FSMustByte(false, "/romdata/eob.yaml"), eobs); err != nil {
panic(err)
}
return eobs[game]
}
// loads text, processes it, and attaches it to matching labels.
func (rom *romState) attachText() {
// load initial text
textMap := make(map[string]map[string]string)
if err := yaml.Unmarshal(
FSMustByte(false, "/romdata/text.yaml"), textMap); err != nil {
panic(err)
}
for label, rawText := range textMap[gameNames[rom.game]] {
if mut, ok := rom.codeMutables[label]; ok {
mut.new = processText(rawText)
} else {
panic(fmt.Sprintf("no code label matches text label %q", label))
}
}
// insert randomized item names into shop text
shopNames := loadShopNames(gameNames[rom.game])
shopMap := map[string]string{
"shopFluteText": "shop, 150 rupees",
}
if rom.game == gameSeasons {
shopMap["membersShopSatchelText"] = "member's shop 1"
shopMap["membersShopGashaText"] = "member's shop 2"
shopMap["membersShopMapText"] = "member's shop 3"
shopMap["marketRibbonText"] = "subrosia market, 1st item"
shopMap["marketPeachStoneText"] = "subrosia market, 2nd item"
shopMap["marketCardText"] = "subrosia market, 5th item"
}
for codeName, slotName := range shopMap {
code := rom.codeMutables[codeName]
itemName := shopNames[rom.itemSlots[slotName].treasure.displayName]
code.new = append(code.new[:2],
append([]byte(itemName), code.new[2:]...)...)
}
}
var hashCommentRegexp = regexp.MustCompile(" #.+?\n")
// processes a raw text string as a go string literal, converting escape
// sequences to their actual values. "comments" and literal newlines are
// stripped.
func processText(s string) []byte {
var err error
s = hashCommentRegexp.ReplaceAllString(s, "")
s, err = strconv.Unquote("\"" + s + "\"")
if err != nil {
panic(err)
}
return []byte(s)
}
var articleRegexp = regexp.MustCompile("^(an?|the) ")
// return a map of internal item names to text that should be displayed for the
// item in shops.
func loadShopNames(game string) map[string]string {
m := make(map[string]string)
// load names used for owl hints
itemFiles := []string{
"/hints/common_items.yaml",
fmt.Sprintf("/hints/%s_items.yaml", game),
}
for _, filename := range itemFiles {
if err := yaml.Unmarshal(
FSMustByte(false, filename), m); err != nil {
panic(err)
}
}
// remove articles
for k, v := range m {
m[k] = articleRegexp.ReplaceAllString(v, "")
}
return m
}
// set up all the pre-randomization asm changes, and track the state so that
// the randomization changes can be applied later.
func (rom *romState) initBanks() {
rom.codeMutables = make(map[string]*mutableRange)
rom.bankEnds = loadBankEnds(gameNames[rom.game])
asm, err := newAssembler()
if err != nil {
panic(err)
}
rom.assembler = asm
// do this before loading asm files, since the sizes of the tables vary
// with the number of checks.
roomTreasureBank := byte(sora(rom.game, 0x3f, 0x38).(int))
numOwlIds := sora(rom.game, 0x1e, 0x14).(int)
rom.replaceRaw(address{0x06, 0}, "collectPropertiesTable",
makeCollectPropertiesTable(rom.game, rom.player, rom.itemSlots))
rom.replaceRaw(address{roomTreasureBank, 0}, "roomTreasures",
makeRoomTreasureTable(rom.game, rom.itemSlots))
rom.replaceRaw(address{0x3f, 0}, "owlTextOffsets",
string(make([]byte, numOwlIds*2)))
// load all asm files in the asm/ directory.
dir, err := FS(false).Open("/asm/")
if err != nil {
panic(err)
}
fi, err := dir.Readdir(-1)
if err != nil {
panic(err)
}
rom.applyAsmFiles(fi)
}
// apply user-included asm files.
func (rom *romState) addIncludes() error {
asmFiles := make([]*asmData, len(rom.includes))
// read from filesystem
for i, path := range rom.includes {
asmFiles[i] = new(asmData)
asmFiles[i].filename = path
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
if err := yaml.NewDecoder(f).Decode(asmFiles[i]); err != nil {
return err
}
}
// apply immediately
labels := rom.applyAsmData(asmFiles)
sort.Strings(labels)
for _, label := range labels {
rom.codeMutables[label].mutate(rom.data)
}
return nil
}