DOCUMENTATION FOR CHARDATA MODULE
Finding miscellaneous data about a character, with Examples

By Stumpy

Is a character a flier? Does he have the TOUGH GUY character attribute? Does he have any ranged, electrical powers? This sort of information is often helpful to people writing mission scripts and particularly for people designing AIs or scripted attributes. The chardata module is designed to run on top of the datfiles module and provide just that sort of information.

Of course, the information itself comes from the DAT (and hero) files and is read by the datfiles module. So why a separate module here? The datfiles module was designed to be independent of the in-game modules, ff and js. It could be run from any python environment, which is sometimes useful, both for testing and for generating data files offline that might be read in later during the game. The chardata module provides an in-game interface to the datfiles data and it requires the js and ff modules to do that.

In addition, chardata requires the 'complex' object attribute be set uniquely for each character in order to function in the Danger Room and with characters not saved in the save data files. That means that DrMike2000's FFX Control Centre should be run whenever a new character is added to the game, either as a built-in or as a custom hero. Other than the 'complex' attribute tag, there is no dependence on FFX (though my hope was that this module will be useful for scripting FFX character attributes).

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

 

Functions

The Base Function

The basic function is GetCharacterData() which takes an in-game object name for a character and returns a filled-out dictionary of data for that object.

GetCharacterData(object)

Returns a dictionary whose keys are the names of all data for a character object. Where that data comes from depends both on the object itself and on the game environment.

If the function is called during a campaign mission, the function tries to get its information from a recent saved game (as returned by datfiles.Campaign_ReadCharactersFromSavedGame()). It tries to be clever about this, but its methods are not bulletproof. It checks the saved game directory (and the quicksave and autosave subdirectories) for saved games and sorts them from most to least recent. Then, starting at the newest saved game, it checks that that game was saved in a mission that came no later in the campaign than the current one.

Thus, if someone plays the last mission in a campaign, saves, and then decides to restart the campaign or replay one of the middle missions, data will be taken from the most recent saved game that is no later than where he's at now, not from the final mission of the campaign.

The rule of thumb is that players should save at the base after they have finished leveling up the characters from the last mission. That ensures that the most recent changes to the character will be in the character data this function returns.

If a valid saved game cannot be found, the function gets data as it would in the Danger Room.

In the Danger Room, data is taken from the current mod's campaign DAT files (characters.dat, powers.dat, etc.) and from hero files. Since the game doesn't generate consistent template names for danger room characters, it is important that FFX Control Centre be run if new heroes have been created, either as custom hero files or as built-ins using FFEdit or from hero files using EZHero's "Send to DAT" function.

The object is the in-game object name (not necessarily the template name, although they are sometime the same). E.g. 'hero_1', 'sc_thug_002', et cetera.

 

Helper Functions

Some other functions filter the dictionary that GetCharacterData() returns and may be useful for scripting. Most of these deal with checking if a character has certain powers and the constants near the top of datfiles.py and described in the datfiles module documentation for Campaign_ReadPowers() will be helpful here, as will the datfiles helper function ShowPower() for testing.

The idea here was to provide basic functionality and examples of what can be done, so not every function is here, but these should provide basic templates for others that may be needed.

For those functions that return a list of powers, the list of powers returned is a list of power dictionaries, not power names. For any power dictionary, the power name is given by the 'PowerName' key.

In each of the functions that has an ignoreBought parameter, the parameter defaults to 0. When 0, the function pays attention to whether the power in question has actually been purchased by the character, for built-in characters. If ignoreBought is 1, it treats the powers as if they have all been bought. That is, at the beginning of the main campaign, El Diablo doesn't have his Flaming Fist power, although it is in his data, so Character_GetMeleePowers() with ignoreBought = 0 will only return it in his list of melee powers if he has spent the CP to buy it, whereas with ignoreBought = 1, it will be in the list even if he doesn't have it available to him yet.

Character_GetPower(char, key1, val1, key2, val2, ignoreBought=0)

This is the general power-finding function. For a given character, it returns the list of all the character's powers that match the given two-key filter criteria. The first two items are one of the power keys paired with the value it must have, and so on. If the key is one that can match on multiple values, then the match is determined by logical and with the given value. For instance, if the key is 'DamageTypesBlocked' and the value is PT_DAMAGE_BLOCKED_ENERGY, then that will match a defense that blocks fire and energy as well as one that only blocks energy.

For example, if I want to find all of hero_1's ranged radiation attacks, I might call Character_GetPower('hero_1', 'PowerType', datfiles.PT_RANGED, 'DamageType', datfiles.PT_DAMAGE_RADIATION)

Character_HasPassiveDefence(char, damage_type_blocked, attack_mode_blocked, ignoreBought=0)

This function determines if the character has a passive defense and, if he does, it returns the success chance the defense has of blocking an attack. Otherwise (or if he has an unbought passive defense and ignoreBought is 0), it returns 0.

For example, if I want to check hero_1's defense against piercing ranged attacks, I might call Character_HasPassiveDefence('hero_1', datfiles.PT_DAMAGE_BLOCKED_PIERCE, datfiles.PT_RANGED_BLOCKED) If the hero is Minute Man and he has his Eternal Vigilance defense at level 4, then this function will return 0.57 (57%), his chance of deflecting this sort of attack.

Note that this can be used for boolean checks. For example:

Code:
if Character_HasPassiveDefence('hero_1', datfiles.PT_DAMAGE_BLOCKED_PIERCE, datfiles.PT_RANGED_BLOCKED):
# hero_1 goes after gun thug
...

Character_HasActiveDefence(char, damage_type_blocked, attack_mode_blocked, ignoreBought=0)

Similarly, this function determines if the character has a personal active defense and, if he does, it returns the block type of that defense (see datfiles.PT_BLOCK_TYPES). Otherwise (or if he has an unbought active defense and ignoreBought is 0), it returns 0. Note that this will not match remote active defenses (which he cannot use on himself), for which Character_HasRemoteDefence() is the appropriate function.

Since the block type constants are all nonzero, this can be used for boolean checks as well.

Note that this function does not determine if the active defense is currently on or off.

Character_HasRemoteDefence(char, damage_type_blocked, attack_mode_blocked, ignoreBought=0)

The compliment for remote defenses, this function determines if the character has a remote active defense and, if he does, it returns the block type of that defense (see datfiles.PT_BLOCK_TYPES). Otherwise (or if he has an unbought active defense and ignoreBought is 0), it returns 0.

Since the block type constants are all nonzero, this can be used for boolean checks as well.

Note that this function does not determine if the remote defense is currently on or off.

Character_GetDamageTypePowers(char, DamageType, ignoreBought=0)

This function returns a list of all powers the character has with the given damage type (one of the PT_DAMAGE_TYPES constants). This ignores special damage type powers; if the power is really an alteration power, it won't show up as a crushing power, even if that is also true (and irrelevant).

For example, if I want to check what powers hero_1 has that do radiation damage, I might call Character_GetDamageTypePowers('hero_1', datfiles.PT_DAMAGE_RADIATION) If the hero is Microwave, this will return a list of several powers. For example:

Code:
>>> import chardata
>>> import datfiles
>>> mw_rad_powers = chardata.Character_GetDamageTypePowers('microwave',datfiles.PT_DAMAGE_RADIATION)
>>> datfiles.ShowPower(mw_rad_powers[0]) 
        PowerName = microwave Rad Bolts
        PowerType = PT_RANGED
        SubType = PT_ATTACK_SUBTYPE_PROJECTILE
        EPCost = medium
        animation = ranged_2
        FX = microwave_radbolts
        Magnitude = medium
        DamageType = PT_DAMAGE_RADIATION
        Speed = slow
        Stun = none
        Knockback = none
        RangeMin = short
        RangeMax = medium
        Accuracy = medium
        Radius = none
        SpecialType = PT_SPECIAL_NONE
        MaxInstances = 3
        AttackFlags = PT_ATTACK_FLIGHT_SPAWN 
        notForCustom = 0
>>> datfiles.ShowPower(mw_rad_powers[1]) 
        PowerName = microwave Microwave Beam
        PowerType = PT_RANGED
        SubType = PT_ATTACK_SUBTYPE_BEAM
        EPCost = very low
        animation = ranged
        FX = microwave_microwavebeam
        Magnitude = medium
        DamageType = PT_DAMAGE_RADIATION
        Speed = normal
        Stun = none
        Knockback = none
        RangeMin = short
        RangeMax = long
        Accuracy = high
        Radius = none
        SpecialType = PT_SPECIAL_NONE
        MaxInstances = 0
        AttackFlags = 
        notForCustom = 0


Note that this can be used for Boolean checks. Any empty list evaluates to false and any non-empty list evaluates to true.

Character_GetSpecialDamageTypePowers(char, SpecialType, ignoreBought=0)

This function returns a list of all powers the character has with the given special type (one of the PT_SPECIAL_TYPES constants).

For example, if I want to check if hero_1 has the 300 percenter-type power, I can call Character_GetSpecialDamageTypePowers('hero_1', datfiles.PT_SPECIAL_PATRIOT_CHARGE).

Character_GetMeleePowers(char, ignoreBought=0)

Character_GetRangedPowers(char, ignoreBought=0)

Character_GetAreaPowers(char, ignoreBought=0)

Character_GetDirectPowers(char, ignoreBought=0)

Character_GetSpecialPowers(char, ignoreBought=0)

Each of these functions returns a list of all powers the character has of the given attack mode (one of the PT_POWER_TYPES constants).

Note that Character_GetRangedPowers() returns strictly ranged powers (projectiles, beams, cones, etc.), not direct or special powers that have a range.

They can be used for Boolean checks. Any empty list evaluates to false and any non-empty list evaluates to true.

Character_HasAttribute(char,attrib,ignoreBought=0)

Returns true if the character has the given character attribute, false otherwise. IMPORTANT: When used in-game, you should employ ffx.py's hasAttribute(char, attrib, ffxOnly=0) instead, as it checks for dynamic attributes before calling the above function.

For example, when first added to the (first game) campaign, The Alchemiss might test out like the following:

Code:
>>> import chardata
>>> print chardata.Character_HasAttribute('alchemiss','timid') 1 >>> print chardata.Character_HasAttribute('alchemiss','level headed') 0 >>> print chardata.Character_HasAttribute('alchemiss','level headed',ignoreBought=1) 1

 

Character Keyframes

Determining animation end times for a character

Character_GetPowerAnimationTime(object,PowerName,ForceRead=0, verbose=0)

Given the character object and power name, return the animation 'end' time for that power.

Determining keyframe file locations for a character

GetKeyframeFile(object)

Return the location of a character's keyframe file. The argument given is the object name (not the template). Must be run from within the game.

UnzipMesh(KFtests, ZipPath)

Check the given ZipPath archive for the keyframe files in the KFtests list. If a match is found, unzip the associated mesh and return the math name to the KF file.

 

The Dictionary

GetCharacterData() 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.

The dictionary returned is "filled out", meaning that has, essentially, the data in it that a hero file would have. All of the character stats (strength, speed, etc.), attributes, powers, object attributes (material, class, etc.), NIF file, and so on. It does not currently contain data about the character's animations. (Maybe some day, but not now.) 

The easiest way to test this is to load a saved game or enter Danger Room and test it. For that purpose, a datfiles function ShowHero() should be useful. It takes a character dictionary as its argument and prints out the character data in a somewhat human-readable format.

With that said, rather than repeat the information about each of the dictionary's entries, I refer the reader to the datfiles documentation for Campaign_ReadCharacters(), Campaign_ReadObjects(), and Campaign_ReadPowers().

 

More Examples

Example 1, A Typical Character Dictionary

First, something very straightforward. To show all the show the typical dictionary returned by the GetCharacterData() function, we might look at the main campaign character El Diablo midway through the campaign.

Code:
>>> import datfiles 
>>> import chardata 
>>> diablo = chardata.GetCharacterData('el_diablo') 
>>> datfiles.ShowHero(diablo) 
            charName : el_diablo
            isCustom : 0
            strength : 3
               speed : 3
             agility : 3
           endurance : 3
              energy : 4
                 VID : ED
                  AI : CGenericHero
                 NIF : library\characters\el_diablo\character.nif
            material : 4.0
                mass : 90.0
            alterEgo : 
    activeAttributes : 2
 characterAttributes : ['flier', 'hot tempered']
    objectAttributes : ['complex', 'class', 'NIF', 'material', 'templateName', 'mass', 'pickupDistance', 'elasticity']
      movementRadius : 1.0
               class : GAME_OBJ_HERO
              CSBase : library\cut_scenes\Eldiablo
                  XP : 2800
                  CP : 38
              tier_a : ['eldiablo Punch', 'eldiablo Fire Shield', 'eldiablo Flaming Fist', 'eldiablo Absorb Heat']
              tier_b : ['eldiablo Flame Projection', 'eldiablo Inferno', 'eldiablo Ignite', 'eldiablo Hellfire']
         powerLevels : {'eldiablo Ignite': 0, 'eldiablo Flame Projection': 5, 'eldiablo Fire Shield': 0, 'eldiablo Inferno': 5, 'eldiablo Absorb Heat': 0, 'eldiablo Punch': 1, 'eldiablo Flaming Fist': 0, 'eldiablo Hellfire': 0}
             complex : 221.0
          elasticity : 0.0
      pickupDistance : 2.0
              powers : ['eldiablo Ignite', 'eldiablo Flame Projection', 'eldiablo Fire Shield', 'eldiablo Inferno', 'eldiablo Absorb Heat', 'eldiablo Punch', 'eldiablo Flaming Fist', 'eldiablo Hellfire']
	eldiablo Ignite:
        PowerName = eldiablo Ignite
        PowerType = PT_DIRECT
        SubType = PT_ATTACK_SUBTYPE_NONE
        EPCost = low
        animation = special
        FX = eldiablo_ignite
        Magnitude = medium
        DamageType = PT_DAMAGE_PIERCE
        Speed = very slow
        Stun = none
        Knockback = none
        RangeMin = short
        RangeMax = long
        Accuracy = very low
        Radius = none
        SpecialType = PT_SPECIAL_IGNITE
        MaxInstances = 0
        AttackFlags = 
        notForCustom = 0
	eldiablo Flame Projection:
        PowerName = eldiablo Flame Projection
        PowerType = PT_RANGED
        SubType = PT_ATTACK_SUBTYPE_BEAM
        EPCost = low
        animation = ranged
        FX = eldiablo_flameproject
        Magnitude = medium
        DamageType = PT_DAMAGE_FIRE
        Speed = normal
        Stun = none
        Knockback = none
        RangeMin = medium
        RangeMax = long
        Accuracy = high
        Radius = none
        SpecialType = PT_SPECIAL_NONE
        MaxInstances = 0
        AttackFlags = 
        notForCustom = 0
	eldiablo Fire Shield:
        PowerName = eldiablo Fire Shield
        PowerType = PT_ACTIVE_DEFENCE
        BlockType = PT_BLOCK_TYPE_NORMAL
        DamageTypesBlocked = PT_DAMAGE_BLOCKED_COLD PT_DAMAGE_BLOCKED_PIERCE PT_DAMAGE_BLOCKED_CRUSH 
        AttackModesBlocked = PT_AREA_BLOCKED PT_RANGED_BLOCKED PT_MELEE_BLOCKED 
        DefenceFlags = PT_DEFENCE_FLAG_INFINITE PT_DEFENCE_FLAG_MOVE 
        EPCost = low
        Duration = medium
        animation = active_defence
        FX = eldiablo_fireshield
        notForCustom = 0
	eldiablo Inferno:
        PowerName = eldiablo Inferno
        PowerType = PT_RANGED
        SubType = PT_ATTACK_SUBTYPE_EXPLOSIVE
        EPCost = medium
        animation = ranged_3
        FX = eldiablo_inferno
        Magnitude = high
        DamageType = PT_DAMAGE_FIRE
        Speed = slow
        Stun = medium
        Knockback = high
        RangeMin = short
        RangeMax = medium
        Accuracy = low
        Radius = medium
        SpecialType = PT_SPECIAL_NONE
        MaxInstances = 0
        AttackFlags = 
        notForCustom = 0
	eldiablo Absorb Heat:
        PowerName = eldiablo Absorb Heat
        PowerType = PT_PASSIVE_DEFENCE
        BlockType = PT_BLOCK_TYPE_ABSORB
        DamageTypesBlocked = PT_DAMAGE_BLOCKED_FIRE 
        AttackModesBlocked = PT_AREA_BLOCKED PT_RANGED_BLOCKED PT_MELEE_BLOCKED 
        DefenceFlags = PT_DEFENCE_FLAG_INACTIVE 
        Success = PT_BLOCK_SUCCESS_FREQUENT
        notForCustom = 0
	eldiablo Punch:
        PowerName = eldiablo Punch
        PowerType = PT_MELEE
        SubType = PT_ATTACK_SUBTYPE_NONE
        EPCost = none
        animation = melee
        FX = 
        Magnitude = low
        DamageType = PT_DAMAGE_CRUSH
        Speed = normal
        Stun = low
        Knockback = low
        RangeMin = medium
        RangeMax = 5
        Accuracy = very low
        Radius = none
        SpecialType = PT_SPECIAL_NONE
        MaxInstances = 0
        AttackFlags = 
        notForCustom = 0
	eldiablo Flaming Fist:
        PowerName = eldiablo Flaming Fist
        PowerType = PT_MELEE
        SubType = PT_ATTACK_SUBTYPE_NONE
        EPCost = low
        animation = melee_2
        FX = eldiablo_flamingfist
        Magnitude = high
        DamageType = PT_DAMAGE_FIRE
        Speed = normal
        Stun = none
        Knockback = none
        RangeMin = medium
        RangeMax = 5
        Accuracy = very low
        Radius = none
        SpecialType = PT_SPECIAL_NONE
        MaxInstances = 0
        AttackFlags = 
        notForCustom = 0
	eldiablo Hellfire:
        PowerName = eldiablo Hellfire
        PowerType = PT_RANGED
        SubType = PT_ATTACK_SUBTYPE_EXPLOSIVE
        EPCost = very high
        animation = ranged_2
        FX = eldiablo_hellfire
        Magnitude = high
        DamageType = PT_DAMAGE_FIRE
        Speed = slow
        Stun = medium
        Knockback = high
        RangeMin = short
        RangeMax = medium
        Accuracy = low
        Radius = medium
        SpecialType = PT_SPECIAL_NONE
        MaxInstances = 2
        AttackFlags = PT_ATTACK_IMPACT_SPAWN 
        notForCustom = 0

Example 2, Mental Power (redux)

In the Strangers campaign, DrMike2000 has given some characters with man amplifier suits a very cool effect where any damage they take is projected to enemies around them as mental damage. The amount of damage projected is a 50% of the damage taken, modified by the target's mental attributes which makes mentally tougher characters more resistant to this damage. The original version accounted for FFX attributes, but now it is possible to account for all character attributes. That way, character who have the DISCIPLINED or similar attributes will take less mental damage, just the way other damage types work.

Here is a variation on the original function that determines what fraction of the original damage a character will take. It takes into account FFX attributes, the in-game character attributes, and any passive area mental defenses the character might have.

(If you actually need functions to detect a character's resistance to various damage types, variants on the code below are already available in ffx.py: cf. getCrushResistanceValue(char, passive=1, active=1, attribs=1, states=1, material=1) and other types.)

Code:
# Modified from the 16b_homecoming2/mission.py script of DrMike2000's Strangers mod.
	import chardata
	def GetMentalDamage(char):
    # speed things up with a little cacheing
    global MentalDamageData
    if 'MentalDamageData' in globals().keys():
        if char in MentalDamageData.keys():
            damage = MentalDamageData[char]
            return damage
    else:
        MentalDamageData = {}
    MentalPassiveD = chardata.Object_HasPassiveDefence(char,datfiles.PT_DAMAGE_BLOCKED_MENTAL,datfiles.PT_AREA_BLOCKED)
    damage = 50*(1.0-MentalPassiveD)
    if (int(Object_GetAttr(char,'gender'))&31)==FFX_GENDER_ROBOT:
        damage=damage/2
    if hasAttribute(char,'mindless'):
        damage=0
    if hasAttribute(char,'fearless') | hasAttribute(char,'dispassionate'):
        damage=damage/2
    if hasAttribute(char,'conduit') | hasAttribute(char,'glassjaw'):
        damage=damage*2
    if chardata.Object_HasAttribute(char,'weakminded'):
        damage=damage*1.5
    if chardata.Object_HasAttribute(char,'timid'):
        damage=damage*1.25
    if chardata.Object_HasAttribute(char,'hot tempered'):
        damage=damage*1.25
    if chardata.Object_HasAttribute(char,'cybernetic brain'):
        damage=damage/1.5
    if chardata.Object_HasAttribute(char,'level headed'):
        damage=damage/1.5
    if chardata.Object_HasAttribute(char,'disciplined'):
        damage=damage/1.5
    if chardata.Object_HasAttribute(char,'tough guy'):
        damage=damage/1.5
    MentalDamageData[char] = damage*0.01
    return damage*0.01

Note that we also do some caching in the script to speed things up if we have already done the calculations for a particular character. Alternately, we could code things slightly differently and use the character attributes straight from GetCharacterData(). This version is slightly tidier, but does the same thing.

Code:
# Modified from the 16b_homecoming2/mission.py script of DrMike2000's Strangers mod.
	import chardata
	def GetMentalDamage(char):
    # speed things up with a little cacheing
    global MentalDamageData
    if 'MentalDamageData' in globals().keys():
        if char in MentalDamageData.keys():
            damage = MentalDamageData[char]
            return damage
    else:
        MentalDamageData = {}
    MentalPassiveD = chardata.Character_HasPassiveDefence(char,datfiles.PT_DAMAGE_BLOCKED_MENTAL,datfiles.PT_AREA_BLOCKED)
    damage = 50.0 * ( 1.0 - MentalPassiveD )
    ch_data = chardata.GetCharacterData(char)
    for ch_attr in ch_data['characterAttributes'][:ch_data['activeAttributes']]:
        if ch_attr == 'mindless':
            damage = 0
            break
        if ch_attr in ['fearless','dispassionate']:
            damage = damage / 2
        if ch_attr in ['conduit','glassjaw']:
            damage = damage * 2
        if ch_attr == 'weakminded':
            damage = damage * 1.5
        if ch_attr in ['timid','hot tempered']:
            damage = damage * 1.25
        if ch_attr in ['cybernetic brain','level headed','disciplined','tough guy']:
            damage = damage / 1.5
    MentalDamageData[char] = damage*0.01
    return damage*0.01

Note on caching:  These functions are pretty quick, but functions that are called often or that are likely to do lots of calculation for the same character (and should yield the same results each time) are good candidates for caching. Caching using dictionaries is simple: just declare a global variable within the function and check if it's already been assigned a value by seeing if it is in the list of global variables stored in globals().keys().  If it's not, then initialize it as an empty dictionary. If it is, check if the character we're currently calculating is in its list of keys and, if so, just return that key's value. If not, then do the calculation as normal and add that character to the dictionary as the value with that character as a key.

 

Notes

First, for these functions to work, there must actually be an objects.dat, characters.dat, powers.dat, and so on files unzipped in the right spots, either in the mod folder for whatever mission is running or where specified by FileName. There is a facility in FFvsT3R to unzip these files from a python script, but it is unavailable in the first game.

I wrote this module with an eye on keeping delays minimal. Because of that, chardata.GetCharacterData() tries to avoid re-reading the DAT files 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 the 1.7MB main campaign objects.dat (the first time) in about 0.35 seconds. This can be a noticeable jump in the game. Some mods have pretty big DAT files, so this call can take over a second. Subsequent calls are much faster (about a millisecond). If this function is to be called during a mission, it is a good idea to put a call to Campaign_ReadCharacterData() (the result doesn't need to be assigned to a variable for the caching to work) in the onPostInit() after the cut-scene, so that the file is read in the background.

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.