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