package client import ( "log" "time" "go-ts/pkg/protocol" "go-ts/pkg/transport" "gopkg.in/hraban/opus.v2" ) type Channel struct { ID uint64 ParentID uint64 Name string Order uint64 } type Client struct { Conn *transport.TS3Conn Handshake *HandshakeState Nickname string ClientID uint16 // Counters PacketIDCounterC2S uint16 VoicePacketID uint16 // State Connected bool // Fragment reassembly FragmentBuffer []byte FragmentStartPktID uint16 FragmentCompressed bool Fragmenting bool // Server Data Channels map[uint64]*Channel // Audio VoiceDecoders map[uint16]*opus.Decoder // Map VID (sender ID) to decoder VoiceEncoder *opus.Encoder // Encoder for outgoing audio } func NewClient(nickname string) *Client { return &Client{ Nickname: nickname, PacketIDCounterC2S: 1, VoicePacketID: 1, Channels: make(map[uint64]*Channel), VoiceDecoders: make(map[uint16]*opus.Decoder), } } func (c *Client) Connect(address string) error { conn, err := transport.NewTS3Conn(address) if err != nil { return err } c.Conn = conn log.Printf("Connected to UDP. Starting Handshake...") // Initialize Handshake State hs, err := NewHandshakeState(c.Conn) if err != nil { return err } c.Handshake = hs // Improve Identity Security Level to 8 (Standard Requirement) c.Handshake.ImproveSecurityLevel(8) // Send Init1 if err := c.Handshake.SendPacket0(); err != nil { return err } // Listen Loop pktChan := c.Conn.PacketChan() ticker := time.NewTicker(3 * time.Second) defer ticker.Stop() for { select { case pkt := <-pktChan: if err := c.handlePacket(pkt); err != nil { log.Printf("Error handling packet: %v", err) } case <-ticker.C: ping := protocol.NewPacket(protocol.PacketTypePing, nil) c.PacketIDCounterC2S++ ping.Header.PacketID = c.PacketIDCounterC2S ping.Header.ClientID = c.ClientID // Must NOT have NewProtocol (0x20) flag for Pings/Pongs ping.Header.Type = uint8(protocol.PacketTypePing) | protocol.PacketFlagUnencrypted // Use SharedMac if available, otherwise zeros (as per ts3j InitPacketTransformation) if c.Handshake != nil && len(c.Handshake.SharedMac) > 0 { copy(ping.Header.MAC[:], c.Handshake.SharedMac) } else { // Initialize Header.MAC with zeros for i := 0; i < 8; i++ { ping.Header.MAC[i] = 0 } } log.Printf("Sending KeepAlive Ping (PID=%d)", ping.Header.PacketID) c.Conn.SendPacket(ping) } } }