A sample text widget

Etiam pulvinar consectetur dolor sed malesuada. Ut convallis euismod dolor nec pretium. Nunc ut tristique massa.

Nam sodales mi vitae dolor ullamcorper et vulputate enim accumsan. Morbi orci magna, tincidunt vitae molestie nec, molestie at mi. Nulla nulla lorem, suscipit in posuere in, interdum non magna.

Sponge Pluginを作る

初めに

logo-spongepowered
SpongePowerd(https://www.spongepowered.org/)のプラグインを作り始めてみましたので、そのメモです。とても基本的なことしか書いてないので、詳しくは後述の公式プラグインドキュメント、あるいはJavadocなどを読んで下さい。

※この文書は時々アップデートします…更新日:2017/04/03:その他サンプルを追記。

MinecraftでForge Mod+Bukkit(spigot)プラグインサーバを建てるとなるとCauldron系サーバが主流ですが、これはMinecraft1.7.10と古いバージョンしか対応してません。1.8以降ではまだまだModは少ないですが、マルチサーバを運用するならSpongePowerd(たぶん正しくはスポンジでいいと思いますが、食器洗いのスポンジが出てくると思うので、ここではこう書いてます)を用いることになると思います。

で、Sponge ForgeであればMinecraftとForgeのバージョンさえ有ってれば既存のModが動きますが、PluginについてはBukkit/Spigotプラグインはそのままでは動きません(一応、不完全ながら動かすmodはあるので、それで代用しても良いですが)。

なのでSponge用プラグインを作成しないとなのですが、国内に情報が非常に少ないので、とりあえず足がかりレベルの情報をここに纏めたいと思います。
※ちなみに個人的にSpigotプラグインも作成してたので、その経験を元にSpigotプラグインとの比較もちりばめてますのでご了承を。

ちなみにこのページは以下の環境を前提に書いてます。多少環境が違ったり、バージョンが違ってもある程度は大丈夫だとは思いますが、一応ご注意下さい。

※SpongeAPIはこの文書を書いている間に徐々に4.1.0から5.0.0へ移行しているようです。なので、参考にしている情報もごちゃ混ぜなので、わかる範囲で注釈は入れますがご注意下さい。
※本ページはSpongeForgeサーバ環境をベースに記述しています。SpongeVanillaサーバ環境だと少し違う動きをしそうな気がします、、、。

Minecraft/Sponge 1.10.2
SpongeAPI 4.1.0 or 5.0.0
Forge 12.18.2.2098
JavaSE 1.8.0.92
Eclipse+Maven

 

参考情報

サーバについておさらい

おさらいというか、今まで書いたことが無いですが、、、

サーバコマンド
https://docs.spongepowered.org/master/ja/server/spongineer/commands.html

  • /helpコマンドなどは、マイクラ画面上だと次のページへなどはクリックで行けますが、コンソールの場合は「/page next」などのコマンドを使います。
  • /sponge plugins…プラグインの一覧
  • /sponge plugins プラグイン名…プラグインの詳細

前提知識?

私自身、Javaは趣味レベルで使ってるので、Javaの知識はしっかりしてないです。なので、コードを書いてて「何これ?」って思ったことを書いておきます。

  • アノテーション
    @Pluginとか@Injectとか。Spigotプラグインを作ってた人なら@Listenerとかも使ってるかもしれない。非推奨のメソッドを使う時にコンパイルエラーを出さないようにするための@SuppressWarnings、メソッド等をオーバライドする@Overrideなどもこれ。
  • @Inject
    SpongePluginのサンプルに多く登場しますが、Sponge特有の物では無いです。どうやら定義済みのクラスをnewして組み込むのを1行で済ませてしまうという物らしい。
    private Logger logger = new Logger();
    が
    @Inject
    private Logger logger;
    と書き換えられるみたいな?ちゃんと調べるとそれだけじゃ無いので興味のある人は自分で調べて貰えればいいけど、要するに使う側にとっては、リファレンスに書いてあるのをパクッて「おまじない」として書けばいいらしい。
  • Java8独自の…
    各所のサンプルを見るとわかりますがチャットコマンドとかにはラムダ式の記法が考慮されてますね。あと、Optionalを意識したメソッドも相当数あります。詳しく書くと大変なことになるので、ここでは省略しますが、1.7に慣れてる人はちょっと大変です。

下地作成

開発環境作成の説明はバサっと省きます。環境やプロジェクトの作成方法は、spigotプラグインとほとんど変わらないので、わからない場合は「http://minecraftjp.info/modding/index.php/Plugin_Tutorial#.E3.83.97.E3.83.AD.E3.82.B8.E3.82.A7.E3.82.AF.E3.83.88.E3.81.AE.E4.BD.9C.E6.88.90」あたりを参考にして下さい。

先ずは、空のシンプルプロジェクトを作成します。私の環境はMavenを使ってるのでpom.xmlを以下のようにします

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <groupId>org.sample.sponge.tools</groupId>
 <artifactId>Tools</artifactId>
 <version>0.0.1-SNAPSHOT</version>
 <name>Tools</name>

 <repositories>
 <repository>
 <id>sponge</id>
 <url>https://repo.spongepowered.org/maven</url>
 </repository>
 </repositories>

 <dependencies>
 <dependency>
 <groupId>org.spongepowered</groupId>
 <artifactId>spongeapi</artifactId>
 <version>5.0.0</version>
 <scope>provided</scope>
 </dependency>
 </dependencies>
</project>

groupId、artifactId、nameは適時修正して下さい。repository、dependencyはこれを書けばSpongeAPIをダウンロードして構成してくれます。
※repositoryのURLが公式サイトでは「http://repo.spongepowered.org/maven」になってますが、API5からは「https://repo.spongepowered.org/maven」にしないとエラーになる場合があるようです。

次にプロジェクトにクラスを追加して以下のような感じで書いてみましょう。

package org.sample.sponge.tools;

import org.slf4j.Logger;
import org.spongepowered.api.event.Listener;
import org.spongepowered.api.event.game.state.*;
import org.spongepowered.api.plugin.Plugin;

import com.google.inject.Inject;

@Plugin(id = "tools", name = "Tools", version = "1.0")
public class Tools {
 @Inject
 private Logger logger;

 // 初期化
 @Listener
 public void onConstruction(GameConstructionEvent event) {
 this.logger.info("GameConstructionEvent");
 }

 @Listener
 public void onPreInitialization(GamePreInitializationEvent event) {
 this.logger.info("GamePreInitializationEvent");
 }

 @Listener
 public void onInitialization(GameInitializationEvent event) {
 this.logger.info("GameInitializationEvent");
 }

 @Listener
 public void onPostInitialization(GamePostInitializationEvent event) {
 this.logger.info("GamePostInitializationEvent");
 }

 @Listener
 public void onLoadComplete(GameLoadCompleteEvent event) {
 this.logger.info("GameLoadCompleteEvent");
 }

 // Running States
 @Listener
 public void onServerAboutToStart(GameAboutToStartServerEvent event) {
 this.logger.info("GameAboutToStartServerEvent");
 }

 @Listener
 public void onServerStart(GameStartingServerEvent event) {
 this.logger.info("GameStartingServerEvent");
 }

 @Listener
 public void onServerStated(GameStartedServerEvent event) {
 this.logger.info("GameStartedServerEvent");
 }

 // Stopping States
 @Listener
 public void onServerStopping(GameStoppingServerEvent event) {
 this.logger.info("GameStoppingServerEvent");
 }

 @Listener
 public void onServerStopped(GameStoppedServerEvent event) {
 this.logger.info("GameStoppedServerEvent");
 }

}

クラス名は結構なんでもいいみたいです。大切なのは「@Plugin」です。内容は後述のメタデータを参照のこと。

ログへの出力は以下の記載が関係しています。

~
import org.slf4j.Logger;
~
@Inject
private Logger logger;
~
this.logger.info("GameConstructionEvent");
~

spigotプラグインの時はロガーのimportクラスはjava.util.logging.Loggerでしたが、spongeはorg.slf4j.Loggerらしいのでご注意を。

@Listenerに続くのはライフサイクルと言われてて、プラグインが読み込み~サーバ停止までの特定のタイミングでCallされます。spigotプラグインで言うところのonEnableやonDisableがさらに細かくなったと思えばOKです。引数があるという意味では、spigotプラグインのイベントハンドラにも似てますね。

だいたい下地はこれでOKでコンパイルして、できあがったjarファイルをSpigotサーバの「mods」フォルダへ格納しサーバを起動すれば読み込まれます。

[22:27:12] [main/INFO] [LaunchWrapper]: Loading tweak class name net.minecraftforge.fml.common.launcher.FMLServerTweaker
[22:27:12] [main/INFO] [LaunchWrapper]: Using primary tweak class name net.minecraftforge.fml.common.launcher.FMLServerTweaker
[22:27:12] [main/INFO] [LaunchWrapper]: Calling tweak class net.minecraftforge.fml.common.launcher.FMLServerTweaker
[22:27:15] [main/INFO] [FML]: Forge Mod Loader version 12.18.2.2098 for Minecraft 1.10.2 loading
[22:27:15] [main/INFO] [FML]: Java is Java HotSpot(TM) 64-Bit Server VM, version 1.8.0_92, running on Linux:amd64:3.10.0-327.28.3.el7.x86_64, installed at /usr/java/jdk1.8.0_92/jre
[22:27:15] [main/INFO] [FML]: Loading tweaker org.spongepowered.asm.launch.MixinTweaker from spongeforge-1.10.2-2098-5.0.0-BETA-1788.jar
[22:27:15] [main/INFO] [LaunchWrapper]: Loading tweak class name net.minecraftforge.fml.common.launcher.FMLInjectionAndSortingTweaker
[22:27:15] [main/INFO] [LaunchWrapper]: Loading tweak class name org.spongepowered.asm.launch.MixinTweaker
[22:27:15] [main/INFO] [mixin]: SpongePowered MIXIN Subsystem Version=0.5.13 Source=file:/***/mods/spongeforge-1.10.2-2098-5.0.0-BETA-1788.jar Env=SERVER
[22:27:16] [main/INFO] [mixin]: Compatibility level set to JAVA_8
[22:27:16] [main/INFO] [mixin]: Adding new token provider org.spongepowered.mod.SpongeCoremod$TokenProvider to MixinEnvironment[DEFAULT]
[22:27:16] [main/INFO] [mixin]: Adding new token provider org.spongepowered.mod.SpongeCoremod$TokenProvider to MixinEnvironment[PREINIT]
[22:27:16] [main/INFO] [mixin]: Adding new token provider org.spongepowered.mod.SpongeCoremod$TokenProvider to MixinEnvironment[INIT]
[22:27:16] [main/INFO] [LaunchWrapper]: Loading tweak class name net.minecraftforge.fml.common.launcher.FMLDeobfTweaker
[22:27:16] [main/INFO] [LaunchWrapper]: Calling tweak class net.minecraftforge.fml.common.launcher.FMLInjectionAndSortingTweaker
[22:27:16] [main/INFO] [LaunchWrapper]: Calling tweak class net.minecraftforge.fml.common.launcher.FMLInjectionAndSortingTweaker
[22:27:16] [main/INFO] [LaunchWrapper]: Calling tweak class net.minecraftforge.fml.relauncher.CoreModManager$FMLPluginWrapper
[22:27:18] [main/INFO] [FML]: Found valid fingerprint for Minecraft Forge. Certificate fingerprint e3c3d50c7c986df74c645c0ac54639741c90a557
[22:27:18] [main/INFO] [LaunchWrapper]: Calling tweak class net.minecraftforge.fml.relauncher.CoreModManager$FMLPluginWrapper
[22:27:18] [main/INFO] [LaunchWrapper]: Calling tweak class org.spongepowered.asm.launch.MixinTweaker
[22:27:21] [main/INFO] [mixin]: Initialised Mixin FML Remapper Adapter with net.minecraftforge.fml.common.asm.transformers.deobf.FMLDeobfuscatingRemapper@609640d5
[22:27:21] [main/INFO] [LaunchWrapper]: Calling tweak class net.minecraftforge.fml.common.launcher.FMLDeobfTweaker
[22:27:22] [main/INFO] [LaunchWrapper]: Loading tweak class name net.minecraftforge.fml.common.launcher.TerminalTweaker
[22:27:22] [main/INFO] [LaunchWrapper]: Loading tweak class name org.spongepowered.asm.mixin.MixinEnvironment$EnvironmentStateTweaker
[22:27:22] [main/INFO] [LaunchWrapper]: Calling tweak class net.minecraftforge.fml.common.launcher.TerminalTweaker
[22:27:22] [main/INFO] [LaunchWrapper]: Calling tweak class org.spongepowered.asm.mixin.MixinEnvironment$EnvironmentStateTweaker
[22:27:32] [main/INFO] [LaunchWrapper]: Launching wrapped minecraft {net.minecraft.server.MinecraftServer}
[22:27:35] [main/WARN] [mixin]: Method overwrite conflict for getSlotProvider in mixins.common.core.json:tileentity.MixinTileEntityHopper, previously written by org.spongepowered.common.mixin.core.item.inventory.TraitInventoryAdapter. Skipping method.
[22:27:35] [main/WARN] [mixin]: Method overwrite conflict for getSlotProvider in mixins.common.core.json:tileentity.MixinTileEntityChest, previously written by org.spongepowered.common.mixin.core.item.inventory.TraitInventoryAdapter. Skipping method.
[22:27:36] [main/WARN] [mixin]: Method overwrite conflict for getSlotProvider in mixins.common.core.json:tileentity.MixinTileEntityFurnace, previously written by org.spongepowered.common.mixin.core.item.inventory.TraitInventoryAdapter. Skipping method.
[22:27:40] [main/WARN] [mixin]: Method overwrite conflict for getSlotProvider in mixins.common.core.json:item.inventory.TraitInventoryAdapter, previously written by org.spongepowered.common.mixin.core.entity.passive.MixinEntityVillager. Skipping method.
[22:27:41] [main/WARN] [mixin]: Method overwrite conflict for getSlotProvider in mixins.common.core.json:item.inventory.TraitInventoryAdapter, previously written by org.spongepowered.common.mixin.core.item.inventory.MixinContainer. Skipping method.
[22:27:42] [main/WARN] [mixin]: Method overwrite conflict for getSlotProvider in mixins.common.core.json:item.inventory.TraitInventoryAdapter, previously written by org.spongepowered.common.mixin.core.entity.player.MixinInventoryPlayer. Skipping method.
[22:27:42] [Server thread/INFO]: Starting minecraft server version 1.10.2
[22:27:42] [Server thread/INFO] [FML]: MinecraftForge v12.18.2.2098 Initialized
[22:27:42] [Server thread/INFO] [FML]: Replaced 232 ore recipes
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.ACHIEVEMENTS
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.AGE
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.ATTACK_DAMAGE
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.BASE_SIZE
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.DAMAGE_ENTITY_MAP
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.DOMINANT_HAND
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.FILLED
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.FLUID_LEVEL
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.HEALTH_SCALE
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.HEIGHT
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.HELD_EXPERIENCE
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.IS_SLEEPING
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.LAST_ATTACKER
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.LAST_DAMAGE
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.LEASH_HOLDER
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.PASSENGERS
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.SCALE
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.SPAWNER_ENTITIES
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.SPAWNER_MAXIMUM_DELAY
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.SPAWNER_MAXIMUM_NEARBY_ENTITIES
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.SPAWNER_NEXT_ENTITY_TO_SPAWN
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.SPAWNER_REQUIRED_PLAYER_RANGE
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.SPAWNER_SPAWN_COUNT
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.SPAWNER_SPAWN_RANGE
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.STATISTICS
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.WILL_SHATTER
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.WIRE_ATTACHMENTS
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.WIRE_ATTACHMENT_EAST
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.WIRE_ATTACHMENT_NORTH
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.WIRE_ATTACHMENT_SOUTH
[22:27:47] [Server thread/WARN] [Sponge]: Skipping org.spongepowered.api.data.key.Keys.WIRE_ATTACHMENT_WEST
[22:27:49] [Server thread/INFO] [FML]: Found 0 mods from the command line. Injecting into mod discoverer
[22:27:49] [Server thread/INFO] [FML]: Searching /***/mods for mods
[22:27:51] [Server thread/WARN] [Sponge]: Plugin 'tools' seems to be missing a valid mcmod.info metadata file. This is not a problem when testing plugins, however it is recommended to include one in public plugins.
Please see https://docs.spongepowered.org/master/en/plugin/plugin-meta.html for details.
[22:27:51] [Server thread/INFO] [FML]: Forge Mod Loader has identified 7 mods to load
[22:27:51] [Server thread/INFO] [FML]: Attempting connection with missing mods [mcp, FML, Forge, spongeapi, sponge, permissionsex, tools] at CLIENT
[22:27:51] [Server thread/INFO] [FML]: Attempting connection with missing mods [mcp, FML, Forge, spongeapi, sponge, permissionsex, tools] at SERVER
[22:27:51] [Server thread/INFO] [tools]: GameConstructionEvent
[22:27:51] [Server thread/INFO] [FML]: Processing ObjectHolder annotations
[22:27:52] [Server thread/INFO] [FML]: Found 423 ObjectHolder annotations
[22:27:52] [Server thread/INFO] [FML]: Identifying ItemStackHolder annotations
[22:27:52] [Server thread/INFO] [FML]: Found 0 ItemStackHolder annotations
[22:27:52] [Server thread/INFO] [FML]: Applying holder lookups
[22:27:52] [Server thread/INFO] [FML]: Holder lookups applied
[22:27:52] [Server thread/INFO] [FML]: Applying holder lookups
[22:27:52] [Server thread/INFO] [FML]: Holder lookups applied
[22:27:52] [Server thread/INFO] [FML]: Applying holder lookups
[22:27:52] [Server thread/INFO] [FML]: Holder lookups applied
[22:27:52] [Server thread/INFO] [FML]: Configured a dormant chunk cache size of 0
[22:27:52] [Forge Version Check/INFO] [ForgeVersionCheck]: [Forge] Starting version check at http://files.minecraftforge.net/maven/net/minecraftforge/forge/promotions_slim.json
[22:27:52] [Server thread/INFO] [permissionsex]: Pre-init of PermissionsEx v2.0-SNAPSHOT-b142
[22:27:52] [Forge Version Check/INFO] [ForgeVersionCheck]: [Forge] Found status: OUTDATED Target: 12.18.2.2099
[22:27:54] [Server thread/INFO] [tools]: GamePreInitializationEvent
[22:27:54] [Server thread/INFO] [FML]: Applying holder lookups
[22:27:54] [Server thread/INFO] [FML]: Holder lookups applied
[22:27:54] [Server thread/INFO] [FML]: Injecting itemstacks
[22:27:54] [Server thread/INFO] [FML]: Itemstack injection complete
[22:27:54] [Server thread/INFO]: Loading properties
[22:27:54] [Server thread/INFO]: Default game type: SURVIVAL
[22:27:54] [Server thread/INFO]: Generating keypair
[22:27:54] [Server thread/INFO]: Starting Minecraft server on *:25565
[22:27:54] [Server thread/INFO]: Using epoll channel type
[22:27:54] [Server thread/INFO] [tools]: GameInitializationEvent
[22:27:54] [Server thread/INFO] [FML]: Injecting itemstacks
[22:27:54] [Server thread/INFO] [FML]: Itemstack injection complete
[22:27:54] [Server thread/INFO] [tools]: GamePostInitializationEvent
[22:27:54] [Server thread/INFO] [tools]: GameLoadCompleteEvent
[22:27:54] [Server thread/INFO] [FML]: Forge Mod Loader has successfully loaded 7 mods
[22:27:54] [Server thread/INFO] [tools]: GameAboutToStartServerEvent
[22:27:54] [Server thread/INFO]: Preparing level "world"
[22:27:54] [Server thread/INFO] [Sponge]: Checking for worlds that need to be migrated...
[22:27:54] [Server thread/INFO] [Sponge]: No worlds were found in need of migration.
[22:27:55] [Server thread/INFO] [FML]: Injecting existing block and item data into this server instance
[22:27:55] [Server thread/INFO] [FML]: Applying holder lookups
[22:27:55] [Server thread/INFO] [FML]: Holder lookups applied
[22:27:55] [Server thread/INFO] [FML]: Loading dimension 0 (world) (DedicatedServer)
[22:27:56] [Server thread/INFO]: Preparing start region for level 0 (world)
[22:27:57] [Server thread/INFO]: Preparing spawn area: 1%
[22:27:58] [Server thread/INFO]: Preparing spawn area: 36%
[22:27:59] [Server thread/INFO]: Preparing spawn area: 96%
[22:27:59] [Server thread/INFO] [Sponge]: Loading world [world] (Overworld)
[22:27:59] [Server thread/INFO] [FML]: Loading dimension -1 (DIM-1) (DedicatedServer)
[22:27:59] [Server thread/INFO] [Sponge]: Loading world [DIM-1] (Nether)
[22:27:59] [Server thread/INFO] [FML]: Loading dimension 1 (DIM1) (DedicatedServer)
[22:27:59] [Server thread/INFO] [Sponge]: Loading world [DIM1] (The End)
[22:27:59] [Server thread/INFO]: Preparing start region for level 0 (world)
[22:27:59] [Server thread/INFO]: Done (4.880s)! For help, type "help" or "?"
[22:27:59] [Server thread/INFO] [tools]: GameStartingServerEvent
[22:27:59] [Server thread/INFO] [tools]: GameStartedServerEvent
> stop
[22:30:56] [Server thread/INFO]: Stopping the server
[22:30:57] [Server thread/INFO] [tools]: GameStoppingServerEvent
[22:30:57] [Server thread/INFO]: Stopping server
[22:30:57] [Server thread/INFO]: Saving players
[22:30:57] [Server thread/INFO]: Saving worlds
[22:30:57] [Server thread/INFO]: Saving chunks for level 'world'/Overworld
[22:30:57] [Server thread/INFO]: Saving chunks for level 'DIM-1'/Nether
[22:30:57] [Server thread/INFO]: Saving chunks for level 'DIM1'/The End
[22:30:57] [Server thread/INFO] [Sponge]: Unloading world [world] (DIM0)
[22:30:57] [Server thread/INFO] [Sponge]: Unloading world [DIM-1] (DIM-1)
[22:30:57] [Server thread/INFO] [Sponge]: Unloading world [DIM1] (DIM1)
[22:30:57] [Server thread/INFO] [FML]: Applying holder lookups
[22:30:57] [Server thread/INFO] [FML]: Holder lookups applied
[22:30:57] [Server thread/INFO] [tools]: GameStoppedServerEvent
[22:30:57] [Server Shutdown Thread/INFO]: Stopping server

ちょっと警告が出てますが、どうも@Plugin以降のメタデータが不足してるというメッセージみたいです(ここではmcmod.infoが足りてない)。一応、動かないことは無いですね。

とりあえず初めて作ってみた感じとしては「何でこのエラーで止まるの?」って思うことも多いですが、ログをしっかり読むとURL等の説明も含めてちゃんと説明が出てる場合も多いので、解決しやすいかな?って感じました。

メタデータ

プラグインとして動作させる為の情報ですね。spigotプラグインを作ってた人なら「plugin.yml」のことと思って貰えればわかりやすいかな。

@Plugin

メインとなるクラスのクラス名の前に直接書きます。

~略~
@Plugin(id = "tools", name = "Tools", version = "1.0")
public class Tools {
~略~

idがプラグインIDと言われ、なるべく他人様のと被らない名前を付けた方が良さそうです。また文字の制限が厳しく、英字小文字である必要があり、大文字が混ざったりすると、サーバ自体がダウンするくらいまともに動きません。一方で、nameは結構適当な名前でいいようです。

@Pluginの中にはいろいろ記載ができますが、ちょっと気になるのが(上記サンプルには書いてないですが)dependenciesという値。このプラグインを読み込ませる前に読み込ませておきたいプラグイン名を書く為の項目のようですが、今ひとつ動きが微妙です。
っていうか、後述のmcmod.infoでも同じことが書ける上に、そっちの方が細かい記述ができるので、あえてこちらに書く必要は無いかなと感じました(たぶん)

mcmod.info

前述でも記載しましたが、このファイルは無くても動きます。ただ、無いと警告がでます。jarファイルの最上位におかなければならないファイルで、JSON形式という書式で記載します。
書き方は、ここで解説するよりも以下のページを参照にした方がわかりやすそうですね。

http://minecraftjp.info/modding/index.php/Mcmod.info%E3%81%AE%E8%A8%AD%E5%AE%9A

前述の@Pluginの説明にも書いて有るとおり、dependencies(先行して読み込ませたいプラグインの指定)の記載もできます。dependenciesは先行プラグインが無くても動きますが、絶対に無いと困るプラグインは「requiredMods」の指定もするといいですね。
※dependenciesは読み込み順に影響し、requiredModsは必須プラグインの記述なので、それぞれ意味が似てますが異なります。また、これらを使うならuseDependencyInformationをセットする必要があるみたい。

ライフサイクル

サーバの起動・実行中・停止時に発生するイベントですね(イベントについては後述で詳しく)。書き方は以下の通りです。とりあえず@Pluginの書いてあるクラスには何かを継承させたりする必要無く、以下のリスナを書けばいいみたいです。

@Listener
public void onPreInitialization(GamePreInitializationEvent event) {
処理内容
}

以下のような物イベントが定義されてます。説明はすべてちゃんと検証をしている訳では無いので、一部公式の直訳を参考にしてます。

初期化

起動時に一回だけ呼ばれるようです。

イベント名 イベントステート 説明
GameConstructionEvent CONSTRUCTION @Pluginクラスがインスタンス化されるときらしい。まさに以下のメッセージの直後に処理される。
[FML]: Forge Mod Loader has identified ? mods to load
GamePreInitializationEvent PRE_INITIALIZATION デフォルトのロガーなどがロードされた後に呼ばれる。現実的にこの前でもロガーとかは使えるけど、外部クラスへのアクセスはこの処理の後の方がいいかも。server.propertiesの読み込み前に呼ばれてるようです。
GameInitializationEvent INITIALIZATION ここでプラグインが機能するのに必要な事前処理を終わらせておく。グローバルイベントハンドラはこの段階で登録するなど?
GamePostInitializationEvent POST_INITIALIZATION APIを提供するプラグインはここで受け入れ準備を完了させる。
GameLoadCompleteEvent LOAD_COMPLETE あと残った初期化処理があるならここで終わらせる。このイベントの直後に以下のメッセージがでます。
[FML]: Forge Mod Loader has successfully loaded ? mods

実行中

サーバ稼働中に複数回発生する可能性があるイベント。要するにプラグイン再起動とかされると呼ばれる?SERVER_ABOUT_TO_STARTとSERVER_STARTINGとSERVER_STARTEDはサーバ起動直後、SERVER_STOPPINGとSERVER_STOPPEDはサーバ停止時に呼ばれます。

イベント名 イベントステート 説明
GameAboutToStartServerEvent SERVER_ABOUT_TO_START 以下のメッセージの直後に発生。
[FML]: Forge Mod Loader has successfully loaded ? mods
サーバインスタンスが稼働している状態。但し、ワールドがロードされてない。このイベントの直後に以下のメッセージが出る。
Preparing level “world”
GameStartingServerEvent SERVER_STARTING ワールドがロードされると発生。まさにこのメッセージは以下の(サーバ起動完了を示す)メッセージの直後に呼ばれます。
Done (6.089s)! For help, type “help” or “?”
プラグインコマンドはこの途中で登録されるらしい。
GameStartedServerEvent SERVER_STARTED  SERVER_STARTINGステートの処理がすべて終わった後かな。
GameStoppingServerEvent SERVER_STOPPING 終了コマンド入力直後の処理開始前に発生。この段階ではまだワールドの保存とかされていない。要するにキャッシュの吐き出しとかはこの辺でやるってことかな。この処理の後にワールドやプレイヤーの保存が行われる。
GameStoppedServerEvent SERVER_STOPPED 既にプレイヤーが居ない状態。

終了中

一見「/stop」コマンドの後に出そうですが、そうでは無いみたいです。今のところ見たことがありません。
※「/stop」コマンド後はSERVER_STOPPING→SERVER_STOPPED

イベント名 イベントステート 説明
GameStoppingServerEvent GAME_STOPPING 停止開始。APIを提供するプラグインはこの段階ではまだAPIを提供し続けるべき。
GameStoppedServerEvent GAME_STOPPED  この後、マインクラフトがシャットダウンされる。この段階ではプラグイン単体のシャットダウンで、他のプラグインに関連する処理はすべきでは無い。

ロギング

前述の通り、spongeのロガーはorg.slf4j.Loggerを使います。個人的にはlatest.logにログを吐いてくれないので不便だなぁ、、、とは思いますが

~
import org.slf4j.Logger;
~
@Inject
private Logger logger;
~
this.logger.info("GameConstructionEvent");
~

これで「GameConstructionEvent」とログを出力してくれます。logger.info以外にもlogger.warn、logger.errorが使えますね。logger.debugとかもありますが、標準出力されません。infoなら、

[23:39:17] [Server thread/INFO] [プラグインID]: メッセージ

と表示されます。warnやerrorは上記の「INFO」の部分の文字が変わるだけで色などは付きません。色は自分で付けましょう。

コンフィグ

設定はサーバディレクトリのconfigディレクトリ内に作られます。作成の際には「config/プラグインID.conf」にしたいか(sharedRoot = true)、「config/プラグインID/適当なファイル名」にするか(sharedRoot = false)の選択が必要です。前者は単一の設定ファイルのみ。後者は複数の設定ファイルを作成したい場合に用います。将来の拡張を考慮するなら後述を用いるべきですかね。

sharedRoot = true

単一の設定ファイル(config/プラグインID.conf)を作成したい場合に使います。

 @Inject
 @DefaultConfig(sharedRoot = true)
 private Path defaultConfig;

@Inject
@ConfigDir(sharedRoot = true)
private Path privateConfigDir;

とした場合、「defaultConfig.toString()」は「config/プラグインID.conf」を、「privateConfigDir.toString()」は「config」を返します。

※値を返すだけで、ファイルやディレクトリは作ってくれません。

sharedRoot = false

複数の設定ファイルを作成したい場合に使います。

 @Inject
 @DefaultConfig(sharedRoot = false)
 private Path defaultConfig;

@Inject
@ConfigDir(sharedRoot = false)
private Path privateConfigDir;

とした場合、「defaultConfig.toString()」は「config/プラグインID/プラグインID.conf」を、「privateConfigDir.toString()」は「config/プラグインID」を返します。

※値を返すだけで、ファイルやディレクトリは作ってくれません。

初期化・セーブ・ロード

まず、各変数の初期化から。
defaultConfigについては前述の通りです。loaderがファイルアクセスをうまくやってくれるクラス。設定データはrootNodeに追加削除してそこにファイルから読み書きをするイメージです。
以下の例では、とりあえず空の設定をrootNodeへ格納しています。

 @Inject
 @DefaultConfig(sharedRoot = false)
 Path defaultConfig;
~略~
 ConfigurationLoader<CommentedConfigurationNode> loader = HoconConfigurationLoader.builder().setPath(defaultConfig).build();
 ConfigurationNode rootNode = loader.createEmptyNode(ConfigurationOptions.defaults());

設定への値の格納は、以下のような感じでrootNodeに対して行います。格納する型はObject型なので、このメソッド一つで一通り扱えます。

rootNode.getNode( "test1", "test2", "test3" ).setValue( "value1" );

値の取り出しもこんな感じです。型によってメソッドが違う点はsetと扱いが異なります。

String v = rootNode.getNode( "test1", "test2", "test3" ).getString();

データの保存(save)はこんな感じです。sharedRoot = falseの場合はconfigディレクトリの下にプラグインIDのディレクトリが必要ですが、その辺の作成もやってくれますね。

try {
 loader.save( rootNode );
} catch(IOException e) {
 // error
 e.printStackTrace();
}

設定の読み込み(load)は以下のような感じです。

try {
 rootNode = loader.load();
} catch(IOException e) {
 // error
 e.printStackTrace();
}

なお、以下のように設定データを格納した場合は、、、

rootNode.getNode( "test1", "test1", "test1" ).setValue( "value1" );
rootNode.getNode( "test1", "test1", "test2" ).setValue( "value2" );
rootNode.getNode( "test1", "test2", "test3" ).setValue( "value3" );
rootNode.getNode( "test2", "test3", "test4" ).setValue( "value4" );

設定ファイルの内容は以下のようになります。

test1 {
  test1 {
    test1=value1
    test2=value2
  }
  test2 {
    test3=value3
  }
}
test2 {
  test3 {
    test4=value4
  }
}

コマンド

コンソール、あるいはプレイヤーなどがチャットコマンド(/tpとか)を実行したときの処理です。基本的には公式説明を丸パクリしてます(すこしいじってますが)
spigotプラグインに比べると作者任せな部分が大分システムで吸収されてる気がしますが、逆に書き方が複雑で覚えるのが大変です。

とりあえず先ずは書き方を解説します。実行結果がどうなるかは後半に書いてあります。

初期化

GameInitializationEventの中でやるのが定石みたいですね。拘らなくてもいいかもしれませんが。

@Listener
public void onInitialization(GameInitializationEvent event) {
  CommandSpec myCommandSpec = CommandSpec.builder()
    .description(Text.of("Hello World Command"))
    .permission("myplugin.command.helloworld")
    .arguments(
      GenericArguments.onlyOne(GenericArguments.player(Text.of("player"))),
      GenericArguments.remainingJoinedStrings(Text.of("message")))
    .executor(new cmdexec())
    .build();
    Sponge.getCommandManager().register(this, myCommandSpec, "helloworld", "hello", "test");
 }

Textは文字をゲーム用に整形できるクラスですが、ここでは説明を省きます。今のところは「文字をそのまま出力する」という理解でいいと思います。

  • CommandSpec myCommandSpec = CommandSpec.builder()…
    コマンドの定義をmyCommandSpecという変数へ格納します。builder()に続き、以下のメソッドを連結して定義します。
  • .description()
    プラグインの説明ですね。/helpコマンドで出てきます。
  • .permission()
    権限ですね。この権限の無いプレイヤーはコマンドを実行するとエラーになります。
  • .arguments()
    コマンドの引数の定義です。引数を必要としないなら.arguments(~)をすべて消してしまって構いません。
    上記サンプルでは1行1引数を示してます。また、GenericArgumentsに続く「onlyOne」や「remainingJoinedStrings」が引数の型を示してます。
    GenericArguments.onlyOne(GenericArguments.player(~略~)…ログイン済みのプレイヤー1人分です。
    GenericArguments.remainingJoinedStrings…複数のスペースで区切られた文字を一つの引数と見なします。
    これ以外にもboolやintegerなどの単純な物や、locationやworldなどマイクラ固有の引数も指定できるようです。定義はいろいろあるので詳しくは以下のページを参考にして下さい。
    https://docs.spongepowered.org/master/ja/plugin/commands/arguments.html
  • .executor(new cmdexec())
    このコマンドが実行された時に実行される処理が書かれたクラスを指定します。ここではcmdexecというクラスを指定していますが、このクラスについての説明は後述に記載します。
  • .build();
    CommandSpec.builder()…からの定義の終端ですね。

とりあえずこれは定義だけで、これだけではコマンドとして登録はまだされてません。
定義については他にもいろいろできるようなので、詳しくは以下のページを参考にして下さい。
https://docs.spongepowered.org/master/ja/plugin/commands/creating.html
コマンドの登録は次の行のSponge.getCommandManager().register(~)で行います。registerの引数は以下の通り。

  • this
    自分のクラスを引き渡します。恐らく@Pluginの書かれたクラスじゃないとダメかなと。
  • myCommandSpec
    前述の定義を引数として引き渡します。
  • “helloworld”
    コマンド名です。これで「/helloworld」というコマンドが登録されます。
  • “hello”,”test”
    エイリアス(コマンドの別名)ですね。「/hello」や「/test」でも同じ動作をします。

コマンド実行後の処理

前述の例では、このコマンドを実行するとcmdexecというクラスに処理が渡されます。クラス名は何でもいいのですが、以下、クラスの書き方です。(packageやimport)は省略してます。

public class cmdexec implements CommandExecutor {
  public CommandResult execute(CommandSource src, CommandContext args) throws CommandException {
// ...1
 Player arg_player = args.<Player>getOne("player").get();
 String arg_message = args.<String>getOne("message").get();
// ...2
 if(src instanceof Player) {
 Player player = (Player) src;
 player.sendMessage(Text.of("Hello " + player.getName() + "!"));
 } else if(src instanceof ConsoleSource) {
 src.sendMessage(Text.of("Hello Command!"));
 } else if(src instanceof CommandBlockSource) {
 src.sendMessage(Text.of("Hello Command Block!"));
 }
// ...3
 src.sendMessage(Text.of( "player=" + arg_player ));
 src.sendMessage(Text.of( "message=" + arg_message ));
// ...4
 return CommandResult.success();
 }
 }

細かい説明は省きますが、だいたい以下の通りです。

  • public class cmdexec implements CommandExecutor {
    クラスを作成の際に、記載の通りCommandExecutorインターフェースを実装します。
  • public CommandResult execute(CommandSource src, CommandContext args) throws CommandException {
    CommandExecutorインターフェースにはexecuteメソッドが定義されてるので、この定義は必須です。CommandSourceは誰が実行したか、argsはコマンドの引数が格納されます。
  • …1の部分
    前述のコマンドの定義で「.arguments」に定義された引数を実際に受け取る処理です。getOneの引数が、「.arguments」で定義した文字と一致している必要がありそうです。
  • …2の部分
    誰が実行したかにより処理を分岐してます。「src instanceof Player」がプレイヤーがコマンドを実行した時の処理。「src instanceof ConsoleSource」がサーバコンソール。「src instanceof CommandBlockSource」がコマンドブロックのようですね。
  • …3の部分
    引数の値を表示しているだけです。
  • …4の部分
    メソッドの結果を返します。これは正常終了ですね。これ以外の処理の書き方については省略します。

コマンドを実行してみる

まず「/help」を実行した時です。前述の「.description」の部分が表示されます。他にならってコマンドオプションの説明を書いた方がいいみたいですね。

> /help
~略~
/give /give <player> <item> [amount] [data] [dataTag]
/helloworld Hello World Command
/help /help [page|command name]
~略~

今回は、.arguments()の引数に第一引数にプレイヤー名(GenericArguments.player)。第二引数以降には任意の文字(GenericArguments.remainingJoinedStrings)という定義にしてあります。引数を付けないで実行すると以下のようになります。

/helloworld
Not enough arguments
Usage: /helloworld <player> <message…>

エラーになりますが、これは定義に従い自動的に表示されます。ですからこの辺のエラー処理は書かなくても大丈夫ということになりますね。ありがたい。

次にちゃんと引数を指定して実行した場合ですが、今回は第一引数が「オンラインプレイヤー」です。オフラインのプレイヤーを指定した場合の実行結果は以下のようになります。

/helloworld Tactica test1 test2
No values matching pattern 'Tactica' present for player!
Tactica test1 test2
^
Usage: /helloworld <player> <message…>

ちゃんとどこがマズいのかを指示してくれますね。

次に実行結果が正常だった場合です。今回はプレイヤー、コンソール、コマンドブロックそれぞれ別の処理を書いてますので、それぞれの実行結果です。
ポイントはそれぞれ実行結果が異なるのと、第二引数以降がスペースで区切られているにもかかわらず、ちゃんと「纏めて一つ」の引数として処理されている点ですかね。

まずはコンソールから。

> helloworld Tactica test1 test2
[21:30:28] [Server thread/INFO]: Hello Command!
[21:30:28] [Server thread/INFO]: player=EntityPlayerMP['Tactica'/363, l='world', x=93.44, y=76.00, z=224.30]
[21:30:28] [Server thread/INFO]: message=test1 test2

次にプレイヤーが実行した場合。ちゃんとプレイヤー名を返しています。

sponge_command_player

次にコマンドブロック。「前回の実行結果」に出てますが、残念ながら1行しか見れませんでした。

sponge_command_cb

その他のコマンド処理の書き方

細かい説明は省きますが、以下のような書き方もあるようです。

child

例えば、「/mail send」と「/mail read」というコマンドがあった場合、前述の例の通り処理を書くと、二つの定義に分けるか、CommandExecutor が処理を受け取って、それぞれ引数を解析して二分岐などとちょっと面倒です。
そんなときは以下の様にも書けるようです。

CommandSpec mailCommandSpec = CommandSpec.builder()
  .permission("myplugin.mail")
  .description(Text.of("Send and receive mails"))
  .child(readCmd, "read", "r", "inbox")
  .child(sendCmd, "send", "s", "write")
  .build();
Sponge.getCommandManager().register(plugin, mailCommandSpec, "mail", "email");

上記例は公式ドキュメントから引用していますが、実際にはchildの引数に「CommandSpec.builder」を書く感じの方がわかりやすいですね。

例)
CommandSpec mailCommandSpec = CommandSpec.builder()
.description(Text.of("Send and receive mails"))
.child(
  CommandSpec.builder()
  .description(Text.of("Send mails"))
  .permission("myplugin.mail.send")
  .executor(new readCmd())
  .build()
, "read", "r", "inbox")
~略~
.build();

参考)https://docs.spongepowered.org/master/ja/plugin/commands/childcommands.html

フラグ

「/command -s」などの「-s」のことを「フラグ」というらしいですが、このコマンドフラグを処理する仕組みもあります。詳細は省きますので後述の公式ページを参照ですが、「-s」を「書いたらtrue」「書かなかったらfalse」という判定ができるようです。
参考)https://docs.spongepowered.org/master/ja/plugin/commands/flags.html

arguments

引数については前述の通り「.arguments()」で定義を書くわけですが、例の書き方の場合は書式がキッチリ決まっている場合は便利です。しかし「複数のString型の引数があったり無かったりで」という場合も多いと思うのでその場合は以下の様に書くようです。

.arguments(
  GenericArguments.optional(GenericArguments.string(Text.of("cmd")))
  ,GenericArguments.optional(GenericArguments.string(Text.of("arg0")))
  ,GenericArguments.optional(GenericArguments.string(Text.of("arg1")))
)

で、値の取得側

@Override
public CommandResult execute(CommandSource src, CommandContext args) throws CommandException {
  String cmd = "null";
  String arg0 = "null";
  String arg1 = "null";

if ( args.hasAny( “cmd” ) ) { cmd = args.<String>getOne(“cmd”).get(); }
if ( args.hasAny( “arg0” ) ) { arg0 = args.<String>getOne(“arg0”).get(); }
if ( args.hasAny( “arg1” ) ) { arg1 = args.<String>getOne(“arg1”).get(); }

src.sendMessage( Text.of( “cmd=” + cmd + ” arg0=” + arg0 + ” arg1=” + arg1 ) );

return CommandResult.success();
}

こうすると、1つめの引数は「cmd」という変数に格納されます。arguments側では「optional(任意)」にしてますし、execute側では「args.hasAny」で引数が有るか無いかのチェックをしてますので、引数が無くても通ります。

で、上記の例ではcmd、arg0、arg1の引数を指定してるので、4つめの引数を付けると、「Too many arguments!」と自動的にエラーを吐いてくれます。

executor

.executor()には「コマンド実行時に呼ばれるクラスを書く」と解説してますが、わざわざコマンド毎にクラスを用意しなくても、以下の方法で同一クラスのメソッドを呼べます。

CommandSpec.builder()
  .description( Text.of("hoge") )
  .executor( (src, args) -> { return this.hoge( src, args ); })
  ~略~
  .build,"hoge");
...
public CommandResult hoge(CommandSource src, CommandContext args) throws CommandException {
  ...
}

 

イベント

イベントとは、「プレイヤーがログインした」とか「ブロックが破壊された」あるいは「ブロックが設置された」などのアクションが起きたときに動作するプログラムで、コード的にはイベントを発生させる側と受ける側を書けます。

ここでは、イベントを受ける側のリスナーだけ書いておきます。ちなみに前述のライフサイクルもイベントリスナーです。
ポイントは「@Listener」とイベントクラス(以下の例だとChangeBlockEvent.Break)ですね。イベントの中身の振る舞いはイベントクラスの扱い方だけなので、ここでは簡単な値取得と表示のサンプルだけ乗せておきます。

各種イベントクラスについては、「Sponge Events」で解説してます。

ちなみに以下の例はブロックを壊したときに壊したプレイヤーやブロックの情報などを表示します。

package hoge;

import org.spongepowered.api.entity.living.player.Player;
import org.spongepowered.api.event.Listener;
import org.spongepowered.api.event.block.ChangeBlockEvent;
import org.spongepowered.api.text.Text;
import org.spongepowered.api.world.Location;
import org.spongepowered.api.world.World;

public class EventTest {
 @Listener
 public void onSomeEvent(ChangeBlockEvent.Break event) {
 if ( event.getCause().first(Player.class).isPresent() ){
   Player player = event.getCause().first(Player.class).get();
   UUID playerUUID = player.getUniqueId();
   String blockName = event.getTransactions().get(0).getOriginal().getState().getType().getName();
   World world = event.getTargetWorld();
   Location<World> loc = event.getTransactions().get(0).getOriginal().getLocation().get();
   player.sendMessage(
     Text.of(
      player.getName()
       + "," + playerUUID.toString()
       + "," + blockName
       + "," + world.getName()
       + "," + loc.getBlockX()
       + "," + loc.getBlockY()
       + "," + loc.getBlockZ()
    )
   );
  }
 }
}

当然、これだけでは動かないです。クラスを登録しないと動きません。振る舞いとしてはやはりGameInitializationEvent(INITIALIZATION)に書くべきなのかなと思います。

Sponge.getEventManager().registerListeners(this, new EventTest());

なお、この例はRegistering Event Listenersという物の書き方の一例です。もう一つUnregistering Event Listenersというのも有るらしいですが、まだ調べてないので省略します。

あと、Registering Event ListenersにはDynamically Registering Event Listenersというのも有るようです。これは@Listenerを使えない/使いたく無い時に用いる手法みたいですね。
書き方は、

  • イベントクラスに、EventListenerを実装する。上記例なら「public class EventTest implements EventListener<ChangeBlockEvent.Break> {…」という書き方。
  • @Listenerの部分はhandleをオーバライドします。
    @Override
    public void handle(ChangeBlockEvent.Break event) throws Exception {…
  • そしてイベントクラスの登録は以下の様になります。
    EventListener<ChangeBlockEvent.Break> listener = new ExampleListener();
    Sponge.getEventManager().registerListener(this, ChangeBlockEvent.Break.class, listener);

イベントデータの振る舞い

イベントクラスから取得可能なデータは様々ですが、ある程度規則性があるので、いくつかの例を紹介しておきます。今回の例は「ブロックを壊した」場合です。this.logger.info(~)は、ここでは特に重要ではあまり気にしないで単純に画面に文字を出すメソッドだと思って下さい。重要なのは赤字の部分です。

@Listener
public void onSomeEvent(ChangeBlockEvent.Break event) {
  this.logger.info( event.getCause().first( Player.class ).get().getName() );
  this.logger.info( event.getTransactions().get(0).getOriginal().getState().getType() );
  this.logger.info( event.getTransactions().get(0).getFinal().getState().getType() );
}

event.getCause()は非常に多くのイベントクラスで使われていますが、主にイベントの発動元がプレイヤーだったり、エンティティだったり、ブロックだったりな場合に、その発動元のクラスを取得することができます。ちなみにfirst()の戻り値はJava8から利用可能になったOptionalなので、発動元がなんだかわからない場合は以下の様な条件分けもできます。

if( event.getCause().first( Player.class ).isPresent() ){
  this.logger.info( event.getCause().first( Player.class ).get().getName() );
}else if( event.getCause().first( Entity.class ).isPresent() ){
  this.logger.info( event.getCause().first( Entity.class ).get().getType().getId() );
}else if( event.getCause().first( BlockSnapshot.class ).isPresent() ){
  this.logger.info( event.getCause().first( BlockSnapshot.class ).get().getState().getType() );
}

event.getCause()は慣れるのがちょっと大変ですが、例えばブロックを壊すだけじゃなくて、火打ち石で着火した場合もFireブロックをChangeBlockEvent.Placeで取得できます。しかもそれだけじゃ無く、着火後の延焼でもプレイヤー情報を保持し続けますし、着火の理由が火打ち石の場合はプレイヤで、延焼の場合はevent.getCause().first( BlockSnapshot.class )を見るとFireが取得できるなど、一つのクラスから複数の情報を取得するようなこともできます。

「event.getTransactions()」もよく見かけますが、なんと説明すればいいかわかりませんが、要するにブロックを「壊す」イベントの場合は、「event.getTransactions().get(0).getOriginal()」が壊す前のブロックの情報、「event.getTransactions().get(0).getFinal()」が壊した後のブロック情報になります。(通常、壊した後のブロックはAirです)

 

次に、event.getCause().firstなどはcauseから該当するクラスを探し出してデータを取得しますが、例えば「プレイヤーが植えた木の葉っぱが腐った」場合でも、間接的にプレイヤーが関与したとしてevent.getCause().first(Player.class)が取得可能です。逆に言えば、event.getCause().first(Player.class)で取得できるPlayerは、何に影響して取得できたのかはこれではわかりません。なので、要因を指定して取得することも可能です。

Optional<Player> notifier = event.getCause().get(NamedCause.NOTIFIER, Player.class);

こう書いた場合は、Nortifier(通知者?)がプレイヤーの場合のみPlayerを取得します。プレイヤーでは無い場合は、Playerが取得できません。NamedCauseにはIGNITER、NOTIFIER、OWNER、PHYSICAL、PLAYER_SIMULATED、SOURCE、THROWERがあります。
例えば、「プレイヤーが植えた木の葉っぱが腐った」場合は、プレイヤーが植えた木であれば、event.getCause().first(Player.class)の場合は、プレイヤーを取得できますが、event.getCause().get(NamedCause.SOURCE, Player.class)の場合は、腐った原因はプレイヤーでは無いので、プレイヤーは取得できません。

スケジューラ

有る時間が経過した後、あるいは一定間隔毎にプログラムを動かす機能です。公式サイトにはいろんな記載方法が紹介されていますが、あまりよくわからないのでわかるのだけ書きます。イメージ的にはspigotプラグインとそんなに違いはありませんが、最低実行単位が1tick(1/20秒)だったのに対し、spongeではミリ秒とか分とかを選択できる仕組みになってます。便利ですね。

まず、スケジューラによって動かされるプログラムの方です。Runnableを実装することで、runメソッドが必須になります。以下の内容はログに「run」という文字を出すだけです。

package hoge;

import org.slf4j.Logger;

public class EventExec implements Runnable {
private Logger logger;

public EventExec( Logger logger ){
this.logger = logger;
}

public void run() {
this.logger.info(“run”);
}
}

次に起動の為の設定です。基本的にはスケジューラを起動したいときに起動すればいいだけですが、以下の例ではプラグイン起動時にスケジューラを起動しています。

public class Tools {
  // Logger
  @Inject
  private Logger logger;

private Task t;

~略~
@Listener
public void onServerStated(GameStartedServerEvent event) {
Scheduler scheduler = Sponge.getScheduler();
Task.Builder taskBuilder = scheduler.createTaskBuilder();
t = taskBuilder
.execute( new EventExec( this.logger ) )
.async()
.delay(100, TimeUnit.MILLISECONDS)
.interval(1000, TimeUnit.MILLISECONDS)
.name(“tools-A-01”)
.submit(this)
;
}

@Listener
public void onServerStopping(GameStoppingServerEvent event) {
t.cancel();
}
~略~
}

  • .execute()
    所定の時間経過後に実行されるクラスです。前述のクラスをnewして、引数にloggerを渡してます。
  • .async()
    「非同期」の意味ですね。とりあえずおまじないみたいな物なので、ここでは詳しい説明は省略します。
  • .delay(遅延時間,単位)
    実行までの待ち時間です。単位が「TimeUnit.MILLISECONDS(ミリ秒)」なので、上記の例では、すぐに実行されず、100ミリ秒おいてから実行されます。サーバなどは基本的に起動直後はデータロードなどで重いので、少し置いてから実行させましょう。
  • .interval(遅延時間,単位)
    繰り返し実行時の実行間隔です。上記の例も単位がミリ秒なので、1000ミリ秒(1秒)毎に実行されます。
  • .name()
    名前ですね。スケジューラは複数起動できますが、エラーなどが発生した際にはこの名前でどのスケジューラかを特定できます。適当な文字でも大丈夫そうですが被らないことが条件で、「プラグインID-A-シリアル番号」という名前が推奨されているようです。
    ※「-A-」は恐らく「async(非同期)」の略で、同期(sync)の場合は「-S-」のようです。シリアル番号は好きな被らない数字を指定すれば良さそうです。
  • .submit(this)
    プラグインのクラスを引数に指定するようです。

基本的にはtaskBuilderで上記の値を指定することでスケジューラは起動し始めます。「t = taskBuilder…」と書いていますが、taskBuilderの戻り値も特に受け取らなくても構いませんが、後続の「t.cancel();※スケジューラを止める」に有る通り、処理をプログラム中で止めたい場合は必要になります。

ItemStack

マイクラ内でのアイテムの形態は、インベントリ内のアイテムと、ブロック化したアイテムとがあり、前者がItemStackとして表現できます。
※ブロックはBlockを使いますが、そちらはそのうち調べます。
例えば、手持ちのアイテム(利き手/メインハンド)の取得は以下の通り。

Player p = (プレイヤー情報を取得する処理)
if ( p.getItemInHand( HandTypes.MAIN_HAND ).isPresent() ){
  ItemStack is = p.getItemInHand( HandTypes.MAIN_HAND ).get();
}

ItemStackから読める情報は主に以下のような感じ

  • is.getItem().getName();
    is.getItem().getId();
    どっちも同じ?「minecraft:diamond_sword」とか
  • is.getItem().getMaxStackQuantity();
    たぶん、最大スタック数。剣とかは1だし、スタック可能なブロックは64が得られる。
  • is.getItem().getQuantity();
    スタック数。
  • is.getTranslation().get();
    「Polished Diorite」「Andesite」「Purpur Stairs」のようにgetNameより「ちゃんとした名前」がGetできます。ちなみにgetにはLocaleを食わせることができるので、試しにLocale.JAPANESEとか入れてみましたが、日本語にはならず。どうもResourceBundleをうまく使えばできそうですが未確認。

ちなみに昔ながらのAirは0、Stoneは1、Diamondは264などのアイテムを数字で表現するのはちょっと無理そうです。元々数字表現は廃止が予定されてて「minecraft:diamond」みたいのが推奨されてたので、しょうが無いかなと(ちゃんと調べた訳ではありませんが)。

また、例えば木(minecraft:log)の別の種類(白樺とかジャングル)とかの表現で、log:1、log:2などの右側の数字については以下の方法で取得できるようです。

is.toContainer().get(DataQuery.of("UnsafeDamage")).get();
※ItemStackではなく、Blockから得たい場合は、BlockからItemStackへ変換すれば取得できます。
ItemStack is = ItemStack.builder().fromBlockState( blockstate ).build();
※BlockStateからの変換ですが、BlockStateは例えばLocationからであれば、getBlockメソッドなどで取得できます。
なお、AIRブロックなどはItemStackに存在せず、強引に変換しようもなら例外が発生するので、注意しましょう。

なんで、「UnsafeDamage」?と思いますが、私も理由は知りません。ただ、一応ちゃんと取れるようです。

Keys

ItemStackクラスだけで得られる情報は少ないですが、さらに踏み込んだ情報はKeysで調べられます。例えば、アイテムの耐久度は以下の通り。

if ( is.get( Keys.ITEM_DURABILITY ).isPresent() ){
  logger.info("耐久度=" + is.get( Keys.ITEM_DURABILITY ).get());
}

Keys.ITEM_DURABILITYについては「https://jd.spongepowered.org/5.0.0/org/spongepowered/api/data/key/Keys.html#ITEM_DURABILITY」に書いて有るとおり、戻り値が「Key<MutableBoundedValue<Integer>>」となってるので、上記例の戻り値はInteger型です。ちなみに、ブロックなどの耐久度が無い物にはKeys.ITEM_DURABILITYが定義されてませんので、上記例だとisPresent()の判定で蹴られます。

・・・という感じなのですが、このKeysというのは「https://jd.spongepowered.org/5.0.0/org/spongepowered/api/data/key/Keys.html」にあるとおりおびただしい数がありますね。

 

その他サンプル

マインクラフトのバーションを取得する

こんな感じでString型で1.10.2とかの値が取れます。

String v = Sponge.getPlatform().getMinecraftVersion().getName();

 

リロード

プラグインは「/sponge plugins reload プラグイン名」でリロードできます。残念ながらjarファイルそのものが再読み込みされる訳では無いようですね。リロードした場合はGameStartingServerEventやGameStoppingServerEventが起きそうですが、これも起きません。GameReloadEventという個別のイベントを使います。

/**
 * GameReloadEvent
 * @param event
 */
@Listener
public void onReload(GameReloadEvent event) {
  ~リロード中に行いたい処理をここに書く~
}

サーバコマンドを実行する

spigot/bukkitではサーバコマンドを実行するdispatchCommandというのがありましたが、そんな感じのメソッドを書いてみました。残念ながらspigot/bukkitのdispatchCommandとは引数が違うので、そのまま差し替えはできません。

/**
 * Server commands
 * @param cmd
 * @return
 */
public static boolean dispatchCommand( String cmd ){
  if ( cmd == null ) return false;
  if ( cmd.isEmpty() ) return false;
  CommandResult result = Sponge.getCommandManager().process( Sponge.getServer().getConsole(), cmd );
  if ( result.equals( CommandResult.success() ) ){
    return true;
  }
  return false;
}

データの見え方

1…(Entity).getType().toString()、2…(Entity).getType().getName()、3…(Entity).getType().getId()、4…(Entity).toString()

  • 対象がプレイヤー(名前がtactica77)の場合
    1=SpongeEntityType{id=minecraft:player, name=player, translation=SpongeTranslation{id=soundCategory.player}, modid=minecraft, class=net.minecraft.entity.player.EntityPlayerMP}
    2=player
    3=minecraft:player
    4=EntityPlayerMP[‘tactica77’/712, l=’res1’, x=115.03, y=64.00, z=248.75]
  • 対象が絵画の場合
    1=SpongeEntityType{id=minecraft:painting, name=painting, translation=SpongeTranslation{id=entity.Painting.name}, modid=minecraft, class=net.minecraft.entity.item.EntityPainting}
    2=painting
    3=minecraft:painting
    4=EntityPainting[‘Painting’/535, l=’res1’, x=114.03, y=65.50, z=251.50]

ほげTypesを全て見る方法

アイテム種別を判定する場合は、ベタに「ItemTypes.SAPLING」とか書きますが、そもそもItemTypes全部だす!とかしたい場合の書き方。

for (BlockState type : Sponge.getRegistry().getAllOf(BlockState.class)) {
  ~処理~
}

for (ItemType type : Sponge.getRegistry().getAllOf(ItemType.class)) {
  ~処理~
}

 

その他細かな話

  • プレイヤーがopかどうかはチェックできない?
    ※クリエイティブモードかサバイバルモードかくらいの判定はできるので、サバイバルモード=op権限とするとか、工夫が必要かも
  • ワールド移動時のみ発生するイベントは無い。
    MoveEntityEvent.Teleportでワールド名が変わったら、、、で判定するしかなさそう。
  • イベントリスナのメソッドでは引数に書いたイベントクラスのみをフックするが、イベントクラスを「Event」にするとすべてのイベントがフックできる。何イベントが呼ばれたかはevent.getClass().getName()で判断可能。
    @Listener
    public void onSomeEvent( Event event) {
    getLogger().info(event.getClass().getName());
    }
    ※注意:但し、spongeイベントだけでなく、forgeのイベントなどもすべて拾うようで、ものすごい数のイベントがでます。

 

コメントを残す

これらのHTMLタグが利用可能です

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

  

  

  

Time limit is exhausted. Please reload CAPTCHA.

A sample text widget

Etiam pulvinar consectetur dolor sed malesuada. Ut convallis euismod dolor nec pretium. Nunc ut tristique massa.

Nam sodales mi vitae dolor ullamcorper et vulputate enim accumsan. Morbi orci magna, tincidunt vitae molestie nec, molestie at mi. Nulla nulla lorem, suscipit in posuere in, interdum non magna.