Hi all,
I've been beating my head against wall for 2 weeks and am hoping you can help me.
In all my TAS runs of Double Dragon Arcade - I've been pulling off a trick where I elbow enemies off the platform in Stage 2 (https://youtu.be/vok6SResKIE?list=PLo3SyZacAQ8ZVNpdn4tiYG6HH5I00Un-w&t=101).
I've been trying to optimize the sequence of input from my last submission, and am discovering I can't recreate this setup. I'm not even close - I'm 4 seconds slower than before. Even after 2 weeks of tinkering, debugging, macro testing, lua scripting yadda yadda, I can't recreate the glitch as quickly. Instead of falling off the cliffs, the enemies seem like they teleport in space as they fall past the pit ledge (something seems to be preventing them from falling all the way into pit below)
I've written LUA scripts to dump the relevant sprite RAM to screen while I test (x position, y positions of all sprites for each element on screen. As best as I can tell, the glitch where I can quickly elbow all enemies quickly over the ledge is dependent on Linda *NOT* being the first enemy the elbow hits. On top of that, there also seems to be a dependency on a certain scroll and x positioning on screen.
I am trying to figure out a reproducible set of conditions to create this glitch - and am flat out stuck (even after digging through source code for how sprite offsets are working). I would love some suggestions on debugging techniques I can use to help find exactly what controls whether or not these enemies are falling into pits. Any tips are welcome.
TLDR:
* Can't reproduce elbow off platform ledge
* Having enemies on exact same Y position as Player isn't enough to reproduce
* Linda (female character with whip) can't be the first enemy hit
* Some dependency on combination of Player's X-offset and Level Scroll
* Would love to figure out how to tell if game is going to execute instructions to fall on ledge, or fall into pit below
Thanks.
PS. Links to previous runs I'm trying to optimize:
http://tasvideos.org/5869S.htmlhttp://tasvideos.org/5957S.html
Joined: 4/17/2010
Posts: 11495
Location: Lake Chargoggagoggmanchauggagoggchaubunagungamaugg
Why won't you post the Lua scripts you've made?
Warning: When making decisions, I try to collect as much data as possible before actually deciding. I try to abstract away and see the principles behind real world events and people's opinions. I try to generalize them and turn into something clear and reusable. I hate depending on unpredictable and having to make lottery guesses. Any problem can be solved by systems thinking and acting.
function draw_enemy(base, id)
if memory.readbyteunsigned(base) < 0x80 then return end
local x = memory.readwordunsigned(base+4)
local y = memory.readwordunsigned(base+6)
local z = memory.readwordunsigned(base+8)
local hp = memory.readbyteunsigned(base+0x1F)
gui.text(0,id*8,string.format("%d: %4X %4X %4X %d", id, x, y, z, hp))
end
function draw_enemies()
for i = 0, 8 do
local base = 0x45E+0x55*i
draw_enemy(base,i)
end
end
gui.register(function()
draw_enemies()
end)
It shows positions and hp.
Make sure you have simlar all three coordinates... not just one or two.
I would call them X, Y, Z. X - left/right, Y - far/near, Z - top/bottom.
I have found corresponding code, but I don't wanna dig into it right now.
Tell me does it something new, or you had this already?
If you curious: at least two locations where Z is used: 5318, 40A0.
It means, that they are in bank, at offsets from 0x318 and 0x10A0.
Bank is most probably in 21j-2-3.25 at offset 0, so it's 0x318 and 0x10A0 there.
But you may just put type "bpset 5318" in debugger, and "bpset 40A0".
This is the script I've been adjusting to debug (I pieced this together from mame source code from video/ddragon.c, drivers/ddragon.c, and lots of time stepping through frame by frame with RAM Watch and RAM Search)
Language: lua
local player_1_hp = 0x03C1
local player_2_hp = 0x041F
local enemy_1 = 0x047D
local enemy_2 = 0x04D2
local enemy_3 = 0x0527
local enemy_4 = 0x057C
-- local enemy_5 = 0x05D1
-- local enemy_6 = 0x0626
-- local enemy_7 = 0x067B
-- local enemy_8 = 0x06D0
local boss = 0x0725
function sprite_data ()
gui.text(180,20,"P1 HP: " .. memory.readbyte(player_1_hp) )
gui.text(180,30,"P2 HP: " .. memory.readbyte(player_2_hp) )
gui.text(180,50,"E1 HP: " .. memory.readbyte(enemy_1) )
gui.text(180,60,"E2 HP: " .. memory.readbyte(enemy_2) )
gui.text(180,70,"E3 HP: " .. memory.readbyte(enemy_3) )
gui.text(180,80,"E4 HP: " .. memory.readbyte(enemy_4) )
gui.text(180,90,"Boss HP: " .. memory.readbyte(boss) )
gui_y = 20;
offset = 1;
-- sprite data starts at offset 2800
-- all characters appear to be comprised of
-- 4 separate sprites
for i=0x2800,0x2940,5 do
sx = memory.readbyte(i + 4);
sy = memory.readbyte(i + 0);
if sy < 140 then
dimension = bit.band(memory.readbyte(i + 1), 48);
which = memory.readbyte(i + 3) + bit.lshift(bit.band(memory.readbyte(i + 2),0x0f), 8);
gui.text(20,gui_y,"S" .. offset .. " X=" .. sx .. " Y=" .. sy); -- .. " DIM=" .. dimension .. " WHICH=" .. which);
gui_y = gui_y + 10;
offset = offset + 1;
end;
end;
end;
while true do
sprite_data();
emu.frameadvance()
end;
Oh wow that's much cleaner! Question - how were you able to find this table so quickly? I had been working from online videos for TAS'ing that mentioned using RAM Watch and RAM Search to find interesting addresses in memory - that's how I ended up finding the HP addresses (which aren't the same locations used in your script).
P.S. - I'm new to the world of TAS'ing and the general architecture/coding/design of all things Arcade game related, but I am very willing to take advise and learn techniques that help me figure things out myself.
To do it fast, you need two features in your emulator:
1) Ram Search
2) Breakpoints on write and pc.
Mame has both.
If your emulator doesn't have (1) you can use CheatEngine.
But then you need to convert host addresses (x86 for example) into game addresses.
Breakpoints can't be replaced.
So, this is basic tools that you need to make research quickly.
To use that tools in mame, you need to know that there is "-debug" option.
So, to run debugger you need to run mame like this:
mame-rr.exe dragonw -debug
This will start your game with debugger opened.
Then, you may type "help" in bottom input field.
it will show help.
First, you need to have idea how are you willing to find your info.
In this game, idea is simple.
Find hp, because you need it anyway.
First, you need to start level in the game and pause the game using P button.
Then you do Ram Search:
1) you type "help", to see topics.
2) you type "help cheats", because ram search is in this category.
3) you type "help cheatinit" to show command usage.
4) you type "cheatinit ub,0,0x1000
5) unpause the game and play a bit without taking damage
6) pause the game
7) type "help cheatnext" to see help how to sieve
8) type "cheatnext eq" to leave addresses which still has same value.
9) optionally repeat from step 5 few more times
10) unpause the game and take damage
11) wait while it will affected for sure, and pause again
12) type "cheatnext ne" to leave addresses which value have changed
13) optionally repeat few times from step 10
Now, you should have narrow range of addresses.
For example, I did it right now, and after step 9 I have "3976 cheats found"
After first execution step 12 I had "37 cheats found". Second "15 cheats found".
Third: "10 cheats found". Fourth: "1 cheats found".
Now, use "cheatlist": Address=03C1 Start=35 Current=02
Note: 0, 0x1000 in cheatinit it's based on info from mame's ddragon.cpp It's maincpu RAM.
If cheatlist is still big, you can check all addresses one by one by looking at them using Memory View for example, or script.
Now, when you have some address of player, you may set breakpoint on write.
1) type "help watchpoints"
2) type "help wpset"
3) type "wpset 3C1,1,w"
4) take damage
This above is basic strategy. But I did a bit different.
I did step 3 at title screen. Then clicked start input button.
Stopped at watchpoint 1 writing byte to 000003C1 (PC=8230) (data=40)
This code is taken using "help memory" and then "dasm qweqwe,0x8224,0x30"
then it was in file qweqwe.
So, you see here $03C1, $041F :)
You can make suggestions that it's both players hp.
Nothing you can tell from this.
But lets hit Debug Run (F5).
Stopped at watchpoint 1 writing byte to 000003C1 (PC=41C3) (data=0)
All those <v>,X where <v> is: $1B, $37, $E, $3, $13...
And all of them addressed from X. And X = $3A2.
This totally looks like "initialization of player structure" :).
And all those offsets are fields in player structure.
Thus, size of player structure is greater than $37. Remember it.
If we wait until that RTS by pressing F10 (Step Over), we will see that it goes back to B372:
Main hint here is: it calls $B36C twice, and second call is after incrementing X by $5E. (LEAX $5E, X)
Remember that X is player structure? :)
This gives us suggestion that next player is located at X+$5E = $3A2+$5E = $400.
Remember that player struct size is greater than $37?
So, this gives us suggestion that $5E is size of player struct.
You can't say at this point that it is in fact true.
But I can now say, after I was digging code much harder than discussed above and below.
What more interesting is code at B36F.
It is JSR that took us to player initialization code.
So, we can put there breakpoint using "bpset B36F", then type "softreset",
and see what happen.
You can also find code that is decreasing player's hp but I don' want it, so I type "wpclear 1", to not disturb me.
Okay, after softreset and pressing start at title screen we have:
Stopped at breakpoint 1
Press F11 (Step Into).
407B: 7E 61 D5 JMP $61D5
Press F11 (Step Into).
So, everything in disasm up to 61E9 was wrong, and correct this part is:
This is due the fact that disasm listing does not know where it came from.
Okay, what is also interesting in that code part that is below, is this:
6228: EC E4 LDD ,S
622A: ED 04 STD $4,X
622C: EC A1 LDD ,Y++
622E: ED 06 STD $6,X
6230: EC A1 LDD ,Y++
6232: ED 08 STD $8,X
You can either spot it now, or find X or Y or Z pos location using RAM Search.
And then, eventually stumbling across it.
So you can say where it is loading X, Y, Z initial player position.
Summary: you could find player struct in different ways, using hp/pos other stuff.
Enemy hp/pos is found in similar way. But code that iterates among them is more complex.
And from that code you can tell that Enemy is skipped if it has negative byte at offset 0.
memory.readbyte in mame-rr Lua for some reason returns unsigned one,
so check is transformed into "grater than 0x80".
Why some enemy doesn't fall, can be tracked down using write-breakpoint on its Z coordinate.
And addresses where it does write in Z I posted.
Idea for grabbind that addresses is to use this great feature:
wpset 465,1,w,1, {printf "%X", pc; g}
this will print PC value when breakpoint is being hit, and continue without suspending.
You can consider me as pro reverser. That is why it's quick for me.
Anyway it took 4 hours to find it + figure out how ddragon mapper works.
Warning: When making decisions, I try to collect as much data as possible before actually deciding. I try to abstract away and see the principles behind real world events and people's opinions. I try to generalize them and turn into something clear and reusable. I hate depending on unpredictable and having to make lottery guesses. Any problem can be solved by systems thinking and acting.
Thanks feos. Yep I was making heavy use of RAM Search/Watch as well (i was using that to cross check the sprite coordinate objects I had figured out from inspecting source code. I also used that to find the HP values for all characters). My mistake was concluding the enemy location objects and HP objects were tracked separately (digging through the source code and sprite data got me off the rails).
I'm still pretty lost in the Assembly Code and Debugger (The little bit of Assembly coding I did do was 18 years ago). - I'll take a step back from pushing through another TAS and focus on fundamentals - doesn't look like I'll get to my sub 6 second runs until I master this level of TAS'ing
It is tracked separately, as far as I know.
If you didn't notice, enemy struct size is 0x55, and player struct size is 0x5E.
Maybe they share some of funcs usage, will see.
I don't think you should dig into this.
Edit: I have found all landing-code that we need. I don't know, should I post it now, or you want to find it yourself.
Spoiler: it's way too complex.
This time it also shows fractional part of positions, and shows speed.
And, it shows same info for players.
Language: Lua
function draw_enemy(base, id, yy)
if memory.readbyteunsigned(base) < 0x80 then return end
local x = memory.readwordunsigned(base+4)
local y = memory.readwordunsigned(base+6)
local z = memory.readwordunsigned(base+8)
local xfrac = memory.readbyteunsigned(base+10)
local yfrac = memory.readbyteunsigned(base+11)
local zfrac = memory.readbyteunsigned(base+12)
local xspeed = memory.readwordunsigned(base+0xF)
local zspeed = memory.readwordunsigned(base+0x11)
local hp = memory.readbyteunsigned(base+0x1F)
gui.text(0, yy,
string.format("%d: %4X%02X %4X%02X %4X%02X %4X %4X %d",
id, x, xfrac, y, yfrac, z, zfrac, xspeed, zspeed, hp))
end
function draw_enemies()
for i = 0, 8 do
local base = 0x45E+0x55*i
draw_enemy(base, i, 32+i*8)
end
end
gui.register(function()
draw_enemy(0x3A2, 0, 0)
draw_enemy(0x400, 1, 8)
draw_enemies()
end)
You can also look at byte 2 and byte 3 in enemy structure, because landing behavior depends on it.
I'm happy to take what you've learned about landing code! I don't mind the spoilers :)
I plan to research this stuff myself too, but it would definitely help to work from a good solution (it will serve as baseline for my TAS of Double Dragon II research)
Man - seriously thank you so much. I was getting lost in all this stuff - your crash course on how you debugged this is huge. Thanks again.
First of all, I'll describe how banking is working here, in ddragonw.
Bank is located from 4000 to 8000 in address space.
So this chunk is replaced from another if new value is writen in bank register.
Bank register is byte at address 3808.
So, when something is written in 3808, it sets bank.
Bank index is stored in highest three bits of that byte.
Other bits is used for other stuff.
If we look at code parts where write in 3808 appear, everywhere we see write at address 003A.
It's backup register value, implemented in software.
It is for transparent code.
When something is need to change bank, it will change it, do stuff, and then it will change it back.
So, each time we want to know current bank, we can do it by typing "print b@3a & E0"
Now, bank values and corresponding chunks of data:
00: 21j-2-3.25 first half (0-4000)
20: 21j-2-3.25 second half (4000-8000)
40: 21a-3.24 first half (0-4000)
60: 21a-3.24 second half (4000-8000)
80: 21j-4.23 first half (0-4000)
A0: 21j-4.23 second half (4000-8000)
Also, in addition, 8000-FFFF of maincpu address space is whole 21j-1.26
Now, ground fetching code is located at $4000 offset when bank is 80.
This means it's at offset 0 in 21a-3.24.
It is library function. I mean someone in ddragon team made it as utility.
Its usage is following.
Programmer needs to set X register to enemy/player, and fill structure located at address B31.
Output of library is stored in same structure.
I'm lazy to calculate its size. But here is its structure:
B31: xpos current (word)
B33: ypos current (word)
B35: zpos current (word)
B37: xpos next (word)
B39: ypos next (word)
B3B: zpos next (word)
B3D: byte_3 (byte)
B3E: four samples. each sample is x,y,z byte coords. 12 bytes total.
B4A: unknown output (byte)
B4B: unknown output (byte) (looks like always zero)
B4C: unknown output (byte) (looks like always zero)
B4D: unknown output (byte) (looks like always zero)
B4E: word xpos result - output
B50: word ypos result - output
B52: word zpos result - output
After this address, temporary data used by this library is stored:
B54: X register backup (word) (enemy/player offset)
B56: map cell offset (word)
B58: iteration (byte) (sample id counting from 1)
B59: direction of change (byte) (range is [0,8], 9 directions. 0 - didn't move)
B5A: x pos for map (word)
B5C: unknown
B5D: unknown
B5E: y pos for map (word)
B60: z pos for map (word)
B62-B68: unknown
B69-B6D: 5 byte heights.
B6E and above - unknown.
NOTE: even though all addresses are BXX, in code they appear without B. Because B is set into DP register, which is concatenated in front to address when offset is byte. This indexing mode is called "direct". I had confusion with it until I read about this.
Structure of calculations:
1) 4000-4038 pc: initialization + transform coords into map space.
transformation looks like: x_map = x, y_map = y, z_map = y+z.
2) 4038: JSR $407F calculation of B59 byte.
3) 403B-4051: first result calculation:
x_result = x_map+xshift,
y_result = y_map + y_shift,
z_result = z_map + y_shift + z_shift,
4) 4053: JSR $40AF sampling
5) 4056: JSR $4507 unknown additional processing.
6) 4059-4077: reverse transformation of resulting coords.
Structure of sampling:
1) 40AF-40B1: iteration initialization
2) 40B3: JSR $40E6 - get sample pos:
it's just result + sample[iteration-1], where result is x,y,z result, and sample is one of four samples at B3E.
If all x,y,z values of sample coords is zero, it's skipped by branching at 40B7.
3) 40B9: JSR $4154 - get map cell. its result in B56
4) 40BC-40DB: loop over cell data.
5) 40DD-405E: increment iteration and continue loop.
Map structure:
It is located at 4780 in same bank.
Offsets starting IDK where, but looks like 4800 already offsets.
Each offset is offset at 32 offsets.
Then, each of this 32 offsets is offset at list of offsets until word=FFFF.
Offsets in the list is actual data.
Their 7-th byte is height mask though. (which height byte should be overwriten to FF)
Step (4) has few JSR-s which does actual processing of cell data.
I didn't dig into details. But when character lands, its z_result overwriten at 4299.
Normaly, enemy is filling this structure and then calls this library, and then, enemy is reading result back as is, without any postprocessing.
First of all, I'll describe how banking is working here, in ddragonw.
Bank is located from 4000 to 8000 in address space.
So this chunk is replaced from another if new value is writen in bank register.
Bank register is byte at address 3808.
So, when something is written in 3808, it sets bank.
Bank index is stored in highest three bits of that byte.
Other bits is used for other stuff.
If we look at code parts where write in 3808 appear, everywhere we see write at address 003A.
It's backup register value, implemented in software.
It is for transparent code.
When something is need to change bank, it will change it, do stuff, and then it will change it back.
So, each time we want to know current bank, we can do it by typing "print b@3a & E0"
Now, bank values and corresponding chunks of data:
00: 21j-2-3.25 first half (0-4000)
20: 21j-2-3.25 second half (4000-8000)
40: 21a-3.24 first half (0-4000)
60: 21a-3.24 second half (4000-8000)
80: 21j-4.23 first half (0-4000)
A0: 21j-4.23 second half (4000-8000)
Also, in addition, 8000-FFFF of maincpu address space is whole 21j-1.26
Now, ground fetching code is located at $4000 offset when bank is 80.
This means it's at offset 0 in 21a-3.24.
It is library function. I mean someone in ddragon team made it as utility.
Its usage is following.
Programmer needs to set X register to enemy/player, and fill structure located at address B31.
Output of library is stored in same structure.
I'm lazy to calculate its size. But here is its structure:
B31: xpos current (word)
B33: ypos current (word)
B35: zpos current (word)
B37: xpos next (word)
B39: ypos next (word)
B3B: zpos next (word)
B3D: byte_3 (byte)
B3E: four samples. each sample is x,y,z byte coords. 12 bytes total.
B4A: unknown output (byte)
B4B: unknown output (byte) (looks like always zero)
B4C: unknown output (byte) (looks like always zero)
B4D: unknown output (byte) (looks like always zero)
B4E: word xpos result - output
B50: word ypos result - output
B52: word zpos result - output
After this address, temporary data used by this library is stored:
B54: X register backup (word) (enemy/player offset)
B56: map cell offset (word)
B58: iteration (byte) (sample id counting from 1)
B59: direction of change (byte) (range is [0,8], 9 directions. 0 - didn't move)
B5A: x pos for map (word)
B5C: unknown
B5D: unknown
B5E: y pos for map (word)
B60: z pos for map (word)
B62-B68: unknown
B69-B6D: 5 byte heights.
B6E and above - unknown.
NOTE: even though all addresses are BXX, in code they appear without B. Because B is set into DP register, which is concatenated in front to address when offset is byte. This indexing mode is called "direct". I had confusion with it until I read about this.
Structure of calculations:
1) 4000-4038 pc: initialization + transform coords into map space.
transformation looks like: x_map = x, y_map = y, z_map = y+z.
2) 4038: JSR $407F calculation of B59 byte.
3) 403B-4051: first result calculation:
x_result = x_map+xshift,
y_result = y_map + y_shift,
z_result = z_map + y_shift + z_shift,
4) 4053: JSR $40AF sampling
5) 4056: JSR $4507 unknown additional processing.
6) 4059-4077: reverse transformation of resulting coords.
Structure of sampling:
1) 40AF-40B1: iteration initialization
2) 40B3: JSR $40E6 - get sample pos:
it's just result + sample[iteration-1], where result is x,y,z result, and sample is one of four samples at B3E.
If all x,y,z values of sample coords is zero, it's skipped by branching at 40B7.
3) 40B9: JSR $4154 - get map cell. its result in B56
4) 40BC-40DB: loop over cell data.
5) 40DD-405E: increment iteration and continue loop.
Map structure:
It is located at 4780 in same bank.
Offsets starting IDK where, but looks like 4800 already offsets.
Each offset is offset at 32 offsets.
Then, each of this 32 offsets is offset at list of offsets until word=FFFF.
Offsets in the list is actual data.
Their 7-th byte is height mask though. (which height byte should be overwriten to FF)
Step (4) has few JSR-s which does actual processing of cell data.
I didn't dig into details. But when character lands, its z_result overwriten at 4299.
Normaly, enemy is filling this structure and then calls this library, and then, enemy is reading result back as is, without any postprocessing.
Sorry for lack of response for two months, I've been off learning assembly basics, struggling to meet a deadline... and new husband duties.
I've been tinkering with the script and notes you gave me during that time... couldn't find much predictability with glitch - even after lots of time in debugger and several nights of frustration.
10 minutes ago I figured it out!
If an enemy is launched while the Y,Y_frac value is less than FF,00.... they will fall off ledge 100% of the time
But how to reliably get all 3 of the mobs to have a Y, Y_frac value is less than FF,00?
It turns out I have to manipulate the enemies so that the 2nd mob out of the elevator is the first one to come down to the edge of the platform.
At the same time, I also have to manipulate the 1st mob's walk path so it resets - otherwise the path down to the edge will always result in a Y, Y_frac value that is FF,4A
Basic Solution (I plan to get exact values this weekend)
1) Throw box off left edge (prevents enemy retargeting)
2) Walk Billy down to platform edge (Y=FE, Y_frac=00)
3) Trigger Elevator Opening
4) Position Billy so his X offset is slightly to the left of first Linda out of Elevator (X=7DD, X_frac=00, Y=FE, Y_frac=00)
5) When Linda exits elevator, stay slightly left of her, walk to X=809). This will pin Linda in Top corner
6) Stand still, wait for enemies while 1st mob is pinned, and AI adjusts to have 2nd mob lead path to Billy
7) Walk a couple of steps to right
At this point, the 2nd mob is guaranteed to have a Y value < FF when they hit the platform edge. The 1st and 3rd mob, following the first path established by 2nd mob, also end up with Y offset < FF
It's also possible to elbow all 3 off the edge at the same time like this [still working out precise values for walk paths]
Thank you so much for all the help - I learned so much starting with what you showed me (it helped get me to the point where I could determine that Y value < FF gives 100% guaranteed results). Process of elimination is also a powerful tool :)
P.S. - Apologies in advance if I vanish again without responding, I work for a startup - when things get too crazy, my TAS hobby gets put on shelf.