301 lines
6.3 KiB
Go
301 lines
6.3 KiB
Go
package client
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"git.konjactw.dev/falloutBot/go-mc/chat"
|
|
"git.konjactw.dev/falloutBot/go-mc/chat/sign"
|
|
pk "git.konjactw.dev/falloutBot/go-mc/net/packet"
|
|
"git.konjactw.dev/falloutBot/go-mc/yggdrasil/user"
|
|
)
|
|
|
|
type PlayerInfo interface {
|
|
pk.Field
|
|
playerInfoBitMask() int
|
|
}
|
|
|
|
type PlayerInfoUpdate struct {
|
|
Players map[uuid.UUID][]PlayerInfo
|
|
}
|
|
|
|
func (p PlayerInfoUpdate) WriteTo(w io.Writer) (n int64, err error) {
|
|
actions, err := collectPlayerInfoActions(p.Players)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
bitset := pk.NewFixedBitSet(8)
|
|
for _, action := range actions {
|
|
bitset.Set(actionIndex(action), true)
|
|
}
|
|
n1, err := bitset.WriteTo(w)
|
|
if err != nil {
|
|
return n1, err
|
|
}
|
|
n += n1
|
|
n2, err := pk.VarInt(len(p.Players)).WriteTo(w)
|
|
if err != nil {
|
|
return n1 + n2, err
|
|
}
|
|
n += n2
|
|
for playerUUID, infos := range p.Players {
|
|
n3, err := (*pk.UUID)(&playerUUID).WriteTo(w)
|
|
if err != nil {
|
|
return n1 + n2 + n3, err
|
|
}
|
|
n += n3
|
|
|
|
infosByAction, err := indexPlayerInfos(infos)
|
|
if err != nil {
|
|
return n, err
|
|
}
|
|
|
|
for _, action := range actions {
|
|
info, ok := infosByAction[action]
|
|
if !ok {
|
|
return n, fmt.Errorf("player %s missing player info action mask %#02x", playerUUID.String(), action)
|
|
}
|
|
n4, err := info.WriteTo(w)
|
|
if err != nil {
|
|
return n1 + n2 + n3 + n4, err
|
|
}
|
|
n += n4
|
|
}
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
func (p *PlayerInfoUpdate) ReadFrom(r io.Reader) (n int64, err error) {
|
|
bitset := pk.NewFixedBitSet(8)
|
|
n1, err := bitset.ReadFrom(r)
|
|
if err != nil {
|
|
return n1, err
|
|
}
|
|
n += n1
|
|
|
|
actions := actionsFromBitSet(bitset)
|
|
|
|
var playerCount pk.VarInt
|
|
n2, err := playerCount.ReadFrom(r)
|
|
if err != nil {
|
|
return n + n2, err
|
|
}
|
|
n += n2
|
|
|
|
players := make(map[uuid.UUID][]PlayerInfo, int(playerCount))
|
|
for i := 0; i < int(playerCount); i++ {
|
|
var playerUUID uuid.UUID
|
|
n3, err := (*pk.UUID)(&playerUUID).ReadFrom(r)
|
|
if err != nil {
|
|
return n + n3, err
|
|
}
|
|
n += n3
|
|
|
|
infos := make([]PlayerInfo, 0, len(actions))
|
|
for _, action := range actions {
|
|
info := newPlayerInfoByAction(action)
|
|
if info == nil {
|
|
return n, fmt.Errorf("unsupported player info action mask %#02x", action)
|
|
}
|
|
n4, err := playerInfoRead(&infos, info, r)
|
|
if err != nil {
|
|
return n + n4, err
|
|
}
|
|
n += n4
|
|
}
|
|
players[playerUUID] = infos
|
|
}
|
|
|
|
p.Players = players
|
|
return n, nil
|
|
}
|
|
|
|
func playerInfoRead(infos *[]PlayerInfo, info PlayerInfo, r io.Reader) (int64, error) {
|
|
n, err := info.ReadFrom(r)
|
|
if err != nil {
|
|
return n, err
|
|
}
|
|
*infos = append(*infos, info)
|
|
return n, err
|
|
}
|
|
|
|
func collectPlayerInfoActions(players map[uuid.UUID][]PlayerInfo) ([]int, error) {
|
|
actions := make(map[int]struct{}, 8)
|
|
for playerID, infos := range players {
|
|
seen := make(map[int]struct{}, len(infos))
|
|
for _, info := range infos {
|
|
mask := info.playerInfoBitMask()
|
|
if err := validatePlayerInfoActionMask(mask); err != nil {
|
|
return nil, fmt.Errorf("player %s has invalid action: %w", playerID.String(), err)
|
|
}
|
|
if _, exists := seen[mask]; exists {
|
|
return nil, fmt.Errorf("player %s has duplicated action mask %#02x", playerID.String(), mask)
|
|
}
|
|
seen[mask] = struct{}{}
|
|
actions[mask] = struct{}{}
|
|
}
|
|
}
|
|
|
|
sorted := make([]int, 0, len(actions))
|
|
for mask := range actions {
|
|
sorted = append(sorted, mask)
|
|
}
|
|
sort.Ints(sorted)
|
|
return sorted, nil
|
|
}
|
|
|
|
func indexPlayerInfos(infos []PlayerInfo) (map[int]PlayerInfo, error) {
|
|
indexed := make(map[int]PlayerInfo, len(infos))
|
|
for _, info := range infos {
|
|
mask := info.playerInfoBitMask()
|
|
if err := validatePlayerInfoActionMask(mask); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, exists := indexed[mask]; exists {
|
|
return nil, fmt.Errorf("duplicated player info action mask %#02x", mask)
|
|
}
|
|
indexed[mask] = info
|
|
}
|
|
return indexed, nil
|
|
}
|
|
|
|
func validatePlayerInfoActionMask(mask int) error {
|
|
if mask <= 0 || mask > 0x80 {
|
|
return fmt.Errorf("action mask out of range: %#02x", mask)
|
|
}
|
|
// Action mask is an enum bit. It must have exactly one bit set.
|
|
if mask&(mask-1) != 0 {
|
|
return fmt.Errorf("action mask must be a single bit: %#02x", mask)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func actionIndex(mask int) int {
|
|
index := 0
|
|
for (mask & 0x01) == 0 {
|
|
index++
|
|
mask >>= 1
|
|
}
|
|
return index
|
|
}
|
|
|
|
func actionsFromBitSet(bitset pk.FixedBitSet) []int {
|
|
actions := make([]int, 0, len(playerInfoActionMasks))
|
|
for _, mask := range playerInfoActionMasks {
|
|
if bitset.Get(actionIndex(mask)) {
|
|
actions = append(actions, mask)
|
|
}
|
|
}
|
|
return actions
|
|
}
|
|
|
|
func newPlayerInfoByAction(mask int) PlayerInfo {
|
|
switch mask {
|
|
case 0x01:
|
|
return &PlayerInfoAddPlayer{}
|
|
case 0x02:
|
|
return &PlayerInfoInitializeChat{}
|
|
case 0x04:
|
|
return &PlayerInfoUpdateGameMode{}
|
|
case 0x08:
|
|
return &PlayerInfoUpdateListed{}
|
|
case 0x10:
|
|
return &PlayerInfoUpdateLatency{}
|
|
case 0x20:
|
|
return &PlayerInfoUpdateDisplayName{}
|
|
case 0x40:
|
|
return &PlayerInfoUpdateListPriority{}
|
|
case 0x80:
|
|
return &PlayerInfoUpdateHat{}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
var playerInfoActionMasks = [...]int{
|
|
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80,
|
|
}
|
|
|
|
//codec:gen
|
|
type PlayerInfoAddPlayer struct {
|
|
Name string
|
|
Properties []user.Property
|
|
}
|
|
|
|
//codec:gen
|
|
type PlayerInfoChatData struct {
|
|
ChatSessionID uuid.UUID `mc:"UUID"`
|
|
Session sign.Session
|
|
}
|
|
|
|
//codec:gen
|
|
type PlayerInfoInitializeChat struct {
|
|
Data pk.Option[PlayerInfoChatData, *PlayerInfoChatData]
|
|
}
|
|
|
|
//codec:gen
|
|
type PlayerInfoUpdateGameMode struct {
|
|
GameMode int32 `mc:"VarInt"`
|
|
}
|
|
|
|
//codec:gen
|
|
type PlayerInfoUpdateListed struct {
|
|
Listed bool
|
|
}
|
|
|
|
//codec:gen
|
|
type PlayerInfoUpdateLatency struct {
|
|
Ping int32 `mc:"VarInt"`
|
|
}
|
|
|
|
//codec:gen
|
|
type PlayerInfoUpdateDisplayName struct {
|
|
DisplayName pk.Option[chat.Message, *chat.Message]
|
|
}
|
|
|
|
//codec:gen
|
|
type PlayerInfoUpdateListPriority struct {
|
|
Priority int32 `mc:"VarInt"`
|
|
}
|
|
|
|
//codec:gen
|
|
type PlayerInfoUpdateHat struct {
|
|
Visible bool
|
|
}
|
|
|
|
func (PlayerInfoAddPlayer) playerInfoBitMask() int {
|
|
return 0x01
|
|
}
|
|
|
|
func (PlayerInfoInitializeChat) playerInfoBitMask() int {
|
|
return 0x02
|
|
}
|
|
|
|
func (PlayerInfoUpdateGameMode) playerInfoBitMask() int {
|
|
return 0x04
|
|
}
|
|
|
|
func (PlayerInfoUpdateListed) playerInfoBitMask() int {
|
|
return 0x08
|
|
}
|
|
|
|
func (PlayerInfoUpdateLatency) playerInfoBitMask() int {
|
|
return 0x10
|
|
}
|
|
|
|
func (PlayerInfoUpdateDisplayName) playerInfoBitMask() int {
|
|
return 0x20
|
|
}
|
|
|
|
func (PlayerInfoUpdateListPriority) playerInfoBitMask() int {
|
|
return 0x40
|
|
}
|
|
|
|
func (PlayerInfoUpdateHat) playerInfoBitMask() int {
|
|
return 0x80
|
|
}
|