First draft of snap unpacking

This commit is contained in:
ChillerDragon 2024-06-24 17:03:13 +08:00
parent 8acb893165
commit 72b008f6d8
18 changed files with 756 additions and 3 deletions

View file

@ -6,6 +6,7 @@ import (
"github.com/teeworlds-go/go-teeworlds-protocol/chunk7"
"github.com/teeworlds-go/go-teeworlds-protocol/network7"
"github.com/teeworlds-go/go-teeworlds-protocol/packer"
"github.com/teeworlds-go/go-teeworlds-protocol/snapshot7"
)
type SnapSingle struct {
@ -15,7 +16,11 @@ type SnapSingle struct {
DeltaTick int
Crc int
PartSize int
Data []byte
// TODO: remove data when snapshot packing works
// as of right now Data and Snapshot are the same thing
Data []byte
Snapshot snapshot7.Snapshot
}
func (msg *SnapSingle) MsgId() int {
@ -50,6 +55,14 @@ func (msg *SnapSingle) Unpack(u *packer.Unpacker) error {
msg.Crc = u.GetInt()
msg.PartSize = u.GetInt()
msg.Data = u.Rest()
// genius
u.Reset(msg.Data)
err := msg.Snapshot.Unpack(u)
if err != nil {
return err
}
return nil
}

26
network6/network6.go Normal file
View file

@ -0,0 +1,26 @@
package network6
const (
ObjInvalid = 0
ObjPlayerInput = 1
ObjProjectile = 2
ObjLaser = 3
ObjPickup = 4
ObjFlag = 5
ObjGameInfo = 6
ObjGameData = 7
ObjCharacterCore = 8
ObjCharacter = 9
ObjPlayerInfo = 10
ObjClientInfo = 11
ObjSpectatorInfo = 12
ObjCommon = 13
ObjExplosion = 14
ObjSpawn = 15
ObjHammerhit = 16
ObjDeath = 17
ObjSoundGlobal = 18
ObjSoundWorld = 19
ObjDamageInd = 20
NumSnapObjects = 21
)

View file

@ -21,6 +21,22 @@ const (
VoteEndPass Vote = 5
VoteEndFail Vote = 6
PickupHealth Pickup = 0
PickupArmor Pickup = 1
PickupGrenade Pickup = 2
PickupShotgun Pickup = 3
PickupLaser Pickup = 4
PickupNinja Pickup = 5
PickupGun Pickup = 6
PickupHammer Pickup = 7
GamestateflagWarmup GameStateFlag = 1
GamestateflagSuddendeath GameStateFlag = 2
GamestateflagRoundover GameStateFlag = 4
GamestateflagGameover GameStateFlag = 8
GamestateflagPaused GameStateFlag = 16
GamestateflagStartcountdown GameStateFlag = 32
// oop!
EmoteOop Emote = 0
// !
@ -53,6 +69,33 @@ const (
MsgCtrlToken = 0x05
MsgCtrlClose = 0x04
ObjInvalid = 0
ObjPlayerInput = 1
ObjProjectile = 2
ObjLaser = 3
ObjPickup = 4
ObjFlag = 5
ObjGameData = 6
ObjGameDataTeam = 7
ObjGameDataFlag = 8
ObjCharacterCore = 9
ObjCharacter = 10
ObjPlayerInfo = 11
ObjSpectatorInfo = 12
ObjDeClientInfo = 13
ObjDeGameInfo = 14
ObjDeTuneParams = 15
ObjCommon = 16
ObjExplosion = 17
ObjSpawn = 18
ObjHammerHit = 19
ObjDeath = 20
ObjSoundWorld = 21
ObjDamage = 22
ObjPlayerInfoRace = 23
ObjGameDataRace = 24
NumNetobjtypes = 25
// TODO: these should preferrably all be devide dinto different type dintegers
// same as ChatMode, etc. so that the user can easily see which integer to pass
// to which function as which parameter
@ -147,7 +190,9 @@ const (
NumWeapons Weapon = 6
)
type GameStateFlag int
type Vote int
type Pickup int
type Emote int
type ChatMode int
type GameTeam int

105
object7/character.go Normal file
View file

@ -0,0 +1,105 @@
package object7
import (
"reflect"
"slices"
"github.com/teeworlds-go/go-teeworlds-protocol/network7"
"github.com/teeworlds-go/go-teeworlds-protocol/packer"
)
type Character struct {
ItemId int
Tick int
X int
Y int
VelX int
VelY int
Angle int
Direction int
Jumped int
HookedPlayer int
HookState int
HookTick int
HookX int
HookY int
HookDx int
HookDy int
Health int
Armor int
AmmoCount int
Weapon int
Emote int
AttackTick int
TriggeredEvents int
}
func (o *Character) Id() int {
return o.ItemId
}
func (o *Character) Type() int {
return network7.ObjCharacter
}
func (o *Character) Size() int {
return reflect.TypeOf(Character{}).NumField() - 1
}
func (o *Character) Pack() []byte {
return slices.Concat(
packer.PackInt(o.Type()),
packer.PackInt(o.Id()),
packer.PackInt(o.Tick),
packer.PackInt(o.X),
packer.PackInt(o.Y),
packer.PackInt(o.VelX),
packer.PackInt(o.VelY),
packer.PackInt(o.Angle),
packer.PackInt(o.Direction),
packer.PackInt(o.Jumped),
packer.PackInt(o.HookedPlayer),
packer.PackInt(o.HookState),
packer.PackInt(o.HookTick),
packer.PackInt(o.HookX),
packer.PackInt(o.HookY),
packer.PackInt(o.HookDx),
packer.PackInt(o.HookDy),
packer.PackInt(o.Health),
packer.PackInt(o.Armor),
packer.PackInt(o.AmmoCount),
packer.PackInt(o.Weapon),
packer.PackInt(o.Emote),
packer.PackInt(o.AttackTick),
packer.PackInt(o.TriggeredEvents),
)
}
func (o *Character) Unpack(u *packer.Unpacker) error {
o.Tick = u.GetInt()
o.X = u.GetInt()
o.Y = u.GetInt()
o.VelX = u.GetInt()
o.VelY = u.GetInt()
o.Angle = u.GetInt()
o.Direction = u.GetInt()
o.Jumped = u.GetInt()
o.HookedPlayer = u.GetInt()
o.HookState = u.GetInt()
o.HookTick = u.GetInt()
o.HookX = u.GetInt()
o.HookY = u.GetInt()
o.HookDx = u.GetInt()
o.HookDy = u.GetInt()
o.Health = u.GetInt()
o.Armor = u.GetInt()
o.AmmoCount = u.GetInt()
o.Weapon = u.GetInt()
o.Emote = u.GetInt()
o.AttackTick = u.GetInt()
o.TriggeredEvents = u.GetInt()
return nil
}

48
object7/flag.go Normal file
View file

@ -0,0 +1,48 @@
package object7
import (
"reflect"
"slices"
"github.com/teeworlds-go/go-teeworlds-protocol/network7"
"github.com/teeworlds-go/go-teeworlds-protocol/packer"
)
type Flag struct {
ItemId int
X int
Y int
Team network7.GameTeam
}
func (o *Flag) Id() int {
return o.ItemId
}
func (o *Flag) Type() int {
return network7.ObjFlag
}
func (o *Flag) Size() int {
return reflect.TypeOf(Flag{}).NumField() - 1
}
func (o *Flag) Pack() []byte {
return slices.Concat(
packer.PackInt(o.Type()),
packer.PackInt(o.Id()),
packer.PackInt(o.X),
packer.PackInt(o.Y),
packer.PackInt(int(o.Team)),
)
}
func (o *Flag) Unpack(u *packer.Unpacker) error {
o.X = u.GetInt()
o.Y = u.GetInt()
o.Team = network7.GameTeam(u.GetInt())
return nil
}

53
object7/game_data.go Normal file
View file

@ -0,0 +1,53 @@
package object7
import (
"reflect"
"slices"
"github.com/teeworlds-go/go-teeworlds-protocol/network7"
"github.com/teeworlds-go/go-teeworlds-protocol/packer"
)
type GameData struct {
ItemId int
GameStartTick int
// TODO: add struct to wrap these flags
// use network7.GameStateFlag bit operations to turn it into a bunch of booleans
// GameStateFlags GameStateFlagsStruct
FlagsRaw int
GameStateEndTick int
}
func (o *GameData) Id() int {
return o.ItemId
}
func (o *GameData) Type() int {
return network7.ObjGameData
}
func (o *GameData) Size() int {
return reflect.TypeOf(GameData{}).NumField() - 1
}
func (o *GameData) Pack() []byte {
return slices.Concat(
packer.PackInt(o.Type()),
packer.PackInt(o.Id()),
packer.PackInt(o.GameStartTick),
packer.PackInt(o.FlagsRaw),
packer.PackInt(o.GameStateEndTick),
)
}
func (o *GameData) Unpack(u *packer.Unpacker) error {
o.GameStartTick = u.GetInt()
o.FlagsRaw = u.GetInt()
o.GameStateEndTick = u.GetInt()
return nil
}

63
object7/game_data_flag.go Normal file
View file

@ -0,0 +1,63 @@
package object7
import (
"reflect"
"slices"
"github.com/teeworlds-go/go-teeworlds-protocol/network7"
"github.com/teeworlds-go/go-teeworlds-protocol/packer"
)
type GameDataFlag struct {
ItemId int
// It is either the client id of the carrier so 0-64 or one of those values
//
// -3 - FLAG_MISSING
// -2 - FLAG_ATSTAND
// -1 - FLAG_TAKEN
FlagCarrierRed int
// It is either the client id of the carrier so 0-64 or one of those values
//
// -3 - FLAG_MISSING
// -2 - FLAG_ATSTAND
// -1 - FLAG_TAKEN
FlagCarrierBlue int
FlagDropTickRed int
FlagDropTickBlue int
}
func (o *GameDataFlag) Id() int {
return o.ItemId
}
func (o *GameDataFlag) Type() int {
return network7.ObjGameDataFlag
}
func (o *GameDataFlag) Size() int {
return reflect.TypeOf(GameDataFlag{}).NumField() - 1
}
func (o *GameDataFlag) Pack() []byte {
return slices.Concat(
packer.PackInt(o.Type()),
packer.PackInt(o.Id()),
packer.PackInt(o.FlagCarrierRed),
packer.PackInt(o.FlagCarrierBlue),
packer.PackInt(o.FlagDropTickRed),
packer.PackInt(o.FlagDropTickBlue),
)
}
func (o *GameDataFlag) Unpack(u *packer.Unpacker) error {
o.FlagCarrierRed = u.GetInt()
o.FlagCarrierBlue = u.GetInt()
o.FlagDropTickRed = u.GetInt()
o.FlagDropTickBlue = u.GetInt()
return nil
}

45
object7/game_data_team.go Normal file
View file

@ -0,0 +1,45 @@
package object7
import (
"reflect"
"slices"
"github.com/teeworlds-go/go-teeworlds-protocol/network7"
"github.com/teeworlds-go/go-teeworlds-protocol/packer"
)
type GameDataTeam struct {
ItemId int
TeamscoreRed int
TeamscoreBlue int
}
func (o *GameDataTeam) Id() int {
return o.ItemId
}
func (o *GameDataTeam) Type() int {
return network7.ObjGameDataTeam
}
func (o *GameDataTeam) Size() int {
return reflect.TypeOf(GameDataTeam{}).NumField() - 1
}
func (o *GameDataTeam) Pack() []byte {
return slices.Concat(
packer.PackInt(o.Type()),
packer.PackInt(o.Id()),
packer.PackInt(o.TeamscoreRed),
packer.PackInt(o.TeamscoreBlue),
)
}
func (o *GameDataTeam) Unpack(u *packer.Unpacker) error {
o.TeamscoreRed = u.GetInt()
o.TeamscoreBlue = u.GetInt()
return nil
}

48
object7/pickup.go Normal file
View file

@ -0,0 +1,48 @@
package object7
import (
"reflect"
"slices"
"github.com/teeworlds-go/go-teeworlds-protocol/network7"
"github.com/teeworlds-go/go-teeworlds-protocol/packer"
)
type Pickup struct {
ItemId int
X int
Y int
PickupType network7.Pickup
}
func (o *Pickup) Id() int {
return o.ItemId
}
func (o *Pickup) Type() int {
return network7.ObjPickup
}
func (o *Pickup) Size() int {
return reflect.TypeOf(Pickup{}).NumField() - 1
}
func (o *Pickup) Pack() []byte {
return slices.Concat(
packer.PackInt(o.Type()),
packer.PackInt(o.Id()),
packer.PackInt(o.X),
packer.PackInt(o.Y),
packer.PackInt(int(o.PickupType)),
)
}
func (o *Pickup) Unpack(u *packer.Unpacker) error {
o.X = u.GetInt()
o.Y = u.GetInt()
o.PickupType = network7.Pickup(u.GetInt())
return nil
}

49
object7/player_info.go Normal file
View file

@ -0,0 +1,49 @@
package object7
import (
"reflect"
"slices"
"github.com/teeworlds-go/go-teeworlds-protocol/network7"
"github.com/teeworlds-go/go-teeworlds-protocol/packer"
)
type PlayerInfo struct {
ItemId int
// TODO: parse flags
PlayerFlags int
Score int
Latency int
}
func (o *PlayerInfo) Id() int {
return o.ItemId
}
func (o *PlayerInfo) Type() int {
return network7.ObjPlayerInfo
}
func (o *PlayerInfo) Size() int {
return reflect.TypeOf(PlayerInfo{}).NumField() - 1
}
func (o *PlayerInfo) Pack() []byte {
return slices.Concat(
packer.PackInt(o.Type()),
packer.PackInt(o.Id()),
packer.PackInt(o.PlayerFlags),
packer.PackInt(o.Score),
packer.PackInt(o.Latency),
)
}
func (o *PlayerInfo) Unpack(u *packer.Unpacker) error {
o.PlayerFlags = u.GetInt()
o.Score = u.GetInt()
o.Latency = u.GetInt()
return nil
}

49
object7/snap_object.go Normal file
View file

@ -0,0 +1,49 @@
package object7
import (
"log"
"github.com/teeworlds-go/go-teeworlds-protocol/network7"
"github.com/teeworlds-go/go-teeworlds-protocol/packer"
)
type SnapObject interface {
Id() int
Type() int
// number of packed integers
// not the number of bytes
Size() int
Pack() []byte
Unpack(u *packer.Unpacker) error
}
// Comes without payload
// you have to call item.Unpack(u) manually after getting it
func NewObject(itemType int, itemId int) SnapObject {
if itemType == network7.ObjFlag {
return &Flag{ItemId: itemId}
} else if itemType == network7.ObjPickup {
return &Pickup{ItemId: itemId}
} else if itemType == network7.ObjGameData {
return &GameData{ItemId: itemId}
} else if itemType == network7.ObjGameDataTeam {
return &GameDataTeam{ItemId: itemId}
} else if itemType == network7.ObjGameDataFlag {
return &GameDataFlag{ItemId: itemId}
} else if itemType == network7.ObjCharacter {
return &Character{ItemId: itemId}
} else if itemType == network7.ObjPlayerInfo {
return &PlayerInfo{ItemId: itemId}
}
// TODO: remove this is just for debugging
log.Fatalf("unknown item type %d\n", itemType)
unknown := &Unknown{
ItemId: itemId,
ItemType: itemType,
}
return unknown
}

55
object7/unknown.go Normal file
View file

@ -0,0 +1,55 @@
package object7
import (
"slices"
"github.com/teeworlds-go/go-teeworlds-protocol/packer"
)
// used for protocol forward compability
// an imaginary protocol version 0.7.6 could add a new snap item
// it would have an unknown type id followed by a size field
// we just consume size amount of integers and store it in this unknown struct
// then we move onto the next item
type Unknown struct {
ItemId int
ItemType int
ItemSize int
Fields []int
}
func (o *Unknown) Id() int {
return o.ItemId
}
func (o *Unknown) Type() int {
return o.ItemType
}
func (o *Unknown) Size() int {
return o.ItemSize
}
func (o *Unknown) Pack() []byte {
data := slices.Concat(
packer.PackInt(o.Type()),
packer.PackInt(o.Id()),
packer.PackInt(o.Size()),
)
for _, f := range o.Fields {
data = append(data, packer.PackInt(f)...)
}
return data
}
func (o *Unknown) Unpack(u *packer.Unpacker) error {
o.ItemSize = u.GetInt()
o.Fields = make([]int, o.Size())
for i := 0; i < o.Size(); i++ {
o.Fields[i] = u.GetInt()
}
return nil
}

View file

@ -31,7 +31,17 @@ func (u *Unpacker) Data() []byte {
}
func (u *Unpacker) Rest() []byte {
return u.data[u.idx:]
rest := u.data[u.idx:]
u.idx = u.Size()
return rest
}
func (u *Unpacker) Size() int {
return len(u.data)
}
func (u *Unpacker) RemainingSize() int {
return u.Size() - u.idx
}
const (

View file

@ -6,7 +6,7 @@ import (
"github.com/teeworlds-go/go-teeworlds-protocol/internal/testutils/require"
)
// rest
// rest and remaining size
func TestUnpackRest(t *testing.T) {
u := Unpacker{}
@ -16,6 +16,10 @@ func TestUnpackRest(t *testing.T) {
want := 1
got := u.GetInt()
require.Equal(t, want, got)
want = 2
got = u.RemainingSize()
require.Equal(t, want, got)
}
{
@ -23,6 +27,12 @@ func TestUnpackRest(t *testing.T) {
got := u.Rest()
require.Equal(t, want, got)
}
{
want := 0
got := u.RemainingSize()
require.Equal(t, want, got)
}
}
// client info

58
snapshot7/snapshot7.go Normal file
View file

@ -0,0 +1,58 @@
package snapshot7
import (
"fmt"
"github.com/teeworlds-go/go-teeworlds-protocol/object7"
"github.com/teeworlds-go/go-teeworlds-protocol/packer"
)
const (
MaxType = 0x7fff
MaxId = 0xffff
MaxParts = 64
MaxSize = MaxParts * 1024
)
type Snapshot struct {
NumRemovedItems int
NumItemDeltas int
Items []object7.SnapObject
}
func (snap *Snapshot) Unpack(u *packer.Unpacker) error {
// TODO: add all the error checking the C++ reference implementation has
snap.NumRemovedItems = u.GetInt()
snap.NumItemDeltas = u.GetInt()
u.GetInt() // _zero
// TODO: copy non deleted items from a delta snapshot
for i := 0; i < snap.NumRemovedItems; i++ {
deleted := u.GetInt()
fmt.Printf("deleted item key = %d\n", deleted)
// TODO: don't copy those from the delta snapshot
}
for i := 0; i < snap.NumItemDeltas; i++ {
itemType := u.GetInt()
itemId := u.GetInt()
item := object7.NewObject(itemType, itemId)
err := item.Unpack(u)
if err != nil {
return err
}
// TODO: update old items
}
if u.RemainingSize() > 0 {
return fmt.Errorf("unexpected remaining size %d after snapshot unpack\n", u.RemainingSize())
}
return nil
}

View file

@ -0,0 +1,47 @@
package snapshot7_test
import (
"testing"
"github.com/teeworlds-go/go-teeworlds-protocol/internal/testutils/require"
"github.com/teeworlds-go/go-teeworlds-protocol/protocol7"
)
func TestRepackSnapshot(t *testing.T) {
// snapshot captured with tcpdump
// generated by a vanilla teeworlds 0.7.5 server
// used the go-teeworlds-protocol sample client to connect to the server
// the package is compressed so the raw bytes are a bit non obvious to look at
//
// libtw2 dissector details
// Teeworlds 0.7 Protocol packet
// Teeworlds 0.7 Protocol chunk: sys.snap_single
// Header (non-vital)
// Message: sys.snap_single
// Tick: 1510
// Delta tick: 1511
// Crc: 16269
// Data (103 bytes)
//
dump := []byte{
0x10, 0x04, 0x01, 0x01, 0x02, 0x03, 0x04,
0xe8, 0x93, 0x29, 0x4b, 0x7c, 0x18, 0xe2, 0xe3, 0x47, 0x0d, 0x46, 0x86, 0xb0, 0xbc, 0xd7,
0x24, 0x0d, 0x93, 0x5e, 0x0f, 0x4d, 0xf2, 0x31, 0xe9, 0x09, 0x4f, 0x98, 0x84, 0x98, 0xf4, 0x2c,
0x3c, 0x0b, 0x93, 0x3c, 0x4d, 0x7a, 0x9e, 0xe7, 0x06, 0xeb, 0x9b, 0x49, 0x0f, 0xed, 0x61, 0xb9,
0xb0, 0x5e, 0x3b, 0x9a, 0x84, 0x87, 0xf5, 0xd0, 0x26, 0x54, 0x37, 0xec, 0x76, 0x92, 0x37, 0xef,
0xf5, 0x0d, 0x3e, 0x26, 0xe5, 0x54, 0xbc, 0x40, 0x5c, 0x69, 0x93, 0x72, 0x58, 0x6f, 0x7d, 0x3d,
0xde, 0x3f, 0x59, 0x71, 0x03,
}
conn := protocol7.Session{}
packet := protocol7.Packet{}
err := packet.Unpack(dump)
require.NoError(t, err)
conn.Ack = packet.Header.Ack
repack := packet.Pack(&conn)
require.Equal(t, dump, repack)
// TODO: test if snap items were parsed correctly
}

View file

@ -3,6 +3,7 @@ package teeworlds7
import (
"github.com/teeworlds-go/go-teeworlds-protocol/messages7"
"github.com/teeworlds-go/go-teeworlds-protocol/protocol7"
"github.com/teeworlds-go/go-teeworlds-protocol/snapshot7"
)
// Processes the incoming packet
@ -19,6 +20,7 @@ type UserMsgCallbacks struct {
PacketOut func(*protocol7.Packet) bool
MsgUnknown func(*messages7.Unknown, DefaultAction)
InternalError func(error)
Snapshot func(*snapshot7.Snapshot, DefaultAction)
CtrlKeepAlive func(*messages7.CtrlKeepAlive, DefaultAction)
CtrlConnect func(*messages7.CtrlConnect, DefaultAction)

View file

@ -3,6 +3,7 @@ package teeworlds7
import (
"github.com/teeworlds-go/go-teeworlds-protocol/messages7"
"github.com/teeworlds-go/go-teeworlds-protocol/protocol7"
"github.com/teeworlds-go/go-teeworlds-protocol/snapshot7"
)
// --------------------------------
@ -34,6 +35,12 @@ func (client *Client) OnUnknown(callback func(msg *messages7.Unknown, defaultAct
client.Callbacks.MsgUnknown = callback
}
// will be called when a snap, snap single or empty snapshot is received
// if you want to know which type of snapshot was received look at OnMsgSnap(), OnMsgSnapEmpty(), OnMsgSnapSingle(), OnMsgSnapSmall()
func (client *Client) OnSnapshot(callback func(snap *snapshot7.Snapshot, defaultAction DefaultAction)) {
client.Callbacks.Snapshot = callback
}
// --------------------------------
// control messages
// --------------------------------
@ -86,3 +93,23 @@ func (client *Client) OnTeam(callback func(msg *messages7.SvTeam, defaultAction
func (client *Client) OnMapChange(callback func(msg *messages7.MapChange, defaultAction DefaultAction)) {
client.Callbacks.SysMapChange = callback
}
// You probably want to use OnSnapshot() instead
func (client *Client) OnMsgSnap(callback func(msg *messages7.Snap, defaultAction DefaultAction)) {
client.Callbacks.SysSnap = callback
}
// You probably want to use OnSnapshot() instead
func (client *Client) OnMsgSnapEmpty(callback func(msg *messages7.SnapEmpty, defaultAction DefaultAction)) {
client.Callbacks.SysSnapEmpty = callback
}
// You probably want to use OnSnapshot() instead
func (client *Client) OnMsgSnapSingle(callback func(msg *messages7.SnapSingle, defaultAction DefaultAction)) {
client.Callbacks.SysSnapSingle = callback
}
// You probably want to use OnSnapshot() instead
func (client *Client) OnMsgSnapSmall(callback func(msg *messages7.SnapSmall, defaultAction DefaultAction)) {
client.Callbacks.SysSnapSmall = callback
}