LogoLogo
English
English
  • Diarkis Help Center
  • Overview of Diarkis
  • Getting Started
    • Diarkis Server Template
    • Diarkis Client SDK
    • Tutorials
      • 1. Launch Diarkis Server in Local Environment
      • 2. Perform Connectivity Check with Test Client
      • 3. Implement Custom Command
      • Connect to Server from Diarkis Client
    • Samples
  • Diarkis Modules
    • Room Module
      • Set Up Room Module on Server
      • Room Sample
        • room_broadcast
      • Utilizing Room Module from Client
      • Additional Features of Room
    • MatchMaker Module
      • Set Up MatchMaker Module on Server
    • Field Module
      • Set Up Field Module on Server
    • P2P Module
      • Set Up P2P Module on Server
      • P2P Sample
    • DM (Direct Message) Module
      • Set Up DM Module on Server
    • Notifier Module
      • Set Up Notifier Module on Server
    • Session Module
      • Set Up Session Module on Server
    • Group Module
      • Set Up Group Module on Server
  • Diarkis Server
    • Launch Diarkis Server in Cloud Environment
      • AWS
    • Launch Diarkis Server on Windows Environment
    • MARS Server
    • UDP Server
    • TCP Server
    • HTTP Server
    • Metrics API
    • Inter-server Communication - Mesh
  • Diarkis Client
    • Runtime Library
      • Diarkis RUDP
    • Diarkis Module
      • Initialization and Termination of Diarkis Module
      • Customization of Diarkis Module
      • Logging System of Diarkis Module
      • Migration
      • Threads of Diarkis
    • Samples
      • C++
        • room_broadcast
        • directmessage_simple
        • group_sample
        • matching_and_turn
        • matchmaker_ticket
        • p2p_rudp_sample
        • session_simple
      • Unreal Engine Plugin
        • FieldWalker
      • Unity Plugin
        • FieldWalker
          • HowToReplicatePosition.md
  • Diarkis Tools
    • Diarkis CLI
      • Procedures to Switch to Diarkis CLI v3
  • References
    • API Reference
    • Release Notes
      • v1.0
      • v1.0.1
      • v1.0.2
      • v1.0.3
      • v1.0.4
      • v1.0.5
      • v1.0.6
  • Support
    • License and Billing
Powered by GitBook
On this page
  • Introduction
  • Puffer Module (Data Serializer provided by Diarkis)
  • Creating a JSON File for Custom Command Data Definition
  • Code Generation
  • Implementing the Attack Command Handler on the Server
  • Publishing the Attack Command
  • Adding the Attack Command to the Test Client
  • Verifying the attack command
  • Execute handson attack with uid: test1
  • Receive push notification of attack result with uid: test2

Was this helpful?

  1. Getting Started
  2. Tutorials

3. Implement Custom Command

This section assumes you have completed Tutorials 1 and 2.

Introduction

Let's continue to explore the customizability of Diarkis by implementing a custom command.

In this tutorial, we will implement the following command:

  • A command to attack enemies in a Room

  • Command version: 2 (versions 0 and 1 are used for built-in commands)

  • Command ID: 300

  • Request: Send a command specifying an attack type (type) (1: Melee, 2: Ranged)

  • Response: Return who dealt damage, the damage value, and the total damage value

  • Push notifications to all room members

The implementation flow is as follows:

  • Write code to generate a payload using puffer (a data serializer provided by Diarkis)

  • Implement the custom command on the server

  • Implement the command in the test client

Puffer Module (Data Serializer provided by Diarkis)

Puffer is a tool that generates code to serialize and deserialize data from JSON.

The output supports C++, C#, and Go, simplifying packet data implementation between clients developed in Unreal or Unity and servers developed in Go.

For more details on Puffer, please refer to the API reference.

Creating a JSON File for Custom Command Data Definition

💾 Add ./puffer/json_definitions/custom/attack.json.

attack is the data used for the custom command's request.

  • type: The attack type can be stored as a uint8

attackResult is the data used for responses and push notifications to room members.

  • type: The attack type can be stored as a uint8

  • uid: Who dealt the damage. Stored as a string

  • damage: Damage value. Stored as uint16

  • totalDamage: Total damage value. Stored as uint16

{
  "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"
    }
  }
}

Code Generation

Execute the following command to generate code. The code will be output to the directory of each language under puffer.

$ make gen
# Code will be output to each language directory under 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

Implementing the Attack Command Handler on the Server

Let's implement the attack command on the server.

💾 Add ./cmds/custom/attack.go.

Comments within the code explain what is being implemented, so please read them as well.

package customcmds

import (
	"errors"
	// pattack is the package generated from 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"
)

// The attack function attacks enemies within the Room.
// 
// All commands in Diarkis take specific arguments:
// - ver: Command version
// - cmd: Command ID
// - payload: Data passed to the command
// - userData: User data
func attack(ver uint8, cmd uint16, payload []byte, userData *user.User, next func(error)) {

	// Since attack is a command to attack enemies in a Room,
	// an error will occur if the user is not in a Room.
	// Whether the user is in a Room is checked by obtaining the roomID.
	roomID := room.GetRoomID(userData)
	// If not in a room, roomID will be empty, so error handling is performed
	if roomID == "" {
		// Return an error response to the user with userData.ServerRespond().
		err := errors.New("not in the room")
		userData.ServerRespond(derror.ErrData(err.Error(), derror.NotAllowed(0)), ver, cmd, server.Bad, true)
		// After returning the response, execute next(err) and return.
		next(err)
		return
	}

	// After calling pattack.NewAttack(), the payload can be deserialized with req.Unpack(payload).
	req := pattack.NewAttack()
	req.Unpack(payload)

	// Calculate damage based on the Type.
	damage := 0
	switch req.Type {
	case 1: // Melee attack (D20)
		damage = util.RandomInt(1, 20)
	case 2: // Ranged attack (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
	}

	// Add the calculated damage to the enemy's total damage.
	// Information can be saved as a Property in the Room.
	// Here, damage is added to the key "DAMAGE".
	// The add operation returns the new value with room.IncrProperty.
	// There are other functions for handling properties. Details can be found below.
	// 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)

	// Return who dealt the damage, the damage value, and the total damage value, and notify room members.
	// Create a new response with res := pattack.NewAttackResult() and set the data to be returned.
	res := pattack.NewAttackResult()
	res.Type = req.Type
	res.Uid = userData.ID
	res.Damage = uint16(damage)
	res.TotalDamage = uint16(updatedDamage)

	// Use userData.ServerRespond() for the user who executed the command,
	// and room.Relay() to notify other room members of the result.
	userData.ServerRespond(res.Pack(), ver, cmd, server.Ok, true)
	room.Relay(roomID, userData, ver, cmd, res.Pack(), true)
	// After returning the response, execute next(nil) and return.
	next(nil)
}

Publishing the Attack Command

Once the command is implemented, it needs to be published externally.

Edit ./cmds/custom/main.go and add the following.

By adding diarkisexec.SetServerCommandHandler() to Expose(), publish the attack command.

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)
 }

Adding the Attack Command to the Test Client

To execute the server's attack command, add the command to the test client.

Add ./testcli/handson/attack.go.

This includes setup to execute the client, sending the command to the server with Attack(), handling the response with onResponse(), and handling push notifications with onPush().

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())
	}
}

Register the handson attack command to the test client.

Edit ./testcli/main.go.

You can register a new command to the test client with cli.RegisterCommands(string, []cli.Command).

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))
+       }
+}

Generate binary and restart the udp server

Run make build-local again to generate the binary.

The server and test client will be updated.

If no errors are found, stop and restart the udp server.

Verifying the attack command

After room create and room join, issue the handson attack command and confirm that the damage is accumulated.

Execute handson attack with uid: test1

> 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

Receive push notification of attack result with uid: test2

[UID: test2][SID(UDP): zzzz][RoomID: yyyy]
 > test1 dealt 13 damage. Total damage: 13
Previous2. Perform Connectivity Check with Test ClientNextConnect to Server from Diarkis Client

Last updated 1 month ago

Was this helpful?

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