文档原址请戳这里
这篇翻译是我在做 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运行时,游戏会暂停几秒