Here you will find some easy and tasty recipes for commonly used tasks and commonly asked questions.
One of the many things that sets Skyrim Platform (SP) apart from Papyrus is that SP doesn't bake variable values into saves.
This isn't good or bad. It just is.
But depending on what you want to do, it may be convenient or inconvenient.
In this recipe we will save variable values, so they can be restored when the player (re)loads a saved game.
You may use PapyrusUtil as well, but we will be using JContainers for this recipe because it's deemed to be harder to use than PapyrusUtil.
-
Follow instructions on how to install JContainers Typescript definitions.
-
Import
JDB
from JContainers.
import * as JDB from "JContainers/JDB";
- Create a
key
name for your mod.
const key = ".my-unique-mod-name";
WARNING: Your key name must ALWAYS start with .
otherwise this recipe won't work.
It's recommended it doesn't contain spaces.
- Save values to your liking.
JDB.solveIntSetter(key + '.someInt', someInt, true);
JDB.solveFltSetter(key + '.someFlt', someFlt, true);
JDB.solveStrSetter(key + '.someStr', someStr, true);
- Restore values to your liking.
someInt = JDB.solveInt(key + '.someInt');
someFlt = JDB.solveFlt(key + '.someFlt');
someStr = JDB.solveStr(key + '.someStr');
Plugin variables need to be initialized when creating a new game and when loading a saved game, but there's no event sent when a new game has been created.
This recipe let us initialize a plugin when:
- A new game is started.
- It's first installed.
- A game has been reloaded.
- Skyrim Platform's hot reloading feature runs.
Same ingredients as Saving variables.
This recipe is an extension of the techniques learned there.
- Create functions to save/load a
boolean
. These will check if our plugin was already initialized.
Imagine we are asking if our plugin has already gone through anOnInit
event.
const initK = ".my-plugin.init";
const MarkInitialized = () => JDB.solveBoolSetter(initK, true, true);
const WasInitialized = () => JDB.solveBool(initK, false);
- Add event hooks to both
loadGame
andupdate
.
Usingonce("update")
means our code will run once as soon as the game has started or hot reloading happened.
on("loadGame", () => {});
// IMPORTANT: we are using ONCE instead of on.
once("update", () => {});
- Add a
boolean
variable to check if your plugin has already been initialized byloadGame
.
This will avoid initialization to be done twice when loading an existing save, but allows it to happen when hot reloading.
let allowInit = false;
on("loadGame", () => {
allowInit = true;
});
once("update", () => {
if (allowInit) {
InitPlugin();
}
});
- Check if your plugin has ever been initialized.
Usually,loadGame
will let you do initializons when your plugin is installed mid game, but this step lets you do them when creating a new game.
Here we need to use thestorage
Map so the value ofallowInit
is remembered between hot reloadings.
let allowInit = storage["my-plugin-init"] as boolean | false;
on("loadGame", () => {
// Initialize when installed mid game and when loading a game
// because this is always needed anyway.
InitPlugin();
allowInit = true;
storage["my-plugin-init"] = true
});
once("update", () => {
// Has this plugin ever been initialized?
// OnInit facsimile.
if (allowInit || !WasInitialized()) {
InitPlugin();
}
});
- Initialize your plugin to your needs.
Usingstorage
is not really necessary if we save to diskpluginVar1
each time we set a new value it (a must if you want your plugin values to be persistent, anyway), but we will use it here to demonstrate how it is used.
// Initialize with the value it had before hot reloading or default 0
let pluginVar1 = storage["my-plugin-var1"] as number | 0;
function InitPlugin() {
const key = ".my-plugin.var1";
// Initialize with the value it had before reloading the game or default 0
pluginVar1 = JDB.solveFlt(key, 0);
// Save values inmediately to both disk and storage, so they don't get lost.
JDB.solveFltSetter(key, pluginVar1, true);
storage["my-plugin-var1"] = pluginVar1;
// Let's suppose this was OnInit.
MarkInitialized();
}
Here's the full code:
import * as JDB from "JContainers/JDB"
import { on, once } from "skyrimPlatform"
const initK = ".my-plugin.init";
const MarkInitialized = () => JDB.solveBoolSetter(initK, true, true);
const WasInitialized = () => JDB.solveBool(initK, false);
export function main() {
let allowInit = storage["my-plugin-init"] as boolean | false;
on("loadGame", () => {
// Initialize when installed mid game and when loading a game
// because this is always needed anyway.
InitPlugin();
allowInit = true;
storage["my-plugin-init"] = true
});
// IMPORTANT: we are using ONCE instead of on.
once("update", () => {
// Has this plugin ever been initialized?
// OnInit facsimile.
if (allowInit || !WasInitialized()) {
InitPlugin();
}
});
// Initialize with the value it had before hot reloading or default 0
let pluginVar1 = storage["my-plugin-var1"] as number | 0;
function InitPlugin() {
const key = ".my-plugin.var1";
// Initialize with the value it had before reloading the game or default 0
pluginVar1 = JDB.solveFlt(key, 0);
// Save values inmediately to both disk and storage, so they don't get lost.
JDB.solveFltSetter(key, pluginVar1, true);
storage["my-plugin-var1"] = pluginVar1;
// Let's suppose this was OnInit.
MarkInitialized();
}
}
Cloaks are infamous for being inefficient, but thanks to the new events defined in SP, they are becoming increasingly unneeded.
The basic need that made cloaks necessary in the past is this:
Some code needs to be executed on some
Actor
.
If your plugin still needs to use this idea, but none of the new events suits you, then you can use a more efficient method; a method we will use for this recipe.
- Create a new Magic Effect in the CK.
- Effect Archetype: Script
- Casting Type: Constant
- Delivery: Self
- Fill other properties according to your needs.
Lets suppose this magic effect has a relative FormId of 0x800
.
-
Create a new spell.
- Type: Ability
- Casting Type: Constant effect.
- Delivery: Self.
- Effects: the magic effect you created in the previous step.
- Fill other properties according to your needs.
-
Distribute that spell according to SPID instructions.
-
Catch the magic effect being applied by using the
effectStart
event.
import { on } from "skyrimPlatform";
on("effectStart", (event) => {
const fx = Game.getFormFromFile(0x800, "my-mod.esp");
if (fx?.getFormID() !== event.effect.getFormID()) return;
DoSomething(event.target);
})
- (Optional) you can use the
effectFinish
event if you need to do some cleaning.
on("effectFinish", (event) => {
const fx = Game.getFormFromFile(0x800, "my-mod.esp");
if (fx?.getFormID() !== event.effect.getFormID()) return;
DoSomeCleaning(event.target);
})