package main import ( "bytes" "crypto/rand" "crypto/rsa" "crypto/tls" "crypto/x509" "crypto/x509/pkix" _ "embed" "encoding/base64" "encoding/binary" "encoding/json" "fmt" "io" "log" "math" "math/big" "net" "net/http" "time" ) //go:embed mock_gamestatus.bin var mockGameStatusBytes []byte //go:embed mock_gamedata.b64 var mockGameDataBase64 string const ( MockUserID = 8381763 MockUserName = "CustomPlayer" ) // User Details Response Structures type UserDetailsResponse struct { Error bool `json:"error"` Status int `json:"status"` Data UserDetailsData `json:"data"` } type UserDetailsData struct { User UserDetails `json:"user"` } type PartnerData struct { PartnerID int `json:"partner_id"` PartnerUserID string `json:"partner_user_id"` CreatedAt string `json:"created_at"` } type UserDetails struct { UserID int `json:"user_id"` LoginName string `json:"login_name"` Email string `json:"email"` Name string `json:"name"` EmailValid bool `json:"email_valid"` Validated bool `json:"validated"` Country string `json:"country"` Language string `json:"language"` TimeZone string `json:"time_zone"` PostedMsgCount int `json:"posted_msg_count"` Features []string `json:"features"` Partners []PartnerData `json:"partners"` BoardGames []interface{} `json:"boardgames"` OnlineGames []interface{} `json:"onlinegames"` Avatar string `json:"avatar"` } // User Search Response Structures type UserSearchResponse struct { Error bool `json:"error"` Status int `json:"status"` Data UserSearchData `json:"data"` } type UserSearchData struct { Total int `json:"total"` Links map[string]interface{} `json:"_links"` Users []SearchUser `json:"users"` } type SearchUser struct { UserID int `json:"user_id"` LoginName string `json:"login_name"` Avatar string `json:"avatar"` Features []string `json:"features"` BoardGames []interface{} `json:"boardgames"` OnlineGames []interface{} `json:"onlinegames"` } var ( // Global state to track if we are in a game (simplistic single-user view for now) hasActiveGame bool = false activeGameID int64 = 0 ) func main() { mux := http.NewServeMux() mux.HandleFunc("/main/v2/oauth/token", handleToken) mux.HandleFunc("/main/v1/user/me", handleUserMe) mux.HandleFunc("/main/v1/user/me/link", handleLink) mux.HandleFunc("/main/v1/users", handleUserSearch) mux.HandleFunc("/main/v3/showcase/games/steam/es", handleShowcase) mux.HandleFunc("/main/v1/user/me/buddies", handleBuddies) mux.HandleFunc("/main/v1/user/me/lastopponents/GOTDBG", handleLastOpponents) // Catch-all to log other requests and return empty JSON instead of text 404 mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Printf("[HTTP] [UNKNOWN] Request to %s (%s) from %s - Full URI: %s\n", r.URL.Path, r.Method, r.RemoteAddr, r.RequestURI) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) w.Write([]byte(`{"error":true,"status":404,"message":"Not found"}`)) }) // 1. HTTP Server for REST API (Port 8080) go func() { fmt.Println("[HTTP] REST API: http://localhost:8080") if err := http.ListenAndServe(":8080", mux); err != nil { log.Fatalf("HTTP server failed: %v", err) } }() // 2. TCP Server for Scalable Server (Port 3000) with SSL tlsConfig, err := generateSelfSignedCert() if err != nil { log.Fatalf("Failed to generate self-signed cert: %v", err) } listener, err := tls.Listen("tcp", ":3000", tlsConfig) if err != nil { log.Fatalf("TCP TLS listener failed: %v", err) } fmt.Println("[TCP] Scalable Server (TLS): localhost:3000") for { conn, err := listener.Accept() if err != nil { fmt.Printf("Accept error: %v\n", err) continue } go handleTCPConnection(conn) } } func generateSelfSignedCert() (*tls.Config, error) { priv, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return nil, err } template := x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{ Organization: []string{"Custom Server Mod"}, }, NotBefore: time.Now(), NotAfter: time.Now().Add(time.Hour * 24 * 365), KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, DNSNames: []string{"localhost"}, } derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) if err != nil { return nil, err } cert := tls.Certificate{ Certificate: [][]byte{derBytes}, PrivateKey: priv, } return &tls.Config{Certificates: []tls.Certificate{cert}}, nil } func handleToken(w http.ResponseWriter, r *http.Request) { fmt.Printf("[HTTP] Request to %s from %s\n", r.URL.Path, r.RemoteAddr) response := map[string]interface{}{ "access_token": "mock_access_token_12345", "token_type": "bearer", "expires_in": 3600, "refresh_token": "mock_refresh_token_67890", "scope": "public private boardgames onlinegames partners features", } w.Header().Set("Content-Type", "application/json") data, _ := json.Marshal(response) w.Write(data) } func handleUserMe(w http.ResponseWriter, r *http.Request) { fmt.Printf("[HTTP] Request to %s from %s\n", r.URL.Path, r.RemoteAddr) response := UserDetailsResponse{ Error: false, Status: 200, Data: UserDetailsData{ User: UserDetails{ UserID: MockUserID, LoginName: MockUserName, Email: "player@customserver.local", Name: MockUserName, EmailValid: true, Validated: true, Country: "US", Language: "en", TimeZone: "UTC", PostedMsgCount: 42, Features: []string{"online_play", "all_expansions", "community", "profile", "userpages"}, Partners: []PartnerData{ { PartnerID: 12, // Steam PartnerUserID: "76561198084728812", CreatedAt: "2026-01-01T00:00:00Z", }, }, BoardGames: []interface{}{}, OnlineGames: []interface{}{}, Avatar: "https://uploads.asmodee.net/builtin/avatar-neutral.jpg", }, }, } w.Header().Set("Content-Type", "application/json") data, _ := json.Marshal(response) w.Write(data) } func handleLink(w http.ResponseWriter, r *http.Request) { fmt.Printf("[HTTP] Request to %s (%s) from %s\n", r.URL.Path, r.Method, r.RemoteAddr) response := map[string]interface{}{ "error": false, "status": 200, "data": map[string]interface{}{}, } w.Header().Set("Content-Type", "application/json") data, _ := json.Marshal(response) w.Write(data) } func handleUserSearch(w http.ResponseWriter, r *http.Request) { fmt.Printf("[HTTP] Request to %s from %s\n", r.URL.Path, r.RemoteAddr) response := UserSearchResponse{ Error: false, Status: 200, Data: UserSearchData{ Total: 1, Links: map[string]interface{}{ "first": nil, "last": nil, "next": nil, "prev": nil, }, Users: []SearchUser{ { UserID: MockUserID, LoginName: MockUserName, Avatar: "https://uploads.asmodee.net/builtin/avatar-neutral.jpg", Features: []string{"online_play", "all_expansions"}, BoardGames: []interface{}{}, OnlineGames: []interface{}{}, }, }, }, } w.Header().Set("Content-Type", "application/json") data, _ := json.Marshal(response) w.Write(data) } func handleShowcase(w http.ResponseWriter, r *http.Request) { fmt.Printf("[HTTP] Request to %s (%s) from %s\n", r.URL.Path, r.Method, r.RemoteAddr) response := map[string]interface{}{ "error": false, "status": 200, "data": []interface{}{}, } w.Header().Set("Content-Type", "application/json") data, _ := json.Marshal(response) w.Write(data) } func handleBuddies(w http.ResponseWriter, r *http.Request) { fmt.Printf("[HTTP] Request to %s from %s\n", r.URL.Path, r.RemoteAddr) response := map[string]interface{}{ "error": false, "status": 200, "data": map[string]interface{}{ "total": 0, "buddies": []interface{}{}, "_links": map[string]interface{}{ "next": nil, "prev": nil, "first": nil, "last": nil, }, }, } w.Header().Set("Content-Type", "application/json") data, _ := json.Marshal(response) w.Write(data) } func handleLastOpponents(w http.ResponseWriter, r *http.Request) { fmt.Printf("[HTTP] Request to %s from %s\n", r.URL.Path, r.RemoteAddr) // Needs to match UserOnlineGameGetRecentOpponentsDataJson structure // { "data": { "opponents": [] } } response := map[string]interface{}{ "data": map[string]interface{}{ "opponents": []interface{}{}, }, } w.Header().Set("Content-Type", "application/json") data, _ := json.Marshal(response) w.Write(data) } func getMockPlayerBytes() []byte { player := make([]byte, 0) // field 1 (Name), string player = append(player, 0x0a) player = append(player, encodeVarint(uint64(len(MockUserName)))...) player = append(player, MockUserName...) // field 2 (Id), varint player = append(player, 0x10) player = append(player, encodeVarint(uint64(MockUserID))...) // field 5 (Karma), varint player = append(player, 0x28) player = append(player, encodeVarint(52)...) // field 7 (RankScore), fixed64 (WireType 1) - double player = append(player, 0x39) // (7 << 3) | 1 = 57 = 0x39 player = append(player, encodeDouble(1500.0)...) // field 9 (WWWId), varint player = append(player, 0x48) player = append(player, encodeVarint(uint64(MockUserID))...) // field 11 (nbGames), varint player = append(player, 0x58) player = append(player, encodeVarint(2)...) // field 12 (Banned), varint player = append(player, 0x60) player = append(player, encodeVarint(0)...) // False // field 14 (Avatar), message avatarMsg := make([]byte, 0) avatarUrl := "https://uploads.asmodee.net/builtin/avatar-neutral.jpg" // Avatar.Image (Field 3), String avatarMsg = append(avatarMsg, 0x1a) avatarMsg = append(avatarMsg, encodeVarint(uint64(len(avatarUrl)))...) avatarMsg = append(avatarMsg, avatarUrl...) player = append(player, 0x72) // Field 14, WireType 2 player = append(player, encodeVarint(uint64(len(avatarMsg)))...) player = append(player, avatarMsg...) // field 15 (Language), varint player = append(player, 0x78) player = append(player, encodeVarint(1)...) // field 16 (Tz), string player = append(player, 0x82, 0x01) // Field 16 (128 + 2), WireType 2 player = append(player, encodeVarint(3)...) player = append(player, "UTC"...) return player } func getMockIronHumanPlayerBytes() []byte { // IronHumanPlayer // Field 1: PlayerId (int32) // Field 2: House (Enum) -> Stark (0) data := make([]byte, 0) // PlayerId data = append(data, 0x08) data = append(data, encodeVarint(uint64(MockUserID))...) // House (0 is default, we skip it) return data } func getMockIronGameConfigurationBytes() []byte { // IronGameConfiguration // Field 1: TotalPlayers (int32) -> 6 // Field 7: HumanPlayers (repeated IronHumanPlayer) // Field 8: CreatedBy (int64) data := make([]byte, 0) // TotalPlayers = 6 data = append(data, 0x08) data = append(data, encodeVarint(6)...) // HumanPlayers (repeated) humanPlayer := getMockIronHumanPlayerBytes() data = append(data, 0x3a) // Field 7, WireType 2 data = append(data, encodeVarint(uint64(len(humanPlayer)))...) data = append(data, humanPlayer...) // CreatedBy data = append(data, 0x40) // Field 8, WireType 0 data = append(data, encodeVarint(uint64(MockUserID))...) return data } func getMockIronGameStateBytes() []byte { // IronGameState // Field 1: GameId (int64) // Field 7: Configuration (IronGameConfiguration) data := make([]byte, 0) // GameId data = append(data, 0x08) data = append(data, encodeVarint(12345)...) // Configuration config := getMockIronGameConfigurationBytes() data = append(data, 0x3a) // Field 7, WireType 2 data = append(data, encodeVarint(uint64(len(config)))...) data = append(data, config...) return data } func handleTCPConnection(conn net.Conn) { defer conn.Close() fmt.Printf("[TCP] New TLS connection from %s\n", conn.RemoteAddr()) for { // Read length (4 bytes, Big Endian) var length uint32 err := binary.Read(conn, binary.BigEndian, &length) if err != nil { if err != io.EOF { fmt.Printf("[TCP] Read length error: %v\n", err) } return } // Read packet data := make([]byte, length) _, err = io.ReadFull(conn, data) if err != nil { fmt.Printf("[TCP] Read data error: %v\n", err) return } fmt.Printf("[TCP] Received packet of %d bytes\n", length) packetID := int64(0) requestNumber := int32(0) var wrapperRequestNumber uint64 var payloadBytes []byte reader := bytes.NewReader(data) for { tag, err := readVarint(reader) if err != nil { break } fieldNum := tag >> 3 wireType := tag & 0x7 if fieldNum == 1 && wireType == 0 { // Packet.id packetID, _ = readVarintInt64(reader) } else if fieldNum == 3 && wireType == 2 { // Packet.payload (Message) payloadLen, _ := readVarint(reader) payloadBytes = make([]byte, payloadLen) reader.Read(payloadBytes) payloadReader := bytes.NewReader(payloadBytes) for { pTag, err := readVarint(payloadReader) if err != nil { break } pFieldNum := pTag >> 3 pWireType := pTag & 0x7 if pFieldNum == 1 && pWireType == 0 { // Message.request_number reqNum64, _ := readVarintInt64(payloadReader) requestNumber = int32(reqNum64) } else { skipField(payloadReader, pWireType) } } } else { skipField(reader, wireType) } } fmt.Printf("[TCP] Got Request ID: %d, Number: %d\n", packetID, requestNumber) var responsePayload []byte var responseFieldNum int switch requestNumber { case 400: // AsyncAuthRequest fmt.Println("[TCP] Handling AsyncAuthRequest") // AsyncConnectedRequest (406) // Matches official: "4016461897007108096" (string) // In official log: session: { id: "..." } // This means Session is a message with id (field 1 probably) as String. // Session Message sessionData := make([]byte, 0) // field 1 (Id), Varint (WireType 0) sessionData = append(sessionData, 0x08) sessionData = append(sessionData, encodeVarint(uint64(time.Now().UnixNano()))...) // Player Message player := getMockPlayerBytes() // AsyncConnectedRequest asyncConnected := make([]byte, 0) // field 1 (Session), message asyncConnected = append(asyncConnected, 0x0a) asyncConnected = append(asyncConnected, encodeVarint(uint64(len(sessionData)))...) asyncConnected = append(asyncConnected, sessionData...) // field 2 (Player), message asyncConnected = append(asyncConnected, 0x12) asyncConnected = append(asyncConnected, encodeVarint(uint64(len(player)))...) asyncConnected = append(asyncConnected, player...) responsePayload = asyncConnected responseFieldNum = 406 case 408: // AskServerStatisticsRequest fmt.Println("[TCP] Handling AskServerStatisticsRequest") // Return ServerStatisticsRequest (409) // Fields: 1:HostedGames, 2:Players, 3:ConnectedPlayers (all int32) stats := make([]byte, 0) // Field 1: HostedGames (Varint) stats = append(stats, 0x08) stats = append(stats, encodeVarint(1)...) // Field 2: Players (Varint) stats = append(stats, 0x10) stats = append(stats, encodeVarint(1)...) // Field 3: ConnectedPlayers (Varint) stats = append(stats, 0x18) stats = append(stats, encodeVarint(1)...) responsePayload = stats responseFieldNum = 409 case 515: // AsyncBuddyListRequest fmt.Println("[TCP] Handling AsyncBuddyListRequest") // Return AsyncBuddyListContentRequest (516) // Field 2: Buddies (AsyncBuddyList) // AsyncBuddyList has repeated fields, so empty message is fine. // We need to provide Field 2 with length 0 to ensure 'Buddies' object is created in C#. responsePayload = []byte{0x12, 0x00} responseFieldNum = 516 case 560: // AsyncIgnoreListRequest fmt.Println("[TCP] Handling AsyncIgnoreListRequest") // Return AsyncIgnoreListContentRequest (561) // Field 2: Ignores (AsyncBuddyList) - Assuming same structure as BuddyList based on typical reuse responsePayload = []byte{0x12, 0x00} responseFieldNum = 561 case 401: // AsyncDisconnectRequest fmt.Println("[TCP] Handling AsyncDisconnectRequest") responsePayload = []byte{} responseFieldNum = 401 case 600: // EnterLobbyRequest fmt.Println("[TCP] Handling EnterLobbyRequest") responsePayload = []byte{} responseFieldNum = 601 case 604: // LobbyPlayerListRequest fmt.Println("[TCP] Handling LobbyPlayerListRequest") // Field 1: repeated Player mockPlayer := getMockPlayerBytes() responsePayload = make([]byte, 0) responsePayload = append(responsePayload, 0x0a) // Field 1, WireType 2 responsePayload = append(responsePayload, encodeVarint(uint64(len(mockPlayer)))...) responsePayload = append(responsePayload, mockPlayer...) responseFieldNum = 604 case 609: // LobbyGameListRequest fmt.Println("[TCP] Handling LobbyGameListRequest") // Return a list containing our single mock game // Field 1: repeated GameDetails // Construct GameDetails (Message) // Field 1: GameId (int64) // Field 2: Players (repeated Player) // Field 3: Configuration (GameConfiguration) gameData := make([]byte, 0) gameData = append(gameData, 0x08) gameData = append(gameData, encodeVarint(4016461897007108096)...) // Players (Field 2) mockPlayer := getMockPlayerBytes() gameData = append(gameData, 0x12) gameData = append(gameData, encodeVarint(uint64(len(mockPlayer)))...) gameData = append(gameData, mockPlayer...) // Configuration (Field 3) // Mock Config for List mockConfig := make([]byte, 0) // Name mockConfig = append(mockConfig, 0x0a) mockConfig = append(mockConfig, encodeVarint(uint64(len("Partida de CustomPlayer")))...) mockConfig = append(mockConfig, "Partida de CustomPlayer"...) // Min/Max Players mockConfig = append(mockConfig, 0x28) mockConfig = append(mockConfig, encodeVarint(3)...) mockConfig = append(mockConfig, 0x30) mockConfig = append(mockConfig, encodeVarint(6)...) gameData = append(gameData, 0x1a) gameData = append(gameData, encodeVarint(uint64(len(mockConfig)))...) gameData = append(gameData, mockConfig...) // Now wrap in LobbyGameListRequest (Field 1 repeated) responsePayload = make([]byte, 0) responsePayload = append(responsePayload, 0x0a) responsePayload = append(responsePayload, encodeVarint(uint64(len(gameData)))...) responsePayload = append(responsePayload, gameData...) responseFieldNum = 609 case 622: // ObservableGameListRequest fmt.Println("[TCP] Handling ObservableGameListRequest") // Similar to 609, return list of games // Field 1: repeated GameDetails // Reuse logic from 609 implicitly by copy-paste for safety // Construct GameDetails (Message) gameData := make([]byte, 0) gameData = append(gameData, 0x08) gameData = append(gameData, encodeVarint(4016461897007108096)...) // Players (Field 2) mockPlayer := getMockPlayerBytes() gameData = append(gameData, 0x12) gameData = append(gameData, encodeVarint(uint64(len(mockPlayer)))...) gameData = append(gameData, mockPlayer...) // Configuration (Field 3) mockConfig := make([]byte, 0) mockConfig = append(mockConfig, 0x0a) mockConfig = append(mockConfig, encodeVarint(uint64(len("Partida de CustomPlayer")))...) mockConfig = append(mockConfig, "Partida de CustomPlayer"...) mockConfig = append(mockConfig, 0x28) mockConfig = append(mockConfig, encodeVarint(3)...) mockConfig = append(mockConfig, 0x30) mockConfig = append(mockConfig, encodeVarint(6)...) gameData = append(gameData, 0x1a) gameData = append(gameData, encodeVarint(uint64(len(mockConfig)))...) gameData = append(gameData, mockConfig...) responsePayload = make([]byte, 0) responsePayload = append(responsePayload, 0x0a) responsePayload = append(responsePayload, encodeVarint(uint64(len(gameData)))...) responsePayload = append(responsePayload, gameData...) responseFieldNum = 622 // ... inside main() function ... case 511: // WhatsNewPussycatRequest fmt.Println("[TCP] Handling WhatsNewPussycatRequest") if hasActiveGame { fmt.Printf("[TCP] StatusReport: Reporting Active Game %d\n", activeGameID) // StatusReport Message (Inner) innerReport := make([]byte, 0) // Field 1: GameId (int64) innerReport = append(innerReport, 0x08) innerReport = append(innerReport, encodeVarint(uint64(activeGameID))...) // Field 2: Status (Enum) = 1 (IN_PROGRESS) innerReport = append(innerReport, 0x10) innerReport = append(innerReport, encodeVarint(1)...) // Field 3: Data (Bytes) - From mock_gamedata.b64 dataBytes, _ := base64.StdEncoding.DecodeString(mockGameDataBase64) innerReport = append(innerReport, 0x1a) innerReport = append(innerReport, encodeVarint(uint64(len(dataBytes)))...) innerReport = append(innerReport, dataBytes...) // Field 4: TurnId (int32) = 4 innerReport = append(innerReport, 0x20) innerReport = append(innerReport, encodeVarint(4)...) // Field 5: NextPlayerIds (Repeated Int32) // 0x2A = Field 5 (0010 1) | WireType 2 nextPlayers := []uint64{uint64(MockUserID)} npBytes := make([]byte, 0) for _, pid := range nextPlayers { npBytes = append(npBytes, encodeVarint(pid)...) } innerReport = append(innerReport, 0x2a) innerReport = append(innerReport, encodeVarint(uint64(len(npBytes)))...) innerReport = append(innerReport, npBytes...) // Field 6: Players (Repeated Player) mockPlayer := getMockPlayerBytes() innerReport = append(innerReport, 0x32) innerReport = append(innerReport, encodeVarint(uint64(len(mockPlayer)))...) innerReport = append(innerReport, mockPlayer...) // Field 14: Configuration (GameConfiguration) fallbackConfig := make([]byte, 0) name := "Active Game Details" fallbackConfig = append(fallbackConfig, 0x0a) fallbackConfig = append(fallbackConfig, encodeVarint(uint64(len(name)))...) fallbackConfig = append(fallbackConfig, name...) fallbackConfig = append(fallbackConfig, 0x10, 0x00, 0x18, 0x01, 0x20, 0x00) // MinPlayers=1 fallbackConfig = append(fallbackConfig, 0x28, 0x01) // MaxPlayers=4 fallbackConfig = append(fallbackConfig, 0x30, 0x04) // GameMode=0 fallbackConfig = append(fallbackConfig, 0x48, 0x00, 0x50, 0x2d) innerReport = append(innerReport, 0x72) innerReport = append(innerReport, encodeVarint(uint64(len(fallbackConfig)))...) innerReport = append(innerReport, fallbackConfig...) // Field 16: ActivePlayer (Int32) innerReport = append(innerReport, 0x80, 0x01) innerReport = append(innerReport, encodeVarint(uint64(MockUserID))...) // Wrap in GameStatusReportRequest (Field 1: repeated StatusReport) // Note: Field 1 of GameStatusReportRequest is 'reports', which is REPEATED StatusReport. // Since we only have one game, we encode one StatusReport message. // Protobuf for repeated message: Tag, Length, Data, Tag, Length, Data... (if not packed? Messages usually not packed?) // Wait, repeated messages are repeated occurrences of {Tag, Length, Data}. // So we append 0x0a [Length] [innerReport]. responsePayload = make([]byte, 0) responsePayload = append(responsePayload, 0x0a) responsePayload = append(responsePayload, encodeVarint(uint64(len(innerReport)))...) responsePayload = append(responsePayload, innerReport...) } else { // Use the embedded binary blob which mimics the official server's complex response (Idle/Lobby) // But we should verify if this blob contains "GameId: 12345" which might confuse things. // For safety, let's use it as is for now if no game is active. fmt.Println("[TCP] StatusReport: Reporting Idle/Lobby State (Embedded)") responsePayload = mockGameStatusBytes } responseFieldNum = 512 wrapperRequestNumber = uint64(packetID) // ... existing handlers ... case 607: // LobbyCreateGameRequest fmt.Println("[TCP] Handling LobbyCreateGameRequest") // The payloadBytes we have here are the 'Message' wrapper. // Field 607 of 'Message' is 'LobbyCreateGameRequest'. // Field 1 of 'LobbyCreateGameRequest' is 'Configuration' (GameConfiguration). var configBytes []byte // 1. Scan 'Message' wrapper for Field 607 msgReader := bytes.NewReader(payloadBytes) var createGameReqBytes []byte for { tag, err := readVarint(msgReader) if err != nil { break } fieldNum := tag >> 3 wireType := tag & 0x7 if fieldNum == 607 && wireType == 2 { length, _ := readVarint(msgReader) createGameReqBytes = make([]byte, length) msgReader.Read(createGameReqBytes) break // Found the request message } else { skipField(msgReader, wireType) } } // 2. Scan 'LobbyCreateGameRequest' for Field 1 (Configuration) if len(createGameReqBytes) > 0 { reqReader := bytes.NewReader(createGameReqBytes) for { tag, err := readVarint(reqReader) if err != nil { break } fieldNum := tag >> 3 wireType := tag & 0x7 if fieldNum == 1 && wireType == 2 { length, _ := readVarint(reqReader) configBytes = make([]byte, length) reqReader.Read(configBytes) break // Found the configuration } else { skipField(reqReader, wireType) } } } // Update Global State hasActiveGame = true activeGameID = 4016461897007108096 // Mock ID // Construct GameDetails (Message) // Field 1: GameId (int64) // Field 2: Players (repeated Player) // Field 3: Configuration (GameConfiguration) // Field 5: UserData (repeated PlayerPregameData) <--- NEW gameDetails := make([]byte, 0) // GameId gameDetails = append(gameDetails, 0x08) gameDetails = append(gameDetails, encodeVarint(uint64(activeGameID))...) // Players (Field 2) mockPlayer := getMockPlayerBytes() gameDetails = append(gameDetails, 0x12) gameDetails = append(gameDetails, encodeVarint(uint64(len(mockPlayer)))...) gameDetails = append(gameDetails, mockPlayer...) // Configuration (Field 3) if len(configBytes) > 0 { fmt.Println("[TCP] CreateGame: Successfully extracted Configuration. Forcing MinPlayers=1") // Force MinPlayers (Field 5) to 1 by appending it to the end (Last Write Wins strategy) // Field 5 (0010 1000) = 0x28 configBytes = append(configBytes, 0x28, 0x01) gameDetails = append(gameDetails, 0x1a) gameDetails = append(gameDetails, encodeVarint(uint64(len(configBytes)))...) gameDetails = append(gameDetails, configBytes...) } else { // ... (Fallback Config) ... fmt.Println("[TCP] CreateGame: Using Fallback Configuration") fallbackConfig := make([]byte, 0) // 1. Name name := "Fallback Game" fallbackConfig = append(fallbackConfig, 0x0a) fallbackConfig = append(fallbackConfig, encodeVarint(uint64(len(name)))...) fallbackConfig = append(fallbackConfig, name...) // 2. Private, 3. Lurkable, etc fallbackConfig = append(fallbackConfig, 0x10, 0x00, 0x18, 0x01, 0x20, 0x00) // 5. MinPlayers (Int32) - FORCE TO 1 fallbackConfig = append(fallbackConfig, 0x28) fallbackConfig = append(fallbackConfig, encodeVarint(1)...) // 6. MaxPlayers (Int32) - 6 fallbackConfig = append(fallbackConfig, 0x30) fallbackConfig = append(fallbackConfig, encodeVarint(6)...) // 9. GameMode (Enum) - Default (0) fallbackConfig = append(fallbackConfig, 0x48, 0x00, 0x50, 0x2d) // Mode 0, Timeout 45 gameDetails = append(gameDetails, 0x1a) gameDetails = append(gameDetails, encodeVarint(uint64(len(fallbackConfig)))...) gameDetails = append(gameDetails, fallbackConfig...) } // UserData (Field 5, Repeated PlayerPregameData) // PlayerPregameData: // Field 1: PlayerId (int32) // Field 2: Data (bytes) - Likely internal game data, send empty bytes or minimal // Assume Player ID is from the mock player. We need to know that ID. // In `getMockPlayerBytes`, the ID is likely embedded. // Let's create a dummy entry. // PlayerId: 108096 (last part of Game ID maybe? or just 1) // Let's define a predictable PlayerID in `getMockPlayerBytes` or just guess '0' or '1'. ppData := make([]byte, 0) ppData = append(ppData, 0x08) // Field 1: PlayerId ppData = append(ppData, encodeVarint(uint64(MockUserID))...) // Using named constant ppData = append(ppData, 0x12, 0x00) // Field 2: Data (Empty bytes) gameDetails = append(gameDetails, 0x2a) // Field 5, WireType 2 gameDetails = append(gameDetails, encodeVarint(uint64(len(ppData)))...) gameDetails = append(gameDetails, ppData...) // Request 608 responsePayload = make([]byte, 0) responsePayload = append(responsePayload, 0x0a) responsePayload = append(responsePayload, encodeVarint(uint64(len(gameDetails)))...) responsePayload = append(responsePayload, gameDetails...) responseFieldNum = 608 case 610: // LobbyJoinGameRequest fmt.Println("[TCP] Handling LobbyJoinGameRequest") hasActiveGame = true activeGameID = 4016461897007108096 responsePayload = []byte{} responseFieldNum = 611 // LobbyNewPlayerRequest (usually broadcasted) // Wait, JoinGame response is NOT 611. JoinGame usually expects a success message or just stats update. // But for now, let's leave it as is if it was working for Quick Match. // Quick match used 610 -> ??? // Check previous successful log for 610 response. It probably triggered `StatusReport` update. case 602: // ExitLobbyRequest fmt.Println("[TCP] Handling ExitLobbyRequest") responsePayload = []byte{} responseFieldNum = 603 // LobbyExitedRequest case 777: // PingRequest fmt.Println("[TCP] Handling PingRequest") pingTimestamp := uint64(0) pReader := bytes.NewReader(payloadBytes) for { pTag, err := readVarint(pReader) if err != nil { break } pFieldNum := pTag >> 3 pWireType := pTag & 0x7 if pFieldNum == 1 && pWireType == 0 { // PingRequest.timestamp pingTimestamp, _ = readVarint(pReader) break } else { skipField(pReader, pWireType) } } responsePayload = make([]byte, 0) if pingTimestamp != 0 { responsePayload = append(responsePayload, 0x08) // field 1 (timestamp) responsePayload = append(responsePayload, encodeVarint(pingTimestamp)...) } responseFieldNum = 777 default: fmt.Printf("[TCP] Warning: Unhandled request number: %d. Sending empty response.\n", requestNumber) responsePayload = []byte{} responseFieldNum = int(requestNumber) } message := make([]byte, 0) message = append(message, 0x08) // field 1 (request_number) if wrapperRequestNumber != 0 { message = append(message, encodeVarint(wrapperRequestNumber)...) } else { message = append(message, encodeVarint(uint64(responseFieldNum))...) } tagBytes := encodeVarint(uint64(uint32(responseFieldNum)<<3 | 2)) message = append(message, tagBytes...) message = append(message, encodeVarint(uint64(len(responsePayload)))...) message = append(message, responsePayload...) // Wrap in Packet packet := make([]byte, 0) packet = append(packet, 0x08) // field 1 (id) packet = append(packet, encodeVarint(uint64(packetID))...) packet = append(packet, 0x1a) // field 3 (payload/Message) packet = append(packet, encodeVarint(uint64(len(message)))...) packet = append(packet, message...) // Send response binary.Write(conn, binary.BigEndian, uint32(len(packet))) conn.Write(packet) fmt.Printf("[TCP] Sent response Field %d for Request %d\n", responseFieldNum, requestNumber) } } func readVarint(r *bytes.Reader) (uint64, error) { return binary.ReadUvarint(r) } func readVarintInt64(r *bytes.Reader) (int64, error) { v, err := binary.ReadUvarint(r) return int64(v), err } func encodeVarint(v uint64) []byte { buf := make([]byte, binary.MaxVarintLen64) n := binary.PutUvarint(buf, v) return buf[:n] } func skipField(r *bytes.Reader, wireType uint64) { switch wireType { case 0: // Varint readVarint(r) case 1: // 64-bit r.Seek(8, io.SeekCurrent) case 2: // Length-delimited len, _ := readVarint(r) r.Seek(int64(len), io.SeekCurrent) case 5: // 32-bit r.Seek(4, io.SeekCurrent) } } func encodeDouble(v float64) []byte { buf := make([]byte, 8) binary.LittleEndian.PutUint64(buf, math.Float64bits(v)) return buf }