Compare commits

...

24 Commits

Author SHA1 Message Date
00aa302229 bin 2025-04-16 12:50:46 +02:00
c720b66818 Add timestamp to protobuf and only send unseen messages to clients 2025-04-16 11:49:49 +02:00
f9ec811b10 Update License 2025-01-20 01:16:42 +01:00
d25ee09155 Add system messages to chat 2025-01-20 00:03:15 +01:00
e3c570349c Merge pull request 'feature/db' (#2) from feature/db into master
Reviewed-on: #2
2025-01-19 21:07:47 +00:00
27da845b11 Rate limit on registration 2025-01-19 22:06:41 +01:00
3f7205d73e based database and account progress 2025-01-19 21:52:37 +01:00
0731339fe8 gitignore the db lol 2025-01-19 21:44:17 +01:00
71d42a8fd6 Remove database from git tracking 2025-01-19 21:23:10 +01:00
52ab45fe53 getting somewhere with db and auth 2025-01-19 21:17:07 +01:00
be32dec202 go sum 2025-01-18 23:29:30 +01:00
2d0cb12532 protobuf 2025-01-18 23:22:19 +01:00
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
368fbdbc47 Implement chat 2025-01-13 13:23:05 +01:00
4b73492ffc Fix panic on disconnect by adding mutex 2025-01-13 10:04:09 +01:00
a459e8b4a5 fix tick rates and action ques and stuff 2025-01-13 09:59:37 +01:00
8290131998 Update protocol buffers with tick synchronization 2025-01-13 00:38:52 +01:00
f91f72c05d update protobuf 2025-01-13 00:30:26 +01:00
1d6d3ab2ea playerID 2024-12-13 20:32:27 +01:00
10 changed files with 1111 additions and 213 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.db

24
LICENSE
View File

@ -1,11 +1,21 @@
“Commons Clause” License Condition v1.0 MIT License
The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition. Copyright (c) 2025 bdnugget
Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, right to Sell the Software. Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
For purposes of the foregoing, “Sell” means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/ support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any license notice or attribution required by the License must also include this Commons Cause License Condition notice. The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
Software: GoonServer (for Goonscape) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
License: Commons Clause v1.0 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
Licensor: bdnugget FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

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,8 +1,8 @@
// Code generated by protoc-gen-go. DO NOT EDIT. // Code generated by protoc-gen-go. DO NOT EDIT.
// versions: // versions:
// protoc-gen-go v1.28.1 // protoc-gen-go v1.36.6
// protoc v3.21.12 // protoc v6.30.1
// source: actions.proto // source: actions/actions.proto
package actions package actions
@ -11,6 +11,7 @@ import (
protoimpl "google.golang.org/protobuf/runtime/protoimpl" protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect" reflect "reflect"
sync "sync" sync "sync"
unsafe "unsafe"
) )
const ( const (
@ -23,16 +24,28 @@ const (
type Action_ActionType int32 type Action_ActionType int32
const ( const (
Action_MOVE Action_ActionType = 0 Action_MOVE Action_ActionType = 0
Action_CHAT Action_ActionType = 1
Action_DISCONNECT Action_ActionType = 2
Action_LOGIN Action_ActionType = 3
Action_REGISTER Action_ActionType = 4
) )
// Enum value maps for Action_ActionType. // Enum value maps for Action_ActionType.
var ( var (
Action_ActionType_name = map[int32]string{ Action_ActionType_name = map[int32]string{
0: "MOVE", 0: "MOVE",
1: "CHAT",
2: "DISCONNECT",
3: "LOGIN",
4: "REGISTER",
} }
Action_ActionType_value = map[string]int32{ Action_ActionType_value = map[string]int32{
"MOVE": 0, "MOVE": 0,
"CHAT": 1,
"DISCONNECT": 2,
"LOGIN": 3,
"REGISTER": 4,
} }
) )
@ -47,11 +60,11 @@ func (x Action_ActionType) String() string {
} }
func (Action_ActionType) Descriptor() protoreflect.EnumDescriptor { func (Action_ActionType) Descriptor() protoreflect.EnumDescriptor {
return file_actions_proto_enumTypes[0].Descriptor() return file_actions_actions_proto_enumTypes[0].Descriptor()
} }
func (Action_ActionType) Type() protoreflect.EnumType { func (Action_ActionType) Type() protoreflect.EnumType {
return &file_actions_proto_enumTypes[0] return &file_actions_actions_proto_enumTypes[0]
} }
func (x Action_ActionType) Number() protoreflect.EnumNumber { func (x Action_ActionType) Number() protoreflect.EnumNumber {
@ -60,27 +73,27 @@ func (x Action_ActionType) Number() protoreflect.EnumNumber {
// Deprecated: Use Action_ActionType.Descriptor instead. // Deprecated: Use Action_ActionType.Descriptor instead.
func (Action_ActionType) EnumDescriptor() ([]byte, []int) { func (Action_ActionType) EnumDescriptor() ([]byte, []int) {
return file_actions_proto_rawDescGZIP(), []int{0, 0} return file_actions_actions_proto_rawDescGZIP(), []int{0, 0}
} }
type Action struct { type Action struct {
state protoimpl.MessageState state protoimpl.MessageState `protogen:"open.v1"`
sizeCache protoimpl.SizeCache Type Action_ActionType `protobuf:"varint,1,opt,name=type,proto3,enum=actions.Action_ActionType" json:"type,omitempty"`
X int32 `protobuf:"varint,2,opt,name=x,proto3" json:"x,omitempty"`
Y int32 `protobuf:"varint,3,opt,name=y,proto3" json:"y,omitempty"`
PlayerId int32 `protobuf:"varint,4,opt,name=player_id,json=playerId,proto3" json:"player_id,omitempty"`
ChatMessage string `protobuf:"bytes,5,opt,name=chat_message,json=chatMessage,proto3" json:"chat_message,omitempty"`
Username string `protobuf:"bytes,6,opt,name=username,proto3" json:"username,omitempty"`
Password string `protobuf:"bytes,7,opt,name=password,proto3" json:"password,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
Type Action_ActionType `protobuf:"varint,1,opt,name=type,proto3,enum=actions.Action_ActionType" json:"type,omitempty"`
X int32 `protobuf:"varint,2,opt,name=x,proto3" json:"x,omitempty"`
Y int32 `protobuf:"varint,3,opt,name=y,proto3" json:"y,omitempty"`
PlayerId int32 `protobuf:"varint,4,opt,name=player_id,json=playerId,proto3" json:"player_id,omitempty"`
} }
func (x *Action) Reset() { func (x *Action) Reset() {
*x = Action{} *x = Action{}
if protoimpl.UnsafeEnabled { mi := &file_actions_actions_proto_msgTypes[0]
mi := &file_actions_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *Action) String() string { func (x *Action) String() string {
@ -90,8 +103,8 @@ func (x *Action) String() string {
func (*Action) ProtoMessage() {} func (*Action) ProtoMessage() {}
func (x *Action) ProtoReflect() protoreflect.Message { func (x *Action) ProtoReflect() protoreflect.Message {
mi := &file_actions_proto_msgTypes[0] mi := &file_actions_actions_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -103,7 +116,7 @@ func (x *Action) ProtoReflect() protoreflect.Message {
// Deprecated: Use Action.ProtoReflect.Descriptor instead. // Deprecated: Use Action.ProtoReflect.Descriptor instead.
func (*Action) Descriptor() ([]byte, []int) { func (*Action) Descriptor() ([]byte, []int) {
return file_actions_proto_rawDescGZIP(), []int{0} return file_actions_actions_proto_rawDescGZIP(), []int{0}
} }
func (x *Action) GetType() Action_ActionType { func (x *Action) GetType() Action_ActionType {
@ -134,33 +147,54 @@ func (x *Action) GetPlayerId() int32 {
return 0 return 0
} }
type ServerMessage struct { func (x *Action) GetChatMessage() string {
state protoimpl.MessageState if x != nil {
sizeCache protoimpl.SizeCache return x.ChatMessage
unknownFields protoimpl.UnknownFields
PlayerId int32 `protobuf:"varint,1,opt,name=player_id,json=playerId,proto3" json:"player_id,omitempty"` // Only used when initially assigning player ID
Players []*PlayerState `protobuf:"bytes,2,rep,name=players,proto3" json:"players,omitempty"` // Player state updates
}
func (x *ServerMessage) Reset() {
*x = ServerMessage{}
if protoimpl.UnsafeEnabled {
mi := &file_actions_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
} }
return ""
} }
func (x *ServerMessage) String() string { func (x *Action) GetUsername() string {
if x != nil {
return x.Username
}
return ""
}
func (x *Action) GetPassword() string {
if x != nil {
return x.Password
}
return ""
}
type ActionBatch struct {
state protoimpl.MessageState `protogen:"open.v1"`
PlayerId int32 `protobuf:"varint,1,opt,name=player_id,json=playerId,proto3" json:"player_id,omitempty"`
Actions []*Action `protobuf:"bytes,2,rep,name=actions,proto3" json:"actions,omitempty"`
Tick int64 `protobuf:"varint,3,opt,name=tick,proto3" json:"tick,omitempty"`
ProtocolVersion int32 `protobuf:"varint,4,opt,name=protocol_version,json=protocolVersion,proto3" json:"protocol_version,omitempty"`
LastSeenMessageTimestamp int64 `protobuf:"varint,5,opt,name=last_seen_message_timestamp,json=lastSeenMessageTimestamp,proto3" json:"last_seen_message_timestamp,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ActionBatch) Reset() {
*x = ActionBatch{}
mi := &file_actions_actions_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ActionBatch) String() string {
return protoimpl.X.MessageStringOf(x) return protoimpl.X.MessageStringOf(x)
} }
func (*ServerMessage) ProtoMessage() {} func (*ActionBatch) ProtoMessage() {}
func (x *ServerMessage) ProtoReflect() protoreflect.Message { func (x *ActionBatch) ProtoReflect() protoreflect.Message {
mi := &file_actions_proto_msgTypes[1] mi := &file_actions_actions_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -170,42 +204,61 @@ func (x *ServerMessage) ProtoReflect() protoreflect.Message {
return mi.MessageOf(x) return mi.MessageOf(x)
} }
// Deprecated: Use ServerMessage.ProtoReflect.Descriptor instead. // Deprecated: Use ActionBatch.ProtoReflect.Descriptor instead.
func (*ServerMessage) Descriptor() ([]byte, []int) { func (*ActionBatch) Descriptor() ([]byte, []int) {
return file_actions_proto_rawDescGZIP(), []int{1} return file_actions_actions_proto_rawDescGZIP(), []int{1}
} }
func (x *ServerMessage) GetPlayerId() int32 { func (x *ActionBatch) GetPlayerId() int32 {
if x != nil { if x != nil {
return x.PlayerId return x.PlayerId
} }
return 0 return 0
} }
func (x *ServerMessage) GetPlayers() []*PlayerState { func (x *ActionBatch) GetActions() []*Action {
if x != nil { if x != nil {
return x.Players return x.Actions
} }
return nil return nil
} }
type PlayerState struct { func (x *ActionBatch) GetTick() int64 {
state protoimpl.MessageState if x != nil {
sizeCache protoimpl.SizeCache return x.Tick
unknownFields protoimpl.UnknownFields }
return 0
}
PlayerId int32 `protobuf:"varint,1,opt,name=player_id,json=playerId,proto3" json:"player_id,omitempty"` func (x *ActionBatch) GetProtocolVersion() int32 {
X int32 `protobuf:"varint,2,opt,name=x,proto3" json:"x,omitempty"` if x != nil {
Y int32 `protobuf:"varint,3,opt,name=y,proto3" json:"y,omitempty"` return x.ProtocolVersion
}
return 0
}
func (x *ActionBatch) GetLastSeenMessageTimestamp() int64 {
if x != nil {
return x.LastSeenMessageTimestamp
}
return 0
}
type PlayerState struct {
state protoimpl.MessageState `protogen:"open.v1"`
PlayerId int32 `protobuf:"varint,1,opt,name=player_id,json=playerId,proto3" json:"player_id,omitempty"`
X int32 `protobuf:"varint,2,opt,name=x,proto3" json:"x,omitempty"`
Y int32 `protobuf:"varint,3,opt,name=y,proto3" json:"y,omitempty"`
Username string `protobuf:"bytes,4,opt,name=username,proto3" json:"username,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
} }
func (x *PlayerState) Reset() { func (x *PlayerState) Reset() {
*x = PlayerState{} *x = PlayerState{}
if protoimpl.UnsafeEnabled { mi := &file_actions_actions_proto_msgTypes[2]
mi := &file_actions_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi)
ms.StoreMessageInfo(mi)
}
} }
func (x *PlayerState) String() string { func (x *PlayerState) String() string {
@ -215,8 +268,8 @@ func (x *PlayerState) String() string {
func (*PlayerState) ProtoMessage() {} func (*PlayerState) ProtoMessage() {}
func (x *PlayerState) ProtoReflect() protoreflect.Message { func (x *PlayerState) ProtoReflect() protoreflect.Message {
mi := &file_actions_proto_msgTypes[2] mi := &file_actions_actions_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
@ -228,7 +281,7 @@ func (x *PlayerState) ProtoReflect() protoreflect.Message {
// Deprecated: Use PlayerState.ProtoReflect.Descriptor instead. // Deprecated: Use PlayerState.ProtoReflect.Descriptor instead.
func (*PlayerState) Descriptor() ([]byte, []int) { func (*PlayerState) Descriptor() ([]byte, []int) {
return file_actions_proto_rawDescGZIP(), []int{2} return file_actions_actions_proto_rawDescGZIP(), []int{2}
} }
func (x *PlayerState) GetPlayerId() int32 { func (x *PlayerState) GetPlayerId() int32 {
@ -252,125 +305,274 @@ func (x *PlayerState) GetY() int32 {
return 0 return 0
} }
var File_actions_proto protoreflect.FileDescriptor func (x *PlayerState) GetUsername() string {
if x != nil {
var file_actions_proto_rawDesc = []byte{ return x.Username
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, 0x89, 0x01, 0x0a, 0x06, 0x41, 0x63, 0x74, return ""
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,
0x79, 0x70, 0x65, 0x12, 0x0c, 0x0a, 0x01, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01,
0x78, 0x12, 0x0c, 0x0a, 0x01, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x79, 0x12,
0x1b, 0x0a, 0x09, 0x70, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01,
0x28, 0x05, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x49, 0x64, 0x22, 0x16, 0x0a, 0x0a,
0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x08, 0x0a, 0x04, 0x4d, 0x4f,
0x56, 0x45, 0x10, 0x00, 0x22, 0x5c, 0x0a, 0x0d, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4d, 0x65,
0x73, 0x73, 0x61, 0x67, 0x65, 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, 0x2e, 0x0a, 0x07, 0x70, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x50, 0x6c,
0x61, 0x79, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x07, 0x70, 0x6c, 0x61, 0x79, 0x65,
0x72, 0x73, 0x22, 0x46, 0x0a, 0x0b, 0x50, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74,
0x65, 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, 0x0c,
0x0a, 0x01, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x78, 0x12, 0x0c, 0x0a, 0x01,
0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x79, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69,
0x74, 0x65, 0x61, 0x2e, 0x62, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x62, 0x65, 0x2f, 0x62, 0x64, 0x6e,
0x75, 0x67, 0x67, 0x65, 0x74, 0x2f, 0x67, 0x6f, 0x6f, 0x6e, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72,
0x2f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
} }
type ChatMessage struct {
state protoimpl.MessageState `protogen:"open.v1"`
PlayerId int32 `protobuf:"varint,1,opt,name=player_id,json=playerId,proto3" json:"player_id,omitempty"`
Username string `protobuf:"bytes,2,opt,name=username,proto3" json:"username,omitempty"`
Content string `protobuf:"bytes,3,opt,name=content,proto3" json:"content,omitempty"`
Timestamp int64 `protobuf:"varint,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ChatMessage) Reset() {
*x = ChatMessage{}
mi := &file_actions_actions_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ChatMessage) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ChatMessage) ProtoMessage() {}
func (x *ChatMessage) ProtoReflect() protoreflect.Message {
mi := &file_actions_actions_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ChatMessage.ProtoReflect.Descriptor instead.
func (*ChatMessage) Descriptor() ([]byte, []int) {
return file_actions_actions_proto_rawDescGZIP(), []int{3}
}
func (x *ChatMessage) GetPlayerId() int32 {
if x != nil {
return x.PlayerId
}
return 0
}
func (x *ChatMessage) GetUsername() string {
if x != nil {
return x.Username
}
return ""
}
func (x *ChatMessage) GetContent() string {
if x != nil {
return x.Content
}
return ""
}
func (x *ChatMessage) GetTimestamp() int64 {
if x != nil {
return x.Timestamp
}
return 0
}
type ServerMessage struct {
state protoimpl.MessageState `protogen:"open.v1"`
PlayerId int32 `protobuf:"varint,1,opt,name=player_id,json=playerId,proto3" json:"player_id,omitempty"`
Players []*PlayerState `protobuf:"bytes,2,rep,name=players,proto3" json:"players,omitempty"`
CurrentTick int64 `protobuf:"varint,3,opt,name=current_tick,json=currentTick,proto3" json:"current_tick,omitempty"`
ChatMessages []*ChatMessage `protobuf:"bytes,4,rep,name=chat_messages,json=chatMessages,proto3" json:"chat_messages,omitempty"`
AuthSuccess bool `protobuf:"varint,5,opt,name=auth_success,json=authSuccess,proto3" json:"auth_success,omitempty"`
ErrorMessage string `protobuf:"bytes,6,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"`
ProtocolVersion int32 `protobuf:"varint,7,opt,name=protocol_version,json=protocolVersion,proto3" json:"protocol_version,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ServerMessage) Reset() {
*x = ServerMessage{}
mi := &file_actions_actions_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ServerMessage) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ServerMessage) ProtoMessage() {}
func (x *ServerMessage) ProtoReflect() protoreflect.Message {
mi := &file_actions_actions_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ServerMessage.ProtoReflect.Descriptor instead.
func (*ServerMessage) Descriptor() ([]byte, []int) {
return file_actions_actions_proto_rawDescGZIP(), []int{4}
}
func (x *ServerMessage) GetPlayerId() int32 {
if x != nil {
return x.PlayerId
}
return 0
}
func (x *ServerMessage) GetPlayers() []*PlayerState {
if x != nil {
return x.Players
}
return nil
}
func (x *ServerMessage) GetCurrentTick() int64 {
if x != nil {
return x.CurrentTick
}
return 0
}
func (x *ServerMessage) GetChatMessages() []*ChatMessage {
if x != nil {
return x.ChatMessages
}
return nil
}
func (x *ServerMessage) GetAuthSuccess() bool {
if x != nil {
return x.AuthSuccess
}
return false
}
func (x *ServerMessage) GetErrorMessage() string {
if x != nil {
return x.ErrorMessage
}
return ""
}
func (x *ServerMessage) GetProtocolVersion() int32 {
if x != nil {
return x.ProtocolVersion
}
return 0
}
var File_actions_actions_proto protoreflect.FileDescriptor
const file_actions_actions_proto_rawDesc = "" +
"\n" +
"\x15actions/actions.proto\x12\aactions\"\x97\x02\n" +
"\x06Action\x12.\n" +
"\x04type\x18\x01 \x01(\x0e2\x1a.actions.Action.ActionTypeR\x04type\x12\f\n" +
"\x01x\x18\x02 \x01(\x05R\x01x\x12\f\n" +
"\x01y\x18\x03 \x01(\x05R\x01y\x12\x1b\n" +
"\tplayer_id\x18\x04 \x01(\x05R\bplayerId\x12!\n" +
"\fchat_message\x18\x05 \x01(\tR\vchatMessage\x12\x1a\n" +
"\busername\x18\x06 \x01(\tR\busername\x12\x1a\n" +
"\bpassword\x18\a \x01(\tR\bpassword\"I\n" +
"\n" +
"ActionType\x12\b\n" +
"\x04MOVE\x10\x00\x12\b\n" +
"\x04CHAT\x10\x01\x12\x0e\n" +
"\n" +
"DISCONNECT\x10\x02\x12\t\n" +
"\x05LOGIN\x10\x03\x12\f\n" +
"\bREGISTER\x10\x04\"\xd3\x01\n" +
"\vActionBatch\x12\x1b\n" +
"\tplayer_id\x18\x01 \x01(\x05R\bplayerId\x12)\n" +
"\aactions\x18\x02 \x03(\v2\x0f.actions.ActionR\aactions\x12\x12\n" +
"\x04tick\x18\x03 \x01(\x03R\x04tick\x12)\n" +
"\x10protocol_version\x18\x04 \x01(\x05R\x0fprotocolVersion\x12=\n" +
"\x1blast_seen_message_timestamp\x18\x05 \x01(\x03R\x18lastSeenMessageTimestamp\"b\n" +
"\vPlayerState\x12\x1b\n" +
"\tplayer_id\x18\x01 \x01(\x05R\bplayerId\x12\f\n" +
"\x01x\x18\x02 \x01(\x05R\x01x\x12\f\n" +
"\x01y\x18\x03 \x01(\x05R\x01y\x12\x1a\n" +
"\busername\x18\x04 \x01(\tR\busername\"~\n" +
"\vChatMessage\x12\x1b\n" +
"\tplayer_id\x18\x01 \x01(\x05R\bplayerId\x12\x1a\n" +
"\busername\x18\x02 \x01(\tR\busername\x12\x18\n" +
"\acontent\x18\x03 \x01(\tR\acontent\x12\x1c\n" +
"\ttimestamp\x18\x04 \x01(\x03R\ttimestamp\"\xad\x02\n" +
"\rServerMessage\x12\x1b\n" +
"\tplayer_id\x18\x01 \x01(\x05R\bplayerId\x12.\n" +
"\aplayers\x18\x02 \x03(\v2\x14.actions.PlayerStateR\aplayers\x12!\n" +
"\fcurrent_tick\x18\x03 \x01(\x03R\vcurrentTick\x129\n" +
"\rchat_messages\x18\x04 \x03(\v2\x14.actions.ChatMessageR\fchatMessages\x12!\n" +
"\fauth_success\x18\x05 \x01(\bR\vauthSuccess\x12#\n" +
"\rerror_message\x18\x06 \x01(\tR\ferrorMessage\x12)\n" +
"\x10protocol_version\x18\a \x01(\x05R\x0fprotocolVersionB,Z*gitea.boner.be/bdnugget/goonserver/actionsb\x06proto3"
var ( var (
file_actions_proto_rawDescOnce sync.Once file_actions_actions_proto_rawDescOnce sync.Once
file_actions_proto_rawDescData = file_actions_proto_rawDesc file_actions_actions_proto_rawDescData []byte
) )
func file_actions_proto_rawDescGZIP() []byte { func file_actions_actions_proto_rawDescGZIP() []byte {
file_actions_proto_rawDescOnce.Do(func() { file_actions_actions_proto_rawDescOnce.Do(func() {
file_actions_proto_rawDescData = protoimpl.X.CompressGZIP(file_actions_proto_rawDescData) file_actions_actions_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_actions_actions_proto_rawDesc), len(file_actions_actions_proto_rawDesc)))
}) })
return file_actions_proto_rawDescData return file_actions_actions_proto_rawDescData
} }
var file_actions_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_actions_actions_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
var file_actions_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_actions_actions_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
var file_actions_proto_goTypes = []interface{}{ var file_actions_actions_proto_goTypes = []any{
(Action_ActionType)(0), // 0: actions.Action.ActionType (Action_ActionType)(0), // 0: actions.Action.ActionType
(*Action)(nil), // 1: actions.Action (*Action)(nil), // 1: actions.Action
(*ServerMessage)(nil), // 2: actions.ServerMessage (*ActionBatch)(nil), // 2: actions.ActionBatch
(*PlayerState)(nil), // 3: actions.PlayerState (*PlayerState)(nil), // 3: actions.PlayerState
(*ChatMessage)(nil), // 4: actions.ChatMessage
(*ServerMessage)(nil), // 5: actions.ServerMessage
} }
var file_actions_proto_depIdxs = []int32{ var file_actions_actions_proto_depIdxs = []int32{
0, // 0: actions.Action.type:type_name -> actions.Action.ActionType 0, // 0: actions.Action.type:type_name -> actions.Action.ActionType
3, // 1: actions.ServerMessage.players:type_name -> actions.PlayerState 1, // 1: actions.ActionBatch.actions:type_name -> actions.Action
2, // [2:2] is the sub-list for method output_type 3, // 2: actions.ServerMessage.players:type_name -> actions.PlayerState
2, // [2:2] is the sub-list for method input_type 4, // 3: actions.ServerMessage.chat_messages:type_name -> actions.ChatMessage
2, // [2:2] is the sub-list for extension type_name 4, // [4:4] is the sub-list for method output_type
2, // [2:2] is the sub-list for extension extendee 4, // [4:4] is the sub-list for method input_type
0, // [0:2] is the sub-list for field type_name 4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
} }
func init() { file_actions_proto_init() } func init() { file_actions_actions_proto_init() }
func file_actions_proto_init() { func file_actions_actions_proto_init() {
if File_actions_proto != nil { if File_actions_actions_proto != nil {
return return
} }
if !protoimpl.UnsafeEnabled {
file_actions_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Action); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_actions_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*ServerMessage); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_actions_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*PlayerState); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{ File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_actions_proto_rawDesc, RawDescriptor: unsafe.Slice(unsafe.StringData(file_actions_actions_proto_rawDesc), len(file_actions_actions_proto_rawDesc)),
NumEnums: 1, NumEnums: 1,
NumMessages: 3, NumMessages: 5,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },
GoTypes: file_actions_proto_goTypes, GoTypes: file_actions_actions_proto_goTypes,
DependencyIndexes: file_actions_proto_depIdxs, DependencyIndexes: file_actions_actions_proto_depIdxs,
EnumInfos: file_actions_proto_enumTypes, EnumInfos: file_actions_actions_proto_enumTypes,
MessageInfos: file_actions_proto_msgTypes, MessageInfos: file_actions_actions_proto_msgTypes,
}.Build() }.Build()
File_actions_proto = out.File File_actions_actions_proto = out.File
file_actions_proto_rawDesc = nil file_actions_actions_proto_goTypes = nil
file_actions_proto_goTypes = nil file_actions_actions_proto_depIdxs = nil
file_actions_proto_depIdxs = nil
} }

View File

@ -7,21 +7,49 @@ option go_package = "gitea.boner.be/bdnugget/goonserver/actions";
message Action { message Action {
enum ActionType { enum ActionType {
MOVE = 0; MOVE = 0;
CHAT = 1;
DISCONNECT = 2;
LOGIN = 3;
REGISTER = 4;
} }
ActionType type = 1; ActionType type = 1;
int32 x = 2; int32 x = 2;
int32 y = 3; int32 y = 3;
int32 player_id = 4; int32 player_id = 4;
string chat_message = 5;
string username = 6;
string password = 7;
} }
message ServerMessage { message ActionBatch {
int32 player_id = 1; // Only used when initially assigning player ID int32 player_id = 1;
repeated PlayerState players = 2; // Player state updates repeated Action actions = 2;
int64 tick = 3;
int32 protocol_version = 4;
int64 last_seen_message_timestamp = 5;
} }
message PlayerState { message PlayerState {
int32 player_id = 1; int32 player_id = 1;
int32 x = 2; int32 x = 2;
int32 y = 3; int32 y = 3;
string username = 4;
}
message ChatMessage {
int32 player_id = 1;
string username = 2;
string content = 3;
int64 timestamp = 4;
}
message ServerMessage {
int32 player_id = 1;
repeated PlayerState players = 2;
int64 current_tick = 3;
repeated ChatMessage chat_messages = 4;
bool auth_success = 5;
string error_message = 6;
int32 protocol_version = 7;
} }

188
db/db.go Normal file
View File

@ -0,0 +1,188 @@
package db
import (
"crypto/sha256"
"database/sql"
"encoding/hex"
"errors"
"fmt"
"sync"
"time"
_ "github.com/mattn/go-sqlite3"
)
var db *sql.DB
var (
ErrUserExists = errors.New("username already exists")
ErrInvalidCredentials = errors.New("invalid username or password")
)
const (
maxRegistrationsPerIP = 3 // Maximum registrations allowed per IP
registrationWindow = 24 * time.Hour // Time window for rate limiting
)
type registrationAttempt struct {
count int
firstTry time.Time
}
var (
registrationAttempts = make(map[string]*registrationAttempt)
rateLimitMutex sync.RWMutex
)
func CleanupOldAttempts() {
rateLimitMutex.Lock()
defer rateLimitMutex.Unlock()
now := time.Now()
for ip, attempt := range registrationAttempts {
if now.Sub(attempt.firstTry) > registrationWindow {
delete(registrationAttempts, ip)
}
}
}
func CheckRegistrationLimit(ip string) error {
rateLimitMutex.Lock()
defer rateLimitMutex.Unlock()
now := time.Now()
attempt, exists := registrationAttempts[ip]
if !exists {
registrationAttempts[ip] = &registrationAttempt{
count: 1,
firstTry: now,
}
return nil
}
// Reset if window has passed
if now.Sub(attempt.firstTry) > registrationWindow {
attempt.count = 1
attempt.firstTry = now
return nil
}
if attempt.count >= maxRegistrationsPerIP {
return fmt.Errorf("registration limit reached for this IP. Please try again in %v",
registrationWindow-now.Sub(attempt.firstTry))
}
attempt.count++
return nil
}
func InitDB(dbPath string) error {
var err error
db, err = sql.Open("sqlite3", dbPath)
if err != nil {
return err
}
// Create tables if they don't exist
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS players (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at DATETIME NOT NULL
);
CREATE TABLE IF NOT EXISTS player_states (
player_id INTEGER PRIMARY KEY,
x INTEGER NOT NULL,
y INTEGER NOT NULL,
last_seen DATETIME NOT NULL,
FOREIGN KEY(player_id) REFERENCES players(id)
);
`)
return err
}
func hashPassword(password string) string {
hash := sha256.Sum256([]byte(password))
return hex.EncodeToString(hash[:])
}
func RegisterPlayer(username, password string) (int, error) {
// Check if username exists
var exists bool
err := db.QueryRow("SELECT EXISTS(SELECT 1 FROM players WHERE username = ?)", username).Scan(&exists)
if err != nil {
return 0, err
}
if exists {
return 0, ErrUserExists
}
// Create new player
result, err := db.Exec(`
INSERT INTO players (username, password_hash, created_at)
VALUES (?, ?, ?)`,
username, hashPassword(password), time.Now().UTC(),
)
if err != nil {
return 0, err
}
id, err := result.LastInsertId()
return int(id), err
}
func AuthenticatePlayer(username, password string) (int, error) {
var id int
var storedHash string
err := db.QueryRow(`
SELECT id, password_hash
FROM players
WHERE username = ?`,
username,
).Scan(&id, &storedHash)
if err == sql.ErrNoRows {
return 0, ErrInvalidCredentials
}
if err != nil {
return 0, err
}
if storedHash != hashPassword(password) {
return 0, ErrInvalidCredentials
}
return id, nil
}
func SavePlayerState(playerID int, x, y int) error {
_, err := db.Exec(`
INSERT OR REPLACE INTO player_states (
player_id, x, y, last_seen
) VALUES (?, ?, ?, ?)`,
playerID, x, y, time.Now().UTC(),
)
return err
}
func LoadPlayerState(playerID int) (x, y int, err error) {
err = db.QueryRow(`
SELECT x, y FROM player_states
WHERE player_id = ?`,
playerID,
).Scan(&x, &y)
if err == sql.ErrNoRows {
// Return default position for new players
return 5, 5, nil
}
return x, y, err
}
func GetUsername(playerID int) (string, error) {
var username string
err := db.QueryRow("SELECT username FROM players WHERE id = ?", playerID).Scan(&username)
return username, err
}

5
go.mod
View File

@ -2,4 +2,7 @@ module gitea.boner.be/bdnugget/goonserver
go 1.23.0 go 1.23.0
require google.golang.org/protobuf v1.35.1 require (
github.com/mattn/go-sqlite3 v1.14.24
google.golang.org/protobuf v1.36.3
)

6
go.sum
View File

@ -1,6 +1,8 @@
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 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= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 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= 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.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=

BIN
goonserver Executable file

Binary file not shown.

471
main.go
View File

@ -1,30 +1,49 @@
package main package main
import ( import (
"bufio"
"encoding/binary"
"fmt" "fmt"
"io"
"log" "log"
"net" "net"
"sync"
"time" "time"
pb "gitea.boner.be/bdnugget/goonserver/actions" pb "gitea.boner.be/bdnugget/goonserver/actions"
"gitea.boner.be/bdnugget/goonserver/db"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
) )
const ( const (
port = ":6969" // Port to listen on port = ":6969" // Port to listen on
tickRate = 600 * time.Millisecond tickRate = 600 * time.Millisecond
protoVersion = 1
) )
type Player struct { type Player struct {
ID int sync.Mutex
X, Y int // Position on the game grid ID int
X, Y int
Username string
LastSeenMsgTimestamp int64 // Track the last message timestamp this player has seen
} }
var players = make(map[int]*Player) var (
var actionQueue = make(map[int][]*pb.Action) // Queue to store actions for each player players = make(map[int]*Player)
actionQueue = make(map[int][]*pb.Action) // Queue to store actions for each player
playerConns = make(map[int]net.Conn) // Map to store player connections
mu sync.RWMutex // Add mutex for protecting shared maps
chatHistory = make([]*pb.ChatMessage, 0, 100)
chatMutex sync.RWMutex
)
func main() { func main() {
if err := db.InitDB("goonserver.db"); err != nil {
log.Fatalf("Failed to initialize database: %v", err)
}
ln, err := net.Listen("tcp", port) ln, err := net.Listen("tcp", port)
if err != nil { if err != nil {
log.Fatalf("Failed to listen on port %s: %v", port, err) log.Fatalf("Failed to listen on port %s: %v", port, err)
@ -32,6 +51,20 @@ func main() {
defer ln.Close() defer ln.Close()
fmt.Printf("Server is listening on port %s\n", port) fmt.Printf("Server is listening on port %s\n", port)
// Create ticker for fixed game state updates
ticker := time.NewTicker(tickRate)
defer ticker.Stop()
// Start registration attempt cleanup goroutine
go func() {
ticker := time.NewTicker(time.Hour)
defer ticker.Stop()
for range ticker.C {
db.CleanupOldAttempts()
}
}()
// Handle incoming connections in a separate goroutine
go func() { go func() {
for { for {
conn, err := ln.Accept() conn, err := ln.Accept()
@ -43,69 +76,425 @@ func main() {
} }
}() }()
lastTick := time.Now() // Main game loop
for { for range ticker.C {
if time.Since(lastTick) >= tickRate { processActions()
lastTick = time.Now()
// log.Printf("Last tick: %s", lastTick)
processActions()
}
} }
} }
func handleConnection(conn net.Conn) { func handleConnection(conn net.Conn) {
defer conn.Close() defer func() {
conn.Close()
log.Printf("Connection closed and cleanup complete")
}()
// Assign a new player ID and add the player to the game state // Get client IP
playerID := len(players) + 1 remoteAddr := conn.RemoteAddr().String()
players[playerID] = &Player{ID: playerID, X: 5, Y: 5} // Start at default position ip, _, err := net.SplitHostPort(remoteAddr)
fmt.Printf("Player %d connected\n", playerID)
// Send player ID to the client
serverMsg := &pb.ServerMessage{
PlayerId: int32(playerID),
}
data, err := proto.Marshal(serverMsg)
if err != nil { if err != nil {
log.Printf("Failed to marshal ServerMessage for player %d: %v", playerID, err) log.Printf("Failed to parse remote address: %v", err)
return return
} }
if _, err := conn.Write(data); err != nil {
log.Printf("Failed to send player ID to player %d: %v", playerID, err) // Read initial message for player ID
reader := bufio.NewReader(conn)
// Wait for authentication
lengthBuf := make([]byte, 4)
if _, err := io.ReadFull(reader, lengthBuf); err != nil {
log.Printf("Failed to read auth message length: %v", err)
return return
} }
messageLength := binary.BigEndian.Uint32(lengthBuf)
messageBuf := make([]byte, messageLength)
if _, err := io.ReadFull(reader, messageBuf); err != nil {
log.Printf("Failed to read auth message: %v", err)
return
}
batch := &pb.ActionBatch{}
if err := proto.Unmarshal(messageBuf, batch); err != nil {
log.Printf("Failed to unmarshal auth message: %v", err)
return
}
if len(batch.Actions) == 0 {
log.Printf("No auth action received")
return
}
action := batch.Actions[0]
var playerID int
var authErr error
if batch.ProtocolVersion == 0 {
response := &pb.ServerMessage{
AuthSuccess: false,
ErrorMessage: "Client using outdated protocol (pre-versioning)",
ProtocolVersion: protoVersion,
}
writeMessage(conn, response)
return
}
if batch.ProtocolVersion < protoVersion {
response := &pb.ServerMessage{
AuthSuccess: false,
ErrorMessage: fmt.Sprintf("Client protocol version too old (client: %d, required: %d)", batch.ProtocolVersion, protoVersion),
ProtocolVersion: protoVersion,
}
writeMessage(conn, response)
return
}
switch action.Type {
case pb.Action_REGISTER:
if err := db.CheckRegistrationLimit(ip); err != nil {
response := &pb.ServerMessage{
AuthSuccess: false,
ErrorMessage: err.Error(),
}
writeMessage(conn, response)
return
}
playerID, authErr = db.RegisterPlayer(action.Username, action.Password)
case pb.Action_LOGIN:
playerID, authErr = db.AuthenticatePlayer(action.Username, action.Password)
default:
log.Printf("Invalid initial action type: %v", action.Type)
return
}
// Send auth response
response := &pb.ServerMessage{
PlayerId: int32(playerID),
AuthSuccess: authErr == nil,
}
if authErr != nil {
response.ErrorMessage = authErr.Error()
if err := writeMessage(conn, response); err != nil {
log.Printf("Failed to send auth response: %v", err)
}
return
}
// Load last known position
x, y, err := db.LoadPlayerState(playerID)
if err != nil {
log.Printf("Error loading state for player %d: %v", playerID, err)
x, y = 5, 5 // Default position
}
username, err := db.GetUsername(playerID)
if err != nil {
log.Printf("Error getting username for player %d: %v", playerID, err)
return
}
log.Printf("Player %d (%s) authenticated successfully, checking for existing session", playerID, username)
// Check for existing session and force disconnect if needed
mu.Lock()
existingPlayer, alreadyLoggedIn := players[playerID]
if alreadyLoggedIn {
log.Printf("Player %d (%s) is already logged in, forcing disconnect of old session", playerID, username)
// An existing session is found - clean it up
if oldConn, exists := playerConns[playerID]; exists {
// Try to close the old connection
oldConn.Close()
delete(playerConns, playerID)
}
// Keep the player object but update its connection
existingPlayer.X = x
existingPlayer.Y = y
playerConns[playerID] = conn
mu.Unlock()
} else {
// Create a new player
player := &Player{
ID: playerID,
X: x,
Y: y,
Username: username,
LastSeenMsgTimestamp: 0, // Initialize to 0 to receive all messages initially
}
players[playerID] = player
playerConns[playerID] = conn
mu.Unlock()
existingPlayer = player
// Announce connection
addSystemMessage(fmt.Sprintf("%s connected", username))
}
// Ensure player state is saved on any kind of disconnect
defer func() {
if p, exists := players[playerID]; exists {
if err := db.SavePlayerState(playerID, p.X, p.Y); err != nil {
log.Printf("Error saving state for player %d: %v", playerID, err)
}
}
addSystemMessage(fmt.Sprintf("%s disconnected", username))
mu.Lock()
delete(players, playerID)
delete(playerConns, playerID)
delete(actionQueue, playerID)
mu.Unlock()
log.Printf("Player %d (%s) disconnected", playerID, username)
}()
// Send initial state with correct position
response = &pb.ServerMessage{
PlayerId: int32(playerID),
AuthSuccess: true,
Players: []*pb.PlayerState{{
PlayerId: int32(playerID),
X: int32(x),
Y: int32(y),
Username: username,
}},
ProtocolVersion: protoVersion,
}
// Send player ID to client
if err := writeMessage(conn, response); err != nil {
log.Printf("Failed to send player ID: %v", err)
return
}
log.Printf("Player %d (%s) connected successfully", playerID, username)
// Listen for incoming actions from this player // Listen for incoming actions from this player
buf := make([]byte, 4096)
for { for {
n, err := conn.Read(buf) // Read message length
if err != nil { lengthBuf := make([]byte, 4)
log.Printf("Error reading from player %d: %v", playerID, err) if _, err := io.ReadFull(reader, lengthBuf); err != nil {
delete(players, playerID) if err == io.EOF {
log.Printf("Player %d disconnected gracefully", playerID)
} else {
log.Printf("Error reading message length from player %d: %v", playerID, err)
}
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)
return return
} }
action := &pb.Action{} batch := &pb.ActionBatch{}
if err := proto.Unmarshal(buf[:n], action); err != nil { if err := proto.Unmarshal(messageBuf, batch); err != nil {
log.Printf("Failed to unmarshal action for player %d: %v", playerID, err) log.Printf("Failed to unmarshal action batch for player %d: %v", playerID, err)
continue continue
} }
// Queue the action for processing in the game loop // Update the last seen message timestamp
actionQueue[playerID] = append(actionQueue[playerID], action) if batch.LastSeenMessageTimestamp > 0 {
existingPlayer.LastSeenMsgTimestamp = batch.LastSeenMessageTimestamp
}
// 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 requested disconnect", playerID)
return
}
}
mu.Lock()
actionQueue[playerID] = append(actionQueue[playerID], batch.Actions...)
mu.Unlock()
}
} }
} }
func addChatMessage(playerID int32, content string) {
player, exists := players[int(playerID)]
if !exists {
return
}
chatMutex.Lock()
defer chatMutex.Unlock()
msg := &pb.ChatMessage{
PlayerId: playerID,
Username: player.Username,
Content: content,
Timestamp: time.Now().UnixNano(),
}
if len(chatHistory) >= 100 {
chatHistory = chatHistory[1:]
}
chatHistory = append(chatHistory, msg)
}
func addSystemMessage(content string) {
chatMutex.Lock()
defer chatMutex.Unlock()
msg := &pb.ChatMessage{
PlayerId: 0, // System messages use ID 0
Username: "System",
Content: content,
Timestamp: time.Now().UnixNano(),
}
if len(chatHistory) >= 100 {
chatHistory = chatHistory[1:]
}
chatHistory = append(chatHistory, msg)
}
func processActions() { func processActions() {
for playerID, actions := range actionQueue { mu.Lock()
player := players[playerID] // Make a list of players to process first, to avoid lock contention
activePlayers := make(map[int]*Player)
for id, p := range players {
activePlayers[id] = p
}
activeConns := make(map[int]net.Conn)
for id, conn := range playerConns {
activeConns[id] = conn
}
activeQueues := make(map[int][]*pb.Action)
for id, actions := range actionQueue {
if len(actions) > 0 {
activeQueues[id] = actions
actionQueue[id] = nil // Clear the queue early to avoid double processing
}
}
mu.Unlock()
// Process actions without holding the global lock
for playerID, actions := range activeQueues {
player, exists := activePlayers[playerID]
if !exists {
continue
}
player.Lock()
for _, action := range actions { for _, action := range actions {
if action.Type == pb.Action_MOVE { switch action.Type {
case pb.Action_MOVE:
player.X = int(action.X) player.X = int(action.X)
player.Y = int(action.Y) player.Y = int(action.Y)
fmt.Printf("Player %d moved to (%d, %d)\n", playerID, player.X, player.Y) fmt.Printf("Player %d moved to (%d, %d)\n", playerID, player.X, player.Y)
case pb.Action_CHAT:
addChatMessage(int32(playerID), action.ChatMessage)
fmt.Printf("Player %d says: %s\n", playerID, action.ChatMessage)
} }
} }
actionQueue[playerID] = nil // Clear the queue after processing player.Unlock()
}
// Prepare current game state
currentTick := time.Now().UnixNano() / int64(tickRate)
// Get recent messages for new connections
chatMutex.RLock()
recentMessages := chatHistory[max(0, len(chatHistory)-5):] // Get last 5 for new connections
chatMutex.RUnlock()
// To avoid holding locks too long, prepare player states first
playerStates := make([]*pb.PlayerState, 0, len(activePlayers))
for id, p := range activePlayers {
p.Lock()
playerStates = append(playerStates, &pb.PlayerState{
PlayerId: int32(id),
X: int32(p.X),
Y: int32(p.Y),
Username: p.Username,
})
p.Unlock()
}
// Now send updates to each player
for playerID, conn := range activeConns {
player, exists := activePlayers[playerID]
if !exists {
continue
}
state := &pb.ServerMessage{
CurrentTick: currentTick,
Players: playerStates,
}
// Add chat messages - only send those the player hasn't seen
player.Lock()
lastSeen := player.LastSeenMsgTimestamp
player.Unlock()
chatMutex.RLock()
var newMessages []*pb.ChatMessage
// For new connections, send the 5 most recent messages
if lastSeen == 0 && len(recentMessages) > 0 {
newMessages = recentMessages
if len(newMessages) > 0 {
// Update the player's timestamp to the latest message
player.Lock()
player.LastSeenMsgTimestamp = newMessages[len(newMessages)-1].Timestamp
player.Unlock()
}
} else {
// For existing connections, only send new messages
for _, msg := range chatHistory {
if msg.Timestamp > lastSeen {
newMessages = append(newMessages, msg)
}
}
// Update the player's timestamp if we sent them new messages
if len(newMessages) > 0 {
player.Lock()
player.LastSeenMsgTimestamp = newMessages[len(newMessages)-1].Timestamp
player.Unlock()
}
}
state.ChatMessages = newMessages
chatMutex.RUnlock()
// Log the number of messages we're sending
if len(newMessages) > 0 {
log.Printf("Sending %d new messages to player %d", len(newMessages), playerID)
}
// Send the state to the player - do this without holding any locks
if err := writeMessage(conn, state); err != nil {
log.Printf("Failed to send update to player %d: %v", playerID, err)
// Handle connection errors by removing the player
mu.Lock()
delete(players, playerID)
delete(playerConns, playerID)
delete(actionQueue, playerID)
mu.Unlock()
}
} }
} }
// 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
}