Files
minego/pkg/protocol/packet/game/client/player_info_update.go
2026-02-16 13:42:03 +08:00

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
}