Compare commits
	
		
			8 Commits
		
	
	
		
			368fbdbc47
			...
			be32dec202
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| be32dec202 | |||
| 2d0cb12532 | |||
| b44cdab611 | |||
| b73d8de851 | |||
| 23474c19dc | |||
| a48fef0186 | |||
| 67e08c5d1e | |||
| 49e2311497 | 
							
								
								
									
										75
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								README.md
									
									
									
									
									
										Normal 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 & | ||||
| ``` | ||||
| @ -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, | ||||
|  | ||||
| @ -8,6 +8,7 @@ message Action { | ||||
|     enum ActionType { | ||||
|         MOVE = 0; | ||||
|         CHAT = 1; | ||||
|         DISCONNECT = 2; | ||||
|     } | ||||
|  | ||||
|     ActionType type = 1; | ||||
|  | ||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							| @ -2,4 +2,4 @@ module gitea.boner.be/bdnugget/goonserver | ||||
|  | ||||
| go 1.23.0 | ||||
|  | ||||
| require google.golang.org/protobuf v1.35.1 | ||||
| require google.golang.org/protobuf v1.36.3 | ||||
|  | ||||
							
								
								
									
										4
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								go.sum
									
									
									
									
									
								
							| @ -2,5 +2,5 @@ github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= | ||||
| github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= | ||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= | ||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= | ||||
| google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= | ||||
| google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= | ||||
| google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= | ||||
|  | ||||
							
								
								
									
										88
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										88
									
								
								main.go
									
									
									
									
									
								
							| @ -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 | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user