1023 lines
32 KiB
Go
1023 lines
32 KiB
Go
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
|
|
}
|