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
Code Generation
Execute the following command to generate code. The code will be output to the directory of each language under puffer.
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.
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.
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().
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).
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
Receive push notification of attack result with uid: test2
$ 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
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)
}