Skip to content

Latest commit

 

History

History
285 lines (207 loc) · 8.66 KB

cookbook.md

File metadata and controls

285 lines (207 loc) · 8.66 KB

The Skyrim Platform Cook Book

Here you will find some easy and tasty recipes for commonly used tasks and commonly asked questions.

Table of contents

Saving variables

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.

Ingredients

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.

Preparation

  1. Follow instructions on how to install JContainers Typescript definitions.

  2. Import JDB from JContainers.

import * as JDB from "JContainers/JDB";
  1. 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.

  1. Save values to your liking.
JDB.solveIntSetter(key + '.someInt', someInt, true);
JDB.solveFltSetter(key + '.someFlt', someFlt, true);
JDB.solveStrSetter(key + '.someStr', someStr, true);
  1. Restore values to your liking.
someInt = JDB.solveInt(key + '.someInt');
someFlt = JDB.solveFlt(key + '.someFlt');
someStr = JDB.solveStr(key + '.someStr');

Plugin initialization

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.

Ingredients

Same ingredients as Saving variables.
This recipe is an extension of the techniques learned there.

Preparation

  1. 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 an OnInit event.
const initK = ".my-plugin.init";

const MarkInitialized = () => JDB.solveBoolSetter(initK, true, true);
const WasInitialized = () => JDB.solveBool(initK, false);
  1. Add event hooks to both loadGame and update.

    Using once("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", () => {});
  1. Add a boolean variable to check if your plugin has already been initialized by loadGame.
    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();
  }
});
  1. 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 the storage Map so the value of allowInit 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();
  }
});
  1. Initialize your plugin to your needs.

    Using storage is not really necessary if we save to disk pluginVar1 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();
  }
}

Getting rid of cloaks

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.

Ingredients

Preparation

  1. 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.

  1. 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.
  2. Distribute that spell according to SPID instructions.

  3. 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);
})
  1. (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);
})