From 151ac6ab8e8d2b753d8d6a1f51bffeccc2087f6b Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Tue, 9 Sep 2025 23:00:09 -0500 Subject: [PATCH] refactor server for split --- server/cmd/login/main.go | 234 ++++++++++++++ server/cmd/world/main.go | 398 ++++++++++++++++++++++++ server/go.mod | 7 + server/go.sum | 6 + server/internal/db/db.go | 183 +++++++++++ server/internal/db/models.go | 30 ++ server/{ => internal}/net/packets.go | 0 server/internal/net/packets/messages.go | 80 +++++ server/internal/net/packets/world.go | 77 +++++ server/{ => internal}/net/server.go | 0 server/{ => internal}/net/world.go | 0 server/main.go | 210 ------------- 12 files changed, 1015 insertions(+), 210 deletions(-) create mode 100644 server/cmd/login/main.go create mode 100644 server/cmd/world/main.go create mode 100644 server/go.sum create mode 100644 server/internal/db/db.go create mode 100644 server/internal/db/models.go rename server/{ => internal}/net/packets.go (100%) create mode 100644 server/internal/net/packets/messages.go create mode 100644 server/internal/net/packets/world.go rename server/{ => internal}/net/server.go (100%) rename server/{ => internal}/net/world.go (100%) delete mode 100644 server/main.go diff --git a/server/cmd/login/main.go b/server/cmd/login/main.go new file mode 100644 index 0000000..f7a316d --- /dev/null +++ b/server/cmd/login/main.go @@ -0,0 +1,234 @@ +package main + +import ( + "crypto/rand" + "encoding/hex" + "encoding/json" + "flag" + "fmt" + "log" + "net" + "server/internal/db" + "time" + + "golang.org/x/crypto/bcrypt" +) + +type LoginServer struct { + listener net.Listener + database *db.Database + worldURL string + serverID string +} + +type LoginRequest struct { + Type string `json:"type"` + Username string `json:"username"` + Password string `json:"password"` +} + +type LoginResponse struct { + Type string `json:"type"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Token string `json:"token,omitempty"` + WorldURL string `json:"worldUrl,omitempty"` + PlayerID int64 `json:"playerId,omitempty"` +} + +type RegisterRequest struct { + Type string `json:"type"` + Username string `json:"username"` + Password string `json:"password"` +} + +type RegisterResponse struct { + Type string `json:"type"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` +} + +func main() { + var ( + port = flag.String("port", "8081", "Login server port") + dbDSN = flag.String("db", "user:password@tcp(localhost:3306)/game", "Database DSN") + worldURL = flag.String("world", "localhost:8082", "World server URL") + ) + flag.Parse() + + database, err := db.New(*dbDSN) + if err != nil { + log.Fatalf("Failed to connect to database: %v", err) + } + defer database.Close() + + server := &LoginServer{ + database: database, + worldURL: *worldURL, + serverID: generateServerID(), + } + + listener, err := net.Listen("tcp", ":"+*port) + if err != nil { + log.Fatalf("Failed to start login server: %v", err) + } + server.listener = listener + + log.Printf("Login server started on port %s (ID: %s)", *port, server.serverID) + + go server.cleanupSessions() + + for { + conn, err := listener.Accept() + if err != nil { + log.Printf("Accept error: %v", err) + continue + } + go server.handleConnection(conn) + } +} + +func (s *LoginServer) handleConnection(conn net.Conn) { + defer conn.Close() + + decoder := json.NewDecoder(conn) + encoder := json.NewEncoder(conn) + + for { + var msg json.RawMessage + if err := decoder.Decode(&msg); err != nil { + return + } + + var baseMsg struct { + Type string `json:"type"` + } + if err := json.Unmarshal(msg, &baseMsg); err != nil { + continue + } + + switch baseMsg.Type { + case "login": + var req LoginRequest + if err := json.Unmarshal(msg, &req); err != nil { + continue + } + s.handleLogin(req, encoder) + + case "register": + var req RegisterRequest + if err := json.Unmarshal(msg, &req); err != nil { + continue + } + s.handleRegister(req, encoder) + } + } +} + +func (s *LoginServer) handleLogin(req LoginRequest, encoder *json.Encoder) { + player, err := s.database.GetPlayerByUsername(req.Username) + if err != nil { + encoder.Encode(LoginResponse{ + Type: "loginResponse", + Success: false, + Message: "Invalid username or password", + }) + return + } + + if err := bcrypt.CompareHashAndPassword([]byte(player.PasswordHash), []byte(req.Password)); err != nil { + encoder.Encode(LoginResponse{ + Type: "loginResponse", + Success: false, + Message: "Invalid username or password", + }) + return + } + + token := generateToken() + if err := s.database.CreateSession(player.ID, token, "world", s.serverID, 24*time.Hour); err != nil { + encoder.Encode(LoginResponse{ + Type: "loginResponse", + Success: false, + Message: "Failed to create session", + }) + return + } + + s.database.UpdateLastLogin(player.ID) + + encoder.Encode(LoginResponse{ + Type: "loginResponse", + Success: true, + Token: token, + WorldURL: s.worldURL, + PlayerID: player.ID, + }) +} + +func (s *LoginServer) handleRegister(req RegisterRequest, encoder *json.Encoder) { + if len(req.Username) < 3 || len(req.Username) > 20 { + encoder.Encode(RegisterResponse{ + Type: "registerResponse", + Success: false, + Message: "Username must be between 3 and 20 characters", + }) + return + } + + if len(req.Password) < 6 { + encoder.Encode(RegisterResponse{ + Type: "registerResponse", + Success: false, + Message: "Password must be at least 6 characters", + }) + return + } + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + if err != nil { + encoder.Encode(RegisterResponse{ + Type: "registerResponse", + Success: false, + Message: "Failed to process password", + }) + return + } + + _, err = s.database.CreatePlayer(req.Username, string(hashedPassword)) + if err != nil { + encoder.Encode(RegisterResponse{ + Type: "registerResponse", + Success: false, + Message: "Username already exists", + }) + return + } + + encoder.Encode(RegisterResponse{ + Type: "registerResponse", + Success: true, + Message: "Registration successful", + }) +} + +func (s *LoginServer) cleanupSessions() { + ticker := time.NewTicker(1 * time.Hour) + defer ticker.Stop() + + for range ticker.C { + if err := s.database.CleanExpiredSessions(); err != nil { + log.Printf("Failed to clean expired sessions: %v", err) + } + } +} + +func generateToken() string { + b := make([]byte, 32) + rand.Read(b) + return hex.EncodeToString(b) +} + +func generateServerID() string { + return fmt.Sprintf("login-%d", time.Now().Unix()) +} \ No newline at end of file diff --git a/server/cmd/world/main.go b/server/cmd/world/main.go new file mode 100644 index 0000000..5f7e118 --- /dev/null +++ b/server/cmd/world/main.go @@ -0,0 +1,398 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "log" + "math" + "net" + "server/internal/db" + "server/internal/net/packets" + "sync" + "time" +) + +type WorldServer struct { + listener net.Listener + database *db.Database + serverID string + clients map[int]*Client + clientLock sync.RWMutex + world *packets.WorldConfig + heightmap [][]float32 + ticker *time.Ticker + timeOfDay float32 +} + +type Client struct { + ID int + PlayerID int64 + Username string + Conn net.Conn + Encoder *json.Encoder + Decoder *json.Decoder + Position packets.Vec3 + Rotation packets.Vec2 + Velocity packets.Vec3 + LastUpdate time.Time +} + +type AuthRequest struct { + Type string `json:"type"` + Token string `json:"token"` +} + +type AuthResponse struct { + Type string `json:"type"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` +} + +func main() { + var ( + port = flag.String("port", "8082", "World server port") + dbDSN = flag.String("db", "user:password@tcp(localhost:3306)/game", "Database DSN") + ) + flag.Parse() + + database, err := db.New(*dbDSN) + if err != nil { + log.Fatalf("Failed to connect to database: %v", err) + } + defer database.Close() + + heightmap := generateDefaultHeightmap(257) + worldConfig := packets.NewWorldConfig(heightmap, 1.0) + + server := &WorldServer{ + database: database, + serverID: generateServerID(), + clients: make(map[int]*Client), + world: worldConfig, + heightmap: heightmap, + timeOfDay: 12.0, + } + + listener, err := net.Listen("tcp", ":"+*port) + if err != nil { + log.Fatalf("Failed to start world server: %v", err) + } + server.listener = listener + + log.Printf("World server started on port %s (ID: %s)", *port, server.serverID) + log.Printf("World bounds: Min(%v), Max(%v)", server.world.MinBounds, server.world.MaxBounds) + + go server.gameLoop() + go server.updateTimeOfDay() + + for { + conn, err := listener.Accept() + if err != nil { + log.Printf("Accept error: %v", err) + continue + } + go server.handleConnection(conn) + } +} + +func (s *WorldServer) handleConnection(conn net.Conn) { + defer conn.Close() + + decoder := json.NewDecoder(conn) + encoder := json.NewEncoder(conn) + + var authReq AuthRequest + if err := decoder.Decode(&authReq); err != nil || authReq.Type != "auth" { + encoder.Encode(AuthResponse{ + Type: "authResponse", + Success: false, + Message: "Authentication required", + }) + return + } + + session, err := s.database.ValidateSession(authReq.Token) + if err != nil { + encoder.Encode(AuthResponse{ + Type: "authResponse", + Success: false, + Message: "Invalid or expired session", + }) + return + } + + player, err := s.database.GetPlayerByID(session.PlayerID) + if err != nil { + encoder.Encode(AuthResponse{ + Type: "authResponse", + Success: false, + Message: "Player not found", + }) + return + } + + encoder.Encode(AuthResponse{ + Type: "authResponse", + Success: true, + }) + + position, _ := s.database.GetPlayerPosition(player.ID) + + client := &Client{ + ID: s.getNextClientID(), + PlayerID: player.ID, + Username: player.Username, + Conn: conn, + Encoder: encoder, + Decoder: decoder, + Position: packets.Vec3{X: position.X, Y: position.Y, Z: position.Z}, + Rotation: packets.Vec2{X: position.Yaw, Y: position.Pitch}, + LastUpdate: time.Now(), + } + + s.addClient(client) + defer s.removeClient(client) + + s.sendInitialState(client) + s.broadcastPlayerJoined(client) + + for { + var msg json.RawMessage + if err := decoder.Decode(&msg); err != nil { + break + } + + var baseMsg struct { + Type string `json:"type"` + } + if err := json.Unmarshal(msg, &baseMsg); err != nil { + continue + } + + switch baseMsg.Type { + case "movement": + var moveMsg packets.MovementMessage + if err := json.Unmarshal(msg, &moveMsg); err != nil { + continue + } + s.handleMovement(client, moveMsg) + + case "chat": + var chatMsg packets.ChatMessage + if err := json.Unmarshal(msg, &chatMsg); err != nil { + continue + } + s.handleChat(client, chatMsg) + } + } + + s.database.SavePlayerPosition(client.PlayerID, &db.Position{ + X: client.Position.X, + Y: client.Position.Y, + Z: client.Position.Z, + Yaw: client.Rotation.X, + Pitch: client.Rotation.Y, + World: "main", + }) +} + +func (s *WorldServer) handleMovement(client *Client, msg packets.MovementMessage) { + newPos := packets.Vec3{X: msg.X, Y: msg.Y, Z: msg.Z} + + if !s.world.IsInBounds(newPos) { + newPos = s.world.ClampPosition(newPos) + client.Encoder.Encode(packets.PositionCorrectionMessage{ + Type: "positionCorrection", + X: newPos.X, + Y: newPos.Y, + Z: newPos.Z, + }) + } + + terrainHeight := s.world.GetHeightAt(s.heightmap, newPos.X, newPos.Z) + if newPos.Y < terrainHeight { + newPos.Y = terrainHeight + client.Encoder.Encode(packets.PositionCorrectionMessage{ + Type: "positionCorrection", + X: newPos.X, + Y: newPos.Y, + Z: newPos.Z, + }) + } + + client.Position = newPos + client.Rotation = packets.Vec2{X: msg.Yaw, Y: msg.Pitch} + client.Velocity = packets.Vec3{X: msg.VelX, Y: msg.VelY, Z: msg.VelZ} + client.LastUpdate = time.Now() + + s.broadcastMovement(client) +} + +func (s *WorldServer) handleChat(client *Client, msg packets.ChatMessage) { + fullMsg := packets.ChatMessage{ + Type: "chat", + Username: client.Username, + Message: msg.Message, + } + s.broadcast(fullMsg) +} + +func (s *WorldServer) sendInitialState(client *Client) { + client.Encoder.Encode(packets.InitMessage{ + Type: "init", + PlayerID: client.ID, + X: client.Position.X, + Y: client.Position.Y, + Z: client.Position.Z, + Yaw: client.Rotation.X, + Pitch: client.Rotation.Y, + TimeOfDay: s.timeOfDay, + }) + + s.clientLock.RLock() + for _, other := range s.clients { + if other.ID != client.ID { + client.Encoder.Encode(packets.PlayerJoinedMessage{ + Type: "playerJoined", + PlayerID: other.ID, + Username: other.Username, + X: other.Position.X, + Y: other.Position.Y, + Z: other.Position.Z, + }) + } + } + s.clientLock.RUnlock() +} + +func (s *WorldServer) broadcastPlayerJoined(client *Client) { + msg := packets.PlayerJoinedMessage{ + Type: "playerJoined", + PlayerID: client.ID, + Username: client.Username, + X: client.Position.X, + Y: client.Position.Y, + Z: client.Position.Z, + } + s.broadcastExcept(msg, client.ID) +} + +func (s *WorldServer) broadcastMovement(client *Client) { + msg := packets.PlayerMovementMessage{ + Type: "playerMovement", + PlayerID: client.ID, + X: client.Position.X, + Y: client.Position.Y, + Z: client.Position.Z, + Yaw: client.Rotation.X, + Pitch: client.Rotation.Y, + VelX: client.Velocity.X, + VelY: client.Velocity.Y, + VelZ: client.Velocity.Z, + } + s.broadcastExcept(msg, client.ID) +} + +func (s *WorldServer) broadcast(msg interface{}) { + s.clientLock.RLock() + defer s.clientLock.RUnlock() + + for _, client := range s.clients { + client.Encoder.Encode(msg) + } +} + +func (s *WorldServer) broadcastExcept(msg interface{}, excludeID int) { + s.clientLock.RLock() + defer s.clientLock.RUnlock() + + for _, client := range s.clients { + if client.ID != excludeID { + client.Encoder.Encode(msg) + } + } +} + +func (s *WorldServer) addClient(client *Client) { + s.clientLock.Lock() + s.clients[client.ID] = client + s.clientLock.Unlock() + log.Printf("Player %s (ID: %d) joined the world", client.Username, client.ID) +} + +func (s *WorldServer) removeClient(client *Client) { + s.clientLock.Lock() + delete(s.clients, client.ID) + s.clientLock.Unlock() + + s.broadcast(packets.PlayerLeftMessage{ + Type: "playerLeft", + PlayerID: client.ID, + }) + log.Printf("Player %s (ID: %d) left the world", client.Username, client.ID) +} + +func (s *WorldServer) gameLoop() { + ticker := time.NewTicker(50 * time.Millisecond) + defer ticker.Stop() + + for range ticker.C { + s.clientLock.RLock() + for _, client := range s.clients { + if time.Since(client.LastUpdate) > 30*time.Second { + go func(c *Client) { + c.Conn.Close() + }(client) + } + } + s.clientLock.RUnlock() + } +} + +func (s *WorldServer) updateTimeOfDay() { + ticker := time.NewTicker(10 * time.Second) + defer ticker.Stop() + + for range ticker.C { + s.timeOfDay += 0.1 + if s.timeOfDay >= 24.0 { + s.timeOfDay -= 24.0 + } + + s.broadcast(packets.TimeUpdateMessage{ + Type: "timeUpdate", + TimeOfDay: s.timeOfDay, + }) + } +} + +func (s *WorldServer) getNextClientID() int { + s.clientLock.Lock() + defer s.clientLock.Unlock() + + id := 1 + for { + if _, exists := s.clients[id]; !exists { + return id + } + id++ + } +} + +func generateDefaultHeightmap(size int) [][]float32 { + heightmap := make([][]float32, size) + for i := range heightmap { + heightmap[i] = make([]float32, size) + for j := range heightmap[i] { + x := float64(j-size/2) / float64(size) * 10 + z := float64(i-size/2) / float64(size) * 10 + heightmap[i][j] = float32(10 * (math.Sin(x) + math.Cos(z))) + } + } + return heightmap +} + +func generateServerID() string { + return fmt.Sprintf("world-%d", time.Now().Unix()) +} \ No newline at end of file diff --git a/server/go.mod b/server/go.mod index c2f0746..d879b96 100644 --- a/server/go.mod +++ b/server/go.mod @@ -1,3 +1,10 @@ module server go 1.25.0 + +require ( + github.com/go-sql-driver/mysql v1.9.3 + golang.org/x/crypto v0.42.0 +) + +require filippo.io/edwards25519 v1.1.0 // indirect diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..6f1abe3 --- /dev/null +++ b/server/go.sum @@ -0,0 +1,6 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= diff --git a/server/internal/db/db.go b/server/internal/db/db.go new file mode 100644 index 0000000..0e100cd --- /dev/null +++ b/server/internal/db/db.go @@ -0,0 +1,183 @@ +package db + +import ( + "database/sql" + "fmt" + "time" + + _ "github.com/go-sql-driver/mysql" +) + +type Database struct { + conn *sql.DB +} + +func New(dsn string) (*Database, error) { + conn, err := sql.Open("mysql", dsn) + if err != nil { + return nil, fmt.Errorf("failed to open database: %w", err) + } + + conn.SetMaxOpenConns(25) + conn.SetMaxIdleConns(5) + conn.SetConnMaxLifetime(5 * time.Minute) + + if err := conn.Ping(); err != nil { + return nil, fmt.Errorf("failed to ping database: %w", err) + } + + db := &Database{conn: conn} + if err := db.createTables(); err != nil { + return nil, fmt.Errorf("failed to create tables: %w", err) + } + + return db, nil +} + +func (db *Database) createTables() error { + queries := []string{ + `CREATE TABLE IF NOT EXISTS players ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_login TIMESTAMP NULL, + INDEX idx_username (username) + )`, + `CREATE TABLE IF NOT EXISTS player_positions ( + player_id INT PRIMARY KEY, + x FLOAT NOT NULL DEFAULT 0, + y FLOAT NOT NULL DEFAULT 0, + z FLOAT NOT NULL DEFAULT 0, + yaw FLOAT NOT NULL DEFAULT 0, + pitch FLOAT NOT NULL DEFAULT 0, + world VARCHAR(255) DEFAULT 'main', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (player_id) REFERENCES players(id) ON DELETE CASCADE + )`, + `CREATE TABLE IF NOT EXISTS player_sessions ( + id INT AUTO_INCREMENT PRIMARY KEY, + player_id INT NOT NULL, + token VARCHAR(255) UNIQUE NOT NULL, + server_type VARCHAR(50) NOT NULL, + server_id VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + expires_at TIMESTAMP NOT NULL, + INDEX idx_token (token), + INDEX idx_player (player_id), + FOREIGN KEY (player_id) REFERENCES players(id) ON DELETE CASCADE + )`, + } + + for _, query := range queries { + if _, err := db.conn.Exec(query); err != nil { + return fmt.Errorf("failed to execute query: %w", err) + } + } + + return nil +} + +func (db *Database) Close() error { + return db.conn.Close() +} + +func (db *Database) CreatePlayer(username, passwordHash string) (int64, error) { + result, err := db.conn.Exec( + "INSERT INTO players (username, password_hash) VALUES (?, ?)", + username, passwordHash, + ) + if err != nil { + return 0, err + } + return result.LastInsertId() +} + +func (db *Database) GetPlayerByUsername(username string) (*Player, error) { + p := &Player{} + err := db.conn.QueryRow( + "SELECT id, username, password_hash FROM players WHERE username = ?", + username, + ).Scan(&p.ID, &p.Username, &p.PasswordHash) + if err != nil { + return nil, err + } + return p, nil +} + +func (db *Database) GetPlayerByID(playerID int64) (*Player, error) { + p := &Player{} + err := db.conn.QueryRow( + "SELECT id, username, password_hash FROM players WHERE id = ?", + playerID, + ).Scan(&p.ID, &p.Username, &p.PasswordHash) + if err != nil { + return nil, err + } + return p, nil +} + +func (db *Database) UpdateLastLogin(playerID int64) error { + _, err := db.conn.Exec( + "UPDATE players SET last_login = CURRENT_TIMESTAMP WHERE id = ?", + playerID, + ) + return err +} + +func (db *Database) SavePlayerPosition(playerID int64, pos *Position) error { + _, err := db.conn.Exec(` + INSERT INTO player_positions (player_id, x, y, z, yaw, pitch, world) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + x = VALUES(x), y = VALUES(y), z = VALUES(z), + yaw = VALUES(yaw), pitch = VALUES(pitch), world = VALUES(world) + `, playerID, pos.X, pos.Y, pos.Z, pos.Yaw, pos.Pitch, pos.World) + return err +} + +func (db *Database) GetPlayerPosition(playerID int64) (*Position, error) { + pos := &Position{} + err := db.conn.QueryRow( + "SELECT x, y, z, yaw, pitch, world FROM player_positions WHERE player_id = ?", + playerID, + ).Scan(&pos.X, &pos.Y, &pos.Z, &pos.Yaw, &pos.Pitch, &pos.World) + if err == sql.ErrNoRows { + return &Position{X: 0, Y: 0, Z: 0, Yaw: 0, Pitch: 0, World: "main"}, nil + } + return pos, err +} + +func (db *Database) CreateSession(playerID int64, token, serverType, serverID string, duration time.Duration) error { + expiresAt := time.Now().Add(duration) + _, err := db.conn.Exec( + `INSERT INTO player_sessions (player_id, token, server_type, server_id, expires_at) + VALUES (?, ?, ?, ?, ?)`, + playerID, token, serverType, serverID, expiresAt, + ) + return err +} + +func (db *Database) ValidateSession(token string) (*Session, error) { + s := &Session{} + err := db.conn.QueryRow( + `SELECT id, player_id, token, server_type, server_id, expires_at + FROM player_sessions + WHERE token = ? AND expires_at > NOW()`, + token, + ).Scan(&s.ID, &s.PlayerID, &s.Token, &s.ServerType, &s.ServerID, &s.ExpiresAt) + if err != nil { + return nil, err + } + return s, nil +} + +func (db *Database) DeleteSession(token string) error { + _, err := db.conn.Exec("DELETE FROM player_sessions WHERE token = ?", token) + return err +} + +func (db *Database) CleanExpiredSessions() error { + _, err := db.conn.Exec("DELETE FROM player_sessions WHERE expires_at < NOW()") + return err +} diff --git a/server/internal/db/models.go b/server/internal/db/models.go new file mode 100644 index 0000000..1dc1114 --- /dev/null +++ b/server/internal/db/models.go @@ -0,0 +1,30 @@ +package db + +import "time" + +type Player struct { + ID int64 + Username string + PasswordHash string + CreatedAt time.Time + LastLogin *time.Time +} + +type Position struct { + X float32 + Y float32 + Z float32 + Yaw float32 + Pitch float32 + World string +} + +type Session struct { + ID int64 + PlayerID int64 + Token string + ServerType string + ServerID string + CreatedAt time.Time + ExpiresAt time.Time +} \ No newline at end of file diff --git a/server/net/packets.go b/server/internal/net/packets.go similarity index 100% rename from server/net/packets.go rename to server/internal/net/packets.go diff --git a/server/internal/net/packets/messages.go b/server/internal/net/packets/messages.go new file mode 100644 index 0000000..4e61428 --- /dev/null +++ b/server/internal/net/packets/messages.go @@ -0,0 +1,80 @@ +package packets + +type Vec3 struct { + X float32 `json:"x"` + Y float32 `json:"y"` + Z float32 `json:"z"` +} + +type Vec2 struct { + X float32 `json:"x"` + Y float32 `json:"y"` +} + +type InitMessage struct { + Type string `json:"type"` + PlayerID int `json:"playerId"` + X float32 `json:"x"` + Y float32 `json:"y"` + Z float32 `json:"z"` + Yaw float32 `json:"yaw"` + Pitch float32 `json:"pitch"` + TimeOfDay float32 `json:"timeOfDay"` +} + +type MovementMessage struct { + Type string `json:"type"` + X float32 `json:"x"` + Y float32 `json:"y"` + Z float32 `json:"z"` + Yaw float32 `json:"yaw"` + Pitch float32 `json:"pitch"` + VelX float32 `json:"velX"` + VelY float32 `json:"velY"` + VelZ float32 `json:"velZ"` +} + +type PlayerMovementMessage struct { + Type string `json:"type"` + PlayerID int `json:"playerId"` + X float32 `json:"x"` + Y float32 `json:"y"` + Z float32 `json:"z"` + Yaw float32 `json:"yaw"` + Pitch float32 `json:"pitch"` + VelX float32 `json:"velX"` + VelY float32 `json:"velY"` + VelZ float32 `json:"velZ"` +} + +type PositionCorrectionMessage struct { + Type string `json:"type"` + X float32 `json:"x"` + Y float32 `json:"y"` + Z float32 `json:"z"` +} + +type PlayerJoinedMessage struct { + Type string `json:"type"` + PlayerID int `json:"playerId"` + Username string `json:"username"` + X float32 `json:"x"` + Y float32 `json:"y"` + Z float32 `json:"z"` +} + +type PlayerLeftMessage struct { + Type string `json:"type"` + PlayerID int `json:"playerId"` +} + +type ChatMessage struct { + Type string `json:"type"` + Username string `json:"username,omitempty"` + Message string `json:"message"` +} + +type TimeUpdateMessage struct { + Type string `json:"type"` + TimeOfDay float32 `json:"timeOfDay"` +} \ No newline at end of file diff --git a/server/internal/net/packets/world.go b/server/internal/net/packets/world.go new file mode 100644 index 0000000..c757fc8 --- /dev/null +++ b/server/internal/net/packets/world.go @@ -0,0 +1,77 @@ +package packets + +import "math" + +type WorldConfig struct { + SamplesPerSide int + UnitsPerSample float32 + WorldWidth float32 + WorldHeight float32 + MinBounds Vec3 + MaxBounds Vec3 + MaxTerrainHeight float32 +} + +func NewWorldConfig(heightmap [][]float32, unitsPerSample float32) *WorldConfig { + samplesPerSide := len(heightmap) + worldSize := float32(samplesPerSide-1) * unitsPerSample + halfSize := worldSize * 0.5 + + maxHeight := float32(0) + for y := range heightmap { + for x := range heightmap[y] { + if heightmap[y][x] > maxHeight { + maxHeight = heightmap[y][x] + } + } + } + + return &WorldConfig{ + SamplesPerSide: samplesPerSide, + UnitsPerSample: unitsPerSample, + WorldWidth: worldSize, + WorldHeight: worldSize, + MinBounds: Vec3{X: -halfSize, Y: 0, Z: -halfSize}, + MaxBounds: Vec3{X: halfSize, Y: maxHeight + 10, Z: halfSize}, + MaxTerrainHeight: maxHeight, + } +} + +func (w *WorldConfig) ClampPosition(pos Vec3) Vec3 { + return Vec3{ + X: float32(math.Max(float64(w.MinBounds.X), math.Min(float64(w.MaxBounds.X), float64(pos.X)))), + Y: float32(math.Max(float64(w.MinBounds.Y), math.Min(float64(w.MaxBounds.Y), float64(pos.Y)))), + Z: float32(math.Max(float64(w.MinBounds.Z), math.Min(float64(w.MaxBounds.Z), float64(pos.Z)))), + } +} + +func (w *WorldConfig) IsInBounds(pos Vec3) bool { + return pos.X >= w.MinBounds.X && pos.X <= w.MaxBounds.X && + pos.Y >= w.MinBounds.Y && pos.Y <= w.MaxBounds.Y && + pos.Z >= w.MinBounds.Z && pos.Z <= w.MaxBounds.Z +} + +func (w *WorldConfig) GetHeightAt(heightmap [][]float32, worldX, worldZ float32) float32 { + sampleX := (worldX + w.WorldWidth*0.5) / w.UnitsPerSample + sampleZ := (worldZ + w.WorldHeight*0.5) / w.UnitsPerSample + + sampleX = float32(math.Max(0, math.Min(float64(w.SamplesPerSide-1), float64(sampleX)))) + sampleZ = float32(math.Max(0, math.Min(float64(w.SamplesPerSide-1), float64(sampleZ)))) + + x0 := int(math.Floor(float64(sampleX))) + z0 := int(math.Floor(float64(sampleZ))) + x1 := int(math.Min(float64(x0+1), float64(w.SamplesPerSide-1))) + z1 := int(math.Min(float64(z0+1), float64(w.SamplesPerSide-1))) + + fx := sampleX - float32(x0) + fz := sampleZ - float32(z0) + + h00 := heightmap[z0][x0] + h10 := heightmap[z0][x1] + h01 := heightmap[z1][x0] + h11 := heightmap[z1][x1] + + h0 := h00*(1-fx) + h10*fx + h1 := h01*(1-fx) + h11*fx + return h0*(1-fz) + h1*fz +} \ No newline at end of file diff --git a/server/net/server.go b/server/internal/net/server.go similarity index 100% rename from server/net/server.go rename to server/internal/net/server.go diff --git a/server/net/world.go b/server/internal/net/world.go similarity index 100% rename from server/net/world.go rename to server/internal/net/world.go diff --git a/server/main.go b/server/main.go deleted file mode 100644 index dd0721a..0000000 --- a/server/main.go +++ /dev/null @@ -1,210 +0,0 @@ -package main - -import ( - "encoding/binary" - "flag" - "fmt" - "image" - "image/color" - "image/png" - "log" - "math" - "math/rand" - "os" - "server/net" -) - -const WorldSize = 100 - -func generateHeightmap(size int) [][]float32 { - heightmap := make([][]float32, size) - for i := range heightmap { - heightmap[i] = make([]float32, size) - } - - // Simple perlin-like noise - for y := range heightmap { - for x := range heightmap[y] { - nx := float64(x) / float64(size) * 4 - ny := float64(y) / float64(size) * 4 - heightmap[y][x] = float32( - math.Sin(nx*2+rand.Float64())*0.5+ - math.Cos(ny*3+rand.Float64())*0.3+ - rand.Float64()*0.2) * 10.0 - } - } - - // Smooth the heightmap - for range 3 { - newHeightmap := make([][]float32, size) - for y := range newHeightmap { - newHeightmap[y] = make([]float32, size) - for x := range newHeightmap[y] { - sum := heightmap[y][x] - count := float32(1) - for dy := -1; dy <= 1; dy++ { - for dx := -1; dx <= 1; dx++ { - nx, ny := x+dx, y+dy - if nx >= 0 && nx < size && ny >= 0 && ny < size { - sum += heightmap[ny][nx] - count++ - } - } - } - newHeightmap[y][x] = sum / count - } - } - heightmap = newHeightmap - } - - return heightmap -} - -func saveHeightmapPNG(heightmap [][]float32, filename string) error { - size := len(heightmap) - img := image.NewGray(image.Rect(0, 0, size, size)) - - // Find min/max for normalization - minH, maxH := heightmap[0][0], heightmap[0][0] - for y := range heightmap { - for x := range heightmap[y] { - if heightmap[y][x] < minH { - minH = heightmap[y][x] - } - if heightmap[y][x] > maxH { - maxH = heightmap[y][x] - } - } - } - - // Apply normalized values to image - for y := range heightmap { - for x := range heightmap[y] { - normalized := (heightmap[y][x] - minH) / (maxH - minH) - img.SetGray(x, y, color.Gray{Y: uint8(normalized * 255)}) - } - } - - file, err := os.Create(filename) - if err != nil { - return fmt.Errorf("failed to create file %s: %w", filename, err) - } - defer file.Close() - - if err := png.Encode(file, img); err != nil { - return fmt.Errorf("failed to encode PNG: %w", err) - } - return nil -} - -func saveHeightmapBinary(heightmap [][]float32, filename string) error { - size := len(heightmap) - file, err := os.Create(filename) - if err != nil { - return fmt.Errorf("failed to create file %s: %w", filename, err) - } - defer file.Close() - - if err := binary.Write(file, binary.LittleEndian, int32(size)); err != nil { - return fmt.Errorf("failed to write size: %w", err) - } - - for y := range heightmap { - for x := range heightmap[y] { - if err := binary.Write(file, binary.LittleEndian, heightmap[y][x]); err != nil { - return fmt.Errorf("failed to write heightmap data: %w", err) - } - } - } - return nil -} - -// loadHeightmapBinary loads a heightmap from a binary file -func loadHeightmapBinary(filename string) ([][]float32, error) { - file, err := os.Open(filename) - if err != nil { - return nil, fmt.Errorf("failed to open file %s: %w", filename, err) - } - defer file.Close() - - var size int32 - if err := binary.Read(file, binary.LittleEndian, &size); err != nil { - return nil, fmt.Errorf("failed to read size: %w", err) - } - - heightmap := make([][]float32, size) - for y := range heightmap { - heightmap[y] = make([]float32, size) - for x := range heightmap[y] { - if err := binary.Read(file, binary.LittleEndian, &heightmap[y][x]); err != nil { - return nil, fmt.Errorf("failed to read heightmap data: %w", err) - } - } - } - return heightmap, nil -} - -func main() { - // Parse command-line flags - var ( - port = flag.String("port", "9999", "UDP port to listen on") - worldSize = flag.Int("size", WorldSize, "World size for heightmap generation") - skipGen = flag.Bool("skip-gen", false, "Skip heightmap generation (fail if none exists)") - forceGen = flag.Bool("force-gen", false, "Force heightmap regeneration even if one exists") - assetsPath = flag.String("assets", "../assets", "Path to assets directory") - ) - flag.Parse() - - // Setup logging - log.SetPrefix("[Game] ") - log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds) - - var heightmap [][]float32 - binPath := fmt.Sprintf("%s/heightmap.bin", *assetsPath) - - // Check if heightmap exists and should be loaded - if _, err := os.Stat(binPath); err == nil && !*forceGen && !*skipGen { - // Heightmap exists and we're not forcing regeneration - log.Printf("Found existing heightmap at %s, loading...", binPath) - heightmap, err = loadHeightmapBinary(binPath) - if err != nil { - log.Printf("Failed to load existing heightmap: %v, generating new one...", err) - heightmap = nil - } else { - log.Printf("Successfully loaded existing heightmap") - } - } else if *forceGen { - log.Printf("Force regeneration requested, ignoring existing heightmap") - } - - // Generate new heightmap if needed (not loaded, force generation, or doesn't exist) - if heightmap == nil && !*skipGen { - log.Printf("Generating new %dx%d heightmap...", *worldSize, *worldSize) - heightmap = generateHeightmap(*worldSize) - - pngPath := fmt.Sprintf("%s/heightmap.png", *assetsPath) - if err := saveHeightmapPNG(heightmap, pngPath); err != nil { - log.Printf("Warning: Failed to save PNG heightmap: %v", err) - } else { - log.Printf("Saved heightmap PNG to %s", pngPath) - } - - if err := saveHeightmapBinary(heightmap, binPath); err != nil { - log.Fatalf("Failed to save binary heightmap: %v", err) - } - log.Printf("Saved heightmap binary to %s", binPath) - } else if *skipGen && heightmap == nil { - // skip-gen was specified but no heightmap exists - log.Fatalf("No existing heightmap found and generation was skipped (--skip-gen flag)") - } - - server, err := net.NewServer(*port, heightmap) - if err != nil { - log.Fatalf("Failed to create server: %v", err) - } - - log.Printf("Starting game server on port %s", *port) - if err := server.Run(); err != nil { - log.Fatalf("Server failed: %v", err) - } -}