06 January 2013

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

This is the fourth and final post of my tutorial Become a Software Engineer by modding Fallout 3. We are about to add the last two missing features to the Extra Perk: (a) rewarding the Player with an additional perk immediately after he gained more than half the experience points required to advance to the next level and (b) reducing the experience points gained by 50%. Currently the Extra Perk rewards the Player immediately when he levels up and does not reduce the experience points gained.

Like in my previous posts, I assume you know everything I explained before. If this is not the case, I would suggest you read those posts (Part 1, Part 2 and Part 3) before continuing with this one. Ready when you are!

What to expect behind the end?

How does feature (a) change the current behavior? At the moment, when the Player picks the Extra Perk, he won't get an additional perk for his current player level. But each time he levels up, he is rewarded with an additional perk, even for the maximum player level.

Implementing the new feature implies that we have to delay when to reward the Player with an additional perk. So when he picks the Extra Perk we have to reward him with an additional perk for the same level. But when he hit the maximum player level, he won't gain any more experience points. Hence we can't reward him with an additional perk for the maximum player level.

The moment when to reward the Player with an additional perk has shifted one half player level to the left. This is an important fact to consider when defining the criterion of when to terminate the execution of the QWEExtraPerkQuestScript (responsible to detect if the Player has gained enough experience points for an additional perk).

In the middle of nowhere

The formula for how to calculate the amount of experience points required, before gaining an additional perk is:

The function xp(playerLevel) returns the amount of experience points required to hit the player level playerLevel. You may wonder why we calculate the amount of required experience points in such a complicated way. A much simpler formula than the one above, which returns the same result in a perfect world, is:

(xp(playerLevel) + xp(playerLevel + 1)) / 2.

The reason why we choose the more complicated variant is to prevent a potential arithmetic overflow.

Most data types define a minimum and maximum value they are capable to represent. If we add some value greater than zero to the maximum value or subtract it from the minimum value, we get confronted with either an arithmetic overflow or arithmetic underflow exception or the calculation results in a value we did not expect. Hence, ensuring every calculation produces only values that are inside the boundaries defined by the data type of the result is very important.

The second formula adds two values and divides the result by 2. Adding two values may result in an arithmetic overflow if both values are positive and large enough or in an arithmetic underflow if both values are negative and small enough.

In contrast to the second formula, the first formula starts with a subtraction:

r1 = xp(playerLevel + 1) - xp(playerLevel)

whereas the first value to subtract from is a larger positive value than the second value to be subtracted. This calculation always results in a positive value smaller than the first value of the subtraction. Therefore the result r1 is inside the boundaries defined by the data type.

The first formula continues with a division:

r2 = r1 / 2 < r1

Dividing a positive value by a positive value greater than or equal to 1 results in either zero (due to truncation) or a positive value not larger than the value that got divided. This implies r2 as well is a value that is inside the boundaries, defined by the data type.

The last calculation performed by the first formula adds the result r2 to the second value of the first calculation:

r3 = xp(playerLevel) + r2 < xp(playerLevel) + r1 = xp(playerLevel + 1)

The value of r2 is smaller than twice its value, which is equal to r1, and if added instead results in the first value of the first calculation. And because this first value already is inside the boundaries defined by the data type, the value calculated by the first formula is always inside those boundaries as well.

The more complicated calculation has an advantage over the simpler calculation: it is fail-safe.

A shift to the left

We are almost ready to add the feature defined by (a) to the Extra Perk. But there is one last thing to decide first: when do we have to calculate the amount of experience points required by the Player, before he gets rewarded with an additional perk?

The first time we have to calculate that amount is when the Player picks the Extra Perk (which means in the initializer of the QWEExtraPerkQuest, the quest stage with an index of 0). And afterwards we have to calculate that amount every time when the Player was rewarded with an additional perk (in the function rewardXP defined by the QWEExtraPerkQuest, the quest stage with an index of 100). But we must not calculate the next amount when the Player got rewarded for the second last possible player level. This amount would be greater than the amount required to hit the last possible player level, which could cause an arithmetic overflow, right? So beware not to!

The formula for calculating the amount of experience points required to advance to a given player level is based on two game setting values: iXPBumpBase and iXPBase. Hence, when calculating the amount of experience points required we have to obtain those game settings using the G.E.C.K function getGameSetting <variable name>.

Let's add a quest stage with an index of 102 to the QWEExtraPerkQuest, acting as a function. I will refer to it by the name definePerkXP.

This function will calculate the amount of experience points required by the Player, before he gets rewarded with an additional perk. The result of the calculation will be stored in the variable perkXP, which we have to add to the QWEExtraPerkQuestScript. Make it look like the image to the left.

To address the changed criterion about when to terminate the QWEExtraPerkQuestScript, we have to move the conditional block responsible (demarcated by the lines 09: if and 11: elseif) to the top. This ensures that no calculation happens that is based on values not properly set. The conditional block can be interpreted as: if the Player has to be rewarded for the maximum player level or above, terminate the script.

Further the statement: 14: setStage QWEExtraPerkQuest 102 was added to the second conditional block (demarcated by the lines 11: elseif and 15: endif), which updates the value of the variable perkXP whenever the Player got rewarded with an additional perk. The function definePerkXP will not calculate a value for the variable perkXP if the player level is either lesser than one or if it is equal to or greater than the maximum player level.

Faster than light

Let's develop the quest stage function definePerkXP. It mainly relies on the function xp(plvl), which calculates the experience points required to reach the player level plvl. The function xp(plvl) is defined as:

xp(plvl) = (plvl - 1) * ((plvl - 2) * iXPBumpBase / 2 + iXPBase)

The new value we are about to calculate and assign to the variable perkXP (as described above) depends on two different player levels: the player level plvl for which to reward the Player with an additional perk and the next player level plvl + 1 the Player may reach.

Because of the fact, the G.E.C.K. does not allow defining the formula as a real function and because we do not want to introduce too many variables acting only as a sharing mechanism for temporary values across different scripts, we embed the formula into the function definePerkXP.

The new value to calculate is:

perkXP = xp(plvl) + (xp(plvl + 1) - xp(plvl)) / 2

The value of each function call is:

xp(plvl) = (plvl - 1) * ((plvl - 2) * iXPBumpBase / 2 + iXPBase)
xp(plvl + 1) = plvl * ((plvl - 1) * iXPBumpBase / 2 + iXPBase)

Performing this computation straight forward requires 10 additions or subtractions, 10 multiplications or divisions and 6 function calls. To avoid unnecessary computations we will optimize the calculation. The expression iXPBumpBase / 2 occur in both function calls. Hence we will compute the expression only once and use the result instead:

bump = 0.5 * getGameSetting iXPBumpBase
xp(plvl) = (plvl - 1) * ((plvl - 2) * bump + iXPBase)
xp(plvl + 1) = plvl * ((plvl - 2) * bump + iXPBase + bump)

The next expression that occurs in both function calls is (plvl - 2) * bump + iXPBase. Computing this expression as well only once will further optimize the calculation:

base = (plvl - 2) * bump + getGameSetting iXPBase
xp(plvl) = (plvl - 1) * base
xp(plvl + 1) = (plvl - 1) * base + base + plvl * bump

The last optimization left is to calculate the expression (plvl - 1) * base only once:

xp = (plvl - 1) * base
xp(plvl) = xp
xp(plvl + 1) = xp + base + plvl * bump

Replacing the function calls of the formula with the optimized calculations results in the following final formula:

perkXP = xp(plvl) + (xp(plvl + 1) - xp(plvl)) / 2
perkXP = xp + (xp + base + plvl * bump - xp) / 2
perkXP = xp + (base + plvl * bump) / 2

We reduced the number of required additions or subtractions from 10 to 5, the number of required multiplications or divisions from 10 to 5 and the number of required function calls from 6 to 2.

Just do it

With these optimizations in place, we are ready to implement the quest stage function definePerkXP. Open the G.E.C.K., select the quest stage 102 and make the associated script look like the image below.

The conditional block demarcated by the lines 07: if and 09: endif prevent the script from computing a value if the player level is outside of the boundaries defined by the function xp(plvl). The Player starts with level 1 and can't pick the Extra Perk before he reaches player level 2. Apart from that the Player can't be rewarded any further with additional perks after he got rewarded for the second last player level. Lines 10: - 13: compute the new value for the variable perkXP.

What's left is to initialize the newly introduced variable perkXP when the Player picks the Extra Perk. Modify the initializer (the quest stage with an index of 0) and make it look like the image below.

Save all the changes in the G.E.C.K. and start Fallout 3. Perform the following actions to verify that things work as intended:

  1. Load a save-game where the Player does not already possess the Extra Perk.
  2. Level the Player up by issuing the command advLevel using the in-game console.
  3. Pick the Extra Perk.
  4. Reward the Player with an amount of experience points, close to but lesser than the amount needed to hit the next player level, by issuing the command rewardXP <amount> using the in-game console.

The Player should be rewarded with an additional perk although he did not hit the next player level.

We should verify once more that the QWEExtraPerkQuest is terminated immediately after the Player has been rewarded with an additional perk for the second last player level:

  1. Load a save-game where the Player does not already possess the Extra Perk.
  2. Set the level of the Player to the third last possible player level (which is either 18 without Broken Steel or 28 with Broken Steel installed) by issuing the command player.setLevel <level> using the in-game console.
  3. Execute the actions 2 - 4 defined above once again.
  4. Issue the command showQuestVars QWEExtraPerkQuest using the in-game console.

You should observe that the QWEExtraPerkQuest is not running anymore.

Keep the World in balance

The final missing feature is the reduction of experience points by 50%. It is an attempt to balance the benefit of one additional perk per level by doubling the time required to hit the next player level.

This can be achieved by intercepting the game engine mechanism which rewards the Player with experience points. A perk is capable of intercepting this mechanism besides many others. The interception is defined in terms of a Perk Entry which in our case reduces the experience points to reward by multiplying it with 0.5.

Start the G.E.C.K. one last time and select the QWEExtraPerk. Add a new Perk Entry defining an Entry Point which adjusts the experience points rewarded by multiplying it with 0.5. Make it look like the image below and save all changes.

Start Fallout 3 and perform the following actions to verify that the experience points gained are half the experience points rewarded:

  1. Load a save-game where the Player does not already possess the Extra Perk.
  2. Level the Player up by issuing the command advLevel using the in-game console.
  3. Pick the Extra Perk.
  4. Open the pipboy and remember the amount of experience points the Player currently has.
  5. Reward the Player with experience points by issuing the command rewardXP 10 using the in-game console.

Open the pipboy once again and verify that the amount of experience points the Player has is by 5 greater than the amount of experience points the Player had in step 4.

That's it. Congratulations! You implemented every feature of the Extra Perk.

Conclusion

Finally we implemented all the requirements as stated in Part 1 of this tutorial. When computing new values we took care not to cause arithmetic underflows or overflows by respecting the boundaries as defined by the data type the computation was based on. Further we protected our scripts from computing new values when certain pre-conditions were not satisfied. To avoid wasting valuable resources, we optimized the computation of values. We used the interception mechanism provided by perks to modify the experience points before they were rewarded to the Player.

I hope you enjoyed this final post and all my previous ones. Feel free to contact me either by e-mail or leave a comment on the Fallout 3 Nexus Site. If you made it this far and liked my tutorial, please consider to endorse it.

The End

Logged Off: Harmlezz