// ABOUTME: Message dispatch for incoming client control frames // ABOUTME: Routes client/time, client/state, client/goodbye, client/command to handlers package sendspin import ( "encoding/json" "log" "github.com/Sendspin/sendspin-go/pkg/protocol" ) func (s *Server) handleClientMessage(c *ServerClient, data []byte) { var msg protocol.Message if err := json.Unmarshal(data, &msg); err != nil { log.Printf("Error unmarshaling message: %v", err) return } switch msg.Type { case "client/time": s.handleTimeSync(c, msg.Payload) case "client/state": s.handleClientState(c, msg.Payload) case "client/goodbye": s.handleClientGoodbye(c, msg.Payload) case "client/command": s.handleClientCommand(c, msg.Payload) default: if s.config.Debug { log.Printf("Unknown message type: %s", msg.Type) } } } func (s *Server) handleTimeSync(c *ServerClient, payload interface{}) { serverRecv := s.getClockMicros() timeData, err := json.Marshal(payload) if err != nil { return } var clientTime protocol.ClientTime if err := json.Unmarshal(timeData, &clientTime); err != nil { return } serverSend := s.getClockMicros() response := protocol.ServerTime{ ClientTransmitted: clientTime.ClientTransmitted, ServerReceived: serverRecv, ServerTransmitted: serverSend, } if err := c.Send("server/time", response); err != nil { if s.config.Debug { log.Printf("Error sending server/time to %s: %v", c.name, err) } } } // handleClientState applies a client's player state update per spec. func (s *Server) handleClientState(c *ServerClient, payload interface{}) { stateData, err := json.Marshal(payload) if err != nil { return } var stateMsg protocol.ClientStateMessage if err := json.Unmarshal(stateData, &stateMsg); err != nil { return } if stateMsg.Player != nil { c.mu.Lock() c.state = stateMsg.Player.State c.volume = stateMsg.Player.Volume c.muted = stateMsg.Player.Muted c.mu.Unlock() if s.config.Debug { log.Printf("Client %s state: %s (vol: %d, muted: %v)", c.name, stateMsg.Player.State, stateMsg.Player.Volume, stateMsg.Player.Muted) } s.defaultGroup.publish(ClientStateChangedEvent{ Client: c, State: stateMsg.Player.State, Volume: stateMsg.Player.Volume, Muted: stateMsg.Player.Muted, }) } } func (s *Server) handleClientCommand(c *ServerClient, payload interface{}) { data, err := json.Marshal(payload) if err != nil { log.Printf("Error marshaling client/command payload: %v", err) return } // client/command payloads are role-keyed: {"controller": {...}, "player": {...}} var rolePayloads map[string]json.RawMessage if err := json.Unmarshal(data, &rolePayloads); err != nil { log.Printf("Error unmarshaling client/command: %v", err) return } for roleFamily, roleData := range rolePayloads { if err := s.defaultGroup.RouteMessage(c, roleFamily, roleData); err != nil { if s.config.Debug { log.Printf("client/command routing error for role %s: %v", roleFamily, err) } } } } func (s *Server) handleClientGoodbye(c *ServerClient, payload interface{}) { goodbyeData, err := json.Marshal(payload) if err != nil { return } var goodbye protocol.ClientGoodbye if err := json.Unmarshal(goodbyeData, &goodbye); err != nil { return } log.Printf("Client %s goodbye: %s", c.name, goodbye.Reason) // Connection close happens in handleConnection's read loop once this returns. }