diff --git a/popnhax/dllmain.cc b/popnhax/dllmain.cc index e3d0831..5aaf2ac 100644 --- a/popnhax/dllmain.cc +++ b/popnhax/dllmain.cc @@ -2175,6 +2175,17 @@ static bool patch_database() { config.disable_expansions = true; } + // Flavor table growth is only safe when the generated patch set covers + // the flavor table manager and those patches will actually be applied. + if (!config.disable_expansions) { + for (size_t i = 0; i < other_offsets.size(); i++) { + if (other_offsets[i]->type == FLAVOR_TABLE_IDX && other_offsets[i]->method == 14) { + g_flavor_slots_expansion = true; + break; + } + } + } + musichax_core_init(&config, target, @@ -2351,6 +2362,16 @@ static bool patch_database() { patch_size = other_offsets[i]->size; break; + // Add limit_diff * 2 (u16 per-entry arrays) + case 13: + value = cur_value + (limit_diff * 2); + break; + + // Add limit_diff * 0x18 (flavor table manager) + case 14: + value = cur_value + (limit_diff * 0x18); + break; + default: printf("Unknown command: %lld\n", other_offsets[i]->method); break; @@ -2608,7 +2629,10 @@ bool force_unlock_charas() { // These should *probably* always be 0 but won't be after the table ends break; } - + + // note: changing this from 3 to 1 can hide enemy-only characters that might appear + // in the chara select by accident if a leak occurs (which hasn't happened to me) + // since i did a clean dump of my omnimix data so... not needed i suppose uint32_t new_flags = entry->flags & ~3; if (new_flags != entry->flags && entry->disp_name_ptr != NULL && strlen((char*)entry->disp_name_ptr) > 0) { diff --git a/popnhax/loader.cc b/popnhax/loader.cc index debaade..a9443b3 100644 --- a/popnhax/loader.cc +++ b/popnhax/loader.cc @@ -48,7 +48,7 @@ uint32_t add_flavor(int32_t cur_idx, uint8_t *phrase1, uint8_t *phrase2, uint8_t uint8_t chara3_birth_month, uint8_t chara1_birth_date, uint8_t chara2_birth_date, uint8_t chara3_birth_date, uint16_t style1, bool style2_flag, uint16_t style3, uint32_t fontstyle_fontface, - uint32_t fontstyle_color, uint32_t fontstyle_height, uint32_t fontstyle_width); + uint32_t fontstyle_color, uint32_t fontstyle_height, uint32_t fontstyle_width, bool override_idx); uint32_t add_chara(int32_t cur_idx, uint8_t *chara_id_ptr, uint32_t flags, uint8_t *folder_ptr, uint8_t *gg_ptr, uint8_t *cs_ptr, uint8_t *icon1_ptr, uint8_t *icon2_ptr, @@ -74,6 +74,9 @@ void parse_musicdb(const char *input_filename, const char *target, struct popnha std::map chart_type_overrides; uint32_t flavor_limit = 0; +// See loader.h: whether the binary patches for the character-select slot +// manager were generated, making flavor table growth safe. +bool g_flavor_slots_expansion = false; uint8_t *filler_str = add_string((uint8_t *)"\x81\x5d\x00"); @@ -362,7 +365,7 @@ uint32_t add_flavor(int32_t cur_idx, uint8_t *phrase1, uint8_t *phrase2, uint8_t uint8_t chara3_birth_month, uint8_t chara1_birth_date, uint8_t chara2_birth_date, uint8_t chara3_birth_date, uint16_t style1, bool style2_flag, uint16_t style3, uint32_t fontstyle_fontface, - uint32_t fontstyle_color, uint32_t fontstyle_height, uint32_t fontstyle_width) { + uint32_t fontstyle_color, uint32_t fontstyle_height, uint32_t fontstyle_width, bool override_idx) { if (flavor_table == NULL || cur_idx + 1 >= (int)flavor_table_size) { // Realloc array to hold more flavors uint32_t new_flavor_table_size = cur_idx + 1 + flavor_table_growth_size; @@ -384,6 +387,7 @@ uint32_t add_flavor(int32_t cur_idx, uint8_t *phrase1, uint8_t *phrase2, uint8_t } // This table is limited so keep it as small as possible + if (!override_idx) { for (int32_t i = 0; i < cur_idx; i++) { flavor_entry *cur = (flavor_entry *)flavor_table + i; @@ -418,6 +422,7 @@ uint32_t add_flavor(int32_t cur_idx, uint8_t *phrase1, uint8_t *phrase2, uint8_t return i; } } + } uint16_t style2 = add_style(fontmax_style_id + 1, fontstyle_fontface, fontstyle_color, fontstyle_height, fontstyle_width) + @@ -789,27 +794,38 @@ void parse_charadb(const char *input_filename, const char *target) { // printf("style[%d] idx[%d]\n", style_table_ptr, style2_idx); } - flavor_idx = add_flavor( - max_flavor_id + 1, phrase1_ptr, phrase2_ptr, phrase3_ptr, phrase4_ptr, - phrase5_ptr, phrase6_ptr, birthday_ptr, chara1_birth_month, chara2_birth_month, - chara3_birth_month, chara1_birth_date, chara2_birth_date, chara3_birth_date, - style1, style2_flag, style3, fontface, color, height, width); - - // printf("flavor[%d] idx[%d]\n", max_flavor_id, flavor_idx); - - if (flavor_idx > (int32_t)flavor_limit) { - // This is a hack because I don't feel like finding the proper buffers to patch - // for this - printf("WARNING: Overflowed flavor text table limit of %d, defaulting to index " - "0 instead of %d\n", - flavor_limit, flavor_idx); + // The flavor index doubles as the character select slot index: the + // select manager has one inline slot vector per flavor. Growing the + // flavor table is only safe when the generated patches cover that + // manager (g_flavor_slots_expansion); otherwise an index past the + // stock limit corrupts it instead of adding a slot. + // An explicit lets a custom chara claim (and rewrite) a stock + // flavor entry; ids whose slot is empty in the stock roster give the + // chara its own select slot (Jam&Fizz: 390, 527, 667, 693, 717). + int16_t explicit_flavor_id = -1; + READ_S16_OPT(config_xml, prop_flavor, "id", explicit_flavor_id) + + int32_t target_flavor_idx = explicit_flavor_id >= 0 ? explicit_flavor_id + : max_flavor_id + 1; + if (target_flavor_idx >= (int32_t)flavor_limit && !g_flavor_slots_expansion) { + LOG("Flavor %d exceeds the table limit of %d and the select-slot " + "manager patches are unavailable, defaulting to index 1\n", + target_flavor_idx, flavor_limit); flavor_idx = -1; + } else { + flavor_idx = add_flavor( + target_flavor_idx, phrase1_ptr, phrase2_ptr, phrase3_ptr, phrase4_ptr, + phrase5_ptr, phrase6_ptr, birthday_ptr, chara1_birth_month, + chara2_birth_month, chara3_birth_month, chara1_birth_date, + chara2_birth_date, chara3_birth_date, style1, style2_flag, style3, + fontface, color, height, width, explicit_flavor_id >= 0); } - c->flavor_idx = flavor_idx; - } - - if ((c->flavor_idx == 0 || c->flavor_idx == -1)) { + c->flavor_idx = (flavor_idx == -1) ? 1 : flavor_idx; + } else if (is_fresh && (c->flavor_idx == 0 || c->flavor_idx == -1)) { + // Brand-new chara without a block: add_chara left the + // default 0, which we can't tell apart from "unset", so give it + // a known-valid flavor. Existing charas keep their stock value. c->flavor_idx = 1; printf("Setting default flavor for chara id %d\n", idx); } @@ -1191,7 +1207,7 @@ void musichax_core_init(struct popnhax_config *config, cur->chara1_birth_month, cur->chara2_birth_month, cur->chara3_birth_month, cur->chara1_birth_date, cur->chara2_birth_date, cur->chara3_birth_date, cur->style1, true, cur->style3, cur_style->fontface, cur_style->color, cur_style->height, - cur_style->width); + cur_style->width, true); } // Copy character database for use in the music database @@ -1293,8 +1309,10 @@ void musichax_core_init(struct popnhax_config *config, } // Add one extra song as padding + // (>= : when the highest added id is exactly the original table size, we + // still grew the table by one entry and need the padding/limit bump) { - if (max_music_id > (int64_t)music_size) { + if (max_music_id >= (int64_t)music_size - 1) { static const uint8_t diffs[6] = {0}; static const uint16_t charts[7] = {0}; static const uint16_t unk1[32] = {0}; @@ -1307,13 +1325,23 @@ void musichax_core_init(struct popnhax_config *config, (uint16_t *)&display_bpm[0], (uint8_t *)&hold_flags[0], true); } - if (max_chara_id > (int64_t)chara_size) { + if (max_chara_id >= (int64_t)chara_size - 1) { add_chara(max_chara_id + 1, add_string((uint8_t *)"bamb_1a"), 0x31, add_string((uint8_t *)"22"), add_string((uint8_t *)"gg_bamb_1a"), add_string((uint8_t *)"cs_bamb_1a"), add_string((uint8_t *)"cs_a"), add_string((uint8_t *)"cs_b"), 240, 220, 0, 0, 0, NULL, NULL, 53, get_lapis_shape_id((uint8_t *)"dia"), get_lapis_color_id((uint8_t *)"yellow"), NULL, NULL, -1, 0); } + + // Flavors follow the same max-vs-count convention as music/chara + // above: the filler entry makes new_flavor_size come out as stock + // size + number of flavors added, which is the limit_diff the + // select-slot manager patches (method 14) are scaled by. Only + // reachable when g_flavor_slots_expansion allowed growth. + if (max_flavor_id >= (int64_t)flavor_size - 1) { + add_flavor(max_flavor_id + 1, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0, 0, 0, 0, + 0, 0, 0, false, 0, 0, 0, 0, 0, false); + } } if (!is_expansion_allowed) { @@ -1322,6 +1350,9 @@ void musichax_core_init(struct popnhax_config *config, fontmax_style_id = style_size; max_flavor_id = flavor_size; max_chart_id = chart_size; + } else { + fontmax_style_id++; + max_chart_id++; } if (is_redirection_allowed) { @@ -1373,7 +1404,11 @@ void musichax_core_init(struct popnhax_config *config, character_entry *c = (character_entry *)*new_chara_table; for (uint64_t i = 0; i < *new_chara_size; i++) { - uint32_t new_flags = c[i].flags & ~3; + // Only clear bit 0 (locked, unlockable by play). Bit 1 marks + // hidden/NPC entries (licensed-song mascots, MZD staff variants) + // that are never player-selectable; clearing it leaks them into + // the character-select roster. + uint32_t new_flags = c[i].flags & ~1; if (new_flags != c[i].flags && c[i].disp_name_ptr != NULL && strlen((char*)c[i].disp_name_ptr) > 0) { printf("Unlocking [%04lld] %s... %08x -> %08x\n", i, c[i].disp_name_ptr, c[i].flags, new_flags); diff --git a/popnhax/loader.h b/popnhax/loader.h index 161a83c..554458e 100644 --- a/popnhax/loader.h +++ b/popnhax/loader.h @@ -6,6 +6,11 @@ int8_t get_chart_type_override(uint8_t *, uint32_t, uint32_t); +// Set by dllmain before musichax_core_init when the generated patch set +// covers the flavor slot manager (one inline slot vector per +// flavor); flavor table growth is only safe with those patches applied. +extern bool g_flavor_slots_expansion; + void musichax_core_init(struct popnhax_config *config, char *target_datecode, char *base_data, uint64_t music_size, uint64_t *new_music_size, char *orig_music_data, uint8_t **new_music_table, diff --git a/popnhax/omnimix_patch.cc b/popnhax/omnimix_patch.cc index 46b5b6e..a0315c8 100644 --- a/popnhax/omnimix_patch.cc +++ b/popnhax/omnimix_patch.cc @@ -744,12 +744,21 @@ bool make_omnimix_patch(const char *dllFilename, membuf_t *membuf, char *datecod membuf_printf(membuf, "\t\n"); /* create update_patches */ - update_patches_t update_patches[23] = { + update_patches_t update_patches[26] = { { .idx = MUSIC_IDX, .patch_method = 0, .value = g_tables[MUSIC_IDX].limit - 1 }, { .idx = MUSIC_IDX, .patch_method = 0, .value = g_tables[MUSIC_IDX].limit }, { .idx = CHART_IDX, .patch_method = 0, .value = g_tables[CHART_IDX].limit }, { .idx = CHART_IDX, .patch_method = 0, .value = g_tables[CHART_IDX].limit - 1 }, { .idx = CHARA_IDX, .patch_method = 0, .value = g_tables[CHARA_IDX].limit }, + // Chara idx clamps ("cmp ax, limit-1; ja invalid" sites) and the + // "Illegal Chara No" range check ("idx+7 <= limit+6", accounts for the + // 7 special negative chara ids). Without these, chara ids beyond the + // stock table fall back to the default chara, which is what prevented + // custom characters from working. + { .idx = CHARA_IDX, .patch_method = 0, .value = g_tables[CHARA_IDX].limit - 1 }, + { .idx = CHARA_IDX, .patch_method = 0, .value = g_tables[CHARA_IDX].limit + 6 }, + // Heap alloc of a per-chara u16 array sized (limit+6)*2 (netvs) + { .idx = CHARA_IDX, .patch_method = 13, .value = (g_tables[CHARA_IDX].limit + 6) * 2 }, { .idx = FLAVOR_IDX, .patch_method = 0, .value = g_tables[FLAVOR_IDX].limit - 1 }, { .idx = FLAVOR_IDX, .patch_method = 0, .value = g_tables[FLAVOR_IDX].limit }, @@ -775,7 +784,7 @@ bool make_omnimix_patch(const char *dllFilename, membuf_t *membuf, char *datecod }; LOG("popnhax: patch_db_gen: other_patches... "); - for (int i = 0; i < 23; i++) + for (size_t i = 0; i < sizeof(update_patches)/sizeof(update_patches[0]); i++) { uint32_t j = update_patches[i].value; bst_t *bst = bst_search(g_xref_bst, j); @@ -798,6 +807,74 @@ bool make_omnimix_patch(const char *dllFilename, membuf_t *membuf, char *datecod } } + /* Flavor table slot manager: the select screen allocates a manager + * object holding one inline 0x18-byte slot vector per flavor (a chara's + * select slot index is its flavor_idx, while charas sharing a flavor are the + * variations cycled within one slot), followed by further members. + * Growing the flavor table therefore also requires rebasing the + * allocation size and every member offset located after that per-flavor + * array by limit_diff*0x18 (patch method 14), on top of the plain limit + * immediates handled above. The delta list below matches the Jam&Fizz + * layout and it is only emitted when the ctor bytecode matches + * ("push ; push 0x18" and "lea ecx, [esi+]"), + * so on a game version with a different layout nothing is emitted and + * the loader keeps the flavor table capped. + * Please note that I only tested this under a recent build of J&F, + * and it might not work on an earlier release even. */ + { + uint32_t flavor_limit = g_tables[FLAVOR_IDX].limit; + uint32_t slot_array_end = flavor_limit * 0x18 + 4; + + char ctor_pat[7] = { '\x68', 0, 0, 0, 0, '\x6A', '\x18' }; + memcpy(&ctor_pat[1], &flavor_limit, 4); + char lea_pat[6] = { '\x8D', '\x8E', 0, 0, 0, 0 }; + memcpy(&lea_pat[2], &slot_array_end, 4); + + if (find_binary(buf, dllSize, ctor_pat, 7, 0, 0) != NULL && + find_binary(buf, dllSize, lea_pat, 6, 0, 0) != NULL) + { + LOG("popnhax: patch_db_gen: select_slot_patches... "); + /* member offsets relative to the end of the per-flavor slot + * array, from the manager ctor/dtor and their SEH unwind + * funclets; the last entry is the manager allocation size */ + static const uint16_t slot_mgr_deltas[] = { + 0x000, 0x010, 0x0D8, 0x0E8, 0x108, 0x114, 0x118, 0x11C, 0x120, + 0x12C, 0x130, 0x134, 0x138, 0x144, 0x148, 0x14C, 0x150, 0x160, + 0x420, 0x42C, 0x430, 0x434, 0x438, 0x444, 0x448, 0x44C, 0x450, + 0x45C, 0x460, 0x464, 0x468, 0x474, 0x478, 0x47C, 0x480, 0x48C, + 0x490, 0x494, 0x498, 0x4A4, 0x4A8, 0x4AC, 0x4B0, 0x4BC, 0x4C0, + 0x4C4, 0x4C8, 0x4D4, 0x4D8, 0x4DC, 0x4E0, 0x4EC, 0x4F0, 0x4F4, + 0x4F8, 0x4FC, 0x500, 0x504, 0x508, 0x50C, 0x51C, 0x524, 0x53C, + }; + + for (size_t i = 0; i < sizeof(slot_mgr_deltas)/sizeof(slot_mgr_deltas[0]); i++) + { + uint32_t j = slot_array_end + slot_mgr_deltas[i]; + bst_t *bst = bst_search(g_xref_bst, j); + if ( bst != NULL ) + { + offset_list_t *curr_elem = (offset_list_t*)(bst->extra_data); + while ( curr_elem ) + { + char line[LINE_SIZE]; + line[0] = '\0'; + int32_t addr_offset = parse_instruction(text, text_size, buf_reloc_delta, curr_elem->offset, j, line); + + if (addr_offset != -1) + { + membuf_printf(membuf, "\t\t\n", line); + membuf_printf(membuf, "\t\t<%s __type=\"str\" method=\"14\" expected=\"0x%x\">0x%0x\n\n", g_tables[FLAVOR_IDX].name, j, (uint32_t)text_base_addr+(curr_elem->offset)+addr_offset, g_tables[FLAVOR_IDX].name); + } + curr_elem = curr_elem->next; + } + } + } + LOG("done\n"); + } else { + LOG("popnhax: patch_db_gen: select_slot_patches: unknown select manager layout, flavor table growth will stay capped\n"); + } + } + update_patches_t weird_update_patches[4]; find_weird_update_patches(buf, dllSize, g_tables[MUSIC_IDX].limit, weird_update_patches, false); for (int i = 0; i < 4; i++) diff --git a/util/membuf.cc b/util/membuf.cc index 5b66173..5a8844e 100644 --- a/util/membuf.cc +++ b/util/membuf.cc @@ -10,11 +10,19 @@ membuf_t* membuf_new(uint32_t size) { char *buffer = (char *)malloc(size); membuf_t *res = (membuf_t *)malloc(sizeof(membuf_t)); - + + if (buffer == NULL || res == NULL) + { + free(buffer); + free(res); + return NULL; + } + res->buffer = buffer; res->size = 0; res->idx = 0; - + res->capacity = size; + return res; } @@ -34,12 +42,34 @@ int membuf_printf(membuf_t *membuf, const char *fmt, ...) va_list args; char line[256]; va_start(args, fmt); - vsprintf(line, fmt, args); + vsnprintf(line, sizeof(line), fmt, args); va_end(args); + + uint32_t len = strlen(line); + + // grow the buffer as needed (+1 for the NUL terminator) + if (membuf->idx + len + 1 > membuf->capacity) + { + uint32_t new_capacity = membuf->capacity * 2; + while (membuf->idx + len + 1 > new_capacity) + new_capacity *= 2; + + char *new_buffer = (char *)realloc(membuf->buffer, new_capacity); + if (new_buffer == NULL) + { + printf("membuf: couldn't grow buffer from %u to %u bytes, dropping write\n", + membuf->capacity, new_capacity); + return 0; + } + + membuf->buffer = new_buffer; + membuf->capacity = new_capacity; + } + strcpy((membuf->buffer)+(membuf->idx), line); - membuf->size += strlen(line); - membuf->idx += strlen(line); - return strlen(line); + membuf->size += len; + membuf->idx += len; + return len; } bool membuf_tofile(membuf_t *membuf, char *filepath) diff --git a/util/membuf.h b/util/membuf.h index 480fc3a..ffc48d1 100644 --- a/util/membuf.h +++ b/util/membuf.h @@ -7,6 +7,7 @@ typedef struct membuf_s { char *buffer; uint32_t size; uint32_t idx; + uint32_t capacity; } membuf_t; membuf_t* membuf_new(uint32_t size);