Wednesday, October 5, 2011

Indiana Bartz

There's something that I neglected to mention in the last post that turned out to be important. I had briefly considered just switching classes to something that can use better armor and moving everyone to the back row. This would cut damage taken way down, but it would also reduce our damage output to a trickle, with several counter attacks on the party for every drip. I had simply discarded the strategy.


I had failed to consider the whip, which the previous boss so conveniently drops at a 100% drop rate. Not only does it have the best attack out of any available weapon up to that point, but it also does full damage from the back row, and it has a good chance to paralyze on hit.

So Bartz is a back-row Freelancer for this fight, and now Galura is a pushover. If I get a paralyze in on Bartz's first hit, then everyone stays alive, and sometimes Galura never gets to attack. If not, then everyone dies except Bartz and we still win. Galura is hitting him for 0-4 damage a shot. It's still technically possible to lose if Galura gets 3 or so Rushes to connect on Bartz and drains his HP out, but it seems like a vanishingly small chance.

Looks like we get our date with Liquid Flame before implementing the Job system after all. I've mapped and pathed to Karnak.

Tuesday, October 4, 2011

Elephants Are Scary

Mapped and pathed to the Galura fight.


Galura beats the tar out of us right now. Granted, we're running a group of half-naked level 6 unskilled brain-dead Monks, but it's still a pretty sad fight.

I wanted to see whether something better than starting equipment would tip the scales, so I worked out the code for shop interactions and bought four Training Gis. We can now buy and sell with impunity, but unfortunately it's fairly game specific code for both FF4 and FF5, and I haven't come up with a nice way to generalize it. Some of it could be moved off into configuration, but there's a point at which that stops making sense. For example, in FF4 you choose the quantity of items you want to buy/sell before you choose the item. In FF5, you choose the item first and then the quantity. There's a half-dozen other little differences surrounding this. This sort of procedural difference doesn't convert very nicely into configuration. I will probably end up building a framework for attaching game-specific code to games on startup so the code is cleaned up a bit.

Anyway, the better armor did help - dropped Galura's hits from 130s to 90s - but it's not enough. He just goes nuts in the second half of the fight every time you hit him, and that's all we're ever doing. Even if I wanted to try the Phoenix Down binge strategy, at 1000 GP a pop Phoenix Downs are prohibitively expensive. I could only buy 3 of them now without level grinding. I could also just level grind a bunch until we can stomp him, but it's not worth it; this is a sufficiently compelling problem to solve more intelligently.

Looks like it's time to start implementing Job and Ability support. Galura exclusively uses physical attacks, so it would be a great place to test a Knight Cover/Guard strategy. We'll see if we need it; to start with, I'll probably give everybody Mantra and have them use that when they're low on HP.

It's going to take a while to get this stuff up and running. From a top-level perspective, I want to have the following:
  • A SetJob:[CharacterName]:[JobName]:[ExtraAbilityName]:[etc] command. Examples: "SetJob:Bartz:Knight:Counter" or "SetJob:Lenna:Freelancer:X-Fight:MagicSword".
  • A SetAbility:[CharacterName]:[AbilityName] command.
  • An UntilCharacterReachesJobLevel:[Number] level grind exit condition. Might choose to add UntilCharacterMastersJob later on.
  • An UntilCharacterLearnsAbility:[AbilityName] level grind exit condition.
In addition to all the other internals the borg needs to support this, I also need to be able to read what a character's current battle commands are; a static config file isn't going to cut it anymore. I might as well see if I can sort that out for FF4 while I'm at it.

Later on I may need to add some more sophisticated code to automatically manage job and ability switching. At the very least, I want to avoid situations where three of the characters are spinning their wheels in a level grind while we wait for one to learn a specific skill.

Problems Worth and Not Worth Solving

I've added the ability to mark a cutscene as a repeatable transition, and I've tought the borg to avoid these when pathing within a map, as well as to use them when navigating to a destination map. Marking a cutscene as such still happens manually, and will probably stay that way.



There's a lot of little fiddly things in this game that don't show up often enough to justify writing code to handle them. For example, there's a few places in the Ship Graveyard where you can hop from stone to stone to reach a destination. This just about the only place in the game where they use this mechanic, so I just included a few manual movement commands to get through those parts. Climbing the vines in the Worus Tower counts as a cutscene, but it only triggers if you walk into the square from the proper direction, *or* if you walk in from the side and then bump the right direction. This happens in more places, but it'll be a big pain to teach the borg that distinction, so I'm leaving that as well. If it happens to walk into a cutscene from the wrong direction, I'll tell it to walk next to the cutscene and add a manual movement command to approach properly.

Other stuff I'm willing to support with new code. FF5 has squares that quickly push you in a specific direction (such as the waterfalls in Worus and, I suspect, sand in the Quicksand Desert). These aren't cutscenes or transitions; they just happen. I haven't implemented any notion of what direction they push you, but I am giving these squares their own thumbnail in the atlas and treating them as obstacles until I find a place where I need to use them. Then we'll decide if anything more involved needs to be written.

There's also an odd quirk with NPCs when entering areas that show a title; NPCs don't seem to get marked as stationary until after that title window gets closed, so if the cartographer walks into a mostly explored map and the only unvisited squares are under stationary NPCs, it'll choose a path to that square, assuming at the time that the NPC could be pushed out of the way. I'll probably teach the Cartographer and Navigator to press A or B to close the title window before choosing their next path.
 

Monday, October 3, 2011

Horrifying Void, North Mountain Branch

Mapped and pathed through the North Mountain.


The borg found the bit of map that has improper pathing layers. The cartographer wandered around out there in the aether until it died for a few runs, and then I decided to manually modify the atlas to seal the evil away. I retained what it was able to map for posterity; maybe if I'm feeling particularly neurotic I'll crack open the seal again and send it out to explore the unknown.


The Magisa / Forza fight is pretty easy; most of the time the borg wins with everyone still standing. However, it is possible for them to get very, very lucky and get one-hit kills in on party members, and if they get 2 or 3 in then the fight is much closer. It looks like the odds are about 1 in 10 that we'll lose just mashing A; I'm okay with that for now. I want to get past the trivial boss fights and get to something that requires a more intelligent strategy.

Monks really are a great early game class. They're big sacks of hit points, they have by far the best physical attacks at this point in the game, and their greatest weakness - poor equipment choices - is almost meaningless so early on.

Hiryuu is convinced it can use transitions, and this leads to some interesting cartography problems. Not only will it try to go through the Torna Canal entrance to try to finish mapping that area out, but it'll also refuse to approach the Castle Tycoon area of the map because there's a transition to the Tycoon Meteor Site in the way. Looks like I need to be able to mark each vehicle as passing or using transitions.

Sunday, October 2, 2011

Doors

Push-open doors are becoming more of a problem than I had anticipated. The borg's movement state doesn't seem to recognize that a move command into a door didn't actually move the player, so we're getting off of our movement path by one square and getting stuck in a lot of places. We already have a check for whether FF5's internal movement flag is set after a movement command, but apparently that's not enough in this case.

My inclination is to add a check for each step to verify that we're on the square we expected to end up on, and to retry the movement if not, retaining the current limit of 200 consecutive failures before panicking. I'm worried that this is going to be a problem with transitions and cutscenes, though. We'll see what happens.

We're pathed and mapped partway through the Ship Graveyard. Karboros wasn't a problem with four monks, as expected.

Edit: Ended up changing how the pre-move cooldown works in the movement state, and our door problems are gone. Pathed through the Siren fight. Sometimes she doesn't even have time to switch form before the fight is over.

Wednesday, September 28, 2011

Fastidiousity

Felt like getting some of the refactoring grunt work out of the way, so I created a BorgCharacter class and gave it most of the code dealing with party members from the state machine. That cleaned up a lot of code, and it'll provide a good jumping off point for implementing the job system.

Tried the new and improved cartographer out on some of the dark, unmapped corners of the Pirate Ship and the Wind Shrine, and it works great. It's nice to see it scurrying around and hunting down unexplored bits that it used to ignore. I'll have to go back and clean up some remote FF4 maps with that sometime.

We have a few more remaining issues with the cartographer, and I'd like to take care of most of them before moving on from the Tule proving grounds.
  • We're not recording gold-filled treasure chests or monster-in-a-boxes properly yet. That should be a relatively quick fix.
  • Cutscenes still are ignored for the purposes of navigation unless they're our destination. At the very least I'd like to be able to manually mark a cutscene as one that repeats and moves you to a different location, so we can treat them like transitions. Without this, the cartographer can get into infinite loops, like trying to explore the wall behind the exit warp in the Wind Shrine and getting warped out over and over again.
  • Cutscenes are still sometimes recorded as starting on the wrong square. I might leave this one for now and build a test case for it when it becomes too annoying.
  • Currently, NPC avoidance assumes that we never put ourselves into a situation where we've hopelessly pinned an NPC between ourselves and our goal. This is generally true during normal gameplay, but the cartographer may end up exploring itself into just such a situation, because it didn't have the problem destination in mind until the NPC was already pinned. My inclination is ignore this problem if we can finish the map by just restarting and rerunning the cartographer when it gets stuck. If that's not good enough, I'll teach it to try to reroute around the NPC if it has to wait more than a certain number of frames.

Saturday, September 24, 2011

Due Process

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.

Monday, September 19, 2011

Transitions and Cutscenes and Menus, oh my!

I looked up / ferreted out enough memory keys to get the basics of our battle code up and running. Targeting specific enemies and using skills isn't ready yet, of course, but we have enough to Defend instead of Attack when the WingRaptor covers itself.

I also confirmed some of my suspicions about how vehicles are handled by FF5, and I have a navigator command that travels to and boards a shore-docking vehicle.

I changed the cartographer code to use transitions to reach sections of the current unfinished map. This should help avoid prematurely marking maps as finished and giving up on sections we know how to reach. I'd love to try it out, but the cartographer continues to have issues, and Tule seems to be an excellent proving grounds for the myriad problems with cutscene detection, cutscene / transition, and NPC avoidance. The borg is also convinced that we start a screen transition - and don't stop it - for as long as the menu is open.

I'd like to start making some videos of the borg in action. Going to do some investigation in producing Youtube videos, so development will slow down a bit.

Thursday, September 15, 2011

No Traffic Regulations Necessary

I'm pretty sure that the Boco used during the intro of Final Fantasy 5 isn't actually a vehicle, and neither is the boat inside the Torna Canal. This was throwing me off for quite a while trying to figure out which memory key stores the current vehicle. I'm betting Boco the vehicle isn't actually used until things open up at endgame.

Anyway, the walking/boat barriers thing is sorted out until we get the hiryuu, and then one minor code change and we'll probably be good after that. This allowed me to map and path up to the WingRaptor fight. When the cartographer got to this fight, it had understandably already been through enough fights to level up a bit, and we actually killed it before it ever covered itself with its wings. Go straight to the boss without leveling up, though, and just mashing A to get through the fight isn't enough.

Out of curiosity, I tried using the dumb timing interval I set up for the Mist Dragon from FF4, just to see what happens. The party actually stopped attacking at about the right time, but waited way too long to start attacking again, and the bird had covered itself again by the time they restarted. Well, fair enough. We'll add some conditionals to our battle strategy and call it good. We've made it to the tier 1 jobs.

I told myself I was going to completely refactor how characters are handled once I got this far, and then build out support for the job system on top of it. Now that I'm here though, I think I should stick with the development strategy I used for FF4. When I started this project, I only wrote enough code to get past the problem right in front of me. This would be a poor strategy for some software projects, but I want to continue seeing where it takes this one, for the sake of experience.

This philosophy often means using some pretty ugly code for a while. But it also means that ugly code gets prettier because it was insufficient, or because it needed to be generalized to solve a related problem. So far, the result has been progressively cleaner code that does exactly what I need it to, without any unused edifice that I built because I thought would be useful later.

So I'm just going to put some manual menu commands into my command queue and turn everybody into Monks. This will almost certainly be enough until the Liquid Flame fight. Expect a shameful battle report when I get there.

Sunday, September 11, 2011

Reinterpretations

Mapped and pathed through the Seaside Cave today. The cutscene detection definitely needs some fine tuning; a lot of regular transitions are getting recorded as cutscenes, and sometimes the cartographer's off by one square on the start point of a cutscene. There's also a few cliff edges that Boco was convinced he could walk through but obviously couldn't. Huh. Will need to go back and investigate that later when I'm not riding a bird and see if that's one of the places where the map is broken.

Made it all the way out to where we're driving the pirate ship, and promptly got stuck. It turns out that open ocean squares are all zeroes on their barrier bits, which so far we've been interpreting as no passage in any direction.

So that sucks.

It'll take some more investigation, but I believe barrier bits have an entirely different meaning on the overworld map. This will mean more code that's likely to be game specific, but if my guess is right, it will also give me the information I need to handle entering/leaving the ship. It looks like barrier bits are always 1 on land, always 0 in open ocean, and switch on the coastline. We'll have to allow stepping over that barrier from land to sea if a boat is there, and from sea to land if the land square can hold an ambulating party.

Definitely not getting to tier 1 jobs this weekend. Gonna take it easy for the next week.

Mapmaking

The cartographer for FF5 is (mostly) up and running. I've mapped out the starting area of the overworld and the Tycoon meteor site. Mobile/stationary NPC detection still isn't quite right, and I still haven't written the code to track GP treasure chests yet, but I'll get to those when they become issues, which will probably be in the village of Tule for both.

I've come up with a way to track barrier information in the atlas that I think I'll be satisfied with. It's still not quite as clean looking as having only one character per square, but unexplored squares and explored no-barrier squares will just have spaces for their barrier byte, which makes the map quite readable.

Here's a cutout of the map the cartographer made of the Tycoon meteor site.

                                                                              #0#0           
          #0#0#0#0#0#0#0#0#0                                                #0_ _ #0       
        #0_7_7_7_7_7_7_7_7_7#0                                              #0_ _ #0       
  #0    #0_D. . . . . . . . _7#0                                            #0_ _ _ #0     
#0_E#0#0#0_D. _A#0#0_9. . . . _7#0                                        ? _ _ _ _ #0   
#0_ _7_7_7. _A#0    #0_9. . . . #0                #0  ?               ?   #0_ _ _ #0 
  #0#0_9. . #0        #0. . . _A#0              #0_ #0, ? ? ? ? ? ? ? , #0_ _ _ #0
      #0. . _7#0#0    #0. . _A#0                #0_ _ . #0, , , , , #0. _ _ _ #0
      #0_9. . _7_7#0  #0. _A#0                  #0_ _ _ _ . . . . . _ _ _ _ _ #0
        #0#0_9. . _7#0#0. #0#0#0#0#0      #0    #0_ _ _ _ _ _ _ _ _ _ _ _ _ _ #0
            #0. . . #0#0. _7_7_7_7_7#0  #0_7#0#0#0_ _ _ _ _ _ _ _ _ _ _ _ _ _ #0
            #0. . . #0#0_9. . . . _E#0#0#0_D_7_7#0_ _ _ _ _ _ _ _ _ _ _ _ _ _ #0
            #0_9. _A#0  #0#0_9. . _ _7_7_7. _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ #0
          #0#0#0. #0        #0_9. . . . . . _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ #0
        #0_7_7_7. #0          #0_9. . . . . _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ #0#0#0#0
          #0. . . #0            #0#0#0#0#0#0#0#0_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _7#0
  ? #0#0#0. . . . _7#0                          #0_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _7#0
? >7_7_7_7. . . . #0                            #0_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ #0#0_9. _7#0
? + . . . . . . . . #0                          #0_ _ _ _ _ _ _ _ _ _ _ _ _ _ #0    #0_9. #0
  ? #0_9. . . . . . #0                          #0_ _ _ _ _ _ _ _ _ _ _ _ _ _ #0      #0. #0
      #0. . . . . . #0                          #0_ _ _ _ _ _ _ _ _ _ _ _ _ _ #0      #0. #0
        #0#0#0#0#0#0                              #0_ _ _ _ _ _ _ _ _ _ _ _ #0        #0. #0
                                                    #0_ _ _ _ _ _ _ _ _ _ #0          #0. #0         
                                                      #0#0#0_ _ _ _ #0#0#0            #0. #0         
                                                            #0#0#0#0                    T0         


Again, not gorgeous, but far better than I thought it would have to be. We'll see how far our current interpretation of map data gets us.

Had to refactor the cutscene state to get us past the part where we name the main character; mashing A or B isn't enough. Since this is the only place in the game where we choose a name that I'm aware of, I wasn't able to come up with a good memory key to tell when we're on the name choosing screen. So the "ChooseName" looks for the X coordinate of the finger cursor and chooses what buttons to press from that. Very specific to this game, but it works.

Should be able to get some mapping and the beginnings of a command queue done at this point. With any luck, before the end of the weekend the borg will make it to the point where we have to manage job assignment and abilities.

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:
  • 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.
I could probably sort out bit flags in the 2-byte overworld tiles for things like which vehicles can pass through which squares and which can land in which squares, but for the moment I have no compelling reason to do so. I'll probably give it a shot when I try to sort out vehicles in FF5.

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.

Thursday, September 8, 2011

Mashing a Square Peg Into a Round Hole

Figured out a ton of memory keys for FF5. Still haven't sorted out vehicle stuff, and I haven't done anything with jobs or character stats because I know I can look those up. But we have more than enough information to start retooling code and get the cartographer running.

The biggest remaining question is how to minimize code that only applies to one game, and organize the game-specific code we're forced to keep around. I'll probably start working on that this weekend.

Wednesday, September 7, 2011

Understanding Basic Navigation in FF5

Final Fantasy 4's map data lent itself fairly nicely to reverse engineering. On local maps, each square has a 1-byte number representing the square's movement layer, and there are some very basic rules about which layers connect to which; I went into more detail on this in the Traffic Jams post. Overworld map data is similar, but there's a lot more values, and different vehicles have different rules for whether they can enter each square.

That's not actually the whole story; each overworld map square actually has 2 bytes of data, but I only found the 2nd byte useful for discerning whether vehicles could land in particular squares.

Final Fantasy 5 is a bit more involved, although perhaps not as much as I initially feared. I made a couple of half-hearted attempts at figuring out memory keys for navigation 2 weeks ago, and was dismayed by the results; it's definitely using a different system than FF4. Each map square uses 2 bytes of data, and so far it doesn't appear to be layer-based. The good news: after taking another shot at deciphering map data, it looks like it's 16 bits of flags, and I may have already sorted out the most important bits.

7E10FA - current_map_square_type

This memory key stores info on the square we're currently standing on. There's data right around this on neighboring squares, which is great; building an atlas just based on reading the square we're standing on is possible, but it would take more code. Here's some sample values broken down into bits. Note that I'm presenting these as big-endian, not little-endian like they are in memory.

Samples taken from Tule Village.
3969 - 0000 1111 1000 0001 - regular walkable square
3843 - 0000 1111 0000 0011 - flowers, square just outside gated area, still can walk anywhere
3978 - 0000 1111 1000 1010 - square just inside gated area, still can walk anywhere
1930 - 0000 0111 1000 1010 - gated area, fence to north
3466 - 0000 1101 1000 1010 - gated area, fence to west
1418 - 0000 0101 1000 1010 - gated area, fence to north and west
2442 - 0000 1001 1000 1010 - gated area, fence to south and west
2954 - 0000 1011 1000 1010 - gated area, fence to south
3722 - 0000 1110 1000 1010 - gated area, fence to east
1923 - 0000 0111 1000 0011 - standing in front of Pub sign (can't walk north)
3971 - 0000 1111 1000 0011 - standing just to right of a building.
3977 - 0000 1111 1000 1001 - partly occluded by chimney / tree / side overhang
3851 - 0000 1111 0000 1011 - mostly occluded by roof of building
0 - 0000 0000 0000 0000 - obstacle.

I was very excited when I started seeing the bit breakdowns for these. Bits 11 through 8 seem to be exactly what I need: 1 if passable to the neighboring square, 0 if barrier, in the order NSWE. The order of directions struck me as a little odd, since for both FF4 and FF5 they seem to usually do directions in the order NESW, but whatever. I'm also glad that generic obstacles are still all zeros, and that treasure chests seem to all be on obstacles. Two more things I don't have to change.

My square->isLayerCompatibleWith(other_square) method is going to have to be a bit more complicated for FF5. We can't just compare numbers; we have know how the two squares are oriented and make sure the relevant passability bit is 1 in both squares.

The other 12 bits of map data obviously mean something, but I haven't sorted out what yet, and probably won't have to for a while. Some samples from the overworld map provide some clues.

Samples taken from overworld map:
12111 - 0010 1111 0100 1111 - grass
53071 - 1100 1111 0100 1111 - forest
61263 - 1110 1111 0100 1111 - desert
27215 - 0110 1010 0100 1111 - shore, can't walk south or east
26191 - 0110 0110 0100 1111 - shore, can't walk north or east
61252 - 1110 1111 0100 0100 - mountain
25212 - 0101 0011 0000 0100 - coast, land to west
26748 - 0110 1000 0111 1100 - coast, land to north
61261 - 1110 1111 0100 1101 - meteor
58437 - 1110 0100 0100 0101 - cave entrance 

These help confirm my analysis of bits 11-8, although I'm not sure about those coast measurements; I'll need to take more data while I'm in the boat. It also looks like a square can't be walked on if its two least significant bits are 0. Might be some FF4-style layering in here after all. That and the passability flags should give me enough to get the cartographer running. I'll figure out vehicles later.

My rule of development on these projects has been to write just enough code to solve the problem right in front of us and then see how much farther the borg gets. Rinse, lather, repeat. I know that my understanding of map data will change as time goes on, but this is good enough for now, and the point in time where it's not good enough is exactly the point that the cartographer will put me in a position to learn more. That's why I'm not testing my assumptions out; we'll find out soon enough how sound they are.

Starting Out on FF5

In some ways, building the borg for FF5 is going to be a lot easier. Most of the important code for the FF4 will drop right in place, provided I find the right memory keys to look at. I've already written the code that's going to save me the most time; namely, the Cartographer and Navigator states. I'll continue to fine-tune these, but I should be able to get started with commands like "GoToCutscene:Bartz Saves Lenna from Goblins" in a week or so. Battle handling will be a bit different, but we should be able to get through a good portion of the game with careful job selection and mashing A in fights.

I think the problems will actually be strategic ones. Not that FF5 is harder; indeed, with the job system, there's far more opportunities for abusing the system. No, the problem is that in Final Fantasy 5, your choices for job development early on heavily influence your battle strategy throughout the game. Not so in Final Fantasy 4, where each character is strapped in to a specific set of skills. Coming up with ways to express job development and battle strategy in a way that doesn't make me rewrite half the command queue if we run into a boss fight we're not prepared for is going to be a challenge. On the other hand, I'd prefer not to just pick a bonehead strategy (e.g. everyone is a Monk for the whole game) for ease of maintenance. I'd like to tell the borg to use some of the cooler skill combinations, most of which require some up front investment in job growth.

Fun! We'll see what happens with the Cartographer this week. Here's my current development to-do list:

  • Cartographer for FF5:
    • Figure out enough memory keys for the Cartographer to get started. Mobility, transitions, basic cutscene detection, treasure, current map.
    • Refactor isLayerCompatibleWith(). Come up with a good way to organize game-specific code and keep it as thin as possible.
    • Refactor BorgMapSquare saving/loading itself.
    • Try out the Cartographer. Run it until it gets stuck and implement what it needs next.
    • Proper monster-in-a-box reward detection.
    • Proper detection of GP in a treasure chest.
    • Include cutscenes in navigation considerations.
      • Is there a way to tell from memory whether a cutscene is repeatable, one time, or repeats on certain conditions? Or do we just have to record what kind they are manually?
      • Repeatable cutscenes should be treated similarly to transitions.
    • Don't mark a room as finished if some items of cartographical interest can be reached through transitions.
  • Add a Navigator command that walks to a square on the current map, using transitions to get there if necessary.
  • Move character stuff into a class.
  • Add a Recovery interrupt.
  • Add support for the job system.
  • Rewrite battle handler to use the skills a character has instead of hard-coding the list.
More important stuff is generally at the top, but I work on whatever I feel like working on. This is for fun, after all.

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:


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.

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.


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.



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.

Take One for the Team

Results of trial run: TPK.

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.
For most of the end game boss fights, I wrote a custom strategy to manage them instead of using the default settings.

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.

Back to the Training, Yeah!

Results of trial run: TPK.

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.


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.

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.

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.

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.

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.

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.

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.

Der Dunkelelfen

Results of trial run: TPK.

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.


A Journey of a Thousand Miles Often Ends Very, Very Badly

Result of trial run: TPK.

Details: Made it all the way to Zeromus and lost.


We simply got Big Banged more than the party can handle. No, stop it, this is serious. The real problem was that I thought I could cut down on MP usage by having Rosa default to Cure 3 and use Cure 4 if anyone is even moderately injured. This is insufficient; without a human behind the wheel or writing some code to estimate when the next Big Bang is coming, we need it to be Cure 4 all the time.
Solution: Rosa's default command is to use Cure 4 on everyone, and the conditional Cure 4 command is gone.

Slow and Steady Wins a Brace

Results of Trial Run: TPK.

Details: Lost against the Giant CPU.


Our strategy up to this point has been for Rydia to spam Bahamut, Cecil and Edge to attack, and Rosa and Fusoya to provide healing. Everyone is on Phoenix Down duty, and Cecil is in charge of using Ethers when casters get low on MP. Most of the time, this works just fine for this fight. The problem comes if the CPU chooses Rosa and Fusoya for its insta-kill attack before bringing it's bits back. If that happens, then we get into a situation where we bring Rosa and Fusoya back just in time to get lasered down by the Attack System, and the rest of the party withers away waiting for a healing spell that will never come.

Solution: After some deliberation, I decided to rewrite the strategy for this fight to the much more boring but much more reliable track: kill just the Defense System and then peck away at the CPU with melee, periodically healing from laser damage. It's boring, but it means the CPU never uses its insta-kill attacks because we never kill the Attack System until the end of the fight.

Packrat

Results of trial run: Inventory management failure.

Details: We tried to buy a Kotetsu for Edge at the Summonville weapon shop, but our inventory was full. Looks like we just got a little bit junk on this run than we normally do.

Solution: Added a SellGarbage command and a SortItems command before buying anything in Summonville. Also, it looks like we weren't loading the mid-game garbage list during the Summonville milestone, so I added that as well.

Monday, August 29, 2011

Traffic Jams

Results of trial run: Pathing/routing failure.

Details: This is where the borg got stuck:


The obvious problem is that our party is trying to walk North, and we've managed to pin an NPC between us and the stairway to the North. However, to really understand everything that's going on here, you're going to need to know more about how FF4 maps and mobile NPCs work.

Every map in FF4 that isn't an overworld map has a byte of layer information for each tile of the map. This layer info helps the game know which tiles you can walk on and when. To help visualize this, here's a picture of the save room in the Antlion's Den:


And here's an ASCII representation of the movement layers of that map:
                                 
   ###                           
  #,,,T#                         
 T,,__,_#                        
 #,,##_#                         
 #,##._.#                        
 #,.....T                        
  #.#.#.#                        
  #..S..#                        
  #.#.#.#                        
  #.....#                        
   ##.##                         
     +                           
                                 
  • Pound signs (#) are layer 0 - obstacles. These are never passable under any circumstances. The capital Ts are actually layer 0 as well, but I've marked them differently because they contain treasures.
  • Periods (.) and commas (,) are layers 1 and 2 respectively. Both can be walked on, but you can't step directly from layer 1 to layer 2 and vise versa. To get between them, you need to step onto layer 3, which is represented here by underscores (_). Layer 3 acts as a connecting layer - you can always step on and off of it.
  • Save points (S) are layer 11, but they are essentially always accessible.
  • Layers 17 and 19 represent transition squares - if you step onto one of these, you'll end up on a different map. I've chosen to mark these with plus signs (+) and greater than signs (>) respectively. Layer 17 is only accessible from layer 1. Layer 19 can be reached from anything.
  • There's one other layer, layer 5, that acts as a bridge layer. You can always step on a bridge layer from layer 1 or 2, but when you leave the bridge you have to step off onto the same layer you entered on. This allows bridges that can be walked over or under, with different rules for where you can walk for each.
So that's how the movement rules work.  Except that that's only how they work for the player, which is where our movement bug comes in.

When the borg chooses a path to a destination and starts moving down it, at every step it checks for NPCs in the way. If someone's in the way, it checks to see if the NPC would get pinned in the players path if we took the next step. If so, it politely waits until the NPC moves out of the way before moving forward. Without this politeness, all too often the borg would get itself into situations like this:


Our destination is the square the NPC is standing on, they have no way to get out of the way, and the borg doesn't know how to solve the situation. The borg eventually gives up and panics.

The problem the borg ran into in this last run is that NPCs don't have the same movement rules as the player does. NPCs are restricted to either layer 1 or layer 2; they can't transition to any layer other than the one they started on, including layer 3. So when you have a map like the top floor of Summonville, whose movement layers look like this:

             ########                  
        #####........#                 
       #.......#####.##                
      T........#   #...#               
    ##..######>#   #...##              
   #...#     #.#######...#             
  #....#     #.......#...#             
  #...#       ######.###..##           
   #.#              T  #....#    
   #_#                 #....#    
   #.T####              ##.#           
   #......#####          #_#           
    #..........#     ####T.#           
    #..........#    #......#           
     ###....>..#   ##......#           
        #......####>#......#           
         ##.####....##.####            
          #_####....##_#               
          #.............#              
           ###.........#               
             #........#                
              ####T###              


...there's a few opportunities scattered about for the borg to pin an NPC in a place that it believes the NPC can escape.

Solution: The borg method that looks for a way for an NPC to escape the player's path, pathToAvoidPlayer(), now starts with all layer 3 tiles on the current map blacklisted. This gets us 99% of the truth for NPC mobility, and should help us avoid any more pinned NPCs.

Mute Shenanigans

Results of trial run: TPK.

Details: Lost the Golbeze fight. Rosa had Silence carried over from the Calcabrena fight, and even though we had purchased 10 Echo Herbs right before this, there were none in our inventory.

Problems with the Calcabrena/Golbeze fights are a bit harder to debug. With most fights, I record a save state right as the fight begins, so if there's a problem and we lose, I can replay with the current strategy and see exactly what happened. This one, however, is a two-part fight, and the real cause of the problems is almost always in the Calcabrena fight, which gets trampled over when the Golbeze fight starts. This situation is also unusual because the Golbeze fight is one of those exceptions where the borg generally fares better than a normal player. It's actually possible to have all 4 party members back on their feet, and have Yang get in a strike before Rydia steps into the battle, all because the borg is able to input commands so quickly. So if the borg loses here, it's because we ended the Calcabrena fight in a rotten situation.

The root of the problem lies in how status flags are handled in FF4. Some status ailments are wiped when a character dies. Others, it turns out, are not, and Silence is one of those. As far as I can tell, two things were happening:
  • When Rosa would get killed in the fight and then brought back with a Phoenix Down, she still had the Silence status and couldn't heal party members.
  • In the strategy for these fights, our conditional battle command to use an Echo Herb on anyone with the Silence status comes before the command to use a Phoenix Down on dead party members. I can imagine Cecil, Kain, and Yang all huddled around Rosa's lifeless body, urgently mashing Echo Herbs into her mouth and manually moving her jaw to make chewing motions, all the while whispering: "Please. Please heal us." All with a horrifying, 12-foot tall animated doll standing behind them. Unfortunately, using an Echo Herb on a dead character doesn't remove Silence from them, but it does consume the item.
Solution: The AllyWithStatus and AllyWithoutStatus targets now only look at live characters.

No One to Blame But the Toroians

Results of trial run: TPK.

Details: Lost the Magus Sisters fight. Don't have any single dominant cause for the loss. We ran out of Hi-Potions. Tellah's reflect casting killed a lot of allies. Things just didn't go very well.

There's a few different ways we could solve this:
  • Teach Tellah/Cecil to cast healing spells on injured party members that don't have Reflect at the moment.
  • Level grind before the fight.
  • Come up with a source of more Hi-Potions.
I really, really want to avoid level grinding for the mid-game boss streak. They're all beatable without it. I'd also like to avoid commands in battle that take more than one condition; I'm sure I'll want them eventually, but I'd like to see how far I can get just allowing one condition. And finally, Hi-Potions can't be purchased at this stage in the game, so I'm stuck with the supply I find in treasure chests or from enemy drops. Farming them would essentially amount to level grinding.

Solution: After reviewing the atlas for sources of Hi-Potions, I made a discovery; we were never actually looting the room in Toroia with like 16 treasure chests, and it has 2 more Hi-Potions. Added that to the command queue after beating the Dark Elf. That should give us a bit more survivability for that fight.

Sunday, August 28, 2011

L'elfe Noir

Results of trial run: TPK.

Details: Lost to the Dark Elf. Random seeds lined up to create a beautiful test case for the current strategy; the elf almost exclusively hammers Tellah throughout the fight, and Tellah's first casting of Tornado failed in the second stage. Couldn't pull it together after that.

Solution: Cecil now has commands to cast Cure on Tellah during the second stage. It works for this iteration, but I'm not convinced this will be enough for this fight.

Furiously scrabbling for a save point

Results of trial run: Shameful TPK while trying to run from a fight.

Details: Party died out trying to run their way to the save point right before the Magus Sisters. I didn't have any Recovery commands between the Flame Dog fight and the save point.

Solution: I don't feel like adding a Recovery interrupt right now. Just sprinkled some Recovery:0.6 in the navigator commands to help make sure we survive to the save point.

Saturday, August 27, 2011

It Burns

Results of trial run: TPK.

Details: Lost the Flame Dog monster-in-a-box fight. Tellah didn't have enough juice to cast Ice 3 or heal party members, and everything went to hell in a handbasket. The real problem was that he blew his MP casting Cure 4 twice right before the fight.

Solution: This seemed like a good time to teach the Recovery state when to cast Cure X on an individual and when to cast it on the whole party. The rule is now: If more than one party member is below the HP threshold, cast the given healing spell on everybody. If only one is below the threshold, just cast it on them. We could still be smarter about it and not always use the most expensive Cure spell available to the caster, but this will still make a big difference, and in most parts of the game, it won't really matter that much.

Can't Cure Cowards

Results of trial run: Battle state bug.

Details: Young Rydia saw that Gilbert was very, very low on HP during our Damycam level grind, but he managed to go into hiding before she got her command off. The borg had been sitting there furiously trying to move the cursor to Gilbert's position for a few hundred thousand frames when I came to check on it.

It surprised me that this bug hadn't come up before for Gilbert; his cowardice is almost as legendary as his impotence as a fighter. I certainly had run into a similar problem with Kain a while ago and adjusted targeting to compensate. However, after some thought I decided this really would be a rare occurrence with our current game strategy; Gilbert has a low chance of being in critical condition during the level grind, he just defends during the Antlion fight, and I think after his fateful solo fight in Kaipo he doesn't automatically hide anymore.

Solution: I already had taught the borg that anyone with the Jump flag isn't targetable (just Kain in this game). Adding Hide to that logic was pretty trivial.

Birds, Pursuing of

Results of trial run: Pathing/routing bug.

Details: So I come back to check how the borg is doing this time around, and I'm greeting with this:



This bird is just sitting there staring at me, as if to say, "What now, asshole?"

It turns out that we managed to catch a regular Chocobo instead of a White Chocobo right at the end of the Mount Ordeals level grind. Early on in development I expected this to happen a lot, but this is the first time we've actually talked to the wrong bird.

Solution: Refactored the movement state a bit. Commands to bump in a certain direction now have a much shorter cooldown than normal movement commands. In the process of refactoring, I also discovered a long-present bug with cooldowns that was probably responsible for jumping the gun on movement between commands. To test this, I set up commands to walk into a Chocobo forest and run laps between talking to the White Chocobo and touching a point in the corner of the clearing. We're much more successful at catching fast moving birds now; before we would often have to follow it around a few squares before managing to talk while it's still in front of us.

Dreaded whitespace

Results of trial run: Game crashed.

Details: Game consistently crashed right after getting to the cutscene where the Red Wings bomb Damycam. After poking around for a while, I finally discovered the problem; there was a trailing space after the filename for the Damycam command queue. Up to this point, my command queue parser hadn't been stripping white space from the end of commands, and hadn't been verifying file handles before trying to use them. I ran into the problem a few times before, but usually caught it pretty early.

Solution: Finally broke down and wrote code to strip whitespace from the ends of commands. Also, checking to see if we were actually able to open the target file of LoadGoals, and panicing if we can't.

Can't get to Chocobo forest from there

Results of trial run: Pathing/routing failure.

Details: The borg made it to the Underworld level grind, and then tried to find a path to the Mount Ordeals Chocobo Forest. Apparently, I failed to call ClearGrindRecovery after the last level grind. Added that in, and reviewed the other level grinds to make sure they clear their recovery loop when they're done.