Scripting a MOD for The Elder Scrolls IV: Oblivion
This was my first foray into modding Elder Scrolls titles and I started with Oblivion mainly because I was actively replaying it and I had an idea. I was playing a battlemage and felt that there was balancing issues. But more on that later. For now I followed a tutorial on Scripting, because as a programmer scripting is what interests me, not so much 3D editing or world building elements.
Setup the TES Construction Kit
The entire Oblivion game can be edited with the TES Construction Kit.
Install, open and then File > Data > Oblivion.esm
.esm files are the Elder Scrolls master files that contain all of the resources for the game.
.esp files are the Elder Scrolls patch files that can make adjustments to the content in the
esm files. This is not unlike DOOM WAD files that I explored.
When saving our mod with File > Save we must place it in the
Data directory of our Installation folder. For me that was in steamapps.
Our First Script: Riddle Chest
This came from a tutorial online and it covers some basic structure for scripts:
- Each script must have its name declared on the first line:
- Event blocks must wrap code and trigger with in-game events such as:
Begin OnActivateif attached to an object activated by the player
- These events essentially run on repeat based on their activation and are called
- This creates a lot of "do once" booleans to check if code has already been run:
The goal of the tutorial was to attach a script that poses a riddle to the player in order for it to open.
Under Gameplace > Edit Scripts > New we can create scripts and the tite line becomes its name. From there it can be edited.
I created an
object type script that lets us pose a riddle to the player on opening an object as a lock:
ScriptName RiddleChestScript ; This script locks an object behind a riddle ; If it is answered correctly it will open to reveal its contents ; If it is failed it will lock cast a curse attack and lock permanently Short controlvar Short button Begin OnActivate if ( controlvar == -1 ) MessageBox "I will suffer no fools." ElseIf ( controlvar == 0 ) MessageBox "Voiceless it cries, wingless flutters, toothless bites, mouthless mutters. What is it?", "Bat", "Old woman", "Wind", "Wraith" Set controlvar to 1 ElseIf ( controlvar > 1 ) Activate EndIf End ; game play loop for player Begin GameMode If ( controlvar == 1 ) ; get which button option was last selected by the player Set button to GetButtonPressed If ( button == -1 ) ; TODO not sure what causes this state...? Return ElseIf ( button == 2) ; running Activate here causes double overlay of UI, one for the message and one for the inventory ; so instead we activate it on the next gameplay loop MessageBox "Your answer was correct." Set controlvar to 2 Else ; no more chances MessageBox "Your answer was wrong." ; punish them with a curse Cast Mg05FingerSpell15 Player Set controlvar to -1 EndIf ElseIf ( controlvar == 2 ) Activate ; move to end state Set controlvar to 3 EndIf End
I went into the
Object Window and under
Clutter attached this script to the "Lectern" object.
There happens to be a lectern on the desk of the Alchemist in Cheydinhall. Upon activating it we get:
Making a Script Global
Scripts are of three types: Object, Quest or Magic Effect. Objects are attached to objects in the world and run when they are being updated as part of their object. A Quest script can run at all times, but since it is not a part of an object it has reference to less reference variables. This is fine because I want my script to be running on a continual basis.
To create a Global Script reference from TES Construction Kit Global Scripts.
This first version of the quest script will initalize some values when the script starts and set the current player level. Then if during gameplay the player level changes we can capture it.
ScriptName PRAE_OnMagicUp short state short lastLevel short currentLevel ; Quest scripts run on 5 second intervals Begin GameMode If ( state == 0 ) Set currentLevel to Player.GetLevel Set lastLevel to currentLevel Message "level is %.0f" currentLevel Set state to 1 Else ; on each update cycle poll our current level Set currentLevel to Player.GetLevel EndIf If ( currentLevel != 0 && lastLevel != currentLevel ) Set lastLevel to currentLevel Message "level up detected lets add some magicka" EndIf End
With the 5 second intervals it takes a second to apply the level detection routine. I think we could adjust this to a
Begin MenuMode 1027 which detects if the level up menu runs.