It is very often useful to know about a power as it is defined on the Powers or Defenses tabs in FFEdit. What type of damage does it inflict (or block, if it is a defense)? What is its attack mode (melee, area, etc.)? Is it penetrating? Et cetera.
NOTE: Obviously, this sort of information is can be very useful when combined with information about a character, perhaps as obtained via a function like Campaign_ReadCharactersFromSavedGame() or Campaign_ReadCharacters(). One of the functions in the chardata module details combining these functions with character data to come up with a function Object_HasPassiveDefense(), which will return the chance that a character has of blocking an attack of a given damage type and attack mode. In addition, one strategy for AI coding might be to write a tactic for (or against) a given power, regardless of who has it.
In addition, there are a couple of utility functions in the module for working around the active defence duration issue in FFEdit2 file.
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 *.
For calls to Campaign_ReadPowers() made in the game, not specifying FileName will cause the function to try and read the powers.dat file for the current mod. Generally, this means the function can be called without any arguments.
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 principle function is Campaign_ReadPowers() which reads power information from the powers.dat file and returns it in a dictionary object.
Returns a dictionary whose keys are the power names of all powers in the powers.dat file. Each value in that dictionary is another dictionary whose keys correspond to the fields of that power's FFEdit data. The contents of that dictionary are discussed below.
FileName is the path and filename of the powers.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.
Helpful for debugging, this prints out a summary of a power when given a power dictionary.
power a power dictionary of the sort returned as one of the entry values in the Campaign_ReadPowers() dictionary. See below for an example.
There isn't a nice way to keep a clean (unrewritten) version of powers.dat without writing wrapper functions for FFedit, EZHero, and any other function that might write changes to the file. Adding further complication, it is possible to use those programs while the game is running. Therefore, after using those programs to change the powers DAT, be sure to delete any powers.dat.bak (or .bak2, .bak3, ...) files from the mod folder.
This makes a change to the powers.dat file for one or more powers. Potentially very useful when used with the loadPowers() function (new to FFvsT3R). This function also changes the internal powers dictionary so that it need not be re-read after making a change to the DAT file. Do not change the names of the powers (the 'PowerName' value).
Note that this function is potentially dangerous. Remember that changes made this way stay in place until they are explicitly changed again, even across game restarts. The first time it is called during mission, this function creates a backup of the powers.dat file before rewriting it. It is tough to know what changing a power that's already in use might do and it is a good idea to keep a backup of powers.dat so that errant uses of this function may be undone. (For that purpose, Campaign_PowersRewritten() below tracks whether or not powers.dat has been rewritten during this mission and Campaign_RevertPowers() reverts powers.dat to a backed up version.)
(Note also that there is, as yet, no way to have the game re-read data for a particular custom hero during a mission. Because of that, I have not written a corresponding function for changing a custom's powers in his hero file, though it would be possible to do so.)
FileName a powers.dat file, as with Campaign_ReadPowers(). Note that, since all the arguments for this function are keyword arguments, calling the function with only one of the arguments is perfectly valid as long as the keyword is specified. That is Campaign_RewritePowers(ChangePowers=[mypower]) is perfectly valid.
ChangePowers is a list of power dictionaries to be changed. Each power dictionary is of the sort returned as a value in the Campaign_ReadPowers() dictionary.
ForceRead forces the powers.dat DAT file to be re-read, even if it has been read before.
Returns 1 if powers have been rewritten via the above function since the last mission restart/reload. This function essentially checks for the presence of the backup of powers.dat and returns one if it is there, zero otherwise. Since Campaign_RewritePowers() cannot be called without making a backup, this should be pretty safe.
Looks for a .bak version of the powers DAT (e.g. powers.dat.bak) and, if it is found, renames that version to powers.dat. Note that, since it is not possible to tell if the player has quit a mission or restart (or crashed) the game, it may be a good idea to call this function at the beginning of a mission so that any unreverted changes from previous calls to Campaign_RewritePowers are undone.
FileName a powers.dat file, as with Campaign_ReadPowers().
ForceReread forces the powers.dat DAT file to be re-read, even if it has been read before.
This registers a callback event sink when the powers DAT file is rewritten. The callback function is run when Campaign_RewritePowers() makes a change to the powers.dat file for the power named powername.
(Note that this creates a pseudo-sink. There are no hooks into the engine for registering arbitrary sinks, so this only emulates one. For practical purposes, the only difference is that js.Event_CancelSinks() will not cancel these sinks. There are other functions below to do so.)
powername the name of the power in the powers.dat file. This should be as it appears in the database. Alternately, this argument can be passed as the empty string '' in order to register the callback function with all powers that are changed.
Regardless of whether a real power name is given or the empty string, the power name will be passed to the callback function as event.object.
callback the name of the callback function to run.
string an arbitrary string to pass to the callback function as event.string.
persistent if nonzero, the callback will be called each time powername is changed, without re-registering the callback. Note that registering a persistent callback more than once will result in the callback being run more than once (just as with regTimer() and the like). Use one of the functions below to remove a callback sink. This is passed to the callback function as event.data.
float an arbitrary number passed to the callback function as event.float.
user an arbitrary number passed to the callback function as event.user.
Analogous to js.Event_CancelSinks(), this cancels all rewrite sinks registering a particular callback function. This should be harmless if called and no such function is registered.
callback the name of the callback function to cancel.
This cancels all rewrite sinks that are registered to a particular power. This should be harmless if called and no sinks are registered for the power.
powername the name of the power whose sinks will be cancelled.
This cancels all rewrite sinks, period.
Campaign_ReadPowers() 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 power in the "Powers" and "Defenses" tab of FFEdit will be a key in the dictionary. For example, in the main campaign, 'mentor Electron Beam' will be a key and 'mentor Psychkinetic Shield' will be a key. The value corresponding to each key is itself a dictionary of data fields read for that power. The following are some of the keys and values for that dictionary.
Many of the fields below have integer values for which I have defined constants for ease of use and portability. Those constants are enumerated below.
'PowerName' : the power's name, a string
'PowerType' : the type of power this is (melee, direct, passive defense, etc.), an integer.
Almost all powers have the above fields. The following apply to most powers:
A couple of utility functions serve to view and modify the powers.dat file. Though they are very specific, they are documented here in case they help in mod-making tasks.
In FFEdit2, active defences duration has no UI. Because of that, whenever a specific active defence is saved, its Duration is set to 0 (short).
This returns defence powers names with their duration as a list of each [stringPowerName, intDuration] and prints out the equivalent to the console.
Rewrite the selected active defence power to change the duration. Valid durations are:
Note that this function does not reload powers.dat; you'll have to do that manually if you're calling Campaign_ChangeActiveDefenceDuration() in-game and want the changes to affect the mission currently playing.
The following constants are defined in datfiles.py and may be useful for working with powers. It is generally better scripting practice, for instance, to check if power['PowerType']==PT_SPECIAL rather than power['PowerType']==7. Although each constant is defined individually in datfiles.py, there is often a dictionary defined as well whose keys are the constant's value and whose values are the constant's name or a list whose entries are strings corresponding to the integer values of the power data. This is useful for getting a more reader-friendly value when testing. For example, assume smash = datfiles.Campaign_ReadPowers()['minute Patriot Smash'] and displace = datfiles.Campaign_ReadPowers()['microwave Displace Image']. It is easier to understand
>>> print PT_POWER_TYPES[smash['PowerType']] PT_MELEE >>> print PT_SPECIAL_TYPES[displace['SpecialType']] PT_SPECIAL_DISPLACE_IMAGE
than
>>> print smash['PowerType'] 1 >>> print displace['SpecialType'] 36
Note that the spellings are often taken to correspond to internal game spellings, so they may not be what is considered standard in some places, e.g. "defence" versus "defense".
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 one of the typical minions.
>>> import datfiles >>> smash = datfiles.Campaign_ReadPowers()['minute Patriot Smash'] >>> print smash
This will return something like
{'Accuracy': 0, 'PowerType': 1, 'Knockback': 0, 'MaxInstances': 0, 'FX': 'minuteman_patriotswing', 'DamageType': 0, 'Stun': 2, 'AttackFlags': 0, 'SubType': 0, 'animation': 'melee_2', 'Radius': 0, 'RangeMin': 1, 'EPCost': 0, 'notForCustom': 0, 'Magnitude': 4, 'RangeMax': 5, 'Speed': 2, 'SpecialType': 0, 'PowerName': 'minute Patriot Smash'}
And, similarly, for testing at the console
>>> datfiles.ShowPower(smash) PowerName = minute Patriot Smash PowerType = PT_MELEE SubType = PT_ATTACK_SUBTYPE_NONE EPCost = none animation = melee_2 FX = minuteman_patriotswing Magnitude = high DamageType = PT_DAMAGE_CRUSH Speed = normal Stun = medium Knockback = none RangeMin = medium RangeMax = 5 Accuracy = very low Radius = none SpecialType = PT_SPECIAL_NONE MaxInstances = 0 AttackFlags = notForCustom = 0
Note that the keys of a python dictionary are not usually sorted.
(Of course, this is assuming we have unzipped objects.dat from data.ff into the Freedom Force\Data directory.)
Let's say that we have a character called the Adaptrix whose Adaptavision changes to help her defeat foes made of special materials. This could be a very complicated script, but for now just assume that she gets an "Adapt to Foe" hand in her right-click menu that calls the following function to change her power based on the enemy's material type:
import datfiles
def AdaptrixVisionChange(target,char): powername = 'adaptrix Adaptavision' # power name in powers.dat power = datfiles.Campaign_ReadPowers()[powername] mat = Object_GetAttr(target,'material') if ( mat == 4 ): # fire power['DamageType'] = PT_DAMAGE_COLD elif ( mat == 9 ) or ( mat == 7 ): # frozen or wood power['DamageType'] = PT_DAMAGE_FIRE elif ( mat == 1 ): # metal power['DamageType'] = PT_DAMAGE_ELECTRICAL else: power['DamageType'] = PT_DAMAGE_ENERGY datfiles.Campaign_ChangePowers('',[power]) loadPowers()
Now, when she chooses "adapt to foe", her power will switch between cold, fire, electrical, and energy damage, depending on her foe's material.
We could have powers that get harder too use as the mission goes on (increased EP cost), allow for a "crack shot" use at higher accuracy, or even change from beam to cone. But, care must be taken to ensure that the powers.dat file is reset if permanent changes to the powers database aren't intended.
Let's say we want to know whenever a power called 'shared power1' is rewritten using Campaign_RewritePowers() during the mission for whatever reason. Assume that we want a function called GenericCallback() to run when the power is rewritten and GenericCallback() looks like
def GenericCallback(event): print 'GenericCallback: event.event '+str(event.event) print 'GenericCallback: event.object '+str(event.object) print 'GenericCallback: event.string '+str(event.string) print 'GenericCallback: event.float '+str(event.float) print 'GenericCallback: event.data '+str(event.data) print 'GenericCallback: event.user '+str(event.user)
This is what we might see trying things out from the console:
>>> # read in the powers >>> from datfiles import * >>> powers = Campaign_ReadPowers() >>> mypower = powers['shared power1'] >>> # register the rewrite sink >>> regPowerRewrite('shared power1','GenericCallback',persistent=1,string='yada yada') >>> # make some change to the power >>> mypower['EPCost'] = 1 # that's "trace" neener, neener, neener! :-) >>> # now rewrite the change and see what shows up >>> Campaign_RewritePowers(ChangePowers=[mypower]) GenericCallback: event.event 65 GenericCallback: event.object shared power1 GenericCallback: event.string yada yada GenericCallback: event.float 0 GenericCallback: event.data 1 GenericCallback: event.user 0 >>> # that sink was persistent; let's cancel it >>> cancelRewriteCallbackSinks('GenericCallback') >>> # now test >>> Campaign_RewritePowers(ChangePowers=[mypower]) >>> # nothing, as expected. we also could have callled >>> cancelRewritePowerSinks('shared power1') >>> # or >>> cancelAllRewriteSinks() >>> # we can register a sink that triggers for any power rewrite >>> regPowerRewrite('','GenericCallback',persistent=1,string='blah blah') >>> # now test it with thee powers out of our powers dictionary >>> pkeys = powers.keys() >>> for p in pkeys[:3]: Campaign_RewritePowers('',[powers[p]]) GenericCallback: event.event 65 GenericCallback: event.object raptor Bite GenericCallback: event.string blah blah GenericCallback: event.float 0 GenericCallback: event.data 1 GenericCallback: event.user 0 GenericCallback: event.event 65 GenericCallback: event.object ffx_flames GenericCallback: event.string blah blah GenericCallback: event.float 0 GenericCallback: event.data 1 GenericCallback: event.user 0 GenericCallback: event.event 65 GenericCallback: event.object ffx_frostbite2 GenericCallback: event.string blah blah GenericCallback: event.float 0 GenericCallback: event.data 1 GenericCallback: event.user 0 >>> # we can have more than one callback at once, of course >>> def spiff(event): print 'spiff: called for power %s' % event.object >>> regPowerRewrite(pkeys[1],'spiff',persistent=1) >>> # now call the change function to do the same thing without the loop >>> Campaign_RewritePowers('',[powers[pkeys[0]],powers[pkeys[1]],powers[pkeys[2]],]) GenericCallback: event.event 65 GenericCallback: event.object raptor Bite GenericCallback: event.string blah blah GenericCallback: event.float 0 GenericCallback: event.data 1 GenericCallback: event.user 0 spiff: called for power ffx_flames GenericCallback: event.event 65 GenericCallback: event.object ffx_flames GenericCallback: event.string blah blah GenericCallback: event.float 0 GenericCallback: event.data 1 GenericCallback: event.user 0 GenericCallback: event.event 65 GenericCallback: event.object ffx_frostbite2 GenericCallback: event.string blah blah GenericCallback: event.float 0 GenericCallback: event.data 1 GenericCallback: event.user 0 >>> # finally, just to see that there is a way to check on all this mucking about >>> print Campaign_PowersRewritten() ['shared power1', 'shared power1', 'raptor Bite', 'ffx_flames', 'ffx_frostbite2', 'raptor Bite', 'ffx_flames', 'ffx_frostbite2'] >>> # and we could clear the list if we needed to >>> print Campaign_PowersRewritten(clearlist=1) []
First, for this function to work, there must actually be an powers.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_ReadPowers() tries to avoid re-reading the powers.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 the 150 KB main campaign powers.dat (the first time) in about 0.25 seconds. Subsequent calls are much faster (under a millisecond). If this function is to be called during a mission, it is a good idea to put a call to Campaign_ReadPowers() (the result doesn't need to be assigned to a variable for the caching to work) or, better yet Campaign_ReadCharacterData(), which reads the characters, objects, and powers DAT files all at once, 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.