Luaスクリプティング:フックとライフサイクル
このページでは、Luaパワーが定義できるすべてのフック、フックの実行順序、各ボスが独自の分離されたランタイムを取得する方法、およびサンドボックス内で利用可能な標準ライブラリ関数について説明します。
まだLuaパワーを書いたことがない場合は、まずはじめにから始めてください。
このページでは、plugins/EliteMobs/powers/内のボスLuaパワーのフックについて説明します。NPC Luaスクリプトは独自のplugins/EliteMobs/npc_scripts/フォルダと、on_npc_interactやon_npc_proximity_enterなどのNPC固有のフックを使用します。NPCスクリプトを参照してください。
フックリファレンス
すべてのLuaパワーファイルはテーブルを返します。そのテーブルの各キー(api_versionとpriorityを除く)は、以下にリストされているフックのいずれかでなければなりません。ランタイムは、対応するゲームイベントが発生するたびに、一致する関数を呼び出します。
| フック | 発火条件 | context.player の有無 |
|---|---|---|
on_spawn | エリートMobがスポーンしたとき | いいえ |
on_game_tick | ランタイムクロックが有効で一時停止していない間、サーバーティックごとに1回。20 TPSでは通常50 ms間隔 | いいえ |
on_boss_damaged | ボスが任意のソースからダメージを受けたとき | いいえ |
on_boss_damaged_by_player | ボスがプレイヤーからダメージを受けたとき | はい |
on_reinforcement_damaged_by_player | このボスが召喚した増援がプレイヤーからダメージを受けたとき。召喚者のパワーで実行 | はい |
on_boss_damaged_by_elite | ボスが別のエリートMobからダメージを受けたとき | いいえ |
on_player_damaged_by_boss | プレイヤーがこのボスからダメージを受けたとき | はい |
on_enter_combat | ボスが戦闘に入ったとき | はい |
on_exit_combat | ボスが戦闘を離れたとき | いいえ |
on_heal | ボスが回復したとき | いいえ |
on_boss_target_changed | 初回の獲得を含むプレイヤー対象イベントをEliteMobsが通知したとき。すべての対象変更や対象喪失を通知するわけではない | はい |
on_death | ボスが死亡したとき | いいえ |
on_phase_switch | フェーズボスが新しいフェーズに切り替わったとき | いいえ |
on_mind_action | ネイティブ Mind が装着されたパワーにアクションを要求したとき。アクションのコンテキストを参照 | いいえ |
on_zone_enter | エンティティが監視中のゾーンに入ったとき | はい(エンティティがプレイヤーの場合) |
on_zone_leave | エンティティが監視中のゾーンから出たとき | はい(エンティティがプレイヤーの場合) |
context.playerが「いいえ」とリストされている場合、それにアクセスするとnilが返されます。使用する前に必ずnilチェックを行ってください。
on_boss_damaged と個別のダメージフックは別々のイベント経路です。1回の攻撃で両方が発火することがあり、排他的ではありません。対象フックはこのLuaイベントテーブルからキャンセルできず、以前の対象も通知しません。
on_zone_enter / on_zone_leave を発火させませんこの2つのフックはローダーに受け付けられ検証もされますが、Luaパワー内のいかなる処理もこれらを発火させることはできません。これらは EliteScript のゾーンリスナーからのみディスパッチされ、そのリスナーは同じボス上のYAMLの eliteScript: が Events: に ZoneEnterEvent または ZoneLeaveEvent を列挙したときに開始されます。Luaパワーしか持たないボスは、これらのフックを含むファイルを問題なく読み込んだうえで、決して呼び出しません。
Luaのウォッチャーは別の仕組みです。context.zones:watch_zone(...) と context.script:zone(...):watch(...) は、渡した on_enter / on_leave コールバックを直接呼び出し、これらのトップレベルフックを経由することはありません。YAMLのゾーンスクリプトと意図的に組み合わせるのでない限り、ウォッチャーを使用してください。
YAMLゾーンイベントは、そのボスの該当する全パワーに届きます。出入りするエンティティは含まれますが、発生元のゾーンやスクリプトの識別子はありません。複数のYAMLリスナーが同じエンティティについて複数回通知することがあります。
NPC Luaスクリプトは別です。そちらの context.zones はMagmaCoreのもので、watch は on_zone_enter / on_zone_leave を通知します。NPCスクリプトを参照してください。
典型的なマルチフックパワー
1つのLuaパワーは、必要なだけ多くのフックを定義できます。以下は、3つのフックを一緒に使用するスケルトンです:
return {
api_version = 1,
on_enter_combat = function(context)
-- Initialize per-fight state when combat begins
context.state.hit_count = 0
context.log:info("Combat started!")
end,
on_boss_damaged_by_player = function(context)
-- Track hits and trigger an ability every 5th hit
context.state.hit_count = (context.state.hit_count or 0) + 1
if context.state.hit_count % 5 ~= 0 then
return
end
if not context.cooldowns:check_local("counter_attack", 100) then
return
end
-- Fire a projectile back at the player
local origin = context.boss:get_location()
origin:add(0, 1, 0)
context.boss:summon_projectile(
"SMALL_FIREBALL", origin, context.player:get_location(), 1.5
)
end,
on_death = function(context)
-- Spawn an explosion particle on death
context.world:spawn_particle_at_location(
context.boss:get_location(), "EXPLOSION_EMITTER", 1
)
end
}
イベントデータ(context.event)
一部のフックは、フックをトリガーしたゲームイベントに関するデータを公開するcontext.eventテーブルを受け取ります。利用可能なフィールドは、どのフックが実行されているかによって異なります。
ダメージフック
on_boss_damaged、on_boss_damaged_by_player、on_reinforcement_damaged_by_player、on_boss_damaged_by_elite、on_player_damaged_by_bossに適用されます。
| フィールド / メソッド | 型 | 説明 |
|---|---|---|
event.damage_amount | double | イベントテーブル作成時のダメージ値 |
event.get_damage_amount() | double | それまでの変更を反映した現在のダメージを取得 |
event.damage_cause | string | SpigotのDamageCause名(例:"ENTITY_ATTACK"、"PROJECTILE") |
event.damager | エンティティテーブル | 元のイベントがエンティティによるダメージなら、Bukkitの直接の加害者。on_boss_damaged でも利用可能。投射物による命中では射手ではなく投射物自体。 |
event.projectile | entity table | ダメージを与えたものが発射物だった場合の発射物エンティティ。 |
event.set_damage_amount(n) | — | ダメージを固定値で上書きします |
event.multiply_damage_amount(n) | — | 現在のダメージをn倍します |
event.cancel_event() | — | ダメージイベントを完全にキャンセルします |
on_boss_damaged_by_player = function(context)
-- Halve all projectile damage
if context.event.damage_cause == "PROJECTILE" then
context.event.multiply_damage_amount(0.5)
end
end
event.set_damage_amount(n) と event.multiply_damage_amount(n) はドットで呼び出してください。この2つのsetterは最初の引数を直接読み、コロン呼び出しの self を除去しません。event:transfer_damage(target, amount) はコロンに対応しています。
プレイヤーからエリートへのダメージフックでは、event.entity が被害を受けたエリートを示します。on_reinforcement_damaged_by_player の context.boss は召喚者のままで、context.player は攻撃したプレイヤーです。フックは増援の元の変更可能なダメージイベントを受け取ります。
これらのフックには event.is_damage_transfer と event:transfer_damage(target, amount) もあります。対象は所有者のボスが作用させられる、別の生存中のエリートである必要があります。量は有限かつ非負です。イベントがキャンセル済み、量がゼロ、または転送が入れ子なら false を返します。有効な呼び出しはプレイヤーに帰属する別の攻撃を送り、true を返します。元の攻撃からダメージを減らさず、別のリスナーが新しい攻撃を許可する保証もありません。複製ではなく分割する場合は event.set_damage_amount(...) で元の値も変更してください。転送されたダメージを再帰的に転送することはできません。
スポーンフック
on_spawnに適用されます。
| フィールド / メソッド | 型 | 説明 |
|---|---|---|
event.spawn_reason | string | SpigotのSpawnReason名 |
event.cancel_event() | — | スポーンをキャンセルします |
死亡フック
on_deathに適用されます。
| フィールド / メソッド | 型 | 説明 |
|---|---|---|
event.entity | entity table | 死亡しつつあるエンティティ |
ゾーンフック
on_zone_enterおよびon_zone_leaveに適用されます。
| フィールド / メソッド | 型 | 説明 |
|---|---|---|
event.entity | entity table | ゾーンに入る、または出るエンティティ |
Lua製のゾーンウォッチャーはcontext.eventを埋めません。入る/出るエンティティをコールバックに直接渡します。
キャンセル可能なイベント(一般)
元のゲームイベントがキャンセル可能なフックは event.cancel_event() を公開します。on_game_tick、on_mind_action、通常のスケジューラコールバックでは context.event は nil です。on_heal などには元のイベントがありますが、このテーブルに回復専用のフィールドはありません。呼び出す前に公開されているメンバーを確認してください。
ボス専用のイベントテーブルには、共有ラッパーの cancel()、uncancel()、is_cancelled はありません。イベントのキャンセルは同じボスの後続パワーへの通知を止めますが、現在のLua関数からは抜けません。以降の処理を実行しない場合は return を追加してください。
エンティティテーブルのフィールドの全容については、ボスとエンティティを参照してください。ダメージ原因とスポーン理由の値については、列挙型と値を参照してください。
フックの実行順序
ボスのイベントディスパッチャーは、装着されたスクリプトパワーを実行順に処理します。Luaパワーの priority が位置を決めます。
- 値が低いものが先に実行されます(デフォルトは
0)。 - 同じ優先度ではボスへのパワー挿入順を維持します。異なる読み込み経路でも特定の順序になるとは限りません。
return {
api_version = 1,
priority = -10, -- runs before most other powers
on_boss_damaged_by_player = function(context)
-- Record this power's last attacker before its later hooks run
context.state.last_attacker = context.player.uuid
end
}
YAML EliteScriptパワーも同じディスパッチャーに優先度 0 で参加します。負のLua優先度ならその前、正ならその後です。これは1体のボスの1つのイベントの順序であり、別のBukkitイベントや予約タスクの順序ではありません。キャンセル済みイベントは処理を止めるため、すべてのパワーが受信するとは限りません。増援自身のパワーが処理した後、イベントがキャンセルされていない場合に限り、直接の召喚者へ増援ダメージフックが通知されます。
ランタイムモデル
パワーとボスの組ごとに1つのランタイム
装着された各Luaパワーは、ボスごとに独立したランタイムを持ちます。開始時にEliteMobsは読み込み済みのソースを新しいサンドボックスで評価し、返されたテーブルを保存します。ボスがデスポーンまたは削除されるとランタイムは終了します。2つのLuaパワーを持つボスには、独立した2つの状態テーブルと環境があります。
これは次のことを意味します:
- ファイル内のローカル変数・関数と定義したLuaグローバルは、そのボスのそのパワーのランタイム内だけで利用できます。
- 返されたテーブルのフック関数は、ボス間で共有されることは決してありません。
ローダーは戻り値のテーブルを検証するためにもファイルを一度評価します。ファイル直下の初期化はフックのコンテキストに依存させないでください。ティック専用パワーはクロックの開始に on_spawn を必要としません。ランタイムの初期化とスポーンコールバックは別の手順です。
状態の分離
各ランタイムの context.state は、同じボスの別パワーや、別ボスの同じパワーから分離されています。フック間でカウンター、フラグ、タイマーなどパワー固有のデータを保持するために使います。状態は有効なランタイムの間だけ保持され、デスポーンや再起動をまたいでディスクには保存されません。
return {
api_version = 1,
on_boss_damaged_by_player = function(context)
-- Each boss tracks its own enrage counter independently
context.state.enrage_hits = (context.state.enrage_hits or 0) + 1
if context.state.enrage_hits >= 20 then
context.boss:apply_potion_effect("SPEED", 200, 2)
end
end
}
スケジュールされたタスクの所有権
context.schedulerを通じて作成されたすべてのタスクは、それらを作成したランタイムによって所有されます。ボスがデスポーンすると:
- ランタイムが
shutdown()を呼び出します。 - 所有されているすべてのタスク(ワンショット(
run_after)と繰り返し(run_every)の両方)が自動的にキャンセルされます。 - すべてのゾーン監視がクリアされます。
- このパワーがまだ保持しているAI、認識、スケール、無敵の変更が復元され、このパワーが設定した回復状態が解除され、追尾火の玉のコントローラーが停止します。
ランタイムはボス削除時に、自分が所有するスケジューラタスクを片付けます。一部のJavaゲーム機能は独自の寿命を持つ別タスクを開始するため、その機能の説明を確認してください。通常のプレイ中でも、不要になった繰り返しタスクはキャンセルしてください。
return {
api_version = 1,
on_enter_combat = function(context)
local pulse_count = 0
local task_id
task_id = context.scheduler:run_every(20, function(tick_context)
pulse_count = pulse_count + 1
if pulse_count > 10 or not tick_context.boss.exists then
tick_context.scheduler:cancel_task(task_id)
return
end
tick_context.world:spawn_particle_at_location(
tick_context.boss:get_location(),
{ particle = "FLAME", amount = 20, speed = 0.1 }
)
end)
end
}
ティックごとのクロックの動作
Luaパワーインスタンスの内部ティッククロックは、パワーがon_game_tickフックを定義している場合にのみ実行されます。context.zones:watch_zone(...)またはcontext.script:zone(...):watch(...)を通じて作成されたゾーン監視は、パワーのトップレベルのon_game_tickフックを有効にする代わりに、独自の所有する繰り返しタスクを作成します。
on_game_tick もゾーンウォッチャーもなければ、ランタイムはそのティッククロックを登録しません。明示的なスケジューラタスクやゲーム機能が開始するタスクは、それぞれのスケジュールで動きます。ティック処理と所有するゾーン監視タスクは、ボスのデスポーンやランタイム終了時にキャンセルされます。
ランタイムを一時停止すると、Lua状態と所有タスクは保持したまま処理を省略します。ボスのイベント経路はパワーの抑制も確認します。ボスの削除やランタイムの終了では、一時停止ではなく所有する処理を終了します。
エラーとパフォーマンスの動作
EliteMobsはLuaパワーに対して厳格なエラーおよびパフォーマンス制限を課します:
例外
フック関数またはスケジュールされたコールバックがLuaエラーをスローした場合(またはAPI呼び出しからJava例外が表面化した場合)、そのボスインスタンスに対してパワーは即座に無効化されます。ランタイムはシャットダウンされ、所有されているすべてのタスクがキャンセルされます。
失敗したインスタンスは閉じたままで、後続のダメージフックが自動で作り直すことはありません。定義を修正して再読み込みし、新しいランタイムを作成してください。所有者による明示的な解体で古い参照が消去され、その後の本体再構築で作り直される場合もあります。
エラーはパワーのファイル名と実行中のフックとともにコンソールへ記録されます。取得できる場合は行番号も含まれます。
[Lua] Error in 'frost_cone.lua' at line 35 during 'on_boss_damaged_by_player':
[Lua] -> ...explanation of what went wrong...
[Lua] -> Script has been disabled for this entity to prevent further errors.
実行バジェット
Luaパワーは、実行後に計測するだけでなく、Lua VMの内部で強制される厳格な実行バジェットの下で動作します。すべてのフック、すべてのスケジュール済みコールバック、そしてファイルの初回評価さえも、このバジェット内で実行されます:
| 制限 | 値 |
|---|---|
| エントリポイントごとのスレッド CPU 時間 | 50 ms |
| CPU 時間を測定できない場合の経過時間 | 250 ms |
| エントリポイントごとのバイトコード命令数 | 250,000 |
VM は命令数を数え、1024命令ごとに時間を確認します。エラーには超過した上限が表示されます。
Lua instruction budget exceeded (250000 instruction limit)
Lua CPU-time budget exceeded (50ms current-thread CPU limit)
Lua elapsed-time fallback budget exceeded (250ms fallback; current-thread CPU time unavailable)
無限Luaループは実行途中で中断されます。ネストした呼び出しは同じエントリポイントのバジェットを共有します。VMは実行中のJava API呼び出しを中断できません。呼び出し後に実経過時間について別の50 ms制限を適用することもありません。ワールドに対する高負荷な処理が安全になるわけではありません。
バジェット内に収めるには:
- フック内で境界のないループを避けてください。
context.scheduler:run_every(...)を使用して、処理を複数のティックに分散させてください。 on_game_tickハンドラは軽量に保ってください。これらは毎ティック実行されます。- 重い初期化処理は毎ティック繰り返すのではなく、
on_spawnやon_enter_combatに移してください。 - 250,000命令を使い切る典型的な原因は、広いゾーンにわたるパーティクルのループです。
coverageを使ってサンプルする位置を減らしてください。
Luaサンドボックス
Luaパワーはサンドボックス化されたLuaJ環境内で実行されます。ファイルシステムやJavaランタイムにアクセスできるいくつかのグローバルは削除されています。
削除されたグローバル
以下の標準Luaグローバルはnilに設定されており、使用できません:
| 削除対象 | 理由 |
|---|---|
debug | 内部VM状態を公開する |
dofile | ファイルシステムアクセス |
io | ファイルシステムアクセス |
load | 任意のコード読み込み |
loadfile | ファイルシステムアクセス |
luajava | 直接のJavaクラスアクセス |
module | モジュールシステム(不要) |
os | オペレーティングシステムアクセス |
package | モジュールシステム(不要) |
require | モジュールシステム / ファイルシステムアクセス |
VMのデバッグフックは内部的に今も設置されています -- それが実行バジェットが命令を数える仕組みです -- が、その直後に debug テーブル自体が nil に設定されるため、スクリプトからは到達できません。
利用可能な標準ライブラリ
サンドボックスにはLuaJの標準ライブラリが残っており、以下のよく使われる関数を利用できます。
| 分類 | 関数 |
|---|---|
| Math | math.abs、math.ceil、math.floor、math.max、math.min、math.random、math.sin、math.cos、math.sqrt、math.pi、およびその他すべてのmath.*関数 |
| String | string.byte、string.char、string.find、string.format、string.gsub、string.len、string.lower、string.match、string.rep、string.sub、string.upper、およびその他すべてのstring.*関数 |
| Table | table.insert、table.remove、table.sort、table.concat、およびその他すべてのtable.*関数 |
| イテレータ | pairs、ipairs、next |
| 型 | type、tostring、tonumber、select、unpack |
| エラー処理 | pcall、xpcall、error、assert |
| その他 | print、rawget、rawset、rawequal、rawlen、setmetatable、getmetatable |
文字列は、ロックされた1つのメタテーブルを共有します。getmetatable("")はfalseを返し、("text"):upper()のような文字列に対するメソッド呼び出しは、スクリプトが自身のstringテーブルに関数を追加または置き換えた後でも、常に標準の文字列関数を使用します。
print はコンソールへ出力しますが、context.log:info(msg) または context.log:warn(msg) を推奨します。ボスのログにはパワーのファイル名が自動で付きません。複数のパワーが同時に記録する場合は、メッセージにファイル名を含めてください。
emヘルパー名前空間
emテーブルはファイルロード時(どのフックが実行されるよりも前)に利用可能です。API全体で使用される位置テーブル、ベクトルテーブル、ゾーン定義を構築するためのヘルパーコンストラクターを提供します。
| 関数 | 用途 |
|---|---|
em.create_location(x, y, z [, world, yaw, pitch]) | オプションのワールド名、ヨー、ピッチを持つ位置テーブルを作成します |
em.create_vector(x, y, z) | ベクトルテーブルを作成します |
em.zone.create_sphere_zone(radius) | 球ゾーン定義を作成します |
em.zone.create_dome_zone(radius) | ドームゾーン定義を作成します |
em.zone.create_cylinder_zone(radius, height) | 円柱ゾーン定義を作成します |
em.zone.create_cuboid_zone(x, y, z) | 直方体ゾーン定義を作成します |
em.zone.create_cone_zone(length, radius) | 円錐ゾーン定義を作成します |
em.zone.create_static_ray_zone(length, thickness) | 静的レイゾーン定義を作成します |
em.zone.create_rotating_ray_zone(length, point_radius, animation_duration) | 回転レイゾーン定義を作成します |
em.zone.create_translating_ray_zone(length, point_radius, animation_duration) | 平行移動レイゾーン定義を作成します |
em の em.location には共有の読み取り専用ワールドクエリ is_in_dungeon、is_protected、owned_by、owners、kinds_at、has_kind があります。第1引数は位置テーブルです。位置がない、テーブルではない、ワールドがない、または未ロードの場合は false か空のテーブルを返します。省略された座標はゼロになりますが、座標の型が不正ならエラーになることがあります。これらのクエリ用の位置にはワールドを明示してください。シグネチャと戻り値はLua APIリファレンスを参照してください。
-- Skip a destructive mechanic inside claims or protected regions
if em.location.is_protected(context.player:get_location()) then return end
建築権限のチェックに対応するLuaバインディングはありません。em.location.is_protected は、いずれかの保護プラグインがその位置を主張しているかどうかを教えるだけで、プレイヤーごとの建築権限を解決するものではありません。
ゾーンビルダーは、:set_center(loc)(またはゾーンタイプに応じて:set_origin(loc) / :set_destination(loc))を持つチェーン可能なテーブルを返します。これらはファイルの先頭やフック内で使用するように設計されています:
-- At file scope: create a reusable zone shape
local blast_zone = em.zone.create_sphere_zone(5)
return {
api_version = 1,
on_boss_damaged_by_player = function(context)
-- Anchor the zone to the boss's current location at call time
blast_zone:set_center(context.boss:get_location())
local entities = context.zones:get_entities_in_zone(blast_zone)
for i = 1, #entities do
if entities[i].is_player then
entities[i]:apply_potion_effect("SLOWNESS", 60, 1)
end
end
end
}
ゾーンの形状、フィルター、ウォッチャー、ターゲティングパターンの完全な内訳については、ゾーンとターゲティングを参照してください。
次のステップ
- ボスとエンティティ --
context.boss、context.player、エンティティラッパー - ワールドと環境 -- パーティクル、サウンド、スポーン、
context.world - ゾーンとターゲティング -- ネイティブゾーン、スクリプトユーティリティ、
context.zones/context.script - 例とパターン -- 学習して応用できる完全に動作するパワー
- 列挙型と値 -- すべての文字列定数のSpigot Javadocリンク
