PalForge
ガイド

パックが同梱できるもの

自分のフォルダから持ち込めるアセットと、参照しかできないアセットを経路ごとに整理する

このページでできるようになること

  • 何かを作り始める前に、自分のファイルのうちゲームが実際に受け取るものを把握する
  • 他人のマシンでも通るパスで OBJ モデルを同梱する
  • 実測済みの経路と、配線だけされている経路を見分ける
  • 同じ結果を今日得るためのバニラアセットを選ぶ

一覧表

PalForge のアセット経路と、それぞれが受け付けるものです。「自分のフォルダから」は自分の MOD フォルダに同梱したファイル、「バニラ」は Palworld 自身の pak にすでにクックされている /Game/... パスを指します。

アセット自分のフォルダから/Game/... パスから
スタティック / スケルタルメッシュ不可可。動作実績あり、クラス検査つき
プロシージャルジオメトリ — ディスク上の .obj対象外
テクスチャ — ディスク上の PNG。インポートしてキャッシュされます
サウンド不可可。1957 件のイベント名カタログ
マテリアル不可可。ロード済みのものを親にした動的インスタンス

2 つの「不可」は、フレームワークではなくエンジンについての記述として読んでください。それらの経路 が書きかけなのではなく、呼ぶべき関数が存在しません。

つまり今日の PalForge は ゲーム自身のアセットを再利用するフレームワークであり、動作を実際に 観測したディスク経路が 2 つ(OBJ ジオメトリと PNG テクスチャ)、呼ぶべき関数そのものが無い行が 3 つあります。モデルを作り始める前に知っておく価値のある事実です。

メッシュはバニラ経路のみ

Mesh.assets.load/ で始まらない文字列を拒否します。UE のオブジェクトパスはマウントポイント を根に持つので必ず / で始まり、Windows のパスは決してそうなりません。この 1 文字があるおかげで、 1 つの model フィールドが両方の文字列を運べて、各バックエンドがどちらを渡されたか言えます。

-- 動く: すでにゲームの pak に入っているパス
Building{ id = "mypack:Statue", mesh = { kind = "static", model = Mesh.assets.SM.ChestWood } }

-- ローダーが拒否する: 自前の .uasset はここからは届かない
Building{ id = "mypack:Statue", mesh = { kind = "static", model = "mypack/statue.uasset" } }

クックされたメッシュを実行時にインポートする経路は存在しないので、2 番目の書き方はパスの綴りを 変えても動きません。このビルドに実在すると分かっているパスの一覧は 参照できるバニラアセットにあります。

OBJ 経路と、それを同梱可能にしているもの

kind = "obj"kind = "procedural" と同じバックエンド)はディスクから Wavefront OBJ を読み、 Lua でパースして ProceduralMeshComponent を組み立てます。この連鎖は実測済みです。 AddComponentByClassCreateMeshSectionSetWorldScale3D の順です。

同梱可能にしている要素は、modeltexture のパスがそれを宣言した .lua ファイルからの 相対パスでよいことです。Mesh.Spec は検証時に両フィールドを utils.file.resolvePackPath に 通すので、パックは自分のコードの隣に自分のモデルを置いて配れます。

Scripts/mypack/marker.lua
-- Scripts/mypack/marker.obj がこのファイルの隣にある
Mesh{ id = "mypack:Marker", kind = "obj", model = "marker.obj", scale = 1.0 }

絶対パスはそのまま残ります。だからこそ、どの種類のパスか事前に知らなくても、仕様が持つすべての パスにこの規則を安全に適用できます。

別の方法でパスを組み立てる場合は、リゾルバを直接呼べます。

local file = require("palforge.utils.file")

file.packDir()                       -- これを呼んだ .lua ファイルのディレクトリ、または nil
file.resolvePackPath("marker.obj")   -- そのディレクトリ + "marker.obj"
file.resolvePackPath("C:/x/m.obj")   -- 変更なし: すでに絶対パス
file.isAbsolute("/Game/A/SK_X")      -- true。オブジェクトパスがディレクトリと連結されることはない

引数なしの packDir() は、PalForge 自身のツリーから外へ歩いて呼び出し元を見つけます。 Scripts/palforge/ の下にないソースを持つ最初のスタックフレームがパックです。だからネストの深さに 関係なく動きます。直接書いた Mesh{ ... }Pal{ ... } の中に入れ子にしたメッシュではスタック の深さが違い、固定のフレーム数ではどちらかが必ず外れます。

nil を返して相対パスを相対のまま残すケースが 2 つあります。文字列チャンクや C から行われた定義と、 PalForge 自身のテストスイートの中から行われた定義です。相対のままなら io.open があなたが書いた 文字列そのもので失敗するので読み取れます。推測したディレクトリと連結されたパスはそうなりません。

テクスチャは実測済み、ただし 1 枚ぶんの代償がある

Renderer.importTextureUKismetRenderingLibrary::ImportFileAsTexture2D(UObject* WorldContextObject, FString Filename) を呼びます。両引数とも出荷バイナリから読み取ったもので、ワールドコンテキストは本当にただの UObject* なのでアクターで条件を満たし、パスも本当に FName ではなく FString です。呼び出しは core.signature を通るので、この関数を宣言していないビルドではゲームに触れる前に拒否されます。

2026-08-02、ロード済みのセーブでこの呼び出しは実行され、答えが返りました。 パックが同梱した PNG は本物の Texture2D として返り(クラス連鎖は Texture2D : Texture : StreamableRenderAsset : Object なので、マテリアル側のセッタが宣言する UTexture* を満たします)、同じパスでの 2 回目は まったく同じ値を返しました。これはキャッシュにしかできません。直前に失敗したパスでも、ファイルを 置いたあとの呼び出しは成功しています。失敗は記憶されません。

この経路の代償は、パスごと・セッションごとに 1 つの UTexture2D が確保され、二度と解放されない ことです。UE4SS の Lua 層には破棄の呼び出しがなく、このプロセスの中の何にも手放せません。キャッシュ の本当の仕事はそこにあります。パルが湧くたびにアタッチされるメッシュのテクスチャでも、インポート は 1 回で済みます。

同じフィールドの /Game/... 側は証拠の面で強いほうです。LoadAssetStaticFindObject であり、 このツリーが毎セッション成功させている呼び出しです。texture は文字列の形で分岐するので、2 つの うち必ず片方だけが適用され、もう片方は試されません。

-- 実測済み: ゲームがすでに同梱しているテクスチャ
Mesh{ id = "mypack:Heli", model = Mesh.assets.SK.AttackHelicopter,
      texture = Mesh.assets.T.HelicopterBase }

-- 実測済み: 自前の PNG、このファイルからの相対パス
Mesh{ id = "mypack:Body", kind = "obj", model = "body.obj", texture = "body.png" }

インポートしたテクスチャはパス文字列そのものをキーに、成功だけをキャッシュします。同じアタッチを 繰り返してもインポートし直しません。

サウンドとマテリアル

Audio.Spec.soundFile定義時のハードエラーです。no-op でも警告でもありません。

PalForge: Audio: soundFile is not accepted (id "mypack:Theme", soundFile "theme.wav"). Custom
audio files do not play on this build: the UE-native route has no importer that survives
shipping (USoundWave declares none, USoundWaveProcedural declares nothing at all) and the
Wwise route rebinds media the cook already staged rather than reading a file. This is the open
item audio-custom-file-loader. ... Name a game sound instead: soundId =
"AKE_UI_Common_Menu_Close", or soundPath = the asset path from PalForge.native.audio.CATALOG.

フィールド自体は仕様に宣言されたまま残ります。エラーがその名前を出せるようにするためと、ツールが 一覧に出せるようにするためです。代わりに使うのはゲームのサウンドで、 PalForge.native.audio.CATALOG が Wwise の AkAudioEvent 名 1957 件をアセットパスつきで持って います。名前を指定するだけで十分です。soundId があり soundPath がない定義には、lowering の 過程で実際のパスが付きます。

マテリアルも答えの形は同じです。パックはマテリアルを同梱できません。できるのは、コンポーネント上に 動的マテリアルインスタンスを作り、すでにロードされているマテリアルを親にして、そこにパラメータを 書き込むことです。Mesh.assets.MI はそのための実測済みマテリアルインスタンスパスを 1 件持って いて、メッシュ仕様の material がその置き場所です。

Palworld のマテリアルが応答するパラメータは動作中のゲームから読み取ったものです (BaseColorBase TextureNormal Map など)。書き込みも宣言どおりに呼ばれています。そして 2026-08-02 に mesh-color-change を人が見ている前で走らせたところ、チェストが赤 → 緑 → 青と 変わりました。この経路は端から端まで実測済みです。

まとめ

  • メッシュ、サウンド、マテリアルは自分のフォルダからは持ち込めません。/Game/... パスを指してください。
  • .obj は持ち込めます。modeltexture の相対パスは、それを宣言した .lua ファイルを基準に解決されます。
  • 自分で組み立てるパスには file.packDir()file.resolvePackPath() が同じ規則を提供します。
  • PNG テクスチャはインポートでき、パスごとにキャッシュされます。パス 1 つにつきセッションのあいだ生き続けるテクスチャが 1 枚ぶん増えます。
  • soundFile は、鳴っていた音を黙らせる代わりに定義時に例外を投げます。
  • 各行のバニラ側はすべて実在し、バニラアセットに既知のものが並んでいます。

On this page