Compare commits
2 Commits
663444157e
...
151ac6ab8e
| Author | SHA1 | Date | |
|---|---|---|---|
| 151ac6ab8e | |||
| dd682b8b67 |
BIN
assets/heightmap_large.bin
Normal file
BIN
assets/heightmap_large.bin
Normal file
Binary file not shown.
BIN
assets/heightmap_small.bin
Normal file
BIN
assets/heightmap_small.bin
Normal file
Binary file not shown.
64
client/config.hpp
Normal file
64
client/config.hpp
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
#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";
|
||||||
|
}
|
||||||
|
};
|
||||||
206
client/terrain/Heightmap.hpp
Normal file
206
client/terrain/Heightmap.hpp
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
|
};
|
||||||
234
server/cmd/login/main.go
Normal file
234
server/cmd/login/main.go
Normal file
@ -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())
|
||||||
|
}
|
||||||
398
server/cmd/world/main.go
Normal file
398
server/cmd/world/main.go
Normal file
@ -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())
|
||||||
|
}
|
||||||
@ -1,3 +1,10 @@
|
|||||||
module server
|
module server
|
||||||
|
|
||||||
go 1.25.0
|
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
|
||||||
|
|||||||
6
server/go.sum
Normal file
6
server/go.sum
Normal file
@ -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=
|
||||||
183
server/internal/db/db.go
Normal file
183
server/internal/db/db.go
Normal file
@ -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
|
||||||
|
}
|
||||||
30
server/internal/db/models.go
Normal file
30
server/internal/db/models.go
Normal file
@ -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
|
||||||
|
}
|
||||||
80
server/internal/net/packets/messages.go
Normal file
80
server/internal/net/packets/messages.go
Normal file
@ -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"`
|
||||||
|
}
|
||||||
77
server/internal/net/packets/world.go
Normal file
77
server/internal/net/packets/world.go
Normal file
@ -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
|
||||||
|
}
|
||||||
89
server/internal/net/world.go
Normal file
89
server/internal/net/world.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
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
210
server/main.go
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BIN
tools/generate_heightmap
Executable file
BIN
tools/generate_heightmap
Executable file
Binary file not shown.
69
tools/generate_heightmap.cpp
Normal file
69
tools/generate_heightmap.cpp
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
#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