# 3. カスタムコマンドを実装する

{% hint style="info" %}
こちらは 1. 2. のチュートリアルが完了していることが前提の内容となっております。
{% endhint %}

## はじめに

続いて Diarkis のカスタマイズ性を体験するために、カスタムコマンドを実装してみましょう。

本チュートリアルでは以下のようなコマンドを実装します。

* Room の敵に攻撃するコマンド
* コマンドバージョン: 2 （0, 1 はビルトインコマンドで利用されています）
* コマンド ID: 300
* リクエスト: 攻撃タイプ (type) を指定してコマンドを送信 (1: 近接、2:遠距離)
* レスポンス: 誰がダメージを与えたか、ダメージ値と合計ダメージ値を返却
* ルームメンバー全員に push 通知

実装の流れは以下を想定しています。

* puffer (Diarkis が提供するデータシリアライザ) を使ってペイロードを生成するためのコードを出
* サーバーにカスタムコマンドを実装
* テストクライアントにコマンドを実装

## Puffer モジュール（Diarkis が提供する データシリアライザ）

Puffer は JSON からデータをシリアライズ、デシリアライズするためのコードを生成するツールです。

出力先は C++、C#、Go に対応しているため、Unreal や Unity で開発するクライアントと、Go で開発するサーバーとのパケットデータに関する実装を簡略化できます。

Puffer の詳細については API リファレンスをご確認ください。

<https://docs.diarkis.io/docs/server/v1.0.0/diarkis/puffer/index.html>

### カスタムコマンドのデータ定義用 JSON ファイルの作成

💾 ./puffer/json\_definitions/custom/attack.json を追加します。

**attack** はカスタムコマンドのリクエストに利用するデータです。

* type: 攻撃タイプを uint8 型で格納できます

**attackResult** はレスポンス、ルームメンバーへの push 通知に使うデータです。

* type: 攻撃タイプを uint8 型で格納できます
* uid: 誰がダメージを与えたか。 string 型
* damage: ダメージ値。 uint16 型
* totalDamage: 合計ダメージ値。 uint16 型

```json
{
  "attack": {
    "ver": 2,
    "cmd": 300,
    "package": "custom",
    "properties": {
      "type": "u8"
    }
  },
  "attackResult": {
    "ver": 2,
    "cmd": 300,
    "package": "custom",
    "properties": {
      "type": "u8",
      "uid": "string",
      "damage": "u16",
      "totalDamage": "u16"
    }
  }
}
```

### コード生成

以下のコマンドを実行して、コードを生成します。puffer 以下の各言語のディレクトリにコードが出力されます。

```bash
$ make gen
# puffer 以下の各言語のディレクトリにコードが出力されます
puffer
├── cpp/custom/Attack.h
├── cpp/custom/AttackResult.h
├── cs/custom/Attack.cs
├── cs/custom/AttackResult.cs
├── go/custom/attack.go
└── go/custom/attackresult.go
```

## サーバーに attack コマンドハンドラを実装する

サーバーに attack コマンドを実装しましょう。

💾 ./cmds/custom/attack.go を追加します。

コード内のコメントにて、どんなことを実装しているか記載していますので、合わせてお読みください。

{% code overflow="wrap" fullWidth="true" %}

```go
package customcmds

import (
	"errors"
	// pattack は先程生成した puffer のパッケージです。
	pattack "handson/puffer/go/custom"

	"github.com/Diarkis/diarkis/derror"
	"github.com/Diarkis/diarkis/room"
	"github.com/Diarkis/diarkis/server"
	"github.com/Diarkis/diarkis/user"
	"github.com/Diarkis/diarkis/util"
)

// attack は Room 内の敵に対して攻撃します。
// 
// Diarkis の全てのコマンドは決まった引数を取ります。
// - ver: コマンドのバージョン
// - cmd: コマンド ID
// - payload: コマンドにわたすデータ
// - userData: ユーザーデータ
func attack(ver uint8, cmd uint16, payload []byte, userData *user.User, next func(error)) {

	// attack コマンドは Room の敵に攻撃するコマンドなので、
	// Room に参加していない場合はエラーとなります。
	// Room に参加しているかどうかは roomID を取得して確認します。
	roomID := room.GetRoomID(userData)
	// room に参加していない場合は、 roomID は空で返却されるので、エラーハンドリングを行います
	if roomID == "" {
		// ユーザーに userData.ServerRespond() でエラーのレスポンスを返します。
		err := errors.New("not in the room")
		userData.ServerRespond(derror.ErrData(err.Error(), derror.NotAllowed(0)), ver, cmd, server.Bad, true)
		// レスポンスを返したら next(err) して return します。
		next(err)
		return
	}

	// pattack.NewAttack() したあとに、req.Unpack(payload) することで
	// ペイロードをデシリアライズできます。
	req := pattack.NewAttack()
	req.Unpack(payload)

	// Type を元にダメージを計算します。
	damage := 0
	switch req.Type {
	case 1: // 近接攻撃 (D20)
		damage = util.RandomInt(1, 20)
	case 2: // 遠距離攻撃 (D12 + 3)
		damage = util.RandomInt(1, 12) + 3
	default:
		err := errors.New("invalid attack type")
		userData.ServerRespond(derror.ErrData(err.Error(), derror.InvalidParameter(0)), ver, cmd, server.Bad, true)
		next(err)
		return
	}

	// 算出したダメージを敵の合計ダメージに加算します。
	// Room に Property として情報を保存することができます。
	// ここでは "DAMAGE" というキーに対してダメージを加算しています。
	// room.IncrProperty を使うと加算後の数値を取得することができます。
	// その他にも Property を扱うための関数が用意されています。詳細は以下をご覧ください。
	// https://docs.diarkis.io/docs/server/v1.0.0/diarkis/room/index.html
	updatedDamage, updated := room.IncrProperty(roomID, "DAMAGE", int64(damage))
	if !updated {
		err := errors.New("incr property failed")
		userData.ServerRespond(derror.ErrData(err.Error(), derror.Internal(0)), ver, cmd, server.Err, true)
		next(err)
		return
	}
	logger.Info("Room %s has been attacked by %s using %s attack by %d damage. Total damage: %d", roomID, userData.ID, req.Type, damage, updatedDamage)

	// 誰がダメージを与えたか、ダメージ値と合計ダメージ値を返却し、ルームメンバーに通知します。
	// res := pattack.NewAttackResult() したあとに返却するデータをセットします。
	res := pattack.NewAttackResult()
	res.Type = req.Type
	res.Uid = userData.ID
	res.Damage = uint16(damage)
	res.TotalDamage = uint16(updatedDamage)

	// コマンドを実行したユーザーには userData.ServerRespond()
	// 他のルームメンバーには room.Relay() を使って結果を通知します。
	userData.ServerRespond(res.Pack(), ver, cmd, server.Ok, true)
	room.Relay(roomID, userData, ver, cmd, res.Pack(), true)
	// レスポンスを返したら next(nil) して return します。
	next(nil)
}
```

{% endcode %}

### attack コマンドを公開する

コマンドを実装したら、外部に公開する必要があります。

💾 ./cmds/custom/main.go を編集し、以下を追加します。

`Expose()` に対して、 `diarkisexec.SetServerCommandHandler()` を追加して、attack コマンドを公開します。

```diff
diff --git a/cmds/custom/main.go b/cmds/custom/main.go
index f2374ed..e98f5ef 100644
--- a/cmds/custom/main.go
+++ b/cmds/custom/main.go
@@ -37,6 +37,7 @@ func Expose() {
        diarkisexec.SetServerCommandHandler(custom.GetFieldInfoVer, custom.GetFieldInfoCmd, getFieldInfo)
        diarkisexec.SetServerCommandHandler(CustomVer, getUserStatusListCmdID, getUserStatusList)
        diarkisexec.SetServerCommandHandler(CustomVer, resonanceCmdID, resonanceCmd)
+       diarkisexec.SetServerCommandHandler(custom.AttackVer, custom.AttackCmd, attack)
 }
```

## テストクライアントに attack コマンドを追加する

サーバーの attack コマンドを実行するためにテストクライアントに attack コマンドを追加します。

💾 ./testcli/handson/attack.go を追加します。

クライアントを実行するための setup の他、サーバーにコマンドを送信する Attack() 、レスポンスをハンドリングする onResponse()、push通知をハンドリングする onPush() を実装しています。

```go
package handson

import (
	"fmt"

	pattack "handson/puffer/go/custom"

	"github.com/Diarkis/diarkis/client/go/tcp"
	"github.com/Diarkis/diarkis/client/go/udp"
)

type Handson struct {
	tcp *tcp.Client
	udp *udp.Client
}

func SetupHandsonAsTCP(c *tcp.Client) *Handson {
	h := &Handson{tcp: c}
	h.setup()
	return h
}

func SetupHandsonAsUDP(c *udp.Client) *Handson {
	h := &Handson{udp: c}
	h.setup()
	return h
}

func (h *Handson) setup() {
	if h.tcp != nil {
		h.tcp.OnResponse(h.onResponse)
		h.tcp.OnPush(h.onPush)
		return
	}
	if h.udp != nil {
		h.udp.OnResponse(h.onResponse)
		h.udp.OnPush(h.onPush)
	}
}

func (h *Handson) onResponse(ver uint8, cmd uint16, status uint8, payload []byte) {
	if ver != pattack.AttackVer || cmd != pattack.AttackCmd {
		return
	}
	if status != uint8(1) {
		fmt.Printf("Attack failed: %v\n", string(payload))
		return
	}
	res := pattack.NewAttackResult()
	err := res.Unpack(payload)
	if err != nil {
		fmt.Printf("Failed to unpack attack response: %v\n", err)
		return
	}
	fmt.Printf("You dealt %d damage. Total damage: %d\n", res.Damage, res.TotalDamage)
}

func (h *Handson) onPush(ver uint8, cmd uint16, payload []byte) {
	// no push
	if ver != pattack.AttackVer || cmd != pattack.AttackCmd {
		return
	}
	res := pattack.NewAttackResult()
	err := res.Unpack(payload)
	if err != nil {
		fmt.Printf("Failed to unpack attack response: %v\n", err)
	}
	fmt.Printf("%s dealt %d damage. Total damage: %d\n", res.Uid, res.Damage, res.TotalDamage)
}

func (h *Handson) Attack(attackType uint8) {
	req := pattack.NewAttack()
	req.Type = attackType
	if h.tcp != nil {
		h.tcp.Send(pattack.AttackVer, pattack.AttackCmd, req.Pack())
		return
	}
	if h.udp != nil {
		h.udp.Send(pattack.AttackVer, pattack.AttackCmd, req.Pack())
	}
}
```

テストクライアントに `handson attack` コマンドを登録します。

💾 ./testcli/main.go を編集します。

cli.RegisterCommands(string, \[]cli.Command) でテストクライアントに新しいコマンドを登録できます。

```diff
diff --git a/testcli/main.go b/testcli/main.go
index bdca4ab..2dbdd2c 100644
--- a/testcli/main.go
+++ b/testcli/main.go
@@ -3,8 +3,11 @@ package main
 import (
        "bufio"
        "fmt"
+       "handson/testcli/handson"
        "handson/testcli/resonance"
        "os"
+       "strconv"
+       "strings"

        "github.com/Diarkis/diarkis/client/go/test/cli"
 )
@@ -12,17 +15,24 @@ import (
 var (
        tcpResonance *resonance.Resonance
        udpResonance *resonance.Resonance
+       tcpHandson   *handson.Handson
+       udpHandson   *handson.Handson
 )

 func main() {
        cli.SetupBuiltInCommands()
        cli.RegisterCommands("test", []cli.Command{{CmdName: "resonate", Desc: "Resonate your message", CmdFunc: resonate}})
+       cli.RegisterCommands("handson", []cli.Command{
+               {CmdName: "attack", Desc: "Attack for hands-on", CmdFunc: attack},
+       })
        cli.Connect()
        if cli.TCPClient != nil {
                tcpResonance = resonance.SetupAsTCP(cli.TCPClient)
+               tcpHandson = handson.SetupHandsonAsTCP(cli.TCPClient)
        }
        if cli.UDPClient != nil {
                udpResonance = resonance.SetupAsUDP(cli.UDPClient)
+               udpHandson = handson.SetupHandsonAsUDP(cli.UDPClient)
        }
        cli.Run()
 }
@@ -45,3 +55,30 @@ func resonate() {
                udpResonance.Resonate(message)
        }
 }
+
+func attack() {
+       reader := bufio.NewReader(os.Stdin)
+       fmt.Println("Which client to join a room? [tcp/udp]")
+       client, _ := reader.ReadString('\n')
+       fmt.Println("Enter the attack type. [1: melee, 2: range]")
+       attackTypeStr, _ := reader.ReadString('\n')
+       attackTypeStr = strings.Trim(attackTypeStr, "\n")
+       attackType, err := strconv.Atoi(attackTypeStr)
+       if err != nil || attackType != 1 && attackType != 2 {
+               fmt.Println("Invalid attack type.")
+               return
+       }
+
+       switch client {
+       case "tcp\n":
+               if tcpHandson == nil {
+                       return
+               }
+               tcpHandson.Attack(uint8(attackType))
+       case "udp\n":
+               if udpHandson == nil {
+                       return
+               }
+               udpHandson.Attack(uint8(attackType))
+       }
+}
```

バイナリを生成し、udp サーバーを再起動する

再度 `make build-local` を実行してバイナリを生成します。

サーバーとテストクライアントが更新されます。

エラーが無いことが確認できたら、一度 udp サーバーを停止して、再起動してください。

## attack コマンドの動作確認

`room create` `room join` した後に `handson attack` コマンドを発行し、ダメージが加算されることを確認してください。

### uid: test1 で handson attack を実行

```
> handson attack
Which client to join a room? [tcp/udp]
udp
Enter the attack type. [1: melee, 2: range]
1
You dealt 13 damage. Total damage: 13
```

### uid: test2 で attack 結果の push 通知を受信

```
[UID: test2][SID(UDP): zzzz][RoomID: yyyy]
 > test1 dealt 13 damage. Total damage: 13
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://help.diarkis.io/getting-started/tutorial/implement-custom-command.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
