Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion popnhax/dllmain.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
83 changes: 59 additions & 24 deletions popnhax/loader.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -74,6 +74,9 @@ void parse_musicdb(const char *input_filename, const char *target, struct popnha
std::map<uint32_t, int8_t> 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");

Expand Down Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -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) +
Expand Down Expand Up @@ -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 <id> 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 <flavor> 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);
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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};
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions popnhax/loader.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
81 changes: 79 additions & 2 deletions popnhax/omnimix_patch.cc
Original file line number Diff line number Diff line change
Expand Up @@ -744,12 +744,21 @@ bool make_omnimix_patch(const char *dllFilename, membuf_t *membuf, char *datecod

membuf_printf(membuf, "\t<other_patches>\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 },

Expand All @@ -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);
Expand All @@ -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 <flavor_limit>; push 0x18" and "lea ecx, [esi+<limit*0x18+4>]"),
* 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<!-- %s -->\n", line);
membuf_printf(membuf, "\t\t<%s __type=\"str\" method=\"14\" expected=\"0x%x\">0x%0x</%s>\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++)
Expand Down
42 changes: 36 additions & 6 deletions util/membuf.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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)
Expand Down
Loading