# This patch changes the number of BB character save slots from 4 to any number up to 127. # This patch is for documentation purposes only; it works when used as a server patch via newserv, but is decidedly # inconvenient to use via this method. This is because it affects logic that runs before any patches can be sent by the # server, so the player has to connect once to get the patch, then disconnect and connect again to use the additional # slots. # As written, this patch changes the slot count from 4 to 12. To use a different slot count, first compute the # following values: # slot count = your desired number of player slots (must be >= 4, <= 127) # total file size = (slot count * 0x2EA4) + 0x14 # bgm_test_songs_unlocked offset = total file size - 0x10 # save_count offset = total file size - 8 # round2_seed offset = total file size - 4 # Then, for each of the above, search for the string to the left of the = sign and change the values used in all of the # matching lines. .meta name="More save slots" .meta description="" .versions 59NJ 59NL entry_ptr: reloc0: .offsetof start # Include a few functions first write_call_to_code: .include WriteCallToCode memcpy: .include CopyData ret start: # Apply all necessary patches call apply_enable_scroll_patch call apply_fix_scroll_patch1 call apply_fix_scroll_patch2 call apply_fix_file_index call apply_preview_window_fix call apply_static_patches # Rewrite the existing char file regions to have the appropriate size; this # must be done after the patches are applied because we call the checksum # function, which is patched by one of the above calls call update_existing_char_file_list jmp update_existing_char_file_list_memcard apply_enable_scroll_patch: # This patch enables scrolling behavior within the character list push -5 # Jump size (negative = jmp instead of call) push # Jump address call get_code_size_for_enable_scroll .deltaof enable_scroll_start, enable_scroll_end get_code_size_for_enable_scroll: pop eax push dword [eax] call enable_scroll_end enable_scroll_start: mov eax, dword ptr [edi + 0x28] # cursor = char_select_menu->cursor_obj (TAdSelectCurGC*) or dword [eax + 0x01F8], 3 # cursor->flags |= 3 # Enable scrolling mov eax, [] # scroll_bar = TAdScrollBarXb_objs[0] mov ecx, [eax + 0xEC] # ecx = scroll_bar->client_id imul ecx, ecx, 0x24 # Set up scroll bar graphics (in struct at scroll_bar + 0x1C) mov dword [eax + ecx + 0x1C], 0x439D0000 mov dword [eax + ecx + 0x20], 0x43360000 mov dword [eax + ecx + 0x24], 0x439D0000 mov dword [eax + ecx + 0x28], 0x4392AB85 mov dword [eax + ecx + 0x2C], 0x40400000 mov dword [eax + ecx + 0x30], 0x425EA3D7 mov dword [eax + ecx + 0x34], 0x00000008 mov dword [eax + ecx + 0x38], 0x00000000 mov dword [eax + ecx + 0x3C], 0x00000000 or dword [eax + 0xF0], 1 # scroll_bar->flags |= 1 mov ecx, [eax + 0xEC] shl ecx, 4 mov dword [eax + ecx + 0xAC], 0 # scroll_bar->selection_state[client_id].scroll_offset = 0 mov dword [eax + ecx + 0xB0], 0 # scroll_bar->selection_state[client_id].selected_index = 0 mov dword [eax + ecx + 0xB4], 4 # scroll_bar->selection_state[client_id].num_items_in_view = 4 mov dword [eax + ecx + 0xB8], 0x0B # scroll_bar->selection_state[client_id].last_item_index = (slot count - 1) pop edi ret enable_scroll_end: call write_call_to_code ret apply_fix_scroll_patch1: # This patch fixes character selection cursor object so it will take the scroll offset into account push 6 # Call size push # Call address call get_code_size_for_fix_scroll_patch1 .deltaof fix_scroll_patch1_start, fix_scroll_patch1_end get_code_size_for_fix_scroll_patch1: pop eax push dword [eax] call fix_scroll_patch1_end fix_scroll_patch1_start: mov edx, [edi + 0x28] # cursor = this->ad_select_cur_obj (TAdSelectCurGC*) mov ebp, [edx + 0x44] # ebp = cursor->selected_index_within_view mov eax, [] # scroll_bar = TAdScrollBarXb_objs[0] add ebp, [eax + 0xAC] # ebp += scroll_bar->selection_state[0].scroll_offset ret fix_scroll_patch1_end: call write_call_to_code ret apply_fix_scroll_patch2: # This patch changes the TAdSinglePlyChrSelectGC::selected_index_within_view to be the selected character's absolute # index (including scroll_offset), not the index only within the displayed four characters push 6 # Call size push # Call address call get_code_size_for_fix_scroll_patch2 .deltaof fix_scroll_patch2_start, fix_scroll_patch2_end get_code_size_for_fix_scroll_patch2: pop eax push dword [eax] call fix_scroll_patch2_end fix_scroll_patch2_start: mov eax, [] # scroll_bar = TAdScrollBarXb_objs[0] mov eax, [eax + 0xAC] # eax = scroll_bar->selection_state[0].scroll_offset mov edx, [edi + 0x28] # cursor = this->ad_select_cur_obj (TAdSelectCurGC*) add eax, [edx + 0x44] # eax += cursor->selected_index_within_view ret fix_scroll_patch2_end: call write_call_to_code ret apply_fix_file_index: # This patch fixes the character file indexing so it will account for the scroll position push 5 # Call size push # Call address call get_code_size_for_selection_index_fix2 .deltaof selection_index_fix2_start, selection_index_fix2_end get_code_size_for_selection_index_fix2: pop eax push dword [eax] call selection_index_fix2_end selection_index_fix2_start: mov eax, [] mov eax, [eax + 0xAC] # eax = TAdScrollBarXb_objs[0]->selection_state[0].scroll_offset add ebp, eax # arg0 += eax mov [esp + 4], ebp mov eax, jmp eax # set_current_char_slot selection_index_fix2_end: call write_call_to_code ret apply_preview_window_fix: # This patch fixes the preview display so it will show the correct section ID, level, etc. push 5 # Call size push 0x0040216C call get_code_size_for_preview_window_fix .deltaof preview_window_fix_start, preview_window_fix_end get_code_size_for_preview_window_fix: pop eax push dword [eax] call preview_window_fix_end preview_window_fix_start: mov eax, [] # scroll_bar = TAdScrollBarXb_objs[0] mov eax, [eax + 0xAC] # eax = scroll_bar->selection_state[0].scroll_offset add [esp + 4], eax mov eax, # get_player_preview_info jmp eax preview_window_fix_end: # This patch applies in two places, so push the second set of args now, then # apply it twice push 5 # Call size push 0x00401842 # Call address push dword [esp + 0x10] # Code size push dword [esp + 0x10] # Code address call write_call_to_code call write_call_to_code ret apply_static_patches: .include WriteCodeBlocks # These patches change various places where the character data size and slot count are referenced .data .data 0x00000001 .binary 0C # slot count; TDataProtocol::handle_E5 .data .data 0x00000001 .binary 0C # slot count; import_player_preview .data .data 0x00000001 .binary 0C # slot count; TDataProtocol::handle_E4 .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000004 .data 0x00022FBC # save_count offset .data .data 0x00000004 .data 0x00022FBC # save_count offset .data .data 0x00000004 .data 0x00022FBC # save_count offset .data .data 0x00000004 .data 0x00022FC0 # round2_seed offset .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000004 .data 0x00022FBC # save_count offset .data .data 0x00000004 .data 0x00022FBC # save_count offset .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000004 .data 0x0000005D # memcard block count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000001 .binary 0C # slot count .data .data 0x00000004 .data 0x00022FC4 # total file size .data .data 0x00000004 .data 0x00022FB4 # bgm_test_songs_unlocked offset # Signature check on all save files (rewritten as loop) .data .deltaof sig_check_begin, sig_check_end sig_check_begin: mov edx, 0xC87ED5B1 # Expected signature value add eax, 0x04E8 # &char_file_list->chars[0].part2.signature mov ecx, 0x0C # slot count again: cmp dword [eax], 0 # signature == 0 (no char in slot) je sig_ok cmp dword [eax], edx # signature == expected value jne sig_bad sig_ok: add eax, 0x2EA4 # Advance to next slot dec ecx jnz again xor eax, eax # All signatures OK (eax = 0) jmp sig_check_end sig_bad: xor eax, eax # Bad signature (eax = 1) inc eax jmp sig_check_end .binary CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC sig_check_end: # # Send slot count in E3 command .data # TDataProtocol::send_E3_for_index .deltaof send_slot_count_in_E3_begin, send_slot_count_in_E3_end send_slot_count_in_E3_begin: # ecx = this (TDataProtocol*) # [esp + 4] = slot_index push 0 push dword [esp + 8] # slot_index push 0x0C # slot count push 0x00E30010 mov eax, esp push 0x10 push eax mov eax, [ecx] call [eax + 0x20] # this->send_command(&cmd, 0x10) // ret 8 add esp, 8 mov eax, call eax # set_current_char_slot(slot_index) // ret 0 add esp, 8 ret 4 send_slot_count_in_E3_end: # Show slot number in each menu item .data 0x00401D57 .deltaof show_slot_number_begin, show_slot_number_end show_slot_number_begin: # Original call (sprintf(line_buf, "LV%d", preview_info->visual.disp.level + 1)) lea edx, [esp + 0x02C4] mov ebx, [ebx + 8] inc ebx push ebx mov ecx, esi push edx mov eax, 0x00402604 call eax # Find the end of the string lea eax, [esp + 0x02C4] show_slot_number_strend_again: cmp word [eax], 0 je show_slot_number_strend_done add eax, 2 jmp show_slot_number_strend_again show_slot_number_strend_done: # Format the slot number and append it to the string mov ecx, [] # scroll_bar = TAdScrollBarXb_objs[0] mov ecx, [ecx + 0xAC] # ecx = scroll_bar->selection_state[0].scroll_offset lea ecx, [ecx + ebp + 1] push ecx # Slot number (scroll_offset + z) call get_show_slot_number_suffix_fmt .binary 20002800230025006400290020000000 # L" (#%d) " get_show_slot_number_suffix_fmt: push eax # Destination buffer mov eax, # _swprintf call eax add esp, 0x0C jmp show_slot_number_end .zero 0x96 show_slot_number_end: # 00401E4D # End static patches .data 0x00000000 .data 0x00000000 update_existing_char_file_list: # Replace the existing character list with an appropriately-longer one. This part does not need to be done if the # patch is applied statically to the executable; this is only necessary when used as a server patch because the # character list is already allocated at the time the patch is applied. push 0x00022FC4 # total file size mov eax, # operator_new call eax add esp, 4 mov edx, [] # edx = old char_file_list mov [], eax mov ecx, [edx + 0xBA94] # Copy bgm_test_songs_unlocked_high to new file mov [eax + 0x00022FB4], ecx mov ecx, [edx + 0xBA98] # Copy bgm_test_songs_unlocked_low to new file mov [eax + 0x00022FB8], ecx mov ecx, [edx + 0xBA9C] # Copy save_count to new file mov [eax + 0x00022FBC], ecx mov ecx, [edx + 0xBAA0] # Copy round2_seed to new file mov [eax + 0x00022FC0], ecx add eax, 4 add edx, 4 mov ecx, 0xBA90 call memcpy # Copy the existing 4 characters over mov eax, [] add eax, 0xBA94 mov ecx, 4 clear_next_char: cmp ecx, 0x0C # slot count jge clear_next_char_done lea edx, [eax + 0x2EA4] # edx = ptr to next char (or footer) clear_next_char_write_again: mov dword [eax], 0 add eax, 4 cmp eax, edx jl clear_next_char_write_again clear_next_char_done: # Call eh_vector_constructor_iterator( # &char_file_list.chars[4], # sizeof(char_file_list.chars[0]), # countof(char_file_list.chars) - 4, # PSOCharacterFile::init, # PSOCharacterFile::destroy) push # PSOCharacterFile::destroy push # PSOCharacterFile::init push 0x08 # slot count - 4 push 0x2EA4 # sizeof(PSOCharacterFile) mov eax, [] add eax, 0xBA94 push eax mov eax, call eax # Fix the file's checksum mov eax, [] mov ecx, jmp ecx # PSOBBCharacterFileList::checksum(char_file_list) update_existing_char_file_list_memcard: # Allocate a new memory card file area and copy the data there too. It seems Sega didn't fully strip out the local # saving code from PSOBB; instead, they just made it write to a heap-allocated buffer. Since the file is much bigger # now, we also have to make that heap-allocated buffer larger. We add a few "blocks" on the end, since the original # code in the game does that too, but it's probably not strictly necessary. Like the above, this part is not # necessary if this patch is statically applied to the executable. mov eax, 0x00022FC4 # total file size add eax, 0x0000FFFF and eax, 0xFFFFC000 push eax mov eax, call eax # malloc10(total file size) add esp, 4 mov [], eax mov edx, [] mov ecx, 0x00022FC4 # total file size jmp memcpy