Advertiser Content

Development [FR] Trainer Interrupting Battle

Started by ghoulslash November 17th, 2018 11:30 AM
  • 1683 views
  • 8 replies
Male
Seen 11 Hours Ago
Posted 2 Days Ago
122 posts
3.6 Years
I know this was implemented in the Emerald battle engine upgrade, and there was some work done by FBI a few years ago on it, but I want to have a collaborative effort on getting this working for Fire Red, especially for hacks that won't use the dissasembly projects.

EDIT: Delta posted a link to a more completed version.

I researched this topic a year ago or so and was able to get a working version with some graphical side effects that I will explain later.

Background info for the curious:
Spoiler:
There are likely multiple ways to do this; the easiest in my opinion is to use battle scripts. There is already a battle script command that brings the trainer sprite out. This command is cmd53, and it takes an argument either 0x0 or 0x1: the former will bring the player sprite out (there is potentially utility here..), or 0x1 for the opposing trainer (not sure what would happen if used in a wild battle).

The problem is that the game never intended to have a battle continue if this command was called, so that the object defined for animation targets gets set to the trainer sprite. These obj IDs are stored in 0x02023d44 in the same order as the banks would be (player 1, enemy 1, player 2, enemy 2).

The other problem is that (as hard as I was willing to look back when I originally researched this) there is no trainer_slide_out command. There is certainly a function to do it, as that is what happens at the beginning of the the trainer battle, but in lieu of finding this, I used whirlwind's animation.

Of course, the trainer sprite object still exists, so we should delete it so that it does not get used in attack animations. The aforementioned graphical issue is related to this; the trainer sprite gets stuck in VRAM, so several moves (Pound, Agility, Quick Attack, etc), generate the trainer sprite again even when the object is deleted. I do not know enough about VRAM functions or the object data structure to know how to fix this at the moment, though it's been a while since I looked at most of this stuff.


Implementation:
So wherever we want to have the trainer interrupt, we need a battle script to be run at that point in the battle. A logical place for the end-of-turn would be after all of the weather, etc checks that occur. Fortunately, there is a table at 0x17bc4 that runs end-of-turn routines and battle scripts if necessary. There is a limiter at 0x17b98 so we can easily repoint and expand this table to add more global end-of-turn effects (note that the last entry in the original table must always be last)

We can run a battle script from here to make the trainer enter the screen, play a custom string, and then use my whirlwind animation hack to push the trainer offscreen, resetting all of the object targets (introduced in the spoiler above).

Note: this works from a functional standpoint, but the trainer sprite is still prevalent in the VRAM or object data structure somehow and will show up again in several attacks such as Pound, Agility, etc. Entering the pokemon menu and returning fixes this problem.


Here is a test case using the global end-of-turn battle scripts. The following code expands the global effect table and loads a string and battle script if a specific byte in RAM is set (so you can load a certain number of strings). It includes the whirlwind anim hack. It deletes the trainer object afterwards but the aforementioned graphical issue is not solved.
Spoiler:

Main code: compile and insert, noting the .org locations
.text
.align 2
.thumb
.thumb_func
.global TrainerInterrupt_EndTurn

.include "bs_macros.asm"

.equ Offset, 0xYYYYYY  @ insert loc (no 0x08 prefix)
.equ romsize, 0x08000000
.equ var8001, 0x020370ba
.equ var8002, 0x020370bc
.equ ATK_WHIRLWIND, 0x12

@ change this if you want
.equ SlideInRAM, 0x0203e05d



.org 0x17b98, 0xff
.byte 0xb

.org 0x17bc0, 0xff
.word (romsize+GlobalEffects)

.org 0x72688, 0xff
.byte 0x0, 0x48, 0x0, 0x47
.word (romsize+anim_hack+1)

.org Offset, 0xff
.align 2
anim_hack:
	ldr r1, [sp]	@move anim table
	add r0, r3, r1
	
CheckTrainerSlide:
	ldr r1, =SlideInRAM
	ldrb r1, [r1, #0x1]		@0x0203e05e - changes whirlwind anim (change if you want)
	cmp r1, #0x0
	bne TrainerSlideOut
	
NormalAnim:	
	ldr r0, [r0, #0x0]
	b StoreAnim
	
TrainerSlideOut:
	ldr r0, =(0x081c9a24)
	
StoreAnim:
	str r0, [r5]
	
Return:
	ldr r0, =(0x08072690 +1)
	bx r0	


battle_script:
copyarray var8002 0x02023d44 0x4		@ save object banks into var8002-3
cmd53 0x1	@ trainer slide out
waitstate
printstring 0x184	@ print string
pause 0x20
sethalfword AttackRAM ATK_WHIRLWIND
copyarray var8001 TargetBank 0x1		@ save target bank into var8001
setbyte TargetBank 0x1
setbyte 0x0203e05e 0x1		@ whirlwind anim hack
attackanimation
waitanimation
pause 0x20
setbyte SlideInRAM 0x0		@ reset trainer slide
setbyte 0x0203e05e 0x0		@ reset whirlwind anim hack
copyarray TargetBank var8001 0x1	@ return target bank
callasm romsize+reset_objs+1		@ reset object banks
end2

.align 2
GlobalEffects:
.word 0x08017bf0
.word 0x08017c76
.word 0x08017d3c
.word 0x08017e08
.word 0x08017ee4
.word 0x08017f9c
.word 0x08018050
.word 0x080180d4
.word 0x08018144
.word 0x0801819c
.word (romsize+slide_in)
.word 0x08018220	@must be last

slide_in:
	ldr r0, =SlideInRAM		@ram to activate, load string from table
	ldrb r0, [r0]	
	cmp r0, #0x0	@ does nothing if set to zero
	beq NoSlide
	
LoadString:
	sub r0, #0x1	@ start at 0
	lsl r0, r0, #0x2
	ldr r1, =(romsize+StringTable)
	add r0, r0, r1
	ldr r0, [r0]
	ldr r1, .Setword
	str r0, [r1]		@ set string to play
	
LoadScript:
	ldr r1, .ScriptPointer
	ldr r0, =(romsize+battle_script)
	str r0, [r1]
	ldr r3, =(0x080181dc +1)
	bx r3	
	
NoSlide:
	ldr r2, =(0x080181fc +1)
	bx r2
	
.align 2
StringTable:
.word (romsize+String)
@etc...	

.align 2
reset_objs:
	push {r4, lr}
	ldr r0, .ObjectBanks
	ldrb r1, [r0, #0x1]		@ trainer sprite obj
	mov r4, r1
	ldr r5, =var8002
	ldr r0, [r5]	@ old object banks
	ldr r1, .ObjectBanks
	str r0, [r1]	@ return object bank data
	mov r0, #68
	mul r0, r1
	ldr r1, .Objects
	add r0, r0, r1
	bl ObjDelete		@ delete trainer sprite obj
	
exit_loop:
	pop {r4}
	pop {r0}
	bx r0
	
	
ObjDelete:
	ldr r2, =(0x08076030 +1)
	bx r2
	
.align 2
.Setword:	.word 0x0203c020
.ScriptPointer:	.word 0x02023d74
.ObjectBanks:	.word 0x02023d44
.Objects:	.word 0x0202063c

String:	.byte S_, c_, a_, r_, e_, d_, Space, P_, o_, t_, t_, e_, r_, QMark, NewBox, 0xff
You'll need to download the attached bs_macros.asm and insert this sethalfword command I wrote as well (or can just use setbyte since Whirlwind is 1 byte). I have it as command 0xFB
.text
.align 2
.thumb
.thumb_func
.global sethalfword_battle
/*
battle command to set half word
command 0xfb
insert pointer+1 at [battle_commands + 4*0xFB] to make this battle script command 0xFB
*/

main:
	push {r1-r4, lr}
	ldr r0, .ScriptPointer
	ldr r0, [r0, #0x0]		@script loc
	ldrb r1, [r0, #0x1]
	ldrb r2, [r0, #0x2]
	lsl r2, r2, #0x08
	orr r1, r2
	ldrb r2, [r0, #0x3]
	lsl r2, r2, #0x10
	orr r1, r2
	ldrb r2, [r0, #0x4]
	lsl r2, r2, #0x18
	orr r1, r2
	
	ldr r0, .ScriptPointer
	ldr r2, [r0]
	ldrb r3, [r2, #0x5]
	ldrb r4, [r2, #0x6]
	lsl r4, r4, #0x8
	orr r3, r4
	strh r3, [r1]
	add r2, #0x7
	str r2, [r0]
	
end:
	pop {r1-r4}
	pop {r0}
	bx r0
	
.align 2
.ScriptPointer:	.word 0x02023d74


Of course, there are plenty of other applications and ways to implement this design. The meat of the research is around cmd53 and the pokemon objects. The main holdup for this to be complete is finding a way to completely remove the trainer sprite from the graphics after the fact. This may be a trivial solution with a simple in-game function, but I have limited experience with graphics-based asm. I will note that going into the pokemon party menu and returning fixes the problem, making me think that it has something to do with the object data.
Male
Seen 11 Hours Ago
Posted 2 Days Ago
122 posts
3.6 Years
Someone has already done it.
Good find! I'll update the main post.

I have problems about compiling. I download the asm file, but if it compiles it is empty ...
What should I write inside?
For example I insert first routine at 900000, second routine at A00000, I must edit them?
Thancks for help :)
For assembly files with .org locations, it will assemble empty files except for the data at the specified offsets, so the assembled file just looks empty. Of course, I would refer you to Delta's posted link since it is more complete and more functional than my test case :)
Seen 1 Hour Ago
Posted 3 Weeks Ago
46 posts
4.8 Years
I have a compatibility problem with this version https://gitgud.io/pfero/trainer_sliding and the MrDollSteak's Decap. and Attack Rombase (Version 1.5a).

I change source/scripts.h and source/scripts.s with 0x300 (before 0x200)
I change version/firered/insert.asm:bs_execute_handle and source/script_commands.h with 0xFF (before 0xF8).
But...it doesn't work :(
I see the offset 0x080d77be is used by both patch...I don't understand how to fix this problem...Help me please :(

Delta231

A noob

Male
India
Seen 3 Days Ago
Posted August 18th, 2019
689 posts
3.4 Years
I have a compatibility problem with this version https://gitgud.io/pfero/trainer_sliding and the MrDollSteak's Decap. and Attack Rombase (Version 1.5a).

I change source/scripts.h and source/scripts.s with 0x300 (before 0x200)
I change version/firered/insert.asm:bs_execute_handle and source/script_commands.h with 0xFF (before 0xF8).
But...it doesn't work :(
I see the offset 0x080d77be is used by both patch...I don't understand how to fix this problem...Help me please :(
You just need to change linker script which is in version/firered and read install.md as well for compiling instructions.

There is a big note there I will put it here.


The patch modifies the battle string loading function, to load a custom string, for string 0x200 (by default). This only happens when using a string ID bigger than 0x17C (for Emerald) or 0x181 (for Fire Red). If you've applied a patch that repoints the table or otherwise modifies the code to expand the table, you might encounter problems with it. This might happen for example when combined with patches that add new move effects. Consider either modifying the string ID used in the patch (source/scripts.h and source/scripts.s) or modifying string_hook (in the corresponding version/ directory) to do whatever suits you best.

Additionally, for Fire Red only, this patch implements the trainer_back_slide battle scripting command. This isn't done by repointing and expanding the battle scripting command table, but by hooking one of the functions that reads it, and overriding the behavior for command 0xf8. If you've applied any patch that repoints this table or otherwise modifies the code to expand the table, you might encounter problems with it. This might happen for example when combined with patches that add new move effects. Consider modifying the command ID used in the patch (version/firered/insert.asm:bs_execute_handle and source/script_commands.h) and/or pointing the command to the right function (version/firered/insert.asm:battlecommand_trainer_back_slide, see the generated *.patched.sym file for an address).

Delta231

A noob

Male
India
Seen 3 Days Ago
Posted August 18th, 2019
689 posts
3.4 Years
I had already read those instructions and done what they said to do, but as I wrote at some point I can not continue because two routines come into conflict and a bug occurs.
Could elaborate?
Seen 1 Hour Ago
Posted 3 Weeks Ago
46 posts
4.8 Years
Solved (more or less...)!
If you want to apply in a hack rom patched by MrDollSteak's Decap. and Attack Rombase (Version 1.5a)...
1) change source/scripts.h and source/scripts.s with 0x300 (before 0x200);
2) change version/firered/insert.asm:bs_execute_handle and source/script_commands.h with 0xFF (before 0xF8);
3) edit trainer_sliding-master\version\firered like this:

.arm.little
.thumb

.open "firered.gba", "firered.patched.gba", 0x08000000

.definelabel free_space, 0x08(offset)

.definelabel battle_malloc_hook, 0x0802e0fa
.definelabel battle_malloc_ret, 0x0802e104|1

.definelabel battle_free_hook, 0x0802e1f8
.definelabel battle_free_ret, 0x0802e200|1

.definelabel sliding_hook, 0x08013c26
.definelabel sliding_hook_buffer, 0x02023DD0
.definelabel sliding_hook_turn_value_cleanup, 0x08015330|1
.definelabel sliding_hook_ret, 0x08013d20|1
.definelabel sliding_hook_continue, 0x08013c30|1

.definelabel string_hook, 0x080d77be
.definelabel string_hook_buffer, 0x0202298c
.definelabel string_hook_ret, 0x080d77e2|1
.definelabel string_hook_decode, 0x080d77dc|1

bs_commands equ 0x0895f480
.definelabel battlecommand_trainer_slide_ptr, (bs_commands + 0x53 * 4)
.definelabel battlecommand_trainer_slide, 0x080250dc|1

.definelabel bs_execute_hook, 0x08015c58
.definelabel battlecommand_trainer_back_slide_battler, 0x02023bc4
.definelabel battlecommand_trainer_back_slide_emit, 0x0800e114|1
.definelabel battlecommand_trainer_back_slide_unk, 0x08017248|1

.org battle_malloc_hook
    ldr r0, =battle_malloc_handle|1
    bx r0
.pool

.org battle_free_hook
    ldr r0, =battle_free_handle|1
    bx r0
.pool

.org sliding_hook
    ldr r0, =sliding_hook_handle|1
    bx r0
.pool

.org string_hook
    ldr r0, =string_hook_handle|1
    bx r0
.pool

.org battlecommand_trainer_slide_ptr
.word battlecommand_trainer_slide_handle|1

.org bs_execute_hook
    ldr r2, =bs_execute_handle|1
    bx r2
.pool


.org free_space
.incbin "trainer_sliding_firered.bin"
.include "trainer_sliding_firered.asm"

battle_malloc_handle:
; This hooks right after all of the other battle structs have been allocated

; State:
; r0-r3, lr are fair game.

	; Run the few instructions we've overwritten
    mov r0, r5
    ldr r1, =calloc
    bl @@bx_r1
    mov r1, r0
    str r1, [r4]

    bl battle_malloc

    ldr r1, =battle_malloc_ret
@@bx_r1:
    bx r1

.pool

battle_free_handle:
; This hooks right after all of the other battle structs have been freed

; State:
; r5 = 0
; r0-r3, lr are fair game.

    ; Run the few instructions we've overwritten
    ldr r0, [r4]
    ldr r1, =free
    bl @@bx_r1
    str r5, [r4]

    bl battle_free

    ldr r1, =battle_free_ret
@@bx_r1:
    bx r1

.pool

sliding_hook_handle:
; This hooks right after Perish Song/Future Sight are handled at the end of a turn.
; If a battle effect is executed, it's supposed to return early.

; State:
; r0-r3, lr are fair game.

; Return hooks:
; sliding_hook_ret:
;   Returns early from the hooked function.
; sliding_hook_continue:
;   Continues running the end-of-turn function, looking for other effects to
;   run and doing other end-of-turn-y stuff.

	bl turn_end_slide
	cmp r0, #0
	beq @@continue

	ldr r1, =sliding_hook_ret
	bx r1

@@continue:
	; Run the few instructions we've overwritten
    mov r0, #0
    ldr r1, =sliding_hook_turn_value_cleanup
    bl @@bx_r1
	ldr r2, =sliding_hook_buffer
	ldr r0, [r2]

	ldr r1, =sliding_hook_continue
@@bx_r1:
	bx r1

.pool

string_hook_handle:
; This hooks the default case in the battle string engine,
;  when a string ID is higher than the size of the string table.
; We can load our custom strings here.

; State:
; r6 = String ID
; r0-r10, lr are fair game.

; Addresses of interest:
; string_buffer:
;   Contains the resulting string to print. Not sure how big it is.
;   Should be filled with a single terminator (0xFF) if no string was found.

; Return hooks:
; string_hook_decode:
;   r7 = String pointer
;   Decodes the string, copying it to string_hook_buffer in the process.
; string_hook_ret:
;   Returns from the string loading function. Should have a valid string in
;   string_hook_buffer.

    mov r0, r6
    bl get_custom_string
    cmp r0, #0
    beq @@ret

    ; Decode the string
	mov r7, r0
	ldr r0, =string_hook_decode
	bx r0

@@ret:
	; Run the few instructions we've overwritten
	ldr r1, =string_hook_buffer
	mov r0, #0xFF
	strb r0, [r1]
	ldr r0, =string_hook_ret
	bx r0

.pool

battlecommand_trainer_slide_handle:
; This replaces the original trainer_slide command.

; We extend the trainer_slide command to handle some values in a different fashion.
; See slide_save_obj for details.

    push {lr}
    bl slide_save_obj
    cmp r0, #0
    pop {r0}
    bne @@bx_r0
    mov lr, r0
    ldr r0, =battlecommand_trainer_slide
@@bx_r0:
    bx r0

.pool

bs_execute_handle:
; This hooks the function used to run some battle scripts,
;  after fetching the next command ID from bs_pointer.
; There are more functions where the command IDs are interpreted,
;  but this is the only one that we need to interpret trainer_back_slide.

; State:
; r0 = Command ID
; r1 = Pointer to bs_commands
; r2, r3 and lr are fair game, the rest should be preserved.

    pop {r2}
    mov lr, r2

    cmp r0, #0xf9
    beq battlecommand_trainer_back_slide

    ; Run whatever command was called upon
    lsl r0, #2
    add r0, r1
    ldr r0, [r0]
    bx r0

.pool

battlecommand_trainer_back_slide:
; Port of trainer_back_slide from emerald into firered

    push {r4, r5, lr}

    ; Get the argument, make sure any value bigger than 1 is 1.
    ldr r5, =bs_pointer
    ldr r0, [r5]
    ldrb r0, [r0, #1]
    cmp r0, #0
    beq @@zero
    mov r0, #1
@@zero:

    ; Get and save the position
    ldr r1, =get_battler_at_position
    bl @@bx_r1
    ldr r4, =battlecommand_trainer_back_slide_battler
    strb r0, [r4]

    ; Emit a trainer slide back
    mov r0, #0
    ldr r1, =battlecommand_trainer_back_slide_emit  ; Fun fact: This function is never used in the game
    bl @@bx_r1

    ; Call an unknown function
    ldrb r0, [r4]
    ldr r1, =battlecommand_trainer_back_slide_unk
    bl @@bx_r1

    ; Skip to the next command
    ldr r0, [r5]
    add r0, #2
    str r0, [r5]

    pop {r4, r5}
    pop {r0}
    bx r0

@@bx_r1:
    bx r1

.pool

; Set up a dummy table for the sliding trainer messages.
_sliding_trainers:
    .word 0
    .word 0

.org sliding_trainers
.word _sliding_trainers

.close
Advertiser Content