文档原址请戳这里
这篇翻译是我在做 Simple UI 时自己做的翻译,应该是全网第一份关于SP的教程翻译,如果发现有任何问题,请在评论区反馈,国内对于SP的讨论可以说是无,据我所知国内的mod作者除我之外,就只有影天大佬用SP做过mod了
Skyrim Platform是Skyrim的一个修改工具,允许用JavaScript/TypeScript编写脚本。其中一个建立在Skyrim Platform上的MOD是skymp客户端。是的,从技术上讲,Skyrim Multiplayer的客户端是使用Skyrim Platform实现的Skyrim特别版的一个MOD
原游戏中Papyrus的类
- SP中全部类拥有和Papyrus相同的名字,例如
Game
,Actor
, Form
,Spell
, Perk
, 等
- 要使用Papyrus中的类,包括调用其方法和函数,需要引入
import { Game, Actor } from "../skyrimPlatform";
原生函数(Native functions)
- 大多数类都有一个本地函数列表,分为静态函数(papyrus中的
Native Global
)和方法(Native
)
- 静态函数在类上调用:
let sunX = Game.GetSunPositionX();
let pl = Game.GetPlayer();
Game.forceFirstPerson();
let isPlayerInCombat = pl.isInCombat();
表(Form)
- 表被大多数有方法的游戏类继承,如
Actor
, Weapon
, 等.
- 每个表都有ID,它是一个32位未标识数字(
uint32_t
)。在Skyrim Platform中由类型number
表示
- 如果你需要通过ID查找表,用
Game.getFormEx
注意是Game.getFormEx
,不是Game.getForm
。后者对大于0x80000000的ID总是返回null
(原游戏的行为behavior)
- 你可以使用
getFormID
方法获取表ID。如果表没有被游戏销毁,Game.getFormEx
则一定能够通过此方法返回的ID找到表。
对象的安全使用
let actor = Game.findClosestActor(x, y, z, radius);
if (actor) {
let isInCombat = actor.isInCombat();
}
或
let actor = Game.findClosestActor(x, y, z, radius);
if (!actor) return;
let isInCombat = actor.isInCombat();
未处理的异常
- 未处理的JS异常将于调用堆栈一起记录到控制台
- 原始的Promise rejections也会输出到控制台
- 不要发布具有未处理的已知错误的插件。未处理的异常无法保证 Skyrim Platform性能
对象比较
- 在SkyrimPlatform比较对象,你需要对比它们的IDs:
if (object1.getFormId() === object2.getFormId()) {
// ...
}
对象转换为字符串
- JS对象的常规操作对Papyrus移植的类型支持有限,如
toString
,toJSON
.
Game.getPlayer().ToString(); // '[object Actor]'
JSON.stringify(Game.getPlayer()); // `{}`
转换
- 如果你有一个作为武器的
Form
对象,并且你需要一个Weapon
对象,你可以使用转换:
let sword = Game.getFormEx(swordId); // Get Form
let weapon = Weapon.from(sword); // Cast to Weapon
- 如果你为实际上不是武器的表单指定ID,
weapon
变量将是null
- 把
null
作为参数传给用于转换类型的函数不会引发异常,但会返回null
:
ObjectReference.from(null); // null
- 尝试转换为没有实例或在继承层次结构中不兼容的类型也将返回
null
:
Game.from(Game.getPlayer()); // null
Spell.from(Game.getPlayer()); // null
- 你也可以用类型转换获取一个基础类型,包括
Form
:
let refr = ObjectReference.from(Game.getPlayer());
let form = Form.from(refr);
let actor = Actor.from(Game.getPlayer());
SkyrimPlatform添加的Papyrus类型
- SkyrimPlatform目前仅添加一种类型:
TESModPlatform
这种类型的实例与Game
类比不存在,以下列出了其静态函数
moveRefrToPosition
- 将对象传送到指定的地点和位置。
setWeaponDrawnMode
- 强制角色始终保持抽出/移除武器
getNthVtableElement
- 从虚拟表中获取函数的偏移量(用于逆向工程)
getSkinColor
- 获取ActorBase的皮肤颜色.
createNpc
- 创建一个新的ActorBase类型的表单.
setNpcSex
- 改变ActorBase的性别.
setNpcRace
- 改变ActorBase的种族.
setNpcSkinColor
- 改变ActorBase的皮肤颜色.
setNpcHairColor
- 改变ActorBase的头发颜色.
resizeHeadpartsArray
- 调整ActorBase头部的数组.
resizeTintsArray
- 调整主角的TintMasks数组.
setFormIdUnsafe
- 改变表单ID。不安全,使用风险自负.
clearTintMasks
- 移除给定角色的TintMasks,如果没有传角色,则移除玩家角色的TintMasks。
pushTintMask
- 为给定的角色或玩家角色(如果没有传角色)添加带有定义参数的TintMask。
pushWornState
,addItemEx
- 从def.ExtraData中添加/删除项目
updateEquipment
- 更新装备(不稳定)。
resetContainer
- 清理基础容器。
异步
- 一些游戏函数需要耗时并发生在后台。这些函数在SkyrimPlatform返回
Promise
:
Game.getPlayer()
.SetPosition(0, 0, 0)
.then(() => {
printConsole("Teleported to the center of the world");
});
Utility.wait(1).then(() => printConsole("1 second passed"));
Utility.wait(1);
printConsole(`Will be displayed immediately, not after a second`);
printConsole(`Should have used then`);
- 可以使用
async
/await
使代码看起来是同步的
let f = async () => {
await Utility.wait(1);
printConsole("1 second passed");
};
事件
- 目前,SkyrimPlatform有能力监听你自己的事件:
update
和tick
.
update
是在你加载存档或开始游戏后,游戏中的每一帧调用一次的事件
import { on } from "../skyrimPlatform";
on("update", () => {
// At this stage, the methods of all imported
// types are already available.
});
tick
是游戏开始后立即为游戏中的每一帧调用一次的事件
import { on } from "../skyrimPlatform";
on("tick", () => {
// No access to game methods here.
});
- 也适用于游戏事件,例如
effectStart
,effectFinish
, magicEffectApply
,equip
, unequip
,hit
, containerChanged
,deathStart
, deathEnd
,loadGame
, combatState
, reset
,scriptInit
, trackedStats
,uniqueIdChange
, switchRaceComplete
,cellFullyLoaded
, grabRelease
,lockChanged
, moveAttachDetach
,objectLoaded
, waitStop
,activate
...
on
可以永久监听事件
import { on } from "../skyrimPlatform";
on("equip", (event) => {
printConsole(`actor: ${event.actor.getBaseObject().getName()}`);
printConsole(`object: ${event.baseObj.getName()}`);
});
once
可以添加一个处理程序,该处理程序将在下次触发事件时调用
import { once } from "../skyrimPlatform";
once("equip", (event) => {
printConsole(`actor: ${event.actor.getBaseObject().getName()}`);
printConsole(`object: ${event.baseObj.getName()}`);
});
Hooks
- Hooks允许你截取游戏引擎某些功能的开始和结束
- 目前支持hooks
sendAnimationEvent
sendPapyrusEvent
import { hooks, printConsole } from "../skyrimPlatform"
hooks.sendAnimationEvent.add({
enter(ctx) {
printConsole(ctx.animEventName);
},
leave(ctx) {
if (ctx.animationSucceeded) printConsole(ctx.selfId);
};
});
hooks.sendAnimationEvent.add({
enter(ctx) {
printConsole("Player's anim:", ctx.animEventName);
},
leave(ctx) {}
}, /* minSelfId = */ 0x14, /* maxSelfId = */ 0x14, /*eventPattern = */ "*");
enter
在启动函数前调用,ctx
包含传给函数的参数和storage
(见下文)
leave
在函数结束前调用,ctx
包含函数的返回值,以及enter
完成后的内容
ctx
是enter
和 leave
调用的同一个对象
ctx.storage
在调用enter
和 leave
之间存储被调用的数据
- Script functions在
enter
和leave
处理程序中不可用
自定义SkyrimPlatform方法和属性
- 导入后可以立即调用
printConsole ()
等方法。它们不属于任何游戏类型
printConsole (... arguments: any []): void
-输出到游戏控制台
import { printConsole, Game } from "../skyrimPlatform";
on("update", () => {
printConsole(`player id = ${Game.getPlayer().getFormID()}`);
});
worldPointToScreenPoint
-将游戏世界中的点数组转换为用户屏幕上的点数组。屏幕上的点有-1到1的3个数字表示
on (eventName: string, callback: any): void
- 监听一个名字为eventName
的事件.
callNative (className: string, functionName: string, self ?: object, ... args: any): any
- 通过名字调用一个原游戏的函数.
getJsMemoryUsage (): number
- 获取嵌入式JS引擎使用的RAM量,以字节为单位
storage
- 用于在重新加载脚本之间保存数据的对象
browser
是一个提供对Chromium嵌入式框架访问的对象
getExtraContainerChanges
- get ExtraContainerChanges of the given ObjectReference...
getContainer
- get all the items of the base container.
settings
- 提供对插件设置访问的对象:
import { settings, printConsole } from "../skyrimPlatform";
let option = settings["plugin-name"]["my-option"];
printConsole(option);
插件设置文件名为plugin-settings.txt
应该放在Data / Platform / Plugins
文件夹。文件格式 - JSON,扩展名.txt
- 方便用户
更改游戏控制台命令
- SkyrimPlatform允许您更改任何游戏控制台命令的实现,对于此类修改,您需要通过将命令名称传递给
findConsoleCommand (commandName)
方法(短或长)来获取控制台命令对象。
let getAV = findConsoleCommand("GetActorValueInfo");
let getAV = findConsoleCommand("GetAVInfo");
- 收到这样的对象后,您可以更改短 (shortName) 或长 (longName) 命令名称,以及接受的参数数量 (numArgs) 和通过游戏控制台调用命令时将执行的函数 (execute) 。
getAV.longName = "printArg";
getAV.shortName = "";
getAV.execute = (refrId: number, arg: string) => {
printConsole(arg);
return false;
};
- 你新实现的返回值指示该命令的原始功能是否将被执行
- 第一个参数是调用控制台命令的对象的 FormId,如果不存在,则为 0
- 其余参数将是调用控制台命令的参数,类型为
string
或number
- 由于游戏函数在这种情况下不可用,你必须注册一个
once
的update
事件处理程序,如果你想在调用控制台命令时调用游戏函数:
getAV.longName = "ShowMessageBox";
getAV.shortName = "";
getAV.execute = (refrId: number, arg: string) => {
once("update", () => {
Debug.messageBox(arg);
});
return false;
};
HTTP请求
SkyrimPlatform为HTTP请求提供有限的支持,目前只有get
import { HttpClient } from "../skyrimPlatfosrm";
let http = new HttpClient("vk.com", 80);
http.get("/").then((response) => printConsole(response.body));
热重载
- 支持SkyrimPlatform插件的热重载,更改
Data / Platform / Plugins
的内容将重载所有插件无需重新启动游戏
- 为了重新利用这些功能,即用Ctrl + S重载你的插件
- 重载插件时,添加的事件和hook处理程序被移除,异步操作中断,所有变量重置,除了
storage
及其属性
转存函数
- SkyrimPlatform具有内置功能,可以让你将有关游戏功能的信息输出到文件
Data / Platform / Output / DumpFunctions.txt
组合键(9+O+L),当DumpFunctions运行时,游戏会暂停几秒