4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Minecraftサーバ開発・運営Advent Calendar 2022

Day 5

PocketMine-MPで任意コードを実行して遊ぼう!(色々と役に立つ EvalBook のご紹介)

Last updated at Posted at 2022-12-04

EvalBook ってなに?

PocketMine-MP サーバーの中で書いたコードを即座に実行できるプラグインです。
名前の通り eval() 関数を使って、任意コード実行を実現しています。

どうして使うの?

皆さん、一度はこんなことを思ったことがあるのではないでしょうか。

  • デバッグ用のコードを書き直すたびにサーバーを再起動するのがめんどくさい
  • 小さな機能を実装して確認するのにいちいちプラグインを作るのがめんどくさい
  • Minecraftの中でコードが書けて実行できればいいのになぁ…

EvalBook はそんなあなたのお悩みをすべて解決します!

EvalBook の導入

EvalBook のダウンロード

EvalBook の最新リリース から EvalBook_vX.Y.Z.phar をクリックしてダウンロードします。
ダウンロードしたファイルは、plugins フォルダーに入れましょう。

リソースパックの導入 (任意)

必須ではありませんが、EditerBook リソースパック を導入すると、本の編集画面が大きくなります。

EvalBook の権限設定

サーバー起動後、plugin_data/EvalBook/allowlist.txt に自分のゲーマータグを追加します。
コンソールから /evalbook reload を実行することで、コマンドが使用できるようになります。

コードを書いてみよう

※この記事に掲載されているコードは特に記載がない場合はCC0(パブリックドメイン)です。

前提知識

EvalBook では、コードを書く際の煩わしさを低減するために、ほぼすべてのインポート文があらかじめ記載されています。また、いくつかの変数や関数も用意されています。
例えば、本を実行したプレイヤーが $_player という変数に代入されたりします。
その他の詳細な仕様については、EvalBook の README.md を参照してください。

ハロー ワールド

まずは、プレイヤーに「Hello World!」と送信してみましょう。
メッセージを送信するだけなので、以下のようなコードになります。

$_player->sendMessage("Hello World!");

本を実行してみよう

本に書かれたコードを実行するには、スニークをしながら、実行したい本をドロップします。
先ほどのコードを実行してみると、プレイヤーに「Hello World!」と送信されましたね。
image.gif

イベントをリッスンする

普通のプラグインでは、イベントをリッスンするコードは以下のように書きますよね?

Main.php
class Main extends PluginBase{
	protected function onEnable() : void{
		$this->getServer()->getPluginManager()->registerEvents(new EventListener(), $this);
	}
}
EventListener.php
class EventListener implements Listener{
	public function onJump(PlayerJumpEvent $event) : void{
		$event->getPlayer()->sendMessage("ジャンプしたよ!");
	}
}

しかし、EvalBook でこのようなコードを書いてしまうと、サーバーがクラッシュしてしまいます。
(クラスを2回定義しようとするとエラーが発生するためです。)

そこで、EvalBook では、無名クラスを使ってクラスを生成し、それを PluginManager へ渡します。

$listener = new class() implements Listener{
	public function onJump(PlayerJumpEvent $event) : void{
		$event->getPlayer()->sendTip("ジャンプしたよ!");
	}
};
$this->getServer()->getPluginManager()->registerEvents($listener, $this);

無事に PlayerJumpEvent をリッスンすることができました。
image.gif

イベントのリスニングを解除する

最初から完璧なコードが書ければよいのですが、人間は間違えるものです。
一度登録したイベントリスナーを解除したいときもありますよね。
そのようなときには、HandlerListManager->unregisterAll() 関数を呼び出しましょう。

このコード例では、すでにイベントがリッスンされているときに、イベントのリスニングを解除します。

if(isset($this->listener)){
	HandlerListManager::global()->unregisterAll($this->listener);
	unset($this->listener);
	$_player->sendMessage("イベントのリスニングを解除しました。");
	return;
}

$this->listener = new class() implements Listener{
	public function onToggleSneak(PlayerToggleSneakEvent $event) : void{
		$player = $event->getPlayer();
		$explosion = new Explosion($player->getPosition(), 2, $player);
		$explosion->explodeA();
		$explosion->explodeB();
	}
};
$this->getServer()->getPluginManager()->registerEvents($this->listener, $this);

スニーク状態を切り替えるたびに爆発するため、実行する際は気を付けてください。
image.gif

ショーケース

ここからは、EvalBook を使った様々な作品を紹介します。

ラングトンの村人

ラングトンのアリ の村人バージョンです。

ラングトンの村人.php
/**
 * Copyright (c) 2022 ねらひかだ. All rights reserved.
 * This work is licensed under the terms of the MIT license.
 * For a copy, see <https://opensource.org/licenses/MIT>.
 */

$colors = DyeColor::getAll();unset($colors["WHITE"]);$colors = array_values($colors);
$white = VanillaBlocks::CONCRETE()->setColor(DyeColor::WHITE());
$black = VanillaBlocks::CONCRETE()->setColor(DyeColor::BLACK());

$algo = function(Living $entity) use ($colors, $white, $black) : void{
	$location = $entity->getLocation();
	$down = $location->getSide(Facing::DOWN);
	$isWhite = $entity->getWorld()->getBlock($down, true, false)->isSameState($white);

	$entity->setRotation($location->getYaw() + ($isWhite ? 90 : -90), $location->getPitch());

	$colored = clone $black;
	$colored->setColor($colors[$entity->getId() % count($colors)]);
	$entity->getWorld()->setBlock($down, $isWhite ? $colored : $white, false);
	
	$to = $entity->getDirectionPlane()->round();
	$entity->teleport($location->add($to->getX(), 0, $to->getY()));
};

$world = $_player->getWorld();
$main = function() use ($world, $algo) : void{
	foreach($world->getEntities() as $entity){
		if($entity instanceof Villager){
			$algo($entity);
		}
	}
};

if(isset($this->H5kk4Aup)){
	$this->H5kk4Aup->cancel();
	unset($this->H5kk4Aup);
	$_player->sendMessage("The task has been canceled!");
	return;
}
$this->H5kk4Aup = $this->getScheduler()->scheduleRepeatingTask(new ClosureTask($main), 20);

ソーヴァultみたいなやつ

VALORANTというゲームに登場するエージェントの一人「ソーヴァ」というキャラクターのULTを再現したものです。

image.gif

ソーヴァultみたいなやつ.php
/**
 * Copyright © 2022 Lyrica0954
 * This work is free. You can redistribute it and/or modify it under the
 * terms of the Do What The Fuck You Want To Public License, Version 2,
 * as published by Sam Hocevar. See http://www.wtfpl.net/ for more details.
 */

$utils = new class() {
	public static function drawParticle(Player $player, Vector3 $pos, string $name) {
		$pk = SpawnParticleEffectPacket::create(DimensionIds::OVERWORLD, -1, $pos, $name, "");
		$player->getNetworkSession()->addToSendBuffer($pk);
	}
	public static function playSound(Player $player, Vector3 $pos, string $name, float $pitch = 1.0, float $volume = 1.0) {
		$pk = PlaySoundPacket::create($name, $pos->x, $pos->y, $pos->z, $volume, $pitch);
		$player->getNetworkSession()->sendDataPacket($pk);
	}
	public static function getLinePos(Vector3 $start, Vector3 $end, float $ppb = 3): array {
		$subt = $end->subtractVector($start);

		$div = 1 + ($subt->length() * $ppb);
		$step = $subt->divide($div);
		$positions = [];
		for ($i = 0; $i <= $div; $i++) {
			$positions[]=$start->addVector($step->multiply($i));
		}

		return $positions;
	}
};

$shootTask = new class($_player, $utils) extends Task {
	public function __construct(Player $player, $utils) {
		$this->player = $player;
		$this->utils = $utils;
		$this->tick = 0;
		$this->aTick = 0;
		$this->pTick = 0;
	}
	public function onRun(): void {
		$this->tick++;
		$this->aTick++;
		if ($this->aTick >= 0) {
			$p = 0.8 + (($this->aTick / 20) * 0.5);
			foreach($this->player->getWorld()->getPlayers() as $player){
				$this->utils->playSound($player, $this->player->getPosition(), "beacon.activate", $p, 1.0);
			}
			$this->player->setMotion($this->player->getMotion()->multiply(0.8));
			$this->pTick++;

			if ($this->pTick >= 5) {
				$this->pTick = 0;

				$dir = $this->player->getDirectionVector();
				$t = $this->player->getPosition()->add(0, $this->player->getEyeHeight(), 0);
				$b = $t->addVector($dir->multiply(20));

				foreach ($this->utils->getLinePos($t, $b) as $pos) {
					foreach ($this->player->getWorld()->getPlayers() as $player) {
						$this->utils->drawParticle($player, $pos, "minecraft:sonic_explosion");
					}
				}
			}
		}

		if ($this->aTick >= 20) {
			$this->aTick = -20;
			$this->pTick = 0;
			$this->utils->playSound($this->player, $this->player->getPosition(), "random.anvil_land", 0.5, 0.5);
		}

		if ($this->tick >= 120) {
			$this->getHandler()->cancel();
		}
	}
};

$this->getScheduler()->scheduleRepeatingTask($shootTask, 1);

エメラルドがビューンってなってビューンって群がってくるやつ

エメラルドが経験値みたいに群がってきたら面白そうだなぁ、と思って作ったものです。

クラスの定義とかを1ページに収めるために、ソースコードを1行に詰めてます。見づらいですね。

エメラルドがビューンってなってビューンって群がってくるやつ.php
/**
 * Copyright (c) 2022 ねらひかだ. All rights reserved.
 * This work is licensed under the terms of the MIT license.
 * For a copy, see <https://opensource.org/licenses/MIT>.
 */
$ItemEntity=new class($_player->getLocation(),VanillaItems::BOW())extends ItemEntity{function isMergeable(ItemEntity$_):bool{return false;}function tryMergeInto(ItemEntity$_):bool{return false;}function canSaveWithChunk():bool{return false;}function onCollideWithPlayer(Player$P):void{if($this->getPickupDelay()!==0)return;$n=match(true){$P->getOffHandInventory()->getItem(0)->canStackWith($i=$this->getItem())&&$P->getOffHandInventory()->getAddableItemQuantity($i)>0=>$P->getOffHandInventory(),$P->getInventory()->getAddableItemQuantity($i)>0=>$P->getInventory(),default=>null};$e=new EntityItemPickupEvent($P,$this,$i,$n);if($P->hasFiniteResources()&&$n===null)$e->cancel();$e->call();if($e->isCancelled())return;$o=$this->location;$k=PlaySoundPacket::create("fall.amethyst_block",$o->x,$o->y,$o->z,0.7,1.5);foreach($this->getViewers()as$v){($s=$v->getNetworkSession())->sendDataPacket($k);$s->onPlayerPickUpItem($P,$this);}if(null!==$n=$e->getInventory())foreach($n->addItem($e->getItem())as$r)$this->getWorld()->dropItem($o,$r,new Vector3(0,0,0));$this->flagForDespawn();}function onUpdate(int$_):bool{$A=parent::onUpdate($_);if($this->pickupDelay!==0||null===$t=$this->getTargetEntity())return$A;$m=$t->getEyePos()->subtractVector($this->location)->normalize();$this->addMotion($m->x,$m->y,$m->z);return true;}};
$ItemEntity->kill();

$dropItem=function(Position$source,Item$item,?Vector3$motion=null,int$delay=10)use($ItemEntity):?ItemEntity{if($item->isNull())return null;$e=new$ItemEntity(Location::fromObject($source,$source->world,lcg_value()*360,0),$item);$e->setPickupDelay($delay);$e->setMotion($motion??new Vector3(lcg_value()*0.2-0.1,0.2,lcg_value()*0.2-0.1));$e->spawnToAll();return$e;};

$r=fn($m=-1000,$M=1000,$_=1000)=>mt_rand($m,$M)/$_;
for($i = 0; $i < 64; ++$i){
$entity = $dropItem($_player->getPosition(), VanillaItems::EMERALD(), new Vector3($r(), $r(0), $r()), 100);
$entity->setTargetEntity($_player);
}

EvalBook ってすごいな!

色々なコード(力作も含めて)を見てきましたが、これを書いてサクッと動作確認できるのはすごく便利ですよね。
便利すぎて普通にプラグインを作る工程がめんどくさく感じられてきます(おい)。
しかし、PHP: eval - Manual にも書いてある通り、eval() は危険な関数です。本番環境では使わないようにしましょう。
それでは、みなさんも良き EvalBook ライフを!

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?