Files
got-mod-customserver-server/cmd/server/main.go.bak

1023 lines
32 KiB
Go
Raw Normal View History

2026-01-14 21:33:21 +01:00
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
}