5 Commits

Author SHA1 Message Date
b73d8de851 Use ticker like in client network.go to simplify logic 2025-01-18 14:23:04 +01:00
23474c19dc Try to handle tcp fragmentation 2025-01-15 10:51:43 +01:00
a48fef0186 Only send last 5 chat messages 2025-01-15 10:36:36 +01:00
67e08c5d1e Readme 2025-01-13 15:12:16 +01:00
49e2311497 Merge pull request 'Implement chat' (#1) from feature/chat into master
Reviewed-on: #1
2025-01-13 13:15:25 +00:00
2 changed files with 128 additions and 26 deletions

75
README.md Normal file
View File

@ -0,0 +1,75 @@
# GoonServer
The server component for GoonScape, handling multiplayer synchronization and chat.
## Features
- Tick-based game state synchronization
- Player movement validation
- Global chat system
- Client connection management
- Protobuf-based network protocol
## Prerequisites
- Go 1.23 or higher
- Protocol Buffers compiler (protoc)
## Installation
1. Clone the repository:
```bash
git clone https://gitea.boner.be/bdnugget/goonserver.git
cd goonserver
```
2. Install dependencies:
```bash
go mod tidy
```
3. Build and run:
```bash
go run main.go
```
## Configuration
Server settings can be modified in `main.go`:
```go
const (
port = ":6969" // Port to listen on
tickRate = 600 * time.Millisecond // Server tick rate
)
```
## Protocol
The server uses Protocol Buffers for client-server communication. The protocol is defined in `actions/actions.proto`:
- `Action`: Player actions (movement, chat)
- `ActionBatch`: Grouped actions from a player
- `ServerMessage`: Game state updates to clients
- `PlayerState`: Individual player state
- `ChatMessage`: Player chat messages
## Development
After modifying the protocol (`actions.proto`), regenerate the Go code:
```bash
protoc --go_out=. actions/actions.proto
```
## Deployment
The server is designed to run on a single instance. For production deployment:
1. Build the binary:
```bash
go build -o goonserver
```
2. Run with logging:
```bash
./goonserver > server.log 2>&1 &
```

79
main.go
View File

@ -1,7 +1,10 @@
package main
import (
"bufio"
"encoding/binary"
"fmt"
"io"
"log"
"net"
"sync"
@ -40,6 +43,11 @@ func main() {
defer ln.Close()
fmt.Printf("Server is listening on port %s\n", port)
// Create ticker for fixed game state updates
ticker := time.NewTicker(tickRate)
defer ticker.Stop()
// Handle incoming connections in a separate goroutine
go func() {
for {
conn, err := ln.Accept()
@ -51,12 +59,9 @@ func main() {
}
}()
lastTick := time.Now()
for {
if time.Since(lastTick) >= tickRate {
lastTick = time.Now()
processActions()
}
// Main game loop
for range ticker.C {
processActions()
}
}
@ -76,22 +81,29 @@ func handleConnection(conn net.Conn) {
PlayerId: int32(playerID),
CurrentTick: 0,
}
data, err := proto.Marshal(serverMsg)
if err != nil {
log.Printf("Failed to marshal ServerMessage for player %d: %v", playerID, err)
return
}
if _, err := conn.Write(data); err != nil {
if err := writeMessage(conn, serverMsg); err != nil {
log.Printf("Failed to send player ID to player %d: %v", playerID, err)
return
}
// Listen for incoming actions from this player
buf := make([]byte, 4096)
reader := bufio.NewReader(conn)
for {
n, err := conn.Read(buf)
if err != nil {
log.Printf("Error reading from player %d: %v", playerID, err)
// Read message length
lengthBuf := make([]byte, 4)
if _, err := io.ReadFull(reader, lengthBuf); err != nil {
log.Printf("Error reading message length from player %d: %v", playerID, err)
delete(players, playerID)
delete(playerConns, playerID)
delete(actionQueue, playerID)
return
}
messageLength := binary.BigEndian.Uint32(lengthBuf)
// Read message body
messageBuf := make([]byte, messageLength)
if _, err := io.ReadFull(reader, messageBuf); err != nil {
log.Printf("Error reading message from player %d: %v", playerID, err)
delete(players, playerID)
delete(playerConns, playerID)
delete(actionQueue, playerID)
@ -99,7 +111,7 @@ func handleConnection(conn net.Conn) {
}
batch := &pb.ActionBatch{}
if err := proto.Unmarshal(buf[:n], batch); err != nil {
if err := proto.Unmarshal(messageBuf, batch); err != nil {
log.Printf("Failed to unmarshal action batch for player %d: %v", playerID, err)
continue
}
@ -154,8 +166,10 @@ func processActions() {
currentTick := time.Now().UnixNano() / int64(tickRate)
state := &pb.ServerMessage{
CurrentTick: currentTick,
Players: make([]*pb.PlayerState, 0, len(players)),
}
// Convert players to PlayerState
for id, p := range players {
p.Lock()
state.Players = append(state.Players, &pb.PlayerState{
@ -166,21 +180,34 @@ func processActions() {
p.Unlock()
}
// Add chat messages to the server message
// Add chat messages to the state
chatMutex.RLock()
state.ChatMessages = chatHistory
state.ChatMessages = chatHistory[max(0, len(chatHistory)-5):] // Only send last 5 messages
chatMutex.RUnlock()
data, err := proto.Marshal(state)
if err != nil {
log.Printf("Failed to marshal game state: %v", err)
return
}
// Send to each connected player
for _, conn := range playerConns {
if _, err := conn.Write(data); err != nil {
if err := writeMessage(conn, state); err != nil {
log.Printf("Failed to send update: %v", err)
}
}
}
// Helper function to write length-prefixed messages
func writeMessage(conn net.Conn, msg proto.Message) error {
data, err := proto.Marshal(msg)
if err != nil {
return err
}
// Write length prefix
lengthBuf := make([]byte, 4)
binary.BigEndian.PutUint32(lengthBuf, uint32(len(data)))
if _, err := conn.Write(lengthBuf); err != nil {
return err
}
// Write message body
_, err = conn.Write(data)
return err
}