I went back and made sure the changes we've made to code so far still work with the FF4 borg. Getting menu and cutscene handling to work with both games is going to be tricky. I don't want to just add lots of configuration flags that determine how each fiddly bit of code behaves, but the unfortunate truth is that every game is going to handle these sorts of things a little bit differently.
Also looked at open source emulators for other consoles that I have games for. Once I'm done with the three Final Fantasy games on the SNES, I think I'll want to switch genres and try working in a different environment.
Showing posts with label ff4. Show all posts
Showing posts with label ff4. Show all posts
Saturday, September 24, 2011
Friday, September 9, 2011
Feeling a Little Silly
While I was working on refactoring isLayerCompatibleWith(), I started looking at FF4 layer numbers and noticed what is now some very obvious bit flags in local map layer numbers:
So the downside is that I looked dumb on the internet, although nobody's watching at the moment. The upside is I know a bit more about how mobility layers work, I got to simplify some important code, and may end up not needing to make isLayerCompatibleWith() game-specific. Yet. We still need to look for barriers between squares, but we can turn the check for that on and off with some configuration.
Oh, that's right, I didn't blog about that.
I've got a bunch of functions that just compare a location in the game's memory against a given value to see whether the game is in a certain state. isInCutscene(), isMoving(), and justOpenedTreasure() are a few examples. The logic for these functions will necessarily different for each game. We could build some C++ class that reads from a config file and interprets the logic needed for each method, and that would work, but I'd really rather not build a makeshift logic interpreter for this project, and it would slow down some functions that may actually need to run quickly.
Enter boost::function and boost::bind. Between FF4 and FF5, I really only need 3 operators for these functions: ==, !=, and <, and they're all comparing a named memory byte against a specific number. So I made three functions, one for each operator, used boost::bind to build a function with the memory key name and expected value from the config file, and saved that as a boost::function. It works great.
- Bits 1 and 0 are our two layer bits.
- Bit 2 is the bridge flag.
- Bit 3 is the save point flag.
- Bit 4 is the transition flag.
So the downside is that I looked dumb on the internet, although nobody's watching at the moment. The upside is I know a bit more about how mobility layers work, I got to simplify some important code, and may end up not needing to make isLayerCompatibleWith() game-specific. Yet. We still need to look for barriers between squares, but we can turn the check for that on and off with some configuration.
Oh, that's right, I didn't blog about that.
I've got a bunch of functions that just compare a location in the game's memory against a given value to see whether the game is in a certain state. isInCutscene(), isMoving(), and justOpenedTreasure() are a few examples. The logic for these functions will necessarily different for each game. We could build some C++ class that reads from a config file and interprets the logic needed for each method, and that would work, but I'd really rather not build a makeshift logic interpreter for this project, and it would slow down some functions that may actually need to run quickly.
Enter boost::function and boost::bind. Between FF4 and FF5, I really only need 3 operators for these functions: ==, !=, and <, and they're all comparing a named memory byte against a specific number. So I made three functions, one for each operator, used boost::bind to build a function with the memory key name and expected value from the config file, and saved that as a boost::function. It works great.
Wednesday, September 7, 2011
Red Letter Day
Over the weekend the borg ran the game successfully. Nine times in a row. The tenth failed for a fairly predictable reason at this point: I should have a Recovery interrupt so we automatically heal during navigation. I've added that to my dev list.
It's time to move onward. I'm tagging the code and command queue as v1.0. I've also started contributing the memory locations I found to this page. Mostly just the Battle section for now. Need to add the rest.
As far as development is concerned:
It's time to move onward. I'm tagging the code and command queue as v1.0. I've also started contributing the memory locations I found to this page. Mostly just the Battle section for now. Need to add the rest.
As far as development is concerned:
I'm sorting out some of the memory keys needed to get cartography up and running on FF5. Should be able to start code changes by this weekend.
If I get bored or stuck with FF5, I'll work on getting the FF4 run to finish faster, or get started on a Cecil solo run.
Monday, September 5, 2011
Packrat Intervention
Results of trial run: Inventory management problem.
Details: Inventory was full when we picked up the second Defense Ring. The borg got stuck in the post-battle loot collection screen because there was no space for our loot.
Solution: Added about a dozen more items to the garbage lists. It's really a shame to lose a run because our inventory filled up.
Details: Inventory was full when we picked up the second Defense Ring. The borg got stuck in the post-battle loot collection screen because there was no space for our loot.
Solution: Added about a dozen more items to the garbage lists. It's really a shame to lose a run because our inventory filled up.
It Was Bound to Happen
Results of trial run: TPK.
Details: Lost to the White Dragon in the Lunar Subterrain. I was wondering if this would ever happen.
Details: Lost to the White Dragon in the Lunar Subterrain. I was wondering if this would ever happen.
After his usual Maelstrom toward the end of the fight, he managed to hit each character that was queued up to use a Phoenix Down right before they used it a few times in a row. Once we got down to 2 live party members, we struggled for a bit to stay afloat and then lost.
In a lot of these fights there seems to be a tipping point at 3, or sometimes even 2 dead party members. If we hit that point and we don't pull it together in the next few actions, there's a good chance the party will go down. Will have to think about that some more; maybe we can add a conditional that checks how many party members are dead and takes more drastic action. Ashura seems like a good choice.
Anyway, part of the problem here is that the White Dragon casts Slow a lot in the lead-up to Maelstrom, which gives him time to pick off healers. Well shoot, we can do something about that, can't we?
Solution: Added a one-time casting of Slow to the default end-game battle strategy.
Sunday, September 4, 2011
Race Conditions
Results of trial run: Battle state bug.
Details: Got stuck in Edge's Ninjutsu menu during the fight with Ashura.
Solution: There's at least two ways I could approach this problem: I could find where the displayed HP/MP values are stored in memory, or I could figure out how to tell if a menu option is disabled, and if our choice is disabled we bail on the command and choose a different one. I chose the second option; that may solve a few other potential problems as well. I spent some time digging around the game's RAM and where spell statuses are stored for battle. There's some other interesting information there that might be worth deciphering in the future.
Details: Got stuck in Edge's Ninjutsu menu during the fight with Ashura.
The locations in memory that store character stats in battle, like current HP and MP, don't change at exactly the same time as they do on-screen. When an enemy attacks you or you heal yourself, you have to wait for the whole animation to be complete before you see the new balance for your hit points. However, in memory, this balance has already been changed to reflect the new value when the animation starts. There's probably some other location in memory that stores the displayed HP/MP values separately, but I haven't tried to track it down.
The consequence of all this is that the borg makes decisions based on what it sees for character HP and MP, which may not be reflected in the game interface yet. Most of the time this is fine, but if you time it just right, you can get into a situation where a character is given orders to cast a spell right before we see another character use an Ether on them, because the borg thinks they have enough MP.
This brings us to the other half of the problem: once you're in the menu to choose a spell, which spells are disabled and the available MP won't update until you back out of the menu and choose it again. This brings us to the screenshot above. The borg thinks Edge has enough MP to use Raijin because Cecil is using an Ether on him now, but he makes it into the Ninjustu menu before the Ether animation is finished. The borg still thinks it can cast Raijin, and technically it's right, but it doesn't know to back out of the menu and try again, so it just sits there mashing the A button.
Solution: There's at least two ways I could approach this problem: I could find where the displayed HP/MP values are stored in memory, or I could figure out how to tell if a menu option is disabled, and if our choice is disabled we bail on the command and choose a different one. I chose the second option; that may solve a few other potential problems as well. I spent some time digging around the game's RAM and where spell statuses are stored for battle. There's some other interesting information there that might be worth deciphering in the future.
Saturday, September 3, 2011
Presenting...
Results of trial run:
I am pleased to announce the first successful start-to-finish run of the FF4 Borg. The borg is an extension of the Snes9x emulator that plays Final Fantasy 4, using only the standard controller buttons as inputs, and reading information from memory that is equivalent to what's available to a competant human player, an atlas of the game (built step by step by the borg itself), and a list of high-level commands that reads a bit like a strategy guide.
The borg made it out of the final battle of the game in about 3,300,000 ticks, and if you looked at the game clock right before going into the final fight, it would read about 14 hours on the game clock.
Over the coming days and weeks, I'll be posting more about how the borg works, as well as where development is going to go from here. My most immediate goal is to put the current borg through its paces some more and get our success right higher. I'll continue to post failure reports and what I've changed to increase our chances of success on the next run. Eventually, I'll have enough data to start tracking statistics on the borg's success rate and where it fails the most.
Then it's on to the more interesting stuff. Did you know Square made more than one Final Fantasy game for the SNES? Because I knew.
Edit: Next run was also successful. Woo!
Second Verse, Same as the First
Results of trial run: TPK.
Details: Lost to the monster that came from a trap door in the Sealed Cave. We went into the fight with all members nearly dead. Starting to sound familiar? I added more Recovery statements in for the Sealed Cave, and I'm just about ready to give up and add a Recovery interrupt.
Almost.
Details: Lost to the monster that came from a trap door in the Sealed Cave. We went into the fight with all members nearly dead. Starting to sound familiar? I added more Recovery statements in for the Sealed Cave, and I'm just about ready to give up and add a Recovery interrupt.
Almost.
Take One for the Team
Results of trial run: TPK.
Details: Lost while fighting two Red Dragons for the Crystal Gauntlets.
This strategy doesn't work for end-game stuff, and here's why: the real threats to the survival of the party are attacks that hit everybody, not big hits on single party members. We can handle one party member dying at a time pretty well. We get in trouble when a big group of them go down at once because they were near dead to begin with and the party got hit with an AOE.
Solution: Changed the strategy for this section. Now, if Rosa's going to do any healing, it's Cure 3 on all allies if a live party member is below 70% HP, or use a Phoenix Down otherwise. No more Cure 3 on individuals.
Details: Lost while fighting two Red Dragons for the Crystal Gauntlets.
We did come into the fight with one party member dead and one nearly dead, which put us at a serious disadvantage. I don't think that was what killed us, but I went back and put a couple more Recovery commands in our command queue for looting the Lunar Subterrain.
The problem here was that we managed to get 3 party members dead with 1 Red Dragon left, and spent all of our time bringing them back to life to get lasered down again. Occasionally we'd pull back up a bit, but it just wasn't quite enough.
The borg has a single battle strategy for this whole section of the game, and so far it's worked pretty well. Up until now, Rosa's part of the strategy has followed something similar to what we've done for most of the game:
- If someone's dead, bring them back with Life or a Phoenix Down.
- If someone's near dead, heal just them.
- If someone's just kind of hurt, heal everybody.
This strategy doesn't work for end-game stuff, and here's why: the real threats to the survival of the party are attacks that hit everybody, not big hits on single party members. We can handle one party member dying at a time pretty well. We get in trouble when a big group of them go down at once because they were near dead to begin with and the party got hit with an AOE.
Solution: Changed the strategy for this section. Now, if Rosa's going to do any healing, it's Cure 3 on all allies if a live party member is below 70% HP, or use a Phoenix Down otherwise. No more Cure 3 on individuals.
Two battle bug fixes
While I was testing out the new Zeromus strategy, I noticed that TimesPerBattle bug happening again, and finally figured out what was happening. It has nothing to do std::map or the used_commands cache. The class I'm using to store battle command information has two constructors, and one of them doesn't initialize the boolean that determines whether the command has been chosen for execution. Fixed!
I also had a couple of issues with party members missing the item/spell they were supposed to use and choosing something else instead. I was having this issue earlier in development and cranked up the cooldown between button presses, but apparently it still happens every once in a while. Instead of increasing the cooldown even more, I chose to rewrite the code that chooses button inputs to only queue one button press at a time while choosing a spell or item to use, and then re-evaluate what to do after each press. That prevents the occasional miss, and it also lets me turn the cooldown way down. The borg's battle commands are flying by again.
I also had a couple of issues with party members missing the item/spell they were supposed to use and choosing something else instead. I was having this issue earlier in development and cranked up the cooldown between button presses, but apparently it still happens every once in a while. Instead of increasing the cooldown even more, I chose to rewrite the code that chooses button inputs to only queue one button press at a time while choosing a spell or item to use, and then re-evaluate what to do after each press. That prevents the occasional miss, and it also lets me turn the cooldown way down. The borg's battle commands are flying by again.
Back to the Training, Yeah!
Results of trial run: TPK.
Details: Lost to Zeromus.
I could write some code that lets me prioritize which party members get raised first, but I'd like to avoid adding more code if it's not necessary to get what we need, and in this case, it's not; we can just arrange the party so the most important characters to raise are at the top.
Solution: Added commands to rearrange the party before the final battle to this order, from top to bottom: Rosa, Cecil, Kain, Edge, Rydia. I also made a few more adjustments to the battle strategy.
Details: Lost to Zeromus.
This is a great example of a fight where a lot of little things could have gone better, but the screenshot above illustrates the final mistake we made.
Currently, the DeadAlly target (as well as most of the other options that target allies) will choose the first character in the party that meets the conditions, starting from the top of the list. In our current party arrangement, that means that if 4 characters are dead and one of them is Rosa, Rosa will be the last one brought back. This is very, very bad planning.
I could write some code that lets me prioritize which party members get raised first, but I'd like to avoid adding more code if it's not necessary to get what we need, and in this case, it's not; we can just arrange the party so the most important characters to raise are at the top.
Solution: Added commands to rearrange the party before the final battle to this order, from top to bottom: Rosa, Cecil, Kain, Edge, Rydia. I also made a few more adjustments to the battle strategy.
- Rosa no longer fires off a one-time Shell on all allies. I think it might actually not help at all against Big Bang. This has been replaced with a one-time Haste on herself.
- Kain now has a one-time command to use Spiderweb (Slow effect item) on first target. Ideally, we'd reapply Slow every time Zeromus wipes statuses, but I haven't written any code to determine enemy statuses, and I don't want to. That flirts with the boundary of what a human player would be able to tell about an enemy, and I'm trying to build this borg to only use information available to a human player. Still, getting Slow out once gives us a bit of an edge for the first part of the fight.
- If all live party members are above 70% HP and someone is dead, Rosa will cast Life 2 on them instead of using a Phoenix Down (unless she doesn't have the MP; then she'll use the Phoenix Down). It takes a bit longer to cast than to use an item, but this is a fight where having someone go from dead to full HP is very useful.
- If everyone is alive and at least one party member is below 30% HP, Rydia will summon Ashura instead of Bahamut. A bit of a long shot move since Ashura is so unpredictable, but in those crevices where the fight isn't faring well, it can help quite a bit. Might choose to bump this up above using Phoenix Down, but probably not: If Rosa is dead, I don't want to cast Ashura and hope that maybe she'll bring everybody back to life. I want Rosa back on her feet.
Valor of the Royal
Results of trial run: Battle state bug resulting in TPK.
Details: Lost during the level grind at Damycam, of all places.
Details: Lost during the level grind at Damycam, of all places.
I probably should have anticipated this the last time we had a problem with Gilbert. His default command here is to Fight, but he ran and hid because he's low on HP and now Show is the only option. So the borg is just pressing up repeatedly here until that one Mini Mage whittles the other two party members down. Then Gilbert comes out, gets off one attack, and succumbs to Hold and a couple more shots.
Solution: Added a top-priority conditional for Gilbert to come out of hiding whenever he has the Hide status for all parts of the game where we have Gilbert. It's a bit silly watching him run back and forth, but it means the other party members can take actions while Gilbert is running laps.
Friday, September 2, 2011
A Curious State
Results of trial run: Battle state bug.
Details: In the Ashura fight, Rosa kept rapidly switching between deciding to cast Reflect a second time and deciding that our once-per-battle command had already been used and she needed to choose another.
I'm not entirely sure why this was happening. As soon as I put logging statements around our used_commands counter, it stops happening and everything behaves as it should. I have a shred of a guess about using += or -= on a std::map<std::string, int>, but it still doesn't make any sense to me.
"Solution": Changed to using value = value + 1 instead of value += 1, and it stopped happening. Not at all convinced that this is the end of this particular problem, but I'm not sure what else to do with it at the moment.
Details: In the Ashura fight, Rosa kept rapidly switching between deciding to cast Reflect a second time and deciding that our once-per-battle command had already been used and she needed to choose another.
I'm not entirely sure why this was happening. As soon as I put logging statements around our used_commands counter, it stops happening and everything behaves as it should. I have a shred of a guess about using += or -= on a std::map<std::string, int>, but it still doesn't make any sense to me.
"Solution": Changed to using value = value + 1 instead of value += 1, and it stopped happening. Not at all convinced that this is the end of this particular problem, but I'm not sure what else to do with it at the moment.
Thursday, September 1, 2011
Not a Grenade, Horseshoe, or the Government
Results of trial run: TPK.
Details: Lost to Zeromus fight. The core of the problem was that Rosa was using a Phoenix Down when she should have been casting Cure 4. This wasn't a problem before because we had Cure 4 as a higher-priority conditional.
Solution: Added a conditional Cure 4 on top of the default Cure 4; now if any live party member is less than 70% on HP, Rosa will cast Cure 4, even if someone is dead. I ran this strategy several times to see how it works out, and I'm feeling pretty good about it.
While I was at it, I thought I would make Edge pull his weight, so I added support for the Dart command and added conditionals to have him throw all of our expensive weapons and Hellwinds during the fight.
Details: Lost to Zeromus fight. The core of the problem was that Rosa was using a Phoenix Down when she should have been casting Cure 4. This wasn't a problem before because we had Cure 4 as a higher-priority conditional.
Solution: Added a conditional Cure 4 on top of the default Cure 4; now if any live party member is less than 70% on HP, Rosa will cast Cure 4, even if someone is dead. I ran this strategy several times to see how it works out, and I'm feeling pretty good about it.
While I was at it, I thought I would make Edge pull his weight, so I added support for the Dart command and added conditionals to have him throw all of our expensive weapons and Hellwinds during the fight.
Meaningless
Results of trial run: TPK.
Details: Died somewhere in Upper Babil running from fights. Probably was too weakened after an unfortunate SteelGolems fight. Yes, I need more Recovery calls during that section, and no, I still don't want to write a Recovery interrupt.
Solution: Using a tent soon after the SteelGolems fight, and sprinkled some Recovery calls from there until we butcher Edge's parents.
Details: Died somewhere in Upper Babil running from fights. Probably was too weakened after an unfortunate SteelGolems fight. Yes, I need more Recovery calls during that section, and no, I still don't want to write a Recovery interrupt.
Solution: Using a tent soon after the SteelGolems fight, and sprinkled some Recovery calls from there until we butcher Edge's parents.
Wednesday, August 31, 2011
Bringing a Knife to a Pudding Feed
Results of trial run: TPK.
Details: The borg got into a fight it thought it was supposed to win in the Tower of Babil. This might have been fine, but Rydia was set to defend instead of summon, and we got into a fight with three of those white marshmallow things that are essentially immune to physical attacks. Lost at the end of a long, embarrassing fight.
The real problem here was that the borg thought it was supposed to fight in the first place. It had just entered a room with a monster-in-a-box treasure chest. The convention up to this point has been: Walk into a room with a monster-in-a-box, switch from running to fighting, open the chest and fight, switch from fighting to running. Most of the time this works, even though it picks up the occasional extra fight on the way to the chest. But if we get into one of these fiddly fights that require a specific strategy, that could put us in trouble.
Solution: Our GetLoot:[Item Name] and GetAccessibleLoot commands now switches to fighting mode when opening a treasure chest, and back to whatever our strife specibus was before after the treasure chest is open. Not only does this avoid trying to win unnecessary fights, but it also cleans up our command queue in a few places.
Details: The borg got into a fight it thought it was supposed to win in the Tower of Babil. This might have been fine, but Rydia was set to defend instead of summon, and we got into a fight with three of those white marshmallow things that are essentially immune to physical attacks. Lost at the end of a long, embarrassing fight.
The real problem here was that the borg thought it was supposed to fight in the first place. It had just entered a room with a monster-in-a-box treasure chest. The convention up to this point has been: Walk into a room with a monster-in-a-box, switch from running to fighting, open the chest and fight, switch from fighting to running. Most of the time this works, even though it picks up the occasional extra fight on the way to the chest. But if we get into one of these fiddly fights that require a specific strategy, that could put us in trouble.
Solution: Our GetLoot:[Item Name] and GetAccessibleLoot commands now switches to fighting mode when opening a treasure chest, and back to whatever our strife specibus was before after the treasure chest is open. Not only does this avoid trying to win unnecessary fights, but it also cleans up our command queue in a few places.
Tuesday, August 30, 2011
Couldn't You Just ... You Know ...
Results of Trial Run: Battle state bug.
Details: We were fighting one of the Behemoths on the way to Bahamut, and Rosa ran out of arrows. Her default command was still Aim, and we don't know how to cancel out of trying to use disabled menu options, so we just sat there while Rosa fiddled with her bow trying to figure out how to Aim with no arrows.
Solution: I don't feel like solving the disabled menu option problem; we're already avoiding it when a caster is unable to cast spells. Just changed Rosa's default command to Defend in this section.
Details: We were fighting one of the Behemoths on the way to Bahamut, and Rosa ran out of arrows. Her default command was still Aim, and we don't know how to cancel out of trying to use disabled menu options, so we just sat there while Rosa fiddled with her bow trying to figure out how to Aim with no arrows.
Solution: I don't feel like solving the disabled menu option problem; we're already avoiding it when a caster is unable to cast spells. Just changed Rosa's default command to Defend in this section.
Sleeping Under the Stars
Results of trial run: Inventory management problem.
Details: We had no tents left when we went to use one after the first Scarmiglione fight. I have no idea how this happened, but it's easy to fix; we're buying two tents earlier on now.
Details: We had no tents left when we went to use one after the first Scarmiglione fight. I have no idea how this happened, but it's easy to fix; we're buying two tents earlier on now.
Really More of a Taupe Elf
Results of trial run: TPK.
Details: Lost to the Dark Elf. Tellah's first Tornado in the second stage failed. The second Tornado connected, but again, we were too busy using Phoenix Downs to deliver the final blow. We really need someone dedicated to striking during the second stage.
Solution: Cecil no longer casts Cure on Tellah during the second stage of the fight. Since we're still Covering him, this should work okay.
Details: Lost to the Dark Elf. Tellah's first Tornado in the second stage failed. The second Tornado connected, but again, we were too busy using Phoenix Downs to deliver the final blow. We really need someone dedicated to striking during the second stage.
Solution: Cecil no longer casts Cure on Tellah during the second stage of the fight. Since we're still Covering him, this should work okay.
Der Dunkelelfen
Results of trial run: TPK.
Details: Lost to the Dark Elf.
Details: Lost to the Dark Elf.
Everything goes great during the first stage. Then he turns into a dragon and hits Tellah for more than his maximum HP. We do eventually get him alive long enough to deliver a Tornado, but then everyone is too busy using Phoenix Downs to poke the Dark Elf once in the ribs, and we get Dark Breathed down.
Tellah going down in one hit made me really sit back and reconsider what needs to happen to make this a reliable fight. I still really, really don't want to have to add another level grind before the the Golbeze fight.
Solution: So what else can we do? Well, maybe we can increase his armor rating. I added some commands back in Baron to buy him a Kenpou and Bandanna; that raises his armor a little bit, but not by a whole lot. However, there's another solution that I don't normally consider; Cecil's Cover command. Firing that off once at the beginning of the fight makes Tellah very, very hard to kill in stage two, assuming he isn't near dead from a Tornado in stage one.
Subscribe to:
Posts (Atom)

