diff --git a/snapshot7/storage.go b/snapshot7/storage.go index 704dd51..37b0de2 100644 --- a/snapshot7/storage.go +++ b/snapshot7/storage.go @@ -5,6 +5,8 @@ import ( "fmt" "slices" "sort" + + "github.com/teeworlds-go/protocol/object7" ) const ( @@ -16,6 +18,7 @@ const ( // can we just put the snap as is in the map? type holder struct { snap *Snapshot + tick int } // TODO: do we need this at all? @@ -24,8 +27,22 @@ type holder struct { // data structure // but in golang users could just define their own map type Storage struct { - holder map[int]*holder + // a backlog of a few snapshots + // kept to unpack new deltas sent by the server + holder map[int]*holder + + // the alt snap is the snapshot + // that should be used for everything gameplay related + // it is the snapshot of the current predicton tick + // and invalid items were already filtered out + // TODO: add prediction ticks and item validation + altSnap holder + + // oldest tick still in the holder + // not the oldest tick we ever received OldestTick int + + // newest tick in the holder NewestTick int // use to store and concatinate data @@ -42,6 +59,32 @@ func NewStorage() *Storage { s.multiPartIncomingData = make([]byte, 0, MaxSize) return s } + +func (s *Storage) AltSnap() (*Snapshot, error) { + if s.altSnap.snap == nil { + return nil, errors.New("there is no alt snap in the storage") + } + return s.altSnap.snap, nil +} + +func (s *Storage) SetAltSnap(tick int, snap *Snapshot) { + s.altSnap.snap = snap + s.altSnap.tick = tick +} + +func (s *Storage) FindAltSnapItem(typeId int, itemId int) (object7.SnapObject, error) { + altSnap, err := s.AltSnap() + if err != nil { + return nil, err + } + key := (typeId << 16) | (itemId & 0xffff) + item := altSnap.GetItemAtKey(key) + if item == nil { + return nil, errors.New("item not found") + } + return *item, nil +} + func (s *Storage) AddIncomingData(part int, numParts int, data []byte) error { if part == 0 { // reset length if we get a new snapshot diff --git a/teeworlds7/callbacks.go b/teeworlds7/callbacks.go index cd28764..7e700dc 100644 --- a/teeworlds7/callbacks.go +++ b/teeworlds7/callbacks.go @@ -37,6 +37,8 @@ func userMsgCallback[T any](userCallbacks []func(T, DefaultAction), msg T, defau // // key is the network7.MessageId // UserMsgCallbacks map[int]UserMsgCallback type UserMsgCallbacks struct { + Tick []func(DefaultAction) + // return false to drop the packet PacketIn []func(*protocol7.Packet) bool diff --git a/teeworlds7/client.go b/teeworlds7/client.go index cc1fe9f..a43f7d6 100644 --- a/teeworlds7/client.go +++ b/teeworlds7/client.go @@ -1,10 +1,14 @@ package teeworlds7 import ( + "errors" "log" "net" + "time" "github.com/teeworlds-go/protocol/messages7" + "github.com/teeworlds-go/protocol/network7" + "github.com/teeworlds-go/protocol/object7" "github.com/teeworlds-go/protocol/protocol7" "github.com/teeworlds-go/protocol/snapshot7" ) @@ -14,9 +18,10 @@ type Player struct { } type Game struct { - Players []Player - Snap *GameSnap - Input *messages7.Input + Players []Player + Snap *GameSnap + Input *messages7.Input + LastSentInput messages7.Input } type Client struct { @@ -34,6 +39,11 @@ type Client struct { // udp connection Conn net.Conn + // when the last packet was sent + // tracked to know when to send keepalives + LastSend time.Time + LastInputSend time.Time + // teeworlds session Session protocol7.Session @@ -42,6 +52,22 @@ type Client struct { // teeworlds game state Game Game + + // might be -1 if we do not know our own id yet + LocalClientId int +} + +// TODO: add this for all items and move it to a different file +func (client *Client) SnapFindCharacter(ClientId int) (*object7.Character, error) { + item, err := client.SnapshotStorage.FindAltSnapItem(network7.ObjCharacter, ClientId) + if err != nil { + return nil, err + } + character, ok := item.(*object7.Character) + if ok == false { + return nil, errors.New("failed to cast character") + } + return character, nil } func NewClient() *Client { @@ -49,9 +75,46 @@ func NewClient() *Client { client.SnapshotStorage = snapshot7.NewStorage() client.Game.Snap = &GameSnap{} client.Game.Input = &messages7.Input{} + client.LocalClientId = -1 + client.LastSend = time.Now() return client } +func (client *Client) sendInputIfNeeded() bool { + diff := time.Now().Sub(client.LastSend) + send := false + // at least every 10hz or on change + if diff.Microseconds() > 1000000 { + send = true + } + if client.Game.LastSentInput != *client.Game.Input { + send = true + } + + if send { + client.SendInput() + } + + return send +} + +func (client *Client) gameTick() { + defaultAction := func() { + if client.sendInputIfNeeded() == true { + return + } + + diff := time.Now().Sub(client.LastSend) + if diff.Seconds() > 2 { + client.SendKeepAlive() + } + } + + for _, callback := range client.Callbacks.Tick { + callback(defaultAction) + } +} + func (client *Client) throwError(err error) { for _, callback := range client.Callbacks.InternalError { if callback(err) == false { diff --git a/teeworlds7/game.go b/teeworlds7/game.go index 0fc5d03..712627a 100644 --- a/teeworlds7/game.go +++ b/teeworlds7/game.go @@ -32,6 +32,9 @@ func (client *Client) processGame(netMsg messages7.NetMessage, response *protoco case *messages7.SvClientInfo: userMsgCallback(client.Callbacks.GameSvClientInfo, msg, func() { client.Game.Players[msg.ClientId].Info = *msg + if msg.Local { + client.LocalClientId = msg.ClientId + } fmt.Printf("got client info id=%d name=%s\n", msg.ClientId, msg.Name) }) case *messages7.SvReadyToEnter: diff --git a/teeworlds7/networking.go b/teeworlds7/networking.go index 26d6cb8..fe9ffb0 100644 --- a/teeworlds7/networking.go +++ b/teeworlds7/networking.go @@ -78,7 +78,7 @@ func (client *Client) Connect(serverIp string, serverPort int) { client.throwError(err) } default: - // do nothing + client.gameTick() } } } diff --git a/teeworlds7/send_message_hooks.go b/teeworlds7/send_message_hooks.go index ae8fca3..2791902 100644 --- a/teeworlds7/send_message_hooks.go +++ b/teeworlds7/send_message_hooks.go @@ -2,6 +2,7 @@ package teeworlds7 import ( "fmt" + "time" "github.com/teeworlds-go/protocol/messages7" ) @@ -122,6 +123,8 @@ func (client *Client) registerMessagesCallbacks(messages []messages7.NetMessage) if userSendMsgCallback(client.Callbacks.SysInputOut, castMsg) == false { continue } + client.Game.LastSentInput = *castMsg + client.LastInputSend = time.Now() case *messages7.RconCmd: if userSendMsgCallback(client.Callbacks.SysRconCmdOut, castMsg) == false { continue diff --git a/teeworlds7/system.go b/teeworlds7/system.go index ee12593..34f0d75 100644 --- a/teeworlds7/system.go +++ b/teeworlds7/system.go @@ -108,6 +108,7 @@ func (client *Client) processSystem(netMsg messages7.NetMessage, response *proto client.Game.Input.AckGameTick = msg.GameTick client.Game.Input.PredictionTick = client.SnapshotStorage.NewestTick client.Game.Snap.fill(newFullSnap) + client.SnapshotStorage.SetAltSnap(msg.GameTick, newFullSnap) response.Messages = append(response.Messages, client.Game.Input) }) @@ -149,7 +150,9 @@ func (client *Client) processSystem(netMsg messages7.NetMessage, response *proto client.Game.Input.AckGameTick = msg.GameTick client.Game.Input.PredictionTick = client.SnapshotStorage.NewestTick client.Game.Snap.fill(newFullSnap) + client.SnapshotStorage.SetAltSnap(msg.GameTick, newFullSnap) + client.SendInput() response.Messages = append(response.Messages, client.Game.Input) }) case *messages7.SnapEmpty: diff --git a/teeworlds7/user_actions.go b/teeworlds7/user_actions.go index 68c9d43..1c44d52 100644 --- a/teeworlds7/user_actions.go +++ b/teeworlds7/user_actions.go @@ -2,6 +2,7 @@ package teeworlds7 import ( "fmt" + "time" "github.com/teeworlds-go/protocol/messages7" "github.com/teeworlds-go/protocol/network7" @@ -80,6 +81,7 @@ func (client *Client) SendPacket(packet *protocol7.Packet) error { } } + client.LastSend = time.Now() client.Conn.Write(packet.Pack(&client.Session)) return nil } @@ -128,39 +130,39 @@ func (client *Client) SendInput() { func (client *Client) Right() { client.Game.Input.Direction = 1 - client.SendInput() + // client.SendInput() } func (client *Client) Left() { client.Game.Input.Direction = -1 - client.SendInput() + // client.SendInput() } func (client *Client) Stop() { client.Game.Input.Direction = 0 - client.SendInput() + // client.SendInput() } func (client *Client) Jump() { client.Game.Input.Jump = 1 - client.SendInput() + // client.SendInput() } func (client *Client) Hook() { client.Game.Input.Hook = 1 - client.SendInput() + // client.SendInput() } func (client *Client) Fire() { // TODO: fire is weird do we ever have to reset or mask it or something? client.Game.Input.Fire++ - client.SendInput() + // client.SendInput() } func (client *Client) Aim(x int, y int) { client.Game.Input.TargetX = x client.Game.Input.TargetY = y - client.SendInput() + // client.SendInput() } // see also SendWhisper() @@ -198,3 +200,7 @@ func (client *Client) SendWhisper(targetId int, msg string) { }, ) } + +func (client *Client) SendKeepAlive() { + client.SendMessage(&messages7.CtrlKeepAlive{}) +} diff --git a/teeworlds7/user_hooks.go b/teeworlds7/user_hooks.go index 26b4271..38f56e6 100644 --- a/teeworlds7/user_hooks.go +++ b/teeworlds7/user_hooks.go @@ -10,6 +10,10 @@ import ( // special cases // -------------------------------- +func (client *Client) OnTick(callback func(defaultAction DefaultAction)) { + client.Callbacks.Tick = append(client.Callbacks.Tick, callback) +} + // if not implemented by the user the application might throw and exit // // return false to drop the error