DOCUMENTATION FOR DATFILES MODULE
Part 2: Reading Characters from characters.dat files, with Examples

By Stumpy

Sometimes we want to know information about a built-in character as it is defined on the Characters tab in FFEdit. What is the character's base strength? How many starting character attributes does he have? What are the names of his powers? Et cetera.

NOTE: For the most part, during a mission, this information is better read from a saved game, since that way we also have power levels, the currently active character attributes, and so on. When that is not available, reading from characters.dat is made possible by the Campaign_ReadCharacters() function.

The module file datfiles.py can go in the Scripts directory. Any python file that uses these functions should import datfiles or from datfiles import *.

 

Reading characters.dat

Notes On Use

For calls to Campaign_ReadCharacters() made in the game, not specifying FileName will cause the function to try and read the characters.dat file for the current mod. Generally, this means the function can be called without any arguments. The two functions that modify the characters.dat file to change a character's attribute list and powers list require specification other arguments (see below).

None of these functions in the datfiles module use the ff, js, cshelper or other Freedom Force-specific modules, so they can be run from a python shell outside the game to summarize or explore DAT file entries, if desired. In that case, FileName must be specified.

The Functions

The principle function is Campaign_ReadCharacters() which reads character information from the characters.dat file and returns a dictionary with that information.

Campaign_ReadCharacters(FileName='',ForceRead=0,verbose=0)

Returns a dictionary whose keys are the character names of all characters in the characters.dat file and whose values are the corresponding FFEdit data fields for those characters.

FileName is the path and filename of the characters.dat file to be read. As noted above, it can usually be left blank when used during a running mission.

ForceRead forces the DAT file to be re-read, even if it has been read before.

verbose prints some info as each object is read from the DAT file.

The following two functions change part of an individual character's entry and rewrite the characters.dat file with that change.

Note that changing these values after the start of a campaign will not change any character who has already been recruited. Moreover, even for not-yet-recruited characters, the game engine does not read in the information contained in the DAT files at the start of every mission. It reads it in only when the game loads or when a call to loadData() is made before a new mission or base is started.

Campaign_RewriteCharacterAttributes(char,attribList,attribStart=5,FileName='', updatePreviousRead=1)

Finds a character's entry in charcters.dat and changes the attribute list (the one seen in FFEdit) for that character, rewriting the DAT file with the new data. It also changes the number of starting attributes for the character to the value specified.

char is the template name of the character whose attributes are to be changed.

attribList is the list of attributes the character is to have. For instance, ['disciplined', 'jumper', 'heroic', 'extra heroic'] would give a character Minute Man's attributes.

attribStart is the number of attributes the character will have when first recruited.

updatePreviousRead is a flag (set by default) that attempts to update the internal data cached when Campaign_ReadCharacters() reads character data, so that it does not have to re-read the DAT file in order to have the updated attributes changed for the character by this function.

FileName is the path and filename of the characters.dat file to be read. As noted above, it can usually be left blank when used during a running mission.

Campaign_RewriteCharacterPowers(char,powersList,tierAStart=5,tierBStart=5,FileName='', updatePreviousRead=1)

Finds a character's entry in charcters.dat and changes the powers list (the Tier A and Tier B lists seen in FFEdit) for that character, rewriting the DAT file with the new data. It also changes the number of starting powers for the character to the values specified for each tier.

char is the template name of the character whose attributes are to be changed.

powersList is the list of powers the character is to have. This is for both the Tier A and Tier B powers. The first five entries in the list are the Tier A powers and the last five are the Tier B powers, where any empty Tier A slots should be filled with empty strings. For instance, ['ironox Biff', 'ironox Haymaker', 'ironox Fancy Footwork', '', '', 'ironox Jab', 'ironox Sunday Punch', 'ironox Iron Jaw', 'ironox Force Wave'] would give a character the same power tiers that Iron Ox has.

tierAStart and tierBStart are the number of powers in each tier the character will have when first recruited.

updatePreviousRead is a flag (set by default) that attempts to update the internal data cached when Campaign_ReadCharacters() reads character data, so that it does not have to re-read the DAT file in order to have the updated powers changed for the character by this function.

FileName is the path and filename of the characters.dat file to be read. As noted above, it can usually be left blank when used during a running mission

UpdateCharactersFromDAT(source, target, items)

This utility function will find those items in each DAT and then copy the items from source into target and save the new target. BACK UP TARGET BEFORE DOING THIS!

 

The Dictionary

Campaign_ReadCharacters() returns a dictionary. See the Python Tutorial for a quick summary of Python dictionaries and the Python Library entry for a more complete description, with more of the methods available for using dictionary data, keeping in mind that Freedom Force uses Python 1.5.

Each character in the "characters" tab of FFEdit will be a key in the dictionary. For example, in the main campaign, 'minute_man' will be a key. The value corresponding to each key is itself a dictionary of data fields read for that character. The following are the keys and values for that dictionary.

'CSBase' : cut-scene base sometimes called "Origin", a string
'alterEgo' : the character's partner for Law/Order type transformations, a string
'strength' : character strength, integer with values from 0 to 9
'speed' : character speed, integer with values from 0 to 9
'endurance' : character endurance, integer with values from 0 to 9
'energy' : character energy, integer with values from 0 to 9
'agility' : character agility, integer with values from 0 to 9
'characterAttributes' : character attributes, a list of strings
'VID' : Voice ID, a string of zero, one, or two characters
'AI' : character AI, a string
'tier_a' : Tier A powers, a list of strings
'tier_b' : Tier B powers, a list of strings
'powers' : powers, a dictionary of empty power dictionaries, see Campaign_ReadPowers() for details.
'tier_a_start' : number of starting powers for Tier A, an integer
'tier_b_start' : number of starting powers for Tier B, an integer
'attrib_start' : number of starting attributes, an integer
'camp_only' : campaign only flag, 0 or 1

Note that I did not decode the information for movement radius and the allowed/preferred terrains.

 

Examples

Example 1, A Typical Character Entry

First, something very straightforward. To show all the show the typical dictionary returned by this function, we might look at the main campaign entry for Alchemiss.

Code:
>>> import datfiles
>>> chars = datfiles.Campaign_ReadCharacters()
>>> print chars['alchemiss']

This will return something like

Code:
{'camp_only': 1, 'speed': 3, 'charName': 'alchemiss', 'isCustom': 0, 'tier_a': ['alchemiss Slap', 'alchemiss Arcane Bolt', 'alchemiss Repel', 'alchemiss Hex', 'alchemiss Deflection Shield'], 'CSBase': 'library\\cut_scenes\\Alchemiss', 'characterAttributes': ['timid', 'level headed', 'levitate'], 'tier_b_start': 2, 'tier_a_start': 3, 'movementRadius': 1.0, 'attrib_start': 1, 'endurance': 3, 'energy': 5, 'powers': {'alchemiss Dimension Shift': {}, 'alchemiss Alteration': {}, 'alchemiss Arcane Bolt': {}, 'alchemiss Deflection Shield': {}, 'alchemiss Repel': {}, 'alchemiss Hex': {}, 'alchemiss Lift': {}, 'alchemiss Ward': {}, 'alchemiss Slap': {}}, 'AI': 'CGenericHero', 'alterEgo': '', 'VID': 'AL', 'tier_b': ['alchemiss Alteration', 'alchemiss Ward', 'alchemiss Lift', 'alchemiss Dimension Shift'], 'strength': 2, 'agility': 3}

Note that the keys of a python dictionary are not usually sorted. We see that the 'powers' entry contains all of her power names, but no data on the powers themselves. That's because power information is not in characters.dat, but in powers.dat. If we were to read in the powers data using Campaign_ReadPowers(), we could update the 'powers' entry for each power. This is done when Campaign_ReadCharacterData() is called.

(Of course, this is assuming we have unzipped characters.dat from data.ff into the Freedom Force\Data directory.)

Example 2, A Starting Powers

What if we want to see Alchemiss' Tier A starting powers? Try the following:

Code:
>>> import datfiles 
>>> chars = datfiles.Campaign_ReadCharacters()
>>> print chars['alchemiss']['tier_a'][:chars['alchemiss']['tier_a_start']]

This will print something like

Code:
['alchemiss Slap', 'alchemiss Arcane Bolt', 'alchemiss Repel']

Example 3, Getting the Right Bad Guys

Now for a somewhat more involved example. What if we are writing a skirmish mod where our mastermind clone villain notices that his crummy minions are getting stunned all the time. Maybe he would be interested in the following.

Code:
import datfiles 
	def FindStunResistant()
   chars = datfiles.Campaign_ReadCharacters()
   stunresistant = []
   for char in chars.keys():
       attribs = chars[char]['characterAttributes'][:chars[char]['attrib_start']]
       if ( 'grim resolve' in attribs ) or ( 'ironjaw' in attribs ):
           stunresistant.append(char)
   return stunresistant

Now we have a list of characters who are generally resistant (or more) to stun. When run in the main campaign, we might get something like:

Code:
['mr_mechanical', 'darkman_extra', 'trex_yellow', 'shadow', 'ironox_extra', 'ffx_tornado', 'man_bot_extra', 'shadow_unmasked', 'time_master', 'darkman_purple', 'trex_purple', 'trex', 'shadow_extra', 'darkman_blue', 'shadow_unmasked_extra']

Now, the stun-happy heroes may be in trouble.

Example 4, Power Cloning

Let's say we have a campaign with a mission where, at the mission's end, we want a character to be transformed so that he has a random set of powers culled from those of all the built-in members of the squad.

If this sounds, contrived, it is. There are probably better examples, but I wanted one where a character ends up with powers that couldn't be known before the mission started (or at the beginning of the campaign).

To pull this off, before ever starting the game, we first create in FFEdit a copy of the character to be transformed in the Characters and Template tabs. If the original had the template name 'mr_spiff' then we call the transformed version 'mr_spiff_t'. We also fix the language files and so on. Then, at the end of the mission where Spiff is transformed, we play our cut-scene with some special effects and run some code like the following (probably in OnMissionWon())

Code:
import datfiles
import random
# somewhere in OnMissionWon()
   squad = getAllHeroes()  # get the squad members
   squad = filter(lambda c: not Object_IsCustomChar(c),squad)  # ignore customs
   characters = datfiles.Campaign_ReadCharacters()
   allpowers = []          # make a list of all the squad's powers
   for hero in squad:
       allpowers.extend(characters[hero]['tier_a'])
       allpowers.extend(characters[hero]['tier_b'])
   newpowers = []          # make a random list of the new powers
   for i in range(10):
       randpower = allpowers[random.randint(0,len(allpowers)-1)]
       newpowers.append(randpower)
       allpowers.remove(randpower)
   # now rewrite the transformed character's powers
   datfiles.Campaign_RewriteCharacterPowers('mr_spiff_t',newpowers,2,2)
   loadData()              # make sure the game re-reads the character database

Now we have a transformed character in the DAT files, but we still need to get rid of the old version and and add the new version to our recruits. So, when we start the next base scene, we include the following code

Code:
# in OnPostInit()
   if Campaign_IsRecruited('mr_spiff'):
       Campaign_Unrecruit('mr_spiff')
       Campaign_Recruit('mr_spiff_t')

Then we should have a Spiff with some nice mixed up powers.

 

Notes

First, for this function to work, there must actually be a characters.dat file, either in the mod folder for whatever mission is running or where specified by FileName.

I wrote this module with an eye on keeping delays minimal. Because of that, Campaign_ReadCharacters() tries to avoid re-reading the characters.dat file every time it is called and that should keep disk I/O pretty low. The first read, however, isn't necessarily quick. On my laptop machine, it read a 240KB characters.dat (the first time) in under a quarter second. Not too bad, but this can be a noticeable jump in the game. Subsequent calls are much faster (under a millisecond).

Finally, I didn't fully decode every byte of the DAT files. There was certain information that I wanted to extract and I tried to ignore the rest. However, that means that there may be parts of a DAT file that confuse the reader functions, though I have tested each of these functions at least on the main campaign DATs. If you have a working DAT file (everything looks fine in FFEdit) with which these functions do not work, PM me and I will check into it.