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
|
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
|
|
||||||
|
|||||||
@ -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