17
1

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のサーバーをDiscordから遠隔起動する

Last updated at Posted at 2023-12-03

この記事は、筑波NSミライラボ Advent Calendar 2023、4日目の記事です。

はじめに

N・S高等学校通学コースに所属している @ramenha0141 です。
この記事では、私が開発したMinecraftのサーバー管理アプリケーションについて紹介していきます。

目的

  • 本来Minecraftのサーバーを構築するにはある程度の知識(ポート解放など)が必要なため、知識がなくても簡単にサーバーを構築して管理できるアプリケーションが欲しい
  • 必要な時だけサーバーを起動しておきたいが、誰かがプレイする際に毎回起動を行うのが大変な場合に、管理者以外が遠隔でサーバーを起動できるようにしたい

作ったもの(未完成)

サーバー作成画面:

スクリーンショット

管理画面:

スクリーンショット

技術選定

Electron

  • ReactなどのWeb技術を用いてデスクトップアプリケーションを開発することができるフレームワーク
  • メインプロセス用にNode.jsと、レンダラープロセス用にChromiumを内蔵している
  • VSCode, Discord などで採用されている
  • 当初、Chromiumの代わりにOS組み込みのWebviewを用いることでアプリケーションのサイズを軽量化したフレームワークである Tauri を採用していたが、JavaScript・Rust間の通信が複雑だったり、そもそもRustの理解度が足りなかったりしたため、Electronに移行した
  • メインプロセス側のランタイムがNode.jsなので、Discord.jsと組み合わせやすいのも利点

React

  • 宣言的UIを実現するための描画ライブラリ

React Router

  • クライアント側でのルーティングを実現するライブラリ
  • 当初はNext.jsやRemixも検討していたが、Electronと組み合わせる場合クライアント側で完結した方が楽そうだったので採用

TailwindCSS

  • クラス名でスタイルを指定するCSSライブラリ
  • 後述するshadcn/uiとでも用いられているため、相性を考えて採用

shadcn/ui

  • TailwindCSSとRadixUIをベースとしたコンポーネントライブラリ(厳密には違う)
  • 最近かなり勢いのあるライブラリで、Vercelなどで採用例が多い(開発者がVercelに入社した)

React Hook Form

  • React用のフォーム管理ライブラリ
  • フォームの処理を簡素化するために使用

Zod

  • Typescriptファーストのスキーマ宣言・検証ライブラリ
  • コンフィグファイルの検証に使用している

Discord.js

  • JavaScriptでDiscordBotを操作するためのライブラリ

機能

サーバー作成

サーバーを一からセットアップする方法と既存のサーバーをインポートする方法を実装しました。今回はセットアップについて紹介していきます。

セットアップ

スクリーンショット

サーバー名、パス、バージョンを指定してEULAに同意すると、適切なバージョンのサーバーJarをダウンロードします。

サーバーJarのダウンロードURLは https://launchermeta.mojang.com/mc/game/version_manifest.json から取得しています。

Discord連携

スクリーンショット

Discord Developer Portal からBotを作成し、ApplicationIdとTokenを設定することで、Botが参加しているDiscordサーバーにおいて遠隔起動・停止・ステータス確認のコマンドが有効になります。

スクリーンショット

これは、Discord.jsのSlashCommandを登録し、InteractionCreateイベントをサブスクライブしてコマンドに応じた応答を返すことで実現しています。

const rest = new REST({ version: '10' }).setToken(config.discord.token);

const status = new SlashCommandBuilder()
    .setName('mc-status')
    .setDescription(`Get status of ${info.name}`);
const start = new SlashCommandBuilder().setName('mc-start').setDescription(`Start ${info.name}`);
const stop = new SlashCommandBuilder().setName('mc-stop').setDescription(`Stop ${info.name}`);
const commands = [status, start, stop];

for (const guild of client.guilds.cache) {
    try {
        await rest.put(Routes.applicationGuildCommands(config.discord.applicationId, guild[1].id), {
            body: commands
        });
        console.log(`discord: registered commands for ${guild[1].name}`);
    } catch (e) {
        console.error(e);
    }
}

function handleInteraction(server: Server) {
    return async (interaction: Interaction) => {
        if (!interaction.isChatInputCommand()) return;

        switch (interaction.commandName) {
            case 'mc-status': {
                await interaction.reply(
                    `Status of ${server.info.name}: ${server.status}`,
                );
                break;
            }
            case 'mc-start': {
                if (server.status === 'idle') {
                    await interaction.reply(`Starting ${server.info.name}...`);
                    if (await server.start()) {
                        await interaction.followUp(
                            `Started ${server.info.name}.`,
                        );
                    } else {
                        await interaction.followUp(
                            `Failed to start ${server.info.name}.`,
                        );
                    }
                } else {
                    await interaction.reply(
                        `${server.info.name} is already running.`,
                    );
                }
                break;
            }
            case 'mc-stop': {
                if (server.status === 'running') {
                    await interaction.reply(`Stopping ${server.info.name}...`);
                    if (await server.stop()) {
                        await interaction.followUp(
                            `Stopped ${server.info.name}.`,
                        );
                    } else {
                        await interaction.followUp(
                            `Failed to stop ${server.info.name}.`,
                        );
                    }
                } else {
                    await interaction.reply(
                        `${server.info.name} is not running.`,
                    );
                }
                break;
            }
        }
    };
}

今後の予定

今後は、コンソール、パフォーマンス監視、バックアップなどの機能を実装し、実用的なアプリケーションにしていきたいと思います。
また、現在は遠隔での操作にDiscordを用いていますが、このアプリケーションは基本的にWebの技術を用いて作成されているため、WebSocketなどを用いてブラウザ上で同じGUIでの操作を実現することも視野に入れています。

最後に

今回のアドベントカレンダーでは、もともと自作言語コンパイラについて書こうと思っていたのですが、なかなかモチベーションが上がらず、そのときに欲しいと思ったものを作成することに決め、Minecraftのサーバー管理アプリケーションを開発するに至りました。
途中までTauriで実装していたものを移行したり、ステート管理をレンダラープロセス側で行っていたものを、明確にフロントエンド・バックエンドで分離するなど、アーキテクチャの変更を余儀なくされることもありましたが、まだ未完成とはいえ、記事にする段階まで持っていくことができて満足しています。

また、このような機会を頂いたN・S高等学校並びに筑波大学情報学群情報科学類の皆様に感謝します。

明日は、@Ryoga-exe さんの「CSS だけでモンテカルロ法」です。

17
1
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
17
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?