package client import ( "log" "sync" "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 } // EventHandler is called when events occur type EventHandler func(eventType string, data map[string]any) type Client struct { Conn *transport.TS3Conn Handshake *HandshakeState Nickname string ClientID uint16 // Counters PacketIDCounterC2S uint16 VoicePacketID uint16 // State Connected bool ServerName string // Fragment reassembly (packet queue like ts3j) CommandQueue map[uint16]*protocol.Packet // Packets waiting for reassembly ExpectedCommandPID uint16 // Next expected packet ID FragmentState bool // Toggle: true = collecting, false = ready // 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 VoiceEncoderMu sync.Mutex // Protects VoiceEncoder // Event handler for public API eventHandler EventHandler // Done channel to signal shutdown done chan struct{} } func NewClient(nickname string) *Client { return &Client{ Nickname: nickname, PacketIDCounterC2S: 1, VoicePacketID: 1, Channels: make(map[uint64]*Channel), VoiceDecoders: make(map[uint16]*opus.Decoder), CommandQueue: make(map[uint16]*protocol.Packet), ExpectedCommandPID: 0, done: make(chan struct{}), } } // SetEventHandler sets the callback for events func (c *Client) SetEventHandler(handler EventHandler) { c.eventHandler = handler } // emitEvent sends an event to the handler if set func (c *Client) emitEvent(eventType string, data map[string]any) { if c.eventHandler != nil { c.eventHandler(eventType, data) } } // Stop signals the client to stop its loops func (c *Client) Stop() { select { case <-c.done: // Already closed default: close(c.done) } } 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 <-c.done: log.Println("Client loop stopped") return nil case pkt := <-pktChan: if pkt == nil { // Channel closed return nil } if err := c.handlePacket(pkt); err != nil { log.Printf("Error handling packet: %v", err) } case <-ticker.C: if !c.Connected { continue // Don't send pings if not connected yet } 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) } } }