init commit

This commit is contained in:
Jose Luis Montañes Ojados
2026-01-14 21:33:21 +01:00
commit dbab788e6b
27 changed files with 4001 additions and 0 deletions

72
internal/handlers/auth.go Normal file
View File

@@ -0,0 +1,72 @@
package handlers
import (
"customServer/internal/protocol"
"customServer/internal/state"
"fmt"
"net"
"time"
)
func HandleAsyncAuthRequest(conn net.Conn, requestData []byte) ([]byte, int) {
fmt.Println("[TCP] Handling AsyncAuthRequest")
// Session Message
sessionData := make([]byte, 0)
// field 1 (Id), Varint (WireType 0)
sessionData = append(sessionData, 0x08)
sessionData = append(sessionData, protocol.EncodeVarint(uint64(time.Now().UnixNano()))...)
// Player Message
player := state.GetMockPlayerBytes()
// AsyncConnectedRequest
asyncConnected := make([]byte, 0)
// field 1 (Session), message
asyncConnected = append(asyncConnected, 0x0a)
asyncConnected = append(asyncConnected, protocol.EncodeVarint(uint64(len(sessionData)))...)
asyncConnected = append(asyncConnected, sessionData...)
// field 2 (Player), message
asyncConnected = append(asyncConnected, 0x12)
asyncConnected = append(asyncConnected, protocol.EncodeVarint(uint64(len(player)))...)
asyncConnected = append(asyncConnected, player...)
return asyncConnected, 406
}
func HandlePingRequest(conn net.Conn, requestData []byte) ([]byte, int) {
fmt.Println("[TCP] Handling PingRequest")
// The client sends a PingRequest with a timestamp (field 1).
// We should respond with a PingRequest (777) containing the same timestamp.
// Extract timestamp if possible, otherwise just send back a default one
// For simplicity, we just echo back what we got if it's small, or send a new one.
return requestData, 777
}
func HandleAsyncDisconnectRequest(conn net.Conn, requestData []byte) ([]byte, int) {
fmt.Println("[TCP] Handling AsyncDisconnectRequest")
return []byte{}, 401
}
func HandleAskServerStatisticsRequest(conn net.Conn, requestData []byte) ([]byte, int) {
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, protocol.EncodeVarint(1)...)
// Field 2: Players (Varint)
stats = append(stats, 0x10)
stats = append(stats, protocol.EncodeVarint(1)...)
// Field 3: ConnectedPlayers (Varint)
stats = append(stats, 0x18)
stats = append(stats, protocol.EncodeVarint(1)...)
return stats, 409
}

View File

@@ -0,0 +1,66 @@
package handlers
import (
"customServer/internal/protocol"
"fmt"
"net"
)
// Dispatcher Function
// Note: We might pass dependencies here if we want to avoid globals,
// but for this refactor we'll stick to dispatching to handler functions.
func Dispatch(conn net.Conn, packetID int64, requestNumber int32, requestData []byte) ([]byte, int) {
fmt.Printf("[Dispatcher] Dispatching Request %d (PacketID: %d)\n", requestNumber, packetID)
switch requestNumber {
// Auth
case 400:
return HandleAsyncAuthRequest(conn, requestData)
case 401:
return HandleAsyncDisconnectRequest(conn, requestData)
case 408:
return HandleAskServerStatisticsRequest(conn, requestData)
// System / Buddies
case 515:
return HandleAsyncBuddyListRequest(conn, requestData)
case 560:
return HandleAsyncIgnoreListRequest(conn, requestData)
// Lobby
case 600:
return HandleEnterLobbyRequest(conn, requestData)
case 604:
return HandleLobbyPlayerListRequest(conn, requestData)
case 607:
return HandleLobbyCreateGameRequest(conn, requestData)
case 609:
return HandleLobbyGameListRequest(conn, requestData)
case 610:
return HandleLobbyJoinGameRequest(conn, requestData)
case 622:
return HandleObservableGameListRequest(conn, requestData)
// Game
case 511:
return HandleWhatsNewPussycatRequest(conn, requestData)
case 608:
// Note: 608 is LobbyGameCreatedRequest (OUT), 607 is IN.
// If client sends 608, it's weird.
return nil, 0
// System / Ping
case 777:
return HandlePingRequest(conn, requestData)
default:
fmt.Printf("[Dispatcher] Unknown Request %d\n", requestNumber)
return nil, 0
}
}
// Helper to encode varint for handlers that need it locally
func encodeVarint(v uint64) []byte {
return protocol.EncodeVarint(v)
}

79
internal/handlers/game.go Normal file
View File

@@ -0,0 +1,79 @@
package handlers
import (
"customServer/internal/protocol"
"customServer/internal/state"
"fmt"
"net"
)
func HandleWhatsNewPussycatRequest(conn net.Conn, requestData []byte) ([]byte, int) {
fmt.Println("[TCP] Handling WhatsNewPussycatRequest")
mgr := state.GlobalManager
if mgr.HasActiveGame() {
activeGameID := mgr.GetActiveGameID()
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, protocol.EncodeVarint(uint64(activeGameID))...)
// Field 2: Status (Enum) = 1 (IN_PROGRESS)
innerReport = append(innerReport, 0x10)
innerReport = append(innerReport, protocol.EncodeVarint(uint64(protocol.GameStatus_IN_PROGRESS))...)
// Field 3: Data (Bytes) - Dynamic IronGameState
dataBytes := state.GetMockIronGameStateBytes()
innerReport = append(innerReport, 0x1a)
innerReport = append(innerReport, protocol.EncodeVarint(uint64(len(dataBytes)))...)
innerReport = append(innerReport, dataBytes...)
// Field 4: TurnId (int32) = 4
innerReport = append(innerReport, 0x20)
innerReport = append(innerReport, protocol.EncodeVarint(4)...)
// Field 5: NextPlayerIds (Repeated Int32)
nextPlayers := []uint64{uint64(state.MockUserID)}
npBytes := make([]byte, 0)
for _, pid := range nextPlayers {
npBytes = append(npBytes, protocol.EncodeVarint(pid)...)
}
innerReport = append(innerReport, 0x2a)
innerReport = append(innerReport, protocol.EncodeVarint(uint64(len(npBytes)))...)
innerReport = append(innerReport, npBytes...)
// Field 6: Players (Repeated Player)
mockPlayer := state.GetMockPlayerBytes()
innerReport = append(innerReport, 0x32)
innerReport = append(innerReport, protocol.EncodeVarint(uint64(len(mockPlayer)))...)
innerReport = append(innerReport, mockPlayer...)
// Field 14: Configuration (GameConfiguration)
fallbackConfig := getFallbackConfigBytes()
innerReport = append(innerReport, 0x72)
innerReport = append(innerReport, protocol.EncodeVarint(uint64(len(fallbackConfig)))...)
innerReport = append(innerReport, fallbackConfig...)
// Field 16: ActivePlayer (Int32)
innerReport = append(innerReport, 0x80, 0x01)
innerReport = append(innerReport, protocol.EncodeVarint(uint64(state.MockUserID))...)
// Wrap in GameStatusReportRequest (Field 1: repeated StatusReport)
responsePayload := make([]byte, 0)
responsePayload = append(responsePayload, 0x0a)
responsePayload = append(responsePayload, protocol.EncodeVarint(uint64(len(innerReport)))...)
responsePayload = append(responsePayload, innerReport...)
return responsePayload, 512
} else {
fmt.Println("[TCP] StatusReport: Reporting Idle/Lobby State (Dynamic)")
// Idle Status (Empty reports list)
responsePayload := make([]byte, 0)
return responsePayload, 512
}
}

208
internal/handlers/lobby.go Normal file
View File

@@ -0,0 +1,208 @@
package handlers
import (
"bytes"
"customServer/internal/protocol"
"customServer/internal/state"
"fmt"
"net"
)
func HandleEnterLobbyRequest(conn net.Conn, requestData []byte) ([]byte, int) {
fmt.Println("[TCP] Handling EnterLobbyRequest")
// Pre-push the game list so it's there when the client transitions
gameList := getMockGameListBytes()
conn.Write(protocol.WrapPacket(609, gameList, 999))
return []byte{}, 601
}
func HandleLobbyPlayerListRequest(conn net.Conn, requestData []byte) ([]byte, int) {
fmt.Println("[TCP] Handling LobbyPlayerListRequest")
mockSmallPlayer := state.GetMockSmallPlayerBytes()
// PlayerList Message
playerList := make([]byte, 0)
playerList = append(playerList, 0x0a)
playerList = append(playerList, protocol.EncodeVarint(uint64(len(mockSmallPlayer)))...)
playerList = append(playerList, mockSmallPlayer...)
// Deflate
compressed := protocol.ZlibCompress(playerList)
responsePayload := make([]byte, 0)
responsePayload = append(responsePayload, 0x0a) // Field 1: playerList (ByteString)
responsePayload = append(responsePayload, protocol.EncodeVarint(uint64(len(compressed)))...)
responsePayload = append(responsePayload, compressed...)
return responsePayload, 604
}
func HandleLobbyGameListRequest(conn net.Conn, requestData []byte) ([]byte, int) {
fmt.Println("[TCP] Handling LobbyGameListRequest")
return getMockGameListBytes(), 609
}
func HandleObservableGameListRequest(conn net.Conn, requestData []byte) ([]byte, int) {
fmt.Println("[TCP] Handling ObservableGameListRequest")
return getMockGameListBytes(), 622
}
func HandleLobbyCreateGameRequest(conn net.Conn, requestData []byte) ([]byte, int) {
fmt.Println("[TCP] Handling LobbyCreateGameRequest")
// 1. Scan the request data for Field 607 (LobbyCreateGameRequest)
// Actually, the dispatcher already gives us the requestData which IS the payload of the request number?
// In the original main.go, the Switch handled requestNumber.
// For 607, it manually scanned payloadBytes again.
var configBytes []byte
msgReader := bytes.NewReader(requestData)
for {
tag, err := protocol.ReadVarint(msgReader)
if err != nil {
break
}
fieldNum := tag >> 3
wireType := tag & 0x7
if fieldNum == 607 && wireType == 2 {
length, _ := protocol.ReadVarint(msgReader)
createGameReqBytes := make([]byte, length)
msgReader.Read(createGameReqBytes)
// Extract Configuration (Field 1) from LobbyCreateGameRequest
reqReader := bytes.NewReader(createGameReqBytes)
for {
rtag, rerr := protocol.ReadVarint(reqReader)
if rerr != nil {
break
}
rfieldNum := rtag >> 3
rwireType := rtag & 0x7
if rfieldNum == 1 && rwireType == 2 {
rlength, _ := protocol.ReadVarint(reqReader)
configBytes = make([]byte, rlength)
reqReader.Read(configBytes)
break
} else {
protocol.SkipField(reqReader, rwireType)
}
}
break
} else {
protocol.SkipField(msgReader, wireType)
}
}
// Update State
newGameID := int64(4016461897007108096)
state.GlobalManager.CreateGame(newGameID)
// Construct Response (LobbyGameCreatedRequest 608)
gameDetails := getMockGameDetailsBytes(newGameID, configBytes, [][]byte{state.GetMockPlayerBytes()})
// Push updated game list
gameList := getMockGameListBytes()
conn.Write(protocol.WrapPacket(609, gameList, 1000))
responsePayload := make([]byte, 0)
responsePayload = append(responsePayload, 0x0a)
responsePayload = append(responsePayload, protocol.EncodeVarint(uint64(len(gameDetails)))...)
responsePayload = append(responsePayload, gameDetails...)
return responsePayload, 608
}
func HandleLobbyJoinGameRequest(conn net.Conn, requestData []byte) ([]byte, int) {
fmt.Println("[TCP] Handling LobbyJoinGameRequest")
state.GlobalManager.CreateGame(4016461897007108096)
// Construct LobbyNewPlayerRequest (611)
gameDetails := getMockGameDetailsBytes(4016461897007108096, nil, [][]byte{state.GetMockPlayerBytes()})
// LobbyNewPlayerRequest Message
newPlayerReq := make([]byte, 0)
// Field 1: GameDetails
newPlayerReq = append(newPlayerReq, 0x0a)
newPlayerReq = append(newPlayerReq, protocol.EncodeVarint(uint64(len(gameDetails)))...)
newPlayerReq = append(newPlayerReq, gameDetails...)
// Field 2: JoiningPlayer
newPlayerReq = append(newPlayerReq, 0x10)
newPlayerReq = append(newPlayerReq, protocol.EncodeVarint(uint64(state.MockUserID))...)
return newPlayerReq, 611
}
// Helpers
func getMockGameDetailsBytes(gameID int64, configBytes []byte, players [][]byte) []byte {
gameDetails := make([]byte, 0)
gameDetails = append(gameDetails, 0x08)
gameDetails = append(gameDetails, protocol.EncodeVarint(uint64(gameID))...)
for _, p := range players {
gameDetails = append(gameDetails, 0x12)
gameDetails = append(gameDetails, protocol.EncodeVarint(uint64(len(p)))...)
gameDetails = append(gameDetails, p...)
}
if len(configBytes) > 0 {
// Field 3: Configuration (From request)
gameDetails = append(gameDetails, 0x1a)
gameDetails = append(gameDetails, protocol.EncodeVarint(uint64(len(configBytes)))...)
gameDetails = append(gameDetails, configBytes...)
} else {
// Field 3: Configuration (Fallback)
fallbackConfig := getFallbackConfigBytes()
gameDetails = append(gameDetails, 0x1a)
gameDetails = append(gameDetails, protocol.EncodeVarint(uint64(len(fallbackConfig)))...)
gameDetails = append(gameDetails, fallbackConfig...)
}
return gameDetails
}
func getMockGameListBytes() []byte {
// For the lobby list, show a game with NO players so anyone can join
gameData := getMockGameDetailsBytes(4016461897007108096, nil, [][]byte{})
// GameList Message
gameList := make([]byte, 0)
gameList = append(gameList, 0x0a)
gameList = append(gameList, protocol.EncodeVarint(uint64(len(gameData)))...)
gameList = append(gameList, gameData...)
// Deflate
compressed := protocol.ZlibCompress(gameList)
responsePayload := make([]byte, 0)
responsePayload = append(responsePayload, 0x0a) // Field 1: gameList (ByteString)
responsePayload = append(responsePayload, protocol.EncodeVarint(uint64(len(compressed)))...)
responsePayload = append(responsePayload, compressed...)
return responsePayload
}
func getFallbackConfigBytes() []byte {
fallbackConfig := make([]byte, 0)
name := "Fallback Game"
fallbackConfig = append(fallbackConfig, 0x0a)
fallbackConfig = append(fallbackConfig, protocol.EncodeVarint(uint64(len(name)))...)
fallbackConfig = append(fallbackConfig, name...)
fallbackConfig = append(fallbackConfig, 0x10, 0x00, 0x18, 0x01, 0x20, 0x00)
fallbackConfig = append(fallbackConfig, 0x28)
fallbackConfig = append(fallbackConfig, protocol.EncodeVarint(1)...)
fallbackConfig = append(fallbackConfig, 0x30)
fallbackConfig = append(fallbackConfig, protocol.EncodeVarint(6)...)
fallbackConfig = append(fallbackConfig, 0x48, 0x00, 0x50, 0x2d)
// Field 11: Data (IronGameConfiguration)
ironData := state.GetMockIronGameConfigurationBytes()
fallbackConfig = append(fallbackConfig, 0x5a) // (11 << 3) | 2 = 88 | 2 = 90 = 0x5a
fallbackConfig = append(fallbackConfig, protocol.EncodeVarint(uint64(len(ironData)))...)
fallbackConfig = append(fallbackConfig, ironData...)
return fallbackConfig
}

View File

@@ -0,0 +1,20 @@
package handlers
import (
"fmt"
"net"
)
func HandleAsyncBuddyListRequest(conn net.Conn, requestData []byte) ([]byte, int) {
fmt.Println("[TCP] Handling AsyncBuddyListRequest")
// Return AsyncBuddyListContentRequest (516)
// Field 2: Buddies (AsyncBuddyList)
return []byte{0x12, 0x00}, 516
}
func HandleAsyncIgnoreListRequest(conn net.Conn, requestData []byte) ([]byte, int) {
fmt.Println("[TCP] Handling AsyncIgnoreListRequest")
// Return AsyncIgnoreListContentRequest (516)
// Field 2: Ignores (AsyncBuddyList)
return []byte{0x12, 0x00}, 561
}

View File

@@ -0,0 +1,161 @@
package network
import (
"customServer/internal/state"
"encoding/json"
"fmt"
"net/http"
)
func StartHTTPServer(addr string) error {
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)
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"}`))
})
fmt.Printf("[HTTP] REST API: http://localhost%s\n", addr)
return http.ListenAndServe(addr, mux)
}
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 := map[string]interface{}{
"error": false,
"status": 200,
"data": map[string]interface{}{
"user": map[string]interface{}{
"user_id": state.MockUserID,
"login_name": state.MockUserName,
"email": "player@customserver.local",
"name": state.MockUserName,
"email_valid": true,
"validated": true,
"country": "US",
"language": "en",
"time_zone": "UTC",
"posted_msg_count": 42,
"features": []string{"online_play", "all_expansions", "community", "profile", "userpages"},
"partners": []interface{}{
map[string]interface{}{
"partner_id": 12, // Steam
"partner_user_id": "76561198084728812",
"created_at": "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 := map[string]interface{}{
"error": false,
"status": 200,
"data": map[string]interface{}{
"total": 1,
"_links": map[string]interface{}{},
"users": []interface{}{
map[string]interface{}{
"user_id": state.MockUserID,
"login_name": state.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{}{},
},
}
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)
response := map[string]interface{}{
"data": map[string]interface{}{
"opponents": []interface{}{},
},
}
w.Header().Set("Content-Type", "application/json")
data, _ := json.Marshal(response)
w.Write(data)
}

View File

@@ -0,0 +1,180 @@
package network
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"customServer/internal/handlers"
"customServer/internal/protocol"
"fmt"
"io"
"math/big"
"net"
"time"
)
func StartTCPServer(addr string) error {
tlsConfig, err := generateSelfSignedCert()
if err != nil {
return fmt.Errorf("failed to generate self-signed cert: %v", err)
}
listener, err := tls.Listen("tcp", addr, tlsConfig)
if err != nil {
return fmt.Errorf("TCP TLS listener failed: %v", err)
}
fmt.Printf("[TCP] Scalable Server (TLS): localhost%s\n", addr)
for {
conn, err := listener.Accept()
if err != nil {
fmt.Printf("Accept error: %v\n", err)
continue
}
go handleTCPConnection(conn)
}
}
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)
length, err := protocol.ReadPacketLength(conn)
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 payloadBytes []byte
reader := bytes.NewReader(data)
for {
tag, err := protocol.ReadVarint(reader)
if err != nil {
break
}
fieldNum := tag >> 3
wireType := tag & 0x7
if fieldNum == 1 && wireType == 0 { // Packet.id
packetID, _ = protocol.ReadVarintInt64(reader)
} else if fieldNum == 3 && wireType == 2 { // Packet.payload (Message)
payloadLen, _ := protocol.ReadVarint(reader)
payloadBytes = make([]byte, payloadLen)
reader.Read(payloadBytes)
payloadReader := bytes.NewReader(payloadBytes)
for {
pTag, err := protocol.ReadVarint(payloadReader)
if err != nil {
break
}
pFieldNum := pTag >> 3
pWireType := pTag & 0x7
if pFieldNum == 1 && pWireType == 0 { // Message.request_number
reqNum64, _ := protocol.ReadVarintInt64(payloadReader)
requestNumber = int32(reqNum64)
} else {
protocol.SkipField(payloadReader, pWireType)
}
}
} else {
protocol.SkipField(reader, wireType)
}
}
fmt.Printf("[TCP] Got Request ID: %d, Number: %d\n", packetID, requestNumber)
responsePayload, responseFieldNum := handlers.Dispatch(conn, packetID, requestNumber, payloadBytes)
if responsePayload != nil {
sendTCPResponse(conn, packetID, responseFieldNum, responsePayload)
}
}
}
func sendTCPResponse(conn net.Conn, packetID int64, fieldNum int, payload []byte) {
// Construct Message wrapper
message := make([]byte, 0)
// Field 1: request_number
message = append(message, 0x08)
message = append(message, protocol.EncodeVarint(uint64(fieldNum))...)
// Field [fieldNum]: payload (WireType 2)
message = append(message, protocol.EncodeVarint(uint64(fieldNum<<3|2))...)
message = append(message, protocol.EncodeVarint(uint64(len(payload)))...)
message = append(message, payload...)
// Construct Packet wrapper
packet := make([]byte, 0)
// Field 1: Id
packet = append(packet, 0x08)
packet = append(packet, protocol.EncodeVarint(uint64(packetID))...)
// Field 3: Payload
packet = append(packet, 0x1a)
packet = append(packet, protocol.EncodeVarint(uint64(len(message)))...)
packet = append(packet, message...)
// Send length + packet
lengthBuf := make([]byte, 4)
length := uint32(len(packet))
lengthBuf[0] = byte(length >> 24)
lengthBuf[1] = byte(length >> 16)
lengthBuf[2] = byte(length >> 8)
lengthBuf[3] = byte(length)
conn.Write(lengthBuf)
conn.Write(packet)
}
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
}

View File

@@ -0,0 +1,95 @@
package protocol
import (
"bytes"
"compress/zlib"
"encoding/binary"
"io"
"math"
)
func ZlibCompress(data []byte) []byte {
var b bytes.Buffer
w := zlib.NewWriter(&b)
w.Write(data)
w.Close()
return b.Bytes()
}
// Codec Functions (migrated from main.go)
func ReadVarint(r io.ByteReader) (uint64, error) {
return binary.ReadUvarint(r)
}
func ReadVarintInt64(r io.ByteReader) (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 EncodeDouble(v float64) []byte {
buf := make([]byte, 8)
binary.LittleEndian.PutUint64(buf, math.Float64bits(v))
return buf
}
func SkipField(reader *bytes.Reader, wireType uint64) {
switch wireType {
case 0: // Varint
ReadVarint(reader)
case 1: // Fixed64
reader.Seek(8, io.SeekCurrent)
case 2: // Length-delimited
length, _ := ReadVarint(reader)
reader.Seek(int64(length), io.SeekCurrent)
case 5: // Fixed32
reader.Seek(4, io.SeekCurrent)
}
}
// Helper to read a packet length (4 bytes, Big Endian) as per official protocol
func ReadPacketLength(conn io.Reader) (uint32, error) {
var length uint32
err := binary.Read(conn, binary.BigEndian, &length)
return length, err
}
// WrapPacket encapsulates a payload in the Message and Packet structure
// 1. Message wrapper (Field 1: requestNumber, Field [requestNumber]: payload field)
// 2. Packet wrapper (Field 1: packetID, Field 3: Message bytes)
// 3. 4-byte Big Endian length prefix
func WrapPacket(requestNumber int, payload []byte, packetID uint32) []byte {
// 1. Wrap in Message
msg := make([]byte, 0)
// Field 1: request_number (Varint)
msg = append(msg, 0x08)
msg = append(msg, EncodeVarint(uint64(requestNumber))...)
// Field [requestNumber]: The actual response payload
msg = append(msg, EncodeVarint(uint64(uint32(requestNumber)<<3|2))...)
msg = append(msg, EncodeVarint(uint64(len(payload)))...)
msg = append(msg, payload...)
// 2. Wrap in Packet
packet := make([]byte, 0)
// Field 1: Id (Varint)
packet = append(packet, 0x08)
packet = append(packet, EncodeVarint(uint64(packetID))...)
// Field 3: Payload (Message)
packet = append(packet, 0x1a)
packet = append(packet, EncodeVarint(uint64(len(msg)))...)
packet = append(packet, msg...)
// 3. Add 4-byte length prefix
final := make([]byte, 4)
binary.BigEndian.PutUint32(final, uint32(len(packet)))
final = append(final, packet...)
return final
}

120
internal/protocol/types.go Normal file
View File

@@ -0,0 +1,120 @@
package protocol
// Enum Definitions
type GameStatus int32
const (
GameStatus_RESERVED GameStatus = 0
GameStatus_IN_PROGRESS GameStatus = 1
GameStatus_OVER GameStatus = 2
GameStatus_NOT_STARTED GameStatus = 3
GameStatus_ABORTED GameStatus = 4
GameStatus_OUTCOME GameStatus = 5
GameStatus_PLAYER_TIMEOUT GameStatus = 6
GameStatus_ABORTING GameStatus = 7
GameStatus_WAITING_INVITATION GameStatus = 8
GameStatus_FIRST_USER_DATA_ROUND GameStatus = 9
GameStatus_SIMULTANEOUS GameStatus = 10
GameStatus_INTERRUPTIBLE GameStatus = 11
)
type ErrorCode int32
const (
ErrorCode_NO_ERROR ErrorCode = 0
)
// Message Definitions
type GameConfiguration struct {
Name string
Private bool
Lurkable bool
Rated bool
MinPlayers int32
MaxPlayers int32
MinKarma int32
FirstPlayer int32
GameMode int32
Timeout int32
Data []byte
ReqFirstUserRound bool
ObservableBy int32
MinRankScore int32
MainVariant string
RulesVer int32
IdleTime int32
}
type Player struct {
Id int32
Name string
Karma int32
RankScore float64
Rank int32
NbGames int32
Language int32
Avatar string // Simplified for mock
Tz string
}
type SmallPlayer struct {
WWWId int64
Name string
Karma int32
RankScore float64
Rank int32
NbGames int32
Language int32
Avatar string
}
type GameDetails struct {
GameId int64
Players []*Player
Configuration *GameConfiguration
Data []byte
UserData []*PlayerPregameData
}
type PlayerPregameData struct {
PlayerId int32
Data []byte
}
type StatusReport struct {
GameId int64
Status GameStatus
Data []byte
TurnId int32
NextPlayerIds []int32
Players []*Player
Configuration *GameConfiguration
ActivePlayer int32
// Add other fields as needed
}
type Session struct {
Id int64
}
// Request/Response Wrappers (Simplification: We handle fields manually in codec mostly,
// but these structs represent the data content)
type AsyncConnectedRequest struct {
Session *Session
Player *Player
}
type ServerStatisticsRequest struct {
HostedGames int32
Players int32
ConnectedPlayers int32
}
type LobbyGameCreatedRequest struct {
Game *GameDetails
}
// Helper: Encode helpers can be added here or in codec.
// For now, these structs serve as the 'definitions' requested by the user.

158
internal/state/helpers.go Normal file
View File

@@ -0,0 +1,158 @@
package state
import (
"customServer/internal/protocol"
)
// Constants
const (
MockUserID = 8381763
MockUserName = "CustomPlayer"
)
// Helpers to generate mock bytes using Protocol Codecs
// We use manual encoding to ensure exact byte match with original monolithic server for safety
func GetMockPlayerBytes() []byte {
player := make([]byte, 0)
// field 1 (Name), string
player = append(player, 0x0a)
player = append(player, protocol.EncodeVarint(uint64(len(MockUserName)))...)
player = append(player, MockUserName...)
// field 2 (Id), varint
player = append(player, 0x10)
player = append(player, protocol.EncodeVarint(uint64(MockUserID))...)
// field 5 (Karma), varint
player = append(player, 0x28)
player = append(player, protocol.EncodeVarint(52)...)
// field 7 (RankScore), fixed64 (WireType 1) - double
player = append(player, 0x39) // (7 << 3) | 1 = 57 = 0x39
player = append(player, protocol.EncodeDouble(1500.0)...)
// field 9 (WWWId), varint
player = append(player, 0x48)
player = append(player, protocol.EncodeVarint(uint64(MockUserID))...)
// field 11 (nbGames), varint
player = append(player, 0x58)
player = append(player, protocol.EncodeVarint(2)...)
// field 12 (Banned), varint
player = append(player, 0x60)
player = append(player, protocol.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, protocol.EncodeVarint(uint64(len(avatarUrl)))...)
avatarMsg = append(avatarMsg, avatarUrl...)
player = append(player, 0x72) // Field 14, WireType 2
player = append(player, protocol.EncodeVarint(uint64(len(avatarMsg)))...)
player = append(player, avatarMsg...)
// field 15 (Language), varint
player = append(player, 0x78)
player = append(player, protocol.EncodeVarint(1)...)
// field 16 (Tz), string
player = append(player, 0x82, 0x01) // Field 16 (128 + 2), WireType 2
player = append(player, protocol.EncodeVarint(uint64(len("UTC")))...)
player = append(player, "UTC"...)
return player
}
func GetMockSmallPlayerBytes() []byte {
player := make([]byte, 0)
// field 1 (WWWId), int32
player = append(player, 0x08)
player = append(player, protocol.EncodeVarint(uint64(MockUserID))...)
// field 2 (Name), string
player = append(player, 0x12)
player = append(player, protocol.EncodeVarint(uint64(len(MockUserName)))...)
player = append(player, MockUserName...)
// field 3 (Karma), int32
player = append(player, 0x18)
player = append(player, protocol.EncodeVarint(52)...)
// field 4 (RankScore), double
player = append(player, 0x21) // (4 << 3) | 1 = 33 = 0x21
player = append(player, protocol.EncodeDouble(1500.0)...)
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, protocol.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, protocol.EncodeVarint(6)...)
// HumanPlayers
hpBytes := GetMockIronHumanPlayerBytes()
data = append(data, 0x3a) // Field 7, WireType 2
data = append(data, protocol.EncodeVarint(uint64(len(hpBytes)))...)
data = append(data, hpBytes...)
// CreatedBy
data = append(data, 0x40) // Field 8, WireType 0
data = append(data, protocol.EncodeVarint(uint64(MockUserID))...)
return data
}
func GetMockIronGameStateBytes() []byte {
// IronGameState
// Field 1: GameId (int64)
// Field 2: Version (int32)
// Field 7: Configuration (IronGameConfiguration)
// Field 9: AllianceStateVersion (int32)
data := make([]byte, 0)
// GameId
data = append(data, 0x08)
data = append(data, protocol.EncodeVarint(4016461897007108096)...)
// Version
data = append(data, 0x10)
data = append(data, protocol.EncodeVarint(1)...)
// Configuration
configBytes := GetMockIronGameConfigurationBytes()
data = append(data, 0x3a) // Field 7, WireType 2
data = append(data, protocol.EncodeVarint(uint64(len(configBytes)))...)
data = append(data, configBytes...)
// AllianceStateVersion
data = append(data, 0x48)
data = append(data, protocol.EncodeVarint(1)...)
return data
}

64
internal/state/manager.go Normal file
View File

@@ -0,0 +1,64 @@
package state
import (
_ "embed"
"encoding/base64"
"fmt"
"sync"
)
// Global Game State Manager
type GameManager struct {
mu sync.Mutex
hasActiveGame bool
activeGameID int64
activeGameData []byte
}
var GlobalManager *GameManager
func init() {
GlobalManager = &GameManager{
hasActiveGame: false,
activeGameID: 0,
}
// Pre-decode the embedded game data
data, err := base64.StdEncoding.DecodeString(MockGameDataBase64)
if err != nil {
fmt.Printf("Error decoding mock game data: %v\n", err)
} else {
GlobalManager.activeGameData = data
}
}
func (m *GameManager) HasActiveGame() bool {
m.mu.Lock()
defer m.mu.Unlock()
return m.hasActiveGame
}
func (m *GameManager) GetActiveGameID() int64 {
m.mu.Lock()
defer m.mu.Unlock()
return m.activeGameID
}
func (m *GameManager) GetActiveGameData() []byte {
m.mu.Lock()
defer m.mu.Unlock()
return m.activeGameData
}
func (m *GameManager) CreateGame(id int64) {
m.mu.Lock()
defer m.mu.Unlock()
m.hasActiveGame = true
m.activeGameID = id
m.activeGameData = GetMockIronGameStateBytes()
}
// Embeds
//go:embed mock_gamedata.b64
var MockGameDataBase64 string

View File

@@ -0,0 +1 @@
CICggOjD6dXeNxAKGAMiVnsibmFtZSI6InN3dCIsInZhbHVlIjp7InNsbiI6eyJlaWQiOjE5MiwidHJzIjpbeyJlbHQiOls2NTNdLCJuYW1lIjoiZWx0ciJ9XX0sImN0ciI6MX19IlZ7Im5hbWUiOiJzd3QiLCJ2YWx1ZSI6eyJzbG4iOnsiZWlkIjo2NTMsInRycyI6W3siZWx0IjpbNTUxXSwibmFtZSI6ImVsdHIifV19LCJjdHIiOjF9fSJWeyJuYW1lIjoic3d0IiwidmFsdWUiOnsic2xuIjp7ImVpZCI6MjAyLCJ0cnMiOlt7ImVsdCI6WzYzOV0sIm5hbWUiOiJlbHRyIn1dfSwiY3RyIjoxfX0iVnsibmFtZSI6InN3dCIsInZhbHVlIjp7InNsbiI6eyJlaWQiOjYzOSwidHJzIjpbeyJlbHQiOls1NTFdLCJuYW1lIjoiZWx0ciJ9XX0sImN0ciI6MX19IlZ7Im5hbWUiOiJzd3QiLCJ2YWx1ZSI6eyJzbG4iOnsiZWlkIjoyMDIsInRycyI6W3siZWx0IjpbNjM5XSwibmFtZSI6ImVsdHIifV19LCJjdHIiOjF9fSJWeyJuYW1lIjoic3d0IiwidmFsdWUiOnsic2xuIjp7ImVpZCI6MTkyLCJ0cnMiOlt7ImVsdCI6WzY2MV0sIm5hbWUiOiJlbHRyIn1dfSwiY3RyIjoxfX0iVnsibmFtZSI6InN3dCIsInZhbHVlIjp7InNsbiI6eyJlaWQiOjE5MCwidHJzIjpbeyJlbHQiOls2NjNdLCJuYW1lIjoiZWx0ciJ9XX0sImN0ciI6MX19IlZ7Im5hbWUiOiJzd3QiLCJ2YWx1ZSI6eyJzbG4iOnsiZWlkIjo2NjMsInRycyI6W3siZWx0IjpbNTUxXSwibmFtZSI6ImVsdHIifV19LCJjdHIiOjF9fSJWeyJuYW1lIjoic3d0IiwidmFsdWUiOnsic2xuIjp7ImVpZCI6MTkwLCJ0cnMiOlt7ImVsdCI6WzY0NV0sIm5hbWUiOiJlbHRyIn1dfSwiY3RyIjoxfX0iIHsibmFtZSI6InN3dCIsInZhbHVlIjp7InNsbiI6eyJlaWQiOjE5MiwidHJzIjpbeyJlbHQiOltdLCJuYW1lIjoiZWx0ciJ9XX0sImN0ciI6NTd9fSJXeyJuYW1lIjoic3d0IiwidmFsdWUiOnsic2xuIjp7ImVpZCI6MTc1LCJ0cnMiOlt7ImVsdCI6WzE2N10sIm5hbWUiOiJlbHRyIn1dfSwiY3RyIjo2MX19IlR7Im5hbWUiOiJzd3QiLCJ2YWx1ZSI6eyJzbG4iOnsiZWlkIjoxNjksInRycyI6W3siZWx0IjpbXSwibmFtZSI6ImVsdHIifV19LCJjdHIiOjYzfX0iV3sibmFtZSI6InN3dCIsInZhbHVlIjp7InNsbiI6eyJlaWQiOjI1MCwidHJzIjpbeyJlbHQiOlsxNTZdLCJuYW1lIjoiZWx0ciJ9XX0sImN0ciI6NzJ9fSIgeyJuYW1lIjoibXMiLCJ2YWx1ZSI6eyJjdHIiOjc1fX0iL3sibmFtZSI6ImdjYyIsInZhbHVlIjp7InNlbGVjdGlvbiI6MCwiY3RyIjo4MX19Ild7Im5hbWUiOiJzd3QiLCJ2YWx1ZSI6eyJzbG4iOnsiZWlkIjoyNTAsInRycyI6W3siZWx0IjpbMTUyXSwibmFtZSI6ImVsdHIifV19LCJjdHIiOjg4fX0iL3sibmFtZSI6ImdjYyIsInZhbHVlIjp7InNlbGVjdGlvbiI6MywiY3RyIjo5N319Ilh7Im5hbWUiOiJzd3QiLCJ2YWx1ZSI6eyJzbG4iOnsiZWlkIjo1NDMsInRycyI6W3siZWx0IjpbMTkyXSwibmFtZSI6ImVsdHIifV19LCJjdHIiOjExMX19IjB7Im5hbWUiOiJnY2MiLCJ2YWx1ZSI6eyJzZWxlY3Rpb24iOjQsImN0ciI6MTE4fX0iMHsibmFtZSI6ImdjYyIsInZhbHVlIjp7InNlbGVjdGlvbiI6MCwiY3RyIjoxMjJ9fToWOgIIAToECAIQBToECAMQAToECAQQBA==

View File