Skip to content

Commit 2172dbe

Browse files
committed
Add parser-backed Mario Kart 64 cheat support
1 parent 10db203 commit 2172dbe

8 files changed

Lines changed: 972 additions & 18 deletions

File tree

backend/cmd/server/cheat_mk64.go

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"strings"
7+
)
8+
9+
const (
10+
mk64EEPROMSize = 0x200
11+
mk64MainOffset = 0x180
12+
mk64BackupOffset = 0x1F8
13+
mk64StuffSize = 0x8
14+
)
15+
16+
type mk64EEPROMCheatEditor struct{}
17+
18+
type mk64ParsedStuff struct {
19+
GrandPrixPoints [4]byte
20+
SoundMode byte
21+
UnknownChecksumByte byte
22+
}
23+
24+
type mk64ParsedEEPROM struct {
25+
Payload []byte
26+
Active mk64ParsedStuff
27+
}
28+
29+
func (mk64EEPROMCheatEditor) ID() string {
30+
return "mk64-eeprom"
31+
}
32+
33+
func (mk64EEPROMCheatEditor) Read(pack cheatPack, payload []byte) (saveCheatEditorState, error) {
34+
parsed, err := parseMK64EEPROM(payload)
35+
if err != nil {
36+
return saveCheatEditorState{}, err
37+
}
38+
return saveCheatEditorState{Values: mk64BuildFieldValues(pack, parsed.Active)}, nil
39+
}
40+
41+
func (mk64EEPROMCheatEditor) Apply(pack cheatPack, payload []byte, slotID string, updates map[string]any) ([]byte, map[string]any, error) {
42+
if strings.TrimSpace(slotID) != "" {
43+
return nil, nil, fmt.Errorf("save slot selection is not supported for %q", strings.TrimSpace(slotID))
44+
}
45+
parsed, err := parseMK64EEPROM(payload)
46+
if err != nil {
47+
return nil, nil, err
48+
}
49+
state := parsed.Active
50+
fieldMap := mk64FieldIndex(pack)
51+
changed := map[string]any{}
52+
for fieldID, value := range updates {
53+
field, ok := fieldMap[fieldID]
54+
if !ok {
55+
return nil, nil, fmt.Errorf("unknown field %q", fieldID)
56+
}
57+
if err := mk64ApplyField(&state, field, value); err != nil {
58+
return nil, nil, fmt.Errorf("apply %s: %w", fieldID, err)
59+
}
60+
changed[fieldID] = value
61+
}
62+
updated := append([]byte(nil), parsed.Payload...)
63+
stuff := mk64BuildStuff(state)
64+
copy(updated[mk64MainOffset:mk64MainOffset+mk64StuffSize], stuff)
65+
copy(updated[mk64BackupOffset:mk64BackupOffset+mk64StuffSize], stuff)
66+
return updated, changed, nil
67+
}
68+
69+
func parseMK64EEPROM(payload []byte) (*mk64ParsedEEPROM, error) {
70+
if len(payload) != mk64EEPROMSize {
71+
return nil, fmt.Errorf("expected %d-byte Mario Kart 64 EEPROM, got %d", mk64EEPROMSize, len(payload))
72+
}
73+
main := payload[mk64MainOffset : mk64MainOffset+mk64StuffSize]
74+
backup := payload[mk64BackupOffset : mk64BackupOffset+mk64StuffSize]
75+
switch {
76+
case mk64VerifyStuff(main):
77+
parsed, err := mk64DecodeStuff(main)
78+
if err != nil {
79+
return nil, err
80+
}
81+
return &mk64ParsedEEPROM{Payload: append([]byte(nil), payload...), Active: parsed}, nil
82+
case mk64VerifyStuff(backup):
83+
parsed, err := mk64DecodeStuff(backup)
84+
if err != nil {
85+
return nil, err
86+
}
87+
return &mk64ParsedEEPROM{Payload: append([]byte(nil), payload...), Active: parsed}, nil
88+
default:
89+
return nil, errors.New("mario kart 64 save does not contain a valid main or backup save-info block")
90+
}
91+
}
92+
93+
func mk64DecodeStuff(block []byte) (mk64ParsedStuff, error) {
94+
if len(block) != mk64StuffSize {
95+
return mk64ParsedStuff{}, fmt.Errorf("expected %d-byte Mario Kart 64 save-info block, got %d", mk64StuffSize, len(block))
96+
}
97+
state := mk64ParsedStuff{SoundMode: block[4], UnknownChecksumByte: block[5]}
98+
copy(state.GrandPrixPoints[:], block[:4])
99+
if _, ok := mk64SoundModeID(state.SoundMode); !ok {
100+
return mk64ParsedStuff{}, fmt.Errorf("unsupported Mario Kart 64 sound mode %d", state.SoundMode)
101+
}
102+
return state, nil
103+
}
104+
105+
func mk64VerifyStuff(block []byte) bool {
106+
if len(block) != mk64StuffSize {
107+
return false
108+
}
109+
checksum1 := mk64Checksum1(block[:5])
110+
checksum2 := mk64Checksum2(checksum1)
111+
return block[6] == checksum1 && block[7] == checksum2
112+
}
113+
114+
func mk64Checksum1(saveInfo []byte) byte {
115+
var sum int
116+
for idx, value := range saveInfo {
117+
sum += (int(value)+1)*(idx+1) + idx
118+
}
119+
return byte(sum % 0x100)
120+
}
121+
122+
func mk64Checksum2(checksum1 byte) byte {
123+
return byte((int(checksum1) + 90) % 0x100)
124+
}
125+
126+
func mk64BuildStuff(state mk64ParsedStuff) []byte {
127+
block := make([]byte, mk64StuffSize)
128+
copy(block[:4], state.GrandPrixPoints[:])
129+
block[4] = state.SoundMode
130+
block[5] = state.UnknownChecksumByte
131+
block[6] = mk64Checksum1(block[:5])
132+
block[7] = mk64Checksum2(block[6])
133+
return block
134+
}
135+
136+
func mk64BuildFieldValues(pack cheatPack, state mk64ParsedStuff) map[string]any {
137+
values := map[string]any{}
138+
for _, section := range pack.Sections {
139+
for _, field := range section.Fields {
140+
switch field.Op.Kind {
141+
case "soundMode":
142+
if modeID, ok := mk64SoundModeID(state.SoundMode); ok {
143+
values[field.ID] = modeID
144+
}
145+
case "gpCupPoints":
146+
modeIndex, ok := mk64ModeIndex(field.Op.Mode)
147+
if !ok {
148+
continue
149+
}
150+
cupIndex, ok := mk64CupIndex(field.Op.Cup)
151+
if !ok {
152+
continue
153+
}
154+
if pointID, ok := mk64CupPointID(mk64CupPoints(state.GrandPrixPoints[modeIndex], cupIndex)); ok {
155+
values[field.ID] = pointID
156+
}
157+
}
158+
}
159+
}
160+
return values
161+
}
162+
163+
func mk64FieldIndex(pack cheatPack) map[string]cheatField {
164+
fields := map[string]cheatField{}
165+
for _, section := range pack.Sections {
166+
for _, field := range section.Fields {
167+
fields[field.ID] = field
168+
}
169+
}
170+
return fields
171+
}
172+
173+
func mk64ApplyField(state *mk64ParsedStuff, field cheatField, value any) error {
174+
switch field.Op.Kind {
175+
case "soundMode":
176+
raw, ok := value.(string)
177+
if !ok {
178+
return errors.New("expected enum string")
179+
}
180+
mode, ok := mk64SoundModeValue(raw)
181+
if !ok {
182+
return fmt.Errorf("unsupported sound mode %q", raw)
183+
}
184+
state.SoundMode = mode
185+
return nil
186+
case "gpCupPoints":
187+
raw, ok := value.(string)
188+
if !ok {
189+
return errors.New("expected enum string")
190+
}
191+
points, ok := mk64CupPointValue(raw)
192+
if !ok {
193+
return fmt.Errorf("unsupported cup value %q", raw)
194+
}
195+
modeIndex, ok := mk64ModeIndex(field.Op.Mode)
196+
if !ok {
197+
return fmt.Errorf("unknown cc mode %q", field.Op.Mode)
198+
}
199+
cupIndex, ok := mk64CupIndex(field.Op.Cup)
200+
if !ok {
201+
return fmt.Errorf("unknown cup %q", field.Op.Cup)
202+
}
203+
state.GrandPrixPoints[modeIndex] = mk64SetCupPoints(state.GrandPrixPoints[modeIndex], cupIndex, points)
204+
return nil
205+
default:
206+
return fmt.Errorf("unsupported op kind %q", field.Op.Kind)
207+
}
208+
}
209+
210+
func mk64ModeIndex(mode string) (int, bool) {
211+
switch strings.TrimSpace(strings.ToLower(mode)) {
212+
case "50cc":
213+
return 0, true
214+
case "100cc":
215+
return 1, true
216+
case "150cc":
217+
return 2, true
218+
case "extra":
219+
return 3, true
220+
default:
221+
return 0, false
222+
}
223+
}
224+
225+
func mk64CupIndex(cup string) (int, bool) {
226+
switch strings.TrimSpace(strings.ToLower(cup)) {
227+
case "mushroom":
228+
return 0, true
229+
case "flower":
230+
return 1, true
231+
case "star":
232+
return 2, true
233+
case "special":
234+
return 3, true
235+
default:
236+
return 0, false
237+
}
238+
}
239+
240+
func mk64CupPoints(ccPoints byte, cupIndex int) byte {
241+
shift := cupIndex * 2
242+
return (ccPoints >> shift) & 0x3
243+
}
244+
245+
func mk64SetCupPoints(ccPoints byte, cupIndex int, points byte) byte {
246+
shift := cupIndex * 2
247+
mask := byte(0x3 << shift)
248+
return (ccPoints &^ mask) | ((points & 0x3) << shift)
249+
}
250+
251+
func mk64CupPointID(points byte) (string, bool) {
252+
switch points {
253+
case 0:
254+
return "none", true
255+
case 1:
256+
return "bronze", true
257+
case 2:
258+
return "silver", true
259+
case 3:
260+
return "gold", true
261+
default:
262+
return "", false
263+
}
264+
}
265+
266+
func mk64CupPointValue(id string) (byte, bool) {
267+
switch strings.TrimSpace(strings.ToLower(id)) {
268+
case "none":
269+
return 0, true
270+
case "bronze":
271+
return 1, true
272+
case "silver":
273+
return 2, true
274+
case "gold":
275+
return 3, true
276+
default:
277+
return 0, false
278+
}
279+
}
280+
281+
func mk64SoundModeID(mode byte) (string, bool) {
282+
switch mode {
283+
case 0:
284+
return "stereo", true
285+
case 1:
286+
return "headphones", true
287+
case 2:
288+
return "unused", true
289+
case 3:
290+
return "mono", true
291+
default:
292+
return "", false
293+
}
294+
}
295+
296+
func mk64SoundModeValue(id string) (byte, bool) {
297+
switch strings.TrimSpace(strings.ToLower(id)) {
298+
case "stereo":
299+
return 0, true
300+
case "headphones":
301+
return 1, true
302+
case "unused":
303+
return 2, true
304+
case "mono":
305+
return 3, true
306+
default:
307+
return 0, false
308+
}
309+
}

0 commit comments

Comments
 (0)