6 Commits

Author SHA1 Message Date
b44cdab611 Graceful disconnect 2025-01-18 22:39:43 +01:00
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
4 changed files with 150 additions and 34 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 &
```

View File

@ -1,6 +1,6 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.2
// protoc-gen-go v1.36.3
// protoc v5.29.2
// source: actions.proto
@ -23,8 +23,9 @@ const (
type Action_ActionType int32
const (
Action_MOVE Action_ActionType = 0
Action_CHAT Action_ActionType = 1
Action_MOVE Action_ActionType = 0
Action_CHAT Action_ActionType = 1
Action_DISCONNECT Action_ActionType = 2
)
// Enum value maps for Action_ActionType.
@ -32,10 +33,12 @@ var (
Action_ActionType_name = map[int32]string{
0: "MOVE",
1: "CHAT",
2: "DISCONNECT",
}
Action_ActionType_value = map[string]int32{
"MOVE": 0,
"CHAT": 1,
"MOVE": 0,
"CHAT": 1,
"DISCONNECT": 2,
}
)
@ -394,7 +397,7 @@ var File_actions_proto protoreflect.FileDescriptor
var file_actions_proto_rawDesc = []byte{
0x0a, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xb6, 0x01, 0x0a, 0x06, 0x41, 0x63, 0x74,
0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0xc6, 0x01, 0x0a, 0x06, 0x41, 0x63, 0x74,
0x69, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
0x0e, 0x32, 0x1a, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x41, 0x63, 0x74, 0x69,
0x6f, 0x6e, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74,
@ -404,9 +407,10 @@ var file_actions_proto_rawDesc = []byte{
0x28, 0x05, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c,
0x63, 0x68, 0x61, 0x74, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0b, 0x63, 0x68, 0x61, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22,
0x20, 0x0a, 0x0a, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x08, 0x0a,
0x30, 0x0a, 0x0a, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x08, 0x0a,
0x04, 0x4d, 0x4f, 0x56, 0x45, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x43, 0x48, 0x41, 0x54, 0x10,
0x01, 0x22, 0x69, 0x0a, 0x0b, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x61, 0x74, 0x63, 0x68,
0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x44, 0x49, 0x53, 0x43, 0x4f, 0x4e, 0x4e, 0x45, 0x43, 0x54, 0x10,
0x02, 0x22, 0x69, 0x0a, 0x0b, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x61, 0x74, 0x63, 0x68,
0x12, 0x1b, 0x0a, 0x09, 0x70, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20,
0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x49, 0x64, 0x12, 0x29, 0x0a,
0x07, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f,

View File

@ -8,6 +8,7 @@ message Action {
enum ActionType {
MOVE = 0;
CHAT = 1;
DISCONNECT = 2;
}
ActionType type = 1;

88
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,13 +111,22 @@ 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
}
// Queue the actions for processing
if batch.PlayerId == int32(playerID) {
for _, action := range batch.Actions {
if action.Type == pb.Action_DISCONNECT {
log.Printf("Player %d disconnected gracefully", playerID)
delete(players, playerID)
delete(playerConns, playerID)
delete(actionQueue, playerID)
return
}
}
actionQueue[playerID] = append(actionQueue[playerID], batch.Actions...)
}
}
@ -154,8 +175,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 +189,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
}