1
0

Compare commits

..

No commits in common. "151ac6ab8e8d2b753d8d6a1f51bffeccc2087f6b" and "663444157ecd990b63ef35f4dec3c15f4ee3596e" have entirely different histories.

18 changed files with 210 additions and 1443 deletions

Binary file not shown.

Binary file not shown.

View File

@ -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";
}
};

View File

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

View File

@ -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())
}

View File

@ -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())
}

View File

@ -1,10 +1,3 @@
module server
go 1.25.0
require (
github.com/go-sql-driver/mysql v1.9.3
golang.org/x/crypto v0.42.0
)
require filippo.io/edwards25519 v1.1.0 // indirect

View File

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

View File

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

View File

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

View File

@ -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"`
}

View File

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

View File

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

View File

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