up3
Build and deploy / Build (push) Has been cancelled
Build and deploy / Update Contributors (push) Has been cancelled
Build and deploy / Deploy (push) Has been cancelled
Build and deploy / Build (push) Has been cancelled
Build and deploy / Update Contributors (push) Has been cancelled
Build and deploy / Deploy (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/cespare/xxhash/v2"
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/df-mc/dragonfly/server/world/chunk"
|
||||
"github.com/sandertv/gophertunnel/minecraft/nbt"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
)
|
||||
|
||||
// subChunkRequests is set to true to enable the sub-chunk request system. This can (likely) cause unexpected issues,
|
||||
// but also solves issues with block entities such as item frames and lecterns as of v1.19.10.
|
||||
const subChunkRequests = true
|
||||
|
||||
// ViewChunk ...
|
||||
func (s *Session) ViewChunk(pos world.ChunkPos, dim world.Dimension, blockEntities map[cube.Pos]world.Block, c *chunk.Chunk) {
|
||||
if !s.conn.ClientCacheEnabled() {
|
||||
s.sendNetworkChunk(pos, dim, c, blockEntities)
|
||||
return
|
||||
}
|
||||
s.sendBlobHashes(pos, dim, c, blockEntities)
|
||||
}
|
||||
|
||||
// ViewSubChunks ...
|
||||
func (s *Session) ViewSubChunks(centre world.SubChunkPos, offsets []protocol.SubChunkOffset, tx *world.Tx) {
|
||||
r := tx.Range()
|
||||
|
||||
entries := make([]protocol.SubChunkEntry, 0, len(offsets))
|
||||
transaction := make(map[uint64]struct{})
|
||||
for _, offset := range offsets {
|
||||
ind := int16(centre.Y()) + int16(offset[1]) - int16(r[0])>>4
|
||||
if ind < 0 || ind > int16(r.Height()>>4) {
|
||||
entries = append(entries, protocol.SubChunkEntry{Result: protocol.SubChunkResultIndexOutOfBounds, Offset: offset})
|
||||
continue
|
||||
}
|
||||
col, ok := s.chunkLoader.Chunk(world.ChunkPos{
|
||||
centre.X() + int32(offset[0]),
|
||||
centre.Z() + int32(offset[2]),
|
||||
})
|
||||
if !ok {
|
||||
entries = append(entries, protocol.SubChunkEntry{Result: protocol.SubChunkResultChunkNotFound, Offset: offset})
|
||||
continue
|
||||
}
|
||||
entries = append(entries, s.subChunkEntry(offset, ind, col, transaction))
|
||||
}
|
||||
if s.conn.ClientCacheEnabled() && len(transaction) > 0 {
|
||||
s.blobMu.Lock()
|
||||
s.openChunkTransactions = append(s.openChunkTransactions, transaction)
|
||||
s.blobMu.Unlock()
|
||||
}
|
||||
dim, _ := world.DimensionID(tx.World().Dimension())
|
||||
s.writePacket(&packet.SubChunk{
|
||||
Dimension: int32(dim),
|
||||
Position: protocol.SubChunkPos(centre),
|
||||
CacheEnabled: s.conn.ClientCacheEnabled(),
|
||||
SubChunkEntries: entries,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Session) subChunkEntry(offset protocol.SubChunkOffset, ind int16, col *world.Column, transaction map[uint64]struct{}) protocol.SubChunkEntry {
|
||||
chunkMap := col.HeightMap()
|
||||
subMapType, subMap := byte(protocol.HeightMapDataHasData), make([]int8, 256)
|
||||
higher, lower := true, true
|
||||
for x := uint8(0); x < 16; x++ {
|
||||
for z := uint8(0); z < 16; z++ {
|
||||
y, i := chunkMap.At(x, z), (uint16(z)<<4)|uint16(x)
|
||||
otherInd := col.SubIndex(y)
|
||||
switch {
|
||||
case otherInd > ind:
|
||||
subMap[i], lower = 16, false
|
||||
case otherInd < ind:
|
||||
subMap[i], higher = -1, false
|
||||
default:
|
||||
subMap[i], lower, higher = int8(y-col.SubY(otherInd)), false, false
|
||||
}
|
||||
}
|
||||
}
|
||||
if higher {
|
||||
subMapType, subMap = protocol.HeightMapDataTooHigh, nil
|
||||
} else if lower {
|
||||
subMapType, subMap = protocol.HeightMapDataTooLow, nil
|
||||
}
|
||||
|
||||
sub := col.Sub()[ind]
|
||||
if sub.Empty() {
|
||||
return protocol.SubChunkEntry{
|
||||
Result: protocol.SubChunkResultSuccessAllAir,
|
||||
HeightMapType: subMapType,
|
||||
HeightMapData: subMap,
|
||||
RenderHeightMapType: subMapType,
|
||||
RenderHeightMapData: subMap,
|
||||
Offset: offset,
|
||||
}
|
||||
}
|
||||
|
||||
serialisedSubChunk := chunk.EncodeSubChunk(col.Chunk, chunk.NetworkEncoding, int(ind))
|
||||
|
||||
blockEntityBuf := bytes.NewBuffer(nil)
|
||||
enc := nbt.NewEncoderWithEncoding(blockEntityBuf, nbt.NetworkLittleEndian)
|
||||
for pos, b := range col.BlockEntities {
|
||||
if n, ok := b.(world.NBTer); ok && col.SubIndex(int16(pos.Y())) == ind {
|
||||
d := n.EncodeNBT()
|
||||
d["x"], d["y"], d["z"] = int32(pos[0]), int32(pos[1]), int32(pos[2])
|
||||
_ = enc.Encode(d)
|
||||
}
|
||||
}
|
||||
|
||||
entry := protocol.SubChunkEntry{
|
||||
Result: protocol.SubChunkResultSuccess,
|
||||
RawPayload: append(serialisedSubChunk, blockEntityBuf.Bytes()...),
|
||||
HeightMapType: subMapType,
|
||||
HeightMapData: subMap,
|
||||
RenderHeightMapType: subMapType,
|
||||
RenderHeightMapData: subMap,
|
||||
Offset: offset,
|
||||
}
|
||||
if s.conn.ClientCacheEnabled() {
|
||||
if hash := xxhash.Sum64(serialisedSubChunk); s.trackBlob(hash, serialisedSubChunk) {
|
||||
transaction[hash] = struct{}{}
|
||||
|
||||
entry.BlobHash = hash
|
||||
entry.RawPayload = blockEntityBuf.Bytes()
|
||||
}
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
// dimensionID returns the dimension ID of the world that the session is in.
|
||||
func (s *Session) dimensionID(dim world.Dimension) int32 {
|
||||
d, _ := world.DimensionID(dim)
|
||||
return int32(d)
|
||||
}
|
||||
|
||||
// sendBlobHashes sends chunk blob hashes of the data of the chunk and stores the data in a map of blobs. Only
|
||||
// data that the client doesn't yet have will be sent over the network.
|
||||
func (s *Session) sendBlobHashes(pos world.ChunkPos, dim world.Dimension, c *chunk.Chunk, blockEntities map[cube.Pos]world.Block) {
|
||||
if subChunkRequests {
|
||||
biomes := chunk.EncodeBiomes(c, chunk.NetworkEncoding)
|
||||
if hash := xxhash.Sum64(biomes); s.trackBlob(hash, biomes) {
|
||||
s.writePacket(&packet.LevelChunk{
|
||||
Dimension: s.dimensionID(dim),
|
||||
SubChunkCount: protocol.SubChunkRequestModeLimited,
|
||||
Position: protocol.ChunkPos(pos),
|
||||
HighestSubChunk: c.HighestFilledSubChunk(),
|
||||
BlobHashes: []uint64{hash},
|
||||
RawPayload: []byte{0},
|
||||
CacheEnabled: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
data = chunk.Encode(c, chunk.NetworkEncoding)
|
||||
count = uint32(len(data.SubChunks))
|
||||
blobs = append(data.SubChunks, data.Biomes)
|
||||
hashes = make([]uint64, len(blobs))
|
||||
m = make(map[uint64]struct{}, len(blobs))
|
||||
)
|
||||
for i, blob := range blobs {
|
||||
h := xxhash.Sum64(blob)
|
||||
hashes[i], m[h] = h, struct{}{}
|
||||
}
|
||||
|
||||
s.blobMu.Lock()
|
||||
s.openChunkTransactions = append(s.openChunkTransactions, m)
|
||||
if l := len(s.blobs); l > 4096 {
|
||||
s.blobMu.Unlock()
|
||||
s.conf.Log.Error("too many blobs pending", "n", l)
|
||||
return
|
||||
}
|
||||
for i := range hashes {
|
||||
s.blobs[hashes[i]] = blobs[i]
|
||||
}
|
||||
s.blobMu.Unlock()
|
||||
|
||||
// Length of 1 byte for the border block count.
|
||||
raw := bytes.NewBuffer(make([]byte, 1, 32))
|
||||
enc := nbt.NewEncoderWithEncoding(raw, nbt.NetworkLittleEndian)
|
||||
for bp, b := range blockEntities {
|
||||
if n, ok := b.(world.NBTer); ok {
|
||||
d := n.EncodeNBT()
|
||||
d["x"], d["y"], d["z"] = int32(bp[0]), int32(bp[1]), int32(bp[2])
|
||||
_ = enc.Encode(d)
|
||||
}
|
||||
}
|
||||
|
||||
s.writePacket(&packet.LevelChunk{
|
||||
Dimension: s.dimensionID(dim),
|
||||
Position: protocol.ChunkPos{pos.X(), pos.Z()},
|
||||
SubChunkCount: count,
|
||||
CacheEnabled: true,
|
||||
BlobHashes: hashes,
|
||||
RawPayload: raw.Bytes(),
|
||||
})
|
||||
}
|
||||
|
||||
// sendNetworkChunk sends a network encoded chunk to the client.
|
||||
func (s *Session) sendNetworkChunk(pos world.ChunkPos, dim world.Dimension, c *chunk.Chunk, blockEntities map[cube.Pos]world.Block) {
|
||||
if subChunkRequests {
|
||||
s.writePacket(&packet.LevelChunk{
|
||||
Dimension: s.dimensionID(dim),
|
||||
SubChunkCount: protocol.SubChunkRequestModeLimited,
|
||||
Position: protocol.ChunkPos(pos),
|
||||
HighestSubChunk: c.HighestFilledSubChunk(),
|
||||
RawPayload: append(chunk.EncodeBiomes(c, chunk.NetworkEncoding), 0),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
data := chunk.Encode(c, chunk.NetworkEncoding)
|
||||
chunkBuf := bytes.NewBuffer(nil)
|
||||
for _, s := range data.SubChunks {
|
||||
_, _ = chunkBuf.Write(s)
|
||||
}
|
||||
_, _ = chunkBuf.Write(data.Biomes)
|
||||
|
||||
// Length of 1 byte for the border block count.
|
||||
chunkBuf.WriteByte(0)
|
||||
|
||||
enc := nbt.NewEncoderWithEncoding(chunkBuf, nbt.NetworkLittleEndian)
|
||||
for bp, b := range blockEntities {
|
||||
if n, ok := b.(world.NBTer); ok {
|
||||
d := n.EncodeNBT()
|
||||
d["x"], d["y"], d["z"] = int32(bp[0]), int32(bp[1]), int32(bp[2])
|
||||
_ = enc.Encode(d)
|
||||
}
|
||||
}
|
||||
|
||||
s.writePacket(&packet.LevelChunk{
|
||||
Dimension: s.dimensionID(dim),
|
||||
Position: protocol.ChunkPos{pos.X(), pos.Z()},
|
||||
SubChunkCount: uint32(len(data.SubChunks)),
|
||||
RawPayload: append([]byte(nil), chunkBuf.Bytes()...),
|
||||
})
|
||||
}
|
||||
|
||||
// trackBlob attempts to track the given blob. If the player has too many pending blobs, it returns false and closes the
|
||||
// connection.
|
||||
func (s *Session) trackBlob(hash uint64, blob []byte) bool {
|
||||
s.blobMu.Lock()
|
||||
if l := len(s.blobs); l > 4096 {
|
||||
s.blobMu.Unlock()
|
||||
s.conf.Log.Error("too many blobs pending", "n", l)
|
||||
return false
|
||||
}
|
||||
s.blobs[hash] = blob
|
||||
s.blobMu.Unlock()
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/df-mc/dragonfly/server/cmd"
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// SendCommandOutput sends the output of a command to the player. It will be shown to the caller of the
|
||||
// command, which might be the player or a websocket server.
|
||||
func (s *Session) SendCommandOutput(output *cmd.Output, l language.Tag) {
|
||||
if s == Nop {
|
||||
return
|
||||
}
|
||||
messages := make([]protocol.CommandOutputMessage, 0, output.MessageCount()+output.ErrorCount())
|
||||
for _, message := range output.Messages() {
|
||||
om := protocol.CommandOutputMessage{Success: true, Message: message.String()}
|
||||
if t, ok := message.(translation); ok {
|
||||
om.Message, om.Parameters = t.Resolve(l), t.Params(l)
|
||||
}
|
||||
messages = append(messages, om)
|
||||
}
|
||||
for _, err := range output.Errors() {
|
||||
om := protocol.CommandOutputMessage{Message: err.Error()}
|
||||
if t, ok := err.(translation); ok {
|
||||
om.Message, om.Parameters = t.Resolve(l), t.Params(l)
|
||||
}
|
||||
messages = append(messages, om)
|
||||
}
|
||||
|
||||
s.writePacket(&packet.CommandOutput{
|
||||
CommandOrigin: s.handlers[packet.IDCommandRequest].(*CommandRequestHandler).origin,
|
||||
OutputType: packet.CommandOutputTypeAllOutput,
|
||||
SuccessCount: uint32(output.MessageCount()),
|
||||
OutputMessages: messages,
|
||||
})
|
||||
}
|
||||
|
||||
type translation interface {
|
||||
Resolve(l language.Tag) string
|
||||
Params(l language.Tag) []string
|
||||
}
|
||||
|
||||
// BuildAvailableCommands builds an AvailableCommands packet and the runnable command map for the Source
|
||||
// passed. The input map may contain aliases. It returns the AvailableCommands packet and the runnable command map
|
||||
// for the commands that the Source can execute.
|
||||
func BuildAvailableCommands(
|
||||
commands map[string]cmd.Command,
|
||||
src cmd.Source,
|
||||
softEnums map[string]struct{},
|
||||
) (*packet.AvailableCommands, map[string]map[int]cmd.Runnable) {
|
||||
m := make(map[string]map[int]cmd.Runnable, len(commands))
|
||||
|
||||
pk := &packet.AvailableCommands{}
|
||||
|
||||
var enums []commandEnum
|
||||
enumIndices := map[string]uint32{}
|
||||
|
||||
var dynamicEnums []commandEnum
|
||||
dynamicEnumIndices := map[string]uint32{}
|
||||
|
||||
suffixIndices := map[string]uint32{}
|
||||
|
||||
for alias, c := range commands {
|
||||
if c.Name() != alias {
|
||||
// Don't add duplicate entries for aliases.
|
||||
continue
|
||||
}
|
||||
|
||||
if run := c.Runnables(src); len(run) > 0 {
|
||||
m[alias] = run
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
params := c.Params(src)
|
||||
overloads := make([]protocol.CommandOverload, len(params))
|
||||
|
||||
aliasesIndex := uint32(math.MaxUint32)
|
||||
if len(c.Aliases()) > 0 {
|
||||
aliasesIndex = uint32(len(enumIndices))
|
||||
enumIndices[c.Name()+"Aliases"] = aliasesIndex
|
||||
enums = append(enums, commandEnum{Type: c.Name() + "Aliases", Options: c.Aliases()})
|
||||
}
|
||||
|
||||
for i, params := range params {
|
||||
for _, paramInfo := range params {
|
||||
t, enum := valueToParamType(paramInfo, src)
|
||||
suffix := paramInfo.Suffix
|
||||
|
||||
opt := byte(0)
|
||||
if suffix != "" {
|
||||
index, ok := suffixIndices[suffix]
|
||||
if !ok {
|
||||
index = uint32(len(pk.Suffixes))
|
||||
suffixIndices[suffix] = index
|
||||
pk.Suffixes = append(pk.Suffixes, suffix)
|
||||
}
|
||||
t = protocol.CommandArgSuffixed | index
|
||||
} else {
|
||||
t |= protocol.CommandArgValid
|
||||
if len(enum.Options) > 0 || enum.Type != "" {
|
||||
_, dynamic := softEnums[enum.Type]
|
||||
if !dynamic {
|
||||
index, ok := enumIndices[enum.Type]
|
||||
if !ok {
|
||||
index = uint32(len(enums))
|
||||
enumIndices[enum.Type] = index
|
||||
enums = append(enums, enum)
|
||||
}
|
||||
t |= protocol.CommandArgEnum | index
|
||||
} else {
|
||||
index, ok := dynamicEnumIndices[enum.Type]
|
||||
if !ok {
|
||||
index = uint32(len(dynamicEnums))
|
||||
dynamicEnumIndices[enum.Type] = index
|
||||
dynamicEnums = append(dynamicEnums, enum)
|
||||
}
|
||||
t |= protocol.CommandArgSoftEnum | index
|
||||
}
|
||||
}
|
||||
}
|
||||
overloads[i].Parameters = append(overloads[i].Parameters, protocol.CommandParameter{
|
||||
Name: paramInfo.Name,
|
||||
Type: t,
|
||||
Optional: paramInfo.Optional,
|
||||
Options: opt,
|
||||
})
|
||||
}
|
||||
}
|
||||
pk.Commands = append(pk.Commands, protocol.Command{
|
||||
Name: c.Name(),
|
||||
Description: c.Description(),
|
||||
AliasesOffset: aliasesIndex,
|
||||
PermissionLevel: protocol.CommandPermissionLevelAny,
|
||||
Overloads: overloads,
|
||||
})
|
||||
}
|
||||
|
||||
pk.DynamicEnums = make([]protocol.DynamicEnum, 0, len(dynamicEnums))
|
||||
for _, e := range dynamicEnums {
|
||||
pk.DynamicEnums = append(pk.DynamicEnums, protocol.DynamicEnum{Type: e.Type, Values: e.Options})
|
||||
}
|
||||
|
||||
enumValueIndices := make(map[string]uint32, len(enums)*3)
|
||||
pk.EnumValues = make([]string, 0, len(enumValueIndices))
|
||||
|
||||
pk.Enums = make([]protocol.CommandEnum, 0, len(enums))
|
||||
for _, enum := range enums {
|
||||
protoEnum := protocol.CommandEnum{Type: enum.Type}
|
||||
for _, opt := range enum.Options {
|
||||
index, ok := enumValueIndices[opt]
|
||||
if !ok {
|
||||
index = uint32(len(pk.EnumValues))
|
||||
enumValueIndices[opt] = index
|
||||
pk.EnumValues = append(pk.EnumValues, opt)
|
||||
}
|
||||
protoEnum.ValueIndices = append(protoEnum.ValueIndices, index)
|
||||
}
|
||||
pk.Enums = append(pk.Enums, protoEnum)
|
||||
}
|
||||
return pk, m
|
||||
}
|
||||
|
||||
// sendAvailableCommands sends all available commands of the server. Once sent, they will be visible in the
|
||||
// /help list and will be auto-completed.
|
||||
func (s *Session) sendAvailableCommands(co Controllable, softEnums map[string]struct{}) map[string]map[int]cmd.Runnable {
|
||||
commands := cmd.Commands()
|
||||
pk, m := BuildAvailableCommands(commands, co, softEnums)
|
||||
s.writePacket(pk)
|
||||
return m
|
||||
}
|
||||
|
||||
type commandEnum struct {
|
||||
Type string
|
||||
Options []string
|
||||
}
|
||||
|
||||
// valueToParamType finds the command argument type of the value passed and returns it, in addition to creating
|
||||
// an enum if applicable.
|
||||
func valueToParamType(i cmd.ParamInfo, source cmd.Source) (t uint32, enum commandEnum) {
|
||||
switch i.Value.(type) {
|
||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
|
||||
return protocol.CommandArgTypeInt, enum
|
||||
case float32, float64:
|
||||
return protocol.CommandArgTypeFloat, enum
|
||||
case string:
|
||||
return protocol.CommandArgTypeString, enum
|
||||
case cmd.Varargs:
|
||||
return protocol.CommandArgTypeRawText, enum
|
||||
case cmd.Target, []cmd.Target:
|
||||
return protocol.CommandArgTypeTarget, enum
|
||||
case bool:
|
||||
return 0, commandEnum{
|
||||
Type: "Boolean",
|
||||
Options: []string{"true", "false"},
|
||||
}
|
||||
case mgl64.Vec3:
|
||||
return protocol.CommandArgTypePosition, enum
|
||||
case cmd.SubCommand:
|
||||
return 0, commandEnum{
|
||||
Type: "SubCommand" + i.Name,
|
||||
Options: []string{i.Name},
|
||||
}
|
||||
}
|
||||
if enum, ok := i.Value.(cmd.Enum); ok {
|
||||
return 0, commandEnum{
|
||||
Type: enum.Type(),
|
||||
Options: enum.Options(source),
|
||||
}
|
||||
}
|
||||
return protocol.CommandArgTypeValue, enum
|
||||
}
|
||||
|
||||
// resendCommands resends all commands that a Session has access to if the map of runnable commands passed does not
|
||||
// match with the commands that the Session is currently allowed to execute.
|
||||
// True is returned if the commands were resent.
|
||||
func (s *Session) resendCommands(
|
||||
before map[string]map[int]cmd.Runnable,
|
||||
co Controllable,
|
||||
softEnums map[string]struct{},
|
||||
) (map[string]map[int]cmd.Runnable, bool) {
|
||||
commands := cmd.Commands()
|
||||
m := make(map[string]map[int]cmd.Runnable, len(commands))
|
||||
|
||||
for alias, c := range commands {
|
||||
if c.Name() == alias {
|
||||
if run := c.Runnables(co); len(run) > 0 {
|
||||
m[alias] = run
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(before) != len(m) {
|
||||
return s.sendAvailableCommands(co, softEnums), true
|
||||
}
|
||||
// First check for commands that were newly added.
|
||||
for name, r := range m {
|
||||
for k := range r {
|
||||
if _, ok := before[name][k]; !ok {
|
||||
return s.sendAvailableCommands(co, softEnums), true
|
||||
}
|
||||
}
|
||||
}
|
||||
// Then check for commands that a player could execute before, but no longer can.
|
||||
for name, r := range before {
|
||||
for k := range r {
|
||||
if _, ok := m[name][k]; !ok {
|
||||
return s.sendAvailableCommands(co, softEnums), true
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, false
|
||||
}
|
||||
|
||||
// enums returns a map of all enums exposed to the Session and records the values those enums currently hold.
|
||||
func (s *Session) enums(co Controllable) (map[string]cmd.Enum, map[string][]string) {
|
||||
enums, enumValues := make(map[string]cmd.Enum), make(map[string][]string)
|
||||
for alias, c := range cmd.Commands() {
|
||||
if c.Name() == alias {
|
||||
for _, params := range c.Params(co) {
|
||||
for _, paramInfo := range params {
|
||||
if enum, ok := paramInfo.Value.(cmd.Enum); ok {
|
||||
enums[enum.Type()] = enum
|
||||
enumValues[enum.Type()] = enum.Options(co)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return enums, enumValues
|
||||
}
|
||||
|
||||
// resendEnums checks the options of the enums passed against the values that were previously recorded. If they do not
|
||||
// match, and the enum is in softEnums, the enum is resent via UpdateSoftEnum. If the enum is not yet in softEnums,
|
||||
// it is added and the full AvailableCommands packet is resent.
|
||||
func (s *Session) resendEnums(
|
||||
enums map[string]cmd.Enum,
|
||||
before map[string][]string,
|
||||
softEnums map[string]struct{},
|
||||
r map[string]map[int]cmd.Runnable,
|
||||
c Controllable,
|
||||
) map[string]map[int]cmd.Runnable {
|
||||
for name, enum := range enums {
|
||||
valuesBefore := before[name]
|
||||
values := enum.Options(c)
|
||||
before[name] = values
|
||||
|
||||
changed := false
|
||||
if len(valuesBefore) != len(values) {
|
||||
changed = true
|
||||
} else {
|
||||
for k, v := range values {
|
||||
if valuesBefore[k] != v {
|
||||
changed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, dynamic := softEnums[name]; dynamic {
|
||||
s.writePacket(&packet.UpdateSoftEnum{EnumType: name, Options: values, ActionType: packet.SoftEnumActionSet})
|
||||
} else {
|
||||
softEnums[name] = struct{}{}
|
||||
r = s.sendAvailableCommands(c, softEnums)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/cmd"
|
||||
"github.com/df-mc/dragonfly/server/entity/effect"
|
||||
"github.com/df-mc/dragonfly/server/item"
|
||||
"github.com/df-mc/dragonfly/server/item/inventory"
|
||||
"github.com/df-mc/dragonfly/server/player/chat"
|
||||
"github.com/df-mc/dragonfly/server/player/debug"
|
||||
"github.com/df-mc/dragonfly/server/player/dialogue"
|
||||
"github.com/df-mc/dragonfly/server/player/form"
|
||||
"github.com/df-mc/dragonfly/server/player/hud"
|
||||
"github.com/df-mc/dragonfly/server/player/skin"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// Controllable represents an entity that may be controlled by a Session. Generally, Controllable is
|
||||
// implemented in the form of a Player.
|
||||
// Methods in Controllable will be added as Session needs them in order to handle packets.
|
||||
type Controllable interface {
|
||||
Name() string
|
||||
world.Entity
|
||||
item.User
|
||||
dialogue.Submitter
|
||||
form.Submitter
|
||||
cmd.Source
|
||||
chat.Subscriber
|
||||
hud.Renderer
|
||||
debug.Renderer
|
||||
|
||||
Locale() language.Tag
|
||||
|
||||
SetHeldItems(right, left item.Stack)
|
||||
SetHeldSlot(slot int) error
|
||||
|
||||
Move(deltaPos mgl64.Vec3, deltaYaw, deltaPitch float64)
|
||||
|
||||
Speed() float64
|
||||
FlightSpeed() float64
|
||||
VerticalFlightSpeed() float64
|
||||
|
||||
Sleep(pos cube.Pos)
|
||||
Wake()
|
||||
|
||||
Chat(msg ...any)
|
||||
ExecuteCommand(commandLine string)
|
||||
GameMode() world.GameMode
|
||||
SetGameMode(mode world.GameMode)
|
||||
Effects() []effect.Effect
|
||||
|
||||
UseItem()
|
||||
ReleaseItem()
|
||||
UseItemOnBlock(pos cube.Pos, face cube.Face, clickPos mgl64.Vec3)
|
||||
UseItemOnEntity(e world.Entity) bool
|
||||
BreakBlock(pos cube.Pos)
|
||||
PickBlock(pos cube.Pos)
|
||||
AttackEntity(e world.Entity) bool
|
||||
Drop(s item.Stack) (n int)
|
||||
SwingArm()
|
||||
PunchAir()
|
||||
|
||||
Health() float64
|
||||
MaxHealth() float64
|
||||
Absorption() float64
|
||||
Food() int
|
||||
|
||||
ExperienceLevel() int
|
||||
ExperienceProgress() float64
|
||||
SetExperienceLevel(level int)
|
||||
|
||||
EnchantmentSeed() int64
|
||||
ResetEnchantmentSeed()
|
||||
|
||||
Respawn() *world.EntityHandle
|
||||
Dead() bool
|
||||
|
||||
StartSneaking()
|
||||
Sneaking() bool
|
||||
StopSneaking()
|
||||
StartSprinting()
|
||||
Sprinting() bool
|
||||
StopSprinting()
|
||||
StartSwimming()
|
||||
Swimming() bool
|
||||
StopSwimming()
|
||||
StartCrawling()
|
||||
Crawling() bool
|
||||
StopCrawling()
|
||||
StartFlying()
|
||||
Flying() bool
|
||||
StopFlying()
|
||||
StartGliding()
|
||||
Gliding() bool
|
||||
StopGliding()
|
||||
Jump()
|
||||
|
||||
StartBreaking(pos cube.Pos, face cube.Face)
|
||||
ContinueBreaking(face cube.Face)
|
||||
FinishBreaking()
|
||||
AbortBreaking()
|
||||
|
||||
Exhaust(points float64)
|
||||
|
||||
OpenSign(pos cube.Pos, frontSide bool)
|
||||
EditSign(pos cube.Pos, frontText, backText string) error
|
||||
TurnLecternPage(pos cube.Pos, page int) error
|
||||
|
||||
EnderChestInventory() *inventory.Inventory
|
||||
MoveItemsToInventory()
|
||||
|
||||
// UUID returns the UUID of the controllable. It must be unique for all controllable entities present in
|
||||
// the server.
|
||||
UUID() uuid.UUID
|
||||
// XUID returns the XBOX Live User ID of the controllable. Every controllable must have one of these if
|
||||
// they are authenticated via XBOX Live, as they must be connected to an XBOX Live account.
|
||||
XUID() string
|
||||
// Skin returns the skin of the controllable. Each controllable must have a skin, as it defines how the
|
||||
// entity looks in the world.
|
||||
Skin() skin.Skin
|
||||
SetSkin(skin.Skin)
|
||||
|
||||
UpdateDiagnostics(Diagnostics)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package session
|
||||
|
||||
// Diagnostics represents the latest diagnostics data of the client.
|
||||
type Diagnostics struct {
|
||||
// AverageFramesPerSecond is the average amount of frames per second that the client has been
|
||||
// running at.
|
||||
AverageFramesPerSecond float64
|
||||
// AverageServerSimTickTime is the average time that the server spends simulating a single tick
|
||||
// in milliseconds.
|
||||
AverageServerSimTickTime float64
|
||||
// AverageClientSimTickTime is the average time that the client spends simulating a single tick
|
||||
// in milliseconds.
|
||||
AverageClientSimTickTime float64
|
||||
// AverageBeginFrameTime is the average time that the client spends beginning a frame in
|
||||
// milliseconds.
|
||||
AverageBeginFrameTime float64
|
||||
// AverageInputTime is the average time that the client spends processing input in milliseconds.
|
||||
AverageInputTime float64
|
||||
// AverageRenderTime is the average time that the client spends rendering in milliseconds.
|
||||
AverageRenderTime float64
|
||||
// AverageEndFrameTime is the average time that the client spends ending a frame in milliseconds.
|
||||
AverageEndFrameTime float64
|
||||
// AverageRemainderTimePercent is the average percentage of time that the client spends on
|
||||
// tasks that are not accounted for.
|
||||
AverageRemainderTimePercent float64
|
||||
// AverageUnaccountedTimePercent is the average percentage of time that the client spends on
|
||||
// unaccounted tasks.
|
||||
AverageUnaccountedTimePercent float64
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// Code generated by .github/workflows/push.yml; DO NOT EDIT
|
||||
|
||||
package session
|
||||
|
||||
// enchantNames are names translated to the 'Standard Galactic Alphabet' client-side. The names generally have no meaning
|
||||
// on the vanilla server implementation, so we can sneak some easter eggs in here without anyone noticing.
|
||||
var enchantNames = []string{"aabstractt", "abimek", "aericio", "aimjel", "akmal fairuz", "alvin0319", "andreashgk", "assassin ghost yt", "atm85", "azvyl", "blackjack200", "cetfu", "cjmustard", "cooldogedev", "cqdetdev", "da pig guy", "daft0175", "dasciam", "deniel world", "didntpot", "driftlgtm", "eminarican", "endermanbugzjfc", "erkam246", "ethaniccc", "fdutch", "flonja", "game parrot", "gewinum", "hashim the arab", "hochbaum", "hydzilla", "im da real ani", "inotflying", "ipad54", "its zodia x", "ivan craft623", "javier leon9966", "just tal develops", "k4ties", "krivey", "manab-pr", "memoxiiii", "mmm545", "mohamed587100", "myma qc", "natuyasai natuo", "neutronic mc", "nonono697", "nope not dark", "provsalt", "restart fu", "riccskn", "robertdudaa", "royal mcpe", "sallypemdas", "sandertv", "schphe", "sculas", "sergittos", "smell-of-curry", "sqmatheus", "ssaini123456", "studgi", "superomarking", "t14 raptor", "tadhunt", "theaddonn", "thicksunny", "thunder33345", "tripple awap", "tristanmorgan", "twisted asylum mc", "unickorn", "unknown ore", "uramnoil", "wqrro", "x natsuri", "x toast-dev", "x4caa", "xd-pro"}
|
||||
@@ -0,0 +1,306 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/entity"
|
||||
"github.com/df-mc/dragonfly/server/entity/effect"
|
||||
"github.com/df-mc/dragonfly/server/internal/nbtconv"
|
||||
"github.com/df-mc/dragonfly/server/item"
|
||||
"github.com/df-mc/dragonfly/server/item/potion"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol"
|
||||
)
|
||||
|
||||
// parseEntityMetadata returns an entity metadata object with default values. It is equivalent to setting
|
||||
// all properties to their default values and disabling all flags.
|
||||
func (s *Session) parseEntityMetadata(e world.Entity) protocol.EntityMetadata {
|
||||
bb := e.H().Type().BBox(e)
|
||||
m := protocol.NewEntityMetadata()
|
||||
|
||||
m[protocol.EntityDataKeyWidth] = float32(bb.Width())
|
||||
m[protocol.EntityDataKeyHeight] = float32(bb.Height())
|
||||
m[protocol.EntityDataKeyEffectColor] = int32(0)
|
||||
m[protocol.EntityDataKeyEffectAmbience] = byte(0)
|
||||
m[protocol.EntityDataKeyColorIndex] = byte(0)
|
||||
|
||||
m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagHasGravity)
|
||||
m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagClimb)
|
||||
if g, ok := e.H().Type().(glint); ok && g.Glint() {
|
||||
m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagEnchanted)
|
||||
}
|
||||
if e.H().Type() == entity.LingeringPotionType {
|
||||
m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagLingering)
|
||||
}
|
||||
s.addSpecificMetadata(e, m)
|
||||
if ent, ok := e.(*entity.Ent); ok {
|
||||
s.addSpecificMetadata(ent.Behaviour(), m)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (s *Session) addSpecificMetadata(e any, m protocol.EntityMetadata) {
|
||||
if sn, ok := e.(sneaker); ok && sn.Sneaking() {
|
||||
m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagSneaking)
|
||||
}
|
||||
if sp, ok := e.(sprinter); ok && sp.Sprinting() {
|
||||
m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagSprinting)
|
||||
}
|
||||
if sw, ok := e.(swimmer); ok && sw.Swimming() {
|
||||
m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagSwimming)
|
||||
}
|
||||
if cr, ok := e.(crawler); ok && cr.Crawling() {
|
||||
m.SetFlag(protocol.EntityDataKeyFlagsTwo, protocol.EntityDataFlagCrawling&63)
|
||||
}
|
||||
if gl, ok := e.(glider); ok && gl.Gliding() {
|
||||
m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagGliding)
|
||||
}
|
||||
if bb, ok := e.(baby); ok && bb.Baby() {
|
||||
m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagBaby)
|
||||
}
|
||||
if b, ok := e.(breather); ok {
|
||||
m[protocol.EntityDataKeyAirSupply] = int16(b.AirSupply().Milliseconds() / 50)
|
||||
m[protocol.EntityDataKeyAirSupplyMax] = int16(b.MaxAirSupply().Milliseconds() / 50)
|
||||
if b.Breathing() {
|
||||
m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagBreathing)
|
||||
}
|
||||
}
|
||||
if i, ok := e.(invisible); ok && i.Invisible() {
|
||||
m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagInvisible)
|
||||
}
|
||||
if i, ok := e.(immobile); ok && i.Immobile() {
|
||||
m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagNoAI)
|
||||
}
|
||||
if o, ok := e.(onFire); ok && o.OnFireDuration() > 0 {
|
||||
m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagOnFire)
|
||||
}
|
||||
if u, ok := e.(using); ok && u.UsingItem() {
|
||||
m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagUsingItem)
|
||||
}
|
||||
if c, ok := e.(arrow); ok && c.Critical() {
|
||||
m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagCritical)
|
||||
}
|
||||
if g, ok := e.(gameMode); ok {
|
||||
if g.GameMode().HasCollision() {
|
||||
m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagHasCollision)
|
||||
}
|
||||
}
|
||||
if o, ok := e.(orb); ok {
|
||||
m[protocol.EntityDataKeyValue] = int32(o.Experience())
|
||||
}
|
||||
if f, ok := e.(firework); ok {
|
||||
m[protocol.EntityDataKeyDisplayTileRuntimeID] = nbtconv.WriteItem(item.NewStack(f.Firework(), 1), false)
|
||||
if o, ok := e.(owned); ok && f.Attached() && o.Owner() != nil {
|
||||
m[protocol.EntityDataKeyCustomDisplay] = int64(s.handleRuntimeID(o.Owner()))
|
||||
}
|
||||
} else if o, ok := e.(owned); ok && o.Owner() != nil {
|
||||
m[protocol.EntityDataKeyOwner] = int64(s.handleRuntimeID(o.Owner()))
|
||||
}
|
||||
if sc, ok := e.(scaled); ok {
|
||||
m[protocol.EntityDataKeyScale] = float32(sc.Scale())
|
||||
}
|
||||
if t, ok := e.(tnt); ok {
|
||||
m[protocol.EntityDataKeyFuseTime] = int32(t.Fuse().Milliseconds() / 50)
|
||||
m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagIgnited)
|
||||
}
|
||||
if n, ok := e.(named); ok {
|
||||
name := n.NameTag()
|
||||
m[protocol.EntityDataKeyName] = name
|
||||
if name == "" {
|
||||
m[protocol.EntityDataKeyAlwaysShowNameTag] = uint8(0)
|
||||
m.UnsetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagAlwaysShowName)
|
||||
m.UnsetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagShowName)
|
||||
} else {
|
||||
m[protocol.EntityDataKeyAlwaysShowNameTag] = uint8(1)
|
||||
m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagAlwaysShowName)
|
||||
m.SetFlag(protocol.EntityDataKeyFlags, protocol.EntityDataFlagShowName)
|
||||
}
|
||||
}
|
||||
if sc, ok := e.(scoreTag); ok {
|
||||
m[protocol.EntityDataKeyScore] = sc.ScoreTag()
|
||||
}
|
||||
if sl, ok := e.(sleeper); ok {
|
||||
if pos, ok := sl.Sleeping(); ok {
|
||||
m[protocol.EntityDataKeyBedPosition] = protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])}
|
||||
|
||||
// For some reason there is no such flag in gophertunnel.
|
||||
m.SetFlag(protocol.EntityDataKeyPlayerFlags, 1)
|
||||
}
|
||||
}
|
||||
if c, ok := e.(areaEffectCloud); ok {
|
||||
m[protocol.EntityDataKeyDataRadius] = float32(c.Radius())
|
||||
|
||||
// We purposely fill these in with invalid values to disable the client-sided shrinking of the cloud.
|
||||
m[protocol.EntityDataKeyDataDuration] = int32(math.MaxInt32)
|
||||
m[protocol.EntityDataKeyDataChangeOnPickup] = float32(math.SmallestNonzeroFloat32)
|
||||
m[protocol.EntityDataKeyDataChangeRate] = float32(math.SmallestNonzeroFloat32)
|
||||
|
||||
colour, am := effect.ResultingColour(c.Effects())
|
||||
m[protocol.EntityDataKeyEffectColor] = nbtconv.Int32FromRGBA(colour)
|
||||
if am {
|
||||
m[protocol.EntityDataKeyEffectAmbience] = byte(1)
|
||||
} else {
|
||||
m[protocol.EntityDataKeyEffectAmbience] = byte(0)
|
||||
}
|
||||
}
|
||||
|
||||
if l, ok := e.(living); ok && s.ent != nil && s.ent.UUID() == l.UUID() {
|
||||
deathPos, deathDimension, died := l.DeathPosition()
|
||||
if died {
|
||||
dim, _ := world.DimensionID(deathDimension)
|
||||
m[protocol.EntityDataKeyPlayerLastDeathPosition] = vec64To32(deathPos)
|
||||
m[protocol.EntityDataKeyPlayerLastDeathDimension] = int32(dim)
|
||||
}
|
||||
m[protocol.EntityDataKeyPlayerHasDied] = boolByte(died)
|
||||
}
|
||||
if p, ok := e.(splash); ok {
|
||||
m[protocol.EntityDataKeyAuxValueData] = int16(p.Potion().Uint8())
|
||||
if tip := p.Potion().Uint8(); tip > 4 {
|
||||
m[protocol.EntityDataKeyCustomDisplay] = tip + 1
|
||||
}
|
||||
}
|
||||
if eff, ok := e.(effectBearer); ok {
|
||||
var packedEffects int64
|
||||
|
||||
for _, ef := range eff.Effects() {
|
||||
if !ef.ParticlesHidden() {
|
||||
id, found := effect.ID(ef.Type())
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
packedEffects = (packedEffects << 7) | int64(id<<1)
|
||||
if ef.Ambient() {
|
||||
packedEffects |= 1
|
||||
}
|
||||
}
|
||||
}
|
||||
m[protocol.EntityDataKeyVisibleMobEffects] = packedEffects
|
||||
}
|
||||
if v, ok := e.(variable); ok {
|
||||
m[protocol.EntityDataKeyVariant] = v.Variant()
|
||||
}
|
||||
if mv, ok := e.(markVariable); ok {
|
||||
m[protocol.EntityDataKeyMarkVariant] = mv.MarkVariant()
|
||||
}
|
||||
}
|
||||
|
||||
type sneaker interface {
|
||||
Sneaking() bool
|
||||
}
|
||||
|
||||
type sprinter interface {
|
||||
Sprinting() bool
|
||||
}
|
||||
|
||||
type swimmer interface {
|
||||
Swimming() bool
|
||||
}
|
||||
|
||||
type crawler interface {
|
||||
Crawling() bool
|
||||
}
|
||||
|
||||
type glider interface {
|
||||
Gliding() bool
|
||||
}
|
||||
|
||||
type baby interface {
|
||||
Baby() bool
|
||||
}
|
||||
|
||||
type breather interface {
|
||||
Breathing() bool
|
||||
AirSupply() time.Duration
|
||||
MaxAirSupply() time.Duration
|
||||
}
|
||||
|
||||
type immobile interface {
|
||||
Immobile() bool
|
||||
}
|
||||
|
||||
type invisible interface {
|
||||
Invisible() bool
|
||||
}
|
||||
|
||||
type scaled interface {
|
||||
Scale() float64
|
||||
}
|
||||
|
||||
type owned interface {
|
||||
Owner() *world.EntityHandle
|
||||
}
|
||||
|
||||
type named interface {
|
||||
NameTag() string
|
||||
}
|
||||
|
||||
type scoreTag interface {
|
||||
ScoreTag() string
|
||||
}
|
||||
|
||||
type splash interface {
|
||||
Potion() potion.Potion
|
||||
}
|
||||
|
||||
type glint interface {
|
||||
Glint() bool
|
||||
}
|
||||
|
||||
type areaEffectCloud interface {
|
||||
effectBearer
|
||||
Radius() float64
|
||||
}
|
||||
|
||||
type onFire interface {
|
||||
OnFireDuration() time.Duration
|
||||
}
|
||||
|
||||
type effectBearer interface {
|
||||
Effects() []effect.Effect
|
||||
}
|
||||
|
||||
type using interface {
|
||||
UsingItem() bool
|
||||
}
|
||||
|
||||
type arrow interface {
|
||||
Critical() bool
|
||||
}
|
||||
|
||||
type orb interface {
|
||||
Experience() int
|
||||
}
|
||||
|
||||
type firework interface {
|
||||
Firework() item.Firework
|
||||
Attached() bool
|
||||
}
|
||||
|
||||
type gameMode interface {
|
||||
GameMode() world.GameMode
|
||||
}
|
||||
|
||||
type sleeper interface {
|
||||
Sleeping() (cube.Pos, bool)
|
||||
}
|
||||
|
||||
type tnt interface {
|
||||
Fuse() time.Duration
|
||||
}
|
||||
|
||||
type living interface {
|
||||
UUID() uuid.UUID
|
||||
DeathPosition() (mgl64.Vec3, world.Dimension, bool)
|
||||
}
|
||||
|
||||
type variable interface {
|
||||
Variant() int32
|
||||
}
|
||||
|
||||
type markVariable interface {
|
||||
MarkVariant() int32
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
)
|
||||
|
||||
// packetHandler represents a type that is able to handle a specific type of incoming packets from the client.
|
||||
type packetHandler interface {
|
||||
// Handle handles an incoming packet from the client. The session of the client is passed to the packetHandler.
|
||||
// Handle returns an error if the packet was in any way invalid.
|
||||
Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
|
||||
"github.com/df-mc/dragonfly/server/block"
|
||||
"github.com/df-mc/dragonfly/server/item"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/df-mc/dragonfly/server/world/sound"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol"
|
||||
)
|
||||
|
||||
const (
|
||||
// anvilInputSlot is the slot index of the input item in the anvil.
|
||||
anvilInputSlot = 0x1
|
||||
// anvilMaterialSlot is the slot index of the material in the anvil.
|
||||
anvilMaterialSlot = 0x2
|
||||
)
|
||||
|
||||
// handleCraftRecipeOptional handles the CraftRecipeOptional request action, sent when taking a result from an anvil
|
||||
// menu. It also contains information such as the new name of the item and the multi-recipe network ID.
|
||||
func (h *ItemStackRequestHandler) handleCraftRecipeOptional(a *protocol.CraftRecipeOptionalStackRequestAction, s *Session, filterStrings []string, co Controllable, tx *world.Tx) (err error) {
|
||||
// First check if there actually is an anvil opened.
|
||||
if !s.containerOpened.Load() {
|
||||
return fmt.Errorf("no anvil container opened")
|
||||
}
|
||||
|
||||
pos := *s.openedPos.Load()
|
||||
anvil, ok := tx.Block(pos).(block.Anvil)
|
||||
if !ok {
|
||||
return fmt.Errorf("no anvil container opened")
|
||||
}
|
||||
if index := int(a.FilterStringIndex); len(filterStrings) > 0 && (index < 0 || index >= len(filterStrings)) {
|
||||
return fmt.Errorf("filter string index %v is out of bounds", a.FilterStringIndex)
|
||||
}
|
||||
|
||||
input, _ := h.itemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerAnvilInput},
|
||||
Slot: anvilInputSlot,
|
||||
}, s, tx)
|
||||
if input.Empty() {
|
||||
return fmt.Errorf("no item in input input slot")
|
||||
}
|
||||
material, _ := h.itemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerAnvilMaterial},
|
||||
Slot: anvilMaterialSlot,
|
||||
}, s, tx)
|
||||
result := input
|
||||
|
||||
// The sum of the input's anvil cost as well as the material's anvil cost.
|
||||
anvilCost := input.AnvilCost()
|
||||
if !material.Empty() {
|
||||
anvilCost += material.AnvilCost()
|
||||
}
|
||||
|
||||
// The material input may be empty (if the player is only renaming, for example).
|
||||
var actionCost, renameCost, repairCount int
|
||||
if !material.Empty() {
|
||||
// First check if we are trying to repair the item with a material.
|
||||
if repairable, ok := input.Item().(item.Repairable); ok && repairable.RepairableBy(material) {
|
||||
result, actionCost, repairCount, err = repairItemWithMaterial(input, material, result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
_, book := material.Item().(item.EnchantedBook)
|
||||
_, durable := input.Item().(item.Durable)
|
||||
|
||||
// Ensure that the input item is repairable, or the material item is an enchanted book. If not, this is an
|
||||
// invalid scenario, and we should return an error.
|
||||
enchantedBook := book && len(material.Enchantments()) > 0
|
||||
if !enchantedBook && (input.Item() != material.Item() || !durable) {
|
||||
return fmt.Errorf("input item is not repairable/same type or material item is not an enchanted book")
|
||||
}
|
||||
|
||||
// If the material is another durable item, we just need to increase the durability of the result by the
|
||||
// material's durability at 12%.
|
||||
if durable && !enchantedBook {
|
||||
result, actionCost = repairItemWithDurable(input, material, result)
|
||||
}
|
||||
|
||||
// Merge enchantments on the material item onto the result item.
|
||||
var hasCompatible, hasIncompatible bool
|
||||
result, hasCompatible, hasIncompatible, actionCost = mergeEnchantments(input, material, result, actionCost, enchantedBook)
|
||||
|
||||
// If we don't have any compatible enchantments and the input item isn't durable, then this is an invalid
|
||||
// scenario, and we should return an error.
|
||||
if !durable && hasIncompatible && !hasCompatible {
|
||||
return fmt.Errorf("no compatible enchantments but have incompatible ones")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a filter string, then the client is intending to rename the item.
|
||||
if len(filterStrings) > 0 {
|
||||
renameCost = 1
|
||||
actionCost += renameCost
|
||||
result = result.WithCustomName(filterStrings[int(a.FilterStringIndex)])
|
||||
}
|
||||
|
||||
// Calculate the total cost. (action cost + anvil cost)
|
||||
cost := actionCost + anvilCost
|
||||
if cost <= 0 {
|
||||
return fmt.Errorf("no action was taken")
|
||||
}
|
||||
|
||||
// If our only action was renaming, the cost should never exceed 40.
|
||||
if renameCost == actionCost && renameCost > 0 && cost >= 40 {
|
||||
cost = 39
|
||||
}
|
||||
|
||||
// We can bypass the "impossible cost" limit if we're in creative mode.
|
||||
c := co.GameMode().CreativeInventory()
|
||||
if cost >= 40 && !c {
|
||||
return fmt.Errorf("impossible cost")
|
||||
}
|
||||
|
||||
// Ensure we have enough levels (or if we're in creative mode, ignore the cost) to perform the action.
|
||||
level := co.ExperienceLevel()
|
||||
if level < cost && !c {
|
||||
return fmt.Errorf("not enough experience")
|
||||
} else if !c {
|
||||
co.SetExperienceLevel(level - cost)
|
||||
}
|
||||
|
||||
// If we had a result item, we need to calculate the new anvil cost and update it on the item.
|
||||
if !result.Empty() {
|
||||
updatedAnvilCost := result.AnvilCost()
|
||||
if !material.Empty() && updatedAnvilCost < material.AnvilCost() {
|
||||
updatedAnvilCost = material.AnvilCost()
|
||||
}
|
||||
if renameCost != actionCost || renameCost == 0 {
|
||||
updatedAnvilCost = updatedAnvilCost*2 + 1
|
||||
}
|
||||
result = result.WithAnvilCost(updatedAnvilCost)
|
||||
}
|
||||
|
||||
// If we're not in creative mode, we have a 12% chance of the anvil degrading down one state. If that is the case, we
|
||||
// need to play the related sound and update the block state. Otherwise, we play a regular anvil use sound.
|
||||
if !c && rand.Float64() < 0.12 {
|
||||
damaged := anvil.Break()
|
||||
if _, ok := damaged.(block.Air); ok {
|
||||
tx.PlaySound(pos.Vec3Centre(), sound.AnvilBreak{})
|
||||
} else {
|
||||
tx.PlaySound(pos.Vec3Centre(), sound.AnvilUse{})
|
||||
}
|
||||
defer tx.SetBlock(pos, damaged, nil)
|
||||
} else {
|
||||
tx.PlaySound(pos.Vec3Centre(), sound.AnvilUse{})
|
||||
}
|
||||
|
||||
h.setItemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerAnvilInput},
|
||||
Slot: anvilInputSlot,
|
||||
}, item.Stack{}, s, tx)
|
||||
if repairCount > 0 {
|
||||
h.setItemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerAnvilMaterial},
|
||||
Slot: anvilMaterialSlot,
|
||||
}, material.Grow(-repairCount), s, tx)
|
||||
} else {
|
||||
h.setItemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerAnvilMaterial},
|
||||
Slot: anvilMaterialSlot,
|
||||
}, item.Stack{}, s, tx)
|
||||
}
|
||||
return h.createResults(s, tx, result)
|
||||
}
|
||||
|
||||
// repairItemWithMaterial is a helper function that repairs an item stack with a given material stack. It returns the new item
|
||||
// stack, the cost, and the repaired items count.
|
||||
func repairItemWithMaterial(input item.Stack, material item.Stack, result item.Stack) (item.Stack, int, int, error) {
|
||||
// Calculate the durability delta using the maximum durability and the current durability.
|
||||
delta := min(input.MaxDurability()-input.Durability(), input.MaxDurability()/4)
|
||||
if delta <= 0 {
|
||||
return item.Stack{}, 0, 0, fmt.Errorf("input item is already fully repaired")
|
||||
}
|
||||
|
||||
// While the durability delta is more than zero and the repaired count is under the material count, increase
|
||||
// the durability of the result by the durability delta.
|
||||
var cost, count int
|
||||
for ; delta > 0 && count < material.Count(); count, delta = count+1, min(result.MaxDurability()-result.Durability(), result.MaxDurability()/4) {
|
||||
result = result.WithDurability(result.Durability() + delta)
|
||||
cost++
|
||||
}
|
||||
return result, cost, count, nil
|
||||
}
|
||||
|
||||
// repairItemWithDurable is a helper function that repairs an item with another durable item stack.
|
||||
func repairItemWithDurable(input item.Stack, durable item.Stack, result item.Stack) (item.Stack, int) {
|
||||
durability := input.Durability() + durable.Durability() + input.MaxDurability()*12/100
|
||||
if durability > input.MaxDurability() {
|
||||
durability = input.MaxDurability()
|
||||
}
|
||||
|
||||
// Ensure the durability is higher than the input's current durability.
|
||||
var cost int
|
||||
if durability > input.Durability() {
|
||||
result = result.WithDurability(durability)
|
||||
cost += 2
|
||||
}
|
||||
return result, cost
|
||||
}
|
||||
|
||||
// mergeEnchantments merges the enchantments of the material item stack onto the result item stack and returns the result
|
||||
// item stack, booleans indicating whether the enchantments had any compatible or incompatible enchantments, and the cost.
|
||||
func mergeEnchantments(input item.Stack, material item.Stack, result item.Stack, cost int, enchantedBook bool) (item.Stack, bool, bool, int) {
|
||||
var hasCompatible, hasIncompatible bool
|
||||
for _, enchant := range material.Enchantments() {
|
||||
// First ensure that the enchantment type is compatible with the input item.
|
||||
enchantType := enchant.Type()
|
||||
compatible := enchantType.CompatibleWithItem(input.Item())
|
||||
if _, ok := input.Item().(item.EnchantedBook); ok {
|
||||
compatible = true
|
||||
}
|
||||
|
||||
// Then ensure that each input enchantment is compatible with this material enchantment. If one is not compatible,
|
||||
// increase the cost by one.
|
||||
for _, otherEnchant := range input.Enchantments() {
|
||||
if otherType := otherEnchant.Type(); enchantType != otherType && !enchantType.CompatibleWithEnchantment(otherType) {
|
||||
compatible = false
|
||||
cost++
|
||||
}
|
||||
}
|
||||
|
||||
// Skip the enchantment if it isn't compatible with enchantments on the input item.
|
||||
if !compatible {
|
||||
hasIncompatible = true
|
||||
continue
|
||||
}
|
||||
hasCompatible = true
|
||||
|
||||
resultLevel := enchant.Level()
|
||||
levelCost := resultLevel
|
||||
|
||||
// Check if we have an enchantment of the same type on the input item.
|
||||
if existingEnchant, ok := input.Enchantment(enchantType); ok {
|
||||
if existingEnchant.Level() > resultLevel || (existingEnchant.Level() == resultLevel && resultLevel == enchantType.MaxLevel()) {
|
||||
// The result level is either lower than the existing enchantment's level or is higher than the maximum
|
||||
// level, so skip this enchantment.
|
||||
hasIncompatible = true
|
||||
continue
|
||||
} else if existingEnchant.Level() == resultLevel {
|
||||
// If the input level is equal to the material level, increase the result level by one.
|
||||
resultLevel++
|
||||
}
|
||||
// Update the level cost. (result level - existing level)
|
||||
levelCost = resultLevel - existingEnchant.Level()
|
||||
}
|
||||
|
||||
// Now calculate the rarity cost. This is just the application cost of the rarity, however if the
|
||||
// material is an enchanted book, then the rarity cost gets halved. If the new rarity cost is under one,
|
||||
// it is set to one.
|
||||
rarityCost := enchantType.Rarity().Cost()
|
||||
if enchantedBook {
|
||||
rarityCost = max(1, rarityCost/2)
|
||||
}
|
||||
|
||||
// Update the result item with the new enchantment.
|
||||
result = result.WithEnchantments(item.NewEnchantment(enchantType, resultLevel))
|
||||
|
||||
// Update the cost appropriately.
|
||||
cost += rarityCost * levelCost
|
||||
if input.Count() > 1 {
|
||||
cost = 40
|
||||
}
|
||||
}
|
||||
return result, hasCompatible, hasIncompatible, cost
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/df-mc/dragonfly/server/block"
|
||||
"github.com/df-mc/dragonfly/server/entity/effect"
|
||||
"github.com/df-mc/dragonfly/server/item"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol"
|
||||
)
|
||||
|
||||
// beaconInputSlot is the slot index of the input item in the beacon.
|
||||
const beaconInputSlot = 0x1b
|
||||
|
||||
// handleBeaconPayment handles the selection of effects in a beacon and the removal of the item used to pay
|
||||
// for those effects.
|
||||
func (h *ItemStackRequestHandler) handleBeaconPayment(a *protocol.BeaconPaymentStackRequestAction, s *Session, tx *world.Tx) error {
|
||||
slot := protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerBeaconPayment},
|
||||
Slot: beaconInputSlot,
|
||||
}
|
||||
// First check if there actually is a beacon opened.
|
||||
if !s.containerOpened.Load() {
|
||||
return fmt.Errorf("no beacon container opened")
|
||||
}
|
||||
pos := *s.openedPos.Load()
|
||||
beacon, ok := tx.Block(pos).(block.Beacon)
|
||||
if !ok {
|
||||
return fmt.Errorf("no beacon container opened")
|
||||
}
|
||||
|
||||
// Check if the item present in the beacon slot is valid.
|
||||
payment, _ := h.itemInSlot(slot, s, tx)
|
||||
if payable, ok := payment.Item().(item.BeaconPayment); !ok || !payable.PayableForBeacon() {
|
||||
return fmt.Errorf("item %#v in beacon slot cannot be used as payment", payment)
|
||||
}
|
||||
|
||||
// Check if the effects are valid and allowed for the beacon's level.
|
||||
if !h.validBeaconEffect(a.PrimaryEffect, beacon) {
|
||||
return fmt.Errorf("primary effect selected is not allowed: %v for level %v", a.PrimaryEffect, beacon.Level())
|
||||
} else if !h.validBeaconEffect(a.SecondaryEffect, beacon) || (beacon.Level() < 4 && a.SecondaryEffect != 0) {
|
||||
return fmt.Errorf("secondary effect selected is not allowed: %v for level %v", a.SecondaryEffect, beacon.Level())
|
||||
}
|
||||
|
||||
primary, pOk := effect.ByID(int(a.PrimaryEffect))
|
||||
secondary, sOk := effect.ByID(int(a.SecondaryEffect))
|
||||
if pOk {
|
||||
beacon.Primary = primary.(effect.LastingType)
|
||||
}
|
||||
if sOk {
|
||||
beacon.Secondary = secondary.(effect.LastingType)
|
||||
}
|
||||
tx.SetBlock(pos, beacon, nil)
|
||||
|
||||
// The client will send a Destroy action after this action, but we can't rely on that because the client
|
||||
// could just not send it.
|
||||
// We just ignore the next Destroy action and set the item to air here.
|
||||
h.setItemInSlot(slot, item.Stack{}, s, tx)
|
||||
h.ignoreDestroy = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// validBeaconEffect checks if the ID passed is a valid beacon effect.
|
||||
func (h *ItemStackRequestHandler) validBeaconEffect(id int32, beacon block.Beacon) bool {
|
||||
switch id {
|
||||
case 1, 3:
|
||||
return beacon.Level() >= 1
|
||||
case 8, 11:
|
||||
return beacon.Level() >= 2
|
||||
case 5:
|
||||
return beacon.Level() >= 3
|
||||
case 10:
|
||||
return beacon.Level() >= 4
|
||||
case 0:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/df-mc/dragonfly/server/block"
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/entity"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// BlockActorDataHandler handles an incoming BlockActorData packet from the client, sent for some block entities like
|
||||
// signs when they are edited.
|
||||
type BlockActorDataHandler struct{}
|
||||
|
||||
// Handle ...
|
||||
func (b BlockActorDataHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error {
|
||||
pk := p.(*packet.BlockActorData)
|
||||
if id, ok := pk.NBTData["id"]; ok {
|
||||
pos := blockPosFromProtocol(pk.Position)
|
||||
if !canReach(c, pos.Vec3Middle()) {
|
||||
return fmt.Errorf("block at %v is not within reach", pos)
|
||||
}
|
||||
if id == "Sign" {
|
||||
return b.handleSign(pk, pos, s, tx, c)
|
||||
}
|
||||
return fmt.Errorf("unhandled block actor data ID %v", id)
|
||||
}
|
||||
return fmt.Errorf("block actor data without 'id' tag: %v", pk.NBTData)
|
||||
}
|
||||
|
||||
// handleSign handles the BlockActorData packet sent when editing a sign.
|
||||
func (b BlockActorDataHandler) handleSign(pk *packet.BlockActorData, pos cube.Pos, s *Session, tx *world.Tx, co Controllable) error {
|
||||
if _, ok := tx.Block(pos).(block.Sign); !ok {
|
||||
s.conf.Log.Debug("no sign at position of sign block actor data", "pos", pos.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
frontText, err := b.textFromNBTData(pk.NBTData, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
backText, err := b.textFromNBTData(pk.NBTData, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := co.EditSign(pos, frontText, backText); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// textFromNBTData attempts to retrieve the text from the NBT data of specific sign from the BlockActorData packet.
|
||||
func (b BlockActorDataHandler) textFromNBTData(data map[string]any, frontSide bool) (string, error) {
|
||||
var sideData map[string]any
|
||||
var side string
|
||||
if frontSide {
|
||||
frontSide, ok := data["FrontText"].(map[string]any)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("sign block actor data 'FrontText' tag was not found or was not a map: %#v", data["FrontText"])
|
||||
}
|
||||
sideData = frontSide
|
||||
side = "front"
|
||||
} else {
|
||||
backSide, ok := data["BackText"].(map[string]any)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("sign block actor data 'BackText' tag was not found or was not a map: %#v", data["BackText"])
|
||||
}
|
||||
sideData = backSide
|
||||
side = "back"
|
||||
}
|
||||
var text string
|
||||
pkText, ok := sideData["Text"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("sign block actor data had no 'Text' tag for side %s", side)
|
||||
}
|
||||
if text, ok = pkText.(string); !ok {
|
||||
return "", fmt.Errorf("sign block actor data 'Text' tag was not a string for side %s: %#v", side, pkText)
|
||||
}
|
||||
|
||||
// Verify that the text was valid. It must be valid UTF8 and not more than 100 characters long.
|
||||
text = strings.TrimRight(text, "\n")
|
||||
if len(text) > 256 {
|
||||
return "", fmt.Errorf("sign block actor data text was longer than 256 characters for side %s", side)
|
||||
}
|
||||
if !utf8.ValidString(text) {
|
||||
return "", fmt.Errorf("sign block actor data text was not valid UTF8 for side %s", side)
|
||||
}
|
||||
return text, nil
|
||||
}
|
||||
|
||||
// canReach checks if a player can reach a position with its current range. The range depends on if the player
|
||||
// is either survival or creative mode.
|
||||
func canReach(c Controllable, pos mgl64.Vec3) bool {
|
||||
const (
|
||||
creativeRange = 14.0
|
||||
survivalRange = 8.0
|
||||
)
|
||||
if !c.GameMode().AllowsInteraction() {
|
||||
return false
|
||||
}
|
||||
|
||||
eyes := entity.EyePosition(c)
|
||||
if c.GameMode().CreativeInventory() {
|
||||
return eyes.Sub(pos).Len() <= creativeRange && !c.Dead()
|
||||
}
|
||||
return eyes.Sub(pos).Len() <= survivalRange && !c.Dead()
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
)
|
||||
|
||||
// BlockPickRequestHandler handles the BlockPickRequest packet.
|
||||
type BlockPickRequestHandler struct{}
|
||||
|
||||
// Handle ...
|
||||
func (b BlockPickRequestHandler) Handle(p packet.Packet, _ *Session, _ *world.Tx, c Controllable) error {
|
||||
pk := p.(*packet.BlockPickRequest)
|
||||
c.PickBlock(cube.Pos{int(pk.Position.X()), int(pk.Position.Y()), int(pk.Position.Z())})
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/df-mc/dragonfly/server/item"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
)
|
||||
|
||||
// BookEditHandler handles the BookEdit packet.
|
||||
type BookEditHandler struct{}
|
||||
|
||||
// Handle ...
|
||||
func (b BookEditHandler) Handle(p packet.Packet, s *Session, _ *world.Tx, _ Controllable) error {
|
||||
pk := p.(*packet.BookEdit)
|
||||
|
||||
it, err := s.inv.Item(int(pk.InventorySlot))
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid inventory slot index %v", pk.InventorySlot)
|
||||
}
|
||||
book, ok := it.Item().(item.BookAndQuill)
|
||||
if !ok {
|
||||
return fmt.Errorf("inventory slot %v does not contain a writable book", pk.InventorySlot)
|
||||
}
|
||||
|
||||
page := int(pk.PageNumber)
|
||||
if page >= 50 || page < 0 {
|
||||
return fmt.Errorf("page number %v is out of bounds", pk.PageNumber)
|
||||
}
|
||||
if len(pk.Text) > 256 {
|
||||
return fmt.Errorf("text can not be longer than 256 bytes")
|
||||
}
|
||||
|
||||
slot := int(pk.InventorySlot)
|
||||
switch pk.ActionType {
|
||||
case packet.BookActionReplacePage:
|
||||
book = book.SetPage(page, pk.Text)
|
||||
case packet.BookActionAddPage:
|
||||
if len(book.Pages) >= 50 {
|
||||
return fmt.Errorf("unable to add page beyond 50")
|
||||
}
|
||||
if page >= len(book.Pages) && page <= len(book.Pages)+2 {
|
||||
book = book.SetPage(page, "")
|
||||
break
|
||||
}
|
||||
if _, ok := book.Page(page); !ok {
|
||||
return fmt.Errorf("unable to insert page at %v", pk.PageNumber)
|
||||
}
|
||||
book = book.InsertPage(page, pk.Text)
|
||||
case packet.BookActionDeletePage:
|
||||
if _, ok := book.Page(page); !ok {
|
||||
// We break here instead of returning an error because the client can be a page or two ahead in the UI then
|
||||
// the actual pages representation server side. The client still sends the deletion indexes.
|
||||
break
|
||||
}
|
||||
book = book.DeletePage(page)
|
||||
case packet.BookActionSwapPages:
|
||||
if pk.SecondaryPageNumber >= 50 {
|
||||
return fmt.Errorf("page number out of bounds")
|
||||
}
|
||||
_, ok := book.Page(page)
|
||||
_, ok2 := book.Page(int(pk.SecondaryPageNumber))
|
||||
if !ok || !ok2 {
|
||||
// We break here instead of returning an error because the client can try to swap pages that don't exist.
|
||||
// This happens as a result of the client being a page or two ahead in the UI then the actual pages
|
||||
// representation server side. The client still sends the swap indexes.
|
||||
break
|
||||
}
|
||||
book = book.SwapPages(page, int(pk.SecondaryPageNumber))
|
||||
case packet.BookActionSign:
|
||||
_ = s.inv.SetItem(slot, it.WithItem(item.WrittenBook{Title: pk.Title, Author: pk.Author, Pages: book.Pages, Generation: item.OriginalGeneration()}))
|
||||
return nil
|
||||
}
|
||||
_ = s.inv.SetItem(slot, it.WithItem(book))
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
)
|
||||
|
||||
// ClientCacheBlobStatusHandler handles the ClientCacheBlobStatus packet.
|
||||
type ClientCacheBlobStatusHandler struct {
|
||||
}
|
||||
|
||||
// Handle ...
|
||||
func (c *ClientCacheBlobStatusHandler) Handle(p packet.Packet, s *Session, _ *world.Tx, _ Controllable) error {
|
||||
pk := p.(*packet.ClientCacheBlobStatus)
|
||||
|
||||
resp := &packet.ClientCacheMissResponse{Blobs: make([]protocol.CacheBlob, 0, len(pk.MissHashes))}
|
||||
|
||||
s.blobMu.Lock()
|
||||
for _, hit := range pk.HitHashes {
|
||||
delete(s.blobs, hit)
|
||||
c.resolveBlob(hit, s)
|
||||
}
|
||||
for _, miss := range pk.MissHashes {
|
||||
blob, ok := s.blobs[miss]
|
||||
if !ok {
|
||||
// This is expected to happen sometimes, for example when we send the same block storage or biomes a lot of
|
||||
// times in a short timeframe. There is no need to log this, it'll just cause unnecessary noise that doesn't
|
||||
// actually aid in terms of information.
|
||||
continue
|
||||
}
|
||||
resp.Blobs = append(resp.Blobs, protocol.CacheBlob{Hash: miss, Payload: blob})
|
||||
c.resolveBlob(miss, s)
|
||||
}
|
||||
s.blobMu.Unlock()
|
||||
|
||||
if len(resp.Blobs) > 0 {
|
||||
s.writePacket(resp)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveBlob resolves a blob hash in the session passed. It assumes s.blobMu is locked upon calling.
|
||||
func (c *ClientCacheBlobStatusHandler) resolveBlob(hash uint64, s *Session) {
|
||||
leftover := make([]map[uint64]struct{}, 0, len(s.openChunkTransactions))
|
||||
for _, m := range s.openChunkTransactions {
|
||||
delete(m, hash)
|
||||
if len(m) != 0 {
|
||||
leftover = append(leftover, m)
|
||||
}
|
||||
}
|
||||
s.openChunkTransactions = leftover
|
||||
delete(s.blobs, hash)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
)
|
||||
|
||||
// CommandRequestHandler handles the CommandRequest packet.
|
||||
type CommandRequestHandler struct {
|
||||
origin protocol.CommandOrigin
|
||||
}
|
||||
|
||||
// Handle ...
|
||||
func (h *CommandRequestHandler) Handle(p packet.Packet, _ *Session, _ *world.Tx, c Controllable) error {
|
||||
pk := p.(*packet.CommandRequest)
|
||||
if pk.Internal {
|
||||
return fmt.Errorf("command request packet must never have the internal field set to true")
|
||||
}
|
||||
|
||||
h.origin = pk.CommandOrigin
|
||||
c.ExecuteCommand(pk.CommandLine)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
)
|
||||
|
||||
// ContainerCloseHandler handles the ContainerClose packet.
|
||||
type ContainerCloseHandler struct{}
|
||||
|
||||
// Handle ...
|
||||
func (h *ContainerCloseHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error {
|
||||
pk := p.(*packet.ContainerClose)
|
||||
|
||||
c.MoveItemsToInventory()
|
||||
|
||||
var containerType byte
|
||||
switch pk.WindowID {
|
||||
case 0:
|
||||
// Closing of the normal inventory.
|
||||
s.invOpened = false
|
||||
case byte(s.openedWindowID.Load()):
|
||||
containerType = byte(s.openedContainerID.Load())
|
||||
s.closeCurrentContainer(tx, true)
|
||||
case 0xff:
|
||||
// Sent when an inventory/container is opened at the same time as chat.
|
||||
s.invOpened = false
|
||||
if s.containerOpened.Load() {
|
||||
s.closeCurrentContainer(tx, false)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
containerType = pk.ContainerType
|
||||
}
|
||||
s.writePacket(&packet.ContainerClose{
|
||||
WindowID: pk.WindowID,
|
||||
ContainerType: containerType,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/df-mc/dragonfly/server/item"
|
||||
"github.com/df-mc/dragonfly/server/item/creative"
|
||||
"github.com/df-mc/dragonfly/server/item/inventory"
|
||||
"github.com/df-mc/dragonfly/server/item/recipe"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol"
|
||||
"math"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// handleCraft handles the CraftRecipe request action.
|
||||
func (h *ItemStackRequestHandler) handleCraft(a *protocol.CraftRecipeStackRequestAction, s *Session, tx *world.Tx) error {
|
||||
craft, ok := s.recipes[a.RecipeNetworkID]
|
||||
if !ok {
|
||||
// Try dynamic recipes if no static recipe matches
|
||||
return h.tryDynamicCraft(s, tx, int(a.NumberOfCrafts))
|
||||
}
|
||||
_, shaped := craft.(recipe.Shaped)
|
||||
_, shapeless := craft.(recipe.Shapeless)
|
||||
if !shaped && !shapeless {
|
||||
return fmt.Errorf("recipe with network id %v is not a shaped or shapeless recipe", a.RecipeNetworkID)
|
||||
}
|
||||
if craft.Block() != "crafting_table" {
|
||||
return fmt.Errorf("recipe with network id %v is not a crafting table recipe", a.RecipeNetworkID)
|
||||
}
|
||||
|
||||
timesCrafted := int(a.NumberOfCrafts)
|
||||
if timesCrafted < 1 {
|
||||
return fmt.Errorf("times crafted must be at least 1")
|
||||
}
|
||||
|
||||
size := s.craftingSize()
|
||||
offset := s.craftingOffset()
|
||||
consumed := make([]bool, size)
|
||||
for _, expected := range craft.Input() {
|
||||
var processed bool
|
||||
for slot := offset; slot < offset+size; slot++ {
|
||||
if consumed[slot-offset] {
|
||||
// We've already consumed this slot, skip it.
|
||||
continue
|
||||
}
|
||||
has, _ := s.ui.Item(int(slot))
|
||||
if has.Empty() != expected.Empty() || has.Count() < expected.Count()*timesCrafted {
|
||||
// We can't process this item, as it's not a part of the recipe.
|
||||
continue
|
||||
}
|
||||
if !matchingStacks(has, expected) {
|
||||
// Not the same item.
|
||||
continue
|
||||
}
|
||||
processed, consumed[slot-offset] = true, true
|
||||
st := has.Grow(-expected.Count() * timesCrafted)
|
||||
h.setItemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerCraftingInput},
|
||||
Slot: byte(slot),
|
||||
}, st, s, tx)
|
||||
break
|
||||
}
|
||||
if !processed {
|
||||
return fmt.Errorf("recipe %v: could not consume expected item: %v", a.RecipeNetworkID, expected)
|
||||
}
|
||||
}
|
||||
return h.createResults(s, tx, repeatStacks(craft.Output(), timesCrafted)...)
|
||||
}
|
||||
|
||||
// handleAutoCraft handles the AutoCraftRecipe request action.
|
||||
func (h *ItemStackRequestHandler) handleAutoCraft(a *protocol.AutoCraftRecipeStackRequestAction, s *Session, tx *world.Tx) error {
|
||||
craft, ok := s.recipes[a.RecipeNetworkID]
|
||||
if !ok {
|
||||
// Try dynamic recipes if no static recipe matches
|
||||
return h.tryDynamicCraft(s, tx, int(a.TimesCrafted))
|
||||
}
|
||||
_, shaped := craft.(recipe.Shaped)
|
||||
_, shapeless := craft.(recipe.Shapeless)
|
||||
if !shaped && !shapeless {
|
||||
return fmt.Errorf("recipe with network id %v is not a shaped or shapeless recipe", a.RecipeNetworkID)
|
||||
}
|
||||
if craft.Block() != "crafting_table" {
|
||||
return fmt.Errorf("recipe with network id %v is not a crafting table recipe", a.RecipeNetworkID)
|
||||
}
|
||||
|
||||
timesCrafted := int(a.TimesCrafted)
|
||||
if timesCrafted < 1 {
|
||||
return fmt.Errorf("times crafted must be at least 1")
|
||||
}
|
||||
|
||||
flattenedInputs := make([]recipe.Item, 0, len(craft.Input()))
|
||||
for _, i := range craft.Input() {
|
||||
if i.Empty() {
|
||||
// We don't actually need this item - it's empty, so avoid putting it in our flattened inputs.
|
||||
continue
|
||||
}
|
||||
|
||||
if ind := slices.IndexFunc(flattenedInputs, func(it recipe.Item) bool {
|
||||
return matchingStacks(it, i)
|
||||
}); ind >= 0 {
|
||||
flattenedInputs[ind] = grow(i, flattenedInputs[ind].Count())
|
||||
continue
|
||||
}
|
||||
flattenedInputs = append(flattenedInputs, i)
|
||||
}
|
||||
|
||||
for _, expected := range flattenedInputs {
|
||||
remaining := expected.Count() * timesCrafted
|
||||
|
||||
for id, inv := range map[byte]*inventory.Inventory{
|
||||
protocol.ContainerCraftingInput: s.ui,
|
||||
protocol.ContainerCombinedHotBarAndInventory: s.inv,
|
||||
} {
|
||||
for slot, has := range inv.Slots() {
|
||||
if has.Empty() {
|
||||
// We don't have this item, skip it.
|
||||
continue
|
||||
}
|
||||
if !matchingStacks(has, expected) {
|
||||
// Not the same item.
|
||||
continue
|
||||
}
|
||||
|
||||
removal := has.Count()
|
||||
if remaining < removal {
|
||||
removal = remaining
|
||||
}
|
||||
remaining -= removal
|
||||
|
||||
has = has.Grow(-removal)
|
||||
h.setItemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: id},
|
||||
Slot: byte(slot),
|
||||
}, has, s, tx)
|
||||
if remaining == 0 {
|
||||
// Consumed this item, so go to the next one.
|
||||
break
|
||||
}
|
||||
}
|
||||
if remaining == 0 {
|
||||
// Consumed this item, so go to the next one.
|
||||
break
|
||||
}
|
||||
}
|
||||
if remaining != 0 {
|
||||
return fmt.Errorf("recipe %v: could not consume expected item: %v", a.RecipeNetworkID, expected)
|
||||
}
|
||||
}
|
||||
|
||||
return h.createResults(s, tx, repeatStacks(craft.Output(), timesCrafted)...)
|
||||
}
|
||||
|
||||
// handleCreativeCraft handles the CreativeCraft request action.
|
||||
func (h *ItemStackRequestHandler) handleCreativeCraft(a *protocol.CraftCreativeStackRequestAction, s *Session, tx *world.Tx, c Controllable) error {
|
||||
if !c.GameMode().CreativeInventory() {
|
||||
return fmt.Errorf("can only craft creative items in gamemode creative/spectator")
|
||||
}
|
||||
index := a.CreativeItemNetworkID - 1
|
||||
if int(index) >= len(creative.Items()) {
|
||||
return fmt.Errorf("creative item with network ID %v does not exist", index)
|
||||
}
|
||||
it := creative.Items()[index].Stack
|
||||
it = it.Grow(it.MaxCount() - 1)
|
||||
return h.createResults(s, tx, it)
|
||||
}
|
||||
|
||||
// craftingSize gets the crafting size based on the opened container ID.
|
||||
func (s *Session) craftingSize() uint32 {
|
||||
if s.openedContainerID.Load() == 1 {
|
||||
return craftingGridSizeLarge
|
||||
}
|
||||
return craftingGridSizeSmall
|
||||
}
|
||||
|
||||
// craftingOffset gets the crafting offset based on the opened container ID.
|
||||
func (s *Session) craftingOffset() uint32 {
|
||||
if s.openedContainerID.Load() == 1 {
|
||||
return craftingGridLargeOffset
|
||||
}
|
||||
return craftingGridSmallOffset
|
||||
}
|
||||
|
||||
// matchingStacks returns true if the two stacks are the same in a crafting scenario.
|
||||
func matchingStacks(has, expected recipe.Item) bool {
|
||||
switch expected := expected.(type) {
|
||||
case item.Stack:
|
||||
switch has := has.(type) {
|
||||
case recipe.ItemTag:
|
||||
name, _ := expected.Item().EncodeItem()
|
||||
return has.Contains(name)
|
||||
case item.Stack:
|
||||
_, variants := expected.Value("variants")
|
||||
if !variants {
|
||||
return has.Comparable(expected)
|
||||
}
|
||||
nameOne, _ := has.Item().EncodeItem()
|
||||
nameTwo, _ := expected.Item().EncodeItem()
|
||||
return nameOne == nameTwo
|
||||
}
|
||||
panic(fmt.Errorf("client has unexpected recipe item %T", has))
|
||||
case recipe.ItemTag:
|
||||
switch has := has.(type) {
|
||||
case item.Stack:
|
||||
name, _ := has.Item().EncodeItem()
|
||||
return expected.Contains(name)
|
||||
case recipe.ItemTag:
|
||||
return has.Tag() == expected.Tag()
|
||||
}
|
||||
panic(fmt.Errorf("client has unexpected recipe item %T", has))
|
||||
}
|
||||
panic(fmt.Errorf("tried to match with unexpected recipe item %T", expected))
|
||||
}
|
||||
|
||||
// repeatStacks multiplies the count of all item stacks provided by the number of repetitions provided. Item
|
||||
// stacks where the new count would exceed the item's max count are split into multiple item stacks.
|
||||
func repeatStacks(items []item.Stack, repetitions int) []item.Stack {
|
||||
output := make([]item.Stack, 0, len(items))
|
||||
for _, o := range items {
|
||||
count, maxCount := o.Count(), o.MaxCount()
|
||||
total := count * repetitions
|
||||
|
||||
stacks := int(math.Ceil(float64(total) / float64(maxCount)))
|
||||
for i := 0; i < stacks; i++ {
|
||||
inc := min(total, maxCount)
|
||||
total -= inc
|
||||
|
||||
output = append(output, o.Grow(inc-count))
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func grow(i recipe.Item, count int) recipe.Item {
|
||||
switch i := i.(type) {
|
||||
case item.Stack:
|
||||
return i.Grow(count)
|
||||
case recipe.ItemTag:
|
||||
return recipe.NewItemTag(i.Tag(), i.Count()+count)
|
||||
}
|
||||
panic(fmt.Errorf("unexpected recipe item %T", i))
|
||||
}
|
||||
|
||||
// tryDynamicCraft attempts to match the items in the crafting grid with any registered dynamic recipes.
|
||||
func (h *ItemStackRequestHandler) tryDynamicCraft(s *Session, tx *world.Tx, timesCrafted int) error {
|
||||
if timesCrafted < 1 {
|
||||
return fmt.Errorf("times crafted must be at least 1")
|
||||
}
|
||||
|
||||
size := s.craftingSize()
|
||||
offset := s.craftingOffset()
|
||||
|
||||
// Collect all items from the crafting grid
|
||||
input := make([]recipe.Item, size)
|
||||
for i := uint32(0); i < size; i++ {
|
||||
slot := offset + i
|
||||
it, _ := s.ui.Item(int(slot))
|
||||
if it.Empty() {
|
||||
input[i] = item.Stack{}
|
||||
} else {
|
||||
input[i] = it
|
||||
}
|
||||
}
|
||||
|
||||
// Try to match with any dynamic recipe
|
||||
for _, dynamicRecipe := range recipe.DynamicRecipes() {
|
||||
if dynamicRecipe.Block() != "crafting_table" {
|
||||
continue
|
||||
}
|
||||
|
||||
output, ok := dynamicRecipe.Match(input)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Found a matching dynamic recipe! Now validate ingredient counts and consume the items
|
||||
// For dynamic recipes, we consume all non-empty slots, but we need to ensure each slot
|
||||
// has enough items to craft timesCrafted times.
|
||||
minStackCount := math.MaxInt
|
||||
for i := uint32(0); i < size; i++ {
|
||||
slot := offset + i
|
||||
it, _ := s.ui.Item(int(slot))
|
||||
if !it.Empty() {
|
||||
if it.Count() < minStackCount {
|
||||
minStackCount = it.Count()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cap timesCrafted to the minimum available stack count to prevent item duplication
|
||||
if minStackCount < timesCrafted {
|
||||
timesCrafted = minStackCount
|
||||
}
|
||||
|
||||
// Now consume the validated amount from each non-empty slot
|
||||
for i := uint32(0); i < size; i++ {
|
||||
slot := offset + i
|
||||
it, _ := s.ui.Item(int(slot))
|
||||
if !it.Empty() {
|
||||
// Consume one item from this slot per craft
|
||||
st := it.Grow(-1 * timesCrafted)
|
||||
h.setItemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerCraftingInput},
|
||||
Slot: byte(slot),
|
||||
}, st, s, tx)
|
||||
}
|
||||
}
|
||||
|
||||
return h.createResults(s, tx, repeatStacks(output, timesCrafted)...)
|
||||
}
|
||||
|
||||
return fmt.Errorf("no matching recipe found for crafting grid")
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
"time"
|
||||
)
|
||||
|
||||
// EmoteHandler handles the Emote packet.
|
||||
type EmoteHandler struct {
|
||||
LastEmote time.Time
|
||||
}
|
||||
|
||||
// Handle ...
|
||||
func (h *EmoteHandler) Handle(p packet.Packet, _ *Session, tx *world.Tx, c Controllable) error {
|
||||
pk := p.(*packet.Emote)
|
||||
|
||||
if pk.EntityRuntimeID != selfEntityRuntimeID {
|
||||
return errSelfRuntimeID
|
||||
}
|
||||
if time.Since(h.LastEmote) < time.Second {
|
||||
return nil
|
||||
}
|
||||
h.LastEmote = time.Now()
|
||||
emote, err := uuid.Parse(pk.EmoteID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, viewer := range tx.Viewers(c.Position()) {
|
||||
viewer.ViewEmote(c, emote)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
"slices"
|
||||
|
||||
"github.com/df-mc/dragonfly/server/block"
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/internal/sliceutil"
|
||||
"github.com/df-mc/dragonfly/server/item"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
)
|
||||
|
||||
const (
|
||||
// enchantingInputSlot is the slot index of the input item in the enchanting table.
|
||||
enchantingInputSlot = 0x0e
|
||||
// enchantingLapisSlot is the slot index of the lapis in the enchanting table.
|
||||
enchantingLapisSlot = 0x0f
|
||||
)
|
||||
|
||||
// handleEnchant handles the enchantment of an item using the CraftRecipe stack request action.
|
||||
func (h *ItemStackRequestHandler) handleEnchant(a *protocol.CraftRecipeStackRequestAction, s *Session, tx *world.Tx, c Controllable) error {
|
||||
// First ensure that the selected slot is not out of bounds.
|
||||
if a.RecipeNetworkID > 2 {
|
||||
return fmt.Errorf("invalid recipe network id: %d", a.RecipeNetworkID)
|
||||
}
|
||||
|
||||
// Now ensure we have an input and only one input.
|
||||
input, err := s.ui.Item(enchantingInputSlot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if input.Count() > 1 {
|
||||
return fmt.Errorf("enchanting tables only accept one item at a time")
|
||||
}
|
||||
|
||||
// Determine the available enchantments using the session's enchantment seed.
|
||||
allCosts, allEnchants := s.determineAvailableEnchantments(tx, c, *s.openedPos.Load(), input)
|
||||
if len(allEnchants) == 0 {
|
||||
return fmt.Errorf("can't enchant non-enchantable item")
|
||||
}
|
||||
|
||||
// Use the slot plus one as the cost. The requirement and enchantments can be found in the results from
|
||||
// determineAvailableEnchantments using the same slot index.
|
||||
cost := int(a.RecipeNetworkID + 1)
|
||||
requirement := allCosts[a.RecipeNetworkID]
|
||||
enchants := allEnchants[a.RecipeNetworkID]
|
||||
|
||||
// If we don't have infinite resources, we need to deduct Lapis Lazuli and experience.
|
||||
if !c.GameMode().CreativeInventory() {
|
||||
// First ensure that the experience level is both underneath the requirement and the cost.
|
||||
if c.ExperienceLevel() < requirement {
|
||||
return fmt.Errorf("not enough levels to meet requirement")
|
||||
}
|
||||
if c.ExperienceLevel() < cost {
|
||||
return fmt.Errorf("not enough levels to meet cost")
|
||||
}
|
||||
|
||||
// Then ensure that the player has input Lapis Lazuli, and enough of it to meet the cost.
|
||||
lapis, err := s.ui.Item(enchantingLapisSlot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, ok := lapis.Item().(item.LapisLazuli); !ok {
|
||||
return fmt.Errorf("lapis lazuli was not input")
|
||||
}
|
||||
if lapis.Count() < cost {
|
||||
return fmt.Errorf("not enough lapis lazuli to meet cost")
|
||||
}
|
||||
|
||||
// Deduct the experience and Lapis Lazuli.
|
||||
c.SetExperienceLevel(c.ExperienceLevel() - cost)
|
||||
h.setItemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerEnchantingMaterial},
|
||||
Slot: enchantingLapisSlot,
|
||||
}, lapis.Grow(-cost), s, tx)
|
||||
}
|
||||
|
||||
// Reset the enchantment seed so different enchantments can be selected.
|
||||
c.ResetEnchantmentSeed()
|
||||
|
||||
// Clear the existing input item, and apply the new item into the crafting result slot of the UI. The client will
|
||||
// automatically move the item into the input slot.
|
||||
h.setItemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerEnchantingInput},
|
||||
Slot: enchantingInputSlot,
|
||||
}, item.Stack{}, s, tx)
|
||||
|
||||
return h.createResults(s, tx, input.WithEnchantments(enchants...))
|
||||
}
|
||||
|
||||
// sendEnchantmentOptions sends a list of available enchantments to the client based on the client's enchantment seed
|
||||
// and nearby bookshelves.
|
||||
func (s *Session) sendEnchantmentOptions(tx *world.Tx, c Controllable, pos cube.Pos, stack item.Stack) {
|
||||
// First determine the available enchantments for the given item stack.
|
||||
selectedCosts, selectedEnchants := s.determineAvailableEnchantments(tx, c, pos, stack)
|
||||
if len(selectedEnchants) == 0 {
|
||||
// No available enchantments.
|
||||
return
|
||||
}
|
||||
|
||||
// Build the protocol variant of the enchantment options.
|
||||
options := make([]protocol.EnchantmentOption, 0, 3)
|
||||
for i := 0; i < 3; i++ {
|
||||
// First build the enchantment instances for each selected enchantment.
|
||||
enchants := make([]protocol.EnchantmentInstance, 0, len(selectedEnchants[i]))
|
||||
for _, enchant := range selectedEnchants[i] {
|
||||
id, _ := item.EnchantmentID(enchant.Type())
|
||||
enchants = append(enchants, protocol.EnchantmentInstance{
|
||||
Type: byte(id),
|
||||
Level: byte(enchant.Level()),
|
||||
})
|
||||
}
|
||||
|
||||
// Then build the enchantment option. We can use the slot as the RecipeNetworkID, since the IDs seem to be unique
|
||||
// to enchanting tables only. We also only need to set the middle index of Enchantments. The other two serve
|
||||
// an unknown purpose and can cause various unexpected issues.
|
||||
options = append(options, protocol.EnchantmentOption{
|
||||
Name: enchantNames[rand.IntN(len(enchantNames))],
|
||||
Cost: uint8(selectedCosts[i]),
|
||||
RecipeNetworkID: uint32(i),
|
||||
Enchantments: protocol.ItemEnchantments{
|
||||
Slot: int32(i),
|
||||
Enchantments: [3][]protocol.EnchantmentInstance{1: enchants},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Send the enchantment options to the client.
|
||||
s.writePacket(&packet.PlayerEnchantOptions{Options: options})
|
||||
}
|
||||
|
||||
// determineAvailableEnchantments returns a list of pseudo-random enchantments for the given item stack.
|
||||
func (s *Session) determineAvailableEnchantments(tx *world.Tx, c Controllable, pos cube.Pos, stack item.Stack) ([]int, [][]item.Enchantment) {
|
||||
// First ensure that the item is enchantable and does not already have any enchantments.
|
||||
enchantable, ok := stack.Item().(item.Enchantable)
|
||||
if !ok {
|
||||
// We can't enchant this item.
|
||||
return nil, nil
|
||||
}
|
||||
if len(stack.Enchantments()) > 0 {
|
||||
// We can't enchant this item.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Search for bookshelves around the enchanting table. Bookshelves help boost the value of the enchantments that
|
||||
// are selected, resulting in enchantments that are rarer but also more expensive.
|
||||
seed := uint64(c.EnchantmentSeed())
|
||||
random := rand.New(rand.NewPCG(seed, seed))
|
||||
bookshelves := searchBookshelves(tx, pos)
|
||||
value := enchantable.EnchantmentValue()
|
||||
|
||||
// Calculate the base cost, used to calculate the upper, middle, and lower level costs.
|
||||
baseCost := random.IntN(8) + 1 + (bookshelves >> 1) + random.IntN(bookshelves+1)
|
||||
|
||||
// Calculate the upper, middle, and lower level costs.
|
||||
upperLevelCost := max(baseCost/3, 1)
|
||||
middleLevelCost := baseCost*2/3 + 1
|
||||
lowerLevelCost := max(baseCost, bookshelves*2)
|
||||
|
||||
// Create a list of available enchantments for each slot.
|
||||
return []int{
|
||||
upperLevelCost,
|
||||
middleLevelCost,
|
||||
lowerLevelCost,
|
||||
}, [][]item.Enchantment{
|
||||
createEnchantments(random, stack, value, upperLevelCost),
|
||||
createEnchantments(random, stack, value, middleLevelCost),
|
||||
createEnchantments(random, stack, value, lowerLevelCost),
|
||||
}
|
||||
}
|
||||
|
||||
// treasureEnchantment represents an enchantment that may be a treasure enchantment.
|
||||
type treasureEnchantment interface {
|
||||
item.EnchantmentType
|
||||
Treasure() bool
|
||||
}
|
||||
|
||||
// createEnchantments creates a list of enchantments for the given item stack and returns them.
|
||||
func createEnchantments(random *rand.Rand, stack item.Stack, value, level int) []item.Enchantment {
|
||||
// Calculate the "random bonus" for this level. This factor is used in calculating the enchantment cost, used
|
||||
// during the selection of enchantments.
|
||||
randomBonus := (random.Float64() + random.Float64() - 1.0) * 0.15
|
||||
|
||||
// Calculate the enchantment cost and clamp it to ensure it is always at least one with triangular distribution.
|
||||
cost := level + 1 + random.IntN(value/4+1) + random.IntN(value/4+1)
|
||||
cost = clamp(int(math.Round(float64(cost)+float64(cost)*randomBonus)), 1, math.MaxInt32)
|
||||
|
||||
// Books are applicable to all enchantments, so make sure we have a flag for them here.
|
||||
it := stack.Item()
|
||||
_, book := it.(item.Book)
|
||||
|
||||
// Now that we have our enchantment cost, we need to select the available enchantments. First, we iterate through
|
||||
// each possible enchantment.
|
||||
availableEnchants := make([]item.Enchantment, 0, len(item.Enchantments()))
|
||||
for _, enchant := range item.Enchantments() {
|
||||
if t, ok := enchant.(treasureEnchantment); ok && t.Treasure() {
|
||||
// We then have to ensure that the enchantment is not a treasure enchantment, as those cannot be selected through
|
||||
// the enchanting table.
|
||||
continue
|
||||
}
|
||||
if !book && !enchant.CompatibleWithItem(it) {
|
||||
// The enchantment is not compatible with the item.
|
||||
continue
|
||||
}
|
||||
|
||||
// Now iterate through each possible level of the enchantment.
|
||||
for i := enchant.MaxLevel(); i > 0; i-- {
|
||||
// Use the level to calculate the minimum and maximum costs for this enchantment.
|
||||
if minCost, maxCost := enchant.Cost(i); cost >= minCost && cost <= maxCost {
|
||||
// If the cost is within the bounds, add the enchantment to the list of available enchantments.
|
||||
availableEnchants = append(availableEnchants, item.NewEnchantment(enchant, i))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(availableEnchants) == 0 {
|
||||
// No available enchantments, so we can't really do much here.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Now we need to select the enchantments.
|
||||
selectedEnchants := make([]item.Enchantment, 0, len(availableEnchants))
|
||||
|
||||
// Select the first enchantment using a weighted random algorithm, favouring enchantments that have a higher weight.
|
||||
// These weights are based on the enchantment's rarity, with common and uncommon enchantments having a higher weight
|
||||
// than rare and very rare enchantments.
|
||||
enchant := weightedRandomEnchantment(random, availableEnchants)
|
||||
selectedEnchants = append(selectedEnchants, enchant)
|
||||
|
||||
// Remove the selected enchantment from the list of available enchantments, so we don't select it again.
|
||||
ind := slices.Index(availableEnchants, enchant)
|
||||
availableEnchants = slices.Delete(availableEnchants, ind, ind+1)
|
||||
|
||||
// Based on the cost, select a random amount of additional enchantments.
|
||||
for random.IntN(50) <= cost {
|
||||
// Ensure that we don't have any conflicting enchantments. If so, remove them from the list of available
|
||||
// enchantments.
|
||||
lastEnchant := selectedEnchants[len(selectedEnchants)-1]
|
||||
if availableEnchants = sliceutil.Filter(availableEnchants, func(enchant item.Enchantment) bool {
|
||||
return lastEnchant.Type().CompatibleWithEnchantment(enchant.Type())
|
||||
}); len(availableEnchants) == 0 {
|
||||
// We've exhausted all available enchantments.
|
||||
break
|
||||
}
|
||||
|
||||
// Select another enchantment using the same weighted random algorithm.
|
||||
enchant = weightedRandomEnchantment(random, availableEnchants)
|
||||
selectedEnchants = append(selectedEnchants, enchant)
|
||||
|
||||
// Remove the selected enchantment from the list of available enchantments, so we don't select it again.
|
||||
ind = slices.Index(availableEnchants, enchant)
|
||||
availableEnchants = slices.Delete(availableEnchants, ind, ind+1)
|
||||
|
||||
// Halve the cost, so we have a lower chance of selecting another enchantment.
|
||||
cost /= 2
|
||||
}
|
||||
return selectedEnchants
|
||||
}
|
||||
|
||||
// searchBookshelves searches for nearby bookshelves around the position passed, and returns the amount found.
|
||||
func searchBookshelves(tx *world.Tx, pos cube.Pos) (shelves int) {
|
||||
for x := -1; x <= 1; x++ {
|
||||
for z := -1; z <= 1; z++ {
|
||||
for y := 0; y <= 1; y++ {
|
||||
if x == 0 && z == 0 {
|
||||
// Ignore the centre block.
|
||||
continue
|
||||
}
|
||||
if _, ok := tx.Block(pos.Add(cube.Pos{x, y, z})).(block.Air); !ok {
|
||||
// There must be a one block space between the bookshelf and the player.
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for a bookshelf two blocks away.
|
||||
if _, ok := tx.Block(pos.Add(cube.Pos{x * 2, y, z * 2})).(block.Bookshelf); ok {
|
||||
shelves++
|
||||
}
|
||||
if x != 0 && z != 0 {
|
||||
// Check for a bookshelf two blocks away on the X axis.
|
||||
if _, ok := tx.Block(pos.Add(cube.Pos{x * 2, y, z})).(block.Bookshelf); ok {
|
||||
shelves++
|
||||
}
|
||||
// Check for a bookshelf two blocks away on the Z axis.
|
||||
if _, ok := tx.Block(pos.Add(cube.Pos{x, y, z * 2})).(block.Bookshelf); ok {
|
||||
shelves++
|
||||
}
|
||||
}
|
||||
|
||||
if shelves >= 15 {
|
||||
// We've found enough bookshelves.
|
||||
return 15
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return shelves
|
||||
}
|
||||
|
||||
// weightedRandomEnchantment returns a random enchantment from the given list of enchantments using the rarity weight of
|
||||
// each enchantment.
|
||||
func weightedRandomEnchantment(rs *rand.Rand, enchants []item.Enchantment) item.Enchantment {
|
||||
var totalWeight int
|
||||
for _, e := range enchants {
|
||||
totalWeight += e.Type().Rarity().Weight()
|
||||
}
|
||||
r := rs.IntN(totalWeight)
|
||||
for _, e := range enchants {
|
||||
r -= e.Type().Rarity().Weight()
|
||||
if r < 0 {
|
||||
return e
|
||||
}
|
||||
}
|
||||
panic("should never happen")
|
||||
}
|
||||
|
||||
// clamp clamps a value into the given range.
|
||||
func clamp(value, min, max int) int {
|
||||
if value < min {
|
||||
return min
|
||||
}
|
||||
if value > max {
|
||||
return max
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
|
||||
"github.com/df-mc/dragonfly/server/block"
|
||||
"github.com/df-mc/dragonfly/server/entity"
|
||||
"github.com/df-mc/dragonfly/server/item"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol"
|
||||
)
|
||||
|
||||
const (
|
||||
// grindstoneFirstInputSlot is the slot index of the first input item in the grindstone.
|
||||
grindstoneFirstInputSlot = 0x10
|
||||
// grindstoneSecondInputSlot is the slot index of the second input item in the grindstone.
|
||||
grindstoneSecondInputSlot = 0x11
|
||||
)
|
||||
|
||||
// handleGrindstoneCraft handles a CraftGrindstoneRecipe stack request action made using a grindstone.
|
||||
func (h *ItemStackRequestHandler) handleGrindstoneCraft(s *Session, tx *world.Tx, c Controllable) error {
|
||||
// First check if there actually is a grindstone opened.
|
||||
if !s.containerOpened.Load() {
|
||||
return fmt.Errorf("no grindstone container opened")
|
||||
}
|
||||
if _, ok := tx.Block(*s.openedPos.Load()).(block.Grindstone); !ok {
|
||||
return fmt.Errorf("no grindstone container opened")
|
||||
}
|
||||
|
||||
// Next, get both input items and ensure they are comparable.
|
||||
firstInput, _ := h.itemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerGrindstoneInput},
|
||||
Slot: grindstoneFirstInputSlot,
|
||||
}, s, tx)
|
||||
secondInput, _ := h.itemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerGrindstoneAdditional},
|
||||
Slot: grindstoneSecondInputSlot,
|
||||
}, s, tx)
|
||||
if firstInput.Empty() && secondInput.Empty() {
|
||||
return fmt.Errorf("input item(s) are empty")
|
||||
}
|
||||
if firstInput.Count() > 1 || secondInput.Count() > 1 {
|
||||
return fmt.Errorf("input item(s) are not single items")
|
||||
}
|
||||
|
||||
resultStack := nonZeroItem(firstInput, secondInput)
|
||||
if !firstInput.Empty() && !secondInput.Empty() {
|
||||
name, meta := firstInput.Item().EncodeItem()
|
||||
name2, meta2 := secondInput.Item().EncodeItem()
|
||||
if name != name2 || meta != meta2 {
|
||||
return fmt.Errorf("input items must be the same type")
|
||||
}
|
||||
if _, ok := firstInput.Item().(item.Durable); !ok {
|
||||
return fmt.Errorf("input items must be durable")
|
||||
}
|
||||
|
||||
// We add the enchantments to the result stack in order to calculate the gained experience. These enchantments
|
||||
// are stripped when creating the result.
|
||||
resultStack = firstInput.WithEnchantments(secondInput.Enchantments()...)
|
||||
|
||||
// Merge the durability of the two input items at 5%.
|
||||
maxDurability := firstInput.MaxDurability()
|
||||
firstDurability, secondDurability := firstInput.Durability(), secondInput.Durability()
|
||||
|
||||
resultStack = resultStack.WithDurability(firstDurability + secondDurability + maxDurability*5/100)
|
||||
}
|
||||
|
||||
for _, o := range entity.NewExperienceOrbs(entity.EyePosition(c), experienceFromEnchantments(resultStack)) {
|
||||
tx.AddEntity(o)
|
||||
}
|
||||
|
||||
h.setItemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerGrindstoneInput},
|
||||
Slot: grindstoneFirstInputSlot,
|
||||
}, item.Stack{}, s, tx)
|
||||
h.setItemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerGrindstoneAdditional},
|
||||
Slot: grindstoneSecondInputSlot,
|
||||
}, item.Stack{}, s, tx)
|
||||
return h.createResults(s, tx, stripPossibleEnchantments(resultStack))
|
||||
}
|
||||
|
||||
// curseEnchantment represents an enchantment that may be a curse enchantment.
|
||||
type curseEnchantment interface {
|
||||
Curse() bool
|
||||
}
|
||||
|
||||
// experienceFromEnchantments returns the amount of experience that is gained from the enchantments on the given stack.
|
||||
func experienceFromEnchantments(stack item.Stack) int {
|
||||
var totalCost int
|
||||
for _, enchant := range stack.Enchantments() {
|
||||
if _, ok := enchant.Type().(curseEnchantment); ok {
|
||||
continue
|
||||
}
|
||||
cost, _ := enchant.Type().Cost(enchant.Level())
|
||||
totalCost += cost
|
||||
}
|
||||
if totalCost == 0 {
|
||||
// No cost, no experience.
|
||||
return 0
|
||||
}
|
||||
|
||||
minExperience := int(math.Ceil(float64(totalCost) / 2))
|
||||
return minExperience + rand.IntN(minExperience)
|
||||
}
|
||||
|
||||
// stripPossibleEnchantments strips all enchantments possible, excluding curses.
|
||||
func stripPossibleEnchantments(stack item.Stack) item.Stack {
|
||||
for _, enchant := range stack.Enchantments() {
|
||||
if _, ok := enchant.Type().(curseEnchantment); ok {
|
||||
continue
|
||||
}
|
||||
stack = stack.WithoutEnchantments(enchant.Type())
|
||||
}
|
||||
return stack.WithAnvilCost(0)
|
||||
}
|
||||
|
||||
// nonZeroItem returns the item.Stack that exists out of two input items. The function expects at least one of the
|
||||
// items to be non-empty.
|
||||
func nonZeroItem(first, second item.Stack) item.Stack {
|
||||
if first.Empty() {
|
||||
return second
|
||||
}
|
||||
return first
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
)
|
||||
|
||||
// InteractHandler handles the packet.Interact.
|
||||
type InteractHandler struct{}
|
||||
|
||||
// Handle ...
|
||||
func (h *InteractHandler) Handle(p packet.Packet, s *Session, _ *world.Tx, c Controllable) error {
|
||||
pk := p.(*packet.Interact)
|
||||
pos := c.Position()
|
||||
|
||||
switch pk.ActionType {
|
||||
case packet.InteractActionMouseOverEntity:
|
||||
// We don't need this action.
|
||||
case packet.InteractActionOpenInventory:
|
||||
if s.invOpened {
|
||||
// When there is latency, this might end up being sent multiple times. If we send a ContainerOpen
|
||||
// multiple times, the client crashes.
|
||||
return nil
|
||||
}
|
||||
s.invOpened = true
|
||||
s.writePacket(&packet.ContainerOpen{
|
||||
WindowID: 0,
|
||||
ContainerType: 0xff,
|
||||
ContainerEntityUniqueID: -1,
|
||||
ContainerPosition: protocol.BlockPos{
|
||||
int32(pos[0]),
|
||||
int32(pos[1]),
|
||||
int32(pos[2]),
|
||||
},
|
||||
})
|
||||
default:
|
||||
return fmt.Errorf("unexpected interact packet action %v", pk.ActionType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/event"
|
||||
"github.com/df-mc/dragonfly/server/item"
|
||||
"github.com/df-mc/dragonfly/server/item/inventory"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
)
|
||||
|
||||
// InventoryTransactionHandler handles the InventoryTransaction packet.
|
||||
type InventoryTransactionHandler struct{}
|
||||
|
||||
// Handle ...
|
||||
func (h *InventoryTransactionHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) (err error) {
|
||||
pk := p.(*packet.InventoryTransaction)
|
||||
|
||||
if len(pk.LegacySetItemSlots) > 2 {
|
||||
return fmt.Errorf("too many slot sync requests in inventory transaction")
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// The client has requested the server to resend the specified slots even if they haven't changed server-side.
|
||||
// Handling these requests is necessary to ensure the client's inventory remains in sync with the server.
|
||||
for _, slot := range pk.LegacySetItemSlots {
|
||||
if len(slot.Slots) > 2 {
|
||||
err = fmt.Errorf("too many slots in slot sync request")
|
||||
return
|
||||
}
|
||||
switch slot.ContainerID {
|
||||
case protocol.ContainerOffhand:
|
||||
s.sendInv(s.offHand, protocol.WindowIDOffHand)
|
||||
case protocol.ContainerInventory:
|
||||
for _, slot := range slot.Slots {
|
||||
if i, err := s.inv.Item(int(slot)); err == nil {
|
||||
s.sendItem(i, int(slot), protocol.WindowIDInventory)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
switch data := pk.TransactionData.(type) {
|
||||
case *protocol.NormalTransactionData:
|
||||
h.resendInventories(s)
|
||||
// Always resend inventories with normal transactions. Most of the time we do not use these
|
||||
// transactions, so we're best off making sure the client and server stay in sync.
|
||||
if err := h.handleNormalTransaction(pk, s, c); err != nil {
|
||||
s.conf.Log.Debug("process packet: InventoryTransaction: verify Normal transaction actions: " + err.Error())
|
||||
}
|
||||
return
|
||||
case *protocol.MismatchTransactionData:
|
||||
// Just resend the inventory and don't do anything.
|
||||
h.resendInventories(s)
|
||||
return
|
||||
case *protocol.UseItemOnEntityTransactionData:
|
||||
if err = s.VerifyAndSetHeldSlot(int(data.HotBarSlot), stackToItem(s.br, data.HeldItem.Stack), c); err != nil {
|
||||
return
|
||||
}
|
||||
return h.handleUseItemOnEntityTransaction(data, s, tx, c)
|
||||
case *protocol.UseItemTransactionData:
|
||||
if err = s.VerifyAndSetHeldSlot(int(data.HotBarSlot), stackToItem(s.br, data.HeldItem.Stack), c); err != nil {
|
||||
return
|
||||
}
|
||||
return h.handleUseItemTransaction(data, s, c)
|
||||
case *protocol.ReleaseItemTransactionData:
|
||||
if err = s.VerifyAndSetHeldSlot(int(data.HotBarSlot), stackToItem(s.br, data.HeldItem.Stack), c); err != nil {
|
||||
return
|
||||
}
|
||||
return h.handleReleaseItemTransaction(c)
|
||||
}
|
||||
return fmt.Errorf("unhandled inventory transaction type %T", pk.TransactionData)
|
||||
}
|
||||
|
||||
// resendInventories resends all inventories of the player.
|
||||
func (h *InventoryTransactionHandler) resendInventories(s *Session) {
|
||||
s.sendInv(s.inv, protocol.WindowIDInventory)
|
||||
s.sendInv(s.ui, protocol.WindowIDUI)
|
||||
s.sendInv(s.offHand, protocol.WindowIDOffHand)
|
||||
s.sendInv(s.armour.Inventory(), protocol.WindowIDArmour)
|
||||
}
|
||||
|
||||
// handleNormalTransaction ...
|
||||
func (h *InventoryTransactionHandler) handleNormalTransaction(pk *packet.InventoryTransaction, s *Session, c Controllable) error {
|
||||
if len(pk.Actions) != 2 {
|
||||
return fmt.Errorf("expected two actions for dropping an item, got %d", len(pk.Actions))
|
||||
}
|
||||
|
||||
var (
|
||||
slot int
|
||||
count int
|
||||
expected item.Stack
|
||||
)
|
||||
for _, action := range pk.Actions {
|
||||
switch {
|
||||
case action.SourceType == protocol.InventoryActionSourceWorld && action.InventorySlot == 0:
|
||||
if old := stackToItem(s.br, action.OldItem.Stack); !old.Empty() {
|
||||
return fmt.Errorf("unexpected non-empty old item in transaction action: %#v", action.OldItem)
|
||||
}
|
||||
count = int(action.NewItem.Stack.Count)
|
||||
case action.SourceType == protocol.InventoryActionSourceContainer && action.WindowID == protocol.WindowIDInventory:
|
||||
if expected = stackToItem(s.br, action.OldItem.Stack); expected.Empty() {
|
||||
return fmt.Errorf("unexpected empty old item in transaction action: %#v", action.OldItem)
|
||||
}
|
||||
slot = int(action.InventorySlot)
|
||||
default:
|
||||
return fmt.Errorf("unexpected action type in drop item transaction")
|
||||
}
|
||||
}
|
||||
|
||||
actual, _ := s.inv.Item(slot)
|
||||
if count < 1 {
|
||||
return fmt.Errorf("expected at least one item to be dropped, got %d", count)
|
||||
}
|
||||
if count > actual.Count() {
|
||||
return fmt.Errorf("tried to throw %v items, but held only %v in slot", count, actual.Count())
|
||||
}
|
||||
if !expected.Equal(actual) {
|
||||
return fmt.Errorf("different item thrown than held in slot: %#v was thrown but held %#v", expected, actual)
|
||||
}
|
||||
|
||||
// Explicitly don't re-use the thrown variable. This item was supplied by the user, and if some
|
||||
// logic in the Comparable() method was flawed, users would be able to cheat with item properties.
|
||||
// Only grow or shrink the held item to prevent any such issues.
|
||||
res := actual.Grow(count - actual.Count())
|
||||
if err := call(event.C(inventory.Holder(c)), int(*s.heldSlot), res, s.inv.Handler().HandleDrop); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n := c.Drop(res)
|
||||
_ = s.inv.SetItem(slot, actual.Grow(-n))
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleUseItemOnEntityTransaction ...
|
||||
func (h *InventoryTransactionHandler) handleUseItemOnEntityTransaction(data *protocol.UseItemOnEntityTransactionData, s *Session, tx *world.Tx, c Controllable) error {
|
||||
s.swingingArm.Store(true)
|
||||
defer s.swingingArm.Store(false)
|
||||
|
||||
if data.TargetEntityRuntimeID == selfEntityRuntimeID {
|
||||
return fmt.Errorf("invalid entity interaction: players cannot interact with themselves")
|
||||
}
|
||||
|
||||
handle, ok := s.entityFromRuntimeID(data.TargetEntityRuntimeID)
|
||||
if !ok {
|
||||
// In some cases, for example when a falling block entity solidifies, latency may allow attacking an entity that
|
||||
// no longer exists server side. This is expected, so we shouldn't kick the player.
|
||||
s.conf.Log.Debug("invalid entity interaction: no entity with runtime ID", "ID", data.TargetEntityRuntimeID)
|
||||
return nil
|
||||
}
|
||||
e, ok := handle.Entity(tx)
|
||||
if !ok {
|
||||
s.conf.Log.Debug("invalid entity interaction: entity is not in the same world (anymore)", "ID", data.TargetEntityRuntimeID)
|
||||
return nil
|
||||
}
|
||||
var valid bool
|
||||
switch data.ActionType {
|
||||
case protocol.UseItemOnEntityActionInteract:
|
||||
valid = c.UseItemOnEntity(e)
|
||||
case protocol.UseItemOnEntityActionAttack:
|
||||
valid = c.AttackEntity(e)
|
||||
default:
|
||||
return fmt.Errorf("unhandled UseItemOnEntity ActionType %v", data.ActionType)
|
||||
}
|
||||
if !valid {
|
||||
slot := int(*s.heldSlot)
|
||||
it, _ := s.inv.Item(slot)
|
||||
s.sendItem(it, slot, protocol.WindowIDInventory)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleUseItemTransaction ...
|
||||
func (h *InventoryTransactionHandler) handleUseItemTransaction(data *protocol.UseItemTransactionData, s *Session, c Controllable) error {
|
||||
pos := cube.Pos{int(data.BlockPosition[0]), int(data.BlockPosition[1]), int(data.BlockPosition[2])}
|
||||
s.swingingArm.Store(true)
|
||||
defer s.swingingArm.Store(false)
|
||||
|
||||
// We reset the inventory so that we can send the held item update without the client already
|
||||
// having done that client-side.
|
||||
// Because of the new inventory system, the client will expect a transaction confirmation, but instead of doing that
|
||||
// it's much easier to just resend the inventory.
|
||||
h.resendInventories(s)
|
||||
|
||||
switch data.ActionType {
|
||||
case protocol.UseItemActionBreakBlock:
|
||||
c.BreakBlock(pos)
|
||||
case protocol.UseItemActionClickBlock:
|
||||
c.UseItemOnBlock(pos, cube.Face(data.BlockFace), vec32To64(data.ClickedPosition))
|
||||
case protocol.UseItemActionClickAir:
|
||||
c.UseItem()
|
||||
default:
|
||||
return fmt.Errorf("unhandled UseItem ActionType %v", data.ActionType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleReleaseItemTransaction ...
|
||||
func (h *InventoryTransactionHandler) handleReleaseItemTransaction(c Controllable) error {
|
||||
c.ReleaseItem()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,511 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/df-mc/dragonfly/server/block"
|
||||
"github.com/df-mc/dragonfly/server/entity"
|
||||
"github.com/df-mc/dragonfly/server/event"
|
||||
"github.com/df-mc/dragonfly/server/item"
|
||||
"github.com/df-mc/dragonfly/server/item/inventory"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ItemStackRequestHandler handles the ItemStackRequest packet. It handles the actions done within the
|
||||
// inventory.
|
||||
type ItemStackRequestHandler struct {
|
||||
currentRequest int32
|
||||
|
||||
changes map[byte]map[byte]changeInfo
|
||||
responseChanges map[int32]map[*inventory.Inventory]map[byte]responseChange
|
||||
|
||||
pendingResults []item.Stack
|
||||
|
||||
current time.Time
|
||||
ignoreDestroy bool
|
||||
}
|
||||
|
||||
// responseChange represents a change in a specific item stack response. It holds the timestamp of the
|
||||
// response which is used to get rid of changes that the client will have received.
|
||||
type responseChange struct {
|
||||
id int32
|
||||
timestamp time.Time
|
||||
}
|
||||
|
||||
// changeInfo holds information on a slot change initiated by an item stack request. It holds both the new and the old
|
||||
// item information and is used for reverting and verifying.
|
||||
type changeInfo struct {
|
||||
after protocol.StackResponseSlotInfo
|
||||
before item.Stack
|
||||
}
|
||||
|
||||
// Handle ...
|
||||
func (h *ItemStackRequestHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error {
|
||||
pk := p.(*packet.ItemStackRequest)
|
||||
h.current = time.Now()
|
||||
|
||||
s.inTransaction.Store(true)
|
||||
defer s.inTransaction.Store(false)
|
||||
|
||||
for _, req := range pk.Requests {
|
||||
if err := h.handleRequest(req, s, tx, c); err != nil {
|
||||
// Item stacks being out of sync isn't uncommon, so don't error. Just debug the error and let the
|
||||
// revert do its work.
|
||||
s.conf.Log.Debug("process packet: ItemStackRequest: resolve item stack request: " + err.Error())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleRequest resolves a single item stack request from the client.
|
||||
func (h *ItemStackRequestHandler) handleRequest(req protocol.ItemStackRequest, s *Session, tx *world.Tx, c Controllable) (err error) {
|
||||
h.currentRequest = req.RequestID
|
||||
defer func() {
|
||||
if err != nil {
|
||||
h.reject(req.RequestID, s, tx)
|
||||
return
|
||||
}
|
||||
h.resolve(req.RequestID, s)
|
||||
h.ignoreDestroy = false
|
||||
}()
|
||||
|
||||
for _, action := range req.Actions {
|
||||
switch a := action.(type) {
|
||||
case *protocol.TakeStackRequestAction:
|
||||
err = h.handleTake(a, s, tx, c)
|
||||
case *protocol.PlaceStackRequestAction:
|
||||
err = h.handlePlace(a, s, tx, c)
|
||||
case *protocol.SwapStackRequestAction:
|
||||
err = h.handleSwap(a, s, tx, c)
|
||||
case *protocol.DestroyStackRequestAction:
|
||||
err = h.handleDestroy(a, s, tx, c)
|
||||
case *protocol.DropStackRequestAction:
|
||||
err = h.handleDrop(a, s, tx, c)
|
||||
case *protocol.BeaconPaymentStackRequestAction:
|
||||
err = h.handleBeaconPayment(a, s, tx)
|
||||
case *protocol.CraftRecipeStackRequestAction:
|
||||
if s.containerOpened.Load() {
|
||||
var special bool
|
||||
switch tx.Block(*s.openedPos.Load()).(type) {
|
||||
case block.SmithingTable:
|
||||
err, special = h.handleSmithing(a, s, tx), true
|
||||
case block.Stonecutter:
|
||||
err, special = h.handleStonecutting(a, s, tx), true
|
||||
case block.EnchantingTable:
|
||||
err, special = h.handleEnchant(a, s, tx, c), true
|
||||
}
|
||||
if special {
|
||||
// This was a "special action" and was handled, so we can move onto the next action.
|
||||
break
|
||||
}
|
||||
}
|
||||
err = h.handleCraft(a, s, tx)
|
||||
case *protocol.AutoCraftRecipeStackRequestAction:
|
||||
err = h.handleAutoCraft(a, s, tx)
|
||||
case *protocol.CraftRecipeOptionalStackRequestAction:
|
||||
err = h.handleCraftRecipeOptional(a, s, req.FilterStrings, c, tx)
|
||||
case *protocol.CraftLoomRecipeStackRequestAction:
|
||||
err = h.handleLoomCraft(a, s, tx)
|
||||
case *protocol.CraftGrindstoneRecipeStackRequestAction:
|
||||
err = h.handleGrindstoneCraft(s, tx, c)
|
||||
case *protocol.CraftCreativeStackRequestAction:
|
||||
err = h.handleCreativeCraft(a, s, tx, c)
|
||||
case *protocol.MineBlockStackRequestAction:
|
||||
err = h.handleMineBlock(a, s, tx)
|
||||
case *protocol.CreateStackRequestAction:
|
||||
err = h.handleCreate(a, s, tx)
|
||||
case *protocol.ConsumeStackRequestAction, *protocol.CraftResultsDeprecatedStackRequestAction:
|
||||
// Don't do anything with this.
|
||||
default:
|
||||
return fmt.Errorf("unhandled stack request action %#v", action)
|
||||
}
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%T: %w", action, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// handleTake handles a Take stack request action.
|
||||
func (h *ItemStackRequestHandler) handleTake(a *protocol.TakeStackRequestAction, s *Session, tx *world.Tx, c Controllable) error {
|
||||
return h.handleTransfer(a.Source, a.Destination, a.Count, s, tx, c)
|
||||
}
|
||||
|
||||
// handlePlace handles a Place stack request action.
|
||||
func (h *ItemStackRequestHandler) handlePlace(a *protocol.PlaceStackRequestAction, s *Session, tx *world.Tx, c Controllable) error {
|
||||
return h.handleTransfer(a.Source, a.Destination, a.Count, s, tx, c)
|
||||
}
|
||||
|
||||
// handleTransfer handles the transferring of x count from a source slot to a destination slot.
|
||||
func (h *ItemStackRequestHandler) handleTransfer(from, to protocol.StackRequestSlotInfo, count byte, s *Session, tx *world.Tx, c Controllable) error {
|
||||
if err := h.verifySlots(s, tx, from, to); err != nil {
|
||||
return fmt.Errorf("source slot out of sync: %w", err)
|
||||
}
|
||||
i, _ := h.itemInSlot(from, s, tx)
|
||||
dest, _ := h.itemInSlot(to, s, tx)
|
||||
if !i.Comparable(dest) {
|
||||
return fmt.Errorf("client tried transferring %v to %v, but the stacks are incomparable", i, dest)
|
||||
}
|
||||
if i.Count() < int(count) {
|
||||
return fmt.Errorf("client tried subtracting %v from item count, but there are only %v", count, i.Count())
|
||||
}
|
||||
if (dest.Count()+int(count) > dest.MaxCount()) && !dest.Empty() {
|
||||
return fmt.Errorf("client tried adding %v to item count %v, but max is %v", count, dest.Count(), dest.MaxCount())
|
||||
}
|
||||
if dest.Empty() {
|
||||
dest = i.Grow(-math.MaxInt32)
|
||||
}
|
||||
|
||||
invA, _ := s.invByID(int32(from.Container.ContainerID), tx)
|
||||
invB, _ := s.invByID(int32(to.Container.ContainerID), tx)
|
||||
|
||||
ctx := event.C(inventory.Holder(c))
|
||||
_ = call(ctx, int(from.Slot), i.Grow(int(count)-i.Count()), invA.Handler().HandleTake)
|
||||
err := call(ctx, int(to.Slot), i.Grow(int(count)-i.Count()), invB.Handler().HandlePlace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.setItemInSlot(from, i.Grow(-int(count)), s, tx)
|
||||
h.setItemInSlot(to, dest.Grow(int(count)), s, tx)
|
||||
h.collectRewards(s, invA, int(from.Slot), tx, c)
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleSwap handles a Swap stack request action.
|
||||
func (h *ItemStackRequestHandler) handleSwap(a *protocol.SwapStackRequestAction, s *Session, tx *world.Tx, c Controllable) error {
|
||||
if err := h.verifySlots(s, tx, a.Source, a.Destination); err != nil {
|
||||
return fmt.Errorf("slot out of sync: %w", err)
|
||||
}
|
||||
i, _ := h.itemInSlot(a.Source, s, tx)
|
||||
dest, _ := h.itemInSlot(a.Destination, s, tx)
|
||||
|
||||
invA, _ := s.invByID(int32(a.Source.Container.ContainerID), tx)
|
||||
invB, _ := s.invByID(int32(a.Destination.Container.ContainerID), tx)
|
||||
|
||||
ctx := event.C(inventory.Holder(c))
|
||||
_ = call(ctx, int(a.Source.Slot), i, invA.Handler().HandleTake)
|
||||
_ = call(ctx, int(a.Source.Slot), dest, invA.Handler().HandlePlace)
|
||||
_ = call(ctx, int(a.Destination.Slot), dest, invB.Handler().HandleTake)
|
||||
err := call(ctx, int(a.Destination.Slot), i, invB.Handler().HandlePlace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h.setItemInSlot(a.Source, dest, s, tx)
|
||||
h.setItemInSlot(a.Destination, i, s, tx)
|
||||
h.collectRewards(s, invA, int(a.Source.Slot), tx, c)
|
||||
h.collectRewards(s, invA, int(a.Destination.Slot), tx, c)
|
||||
return nil
|
||||
}
|
||||
|
||||
// collectRewards checks if the source inventory has rewards for the player, for example, experience rewards when
|
||||
// smelting. If it does, it will drop the rewards at the player's location.
|
||||
func (h *ItemStackRequestHandler) collectRewards(s *Session, inv *inventory.Inventory, slot int, tx *world.Tx, c Controllable) {
|
||||
if inv == s.openedWindow.Load() && s.containerOpened.Load() && slot == inv.Size()-1 {
|
||||
if f, ok := tx.Block(*s.openedPos.Load()).(smelter); ok {
|
||||
for _, o := range entity.NewExperienceOrbs(entity.EyePosition(c), f.ResetExperience()) {
|
||||
tx.AddEntity(o)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleDestroy handles the destroying of an item by moving it into the creative inventory.
|
||||
func (h *ItemStackRequestHandler) handleDestroy(a *protocol.DestroyStackRequestAction, s *Session, tx *world.Tx, c Controllable) error {
|
||||
if h.ignoreDestroy {
|
||||
return nil
|
||||
}
|
||||
if !c.GameMode().CreativeInventory() {
|
||||
return fmt.Errorf("can only destroy items in gamemode creative/spectator")
|
||||
}
|
||||
if err := h.verifySlot(a.Source, s, tx); err != nil {
|
||||
return fmt.Errorf("source slot out of sync: %w", err)
|
||||
}
|
||||
i, _ := h.itemInSlot(a.Source, s, tx)
|
||||
if i.Count() < int(a.Count) {
|
||||
return fmt.Errorf("client attempted to destroy %v items, but only %v present", a.Count, i.Count())
|
||||
}
|
||||
|
||||
h.setItemInSlot(a.Source, i.Grow(-int(a.Count)), s, tx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleDrop handles the dropping of an item by moving it outside the inventory while having the
|
||||
// inventory opened.
|
||||
func (h *ItemStackRequestHandler) handleDrop(a *protocol.DropStackRequestAction, s *Session, tx *world.Tx, c Controllable) error {
|
||||
if err := h.verifySlot(a.Source, s, tx); err != nil {
|
||||
return fmt.Errorf("source slot out of sync: %w", err)
|
||||
}
|
||||
i, _ := h.itemInSlot(a.Source, s, tx)
|
||||
if i.Count() < int(a.Count) {
|
||||
return fmt.Errorf("client attempted to drop %v items, but only %v present", a.Count, i.Count())
|
||||
}
|
||||
|
||||
inv, _ := s.invByID(int32(a.Source.Container.ContainerID), tx)
|
||||
if err := call(event.C(inventory.Holder(c)), int(a.Source.Slot), i.Grow(int(a.Count)-i.Count()), inv.Handler().HandleDrop); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n := c.Drop(i.Grow(int(a.Count) - i.Count()))
|
||||
h.setItemInSlot(a.Source, i.Grow(-n), s, tx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleMineBlock handles the action associated with a block being mined by the player. This seems to be a workaround
|
||||
// by Mojang to deal with the durability changes client-side.
|
||||
func (h *ItemStackRequestHandler) handleMineBlock(a *protocol.MineBlockStackRequestAction, s *Session, tx *world.Tx) error {
|
||||
slot := protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerInventory},
|
||||
Slot: byte(a.HotbarSlot),
|
||||
StackNetworkID: a.StackNetworkID,
|
||||
}
|
||||
if err := h.verifySlot(slot, s, tx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update the slots through ItemStackResponses, don't actually do anything special with this action.
|
||||
i, _ := h.itemInSlot(slot, s, tx)
|
||||
h.setItemInSlot(slot, i, s, tx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleCreate handles the CreateStackRequestAction sent by the client when a recipe outputs more than one item. It
|
||||
// contains a result slot, which should map to one of the output items. From there, the server should create the relevant
|
||||
// output as usual.
|
||||
func (h *ItemStackRequestHandler) handleCreate(a *protocol.CreateStackRequestAction, s *Session, tx *world.Tx) error {
|
||||
slot := int(a.ResultsSlot)
|
||||
if slot >= len(h.pendingResults) {
|
||||
return fmt.Errorf("invalid pending result slot: %v", a.ResultsSlot)
|
||||
}
|
||||
|
||||
res := h.pendingResults[slot]
|
||||
if res.Empty() {
|
||||
return fmt.Errorf("tried duplicating created result: %v", slot)
|
||||
}
|
||||
h.pendingResults[slot] = item.Stack{}
|
||||
|
||||
h.setItemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerCreatedOutput},
|
||||
Slot: craftingResult,
|
||||
}, res, s, tx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// defaultCreation represents the CreateStackRequestAction used for single-result crafts.
|
||||
var defaultCreation = &protocol.CreateStackRequestAction{}
|
||||
|
||||
// createResults creates a new craft result and adds it to the list of pending craft results.
|
||||
func (h *ItemStackRequestHandler) createResults(s *Session, tx *world.Tx, result ...item.Stack) error {
|
||||
h.pendingResults = append(h.pendingResults, result...)
|
||||
if len(result) > 1 {
|
||||
// With multiple results, the client notifies the server on when to create the results.
|
||||
return nil
|
||||
}
|
||||
return h.handleCreate(defaultCreation, s, tx)
|
||||
}
|
||||
|
||||
// verifySlots verifies a list of slots passed.
|
||||
func (h *ItemStackRequestHandler) verifySlots(s *Session, tx *world.Tx, slots ...protocol.StackRequestSlotInfo) error {
|
||||
for _, slot := range slots {
|
||||
if err := h.verifySlot(slot, s, tx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// verifySlot checks if the slot passed by the client is the same as that expected by the server.
|
||||
func (h *ItemStackRequestHandler) verifySlot(slot protocol.StackRequestSlotInfo, s *Session, tx *world.Tx) error {
|
||||
if err := h.tryAcknowledgeChanges(s, tx, slot); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(h.responseChanges) > 256 {
|
||||
return fmt.Errorf("too many unacknowledged request slot changes")
|
||||
}
|
||||
inv, _ := s.invByID(int32(slot.Container.ContainerID), tx)
|
||||
|
||||
i, err := h.itemInSlot(slot, s, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clientID, err := h.resolveID(inv, slot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// The client seems to send negative stack network IDs for predictions, which we can ignore. We'll simply
|
||||
// override this network ID later.
|
||||
if id := item_id(i); id != clientID {
|
||||
return fmt.Errorf("stack ID mismatch: client expected %v, but server had %v", clientID, id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveID resolves the stack network ID in the slot passed. If it is negative, it points to an earlier
|
||||
// request, in which case it will look it up in the changes of an earlier response to a request to find the
|
||||
// actual stack network ID in the slot. If it is positive, the ID will be returned again.
|
||||
func (h *ItemStackRequestHandler) resolveID(inv *inventory.Inventory, slot protocol.StackRequestSlotInfo) (int32, error) {
|
||||
if slot.StackNetworkID >= 0 {
|
||||
return slot.StackNetworkID, nil
|
||||
}
|
||||
containerChanges, ok := h.responseChanges[slot.StackNetworkID]
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("slot pointed to stack request %v, but request could not be found", slot.StackNetworkID)
|
||||
}
|
||||
changes, ok := containerChanges[inv]
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("slot pointed to stack request %v with container %v, but that container was not changed in the request", slot.StackNetworkID, slot.Container.ContainerID)
|
||||
}
|
||||
actual, ok := changes[slot.Slot]
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("slot pointed to stack request %v with container %v and slot %v, but that slot was not changed in the request", slot.StackNetworkID, slot.Container.ContainerID, slot.Slot)
|
||||
}
|
||||
return actual.id, nil
|
||||
}
|
||||
|
||||
// tryAcknowledgeChanges iterates through all cached response changes and checks if the stack request slot
|
||||
// info passed from the client has the right stack network ID in any of the stored slots. If this is the case,
|
||||
// that entry is removed, so that the maps are cleaned up eventually.
|
||||
func (h *ItemStackRequestHandler) tryAcknowledgeChanges(s *Session, tx *world.Tx, slot protocol.StackRequestSlotInfo) error {
|
||||
inv, ok := s.invByID(int32(slot.Container.ContainerID), tx)
|
||||
if !ok {
|
||||
return fmt.Errorf("could not find container with id %v", slot.Container.ContainerID)
|
||||
}
|
||||
|
||||
for requestID, containerChanges := range h.responseChanges {
|
||||
for newInv, changes := range containerChanges {
|
||||
for slotIndex, val := range changes {
|
||||
if (slot.Slot == slotIndex && slot.StackNetworkID >= 0 && newInv == inv) || h.current.Sub(val.timestamp) > time.Second*5 {
|
||||
delete(changes, slotIndex)
|
||||
}
|
||||
}
|
||||
if len(changes) == 0 {
|
||||
delete(containerChanges, newInv)
|
||||
}
|
||||
}
|
||||
if len(containerChanges) == 0 {
|
||||
delete(h.responseChanges, requestID)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// itemInSlot looks for the item in the slot as indicated by the slot info passed.
|
||||
func (h *ItemStackRequestHandler) itemInSlot(slot protocol.StackRequestSlotInfo, s *Session, tx *world.Tx) (item.Stack, error) {
|
||||
inv, ok := s.invByID(int32(slot.Container.ContainerID), tx)
|
||||
if !ok {
|
||||
return item.Stack{}, fmt.Errorf("unable to find container with ID %v", slot.Container.ContainerID)
|
||||
}
|
||||
|
||||
sl := int(slot.Slot)
|
||||
if inv == s.offHand {
|
||||
sl = 0
|
||||
}
|
||||
|
||||
i, err := inv.Item(sl)
|
||||
if err != nil {
|
||||
return i, err
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// setItemInSlot sets an item stack in the slot of a container present in the slot info.
|
||||
func (h *ItemStackRequestHandler) setItemInSlot(slot protocol.StackRequestSlotInfo, i item.Stack, s *Session, tx *world.Tx) {
|
||||
inv, _ := s.invByID(int32(slot.Container.ContainerID), tx)
|
||||
|
||||
sl := int(slot.Slot)
|
||||
if inv == s.offHand {
|
||||
sl = 0
|
||||
}
|
||||
|
||||
before, _ := inv.Item(sl)
|
||||
_ = inv.SetItem(sl, i)
|
||||
|
||||
respSlot := protocol.StackResponseSlotInfo{
|
||||
Slot: slot.Slot,
|
||||
HotbarSlot: slot.Slot,
|
||||
Count: byte(i.Count()),
|
||||
StackNetworkID: item_id(i),
|
||||
DurabilityCorrection: int32(i.MaxDurability() - i.Durability()),
|
||||
}
|
||||
|
||||
if h.changes[slot.Container.ContainerID] == nil {
|
||||
h.changes[slot.Container.ContainerID] = map[byte]changeInfo{}
|
||||
}
|
||||
h.changes[slot.Container.ContainerID][slot.Slot] = changeInfo{
|
||||
after: respSlot,
|
||||
before: before,
|
||||
}
|
||||
|
||||
if h.responseChanges[h.currentRequest] == nil {
|
||||
h.responseChanges[h.currentRequest] = map[*inventory.Inventory]map[byte]responseChange{}
|
||||
}
|
||||
if h.responseChanges[h.currentRequest][inv] == nil {
|
||||
h.responseChanges[h.currentRequest][inv] = map[byte]responseChange{}
|
||||
}
|
||||
h.responseChanges[h.currentRequest][inv][slot.Slot] = responseChange{
|
||||
id: respSlot.StackNetworkID,
|
||||
timestamp: h.current,
|
||||
}
|
||||
}
|
||||
|
||||
// resolve resolves the request with the ID passed.
|
||||
func (h *ItemStackRequestHandler) resolve(id int32, s *Session) {
|
||||
info := make([]protocol.StackResponseContainerInfo, 0, len(h.changes))
|
||||
for container, slotInfo := range h.changes {
|
||||
slots := make([]protocol.StackResponseSlotInfo, 0, len(slotInfo))
|
||||
for _, slot := range slotInfo {
|
||||
slots = append(slots, slot.after)
|
||||
}
|
||||
info = append(info, protocol.StackResponseContainerInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: container},
|
||||
SlotInfo: slots,
|
||||
})
|
||||
}
|
||||
s.writePacket(&packet.ItemStackResponse{Responses: []protocol.ItemStackResponse{{
|
||||
Status: protocol.ItemStackResponseStatusOK,
|
||||
RequestID: id,
|
||||
ContainerInfo: info,
|
||||
}}})
|
||||
|
||||
h.changes = map[byte]map[byte]changeInfo{}
|
||||
h.pendingResults = nil
|
||||
}
|
||||
|
||||
// reject rejects the item stack request sent by the client so that it is reverted client-side.
|
||||
func (h *ItemStackRequestHandler) reject(id int32, s *Session, tx *world.Tx) {
|
||||
s.writePacket(&packet.ItemStackResponse{
|
||||
Responses: []protocol.ItemStackResponse{{
|
||||
Status: protocol.ItemStackResponseStatusError,
|
||||
RequestID: id,
|
||||
}},
|
||||
})
|
||||
|
||||
// Revert changes that we already made for valid actions.
|
||||
for container, slots := range h.changes {
|
||||
for slot, info := range slots {
|
||||
inv, _ := s.invByID(int32(container), tx)
|
||||
_ = inv.SetItem(int(slot), info.before)
|
||||
}
|
||||
}
|
||||
|
||||
h.changes = map[byte]map[byte]changeInfo{}
|
||||
h.pendingResults = nil
|
||||
}
|
||||
|
||||
// call uses an event.Context, slot and item.Stack to call the event handler function passed. An error is returned if
|
||||
// the event.Context was cancelled either before or after the call.
|
||||
func call(ctx *inventory.Context, slot int, it item.Stack, f func(ctx *inventory.Context, slot int, it item.Stack)) error {
|
||||
if ctx.Cancelled() {
|
||||
return fmt.Errorf("action was cancelled")
|
||||
}
|
||||
f(ctx, slot, it)
|
||||
if ctx.Cancelled() {
|
||||
return fmt.Errorf("action was cancelled")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/df-mc/dragonfly/server/block"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
)
|
||||
|
||||
// LecternUpdateHandler handles the LecternUpdate packet, sent when a player interacts with a lectern.
|
||||
type LecternUpdateHandler struct{}
|
||||
|
||||
// Handle ...
|
||||
func (LecternUpdateHandler) Handle(p packet.Packet, _ *Session, tx *world.Tx, c Controllable) error {
|
||||
pk := p.(*packet.LecternUpdate)
|
||||
pos := blockPosFromProtocol(pk.Position)
|
||||
if !canReach(c, pos.Vec3Middle()) {
|
||||
return fmt.Errorf("block at %v is not within reach", pos)
|
||||
}
|
||||
if _, ok := tx.Block(pos).(block.Lectern); !ok {
|
||||
return fmt.Errorf("block at %v is not a lectern", pos)
|
||||
}
|
||||
return c.TurnLecternPage(pos, int(pk.Page))
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/df-mc/dragonfly/server/block"
|
||||
"github.com/df-mc/dragonfly/server/item"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol"
|
||||
)
|
||||
|
||||
const (
|
||||
// loomInputSlot is the slot index of the input item in the loom table.
|
||||
loomInputSlot = 0x09
|
||||
// loomDyeSlot is the slot index of the dye item in the loom table.
|
||||
loomDyeSlot = 0x0a
|
||||
// loomPatternSlot is the slot index of the pattern item in the loom table.
|
||||
loomPatternSlot = 0x0b
|
||||
)
|
||||
|
||||
// handleLoomCraft handles a CraftLoomRecipe stack request action made using a loom table.
|
||||
func (h *ItemStackRequestHandler) handleLoomCraft(a *protocol.CraftLoomRecipeStackRequestAction, s *Session, tx *world.Tx) error {
|
||||
// First check if there actually is a loom opened.
|
||||
if _, ok := tx.Block(*s.openedPos.Load()).(block.Loom); !ok || !s.containerOpened.Load() {
|
||||
return fmt.Errorf("no loom container opened")
|
||||
}
|
||||
timesCrafted := int(a.TimesCrafted)
|
||||
if timesCrafted < 1 {
|
||||
return fmt.Errorf("times crafted must be least 1")
|
||||
}
|
||||
|
||||
// Next, check if the input slot has a valid banner item.
|
||||
input, _ := h.itemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerLoomInput},
|
||||
Slot: loomInputSlot,
|
||||
}, s, tx)
|
||||
if input.Count() < timesCrafted {
|
||||
return fmt.Errorf("input item count is less than times crafted")
|
||||
}
|
||||
b, ok := input.Item().(block.Banner)
|
||||
if !ok {
|
||||
return fmt.Errorf("input item is not a banner")
|
||||
}
|
||||
if b.Illager {
|
||||
return fmt.Errorf("input item is an illager banner")
|
||||
}
|
||||
|
||||
// Do the same with the input dye.
|
||||
dye, _ := h.itemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerLoomDye},
|
||||
Slot: loomDyeSlot,
|
||||
}, s, tx)
|
||||
if dye.Count() < timesCrafted {
|
||||
return fmt.Errorf("dye item count is less than times crafted")
|
||||
}
|
||||
d, ok := dye.Item().(item.Dye)
|
||||
if !ok {
|
||||
return fmt.Errorf("dye item is not a dye")
|
||||
}
|
||||
|
||||
// The action contains the pattern that the client wanted to apply, so parse the ID and check if it is a valid
|
||||
// pattern.
|
||||
expectedPattern, exists := block.BannerPatternByID(a.Pattern)
|
||||
if !exists {
|
||||
return fmt.Errorf("unknown banner pattern id %q", a.Pattern)
|
||||
}
|
||||
|
||||
// Some banner patterns have equivalent banner pattern items that are required to craft the pattern. If the expected
|
||||
// pattern has a pattern item, check if the player input the correct pattern item.
|
||||
pattern, _ := h.itemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerLoomMaterial},
|
||||
Slot: loomPatternSlot,
|
||||
}, s, tx)
|
||||
if expectedPatternItem, hasPatternItem := expectedPattern.Item(); hasPatternItem {
|
||||
if pattern.Empty() {
|
||||
return fmt.Errorf("pattern item is empty but the pattern is required")
|
||||
}
|
||||
p, ok := pattern.Item().(item.BannerPattern)
|
||||
if !ok {
|
||||
return fmt.Errorf("pattern item is not a banner pattern")
|
||||
}
|
||||
if expectedPatternItem != p.Type {
|
||||
return fmt.Errorf("pattern item does not match the expected pattern")
|
||||
}
|
||||
}
|
||||
|
||||
// Add a new pattern layer onto the banner, and create the result.
|
||||
b.Patterns = append(b.Patterns, block.BannerPatternLayer{
|
||||
Type: expectedPattern,
|
||||
Colour: d.Colour,
|
||||
})
|
||||
h.setItemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerLoomInput},
|
||||
Slot: loomInputSlot,
|
||||
}, input.Grow(-timesCrafted), s, tx)
|
||||
h.setItemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerLoomDye},
|
||||
Slot: loomDyeSlot,
|
||||
}, dye.Grow(-timesCrafted), s, tx)
|
||||
return h.createResults(s, tx, input.WithItem(b))
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
)
|
||||
|
||||
// MobEquipmentHandler handles the MobEquipment packet.
|
||||
type MobEquipmentHandler struct{}
|
||||
|
||||
// Handle ...
|
||||
func (*MobEquipmentHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error {
|
||||
pk := p.(*packet.MobEquipment)
|
||||
|
||||
if pk.EntityRuntimeID != selfEntityRuntimeID {
|
||||
return errSelfRuntimeID
|
||||
}
|
||||
switch pk.WindowID {
|
||||
case protocol.WindowIDOffHand:
|
||||
// This window ID is expected, but we don't handle it.
|
||||
return nil
|
||||
case protocol.WindowIDInventory:
|
||||
return s.VerifyAndSetHeldSlot(int(pk.InventorySlot), stackToItem(s.br, pk.NewItem.Stack), c)
|
||||
default:
|
||||
return fmt.Errorf("only main inventory should be involved in slot change, got window ID %v", pk.WindowID)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/df-mc/dragonfly/server/player/form"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// ModalFormResponseHandler handles the ModalFormResponse packet.
|
||||
type ModalFormResponseHandler struct {
|
||||
mu sync.Mutex
|
||||
forms map[uint32]form.Form
|
||||
currentID atomic.Uint32
|
||||
}
|
||||
|
||||
// Handle ...
|
||||
func (h *ModalFormResponseHandler) Handle(p packet.Packet, _ *Session, tx *world.Tx, c Controllable) error {
|
||||
pk := p.(*packet.ModalFormResponse)
|
||||
|
||||
h.mu.Lock()
|
||||
f, ok := h.forms[pk.FormID]
|
||||
delete(h.forms, pk.FormID)
|
||||
h.mu.Unlock()
|
||||
|
||||
resp, exists := pk.ResponseData.Value()
|
||||
if !ok && !exists {
|
||||
// Sometimes the client seems to send a second response with no data, which would cause the player to be kicked
|
||||
// by the server. This should patch that.
|
||||
return nil
|
||||
}
|
||||
if !exists || len(resp) == 0 {
|
||||
// The form was cancelled: The cross in the top right corner was clicked.
|
||||
resp = nil
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("no form with ID %v currently opened", pk.FormID)
|
||||
}
|
||||
if err := f.SubmitJSON(resp, c, tx); err != nil {
|
||||
return fmt.Errorf("error submitting form data: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/df-mc/dragonfly/server/player/dialogue"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
)
|
||||
|
||||
// NPCRequestHandler handles the NPCRequest packet.
|
||||
type NPCRequestHandler struct {
|
||||
dialogue dialogue.Dialogue
|
||||
entityRuntimeID uint64
|
||||
}
|
||||
|
||||
// Handle ...
|
||||
func (h *NPCRequestHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error {
|
||||
pk := p.(*packet.NPCRequest)
|
||||
if h.entityRuntimeID == 0 {
|
||||
// No dialogue is currently open for this session, so there is nothing to submit or close.
|
||||
return nil
|
||||
}
|
||||
switch pk.RequestType {
|
||||
case packet.NPCRequestActionExecuteAction:
|
||||
if err := h.dialogue.Submit(uint(pk.ActionType), c, tx); err != nil {
|
||||
return fmt.Errorf("error submitting dialogue: %w", err)
|
||||
}
|
||||
case packet.NPCRequestActionExecuteClosingCommands:
|
||||
h.dialogue.Close(c, tx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
)
|
||||
|
||||
// PlayerActionHandler handles the PlayerAction packet.
|
||||
type PlayerActionHandler struct{}
|
||||
|
||||
// Handle ...
|
||||
func (*PlayerActionHandler) Handle(p packet.Packet, s *Session, _ *world.Tx, c Controllable) error {
|
||||
pk := p.(*packet.PlayerAction)
|
||||
|
||||
return handlePlayerAction(pk.ActionType, pk.BlockFace, pk.BlockPosition, pk.EntityRuntimeID, s, c)
|
||||
}
|
||||
|
||||
// handlePlayerAction handles an action performed by a player, found in packet.PlayerAction and packet.PlayerAuthInput.
|
||||
func handlePlayerAction(action int32, face int32, pos protocol.BlockPos, entityRuntimeID uint64, s *Session, c Controllable) error {
|
||||
if entityRuntimeID != selfEntityRuntimeID {
|
||||
return errSelfRuntimeID
|
||||
}
|
||||
switch action {
|
||||
case protocol.PlayerActionStartSleeping, protocol.PlayerActionRespawn, protocol.PlayerActionDimensionChangeDone:
|
||||
// Don't do anything for these actions.
|
||||
case protocol.PlayerActionStopSleeping:
|
||||
c.Wake()
|
||||
case protocol.PlayerActionStartBreak, protocol.PlayerActionContinueDestroyBlock:
|
||||
s.swingingArm.Store(true)
|
||||
defer s.swingingArm.Store(false)
|
||||
|
||||
s.breakingPos = cube.Pos{int(pos[0]), int(pos[1]), int(pos[2])}
|
||||
c.StartBreaking(s.breakingPos, cube.Face(face))
|
||||
case protocol.PlayerActionAbortBreak:
|
||||
c.AbortBreaking()
|
||||
case protocol.PlayerActionPredictDestroyBlock, protocol.PlayerActionStopBreak:
|
||||
s.swingingArm.Store(true)
|
||||
defer s.swingingArm.Store(false)
|
||||
c.FinishBreaking()
|
||||
case protocol.PlayerActionCrackBreak:
|
||||
// Don't do anything for this action. It is no longer used. Block
|
||||
// cracking is done fully server-side.
|
||||
case protocol.PlayerActionStartItemUseOn:
|
||||
// TODO: Properly utilize these actions.
|
||||
case protocol.PlayerActionStopItemUseOn:
|
||||
c.ReleaseItem()
|
||||
case protocol.PlayerActionStartBuildingBlock:
|
||||
// Don't do anything for this action.
|
||||
case protocol.PlayerActionCreativePlayerDestroyBlock:
|
||||
// Don't do anything for this action.
|
||||
case protocol.PlayerActionMissedSwing:
|
||||
s.swingingArm.Store(true)
|
||||
defer s.swingingArm.Store(false)
|
||||
c.PunchAir()
|
||||
default:
|
||||
return fmt.Errorf("unhandled ActionType %v", action)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/go-gl/mathgl/mgl32"
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
)
|
||||
|
||||
// PlayerAuthInputHandler handles the PlayerAuthInput packet.
|
||||
type PlayerAuthInputHandler struct{}
|
||||
|
||||
// Handle ...
|
||||
func (h PlayerAuthInputHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, c Controllable) error {
|
||||
pk := p.(*packet.PlayerAuthInput)
|
||||
if err := h.handleMovement(pk, s, c); err != nil {
|
||||
return err
|
||||
}
|
||||
return h.handleActions(pk, s, tx, c)
|
||||
}
|
||||
|
||||
// handleMovement handles the movement part of the packet.PlayerAuthInput.
|
||||
func (h PlayerAuthInputHandler) handleMovement(pk *packet.PlayerAuthInput, s *Session, c Controllable) error {
|
||||
yaw, pitch := c.Rotation().Elem()
|
||||
pos := c.Position()
|
||||
|
||||
reference := []float64{pitch, yaw, yaw, pos[0], pos[1], pos[2]}
|
||||
for i, v := range [...]*float32{&pk.Pitch, &pk.Yaw, &pk.HeadYaw, &pk.Position[0], &pk.Position[1], &pk.Position[2]} {
|
||||
f := float64(*v)
|
||||
if math.IsNaN(f) || math.IsInf(f, 1) || math.IsInf(f, 0) {
|
||||
// Sometimes, the PlayerAuthInput packet is in fact sent with NaN/INF after being teleported (to another
|
||||
// world), see #425. For this reason, we don't actually return an error if this happens, because this will
|
||||
// result in the player being kicked. Just log it and replace the NaN value with the one we have tracked
|
||||
// server-side.
|
||||
s.conf.Log.Debug("process packet: PlayerAuthInput: found nan/inf values. assuming server-side values", "pos", fmt.Sprint(pk.Position), "yaw", pk.Yaw, "head-yaw", pk.HeadYaw, "pitch", pk.Pitch)
|
||||
*v = float32(reference[i])
|
||||
}
|
||||
}
|
||||
|
||||
pk.Position = pk.Position.Sub(mgl32.Vec3{0, 1.62}) // Sub the base offset of players from the pos.
|
||||
|
||||
newPos := vec32To64(pk.Position)
|
||||
deltaPos, deltaYaw, deltaPitch := newPos.Sub(pos), float64(pk.Yaw)-yaw, float64(pk.Pitch)-pitch
|
||||
if mgl64.FloatEqual(deltaPos.Len(), 0) && mgl64.FloatEqual(deltaYaw, 0) && mgl64.FloatEqual(deltaPitch, 0) {
|
||||
// The PlayerAuthInput packet is sent every tick, so don't do anything if the position and rotation
|
||||
// were unchanged.
|
||||
return nil
|
||||
}
|
||||
|
||||
if expected := s.teleportPos.Load(); expected != nil {
|
||||
if newPos.Sub(*expected).Len() > 1 {
|
||||
// The player has moved before it received the teleport packet. Ignore this movement entirely and
|
||||
// wait for the client to sync itself back to the server. Once we get a movement that is close
|
||||
// enough to the teleport position, we'll allow the player to move around again.
|
||||
return nil
|
||||
}
|
||||
s.teleportPos.Store(nil)
|
||||
}
|
||||
|
||||
s.moving = true
|
||||
c.Move(deltaPos, deltaYaw, deltaPitch)
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleActions handles the actions with the world that are present in the PlayerAuthInput packet.
|
||||
func (h PlayerAuthInputHandler) handleActions(pk *packet.PlayerAuthInput, s *Session, tx *world.Tx, c Controllable) error {
|
||||
if pk.InputData.Load(packet.InputFlagPerformItemInteraction) {
|
||||
if err := h.handleUseItemData(pk.ItemInteractionData, s, c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if pk.InputData.Load(packet.InputFlagPerformBlockActions) {
|
||||
if err := h.handleBlockActions(pk.BlockActions, s, c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
h.handleInputFlags(pk.InputData, s, c)
|
||||
|
||||
if pk.InputData.Load(packet.InputFlagPerformItemStackRequest) {
|
||||
s.inTransaction.Store(true)
|
||||
defer s.inTransaction.Store(false)
|
||||
|
||||
// As of 1.18 this is now used for sending item stack requests such as when mining a block.
|
||||
sh := s.handlers[packet.IDItemStackRequest].(*ItemStackRequestHandler)
|
||||
if err := sh.handleRequest(pk.ItemStackRequest, s, tx, c); err != nil {
|
||||
// Item stacks being out of sync isn't uncommon, so don't error. Just debug the error and let the
|
||||
// revert do its work.
|
||||
s.conf.Log.Debug("process packet: PlayerAuthInput: resolve item stack request: " + err.Error())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleInputFlags handles the toggleable input flags set in a PlayerAuthInput packet.
|
||||
func (h PlayerAuthInputHandler) handleInputFlags(flags protocol.Bitset, s *Session, c Controllable) {
|
||||
if flags.Load(packet.InputFlagStartSprinting) {
|
||||
c.StartSprinting()
|
||||
}
|
||||
if flags.Load(packet.InputFlagStopSprinting) {
|
||||
c.StopSprinting()
|
||||
}
|
||||
if flags.Load(packet.InputFlagStartSneaking) {
|
||||
c.StartSneaking()
|
||||
}
|
||||
if flags.Load(packet.InputFlagStopSneaking) {
|
||||
c.StopSneaking()
|
||||
}
|
||||
if flags.Load(packet.InputFlagStartSwimming) {
|
||||
c.StartSwimming()
|
||||
}
|
||||
if flags.Load(packet.InputFlagStopSwimming) {
|
||||
c.StopSwimming()
|
||||
}
|
||||
if flags.Load(packet.InputFlagStartGliding) {
|
||||
c.StartGliding()
|
||||
}
|
||||
if flags.Load(packet.InputFlagStopGliding) {
|
||||
c.StopGliding()
|
||||
}
|
||||
if flags.Load(packet.InputFlagStartJumping) {
|
||||
c.Jump()
|
||||
}
|
||||
if flags.Load(packet.InputFlagStartCrawling) {
|
||||
c.StartCrawling()
|
||||
}
|
||||
if flags.Load(packet.InputFlagStopCrawling) {
|
||||
c.StopCrawling()
|
||||
}
|
||||
if flags.Load(packet.InputFlagMissedSwing) {
|
||||
s.swingingArm.Store(true)
|
||||
defer s.swingingArm.Store(false)
|
||||
c.PunchAir()
|
||||
}
|
||||
if flags.Load(packet.InputFlagStartFlying) {
|
||||
if !c.GameMode().AllowsFlying() {
|
||||
s.conf.Log.Debug("process packet: PlayerAuthInput: flying flag enabled while unable to fly")
|
||||
s.SendAbilities(c)
|
||||
} else {
|
||||
c.StartFlying()
|
||||
}
|
||||
}
|
||||
if flags.Load(packet.InputFlagStopFlying) {
|
||||
c.StopFlying()
|
||||
}
|
||||
}
|
||||
|
||||
// handleUseItemData handles the protocol.UseItemTransactionData found in a packet.PlayerAuthInput.
|
||||
func (h PlayerAuthInputHandler) handleUseItemData(data protocol.UseItemTransactionData, s *Session, c Controllable) error {
|
||||
s.swingingArm.Store(true)
|
||||
defer s.swingingArm.Store(false)
|
||||
|
||||
held, _ := c.HeldItems()
|
||||
if !held.Equal(stackToItem(s.br, data.HeldItem.Stack)) {
|
||||
s.conf.Log.Debug("process packet: PlayerAuthInput: UseItemTransaction: mismatch between actual held item and client held item")
|
||||
return nil
|
||||
}
|
||||
pos := cube.Pos{int(data.BlockPosition[0]), int(data.BlockPosition[1]), int(data.BlockPosition[2])}
|
||||
|
||||
// Seems like this is only used for breaking blocks at the moment.
|
||||
switch data.ActionType {
|
||||
case protocol.UseItemActionBreakBlock:
|
||||
c.BreakBlock(pos)
|
||||
default:
|
||||
return fmt.Errorf("unhandled UseItem ActionType for PlayerAuthInput packet %v", data.ActionType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleBlockActions handles a slice of protocol.PlayerBlockAction present in a PlayerAuthInput packet.
|
||||
func (h PlayerAuthInputHandler) handleBlockActions(a []protocol.PlayerBlockAction, s *Session, c Controllable) error {
|
||||
for _, action := range a {
|
||||
if err := handlePlayerAction(action.Action, action.Face, action.BlockPos, selfEntityRuntimeID, s, c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
)
|
||||
|
||||
// PlayerSkinHandler handles the PlayerSkin packet.
|
||||
type PlayerSkinHandler struct{}
|
||||
|
||||
// Handle ...
|
||||
func (PlayerSkinHandler) Handle(p packet.Packet, _ *Session, _ *world.Tx, c Controllable) error {
|
||||
pk := p.(*packet.PlayerSkin)
|
||||
|
||||
playerSkin, err := protocolToSkin(pk.Skin)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error decoding skin: %w", err)
|
||||
}
|
||||
|
||||
c.SetSkin(playerSkin)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
)
|
||||
|
||||
// RequestAbilityHandler handles the RequestAbility packet.
|
||||
type RequestAbilityHandler struct{}
|
||||
|
||||
// Handle ...
|
||||
func (a RequestAbilityHandler) Handle(p packet.Packet, s *Session, _ *world.Tx, c Controllable) error {
|
||||
pk := p.(*packet.RequestAbility)
|
||||
if pk.Ability == packet.AbilityFlying {
|
||||
if !c.GameMode().AllowsFlying() {
|
||||
s.conf.Log.Debug("process packet: RequestAbility: flying flag enabled while unable to fly")
|
||||
s.SendAbilities(c)
|
||||
return nil
|
||||
}
|
||||
c.StartFlying()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
)
|
||||
|
||||
// RequestChunkRadiusHandler handles the RequestChunkRadius packet.
|
||||
type RequestChunkRadiusHandler struct{}
|
||||
|
||||
// Handle ...
|
||||
func (*RequestChunkRadiusHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, _ Controllable) error {
|
||||
pk := p.(*packet.RequestChunkRadius)
|
||||
|
||||
if pk.ChunkRadius > s.maxChunkRadius {
|
||||
pk.ChunkRadius = s.maxChunkRadius
|
||||
}
|
||||
s.chunkRadius = pk.ChunkRadius
|
||||
|
||||
s.chunkLoader.ChangeRadius(tx, int(pk.ChunkRadius))
|
||||
|
||||
s.writePacket(&packet.ChunkRadiusUpdated{ChunkRadius: s.chunkRadius})
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
)
|
||||
|
||||
// RespawnHandler handles the Respawn packet.
|
||||
type RespawnHandler struct{}
|
||||
|
||||
// Handle ...
|
||||
func (*RespawnHandler) Handle(p packet.Packet, _ *Session, _ *world.Tx, c Controllable) error {
|
||||
pk := p.(*packet.Respawn)
|
||||
if pk.EntityRuntimeID != selfEntityRuntimeID {
|
||||
return errSelfRuntimeID
|
||||
}
|
||||
if pk.State != packet.RespawnStateClientReadyToSpawn {
|
||||
return fmt.Errorf("respawn state must always be %v, but got %v", packet.RespawnStateClientReadyToSpawn, pk.State)
|
||||
}
|
||||
c.Respawn()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
)
|
||||
|
||||
// ServerBoundDiagnosticsHandler handles diagnostic updates from the client.
|
||||
type ServerBoundDiagnosticsHandler struct{}
|
||||
|
||||
// Handle ...
|
||||
func (h *ServerBoundDiagnosticsHandler) Handle(p packet.Packet, _ *Session, _ *world.Tx, c Controllable) error {
|
||||
pk := p.(*packet.ServerBoundDiagnostics)
|
||||
c.UpdateDiagnostics(Diagnostics{
|
||||
AverageFramesPerSecond: float64(pk.AverageFramesPerSecond),
|
||||
AverageServerSimTickTime: float64(pk.AverageServerSimTickTime),
|
||||
AverageClientSimTickTime: float64(pk.AverageClientSimTickTime),
|
||||
AverageBeginFrameTime: float64(pk.AverageBeginFrameTime),
|
||||
AverageInputTime: float64(pk.AverageInputTime),
|
||||
AverageRenderTime: float64(pk.AverageRenderTime),
|
||||
AverageEndFrameTime: float64(pk.AverageEndFrameTime),
|
||||
AverageRemainderTimePercent: float64(pk.AverageRemainderTimePercent),
|
||||
AverageUnaccountedTimePercent: float64(pk.AverageUnaccountedTimePercent),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// ServerBoundLoadingScreenHandler handles loading screen updates from the clients. It is used to ensure that
|
||||
// the server knows when the client is loading a screen, and when it is done loading it.
|
||||
type ServerBoundLoadingScreenHandler struct {
|
||||
currentID atomic.Uint32
|
||||
expectedID atomic.Uint32
|
||||
}
|
||||
|
||||
// Handle ...
|
||||
func (h *ServerBoundLoadingScreenHandler) Handle(p packet.Packet, s *Session, _ *world.Tx, _ Controllable) error {
|
||||
pk := p.(*packet.ServerBoundLoadingScreen)
|
||||
v, ok := pk.LoadingScreenID.Value()
|
||||
expected := h.expectedID.Load()
|
||||
|
||||
switch {
|
||||
case !ok || expected == 0:
|
||||
return nil
|
||||
case v != expected:
|
||||
return fmt.Errorf("expected loading screen ID %d, got %d", expected, v)
|
||||
case pk.Type == packet.LoadingScreenTypeEnd:
|
||||
s.changingDimension.Store(false)
|
||||
h.expectedID.Store(0)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/df-mc/dragonfly/server/item"
|
||||
"github.com/df-mc/dragonfly/server/item/recipe"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol"
|
||||
)
|
||||
|
||||
const (
|
||||
// smithingInputSlot is the slot index of the input item in the smithing table.
|
||||
smithingInputSlot = 0x33
|
||||
// smithingMaterialSlot is the slot index of the material in the smithing table.
|
||||
smithingMaterialSlot = 0x34
|
||||
// smithingTemplateSlot is the slot index of the template item in the smithing table.
|
||||
smithingTemplateSlot = 0x35
|
||||
)
|
||||
|
||||
// handleSmithing handles a CraftRecipe stack request action made using a smithing table.
|
||||
func (h *ItemStackRequestHandler) handleSmithing(a *protocol.CraftRecipeStackRequestAction, s *Session, tx *world.Tx) error {
|
||||
// First, check the recipe and ensure it is valid for the smithing table.
|
||||
craft, ok := s.recipes[a.RecipeNetworkID]
|
||||
if !ok {
|
||||
return fmt.Errorf("recipe with network id %v does not exist", a.RecipeNetworkID)
|
||||
}
|
||||
if craft.Block() != "smithing_table" {
|
||||
return fmt.Errorf("recipe with network id %v is not a smithing table recipe", a.RecipeNetworkID)
|
||||
}
|
||||
switch craft.(type) {
|
||||
case recipe.SmithingTransform, recipe.SmithingTrim:
|
||||
default:
|
||||
return fmt.Errorf("recipe with network id %v is not a smithing recipe", a.RecipeNetworkID)
|
||||
}
|
||||
|
||||
// Check if the input item and material item match what the recipe requires.
|
||||
expectedInputs := craft.Input()
|
||||
input, _ := h.itemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerSmithingTableInput},
|
||||
Slot: smithingInputSlot,
|
||||
}, s, tx)
|
||||
if !matchingStacks(input, expectedInputs[0]) {
|
||||
return fmt.Errorf("input item is not the same as expected input")
|
||||
}
|
||||
material, _ := h.itemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerSmithingTableMaterial},
|
||||
Slot: smithingMaterialSlot,
|
||||
}, s, tx)
|
||||
if !matchingStacks(material, expectedInputs[1]) {
|
||||
return fmt.Errorf("material item is not the same as expected material")
|
||||
}
|
||||
template, _ := h.itemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerSmithingTableTemplate},
|
||||
Slot: smithingTemplateSlot,
|
||||
}, s, tx)
|
||||
if !matchingStacks(template, expectedInputs[2]) {
|
||||
return fmt.Errorf("template item is not the same as expected template")
|
||||
}
|
||||
|
||||
// Create the output using the input stack as reference and the recipe's output item type.
|
||||
h.setItemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerSmithingTableInput},
|
||||
Slot: smithingInputSlot,
|
||||
}, input.Grow(-1), s, tx)
|
||||
h.setItemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerSmithingTableMaterial},
|
||||
Slot: smithingMaterialSlot,
|
||||
}, material.Grow(-1), s, tx)
|
||||
h.setItemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerSmithingTableTemplate},
|
||||
Slot: smithingTemplateSlot,
|
||||
}, template.Grow(-1), s, tx)
|
||||
|
||||
if _, ok = craft.(recipe.SmithingTrim); ok {
|
||||
var trim item.ArmourTrim
|
||||
if t, ok := template.Item().(item.SmithingTemplate); ok {
|
||||
trim.Template = t.Template
|
||||
} else {
|
||||
return fmt.Errorf("template item is not a smithing template")
|
||||
}
|
||||
if trim.Material, ok = material.Item().(item.ArmourTrimMaterial); !ok {
|
||||
return fmt.Errorf("material item is not an armour trim material")
|
||||
}
|
||||
trimmable, ok := input.Item().(item.Trimmable)
|
||||
if !ok {
|
||||
return fmt.Errorf("input item is not trimmable")
|
||||
}
|
||||
return h.createResults(s, tx, input.WithItem(trimmable.WithTrim(trim)))
|
||||
}
|
||||
return h.createResults(s, tx, input.WithItem(craft.Output()[0].Item()))
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/df-mc/dragonfly/server/item/recipe"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol"
|
||||
)
|
||||
|
||||
// stonecutterInputSlot is the slot index of the input item in the stonecutter.
|
||||
const stonecutterInputSlot = 0x03
|
||||
|
||||
// handleStonecutting handles a CraftRecipe stack request action made using a stonecutter.
|
||||
func (h *ItemStackRequestHandler) handleStonecutting(a *protocol.CraftRecipeStackRequestAction, s *Session, tx *world.Tx) error {
|
||||
craft, ok := s.recipes[a.RecipeNetworkID]
|
||||
if !ok {
|
||||
return fmt.Errorf("recipe with network id %v does not exist", a.RecipeNetworkID)
|
||||
}
|
||||
if _, shapeless := craft.(recipe.Shapeless); !shapeless {
|
||||
return fmt.Errorf("recipe with network id %v is not a shapeless recipe", a.RecipeNetworkID)
|
||||
}
|
||||
if craft.Block() != "stonecutter" {
|
||||
return fmt.Errorf("recipe with network id %v is not a stonecutter recipe", a.RecipeNetworkID)
|
||||
}
|
||||
|
||||
timesCrafted := int(a.NumberOfCrafts)
|
||||
if timesCrafted < 1 {
|
||||
return fmt.Errorf("times crafted must be at least 1")
|
||||
}
|
||||
|
||||
expectedInputs := craft.Input()
|
||||
input, _ := h.itemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerStonecutterInput},
|
||||
Slot: stonecutterInputSlot,
|
||||
}, s, tx)
|
||||
if input.Count() < timesCrafted {
|
||||
return fmt.Errorf("input item count is less than number of crafts")
|
||||
}
|
||||
if !matchingStacks(input, expectedInputs[0]) {
|
||||
return fmt.Errorf("input item is not the same as expected input")
|
||||
}
|
||||
|
||||
output := craft.Output()
|
||||
h.setItemInSlot(protocol.StackRequestSlotInfo{
|
||||
Container: protocol.FullContainerName{ContainerID: protocol.ContainerStonecutterInput},
|
||||
Slot: stonecutterInputSlot,
|
||||
}, input.Grow(-timesCrafted), s, tx)
|
||||
return h.createResults(s, tx, repeatStacks(output, timesCrafted)...)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
)
|
||||
|
||||
// SubChunkRequestHandler handles sub-chunk requests from the client. The server will respond with a packet containing
|
||||
// the requested sub-chunks.
|
||||
type SubChunkRequestHandler struct{}
|
||||
|
||||
// Handle ...
|
||||
func (*SubChunkRequestHandler) Handle(p packet.Packet, s *Session, tx *world.Tx, _ Controllable) error {
|
||||
pk := p.(*packet.SubChunkRequest)
|
||||
if dimID, _ := world.DimensionID(tx.World().Dimension()); pk.Dimension != int32(dimID) {
|
||||
// Outdated sub chunk request from a previous dimension.
|
||||
s.writePacket(&packet.SubChunk{
|
||||
Dimension: pk.Dimension,
|
||||
Position: pk.Position,
|
||||
CacheEnabled: s.conn.ClientCacheEnabled(),
|
||||
SubChunkEntries: []protocol.SubChunkEntry{},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
s.ViewSubChunks(world.SubChunkPos(pk.Position), pk.Offsets, tx)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
)
|
||||
|
||||
// TextHandler handles the Text packet.
|
||||
type TextHandler struct{}
|
||||
|
||||
// Handle ...
|
||||
func (TextHandler) Handle(p packet.Packet, s *Session, _ *world.Tx, c Controllable) error {
|
||||
pk := p.(*packet.Text)
|
||||
|
||||
if pk.TextType != packet.TextTypeChat {
|
||||
return fmt.Errorf("TextType should always be Chat (%v), but got %v", packet.TextTypeChat, pk.TextType)
|
||||
}
|
||||
if pk.SourceName != s.conn.IdentityData().DisplayName {
|
||||
return fmt.Errorf("SourceName must be equal to DisplayName")
|
||||
}
|
||||
if pk.XUID != s.conn.IdentityData().XUID {
|
||||
return fmt.Errorf("XUID must be equal to player's XUID")
|
||||
}
|
||||
c.Chat(pk.Message)
|
||||
return nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,598 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/df-mc/dragonfly/server/block/cube"
|
||||
"github.com/df-mc/dragonfly/server/cmd"
|
||||
"github.com/df-mc/dragonfly/server/item/inventory"
|
||||
"github.com/df-mc/dragonfly/server/item/recipe"
|
||||
"github.com/df-mc/dragonfly/server/player/chat"
|
||||
"github.com/df-mc/dragonfly/server/player/debug"
|
||||
"github.com/df-mc/dragonfly/server/player/form"
|
||||
"github.com/df-mc/dragonfly/server/player/hud"
|
||||
"github.com/df-mc/dragonfly/server/player/skin"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/go-gl/mathgl/mgl64"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sandertv/gophertunnel/minecraft"
|
||||
"github.com/sandertv/gophertunnel/minecraft/nbt"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/login"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
)
|
||||
|
||||
// Session handles incoming packets from connections and sends outgoing packets by providing a thin layer
|
||||
// of abstraction over direct packets. A Session basically 'controls' an entity.
|
||||
type Session struct {
|
||||
conf Config
|
||||
once, connOnce sync.Once
|
||||
|
||||
ent *world.EntityHandle
|
||||
conn Conn
|
||||
handlers map[uint32]packetHandler
|
||||
packets chan packet.Packet
|
||||
|
||||
currentScoreboard atomic.Pointer[string]
|
||||
currentLines atomic.Pointer[[]string]
|
||||
|
||||
chunkLoader *world.Loader
|
||||
chunkRadius, maxChunkRadius int32
|
||||
|
||||
emoteChatMuted bool
|
||||
|
||||
teleportPos atomic.Pointer[mgl64.Vec3]
|
||||
|
||||
entityMutex sync.RWMutex
|
||||
// currentEntityRuntimeID holds the runtime ID assigned to the last entity. It is incremented for every
|
||||
// entity spawned to the session.
|
||||
currentEntityRuntimeID uint64
|
||||
// entityRuntimeIDs holds the runtime IDs of entities shown to the session.
|
||||
entityRuntimeIDs map[*world.EntityHandle]uint64
|
||||
entities map[uint64]*world.EntityHandle
|
||||
hiddenEntities map[uuid.UUID]struct{}
|
||||
|
||||
// heldSlot is the slot in the inventory that the controllable is holding.
|
||||
heldSlot *uint32
|
||||
inv, offHand, enderChest, ui *inventory.Inventory
|
||||
armour *inventory.Armour
|
||||
|
||||
// joinSkin is the first skin that the player joined with. It is sent on
|
||||
// spawn for the player list, but otherwise updated immediately when the
|
||||
// player is viewed.
|
||||
joinSkin skin.Skin
|
||||
|
||||
breakingPos cube.Pos
|
||||
|
||||
inTransaction, containerOpened atomic.Bool
|
||||
openedWindowID atomic.Uint32
|
||||
openedContainerID atomic.Uint32
|
||||
openedWindow atomic.Pointer[inventory.Inventory]
|
||||
openedPos atomic.Pointer[cube.Pos]
|
||||
swingingArm atomic.Bool
|
||||
changingSlot atomic.Bool
|
||||
changingDimension atomic.Bool
|
||||
moving bool
|
||||
|
||||
lastChunkPos world.ChunkPos
|
||||
|
||||
recipes map[uint32]recipe.Recipe
|
||||
|
||||
blobMu sync.Mutex
|
||||
blobs map[uint64][]byte
|
||||
openChunkTransactions []map[uint64]struct{}
|
||||
invOpened bool
|
||||
|
||||
hudMu sync.RWMutex
|
||||
hudUpdates map[hud.Element]bool
|
||||
hiddenHud map[hud.Element]struct{}
|
||||
|
||||
debugShapesMu sync.RWMutex
|
||||
debugShapes map[int]debug.Shape
|
||||
debugShapeUpdates []debugShapeUpdate
|
||||
|
||||
viewLayer *world.ViewLayer
|
||||
|
||||
closeBackground chan struct{}
|
||||
|
||||
br world.BlockRegistry
|
||||
}
|
||||
|
||||
// debugShapeUpdate represents a pending debug shape mutation. If shape is nil, the update removes the
|
||||
// debug shape with the matching ID. Updates are applied in order when the session sends debug shapes.
|
||||
type debugShapeUpdate struct {
|
||||
id int
|
||||
shape debug.Shape
|
||||
}
|
||||
|
||||
// Conn represents a connection that packets are read from and written to by a Session. In addition, it holds some
|
||||
// information on the identity of the Session.
|
||||
type Conn interface {
|
||||
io.Closer
|
||||
// IdentityData returns the login.IdentityData of a Conn. It contains the UUID, XUID and username of the connection.
|
||||
IdentityData() login.IdentityData
|
||||
// ClientData returns the login.ClientData of a Conn. This includes less sensitive data of the player like its skin,
|
||||
// language code and other non-essential information.
|
||||
ClientData() login.ClientData
|
||||
// ClientCacheEnabled specifies if the Conn has the client cache, used for caching chunks client-side, enabled or
|
||||
// not. Some platforms, like the Nintendo Switch, have this disabled at all times.
|
||||
ClientCacheEnabled() bool
|
||||
// ChunkRadius returns the chunk radius as requested by the client at the other end of the Conn.
|
||||
ChunkRadius() int
|
||||
// Latency returns the current latency measured over the Conn.
|
||||
Latency() time.Duration
|
||||
// Flush flushes the packets buffered by the Conn, sending all of them out immediately.
|
||||
Flush() error
|
||||
// RemoteAddr returns the remote network address.
|
||||
RemoteAddr() net.Addr
|
||||
// ReadPacket reads a packet.Packet from the Conn. An error is returned if a deadline was set that was
|
||||
// exceeded or if the Conn was closed while awaiting a packet.
|
||||
ReadPacket() (pk packet.Packet, err error)
|
||||
// WritePacket writes a packet.Packet to the Conn. An error is returned if the Conn was closed before sending the
|
||||
// packet.
|
||||
WritePacket(pk packet.Packet) error
|
||||
// StartGameContext starts the game for the Conn with a context to cancel it.
|
||||
StartGameContext(ctx context.Context, data minecraft.GameData) error
|
||||
}
|
||||
|
||||
// Nop represents a no-operation session. It does not do anything when sending a packet to it.
|
||||
var Nop = &Session{conf: Config{Log: slog.New(slog.DiscardHandler)}}
|
||||
|
||||
// selfEntityRuntimeID is the entity runtime (or unique) ID of the controllable that the session holds.
|
||||
const selfEntityRuntimeID = 1
|
||||
|
||||
// errSelfRuntimeID is an error returned during packet handling for fields that refer to the player itself and
|
||||
// must therefore always be 1.
|
||||
var errSelfRuntimeID = errors.New("invalid entity runtime ID: runtime ID for self must always be 1")
|
||||
|
||||
type Config struct {
|
||||
Log *slog.Logger
|
||||
|
||||
MaxChunkRadius int
|
||||
|
||||
EmoteChatMuted bool
|
||||
|
||||
JoinMessage, QuitMessage chat.Translation
|
||||
|
||||
HandleStop func(*world.Tx, Controllable)
|
||||
// BlockRegistry overrides the registry used for network serialization. If nil, world.DefaultBlockRegistry is used.
|
||||
BlockRegistry world.BlockRegistry
|
||||
}
|
||||
|
||||
func (conf Config) New(conn Conn) *Session {
|
||||
r := conn.ChunkRadius()
|
||||
if r > conf.MaxChunkRadius {
|
||||
r = conf.MaxChunkRadius
|
||||
_ = conn.WritePacket(&packet.ChunkRadiusUpdated{ChunkRadius: int32(r)})
|
||||
}
|
||||
if conf.Log == nil {
|
||||
conf.Log = slog.Default()
|
||||
}
|
||||
conf.Log = conf.Log.With("name", conn.IdentityData().DisplayName, "uuid", conn.IdentityData().Identity, "raddr", conn.RemoteAddr().String())
|
||||
|
||||
s := &Session{}
|
||||
*s = Session{
|
||||
openChunkTransactions: make([]map[uint64]struct{}, 0, 8),
|
||||
closeBackground: make(chan struct{}),
|
||||
handlers: map[uint32]packetHandler{},
|
||||
packets: make(chan packet.Packet, 256),
|
||||
entityRuntimeIDs: map[*world.EntityHandle]uint64{},
|
||||
entities: map[uint64]*world.EntityHandle{},
|
||||
hiddenEntities: map[uuid.UUID]struct{}{},
|
||||
blobs: map[uint64][]byte{},
|
||||
chunkRadius: int32(r),
|
||||
maxChunkRadius: int32(conf.MaxChunkRadius),
|
||||
emoteChatMuted: conf.EmoteChatMuted,
|
||||
conn: conn,
|
||||
currentEntityRuntimeID: 1,
|
||||
heldSlot: new(uint32),
|
||||
recipes: make(map[uint32]recipe.Recipe),
|
||||
conf: conf,
|
||||
hudUpdates: make(map[hud.Element]bool),
|
||||
hiddenHud: make(map[hud.Element]struct{}),
|
||||
debugShapes: make(map[int]debug.Shape),
|
||||
debugShapeUpdates: make([]debugShapeUpdate, 0, 256),
|
||||
}
|
||||
s.viewLayer = world.NewViewLayer(s)
|
||||
s.openedWindow.Store(inventory.New(1, nil))
|
||||
s.openedPos.Store(&cube.Pos{})
|
||||
|
||||
var scoreboardName string
|
||||
var scoreboardLines []string
|
||||
s.currentScoreboard.Store(&scoreboardName)
|
||||
s.currentLines.Store(&scoreboardLines)
|
||||
|
||||
if conf.BlockRegistry == nil {
|
||||
s.br = world.DefaultBlockRegistry
|
||||
} else {
|
||||
s.br = conf.BlockRegistry
|
||||
}
|
||||
|
||||
s.registerHandlers()
|
||||
s.sendBiomes()
|
||||
groups, items := creativeContent(s.br)
|
||||
s.writePacket(&packet.CreativeContent{Groups: groups, Items: items})
|
||||
s.sendRecipes()
|
||||
s.sendArmourTrimData()
|
||||
s.SendSpeed(0.1)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-s.closeBackground:
|
||||
return
|
||||
case pk := <-s.packets:
|
||||
_ = conn.WritePacket(pk)
|
||||
}
|
||||
}
|
||||
}()
|
||||
return s
|
||||
}
|
||||
|
||||
// SetHandle sets the world.EntityHandle of the Session and attaches a skin to
|
||||
// other players on join.
|
||||
func (s *Session) SetHandle(handle *world.EntityHandle, skin skin.Skin) {
|
||||
s.ent = handle
|
||||
s.entityRuntimeIDs[handle] = selfEntityRuntimeID
|
||||
s.entities[selfEntityRuntimeID] = handle
|
||||
|
||||
s.joinSkin = skin
|
||||
sessions.Add(s)
|
||||
}
|
||||
|
||||
// Spawn makes the Controllable passed spawn in the world.World.
|
||||
// The function passed will be called when the session stops running.
|
||||
func (s *Session) Spawn(c Controllable, tx *world.Tx) {
|
||||
s.SendHealth(c.Health(), c.MaxHealth(), c.Absorption())
|
||||
s.SendExperience(c.ExperienceLevel(), c.ExperienceProgress())
|
||||
s.SendFood(c.Food(), 0, 0)
|
||||
|
||||
pos := c.Position()
|
||||
s.chunkLoader = world.NewLoader(int(s.chunkRadius), tx.World(), s)
|
||||
s.chunkLoader.Move(tx, pos)
|
||||
s.writePacket(&packet.NetworkChunkPublisherUpdate{
|
||||
Position: protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])},
|
||||
Radius: uint32(s.chunkRadius) << 4,
|
||||
})
|
||||
|
||||
s.sendAvailableEntities(tx.World())
|
||||
|
||||
c.SetGameMode(c.GameMode())
|
||||
for _, e := range c.Effects() {
|
||||
s.SendEffect(e)
|
||||
}
|
||||
s.ViewEntityState(c)
|
||||
|
||||
s.sendInv(s.inv, protocol.WindowIDInventory)
|
||||
s.sendInv(s.ui, protocol.WindowIDUI)
|
||||
s.sendInv(s.offHand, protocol.WindowIDOffHand)
|
||||
s.sendInv(s.armour.Inventory(), protocol.WindowIDArmour)
|
||||
|
||||
chat.Global.Subscribe(c)
|
||||
if !s.conf.JoinMessage.Zero() {
|
||||
chat.Global.Writet(s.conf.JoinMessage, s.conn.IdentityData().DisplayName)
|
||||
}
|
||||
|
||||
go s.background()
|
||||
go s.handlePackets()
|
||||
}
|
||||
|
||||
// Close closes the session, which in turn closes the controllable and the connection that the session
|
||||
// manages. Close ensures the method only runs code on the first call.
|
||||
func (s *Session) Close(tx *world.Tx, c Controllable) {
|
||||
s.once.Do(func() {
|
||||
s.close(tx, c)
|
||||
})
|
||||
}
|
||||
|
||||
// close closes the session, which in turn closes the controllable and the connection that the session
|
||||
// manages.
|
||||
func (s *Session) close(tx *world.Tx, c Controllable) {
|
||||
c.MoveItemsToInventory()
|
||||
s.closeCurrentContainer(tx, false)
|
||||
if s.viewLayer != nil {
|
||||
_ = s.viewLayer.Close()
|
||||
}
|
||||
|
||||
s.conf.HandleStop(tx, c)
|
||||
|
||||
// Clear the inventories so that they no longer hold references to the connection.
|
||||
_ = s.inv.Close()
|
||||
_ = s.offHand.Close()
|
||||
_ = s.armour.Close()
|
||||
|
||||
s.chunkLoader.Close(tx)
|
||||
|
||||
if !s.conf.QuitMessage.Zero() {
|
||||
chat.Global.Writet(s.conf.QuitMessage, s.conn.IdentityData().DisplayName)
|
||||
}
|
||||
chat.Global.Unsubscribe(c)
|
||||
|
||||
// Note: Be aware of where RemoveEntity is called. This must not be done too
|
||||
// early.
|
||||
tx.RemoveEntity(c)
|
||||
_ = s.ent.Close()
|
||||
|
||||
// This should always be called last due to the timing of the removal of
|
||||
// entity runtime IDs.
|
||||
sessions.Remove(s, c)
|
||||
s.entityMutex.Lock()
|
||||
clear(s.entityRuntimeIDs)
|
||||
clear(s.entities)
|
||||
s.entityMutex.Unlock()
|
||||
}
|
||||
|
||||
// CloseConnection closes the underlying connection of the session so that the session ends up being closed
|
||||
// eventually.
|
||||
func (s *Session) CloseConnection() {
|
||||
s.connOnce.Do(func() {
|
||||
_ = s.conn.Close()
|
||||
close(s.closeBackground)
|
||||
})
|
||||
}
|
||||
|
||||
// Addr returns the net.Addr of the client.
|
||||
func (s *Session) Addr() net.Addr {
|
||||
return s.conn.RemoteAddr()
|
||||
}
|
||||
|
||||
// Latency returns the latency of the connection.
|
||||
func (s *Session) Latency() time.Duration {
|
||||
return s.conn.Latency()
|
||||
}
|
||||
|
||||
// ClientData returns the login.ClientData of the underlying *minecraft.Conn.
|
||||
func (s *Session) ClientData() login.ClientData {
|
||||
return s.conn.ClientData()
|
||||
}
|
||||
|
||||
// handlePackets continuously handles incoming packets from the connection. It processes them accordingly.
|
||||
// Once the connection is closed, handlePackets will return.
|
||||
func (s *Session) handlePackets() {
|
||||
defer func() {
|
||||
// First close the Controllable. This might lead to a world change
|
||||
// (player might be dead while disconnecting, in which case it will
|
||||
// respawn first).
|
||||
s.ent.ExecWorld(func(tx *world.Tx, e world.Entity) {
|
||||
_ = e.(Controllable).Close()
|
||||
})
|
||||
// Because the player might no longer be in the same world after
|
||||
// closing, we create a new transaction
|
||||
s.ent.ExecWorld(func(tx *world.Tx, e world.Entity) {
|
||||
s.Close(tx, e.(Controllable))
|
||||
})
|
||||
}()
|
||||
for {
|
||||
pk, err := s.conn.ReadPacket()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s.ent.ExecWorld(func(tx *world.Tx, e world.Entity) {
|
||||
err = s.handlePacket(pk, tx, e.(Controllable))
|
||||
})
|
||||
if err != nil {
|
||||
s.conf.Log.Debug("process packet: " + err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// background performs background tasks of the Session. This includes chunk sending and automatic command updating.
|
||||
// background returns when the Session's connection is closed using CloseConnection.
|
||||
func (s *Session) background() {
|
||||
var (
|
||||
r map[string]map[int]cmd.Runnable
|
||||
enums map[string]cmd.Enum
|
||||
enumValues map[string][]string
|
||||
softEnums = make(map[string]struct{})
|
||||
ok bool
|
||||
i int
|
||||
)
|
||||
|
||||
s.ent.ExecWorld(func(tx *world.Tx, e world.Entity) {
|
||||
co := e.(Controllable)
|
||||
r = s.sendAvailableCommands(co, softEnums)
|
||||
enums, enumValues = s.enums(co)
|
||||
})
|
||||
|
||||
t := time.NewTicker(time.Second / 20)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-t.C:
|
||||
s.ent.ExecWorld(func(tx *world.Tx, e world.Entity) {
|
||||
c := e.(Controllable)
|
||||
|
||||
if i++; i%20 == 0 {
|
||||
// Enum resending happens relatively often and frequent updates are more important than with full
|
||||
// command changes. Those are generally only related to permission changes, which doesn't happen often.
|
||||
r = s.resendEnums(enums, enumValues, softEnums, r, c)
|
||||
}
|
||||
if i%100 == 0 {
|
||||
// Try to resend commands only every 5 seconds.
|
||||
if r, ok = s.resendCommands(r, c, softEnums); ok {
|
||||
enums, enumValues = s.enums(c)
|
||||
}
|
||||
}
|
||||
s.sendChunks(tx, c)
|
||||
})
|
||||
case <-s.closeBackground:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sendChunks sends the next up to 4 chunks to the connection. What chunks are loaded depends on the connection of
|
||||
// the chunk loader and the chunks that were previously loaded.
|
||||
func (s *Session) sendChunks(tx *world.Tx, c Controllable) {
|
||||
var worldSwitched bool
|
||||
if w := tx.World(); s.chunkLoader.World() != w && w != nil {
|
||||
worldSwitched = true
|
||||
s.handleWorldSwitch(w, tx, c)
|
||||
}
|
||||
pos := c.Position()
|
||||
s.chunkLoader.Move(tx, pos)
|
||||
chunkPos := world.ChunkPos{int32(pos[0]) << 4, int32(pos[2]) << 4}
|
||||
if s.lastChunkPos != chunkPos || worldSwitched {
|
||||
s.lastChunkPos = chunkPos
|
||||
s.writePacket(&packet.NetworkChunkPublisherUpdate{
|
||||
Position: protocol.BlockPos{int32(pos[0]), int32(pos[1]), int32(pos[2])},
|
||||
Radius: uint32(s.chunkRadius) << 4,
|
||||
})
|
||||
}
|
||||
|
||||
s.blobMu.Lock()
|
||||
const maxChunkTransactions = 8
|
||||
toLoad := maxChunkTransactions - len(s.openChunkTransactions)
|
||||
s.blobMu.Unlock()
|
||||
if toLoad > 4 {
|
||||
toLoad = 4
|
||||
}
|
||||
s.chunkLoader.Load(tx, toLoad)
|
||||
}
|
||||
|
||||
// handleWorldSwitch handles the player of the Session switching worlds.
|
||||
func (s *Session) handleWorldSwitch(w *world.World, tx *world.Tx, c Controllable) {
|
||||
if s.conn.ClientCacheEnabled() {
|
||||
s.blobMu.Lock()
|
||||
s.blobs = map[uint64][]byte{}
|
||||
s.openChunkTransactions = nil
|
||||
s.blobMu.Unlock()
|
||||
}
|
||||
|
||||
dim, _ := world.DimensionID(w.Dimension())
|
||||
same := w.Dimension() == s.chunkLoader.World().Dimension()
|
||||
if !same {
|
||||
s.changeDimension(int32(dim), false, c)
|
||||
}
|
||||
s.ViewEntityTeleport(c, c.Position())
|
||||
s.chunkLoader.ChangeWorld(tx, w)
|
||||
}
|
||||
|
||||
// changeDimension changes the dimension of the client. If silent is set to true, the portal noise will be stopped
|
||||
// immediately.
|
||||
func (s *Session) changeDimension(dim int32, silent bool, c Controllable) {
|
||||
s.changingDimension.Store(true)
|
||||
h := s.handlers[packet.IDServerBoundLoadingScreen].(*ServerBoundLoadingScreenHandler)
|
||||
id := h.currentID.Add(1)
|
||||
h.expectedID.Store(id)
|
||||
|
||||
s.writePacket(&packet.ChangeDimension{
|
||||
Dimension: dim,
|
||||
Position: vec64To32(c.Position().Add(entityOffset(c))),
|
||||
LoadingScreenID: protocol.Option(id),
|
||||
})
|
||||
s.writePacket(&packet.StopSound{StopAll: silent})
|
||||
s.writePacket(&packet.PlayStatus{Status: packet.PlayStatusPlayerSpawn})
|
||||
|
||||
// As of v1.19.50, the dimension ack that is meant to be sent by the client is now sent by the server. The client
|
||||
// still sends the ack, but after the server has sent it. Thanks to Mojang for another groundbreaking change.
|
||||
s.writePacket(&packet.PlayerAction{
|
||||
EntityRuntimeID: selfEntityRuntimeID,
|
||||
ActionType: protocol.PlayerActionDimensionChangeDone,
|
||||
})
|
||||
}
|
||||
|
||||
// ChangingDimension returns whether the session is currently changing dimension or not.
|
||||
func (s *Session) ChangingDimension() bool {
|
||||
return s.changingDimension.Load()
|
||||
}
|
||||
|
||||
// ChunkRadius returns the chunk radius of the session.
|
||||
func (s *Session) ChunkRadius() int32 {
|
||||
return s.chunkRadius
|
||||
}
|
||||
|
||||
// handlePacket handles an incoming packet, processing it accordingly. If the packet had invalid data or was
|
||||
// otherwise not valid in its context, an error is returned.
|
||||
func (s *Session) handlePacket(pk packet.Packet, tx *world.Tx, c Controllable) (err error) {
|
||||
handler, ok := s.handlers[pk.ID()]
|
||||
if !ok {
|
||||
s.conf.Log.Debug("unhandled packet", "packet", fmt.Sprintf("%T", pk), "data", fmt.Sprintf("%+v", pk)[1:])
|
||||
return nil
|
||||
}
|
||||
if handler == nil {
|
||||
// A nil handler means it was explicitly unhandled.
|
||||
return nil
|
||||
}
|
||||
if err := handler.Handle(pk, s, tx, c); err != nil {
|
||||
return fmt.Errorf("%T: %w", pk, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// registerHandlers registers all packet handlers found in the packetHandler package.
|
||||
func (s *Session) registerHandlers() {
|
||||
s.handlers = map[uint32]packetHandler{
|
||||
packet.IDActorEvent: nil,
|
||||
packet.IDAdventureSettings: nil, // Deprecated, the client still sends this though.
|
||||
packet.IDAnimate: nil,
|
||||
packet.IDAnvilDamage: nil,
|
||||
packet.IDBlockActorData: &BlockActorDataHandler{},
|
||||
packet.IDBlockPickRequest: &BlockPickRequestHandler{},
|
||||
packet.IDBookEdit: &BookEditHandler{},
|
||||
packet.IDBossEvent: nil,
|
||||
packet.IDClientCacheBlobStatus: &ClientCacheBlobStatusHandler{},
|
||||
packet.IDCommandRequest: &CommandRequestHandler{},
|
||||
packet.IDContainerClose: &ContainerCloseHandler{},
|
||||
packet.IDEmote: &EmoteHandler{},
|
||||
packet.IDEmoteList: nil,
|
||||
packet.IDFilterText: nil,
|
||||
packet.IDInteract: &InteractHandler{},
|
||||
packet.IDInventoryTransaction: &InventoryTransactionHandler{},
|
||||
packet.IDItemStackRequest: &ItemStackRequestHandler{changes: map[byte]map[byte]changeInfo{}, responseChanges: map[int32]map[*inventory.Inventory]map[byte]responseChange{}},
|
||||
packet.IDLecternUpdate: &LecternUpdateHandler{},
|
||||
packet.IDMobEquipment: &MobEquipmentHandler{},
|
||||
packet.IDModalFormResponse: &ModalFormResponseHandler{forms: make(map[uint32]form.Form)},
|
||||
packet.IDMovePlayer: nil,
|
||||
packet.IDNPCRequest: &NPCRequestHandler{},
|
||||
packet.IDPlayerAction: &PlayerActionHandler{},
|
||||
packet.IDPlayerAuthInput: &PlayerAuthInputHandler{},
|
||||
packet.IDPlayerSkin: &PlayerSkinHandler{},
|
||||
packet.IDRequestAbility: &RequestAbilityHandler{},
|
||||
packet.IDRequestChunkRadius: &RequestChunkRadiusHandler{},
|
||||
packet.IDRespawn: &RespawnHandler{},
|
||||
packet.IDSetPlayerInventoryOptions: nil,
|
||||
packet.IDSubChunkRequest: &SubChunkRequestHandler{},
|
||||
packet.IDText: &TextHandler{},
|
||||
packet.IDServerBoundLoadingScreen: &ServerBoundLoadingScreenHandler{},
|
||||
packet.IDServerBoundDiagnostics: &ServerBoundDiagnosticsHandler{},
|
||||
}
|
||||
}
|
||||
|
||||
// writePacket writes a packet to the session's connection if it is not Nop.
|
||||
func (s *Session) writePacket(pk packet.Packet) {
|
||||
if s == Nop {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case s.packets <- pk:
|
||||
case <-s.closeBackground:
|
||||
}
|
||||
}
|
||||
|
||||
// actorIdentifier represents the structure of an actor identifier sent over the network.
|
||||
type actorIdentifier struct {
|
||||
// ID is a unique namespaced identifier for the entity.
|
||||
ID string `nbt:"id"`
|
||||
}
|
||||
|
||||
// sendAvailableEntities sends all registered entities to the player.
|
||||
func (s *Session) sendAvailableEntities(w *world.World) {
|
||||
var identifiers []actorIdentifier
|
||||
for _, t := range w.EntityRegistry().Types() {
|
||||
identifiers = append(identifiers, actorIdentifier{ID: t.EncodeEntity()})
|
||||
}
|
||||
serialisedEntityData, err := nbt.Marshal(map[string]any{"idlist": identifiers})
|
||||
if err != nil {
|
||||
panic("should never happen")
|
||||
}
|
||||
s.writePacket(&packet.AvailableActorIdentifiers{SerialisedEntityIdentifiers: serialisedEntityData})
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/df-mc/dragonfly/server/internal/sliceutil"
|
||||
"github.com/df-mc/dragonfly/server/player/skin"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
)
|
||||
|
||||
var sessions = new(sessionList)
|
||||
|
||||
type sessionList struct {
|
||||
mu sync.Mutex
|
||||
s []*Session
|
||||
}
|
||||
|
||||
func (l *sessionList) Add(s *Session) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
for _, other := range l.s {
|
||||
// Show all sessions to the new session and the new session to all
|
||||
// existing sessions.
|
||||
l.sendSessionTo(s, other)
|
||||
l.sendSessionTo(other, s)
|
||||
}
|
||||
// Show the new session to itself.
|
||||
l.sendSessionTo(s, s)
|
||||
l.s = append(l.s, s)
|
||||
}
|
||||
|
||||
func (l *sessionList) Remove(s *Session, entity world.Entity) {
|
||||
l.mu.Lock()
|
||||
removedFrom := slices.Clone(l.s)
|
||||
for _, other := range l.s {
|
||||
l.unsendSessionFrom(s, other)
|
||||
}
|
||||
l.s = sliceutil.DeleteVal(l.s, s)
|
||||
l.mu.Unlock()
|
||||
|
||||
if entity == nil {
|
||||
return
|
||||
}
|
||||
for _, other := range removedFrom {
|
||||
if other.viewLayer != nil {
|
||||
other.viewLayer.Remove(entity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *sessionList) Lookup(id uuid.UUID) (*Session, bool) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
|
||||
if index := slices.IndexFunc(l.s, func(session *Session) bool {
|
||||
return session.ent.UUID() == id
|
||||
}); index != -1 {
|
||||
return l.s[index], true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (l *sessionList) sendSessionTo(s, to *Session) {
|
||||
runtimeID := uint64(selfEntityRuntimeID)
|
||||
|
||||
to.entityMutex.Lock()
|
||||
if s != to {
|
||||
to.currentEntityRuntimeID += 1
|
||||
runtimeID = to.currentEntityRuntimeID
|
||||
}
|
||||
to.entityRuntimeIDs[s.ent] = runtimeID
|
||||
to.entities[runtimeID] = s.ent
|
||||
to.entityMutex.Unlock()
|
||||
|
||||
to.writePacket(&packet.PlayerList{
|
||||
ActionType: packet.PlayerListActionAdd,
|
||||
Entries: []protocol.PlayerListEntry{{
|
||||
UUID: s.ent.UUID(),
|
||||
EntityUniqueID: int64(runtimeID),
|
||||
Username: s.conn.IdentityData().DisplayName,
|
||||
XUID: s.conn.IdentityData().XUID,
|
||||
Skin: skinToProtocol(s.joinSkin),
|
||||
}},
|
||||
})
|
||||
}
|
||||
|
||||
func (l *sessionList) unsendSessionFrom(s, from *Session) {
|
||||
from.entityMutex.Lock()
|
||||
delete(from.entities, from.entityRuntimeIDs[s.ent])
|
||||
delete(from.entityRuntimeIDs, s.ent)
|
||||
from.entityMutex.Unlock()
|
||||
|
||||
from.writePacket(&packet.PlayerList{
|
||||
ActionType: packet.PlayerListActionRemove,
|
||||
Entries: []protocol.PlayerListEntry{{UUID: s.ent.UUID()}},
|
||||
})
|
||||
}
|
||||
|
||||
// skinToProtocol converts a skin to its protocol representation.
|
||||
func skinToProtocol(s skin.Skin) protocol.Skin {
|
||||
var animations []protocol.SkinAnimation
|
||||
for _, animation := range s.Animations {
|
||||
protocolAnim := protocol.SkinAnimation{
|
||||
ImageWidth: uint32(animation.Bounds().Max.X),
|
||||
ImageHeight: uint32(animation.Bounds().Max.Y),
|
||||
ImageData: animation.Pix,
|
||||
FrameCount: float32(animation.FrameCount),
|
||||
}
|
||||
switch animation.Type() {
|
||||
case skin.AnimationHead:
|
||||
protocolAnim.AnimationType = protocol.SkinAnimationHead
|
||||
case skin.AnimationBody32x32:
|
||||
protocolAnim.AnimationType = protocol.SkinAnimationBody32x32
|
||||
case skin.AnimationBody128x128:
|
||||
protocolAnim.AnimationType = protocol.SkinAnimationBody128x128
|
||||
}
|
||||
protocolAnim.ExpressionType = uint32(animation.AnimationExpression)
|
||||
animations = append(animations, protocolAnim)
|
||||
}
|
||||
|
||||
fullID := s.FullID
|
||||
if fullID == "" {
|
||||
fullID = uuid.New().String()
|
||||
}
|
||||
return protocol.Skin{
|
||||
PlayFabID: s.PlayFabID,
|
||||
SkinID: uuid.New().String(),
|
||||
SkinResourcePatch: s.ModelConfig.Encode(),
|
||||
SkinImageWidth: uint32(s.Bounds().Max.X),
|
||||
SkinImageHeight: uint32(s.Bounds().Max.Y),
|
||||
SkinData: s.Pix,
|
||||
CapeImageWidth: uint32(s.Cape.Bounds().Max.X),
|
||||
CapeImageHeight: uint32(s.Cape.Bounds().Max.Y),
|
||||
CapeData: s.Cape.Pix,
|
||||
SkinGeometry: s.Model,
|
||||
PersonaSkin: s.Persona,
|
||||
CapeID: uuid.New().String(),
|
||||
FullID: fullID,
|
||||
Animations: animations,
|
||||
Trusted: true,
|
||||
OverrideAppearance: true,
|
||||
GeometryDataEngineVersion: []byte(protocol.CurrentVersion),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/df-mc/dragonfly/server/player/chat"
|
||||
"github.com/df-mc/dragonfly/server/player/scoreboard"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol"
|
||||
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// SendMessage ...
|
||||
func (s *Session) SendMessage(message string) {
|
||||
s.writePacket(&packet.Text{
|
||||
TextType: packet.TextTypeRaw,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// SendTranslation sends a translation localised for a specific language.Tag.
|
||||
func (s *Session) SendTranslation(t chat.Translation, l language.Tag, a []any) {
|
||||
tr := t.F(a...)
|
||||
s.writePacket(&packet.Text{
|
||||
TextType: packet.TextTypeTranslation,
|
||||
NeedsTranslation: true,
|
||||
Message: tr.Resolve(l),
|
||||
Parameters: tr.Params(l),
|
||||
})
|
||||
}
|
||||
|
||||
// SendTip ...
|
||||
func (s *Session) SendTip(message string) {
|
||||
s.writePacket(&packet.Text{
|
||||
TextType: packet.TextTypeTip,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// SendAnnouncement ...
|
||||
func (s *Session) SendAnnouncement(message string) {
|
||||
s.writePacket(&packet.Text{
|
||||
TextType: packet.TextTypeAnnouncement,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// SendPopup ...
|
||||
func (s *Session) SendPopup(message string) {
|
||||
s.writePacket(&packet.Text{
|
||||
TextType: packet.TextTypePopup,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// SendJukeboxPopup ...
|
||||
func (s *Session) SendJukeboxPopup(message string) {
|
||||
s.writePacket(&packet.Text{
|
||||
TextType: packet.TextTypeJukeboxPopup,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// SendToast ...
|
||||
func (s *Session) SendToast(title, message string) {
|
||||
s.writePacket(&packet.ToastRequest{
|
||||
Title: title,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
// SendScoreboard ...
|
||||
func (s *Session) SendScoreboard(sb *scoreboard.Scoreboard) {
|
||||
if s == Nop {
|
||||
return
|
||||
}
|
||||
currentName, currentLines := *s.currentScoreboard.Load(), *s.currentLines.Load()
|
||||
|
||||
if currentName != sb.Name() {
|
||||
s.RemoveScoreboard()
|
||||
pk := &packet.SetDisplayObjective{
|
||||
DisplaySlot: "sidebar",
|
||||
ObjectiveName: sb.Name(),
|
||||
DisplayName: sb.Name(),
|
||||
CriteriaName: "dummy",
|
||||
}
|
||||
if sb.Descending() {
|
||||
pk.SortOrder = packet.ScoreboardSortOrderDescending
|
||||
} else {
|
||||
pk.SortOrder = packet.ScoreboardSortOrderAscending
|
||||
}
|
||||
s.writePacket(pk)
|
||||
name, lines := sb.Name(), append([]string(nil), sb.Lines()...)
|
||||
s.currentScoreboard.Store(&name)
|
||||
s.currentLines.Store(&lines)
|
||||
} else {
|
||||
// Remove all current lines from the scoreboard. We can't replace them without removing them.
|
||||
pk := &packet.SetScore{ActionType: packet.ScoreboardActionRemove}
|
||||
for i := range currentLines {
|
||||
pk.Entries = append(pk.Entries, protocol.ScoreboardEntry{
|
||||
EntryID: int64(i),
|
||||
ObjectiveName: currentName,
|
||||
Score: int32(i),
|
||||
})
|
||||
}
|
||||
if len(pk.Entries) > 0 {
|
||||
s.writePacket(pk)
|
||||
}
|
||||
}
|
||||
pk := &packet.SetScore{ActionType: packet.ScoreboardActionModify}
|
||||
for k, line := range sb.Lines() {
|
||||
if len(line) == 0 {
|
||||
line = "§" + colours[k]
|
||||
}
|
||||
pk.Entries = append(pk.Entries, protocol.ScoreboardEntry{
|
||||
EntryID: int64(k),
|
||||
ObjectiveName: sb.Name(),
|
||||
Score: int32(k),
|
||||
IdentityType: protocol.ScoreboardIdentityFakePlayer,
|
||||
DisplayName: line,
|
||||
})
|
||||
}
|
||||
if len(pk.Entries) > 0 {
|
||||
s.writePacket(pk)
|
||||
}
|
||||
}
|
||||
|
||||
// colours holds a list of colour codes to be filled out for empty lines in a scoreboard.
|
||||
var colours = [15]string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"}
|
||||
|
||||
// RemoveScoreboard ...
|
||||
func (s *Session) RemoveScoreboard() {
|
||||
s.writePacket(&packet.RemoveObjective{ObjectiveName: *s.currentScoreboard.Load()})
|
||||
var name string
|
||||
var lines []string
|
||||
s.currentScoreboard.Store(&name)
|
||||
s.currentLines.Store(&lines)
|
||||
}
|
||||
|
||||
// SendBossBar sends a boss bar to the player with the text passed and the health percentage of the bar.
|
||||
// SendBossBar removes any boss bar that might be active before sending the new one.
|
||||
func (s *Session) SendBossBar(text string, colour uint8, healthPercentage float64) {
|
||||
s.RemoveBossBar()
|
||||
s.writePacket(&packet.BossEvent{
|
||||
BossEntityUniqueID: selfEntityRuntimeID,
|
||||
EventType: packet.BossEventShow,
|
||||
BossBarTitle: text,
|
||||
HealthPercentage: float32(healthPercentage),
|
||||
Colour: colour,
|
||||
})
|
||||
}
|
||||
|
||||
// RemoveBossBar removes any boss bar currently active on the player's screen.
|
||||
func (s *Session) RemoveBossBar() {
|
||||
s.writePacket(&packet.BossEvent{
|
||||
BossEntityUniqueID: selfEntityRuntimeID,
|
||||
EventType: packet.BossEventHide,
|
||||
})
|
||||
}
|
||||
|
||||
const tickLength = time.Second / 20
|
||||
|
||||
// SetTitleDurations ...
|
||||
func (s *Session) SetTitleDurations(fadeInDuration, remainDuration, fadeOutDuration time.Duration) {
|
||||
s.writePacket(&packet.SetTitle{
|
||||
ActionType: packet.TitleActionSetDurations,
|
||||
FadeInDuration: int32(fadeInDuration / tickLength),
|
||||
RemainDuration: int32(remainDuration / tickLength),
|
||||
FadeOutDuration: int32(fadeOutDuration / tickLength),
|
||||
})
|
||||
}
|
||||
|
||||
// SendTitle ...
|
||||
func (s *Session) SendTitle(text string) {
|
||||
s.writePacket(&packet.SetTitle{ActionType: packet.TitleActionSetTitle, Text: text})
|
||||
}
|
||||
|
||||
// SendSubtitle ...
|
||||
func (s *Session) SendSubtitle(text string) {
|
||||
s.writePacket(&packet.SetTitle{ActionType: packet.TitleActionSetSubtitle, Text: text})
|
||||
}
|
||||
|
||||
// SendActionBarMessage ...
|
||||
func (s *Session) SendActionBarMessage(text string) {
|
||||
s.writePacket(&packet.SetTitle{ActionType: packet.TitleActionSetActionBar, Text: text})
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package session
|
||||
|
||||
import "github.com/df-mc/dragonfly/server/world"
|
||||
|
||||
// ViewLayer returns the session's ViewLayer. The layer may be used to override how entities are viewed
|
||||
// by this session, such as with a different name tag or visibility state.
|
||||
func (s *Session) ViewLayer() *world.ViewLayer {
|
||||
return s.viewLayer
|
||||
}
|
||||
|
||||
// ViewNameTag overwrites the public name tag of the entity and immediately refreshes it for this session.
|
||||
func (s *Session) ViewNameTag(entity world.Entity, nameTag string) {
|
||||
if s.viewLayer == nil {
|
||||
return
|
||||
}
|
||||
s.viewLayer.ViewNameTag(entity, nameTag)
|
||||
}
|
||||
|
||||
// ViewPublicNameTag removes the name tag override from the entity and immediately refreshes it for this session.
|
||||
func (s *Session) ViewPublicNameTag(entity world.Entity) {
|
||||
if s.viewLayer == nil {
|
||||
return
|
||||
}
|
||||
s.viewLayer.ViewPublicNameTag(entity)
|
||||
}
|
||||
|
||||
// ViewScoreTag overwrites the public score tag of the entity and immediately refreshes it for this session.
|
||||
func (s *Session) ViewScoreTag(entity world.Entity, scoreTag string) {
|
||||
if s.viewLayer == nil {
|
||||
return
|
||||
}
|
||||
s.viewLayer.ViewScoreTag(entity, scoreTag)
|
||||
}
|
||||
|
||||
// ViewPublicScoreTag removes the score tag override from the entity and immediately refreshes it for this session.
|
||||
func (s *Session) ViewPublicScoreTag(entity world.Entity) {
|
||||
if s.viewLayer == nil {
|
||||
return
|
||||
}
|
||||
s.viewLayer.ViewPublicScoreTag(entity)
|
||||
}
|
||||
|
||||
// ViewVisibility overwrites the public visibility of the entity and immediately refreshes it for this session.
|
||||
func (s *Session) ViewVisibility(entity world.Entity, level world.VisibilityLevel) {
|
||||
if s.viewLayer == nil {
|
||||
return
|
||||
}
|
||||
s.viewLayer.ViewVisibility(entity, level)
|
||||
}
|
||||
|
||||
// RemoveViewLayer removes all overrides for the entity and immediately refreshes it for this session.
|
||||
func (s *Session) RemoveViewLayer(entity world.Entity) {
|
||||
if s.viewLayer == nil {
|
||||
return
|
||||
}
|
||||
s.viewLayer.Remove(entity)
|
||||
}
|
||||
|
||||
// ViewLayerEntityChanged refreshes the entity metadata for this session if the entity is currently visible.
|
||||
func (s *Session) ViewLayerEntityChanged(e world.Entity) {
|
||||
if s.entityHidden(e) || !s.viewingEntity(e.H()) {
|
||||
return
|
||||
}
|
||||
s.ViewEntityState(e)
|
||||
}
|
||||
|
||||
// viewingEntity checks if this session currently has a runtime ID assigned to the entity handle.
|
||||
func (s *Session) viewingEntity(handle *world.EntityHandle) bool {
|
||||
s.entityMutex.RLock()
|
||||
_, ok := s.entityRuntimeIDs[handle]
|
||||
s.entityMutex.RUnlock()
|
||||
return ok
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user