エディタ設定
書いているそばからエディタが全項目を一覧し、意味を教え、打ち間違いをゲーム起動前に見つけてくれるようにします
このページでできるようになること
- パルやアイテム、建物に指定できる項目を、打ちながらエディタに一覧してもらう
- 各項目の意味と、書かなかったときの既定値をその場で確認する
- 項目名の打ち間違いを、ゲームを起動する前にエディタで見つける
- 決まった値しか取らない項目で、使える値の中から選ぶ
- エディタから離れているときは、同じ一覧をゲーム内で表示する
コンテンツパックはただの Lua ファイルです。コンパイルも、エディタのプラグインの導入も必要 ありません。
やることは 1 つ、PalForge の場所を 1 つのプログラムに教えるだけです。そのプログラムが
lua-language-server、通称 LuaLS で、エディタ
が Lua を理解するために裏で動いています。PalForge の Scripts フォルダを教えておくと、
Pal{ ... } や Item{ ... }、Building{ ... } を書いたときにフィールドの候補が出るように
なります。フィールドとは、波かっこの中の name = や mesh = のような 1 行のことです。候補
には、パック読み込み時に PalForge が照合するのと同じ説明が付いてきます。
エディタを設定する
LuaLS から Scripts/palforge/ フォルダが見えている必要があります。どこを見るかは
.luarc.json というファイルで指定します。このファイルを置く場所は、パックを自分のフォルダで
管理しているか、PalForge のフォルダの中で書いているかで変わります。
.luarc.json をパックのフォルダの一番上に置き、PalForge の Scripts ディレクトリをライブラリ
として並べます。パスは .luarc.json からの相対です。
{
"runtime.version": "Lua 5.4",
"workspace.library": [
"../PalForge/Scripts"
],
"diagnostics.globals": [
"FindFirstOf", "FindAllOf", "StaticFindObject", "LoadAsset",
"RegisterHook", "RegisterKeyBind", "RegisterConsoleCommandHandler",
"ExecuteInGameThread", "LoopAsync", "FName", "Key"
]
}フルパスでも書けます。パックを UE4SS の mods フォルダに置き、PalForge はディスク上の別の場所 にある、という場合はこちらが向いています。
{
"workspace.library": [
"C:/games/Palworld/Pal/Binaries/Win64/ue4ss/Mods/PalForge/Scripts"
]
}PalForge のコピーによっては、Scripts/palforge/deprecated/ と Scripts/palforge/tmp/ という
フォルダがディスク上にあります。リリースには含まれないので、手元にない場合もあります。ある
場合は、古い定義が候補に混ざらないよう、エディタに読み飛ばしてもらってください。
{
"workspace.ignoreDir": [
"Scripts/palforge/deprecated",
"Scripts/palforge/tmp"
]
}エディタに出てくるもの
パック側のファイルに import するものはありません。Pal、Item、Building、Skill、
Effect、Audio、Mesh、UI、Player は最初からグローバルとして置かれ、それぞれに型が
書かれているので、local を書かずにそのまま使えます。
---@type palforge.pal
_G.Pal = Pal波かっこの中身が補完されるのは、各モジュールに付いた ---@overload の行のおかげです。
Pal{ ... } が Pal.Spec と、省略可能な第 2 引数 opts
({ register = false, pack = "mypack" })を受け取り、Pal.Handle を返す、とエディタに
伝えています。
---@class palforge.pal
---@overload fun(spec: Pal.Spec, opts: table?): Pal.Handle
local Pal = {}設定前 — Scripts がワークスペースの外にあると、エディタは Pal.Spec を知らないので、
波かっこはただのテーブルで、出せる候補がありません。
Pal{
| no suggestions; a typo like displayName surfaces only when the game runs
}設定後 — 候補一覧は Pal.Spec のフィールドそのもので、宣言された順に並び、それぞれに説明
が付きます。
Pal{
|
id string pal id: a game CharacterID ("ChickenPal") or "pack:name"
name string? shown in UI (defaults to id)
description string? one-line description, for UI and tooling
skills string[]? skill ids this pal owns (see Skill)
mesh Mesh.Spec|Mesh.Handle? the mesh worn by a spawned pawn (inline, or a Mesh{ ... } handle); attached automatically on pal.spawned
material Pal.Spec.Material? material override applied to that mesh
color table? base tint { r, g, b, a } (shorthand for material.color)
texture string? png path applied to the mesh (shorthand for material.texture)
icon string? /Game/... texture path used when the icon DataTable has no row for this id
events Pal.Spec.Events? lifecycle handlers (grouped)
data table? free-form payload of your own, carried onto the definition
}ここで一度に 4 つのことが手に入ります。
- 各フィールドとその説明。
---@field行の#の後ろは、スペック宣言のdoc =の文字列 そのものです。maxStackにホバーすれば stack ceiling you declare; the GAME's ceiling is a DataTable column (default 1) と出て、既定値まで含まれます。 - 決まった値しか取らない項目の、短い選択肢。
valuesを持つフィールドは専用の型になり、 それが 30 個あります。ドメインのスペック由来が 5 つ(Mesh.Spec.Kind、Item.Spec.Category、Building.Spec.Mesh.Kind、Skill.Spec.Kind、Audio.Spec.Kind)、 残る 25 個はapi/ui.lua由来です。UI.Spec.Input、UI.Node.Button.LabelAlign、UI.Node.Sprite.From、そして 11 個のノードコンストラクタそれぞれに付くHAlignとVAlignの対。メッシュでkind =の後に引用符を打つと、"procedural"、"static"、"skeletal"、"obj"だけが出て、それ以外は出ません。 - ネストした形は、どちらの書き方でも。
meshの型はMesh.Spec|Mesh.Handleです。メッシュ をその場に直接書いても、Mesh{ ... }が返したものを渡しても、どちらでも通ります。 - 返ってきたもので何ができるか。
Pal{ ... }はPal.Handleを返すので、pal:と打つとspawn、renderOn、skillsOf、mesh、iconOf、name、descriptionとイベントの フォワーダが、api/pal.luaでそれぞれの上に書かれたコメントつきで出ます。
local pal = Pal{
id = "example:Boss",
name = "Boss",
description = "the one that greets you",
mesh = Mesh{
id = "example:body",
kind = "skeletal", -- completes to the four Mesh.Spec.Kind values
model = "/Game/Pal/Model/Character/Monster/ChickenPal/SK_ChickenPal",
},
events = { -- completes to Pal.Spec.Events, five handlers
onSpawned = function(pal, ctx) -- pal is a Pal.Handle, so pal: completes
pal:renderOn(ctx.actor)
end,
},
}
pal:spawn(Player.coordinate()) -- Player.coordinate returns Coord?ハンドルとビルディングインスタンスの型は、メソッドの実装のすぐ隣に手で書かれています。
Pal.Handle は api/pal.lua、Building.Handle と Building.Instance は
api/building.lua、という具合です。だからビルディングのハンドラでは、インスタンスの側も
補完されます。
Building{
id = "example:Beacon",
state = { charges = 3 },
events = {
onRightClick = function(inst, ctx) -- inst is a Building.Instance
inst.state.charges = inst.state.charges - 1
inst:save() -- .actor .pos .state .buildId .key all complete
end,
},
}候補を運んでくるファイル
補完に出てくる文字列は、すべて Scripts/palforge/types.lua から来ています。LuaLS 用の定義
ファイルで、中身は説明だけ、実行されるコードはありません。api が受け取るすべての形について
---@class と ---@alias を宣言しています。先頭の ---@meta の行が、これは実際のコードでは
なく型の記述だと LuaLS に伝えていて、フレームワーク側がこのファイルを読み込むことはありません。
-- PalForge type definitions — GENERATED, do not edit.
--
-- Regenerate with: lua5.4 tools/gen-types.lua
-- Source of truth: the schema declarations in Scripts/palforge/api/*.lua
--
-- Annotations only: nothing requires this file at runtime. It exists so an editor
-- (LuaLS / lua-language-server) can complete the fields of every X{ ... } call,
-- show each field's meaning, and jump from a spec name to its field list.
---@meta
---@alias Mesh.Spec.Kind "procedural"|"static"|"skeletal"|"obj"
---@class Mesh.Spec
---@field id? string # mesh id, e.g. "pack:name" (required when defined directly; omit when inline)
---@field kind? Mesh.Spec.Kind # which core.mesh backend renders it (default skeletal)
---@field model string # a /Game/... USkeletalMesh or UStaticMesh path (see Mesh.assets); for procedural / obj, an .obj file path - absolute, or relative to the .lua file that declares itこのファイルには 38 個のクラスと 30 個のエイリアスが入っていて、長さもあります。
ジェネレータの申告で 8759 行です。クラスのうち 31 個は宣言済みの形で、schema.all() が返す
名前と 1 対 1 に対応します。Mesh.Spec、Pal.Spec とその Events・Material、Item.Spec
とその Recipe・Events・Restores、4 つの Building.Spec.*、Skill.Spec とその Events、
Effect.Spec とその Events、Audio.Spec、11 個の UI.Node.*、UI.Spec と
UI.Spec.Host、そして Coord です。
残る 7 個、そして行数のほとんどはネイティブコンテンツのカタログです。
palforge.native.buildings・.items・.pals・.skills・.effects・.audio と、それらを
まとめる palforge.native。native.items.Arrow_Fire は実行時にはメタテーブルの __index
(native/_catalog.lua)が返しており、エディタはメタテーブルの向こう側を見られません。そこで
ジェネレータがカタログ自身から名前を列挙します。このビルドでは 8293 個です。ネイティブの id
が「知っていないと書けないもの」ではなく「打てば見つかるもの」になっているのは、これのおかげ
です。
types.lua を削除して壊れるのは補完だけです。ゲームはこのファイルを読みませんし、
Pal{ ... } のチェック結果もあるとき・ないときで同じです。チェックは
Scripts/palforge/api/*.lua のスペック宣言に対して行われ、このファイルもそこから作られて
います。
types.lua を再生成する
スペックを変えたらジェネレータを実行します。開発用のツールで、ゲームを閉じたまま素の Lua インタプリタで動き、リポジトリ以外には何も要りません。
cd /path/to/PalForge
lua5.4 tools/gen-types.lua出力は 1 行だけで、それが結果のすべてです。
wrote ./Scripts/palforge/types.lua (38 classes, 8763 lines, 8293 native catalog names)classes は書き出した ---@class 行の数、lines は書き出したファイルの行数、
native catalog names はカタログから列挙した native.* フィールドの数です。ファイルは毎回
まるごと上書きされます。
引数は出力先ではなくリポジトリのルートです。渡さなければ今いるフォルダを使い、常に
<root>/Scripts/palforge/types.lua に書き出します。
lua5.4 tools/gen-types.lua /path/to/PalForgeスペックには palforge.api を require して到達します。これがすべての形をスキーマレジストリ
に入れ、そのあと schema.all() を辿ります。読み込めなかったカタログは実行を止めず、標準
エラーに gen-types: skipping <module> を出して飛ばされるので、native catalog names の数が
少ないときは何かの読み込みに失敗しています。
schema.define や schema.derive の呼び出しが変わったら、そのつど実行し直してください。
フィールドの追加、使える値の追加、既定値の変更、説明の書き換え、そしてネイティブカタログを
再生成したときも同じです。types.lua はリポジトリにコミットされているので、再生成した
ファイルは変更と同じコミットに入れます。
types.lua を手で編集しないでください。次にジェネレータを実行した時点でファイル全体が
書き直されます。Scripts/palforge/api/*.lua の schema.define を変更して再生成します。
候補がいつもゲームと一致する理由
エディタは types.lua を読み、ゲームは Scripts/palforge/api/*.lua のスペック宣言を読みます。
この 2 つは、別々に合わせ込む一覧ではありません。ジェネレータは palforge.api を読み込んで
すべての形をスキーマレジストリに載せ、schema.all() を辿ります。定義呼び出しのたびに
Spec:validate が使うのと同じスペックオブジェクトです。
そのため、エディタが出すフィールドはゲームが受け付けるフィールドであり、ゲームが弾く フィールドが候補に出ることはありません。フィールド宣言の各要素は、生成される行に決まった形で 効きます。
| スキーマのディスクリプタ | 生成されるアノテーション |
|---|---|
required = true | ? の付かないフィールド名 |
default = "material" | 説明の末尾に (default material) を追加 |
values = { "active", "passive" } | ---@alias Skill.Spec.Kind "active"|"passive" |
of = Recipe(ネストしたスペック) | そのスペック名、つまり Item.Spec.Recipe。ハンドルを名乗っていれば Mesh.Spec|Mesh.Handle |
arrayOf = "string" | string[] |
mapOf = "number" | table<string, number> |
sig = "fun(self: Pal.Handle, ctx: table)" | そのシグネチャをそのまま |
doc = "..." | # の後ろのテキスト |
check = schema.validId | 何も出ません。check は定義時にだけ走ります |
type なし | any |
知っておくとよいことが 2 つあります。
schema.deriveで宣言した形は、独自のクラスと独自の値リストを持ちます。Building.Spec.MeshはMesh.Specの 3 フィールドを調整したもので、kindの既定値を"skeletal"から"static"へ、modelとoffsetの説明をより具体的にしてあります。 そのためBuilding.Spec.Mesh.Kindが、同じ 4 つの値と別の既定値の記述で生成されます。- クラスは宣言順に出力され、形の名前の最初の部分でまとめられます。だから
Mesh.SpecはMeshの見出しの下に並び、ドメイン接頭辞を持たないCoordはCommonに入ります。 ネイティブカタログのクラスは、宣言済みの形をすべて出し切ったあと、最後に来ます。
UE4SS がゲーム実行中に足す名前
FindFirstOf、FindAllOf、StaticFindObject、LoadAsset、RegisterHook、
RegisterKeyBind、RegisterConsoleCommandHandler、ExecuteInGameThread、LoopAsync、
FName、Key は、PalForge が動く土台である MOD ローダー UE4SS から来ます。どの Lua ファイル
にも書かれていないため、並べておかない限り LuaLS は未定義として印を付けます。上の
diagnostics.globals はそのためのものです。
ジェネレータ側の扱いは違います。ゲームを閉じたまま api モジュールを実際に動かす必要がある ので、何かを読み込む前に、それぞれの名前に何もしない代役を置きます。
for _, name in ipairs({ "FindFirstOf", "FindAllOf", "StaticFindObject", "LoadAsset",
"RegisterHook", "RegisterKeyBind", "RegisterConsoleCommandHandler",
"ExecuteInGameThread", "LoopAsync" }) do
_G[name] = function() return nil end
end
_G.FName = function(s) return { ToString = function() return s end } end
_G.Key = {}api モジュールを読み込んでもテーブルが組み立てられるだけなので、代役が呼ばれることはありま せん。一緒に読み込まれるゲーム寄りのファイルは、名前が存在することだけを求めています。
UE4SS の Lua 型定義を持っているなら、そのフォルダも workspace.library に追加し、対応する
名前を diagnostics.globals から外してください。何も出ない代わりに、実際のシグネチャが出る
ようになります。PalForge はその型定義を同梱していません。
同じ一覧をゲーム内で見る
すべての形は、実行中のゲームからも読めます。キーバインドやコンソールコマンドから、あるいは 検証エラーの原因を追っている最中に役立ちます。
local schema = require("palforge.core.schema")
print(schema.help("Pal.Spec")) -- printable field list
local fields = schema.get("Pal.Spec").fields -- the same as an ordered array, for tooling
local specs = schema.all() -- every declared spec, in declaration orderschema.help はフィールドを宣言順に 1 行ずつ出力します。
Pal.Spec {
id string (required) pal id: a game CharacterID ("ChickenPal") or "pack:name"
name string shown in UI (defaults to id)
description string one-line description, for UI and tooling
skills table (string[]) skill ids this pal owns (see Skill)
mesh table (Mesh.Spec) the mesh worn by a spawned pawn (inline, or a Mesh{ ... } handle); attached automatically on pal.spawned
material table (Pal.Spec.Material) material override applied to that mesh
color table base tint { r, g, b, a } (shorthand for material.color)
texture string png path applied to the mesh (shorthand for material.texture)
icon string /Game/... texture path used when the icon DataTable has no row for this id
events table (Pal.Spec.Events) lifecycle handlers (grouped)
data table free-form payload of your own, carried onto the definition
}宣言されていない名前を渡すと、宣言済みの名前の一覧が返ってきます。
PalForge: no spec named "Pal.Events". Declared: Audio.Spec, Building.Spec, Building.Spec.Events, Building.Spec.Material, Building.Spec.Mesh, Coord, Effect.Spec, Effect.Spec.Events, Item.Spec, Item.Spec.Events, Item.Spec.Recipe, Mesh.Spec, Pal.Spec, Pal.Spec.Events, Pal.Spec.Material, Skill.Spec, Skill.Spec.Events, UI.Node.Border, UI.Node.Button, UI.Node.Frame, UI.Node.GameWidget, UI.Node.HBox, UI.Node.Label, UI.Node.Overlay, UI.Node.ScrollBox, UI.Node.SizeBox, UI.Node.Sprite, UI.Node.VBox, UI.Spec, UI.Spec.Host同じ情報が出てくる 3 つ目の場所は、エラーそのものです。問題が見つかると読み込みは止まり、 ドメイン名、フィールド名、そして有効な選択肢が示されます。つまり読み込みに成功したパックは、 綴りが正しかったということです。
PalForge: Pal: unknown field "displayName" (did you mean "name"?). Valid fields: id, name, description, skills, mesh, material, color, texture, icon, events, data
PalForge: Pal: field "id" is required (pal id: a game CharacterID ("ChickenPal") or "pack:name")
PalForge: Pal: field "mesh" (Mesh.Spec): field "kind" must be one of { "procedural", "static", "skeletal", "obj" }, got "voxel"
PalForge: Pal: field "mesh" (Mesh.Spec): field "model" is required (a /Game/... USkeletalMesh or UStaticMesh path (see Mesh.assets); for procedural / obj, an .obj file path - absolute, or relative to the .lua file that declares it)
PalForge: Pal: field "id" is invalid: invalid pack id 'my-pack' in 'my-pack:Boss' (letters/digits/_ only)レシピ
パックのフォルダをゼロから用意する
PalForge の隣にパックを置く
mods/
PalForge/
Scripts/
main.lua
palforge/
MyPack/
.luarc.json
content/
pals.lua.luarc.json を書く
{
"runtime.version": "Lua 5.4",
"workspace.library": [
"../PalForge/Scripts"
],
"diagnostics.globals": [
"FindFirstOf", "FindAllOf", "StaticFindObject", "LoadAsset",
"RegisterHook", "RegisterKeyBind", "RegisterConsoleCommandHandler",
"ExecuteInGameThread", "LoopAsync", "FName", "Key"
]
}コンテンツを書いて補完を確かめる
local pal = Pal{
id = "ChickenPal",
name = "Lookout Chicken",
skills = { "example:Screech" },
events = {
onCaptured = function(pal, ctx)
Item.get("Berries"):give(3)
end,
},
}
return palPal も Item も require は不要です。api/init.lua がグローバルとして置き、型も書いて
くれているので、LuaLS はライブラリパス越しにそれを拾います。
スペックを変えたら再生成する
フィールドの追加は、宣言 1 つとコマンド 1 つです。その形を持っているモジュールで宣言します。
local Spec = schema.define("Skill.Spec", {
{ "id", type = "string", required = true, check = schema.validId,
doc = "skill id: a game row id or \"pack:name\"" },
{ "name", type = "string", doc = "shown in skill lists (defaults to id)" },
{ "description", type = "string", doc = "one-line description, for UI and tooling" },
{ "kind", type = "string", values = { "active", "passive" }, default = "active",
doc = "an active skill is fired; a passive one is equipped" },
-- element, cooldown, power, icon, events, data unchanged
{ "range", type = "number", doc = "metres the skill reaches" }, -- new
})そのうえで再生成し、両方のファイルをコミットします。
lua5.4 tools/gen-types.lua
git add Scripts/palforge/api/skill.lua Scripts/palforge/types.luaクラスは宣言順に出力されるので新しい行は最後に並び、range は Skill{ ... } に受け付けられる
のと同時に、エディタからも候補に出ます。
---@alias Skill.Spec.Kind "active"|"passive"
---@class Skill.Spec
---@field id string # skill id: a game row id or "pack:name"
---@field name? string # shown in skill lists (defaults to id)
---@field description? string # one-line description, for UI and tooling
---@field kind? Skill.Spec.Kind # an active skill is fired; a passive one is equipped (default active)
---@field element? string # attribute / element (fire, water, ...). AUTHOR METADATA: stored and handed back, read by nothing
---@field cooldown? number # seconds between activations (enforced by :activate)
---@field power? number # base power / magnitude. AUTHOR METADATA: stored and handed back, read by nothing
---@field icon? string # /Game/... texture path used when the icon DataTable has no row for this id
---@field events? Skill.Spec.Events # behaviour handlers (grouped)
---@field data? table # free-form payload of your own, carried onto the definition
---@field range? number # metres the skill reaches宣言済みのすべての形を UE4SS のログに出す
エディタから離れているときや、原因を探しに来たエラーのすぐ隣にフィールド一覧が欲しいときに 使えます。
local schema = require("palforge.core.schema")
local event = require("palforge.core.event")
local log = require("palforge.utils.log").scope("mypack")
event.on("world.ready", function()
for _, spec in ipairs(schema.all()) do
log.info(spec.name .. " (" .. #spec.fields .. " fields)")
end
log.info(schema.help("Building.Spec"))
end)各行は [PalForge.mypack][info] ... の形で出ます。全部を出さずに 1 つの形だけを見たいときは、
フィールドのディスクリプタを自分で走査します。
local schema = require("palforge.core.schema")
for _, f in ipairs(schema.get("Effect.Spec").fields) do
print(string.format("%-12s %-8s %s%s",
f.name,
f.type or "any",
f.required and "required " or "",
f.doc or ""))
endまとめ
.luarc.jsonのworkspace.libraryに PalForge のScriptsフォルダを足すと、すべてのX{ ... }がフィールドを補完し、ゲームが照合するのと同じ説明を見せてくれます。- UE4SS の名前(
FindFirstOf、LoadAsset、RegisterHookなど)をdiagnostics.globalsに 並べると、エディタが未定義扱いするのをやめます。 - 補完を運んでいるのは
Scripts/palforge/types.luaです。手で編集せず、スペックを変えたらlua5.4 tools/gen-types.luaを実行し、両方のファイルをコミットします。 - エディタが出すフィールドはゲームが受け付けるフィールドです。どちらも
Scripts/palforge/api/*.luaの同じスペック宣言から来ているからです。 - エディタから離れていても、
schema.help("Pal.Spec")が同じ一覧をゲーム内に出しますし、 検証エラーは必ずフィールド名と有効な選択肢を教えてくれます。
次は フィールドと検証 がディスクリプタの各キーとそこから出るエラーを、 Pal や Mesh などのドメインごとのページが各フィールドの意味を、 ライフサイクルとイベント が実際に発火するハンドラを説明します。