Tutorial 5: Basketweaving – Appendix

This page details some advanced concepts and answers some questions you may have after having followed the Basketweaving tutorial.


Skill Level: Advanced

This is an advanced tutorial and is targeted toward experienced modders.

Important Note

There is a lot of information in this tutorial. Please take your time working through it.

Appendix A: Progression System Code

As mentioned in Part 1, the Campfire Skill System is not the progression system itself; that’s an exercise left up to the reader.

This is a pattern I used for the Camping skill in Campfire. Hopefully this will give you an idea of what you might need to do in your own mod. Fundamentally, you need to:

  • Figure out how many actions / events / etc required to get to the next level of your skill. I like to increase the number of actions required for each new point in the skill, which is consistent with other systems in the game. Give the player the first point after only a few actions, and then each additional one requires a little more time / actions / events.
  • Update the Skill Progress global so that the player knows how close they are to the next point when they open up the skill.
  • If the progress is greater than or equal to 100%, give the player a skill point. Display a message to let them know they gained a point.
  • If the progress wasn’t enough to gain a point, let the player know that they advanced the skill instead.  Do this only if the action the player would be taking is seldom; we don’t want to spam the player with advancement messages. If the action happens often, maybe display the advancement message every 25% of progress.

Here is an example of this. For Basketweaving, we said that the player should advance the skill when they read special Basketweaving skill books. We can assume that there’s a script attached to the skill books that does this, so, that’s probably a good place to put this code.

Feel free to comment out the debug.trace() calls if you don’t want them in your user’s logs. May be helpful during early testing, but probably not after that.

Info
This code is available in the example plug-in download.
scriptname BasketweavingSkillBook extends ObjectReference

GlobalVariable property BasketweavingPerkPoints auto
GlobalVariable property BasketweavingPerkPointsEarned auto
GlobalVariable property BasketweavingPerkPointProgress auto
GlobalVariable property BasketweavingPerkPointsTotal auto

Message property Basketweaving_PerkEarned auto
Message property Basketweaving_PerkAdvancement auto

Event OnRead()
    AdvanceBasketweaving()
EndEvent

function AdvanceBasketweaving()
    if BasketweavingPerkPointsEarned.GetValueInt() < BasketweavingPerkPointsTotal.GetValueInt()
        ;Figure out the number of books needed for the next point.
        debug.trace("Advancing Basketweaving skill.")
        int next_level = BasketweavingPerkPointsEarned.GetValueInt() + 1

        ; 3, 5, 7, 9, 10, 11, 12, 13...
        float books_required
        if next_level <= 4
            books_required = (2 * next_level) + 1
        else
            books_required = (9 + (next_level - 4))
        endif

        ; What is the current progress toward the next point?
        float progress_value = (1.0 / books_required)
        BasketweavingPerkPointProgress.SetValue(BasketweavingPerkPointProgress.GetValue() + progress_value)
        debug.trace("Basketweaving perk progress value: " + BasketweavingPerkPointProgress.GetValue())

        ; If we have >= 100% progress, give the player a point.
        if (BasketweavingPerkPointProgress.GetValue() + 0.01) >= 1.0
            debug.trace("Granting a Basketweaving perk point.")
            Basketweaving_PerkEarned.Show()
            BasketweavingPerkPointsEarned.SetValueInt(BasketweavingPerkPointsEarned.GetValueInt() + 1)
            BasketweavingPerkPoints.SetValueInt(BasketweavingPerkPoints.GetValueInt() + 1)

            ; If we've maxed out the skill, leave the progress at 100% so the player knows
            ; that they can't get any more advancement in this skill.
            if BasketweavingPerkPointsEarned.GetValueInt() >= BasketweavingPerkPointsTotal.GetValueInt()
                BasketweavingPerkPointProgress.SetValue(1.0)
            else
                BasketweavingPerkPointProgress.SetValue(0.0)
            endif
        else
            ; We didn't have enough progress for a point; show the advancement message instead.
            Basketweaving_PerkAdvancement.Show()
        endif
    endif
endFunction

Appendix B: Manual Skill Tree Registration

Registering your skill tree manually may be a better option than the automatic player alias method if you need greater control over the availability of your skill tree, such as making sure that your mod is “running” before you display it.

Here’s an example of that.

Important: If you follow this method, try not to register the tree immediately when the game starts (like in an OnInit or PlayerLoadGame event). You must allow time for Campfire to initialize on start-up. You can register for the Campfire_Loaded mod event in order to detect when Campfire is finished starting up when the game loads.

scriptname MyModStartUpScript extends Whatever

Activator property SuperCoolNodeController auto

function StartMyAwesomeMod()
    ; Do a bunch of stuff to start the mod
    ; And then...

    ; For safety, make sure to check the API version is at least 4.
    GlobalVariable CampfireAPIVersion = Game.GetFormFromFile(0x03F1BE, "Campfire.esm") as GlobalVariable
    if CampfireAPIVersion.GetValueInt() >= 4
        CampUtil.RegisterPerkTree(SuperCoolNodeController, "my_cool_mod.esp")
    endif
endFunction

function StopMyAwesomeMod()
    ; Do a bunch of stuff to stop the mod
    ; And then...


    ; This controls run-time availability, in case user stops the mod during play.
    ; Campfire will automatically unregister the tree if your mod
    ; is uninstalled or removed.
    GlobalVariable CampfireAPIVersion = Game.GetFormFromFile(0x03F1BE, "Campfire.esm") as GlobalVariable
    if CampfireAPIVersion.GetValueInt() >= 4
        CampUtil.UnregisterPerkTree(SuperCoolNodeController, "my_cool_mod.esp")
    endif
endFunction

Appendix C: SkyUI MCM Skill Respec and Refund

Your user may want easy access to control certain parameters of your skill. They might want to reallocate their earned points. Or, they might have had to perform a clean save of your mod, which would wipe out their skill progress, and they want those points back. Last but not least, they might want to cheat and get all of the points immediately!

The following is a standard set of SkyUI MCM code I like to use in my mods. This will allow your users to respec or “restore” (or cheat) their points.

This is a fully-functional SkyUI MCM script; adapt it into your existing MCM script as needed.

Info
This code is available in the example plug-in download.
Scriptname Basketweaving_SkyUIMCMScript extends SKI_ConfigBase

; Be sure to make translator's lives easier and convert
; all string literals below to translation file variables
; and include a translation file with your mod.

int BasketweavingSkillRespec_OID
int BasketweavingSkillRestore_OID
int BasketweavingSkillRestoreSlider_OID

Globalvariable property BasketweavingPerkPoints auto
Globalvariable property BasketweavingPerkPointsEarned auto
Globalvariable property BasketweavingPerkPointProgress auto
Globalvariable property BasketweavingPerkPointsTotal auto

Globalvariable Property Basket_PerkRank_BasketBeginner auto
Globalvariable Property Basket_PerkRank_RedBasketExpert auto
Globalvariable Property Basket_PerkRank_HappyBasketCrafter auto
Globalvariable Property Basket_PerkRank_TravelBasket auto
Globalvariable Property Basket_PerkRank_BasketMaster auto

Event OnConfigInit()
    Pages = new string[1]
    Pages[0] = "Basketweaving"
endEvent

Event OnPageReset(string page)
    if page == "Basketweaving"
        PageReset_Basketweaving()
    endif
endEvent

function PageReset_Basketweaving()
    SetCursorFillMode(TOP_TO_BOTTOM)
    
    AddHeaderOption("Basketweaving Skill")
    BasketweavingSkillRespec_OID = AddTextOption("Respec Skill Points", "")
    BasketweavingSkillRestore_OID = AddToggleOption("Restore Skill Progress", false)
    BasketweavingSkillRestoreSlider_OID = AddSliderOption("Points to Restore", 0, "{0}", OPTION_FLAG_DISABLED)
endFunction

Event OnOptionHighlight(int option)
    if option == BasketweavingSkillRespec_OID
        SetInfoText("Select to have all spent skill points refunded.")
    elseif option == BasketweavingSkillRestore_OID
        SetInfoText("Select to restore lost skill point progress.")
    endif
EndEvent

Event OnOptionSelect(int option)
    if option == BasketweavingSkillRespec_OID
        bool b = ShowMessage("Are you sure you want to refund all earned Basketweaving skill points so you can reallocate them?")
        if b
            RefundSkillPoints()
            ShowMessage("Skill points restored successfully.", false)
        endif
    elseif option == BasketweavingSkillRestore_OID
        bool b = ShowMessage("This option is intended to be used to reclaim Basketweaving skill progress that was lost due to a clean save or mod uninstallation. Do you wish to continue?")
        if b
            ShowMessage("Select the total number of skill points to restore. This will replace your current progress.")
            SetToggleOptionValue(BasketweavingSkillRestore_OID, true, true)
            SetOptionFlags(BasketweavingSkillRestoreSlider_OID, OPTION_FLAG_NONE)
        endif
    endif
endEvent

event OnOptionSliderOpen(int option)
    if option == BasketweavingSkillRestoreSlider_OID
        SetSliderDialogStartValue(0.0)
        SetSliderDialogDefaultValue(0.0)
        SetSliderDialogRange(0, BasketweavingPerkPointsTotal.GetValue())
        SetSliderDialogInterval(1.0)
    endif
endEvent

event OnOptionSliderAccept(int option, float value)
    if option == BasketweavingSkillRestoreSlider_OID
        BasketweavingPerkPointProgress.SetValue(0.0)
        BasketweavingPerkPoints.SetValue(value)
        BasketweavingPerkPointsEarned.SetValue(value)
        ClearBasketweavingPerks()
        ShowMessage("Skill points restored successfully.", false)
        SetOptionFlags(BasketweavingSkillRestoreSlider_OID, OPTION_FLAG_DISABLED, true)
        SetToggleOptionValue(BasketweavingSkillRestore_OID, false)
    endif
endEvent

function RefundSkillPoints()
    ClearBasketweavingPerks()
    BasketweavingPerkPoints.SetValueInt(BasketweavingPerkPointsEarned.GetValueInt())
    BasketweavingPerkPointProgress.SetValue(0.0)
endFunction

function ClearBasketweavingPerks()
    ; All current perk rank globals go here.
    Basket_PerkRank_BasketBeginner.SetValueInt(0)
    Basket_PerkRank_RedBasketExpert.SetValueInt(0)
    Basket_PerkRank_HappyBasketCrafter.SetValueInt(0)
    Basket_PerkRank_TravelBasket.SetValueInt(0)
    Basket_PerkRank_BasketMaster.SetValueInt(0)
endFunction

This code will generate a set of controls on your MCM page that look like the following.

Appendix D: Creating Skill Tree Without Campfire.esm Dependency

This involves a few more steps than if Campfire.esm is required, but it is possible. There are two options:

  • Option A – Create a plug-in that requires both your main plug-in, and Campfire.esm, and build your skill tree in this secondary optional plug-in. This may make things a bit harder on your end since you may have to test for the presence of both Campfire AND your optional Campfire Skill Tree plugin, and react accordingly if necessary, unless you make your secondary plug-in completely self-contained. This also requires your users to use another plug-in in their load order if they want the perk tree and some may not have the space for it.
  • Option B – Copy forms from Campfire.esm into your plug-in and then build your tree in your main plugin file. Doing things this way may be easier to manage but you will need to carefully copy forms using TES5Edit.

 

Option A – Secondary Optional Plug-In

Complete the following steps:

1. Create a plug-in similar to creating a compatibility patch (using TES5Edit or Wrye Bash) and require both your mod and Campfire.esm as master files.

2. Once that’s done, open your new optional plug-in and continue the tutorial from Part 1, Globals; you will have access to all of the examples and template files that Campfire provides.

Option B – Integration into Main Plug-In


A fully-functional version of the Basketweaving example that doesn’t require Campfire.esm is available here:

Download


Warning
Make a back-up of your main plug-in before you begin! You could lose work if you make a mistake!

You can integrate a Campfire Skill Tree into your main plug-in without a hard dependency on Campfire.esm that will work if Campfire is loaded and fails gracefully if it isn’t.

To do this, you will need to copy forms from Campfire.esm into your main plug-in so you can use them.

Complete the following steps:

1. Open TES5Edit, and load Campfire.esm and your main plug-in.

2. Right-click on the following records in Campfire.esm and select Copy as new record into…, and select your plug-in as the destination (ignoring the error about Editor IDs; it’s fine to keep the names the same as you won’t be loading your plug-in and Campfire.esm in the CK at the same time):

Global: CampfireIsPerkEligibleToBuy
Activator: _Camp_PerkLine_DefaultForModdersCOPYME
Activator: _Camp_PerkNode_DefaultForModdersCOPYME
Activator: _Camp_PerkNodeController_DefaultForModdersCOPYME
Activator: _Camp_PerkNodePosRefDummy
Activator: _Camp_PerkLinePosRefDummy
Static: _Camp_CampfireStagingStaticDONOTDELETE
Static: _Camp_PerkArtPlane_DefaultForModdersCOPYME
TextureSet: _Camp_PerkArt_Example

3. After you’ve copied the records, right-click the CampfireIsPerkEligibleToBuy Global you copied in your plug-in and select Change FormID. Change the FormID to 01CC0D02. This will inject the global into Update.esm and you can now use it to conditionalize your “Choose Perk” button in your perk Message objects.

4. Exit TES5Edit and save your plug-in. Make a back-up. You might need it if something went wrong.

5. You’ll need to stage the position references yourself since you can’t copy the default from Campfire’s modder staging cell. Begin by going to World, Cells…, right click the list, and create a new Cell. Inside the render window, drop a RifRmBgKeepFloorStone01 into the cell positioned at X 0, Y 0, Z 0. Also at 0, 0, 0 drop a copy of _Camp_CampfireStagingStaticDONOTDELETE and an XMarker static. Do not rotate anything.
Turn on Snap to Grid (snap_to_grid) to make positioning them exactly at 0, 0, 0 a bit easier. Turn it off when you’re done.

6. Drag a copy of _Camp_PerkArtPlane_DefaultForModdersCOPYME into the cell. Position it -47 units Y and +109 units Z from the XMarker. If your XMarker is at 0, 0, 0, that means your art plane will be at X 0, Y -47, and Z 109.

(Note: Your copy of the _Camp_PerkArtPlane_DefaultForModdersCOPYME might be a solid, translucent blue instead of the text below. This is fine, it just means that the TextureSet got disassociated with the static when you copied the records in TES5Edit. It won’t matter in a few minutes when you replace it with your own perk art plane anyway. If it bothers you or makes it difficult to work, open _Camp_PerkArtPlane_DefaultForModdersCOPYME in the Object Window, select Mesh, Edit, and assign it the _Camp_PerkArt_Example TextureSet.)

7. Drop a _Camp_PerkNodePosRefDummy into the cell. Position it -68 units Y from the XMarker.

8. Finally, drop a _Camp_PerkLinePosRefDummy into the cell. Position it at the same coordinates as the node. Set the X Angle to 90 and the Y Angle to 0. (Very important for proper display!) After that, adjust only the Z angle to “spin” the line into the direction you want it to face and scale accordingly.

You’re now ready to set up the rest of your tree position references in your staging cell when the time comes in Part 3. Just duplicate the nodes and lines as necessary.

9. You should now able to continue the tutorial from Part 1, Globals and create the rest of your skill tree. When you reach Part 3 and begin setting up your position references, just edit the references (perk nodes, lines, perk art plane) you created on this page; you don’t need to make another copy of them.