02 December 2012

Become a Software Engineer by modding Fallout 3 - Tutorial Part 3

Welcome (back) to the third part of my tutorial Become a Software Engineer by modding Fallout 3. In this post I will explain how to add the missing functionality, of rewarding the Player with an additional perk whenever he levels up, to the Extra Perk Quest Script. Adding that behavior will cause some unintended and unwanted side-effects, we have to address later on. There will be many possible solutions to choose from, of how to compensate those side-effects. A good solution has to consider behavior, potentially introduced by other mods, which we can only assume. Finding the proper trade-off between correctness and efficiency will be the challenge in selecting a good solution. Scripting, awareness, reasoning and algorithms are the major topics of this post.

I expect you already know what I explained in Part 1 and Part 2 of this tutorial. If you intend to implement the Extra Perk by yourself, you have to catch up to the point where Part 2 of the tutorial ends.

Detecting if the Player leveled up

The way we had to implement tracking, as defined by the Extra Perk Quest Script, is called polling. Instead of getting notified when an event of interest happened, we have to check actively and repeatedly if an event occurred.

To detect if the Player leveled up, we have to remember the level he had before we ran the check. Remembering something means adding a variable to the Extra Perk Quest Script. The statement at line 1: int defines an integer variable playerLevel, which we use to store the last player level, already handled by the script. Whenever the level of the Player is greater than the level stored in playerLevel, he leveled up between the current and the previous check. The statements defined by the conditional block, demarcated by lines 4: if and 6: endif, will be executed only, if the condition player.getLevel > playerLevel is true. Besides doing something useful, we have to avoid rewarding the Player twice. Therefore we will reflect the detection and handling of the Player by increasing the value of playerLevel by 1 (as defined by line 5: set).

Start the G.E.C.K. and add the new lines to the QWEExtraPerkQuestScript. Make the script look like the screenshot to the right.

After adding the new lines, read the whole script once again. Is the condition defined by the second conditional block still proper in terms of our requirements? In my opinion it is not, after the introduction of the variable playerLevel. It should be rephrased as: stop the Extra Perk Quest Script when the Player has been rewarded with an additional perk for the maximum player level instead of: stop the Extra Perk Quest Script when the Player has leveled up to the maximum player level. It may seem to be a tiny difference, but some subtle bugs (which are the most difficult to find) stem from this kind of differences. Hence we will change the condition of the second conditional block from player.getLevel >= maxPlayerLevel to playerLevel >= maxPlayerLevel. I suggest you always read the whole script for which you introduced a new variable, as well as all scripts referencing any variable defined by that script, once again. The likeliness of find potential issues is high when inspecting scripts immediately after altering them.

Change the condition of the second conditional block in the G.E.C.K. as described above. What's left is to initialize the variable playerLevel with the current level of the Player, at the moment he picks the Extra Perk.

Select the stage with an index of 0 from the QWEExtraPerkQuest and add the statement set playerLevel to player.getLevel to its embedded script (the function acting as the quest initializer). Don't forget to save all changes made to the Extra Perk.esp.

Let's test the new behavior by starting Fallout 3 and loading a save-game where you have not already obtained the Extra Perk. I guess that Fallout 3 treats a save-game like a mod, placed last in the loading order. Hence objects contained in the save-game will override previously loaded objects. A save-game containing an old version of the QWEExtraPerkQuest would be associated with an old version of the Extra Perk Quest Script, not defining the newly introduced variable playerLevel. It's only a guess, but the effect is as described.

Open the in-game console and submit the command: advancePCLevel. The Level-Up-Dialogue should be displayed. Distribute the skill points and pick the Extra Perk. Open the in-game console again and submit the command: showQuestVars QWEExtraPerkQuest. The variable playerLevel should be displayed, having a value equal to the current Player's level, which means the quest initializer works as intended. Now set the Player's level to the same value assigned to the variable maxPlayerLevel by submitting the command: player.setLevel <level>. You should be able to observe that the variable playerLevel gets incremented by 1 every 10 seconds (the delay between two runs of the QWEExtraPerkQuestScript as defined by the QWEExtraPerkQuest). As soon as the value of the variable playerLevel is equal to the value of the variable maxPlayerLevel the QWEExtraPerkQuest will be stopped. This behavior would not be observable if we didn't changed the condition of the last conditional block from player.getLevel >= maxPlayerLevel to playerLevel >= maxPlayerLevel (you may try it).

Rewarding the Player with an additional perk

Finally everything is in place. We are ready to add the code necessary to reward the Player with an additional perk. But neither the G.E.C.K. nor FOSE provide a function to display the Perk-Selection-Dialogue. Hence we will level the Player up to force the Perk-Selection-Dialogue to be displayed as a side-effect. Afterwards we have to restore the Player's level to the level he had before.

How to level the Player up we already know (we submitted the command advancePCLevel many times using the in-game console). And how to set the Player's level to a specific value we know as well (we also submitted the command player.setLevel before). But there is one thing we have to consider: what if the Player leveled up to the maximum player level? Is it possible and safe to level him up once more? Decreasing the Player's current level first and leveling him up afterwards seems to be the better solution. Detecting that the Player leveled up implies he had a lower level before. Hence decreasing the current Player's level is always safe.

It is very important to be aware of the boundaries a variable defines for its value to be valid. The type of a variable defines the widest boundaries possible for a variable, but most often not all values inside those boundaries are valid. In our case only player levels between 1 and maxPlayerLevel inclusively are valid values for the variable playerLevel. Whenever we introduce a statement which assigns a value to a variable, we should ask ourselves:

  • Are we aware of the boundaries defined by the variable?
  • Does the new value respect the boundaries defined by the variable?
  • Where does the new value come from? Who calculated the new value?
  • Is it possible to verify that the calculated value is inside the boundaries?

Questioning the code you just created does reduce your productivity. But of which value is code, you are not confident about its correctness? My preferred way of adding new code is using the Test First approach. But this is not possible when modding Fallout 3 (besides porting the whole functionality to a language supporting Test First). So if you observe that debugging does follow your coding often, slow down your coding. Take your time and think more deeply about what you want to achieve before putting your fingers onto the keyboard again. Your productivity should increase.

Start the G.E.C.K., load the Extra Perk and alter the first conditional block as shown above. Don't forget to define the variable previousLevel as well. Save your changes and start Fallout 3. Load a proper save-game. Follow my instructions carefully to ensure, you will observe what I intend you to observe:

  1. Open the Pipboy and find the information on top of the screen, displaying the Player's current experience points (620 in this example) together with the experience points the Player requires to level up (1050 in this example). Calculate the difference, the amount of experience points required to level up (430 in this example), and write down the experience points required to level up (1050 in this example). Leave the Pipboy.
  2. Enter the in-game console and submit the command: rewardXP <difference>. Leave the in-game console.
  3. The Player should level up. Distribute the skill points and pick the Extra Perk.
  4. Open the Pipboy again and inspect the current experience points of the Player. They should be equal to the experience points he required to level up (as written down in step 1). Calculate the difference and remember the experience points required to level up once again. Leave the Pipboy.
  5. Enter the in-game console and level the Player up as described in step 2, using the newly calculated difference. Leave the in-game console.

The Level-Up-Dialogue should be displayed. After distributing the skill points and picking another perk, wait at most 10 seconds. The Level-Up-Dialogue should show up. If that happens, the Extra Perk does something we expected it to do. But after distributing the skill points (wait! we didn't intend to reward the Player with more skill points, did we?) and picking the additional perk, open the Pipboy and inspect the current experience points the Player has. They should be different from what you have written down.

Our current solution of how to reward the Player with an additional perk causes two side-effects, we are not interested in:

  • The Player's current experience points are modified.
  • The Player is rewarded with additional skill points.

Compensating missing experience points

On solution to adjust the modified experience points would be to reset its value to the value it had before the Extra Perk rewarded the Player with an additional perk. The G.E.C.K. provides a function setActorValue taking two input parameters: the state to be modified and the value to set the state to. For example, this function works well for all S.P.E.C.I.A.L stats, but sadly it does not for the XP state.

Another possible solution would be to add the difference between the experience points the Player had before the modification occurred and the experience points he currently has to his current experience points, using the function rewardXP. This is an option, because the modification always results in a reduction of the Player's experience points.

To reward the Player with an additional perk while preserving his experience points, we have to do something like this:

  1. Remember the Player's experience points
  2. Reward the Player with an additional perk
  3. Calculate the missing experience points
  4. Reward the player with the calculated difference

The requirement defined by step 2 is already implemented in terms of three statements, as defined by lines 06: - 08: of the QWEExtraPerkQuestScript. The complete algorithm would consist of six statements, if we were able to formulate the requirements defined by each of the remaining three steps by a single statement. All this statements together implement the high-level functionality of rewarding the Player with an additional perk.

The four steps formulated above are at different levels of abstraction. Step 1 is a technical prerequisite, required to calculate the difference between the experience points done in step 3. Step 2 is at the same level of abstraction as the level we formulated the requirements of the Extra Perk. And step 4 is at a lower level of abstraction than step 2, but at the same level as step 1 and 3.

If we would put all those statements into the conditional block, demarcated by the lines 05: if and 10: endif, it would be hard to recognize their higher level semantic. Moreover we would mix up different level of abstraction the QWEExtraPerkQuestScript would be composed of. Keeping all statements of a script at the same level of abstraction makes it more readable and hence more maintainable.

The complexity of a piece of software increases with the amount of responsibilities it has. Higher complexity always bears the risk of more hidden bugs. Thus we should strive to reduce the number of responsibilities for one piece of software whenever possible. But there is no simple rule when more than one responsibility is appropriate; it's a matter of good design.

Motivated by the reasoning and advices given, we will encapsulate the whole functionality, as defined by steps 1 - 4, by a separate script, associated with a quest stage (misusing the concept to implement something similar to a function).

Start the G.E.C.K. and load the Extra Perk.esp. Find the QWEExtraPerkQuest and open the edit dialogue. Select the Quest Stages tab and create a new Quest Stage with an index of 100. Add a new Log Entry and edit its associated script (as we did for the Quest Stage acting as the quest initializer) by adding all the statements shown in the script below. Let's have a closer look at what the script does:

Line 05: set stores the experience points the Player has before we reward him with an additional perk, in the local variable targetXP. The statements of lines 06: - 08: are equal to the statements of the same lines defined by the QWEExtraPerkQuestScript. Together they reward the Player with more skill points and an additional perk, while reducing his experience points. Line 09: set calculates the missing experience points and stores them into the local variable diffXP. Line 10: rewards the Player with the amount of experience points he lost, as stored in the variable diffXP.

If the G.E.C.K. would support naming a quest stage, we would name the stage with index 100 something like rewardPerk. To keep the tutorial more expressive, I will refer to the quest stage with index 100 using that name.

What's left is to call the quest stage function rewardPerk every time when we detect that the Player leveled up. Find the QWEExtraPerkQuestScript and make it look like the image to the left.

Now all statements of the script are at the same level of abstraction. The first conditional block (demarcated by the lines 07: - 10:) handles when and how often to reward the Player with an additional perk. The second conditional block (demarcated by the lines 11: - 13:) decide when to stop tracking the Player's level. All those statements are at a very high level of abstraction, just one level below the level we formulated the requirements.

If we wouldn't cache the maximum player level (which I did only to explain the use of an initializer quest stage) by in-lining the function call instead, the QWEExtraPerkQuestScript would define only one variable, the playerLevel, tracking the last player level we do not have to reward the Player with an additional perk anymore. Hence the QWEExtraPerkQuestScript has a single responsibility (besides terminating the execution of itself).

Start Fallout 3 and test if the experience points as well as the player level are unchanged after the Extra Perk rewarded the Player with an additional perk. After you verified that everything works fine, go and level up the Player once again. Surprise! The Extra Perk won't reward the Player with additional perks any further (even so the Extra Perk Quest is still running). The reason why this happens is because by default a quest does not allow a quest stage to be set repeatedly. To change that, we have to enable the option Allow Repeated Stages for the QWEExtraPerkQuest (available at the Quest Data tab of the quest dialogue), which implies that any script associated with a quest stage is run every time the quest is set to that stage.

I don't know why we have to decided, if a quest stage can be set repeatedly for a whole quest instead for every quest stage. In our case we don't want to allow setting the quest stage with index 0 multiple times, but we have to. Open the G.E.C.K. and enable the option of the QWEExtraPerkQuest. Start Fallout 3 again and verify the Extra Perk does reward the Player once for every level the Player gains after he picked the perk.

Expecting the unexpected

Our current solution of how to compensate the missing experience points (caused by leveling the Player first down and afterwards up) relies on the expectation, that the Player gains the exact amount of experience points we reward him with. The vanilla Fallout 3 has a perk Swift Learner, which gives the Player 10% more experience points per rank than he is rewarded with. This means, if the Player has both perks (the Extra Perk and Swift Learner) our current solution of compensating the missing experience points would give the Player too many experience points.

To verify my prediction, start Fallout 3 and pick the Extra Perk by leveling the Player up. Calculate the amount of experience points required to level the Player up once more. Remember the total amount of experience points required before rewarding the Player with the calculated difference. When leveling up pick the Swift Learner perk and wait for the Extra Perk to reward the Player with an additional perk. Pick the Swift Learner perk again (now the Player gains 20% more experience points). Afterwards check the Player's experience points, which should be larger than the experience points you remembered before rewarding him the last time.

We could fix our solution by taking into account, if the Player has obtained the Swift Learner perk and which rank. But what if some other mod would change the rewarded experience points as well? Whenever we detect a defect in one of our algorithms, we shouldn't fix it by simply addressing the special case that revealed the defect, but by addressing the root, causing the defect. In terms of our Extra Perk this means, we have to expect that the amount of experience points rewarded to the Player may be different from the amount of experience points he gained.

Compensating the missing experience points by guessing iteratively

Our second solution has to deal with an unknown function that may modify the amount of experience points before rewarding the Player. One thing we know about the function, is that it grows at most linear. This means, the resulting experience points calculated are always less than or equal to the product of a fixed multiplier m and the initial experience points. Or more specific: F(xp) = m * xp + c.

This knowledge can be derived from the functions provided by the G.E.C.K. to modify experience points by perks. Select the object category Perk and filter the list of objects by the prefix Swift. Open the Edit-Dialogue for the Swift Learner perk and double-click any of its Perk Entries. Inspect all available modifications by browsing through the drop-down menu labeled Function.

The image blow shows the new script for compensating the missing experience points after the Extra Perk leveled the Player first down and afterwards up again.

Before I start to explain the whole script in detail, I will outline its major structure. Lines 10: label 1 and 28: goto 1 demarcate a loop. The condition, if to continue executing statements inside the loop, is defined by line 11: if diffXP > 0. If the body of the loop is not entered, the execution of script will be terminated. The loop condition states: as long as the difference between the experience points the Player had before the Extra Perk did his job and the current amount of experience points the Player has is greater than 0, execute statements defined by the body of the loop.

Analyzing the script in detail

Lines 01: - 08: define and initialize local variables used by the script. The variable targetXP is not required but improves the readability of the script. It could be replaced by the alternative expression QWEExtraPerkQuest.targetXP which in my opinion is much too long. Its value is the amount of experience points the Player had before the Extra Perk reduced them.

The variable diffXP defines the missing difference of experience points between the target experience points and the experience points the Player has at the moment of the calculation. When this value is zero or less, the script has done its job. The variable xp defines the amount of experience points to reward the Player with. Its value reflects the best guess the script made based on information collected in the previous iteration. It gets initialized with 1, the smallest meaningful amount of experience points. In contrast the variable gainedXP holds the amount of experience points the Player gained (which could be different from the amount of experience points the Player was rewarded with). And the last variable gainRatio is the ratio between the experience points the Player gained and the experience points the Player was rewarded with. If the value of gainRatio is 1, the Player gained the same amount he was rewarded with. If it is for example 2, the Player gained double the amount he was rewarded with. It is a hint the script uses to predict the amount of experience points the Player will gain when rewarded the next time with an amount of experience points.

Lines 12: - 14: make sure the amount of experience points to reward the Player with is at least 1, the smallest meaningful amount of experience points to reward the Player with.

Line 15: rewards the Player with an amount of experience points while line 16: calculates the gained amount of experience points (which is the difference between the missing amount before the Player was rewarded and the missing amount afterwards).

Now the script tries to predict the amount of experience points to reward the Player with next, while avoiding to exceed the amount of target experience points. To achieve this, the script relays on the fact, that the function F(xp) does grow at most linear. The returned value of each such function is an integer value, while the calculated result most likely is a floating point value. Before the calculated result is returned, the floating point value is rounded up. We should consider two important cases for the function F(xp) = m * xp + c:

  • The values of the variable xp and of the multiplier m are small while the value of the constant c is large.
  • The values of the variable xp and of the multiplier m are large while the value of the constant c is small.

In the first case the returned value is dominated by the constant c which is much larger than m (the multiplier the script tries to find out). With increasing values of xp the dominance of c dims away. In the second case the returned value is dominated by the multiplier m regardless of the value of xp. But most importantly, in both cases the ratio between the experience points gained (the value returned by F(xp)) and the experience points rewarded (the value of the variable xp) is always greater than the multiplier m the script is in search for (ignoring negative values for constant c). And for increasing values of xp the ratio becomes either smaller or stays the same.

Lines 17: - 25: handle the cases when the experience points rewarded last lead to a positive gain of experience points. In that case the script calculates the changed amount of missing experience points (line 18:). If that amount is larger than or equal to the amount of experience points gained last (line 19:), than the value for the variable xp is set to the remaining amount of missing experience points diffXP divided by the gainRatio. Due to the fact, that the gain ratio will stay the same or becomes smaller, when the amount of experience points rewarded is increased, we will not exceed the target experience points.

Otherwise, if the missing amount of experience points is lesser than the last gained amount of experience points, we have to decrease the amount of experience points to reward the Player with. This could result in a larger gain ratio value, which we do not know. We could have stored the largest value for the gain ratio in another local variable, at the first time we rewarded the Player with 1 experience point, but in my opinion the increase of complexity and therefore reduction of maintainability isn't worth the gain of a slightly faster algorithm. Hence the script starts over to reward the Player with 1 experience point (line 23:) as it did when the statements of the loop body were executed the first time (lines 12: - 14:).

Lines 25: - 27: handle the case, when rewarding the Player with a positive amount of experience points results in a gain of zero experience points. This may happen only, if either m is zero or c is negative. If both is true, rewarding the Player with any amount of experience points always resulting in a negative gain (0 * xp is always zero and a negative c makes the result negative). If m is at least a positive number (even if less than 1) and c is a negative number, the result could become positive if the value of xp is large enough. To be more specific: if xp > abs(c) / m , where abs() is a function returning the absolute value of a given input value.

The risk taken, when doubling the value of xp in each iteration step, is a potential raise of the amount of experience points gained from zero to abs(c). This results from the following equation: F(xp) = m * xp + c <= 0 which is equivalent to m * xp <= abs(c). The worst case in terms of the largest possible amount of experience points gained by the Player, which exceeds the missing amount of experience points to compensate, is when both sides of the equation are equal and the value of xp is 1. Increasing xp to the next larger value 2 immediately cause F(xp) to return a value of abs(c).

On the other hand, if we do not double the value in each iteration step but increase it by 1 instead, the number of required iterations could become very large (consider m to be a very small value less than 1). We have to choose between two evils:

  • Try to make the smallest possible error by risking a very large number of iterations needed to compensate the missing experience points.
  • Hope the function F(xp) rarely returns non positive values, and in case it does, the value of m as well as the value of c is a small numbers.

In my opinion, the second case is much more likely to be introduced by other mods and perks. Most of the mods I know tend to increase the amount of experience points gained or reduce them slightly by some degree, but never eliminate them at all (setting m to zero). And I have never heard about a mod, removing a constant large amount of experience points (setting c to a large negative value). Hence I doubled the value of xp in each iteration step instead of increasing it by 1.

Wiring things together

What's left is to associate the new script with the stage 101 of the QWEExtraPerkQuest, which we have to create first. From now on I will refer to the script, associated with stage 101, using the name repairXP. In addition we have to change the script rewardPerk to look like the image to the right. The variable targetXP, which is set in the script rewardPerk and used in the script repairXP, has to be made accessible to both of those scripts. The G.E.C.K. does not support any mechanism for passing values around other than using some kind of global variables. Hence the QWEExtraPerkQuestScript has to define the variable targetXP. The local variable diffXP has been moved to the script repairXP, which took over the responsibility of compensating the missing amount of experience points. Make the script look like the image below.

After saving all changes in the G.E.C.K., start Fallout 3 and repeat the procedure described at Expecting the unexpected. This time the effect should not be observable.

You may have noticed, that sometimes the Player has one more experience point than he had, before the Extra Perk rewarded him with an additional perk. The reason for it is, that Fallout 3 does round up the experience points calculated. For example, if the Player misses 1 experience point, the script repairXP will reward him with 1 experience point, which gets multiplied by the Swift Learner perk by either 1.1, 1.2 or 1.3. The result is a float value greater than 1 but lesser then 2, rounded up to 2. This effect can't be eliminated easily. Later in the game, gaining a few more experience points is not very significant. Hence we avoid addressing this effect, which would result in a much more complex script. Instead we ignore the effect.

Eliminating the reward of additional skill points

The second side-effect caused by leveling the Player up, is the gain of additional skill points each time the Extra Perk rewards the Player. To avoid this behavior we create the QWENoSkillPerk, which sets the skill points gained to zero. This perk we will add to the Player before the function call advancePCLevel (made in the script rewardXP), and remove it immediately afterwards.

Open the G.E.C.K. and create a new perk with an ID QWENoSkillPerk. Make sure the perk is not playable, which means it is neither visible nor can be picked in the Perk-Selection-Menu. Even so we remove the perk immediately after leveling the Player up, make the perk hidden to document that it is intended to be used as a function only.

Add a new Perk Entry with a Rank of 1 to the QWENoSkillPerk, adjusting the gained skill points to the fixed value 0, which effectively changes the Level-Up-Dialogue to a Pick-Perk-Dialogue.

What's left is to add the QWENoSkillPerk to the Player before leveling him up and to remove it immediately afterwards.

Select the QWEExtraPerkQuest and navigate to the Quest Stages tab. Open the script rewardXP and add the lines 06: and 08: to make the script look like the image below.

Save all the changes and start Fallout 3 to verify, that whenever the Extra Perk rewards the Player with an additional perk, no skill points are gained. But when the Player levels up, he is gaining skill points and has to pick one more perk.

Finally we implemented the reduced requirements as stated in Part 2 of this tutorial. We are done for now. There are more things left to be implemented in the future, in Part 4. Have a break and come back soon! I have already started to work on Part 4.

Conclusion

We added the behavior of rewarding the Player with an additional perk whenever he levels up. Our solution caused some side-effects, we had to eliminate. Because of the absence of information, we had to implement an algorithm that iteratively tried to guess how to compensate experience points, which got removed in the process of rewarding the Player. We used a perk to suppress the gain of additional skill points by dynamically adding and removing it to and from the Player.

What's left is to reduce the experience points gained when the Player has the Extra Perk. The additional perk has to be rewarded when the Player has collected at least half the experience points needed to reach the next level, instead of rewarding him immediately after he leveled up. All this is covered by the final Part 4 of my tutorial.

I hope you enjoyed this post so far. Feel free to contact me either by e-mail or leave a comment at the Fallout 3 Nexus Site. If you think this tutorial is worth to be recommended, please consider to endorse it.

Logged Off: Harmlezz