Compare commits
No commits in common. "151ac6ab8e8d2b753d8d6a1f51bffeccc2087f6b" and "663444157ecd990b63ef35f4dec3c15f4ee3596e" have entirely different histories.
151ac6ab8e
...
663444157e
Binary file not shown.
Binary file not shown.
@ -1,64 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
struct GameConfig {
|
||||
// Terrain configuration
|
||||
std::string heightmapFile = "../assets/heightmap.bin";
|
||||
float unitsPerSample = 1.0f; // Meters per heightmap sample
|
||||
float heightScale = 1.0f; // Height multiplier
|
||||
|
||||
// Window configuration
|
||||
int windowWidth = 1280;
|
||||
int windowHeight = 720;
|
||||
std::string windowTitle = "Game";
|
||||
int targetFPS = 60;
|
||||
|
||||
// Movement
|
||||
float moveSpeed = 15.0f;
|
||||
|
||||
// Network
|
||||
float heartbeatInterval = 5.0f;
|
||||
|
||||
// Debug options
|
||||
bool showDebugInfo = false;
|
||||
|
||||
// Load configuration from command line args or config file
|
||||
static GameConfig fromArgs(int argc, char* argv[]) {
|
||||
GameConfig config;
|
||||
|
||||
// Parse command line arguments
|
||||
for (int i = 1; i < argc; i++) {
|
||||
std::string arg = argv[i];
|
||||
|
||||
if (arg == "--heightmap" && i + 1 < argc) {
|
||||
config.heightmapFile = argv[++i];
|
||||
} else if (arg == "--scale" && i + 1 < argc) {
|
||||
config.unitsPerSample = std::stof(argv[++i]);
|
||||
} else if (arg == "--height-scale" && i + 1 < argc) {
|
||||
config.heightScale = std::stof(argv[++i]);
|
||||
} else if (arg == "--debug") {
|
||||
config.showDebugInfo = true;
|
||||
} else if (arg == "--help") {
|
||||
printHelp();
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
private:
|
||||
static void printHelp() {
|
||||
std::cout << "Game Options:\n";
|
||||
std::cout << " --heightmap <file> Load specific heightmap file\n";
|
||||
std::cout << " --scale <value> Units per heightmap sample (default: 1.0)\n";
|
||||
std::cout << " --height-scale <val> Height multiplier (default: 1.0)\n";
|
||||
std::cout << " --debug Show debug information\n";
|
||||
std::cout << " --help Show this help\n";
|
||||
std::cout << "\nExamples:\n";
|
||||
std::cout << " ./game --heightmap ../assets/heightmap_small.bin\n";
|
||||
std::cout << " ./game --heightmap ../assets/heightmap_large.bin --scale 2.0\n";
|
||||
}
|
||||
};
|
||||
@ -1,206 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <raylib.h>
|
||||
#include <raymath.h>
|
||||
#include <vector>
|
||||
#include <fstream>
|
||||
#include <cstdint>
|
||||
|
||||
class Heightmap {
|
||||
public:
|
||||
struct Config {
|
||||
float unitsPerSample = 1.0f; // How many world units (meters) per heightmap sample
|
||||
float heightScale = 1.0f; // Scale factor for height values
|
||||
};
|
||||
|
||||
private:
|
||||
std::vector<float> data;
|
||||
int samplesPerSide{0}; // Number of samples along one edge
|
||||
float worldWidth{0.0f}; // Total world width in units
|
||||
float worldHeight{0.0f}; // Total world height in units
|
||||
Config config;
|
||||
|
||||
public:
|
||||
Heightmap() : config{} {}
|
||||
Heightmap(const Config& cfg) : config(cfg) {}
|
||||
|
||||
// Load heightmap from binary file
|
||||
bool load(const std::string& filename) {
|
||||
std::ifstream file(filename, std::ios::binary);
|
||||
if (!file) return false;
|
||||
|
||||
int32_t fileSize;
|
||||
file.read(reinterpret_cast<char*>(&fileSize), sizeof(fileSize));
|
||||
samplesPerSide = fileSize;
|
||||
|
||||
data.resize(samplesPerSide * samplesPerSide);
|
||||
file.read(reinterpret_cast<char*>(data.data()), data.size() * sizeof(float));
|
||||
|
||||
// Calculate world dimensions based on samples and units per sample
|
||||
worldWidth = worldHeight = (samplesPerSide - 1) * config.unitsPerSample;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get interpolated height at world position
|
||||
float getHeightAtPosition(float worldX, float worldZ) const {
|
||||
// Convert world coordinates to sample coordinates
|
||||
float sampleX = (worldX + worldWidth * 0.5f) / config.unitsPerSample;
|
||||
float sampleZ = (worldZ + worldHeight * 0.5f) / config.unitsPerSample;
|
||||
|
||||
// Clamp to valid range
|
||||
sampleX = std::max(0.0f, std::min(sampleX, float(samplesPerSide - 1)));
|
||||
sampleZ = std::max(0.0f, std::min(sampleZ, float(samplesPerSide - 1)));
|
||||
|
||||
// Get integer sample indices
|
||||
int x0 = static_cast<int>(sampleX);
|
||||
int z0 = static_cast<int>(sampleZ);
|
||||
int x1 = std::min(x0 + 1, samplesPerSide - 1);
|
||||
int z1 = std::min(z0 + 1, samplesPerSide - 1);
|
||||
|
||||
// Get fractional parts for interpolation
|
||||
float fx = sampleX - x0;
|
||||
float fz = sampleZ - z0;
|
||||
|
||||
// Get heights at four corners
|
||||
float h00 = data[z0 * samplesPerSide + x0] * config.heightScale;
|
||||
float h10 = data[z0 * samplesPerSide + x1] * config.heightScale;
|
||||
float h01 = data[z1 * samplesPerSide + x0] * config.heightScale;
|
||||
float h11 = data[z1 * samplesPerSide + x1] * config.heightScale;
|
||||
|
||||
// Bilinear interpolation
|
||||
float h0 = h00 * (1.0f - fx) + h10 * fx;
|
||||
float h1 = h01 * (1.0f - fx) + h11 * fx;
|
||||
return h0 * (1.0f - fz) + h1 * fz;
|
||||
}
|
||||
|
||||
// Generate mesh from heightmap
|
||||
Mesh generateMesh() const {
|
||||
auto mesh = Mesh{};
|
||||
int vertexCount = samplesPerSide * samplesPerSide;
|
||||
int triangleCount = (samplesPerSide - 1) * (samplesPerSide - 1) * 2;
|
||||
|
||||
mesh.vertexCount = vertexCount;
|
||||
mesh.triangleCount = triangleCount;
|
||||
mesh.vertices = (float*)MemAlloc(vertexCount * 3 * sizeof(float));
|
||||
mesh.texcoords = (float*)MemAlloc(vertexCount * 2 * sizeof(float));
|
||||
mesh.normals = (float*)MemAlloc(vertexCount * 3 * sizeof(float));
|
||||
mesh.indices = (unsigned short*)MemAlloc(triangleCount * 3 * sizeof(unsigned short));
|
||||
|
||||
// Generate vertices
|
||||
for (int z = 0; z < samplesPerSide; z++) {
|
||||
for (int x = 0; x < samplesPerSide; x++) {
|
||||
int idx = z * samplesPerSide + x;
|
||||
|
||||
// World position in units (meters)
|
||||
// Center the terrain around origin
|
||||
float worldX = (x * config.unitsPerSample) - worldWidth * 0.5f;
|
||||
float worldZ = (z * config.unitsPerSample) - worldHeight * 0.5f;
|
||||
float worldY = data[idx] * config.heightScale;
|
||||
|
||||
mesh.vertices[idx * 3] = worldX;
|
||||
mesh.vertices[idx * 3 + 1] = worldY;
|
||||
mesh.vertices[idx * 3 + 2] = worldZ;
|
||||
|
||||
// Texture coordinates: 1 unit = 1 texture tile
|
||||
// Map from world coordinates to texture coordinates
|
||||
float texU = worldX + worldWidth * 0.5f;
|
||||
float texV = worldZ + worldHeight * 0.5f;
|
||||
|
||||
mesh.texcoords[idx * 2] = texU;
|
||||
mesh.texcoords[idx * 2 + 1] = texV;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate indices
|
||||
int triIdx = 0;
|
||||
for (int z = 0; z < samplesPerSide - 1; z++) {
|
||||
for (int x = 0; x < samplesPerSide - 1; x++) {
|
||||
int topLeft = z * samplesPerSide + x;
|
||||
int topRight = topLeft + 1;
|
||||
int bottomLeft = (z + 1) * samplesPerSide + x;
|
||||
int bottomRight = bottomLeft + 1;
|
||||
|
||||
mesh.indices[triIdx++] = topLeft;
|
||||
mesh.indices[triIdx++] = bottomLeft;
|
||||
mesh.indices[triIdx++] = topRight;
|
||||
|
||||
mesh.indices[triIdx++] = topRight;
|
||||
mesh.indices[triIdx++] = bottomLeft;
|
||||
mesh.indices[triIdx++] = bottomRight;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate proper normals
|
||||
calculateNormals(mesh);
|
||||
|
||||
UploadMesh(&mesh, false);
|
||||
return mesh;
|
||||
}
|
||||
|
||||
// Getters
|
||||
float getWorldWidth() const { return worldWidth; }
|
||||
float getWorldHeight() const { return worldHeight; }
|
||||
Vector3 getWorldBounds() const {
|
||||
return {worldWidth, getMaxHeight() * config.heightScale, worldHeight};
|
||||
}
|
||||
Vector3 getWorldCenter() const { return {0, 0, 0}; }
|
||||
int getSamplesPerSide() const { return samplesPerSide; }
|
||||
|
||||
private:
|
||||
void calculateNormals(Mesh& mesh) const {
|
||||
// Initialize normals to zero
|
||||
for (int i = 0; i < mesh.vertexCount * 3; i++) {
|
||||
mesh.normals[i] = 0.0f;
|
||||
}
|
||||
|
||||
// Calculate face normals and add to vertex normals
|
||||
for (int i = 0; i < mesh.triangleCount; i++) {
|
||||
unsigned short i1 = mesh.indices[i * 3];
|
||||
unsigned short i2 = mesh.indices[i * 3 + 1];
|
||||
unsigned short i3 = mesh.indices[i * 3 + 2];
|
||||
|
||||
Vector3 v1 = {mesh.vertices[i1 * 3], mesh.vertices[i1 * 3 + 1], mesh.vertices[i1 * 3 + 2]};
|
||||
Vector3 v2 = {mesh.vertices[i2 * 3], mesh.vertices[i2 * 3 + 1], mesh.vertices[i2 * 3 + 2]};
|
||||
Vector3 v3 = {mesh.vertices[i3 * 3], mesh.vertices[i3 * 3 + 1], mesh.vertices[i3 * 3 + 2]};
|
||||
|
||||
Vector3 edge1 = Vector3Subtract(v2, v1);
|
||||
Vector3 edge2 = Vector3Subtract(v3, v1);
|
||||
Vector3 normal = Vector3Normalize(Vector3CrossProduct(edge1, edge2));
|
||||
|
||||
// Add face normal to each vertex
|
||||
mesh.normals[i1 * 3] += normal.x;
|
||||
mesh.normals[i1 * 3 + 1] += normal.y;
|
||||
mesh.normals[i1 * 3 + 2] += normal.z;
|
||||
|
||||
mesh.normals[i2 * 3] += normal.x;
|
||||
mesh.normals[i2 * 3 + 1] += normal.y;
|
||||
mesh.normals[i2 * 3 + 2] += normal.z;
|
||||
|
||||
mesh.normals[i3 * 3] += normal.x;
|
||||
mesh.normals[i3 * 3 + 1] += normal.y;
|
||||
mesh.normals[i3 * 3 + 2] += normal.z;
|
||||
}
|
||||
|
||||
// Normalize vertex normals
|
||||
for (int i = 0; i < mesh.vertexCount; i++) {
|
||||
Vector3 normal = {
|
||||
mesh.normals[i * 3],
|
||||
mesh.normals[i * 3 + 1],
|
||||
mesh.normals[i * 3 + 2]
|
||||
};
|
||||
normal = Vector3Normalize(normal);
|
||||
mesh.normals[i * 3] = normal.x;
|
||||
mesh.normals[i * 3 + 1] = normal.y;
|
||||
mesh.normals[i * 3 + 2] = normal.z;
|
||||
}
|
||||
}
|
||||
|
||||
float getMaxHeight() const {
|
||||
float maxHeight = 0.0f;
|
||||
for (float h : data) {
|
||||
maxHeight = std::max(maxHeight, h);
|
||||
}
|
||||
return maxHeight;
|
||||
}
|
||||
};
|
||||
@ -1,234 +0,0 @@
|
||||
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())
|
||||
}
|
||||
@ -1,398 +0,0 @@
|
||||
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())
|
||||
}
|
||||
@ -1,10 +1,3 @@
|
||||
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
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
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=
|
||||
@ -1,183 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -1,80 +0,0 @@
|
||||
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"`
|
||||
}
|
||||
@ -1,77 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -1,89 +0,0 @@
|
||||
package net
|
||||
|
||||
import "math"
|
||||
|
||||
// WorldConfig holds world configuration based on heightmap
|
||||
type WorldConfig struct {
|
||||
SamplesPerSide int // Number of heightmap samples per side
|
||||
UnitsPerSample float32 // World units (meters) per sample
|
||||
WorldWidth float32 // Total world width in units
|
||||
WorldHeight float32 // Total world height (depth) in units
|
||||
MinBounds Vec3 // Minimum world bounds
|
||||
MaxBounds Vec3 // Maximum world bounds
|
||||
MaxTerrainHeight float32 // Maximum terrain height
|
||||
}
|
||||
|
||||
// NewWorldConfig creates world configuration from heightmap
|
||||
func NewWorldConfig(heightmap [][]float32, unitsPerSample float32) *WorldConfig {
|
||||
samplesPerSide := len(heightmap)
|
||||
worldSize := float32(samplesPerSide-1) * unitsPerSample
|
||||
halfSize := worldSize * 0.5
|
||||
|
||||
// Find max terrain height
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
// ClampPosition clamps a position to world bounds
|
||||
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)))),
|
||||
}
|
||||
}
|
||||
|
||||
// IsInBounds checks if a position is within world bounds
|
||||
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
|
||||
}
|
||||
|
||||
// GetHeightAt gets interpolated height at world position
|
||||
func (w *WorldConfig) GetHeightAt(heightmap [][]float32, worldX, worldZ float32) float32 {
|
||||
// Convert world coordinates to sample coordinates
|
||||
sampleX := (worldX + w.WorldWidth*0.5) / w.UnitsPerSample
|
||||
sampleZ := (worldZ + w.WorldHeight*0.5) / w.UnitsPerSample
|
||||
|
||||
// Clamp to valid range
|
||||
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))))
|
||||
|
||||
// Get integer sample indices
|
||||
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)))
|
||||
|
||||
// Get fractional parts for interpolation
|
||||
fx := sampleX - float32(x0)
|
||||
fz := sampleZ - float32(z0)
|
||||
|
||||
// Get heights at four corners
|
||||
h00 := heightmap[z0][x0]
|
||||
h10 := heightmap[z0][x1]
|
||||
h01 := heightmap[z1][x0]
|
||||
h11 := heightmap[z1][x1]
|
||||
|
||||
// Bilinear interpolation
|
||||
h0 := h00*(1-fx) + h10*fx
|
||||
h1 := h01*(1-fx) + h11*fx
|
||||
return h0*(1-fz) + h1*fz
|
||||
}
|
||||
210
server/main.go
Normal file
210
server/main.go
Normal file
@ -0,0 +1,210 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@ -1,69 +0,0 @@
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
// Simple heightmap generator for testing different world sizes
|
||||
void generateHeightmap(const std::string& filename, int size, float amplitude = 10.0f) {
|
||||
std::vector<float> data(size * size);
|
||||
|
||||
// Generate a simple heightmap with some hills
|
||||
for (int z = 0; z < size; z++) {
|
||||
for (int x = 0; x < size; x++) {
|
||||
float fx = (float)x / (size - 1) * 2.0f - 1.0f;
|
||||
float fz = (float)z / (size - 1) * 2.0f - 1.0f;
|
||||
|
||||
// Create some hills using sine waves
|
||||
float height = 0;
|
||||
height += sin(fx * 3.14159f * 2.0f) * cos(fz * 3.14159f * 2.0f) * amplitude * 0.5f;
|
||||
height += sin(fx * 3.14159f * 4.0f) * sin(fz * 3.14159f * 4.0f) * amplitude * 0.25f;
|
||||
|
||||
// Add a central mountain
|
||||
float dist = sqrt(fx * fx + fz * fz);
|
||||
height += std::max(0.0f, (1.0f - dist) * amplitude);
|
||||
|
||||
data[z * size + x] = height;
|
||||
}
|
||||
}
|
||||
|
||||
// Write to binary file
|
||||
std::ofstream file(filename, std::ios::binary);
|
||||
if (!file) {
|
||||
std::cerr << "Failed to open file: " << filename << "\n";
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t fileSize = size;
|
||||
file.write(reinterpret_cast<char*>(&fileSize), sizeof(fileSize));
|
||||
file.write(reinterpret_cast<char*>(data.data()), data.size() * sizeof(float));
|
||||
|
||||
std::cout << "Generated heightmap: " << filename << " (" << size << "x" << size << ")\n";
|
||||
std::cout << "World size will be: " << (size-1) << "x" << (size-1) << " units\n";
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
if (argc < 3) {
|
||||
std::cout << "Usage: " << argv[0] << " <size> <output_file> [amplitude]\n";
|
||||
std::cout << "Example: " << argv[0] << " 256 heightmap_256.bin 20.0\n";
|
||||
std::cout << "\nCommon sizes:\n";
|
||||
std::cout << " 64x64 -> 63x63 unit world (small)\n";
|
||||
std::cout << " 128x128 -> 127x127 unit world (medium)\n";
|
||||
std::cout << " 256x256 -> 255x255 unit world (large)\n";
|
||||
std::cout << " 512x512 -> 511x511 unit world (huge)\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
int size = std::stoi(argv[1]);
|
||||
std::string filename = argv[2];
|
||||
float amplitude = (argc > 3) ? std::stof(argv[3]) : 10.0f;
|
||||
|
||||
if (size < 2) {
|
||||
std::cerr << "Size must be at least 2\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
generateHeightmap(filename, size, amplitude);
|
||||
return 0;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user