-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathspyro_gci_gamecube.bt
400 lines (341 loc) · 19.6 KB
/
spyro_gci_gamecube.bt
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
//------------------------------------------------
//--- 010 Editor v12.0.1k Binary Template
//
// File: Spyro: A Hero's Tail - GameCube save data (three-slot sections)
// Authors: Swyter
// Version: 2022.11.22
// Purpose: This will probably work for any other GCI saves, I think.
// Category: Game
// File Mask: *.gci
//------------------------------------------------
BigEndian();
#include "spyro_hashcodes_enum.h"
struct
{
enum<ubyte> gci_tex_fmt { fmt_none = 0, fmt_palette = 1, fmt_truecolor = 2, fmt_palette_pingpong = 5, fmt_truecolor_pingpong = 6 };
char game_id[4]; char publisher_id[2];
ubyte unk_ff <format=hex>; gci_tex_fmt banner_format; /* swy: FF 00 = no banner */
char game_tag[4 + 28];
uint variable_size_thing_timestamp_maybe <format=hex>;
uint graphic_assets_offset <format=hex>;
union { ushort val; struct { ushort h:2; ushort g:2; ushort f:2; ushort e:2; ushort d:2; ushort c:2; ushort b:2; ushort a:2; } ind; } icon_frame_color_fmt; /* swy: seems like each nibble controls the format of two frames, so there's probably a maximum of 8 frames */
ushort icon_anim_speed_data; /* swy: top bit of the second nibble controls animation speed, rest controls the amount of frames somehow */
ushort unk_a <format=hex>, unk_b <format=hex>;
ushort storage_blocks, unk_ff <format=hex>;
uint text_labels_offset <format=hex>;
} gci_head;
Printf("GCI: (%#x bytes - 0x40 of header) / 0x2000 => %.2f storage blocks, stated: %u\n", FileSize(), (FileSize() - 0x40) / 0x2000, gci_head.storage_blocks); /* swy: hex(int(0x18000/12)) = 0x2000 bytes per block? */
Assert(FileSize() == (gci_head.storage_blocks * 0x2000) + 0x40);
FSeek(startof(gci_head) + sizeof(gci_head) + gci_head.text_labels_offset);
char gci_game_title_str[32];
char gci_game_descr_str[32];
FSeek(startof(gci_head) + sizeof(gci_head) + gci_head.graphic_assets_offset);
/* swy: a banner seems to always be 96x32px, the icon seems 32x32px, 96*32 = 3072, 32*32 = 1024 */
/* swy: format 5 seems to be grayscale, 1 too, 2 has colors, takes double of space, 3 no banner, 4 no banner, 6 seems same at 2 */
/* swy: 1/5 = 1 byte is 1 pixel, 2/6 = colors, 0/3/7 = none */
/* swy: format 1 = (width * height * frames) + 0x200 of palette */
/* swy: format 6 = (width * height * frames) * 2 */
/* swy: 2 restart animation again, 6 ping-pong */
/* swy: 1 restart animation again, 5 ping-pong - probably */
local uint has_palette = (gci_head.banner_format & 0b11) == 1; /* swy: third bit signals ping-pong animation */
local uint banner_byte_size = (96 * 32) * (has_palette ? 1 : 2);
local uint icon_byte_size = (32 * 32) * 1.5; /* swy: usually 0x600 bytes per icon */
if ((gci_head.banner_format & 0b11) != 0)
{
byte gci_img_banner[banner_byte_size]; /* swy: banner goes first, if any. then the icon, many eurocom games take 0x2000 for both, so I thought it was static */
if (has_palette) byte gci_img_banner_pal[0x200];
}
/* swy: now parse the cursed icon animation descriptor mechanism that took hours to reverse;
prettiest code in town, see below */
ubyte icon_get_anim_frame(uint index)
{
switch (index)
{
case 0: return gci_head.icon_frame_color_fmt.ind.a;
case 1: return gci_head.icon_frame_color_fmt.ind.b;
case 2: return gci_head.icon_frame_color_fmt.ind.c;
case 3: return gci_head.icon_frame_color_fmt.ind.d;
case 4: return gci_head.icon_frame_color_fmt.ind.e;
case 5: return gci_head.icon_frame_color_fmt.ind.f;
case 6: return gci_head.icon_frame_color_fmt.ind.g;
case 7: return gci_head.icon_frame_color_fmt.ind.h;
}
}
/* swy: go 2-bit frame slot by 2-bit frame slot and see who's at home */
local uint i = 0; local uint frame_count = 0;
for (i = 0; i < 8; i++)
frame_count += (icon_get_anim_frame(i) != 0) ? 1 : 0;
/* swy: we check each entry individually instead of only the first one, but the C standard
doesn't allow arrays of bitfields, forcing us to make an accessor
function for syntax reasons, and to be able to loop */
struct icon_t { byte data[32 * 32 * (icon_get_anim_frame(i) == 1 ? 1 : 2)]; };
for (i = 0; i < frame_count; i++)
icon_t gci_img_icon; /* swy: 0x2200, 8 images */
if (gci_head.icon_frame_color_fmt.ind.a == 1) byte gci_img_icon_pal[0x200];
/* -- */
if (!(gci_head.game_id[1] == '5' && gci_head.game_id[2] == 'S'))
{
Warning("This doesn't look like a Spyro: A Hero's Tail GameCube save. Stopping.");
return;
} //Assert(gci_head.game_id[1] == '5' && gci_head.game_id[2] == 'S', "This doesn't look like a Spyro: A Hero's Tail GameCube save. Stopping.");
FSeek( 0x40); uint checksum <format=hex>, weird_padding[0x17c / 4] <hidden=true>; /* swy: seems to be unique to Eurocom; CRC32 from 0x1c0 to data end (0x28040) */
FSeek(0x2200);
struct
{
ubyte slot_a, slot_b, slot_c; ubyte more[5]; char pad[0x1e38];
} eurocom_head;
char game_build_time[32];
char game_build_date[32];
enum<uint> bool { off = 0, enabled = 1 };
enum<uint> cam_reversal { normal = 0, reversed = 1 };
enum<uint> cam_mode { passive, active };
int slot_count; /* swy: three ¯\_(ツ)_/¯ */
struct
{
uint slot_checksum_a <format=hex>, /* swy: these don't actually work as normal checksums, */
slot_checksum_b <format=hex>, /* the game only checks that slot_checksum_a == slot_a.slot_checksum on load, and so on */
slot_checksum_c <format=hex>;
uint slot_save_size <format=hex>; /* swy: the game says 0xB258, I say 0xb260 */
struct { int edb_file_hc <format=hex>, zero, zero; } load_files[16] <format=hex>; /* swy: experiment with this. I don't know, yet */
byte unk_zero_a;
struct
{
byte unk : 1; byte unk : 1; byte unk : 1;
byte character_select_flame_ember_model_viewer_play_minigame : 1;
byte character_select_flame_ember_model_viewer : 1;
byte character_select_ember_model_viewer : 1;
byte model_viewer : 1;
byte concept_art_viewer : 1;
} unlocked_extras; /* swy: zero: no unlockables; 1, concept art viewer, 2 model viewer/ember, 4 also character select top three, 8/0b1000 spyro/flame/ember, 0b10000 play minigame */
ushort unlocked_thing_b_maybe;
uint unk_zero_b;
float sfx_volume; uint music_volume; /* swy 0-100 */
cam_reversal first_person_y_axis, sgt_byrd_y_axis, sparx_flying_y_axis;
cam_mode camera;
bool rumble_feature;
enum player_character_id selected_bonus_character_maybe;
ubyte uninitialized_mem[4];
} common_spyro_save_block;
typedef struct
{
uint slot_checksum <format=hex>;
enum<ubyte> { slot_is_active = 1, slot_empty = 0 } slot_active; ubyte zero_pad[3];
float decimal_something; uint player_char_maybe;
enum map_list
{
/* 0x010000A9 - loading.edb */ MapLoading = 0,
/* 0x01 - .edb */ LoadingLoop2D = 1,
/* 0x010000D3 - mr1_blk.edb (Blink minigame 1) */ Completely_Swamped = 2,
/* 0x01000093 - mr1_sgt.edb (Sgt Byrd minigame 1) */ Island_Speedway = 3,
/* 0x01000094 - mr1_spx.edb (Sparx minigame 1) */ Cavern_Chaos = 4,
/* 0x01000092 - mr1_spy.edb (Spyro minigame 1) */ Critter_Calamity = 5,
/* 0x010000C9 - mr2_blk.edb (Blink minigame 2) */ All_Washed_Up = 6,
/* 0x010000D7 - mr2_sgt.edb (Sgt Byrd minigame 2) */ Cloudy_Speedway = 7,
/* 0x010000D6 - mr2_spx.edb (Sparx minigame 2) */ Outlandish_Inlet = 8,
/* 0x01000098 - mr2_spy.edb (Spyro minigame 2) */ Turtle_Turmoil = 9,
/* 0x010000D4 - mr3_blk.edb (Blink minigame 3) */ Snowed_Under = 10,
/* 0x0100009A - mr3_sgt.edb (Sgt Byrd minigame 3) */ Iceberg_Aerobatics = 11,
/* 0x010000C8 - mr3_spx.edb (Sparx minigame 3) */ Frosty_Flight = 12,
/* 0x010000BE - mr3_spy.edb (Spyro minigame 3) */ Iced_TNT = 13,
/* 0x010000D5 - mr4_blk.edb (Blink minigame 4) */ Mined_Out = 14,
/* 0x010000A3 - mr4_sgt.edb (Sgt Byrd minigame 4) */ Lava_Palaver = 15,
/* 0x010000B4 - mr4_spx.edb (Sparx minigame 4) */ Sparx_Will_Fly = 16,
/* 0x010000BF - mr4_spy.edb (Spyro minigame 4) */ Storming_the_Beach = 17,
/* 0x0100015c - credits.edb (Credits) */ Credits = 18,
/* 0x0100005A - realm2b.edb (Sunken Ruins) */ Sunken_Ruins = 19,
/* 0x0100005B - realm2c.edb (Cloudy Domain) */ Cloudy_Domain = 20,
/* 0x0100005B - realm2c.edb (Cloudy Domain Ball Gadget) */ Cloudy_Domain_b = 21,
/* 0x0100003A - realm1c.edb (Dragonfly Falls) */ Dragonfly_Falls = 22,
/* 0x01000039 - realm1b.edb (Crocovile Swamp) */ Crocovile_Swamp = 23,
/* 0x01000037 - realm1a.edb (Dragon Village) */ Dragon_Village = 24,
/* 0x0100006D - r1linkab.edb (Link map: DV to CS) */ MapR1LinkAB = 25,
/* 0x01000082 - r1linkac.edb (Link map: DV to DF (Ball Gadget)) */ MapR1LinkAC = 26,
/* 0x01000008 - hogwarts.edb */ MapExample = 27, /* swy: Hogwarts castle */
/* 0x01000058 - test_dp.edb */ testdp = 28,
/* 0x01000043 - test_sc.edb */ MapMorphEnv = 29,
/* 0x010000A1 - realm4d.edb (Dark Mine) */ Dark_Mine = 30,
/* 0x01000038 - realm3a.edb (Frostbite Village) */ Frostbite_Village = 31,
/* 0x01000087 - realm1z.edb (Gnasty’s Cave) */ Gnastys_Cave = 32,
/* 0x01000059 - realm3c.edb (Ice Citadel) */ Ice_Citadel = 33,
/* 0x010000ED - realm4z.edb (Red’s Lair) */ Reds_Lair = 34,
/* 0x0100005C - realm3b.edb (Gloomy Glacier) */ Gloomy_Glacier = 35,
/* 0x0100000E - playroom.edb */ Playroom = 36,
/* 0x01000074 - model.edb */ Model_Viewer = 37,
/* 0x0100001D - test_tl.edb */ Test_TL = 38,
/* 0x0100001B - test_pb.edb */ Test_PB = 39,
/* 0x010000A2 - realm4e.edb (Red’s Laboratory) */ Reds_Laboratory = 40,
/* 0x010000E5 - realm3z.edb (Red’s Chamber) */ Reds_Chamber = 41,
/* 0x0100001A - test_mf.edb */ Test_MF = 42,
/* 0x0100003D - shop.edb */ Shop = 43,
/* 0x0100005D - realm4a.edb (Stormy Beach) */ Stormy_Beach = 44,
/* 0x0100003B - realm2a.edb (Coastal Remains) */ Coastal_Remains = 45,
/* 0x01000150 - r2linkab.edb (Link map: CR to SR (elevator)) */ MapR2LinkAB = 46,
/* 0x01000151 - r2linkac.edb (Link map: CR to CD (elevator)) */ MapR2LinkAC = 47,
/* 0x0100001E - test_js.edb */ Test_JS = 48,
/* 0x01000009 - test_ka.edb */ Test_KA = 49,
/* 0x01000007 - test_nb.edb */ Test_NB = 50,
/* 0x0100015D - test_nb2.edb */ Test_NB2 = 51,
/* 0x0100000A - test_sj.edb */ Test_SJ = 52,
/* 0x01000020 - test_wts.edb */ Platform_Area = 53, /* Inaccessible - probably removed */
/* 0x0100002b - test_bch.edb */ TestBeach = 54,
/* 0x0100002E - testball.edb */ TestBall = 55, /* Inaccessible - probably removed */
/* 0x01000029 - titles.edb (Title screen) */ Titles = 56,
/* 0x01000154 - r4linkbc.edb (Link map: MM to MFT (elevator)) */ MapR4LinkBC = 57,
/* 0x01000156 - r4linkcd.edb (Link map: MFB to DM) */ MapR4LinkCD = 58,
/* 0x01000155 - r4linkde.edb (Link map: DM to RL) */ MapR4LinkDE = 59,
/* 0x0100005E - realm4b.edb (Molten Mount) */ Molten_Mount = 60,
/* 0x0100005F - realm4c.edb (Magma Falls Top) */ Magma_Falls_Top = 61,
/* 0x0100005F - realm4c.edb (Magma Falls Ball Gadget) */ Magma_Falls_Ball_Gadget = 62,
/* 0x0100005F - realm4c.edb (Magma Falls Bottom) */ Magma_Falls_Bottom = 63,
/* 0x010000EC - realm2z.edb (Watery Tomb) */ Watery_Tomb = 64
} cur_map;
uint zero;
uint saved_count;
struct saved_blocks
{
ushort id_a <format=hex>, id_b <format=hex>;
float x, y, z;
int one <format=hex>, tail_b <format=hex>, start_point_hc <format=hex>, text_prompt_hc <format=hex>;
} blocks[saved_count], unused_blocks[256-saved_count]; uint pad_align;
ushort year <bgcolor=0xfff>; ubyte month, day; ubyte hour, minute, second, zero;
float play_time_in_secs; uint pad;
enum
{
breath_none,
breath_fire = 1,
breath_water = 2,
breath_ice = 4,
breath_electric = 8,
} breath_selected;
uint health, gem_total_count, gem_counter;
ubyte lock_pick_count,lock_pick_limit,a,b;
typedef struct
{
ubyte amount, carry_limit, magazine_limit, magazine_amount;
} element_storage <read=Str("[%02u/%02u, mag: %03u/%03u]", amount, carry_limit, magazine_amount, magazine_limit)>;
element_storage fire, ice, water, electric;
ushort fire_arrows_count;
ushort fire_arrows_limit;
struct
{
bool abiflg_unk_a : 1;
bool abiflg_unk_b : 1;
bool abiflg_unk_c : 1;
bool abiflg_unk_d : 1;
bool abiflg_unk_e : 1;
bool abiflg_unk_f : 1;
bool abiflg_unk_g : 1;
bool abiflg_unk_h : 1;
bool abiflg_unk_i : 1;
bool abiflg_unk_j : 1;
bool abiflg_unk_k : 1;
bool abiflg_unk_l : 1;
bool abiflg_unk_m : 1;
bool abiflg_butterfly_jar : 1;
bool abiflg_horndive_upgrade : 1;
bool abiflg_wall_kick : 1;
bool abiflg_wing_shield : 1;
bool abiflg_shop_sells_everything : 1;
bool abiflg_invincibility : 1;
bool abiflg_supercharge : 1;
bool abiflg_aqualung : 1;
bool abiflg_unk_n : 1;
bool abiflg_double_gem : 1;
bool abiflg_unk_o : 1;
bool abiflg_water_breath : 1;
bool abiflg_electric_breath : 1;
bool abiflg_ice_breath : 1;
bool abiflg_pole_spin : 1;
bool abiflg_unk_p : 1;
bool abiflg_hit_point_upgrade : 1;
bool abiflg_unk_q : 1;
bool abiflg_double_jump : 1;
} ability_flags;
//uint unk_a <format=hex>, zero, zero, zero, zero, unk_c <format=hex>, unk_d <format=hex>, zero;
float blink_cooldown, supercharge_cooldown, unk_cooldown1, invincibility_cooldown, cooldown_upperbound, unk_cooldown2, unk_cooldown3, sgtbyrd_boost;
ushort sgtbyrd_bombs, sgtbyrd_missiles, thung, thang, zero_pad_maybe;
ubyte light_gems, dark_gems, eggs, more;
ushort alignment_b;
uint zero;
float player_spawn_pos_x, player_spawn_pos_y, player_spawn_pos_z, player_spawn_pos_padding; float player_spawn_rot_p, player_spawn_rot_y, player_spawn_rot_r, player_spawn_rot_padding;
uint character_UI, thing, thxng, thyng, thzng, thcng;
//uint maininfo_endpad[0x20 / 4];
uint bytepad_maybe[(0x90 / 4)];
BitfieldDisablePadding();
enum<uint> objective_state
{
Uncompleted = 0,
Completed = 1,
};
struct {
local int num_of_objectives = 0x200;
local int num_of_completed_objectives = 0;
for (i = 0; i < num_of_objectives; i++) {
struct {
objective_state o_state : 1;
local EXHashCode o_hash = (i+1) | 0x44000000;
if (o_state == 1) { num_of_completed_objectives++; }
} objective <read=Str("[%s] - %s", EnumToString(o_state), EnumToString(o_hash)), optimize=false>;
}
} objectives <read=Str("%d completed", num_of_completed_objectives), optimize=false>;
enum<uint> task_state
{
Not_Found = 0,
Not_Done = 1,
INVALID = 2,
Done = 3,
};
struct {
local int num_of_tasks = 0x50;
local int num_of_tasks_found = 0;
local int num_of_tasks_done = 0;
for (i = 0; i < num_of_tasks; i++) {
struct {
task_state t_state : 2;
local EXHashCode t_hash = (i+1) | 0x45000000;
if (t_state == 1) { num_of_tasks_found++; }
if (t_state == 3) { num_of_tasks_found++; num_of_tasks_done++; }
} task <read=Str("[%s] - %s", EnumToString(t_state), EnumToString(t_hash)), optimize=false>;
}
} tasks <read=Str("%d/%d done", num_of_tasks_done, num_of_tasks_found), optimize=false>;
uint trigger_state_bitflags[(0x4292 / 4)];
local uint map_count = 0;
typedef struct
{
int start_point_hashcode <format=hex>;
int one_or_zero;
int total_dark_gems;
int total_dragon_eggs;
int total_light_gems;
int p[7] <format=hex>;
struct
{
int lvl_darkgem_tally, lvl_lightgem_tally, lvl_egg_conceptart_tally, lvl_egg_charviewer_tally, lvl_egg_ember_tally, lvl_egg_flame_tally, lvl_egg_blink_tally, lvl_egg_turret_tally, lvl_egg_sparx_tally, lvl_egg_blink_tally;
} level_collectible_tallies;
int one_or_zero_b, f <format=hex>, g;
local enum map_list cur_map = map_count++;
} level_stat <read=Str("[%x] - %s", start_point_hashcode, EnumToString(cur_map)), optimize=false>;
level_stat level_stats[200]; /* swy: there's one start point set for level; e.g. if the current map is Dragon_Village (23), the game uses the array element on index 23, each fairy check/restart point is tagged with an unique hashcode: 0x4A000005, 0x4A000006, ... */
uint alignment_pad_end <format=hex>;
} save_slot <read=Str(
"[Started: %u-%02u-%02u %02u:%02u:%02u] [Played: %02u:%02u:%02u] [d:%02u/40 l:%03u/100 e:%02u/80] - %s",
year, month, day, hour, minute, second,
play_time_in_secs / (60 * 60), (play_time_in_secs % (60. * 60.)) / 60, play_time_in_secs % 60,
dark_gems, light_gems, eggs, EnumToString(cur_map)
)>;
FSeek(0x04180); struct save_slot slot_a; /* swy: each slot has 0xb260 bytes of available space */
FSeek(0x0f3e0); struct save_slot slot_b;
FSeek(0x1A640); struct save_slot slot_c;
/* -- */
enum player_character_id /* swy: provided by Ebbe */
{
No_character = 0,
Spyro = 1,
Hunter = 2,
Sparx = 3,
Blink = 4,
Sgt_Byrd = 5,
Ball_gadget = 6,
Ember = 7,
Flame = 8
};