メインコンテンツまでスキップ

Luaスクリプティング:フックとライフサイクル

webapp_banner.jpg

このページでは、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イベントテーブルからキャンセルできず、以前の対象も通知しません。

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_amountdoubleイベントテーブル作成時のダメージ値
event.get_damage_amount()doubleそれまでの変更を反映した現在のダメージを取得
event.damage_causestringSpigotのDamageCause名(例:"ENTITY_ATTACK"、"PROJECTILE")
event.damagerエンティティテーブル元のイベントがエンティティによるダメージなら、Bukkitの直接の加害者。on_boss_damaged でも利用可能。投射物による命中では射手ではなく投射物自体。
event.projectileentity 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_reasonstringSpigotのSpawnReason名
event.cancel_event()—スポーンをキャンセルします

死亡フック​

on_deathに適用されます。

フィールド / メソッド型説明
event.entityentity table死亡しつつあるエンティティ

ゾーンフック​

on_zone_enterおよびon_zone_leaveに適用されます。

フィールド / メソッド型説明
event.entityentity 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を通じて作成されたすべてのタスクは、それらを作成したランタイムによって所有されます。ボスがデスポーンすると:

  1. ランタイムがshutdown()を呼び出します。
  2. 所有されているすべてのタスク(ワンショット(run_after)と繰り返し(run_every)の両方)が自動的にキャンセルされます。
  3. すべてのゾーン監視がクリアされます。
  4. このパワーがまだ保持している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の標準ライブラリが残っており、以下のよく使われる関数を利用できます。

分類関数
Mathmath.abs、math.ceil、math.floor、math.max、math.min、math.random、math.sin、math.cos、math.sqrt、math.pi、およびその他すべてのmath.*関数
Stringstring.byte、string.char、string.find、string.format、string.gsub、string.len、string.lower、string.match、string.rep、string.sub、string.upper、およびその他すべてのstring.*関数
Tabletable.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
}

ゾーンの形状、フィルター、ウォッチャー、ターゲティングパターンの完全な内訳については、ゾーンとターゲティングを参照してください。


次のステップ​