1
0

first pass on bringing client up to date with new server structure

This commit is contained in:
Sky Johnson 2025-09-09 23:18:55 -05:00
parent 151ac6ab8e
commit 72062cfa2c
8 changed files with 26786 additions and 917 deletions

136
client/LoginWindow.cpp Normal file
View File

@ -0,0 +1,136 @@
#include <raylib.h>
#include <raygui.h>
#include <cstring>
#include <string>
#include <memory>
#include "net/NetworkManager.hpp"
class LoginWindow {
private:
std::shared_ptr<NetworkManager> network;
char usernameBuffer[32] = "";
char passwordBuffer[32] = "";
bool editingUsername = false;
bool editingPassword = false;
bool attemptingLogin = false;
std::string statusMessage = "";
LoginResult loginResult;
public:
bool Run(std::shared_ptr<NetworkManager> net) {
network = net;
InitWindow(400, 300, "Game Login");
SetTargetFPS(60);
// Connect to login server
network->connectToLoginServer("localhost", 9998);
bool loginSuccess = false;
while (!WindowShouldClose() && !loginSuccess) {
// Handle login attempt result
if (attemptingLogin) {
LoginResult result;
if (network->waitForLoginResponse(result, 0.1f)) {
attemptingLogin = false;
if (result.success) {
loginResult = result;
loginSuccess = true;
statusMessage = "Login successful!";
} else {
statusMessage = result.message;
}
}
}
BeginDrawing();
ClearBackground(DARKGRAY);
// Title
DrawText("Game Login", 400/2 - MeasureText("Game Login", 30)/2, 20, 30, RAYWHITE);
// Username field
DrawText("Username:", 50, 80, 20, RAYWHITE);
if (GuiTextBox((Rectangle){50, 105, 300, 30}, usernameBuffer, 32, editingUsername)) {
editingUsername = !editingUsername;
if (editingUsername) editingPassword = false;
}
// Password field
DrawText("Password:", 50, 145, 20, RAYWHITE);
// Simple password masking - show asterisks
char displayBuffer[32];
int passLen = strlen(passwordBuffer);
for (int i = 0; i < passLen && i < 31; i++) {
displayBuffer[i] = '*';
}
displayBuffer[passLen] = '\0';
if (editingPassword) {
// Show actual password when editing
if (GuiTextBox((Rectangle){50, 170, 300, 30}, passwordBuffer, 32, editingPassword)) {
editingPassword = !editingPassword;
}
} else {
// Show masked password when not editing
if (GuiTextBox((Rectangle){50, 170, 300, 30}, displayBuffer, 32, editingPassword)) {
editingPassword = !editingPassword;
if (editingPassword) editingUsername = false;
}
// Allow typing into password field even when showing asterisks
if (editingPassword == false && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) {
Rectangle passBox = {50, 170, 300, 30};
if (CheckCollisionPointRec(GetMousePosition(), passBox)) {
editingPassword = true;
editingUsername = false;
}
}
}
// Login button (or Enter key)
bool doLogin = GuiButton((Rectangle){150, 215, 100, 30}, "Login");
if (IsKeyPressed(KEY_ENTER) && !editingUsername && !editingPassword) {
doLogin = true;
}
if (doLogin && !attemptingLogin) {
if (strlen(usernameBuffer) > 0 && strlen(passwordBuffer) > 0) {
attemptingLogin = true;
statusMessage = "Logging in...";
network->sendLoginRequest(usernameBuffer, passwordBuffer);
} else {
statusMessage = "Please enter username and password";
}
}
// Status message
if (!statusMessage.empty()) {
Color msgColor = (statusMessage.find("successful") != std::string::npos) ? GREEN :
(statusMessage.find("...") != std::string::npos) ? YELLOW : RED;
int textWidth = MeasureText(statusMessage.c_str(), 16);
DrawText(statusMessage.c_str(), 400/2 - textWidth/2, 255, 16, msgColor);
}
EndDrawing();
}
CloseWindow();
return loginSuccess;
}
LoginResult GetLoginResult() const {
return loginResult;
}
};
// Export function for main.cpp to use
bool ShowLoginWindow(std::shared_ptr<NetworkManager> network, LoginResult& result) {
LoginWindow window;
if (window.Run(network)) {
result = window.GetLoginResult();
return true;
}
return false;
}

25526
client/includes/json.hpp Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,6 @@
#include <raylib.h> #include <raylib.h>
#include <raymath.h> #include <raymath.h>
#include <raygui.h>
#define RAYGUI_IMPLEMENTATION
#include "includes/raygui.h"
#include <iostream> #include <iostream>
#include <vector> #include <vector>
@ -10,21 +8,17 @@
#include <thread> #include <thread>
#include <chrono> #include <chrono>
#include <memory> #include <memory>
#include <sstream>
#include "entity/player/PlayerController.hpp" #include "entity/player/PlayerController.hpp"
#include "entity/player/PlayerRenderer.hpp" #include "entity/player/PlayerRenderer.hpp"
#include "net/NetworkManager.hpp" #include "net/NetworkManager.hpp"
#include "ui/LoginWindow.hpp"
#include "sky/Sky.hpp" #include "sky/Sky.hpp"
#include "render/RenderContext.hpp" #include "render/RenderContext.hpp"
#include "utils/Coords.hpp" #include "utils/Coords.hpp"
#include "terrain/Heightmap.hpp" #include "terrain/Heightmap.hpp"
#include "config.hpp" #include "config.hpp"
//Forwarddeclaration of login window functionbool ShowLoginWindow(std::shared_ptr<NetworkManager> network, LoginResult& result)
enum GameState {
STATE_LOGIN,
STATE_CONNECTING,
STATE_PLAYING
};
class Game { class Game {
GameConfig config; GameConfig config;
PlayerController playerController; PlayerController playerController;
@ -32,27 +26,84 @@ class Game {
RenderContext renderContext; RenderContext renderContext;
Model terrainModel; Model terrainModel;
Heightmap heightmap; Heightmap heightmap;
NetworkManager network; std::shared_ptr<NetworkManager> network;
std::unique_ptr<Sky> sky; std::unique_ptr<Sky> sky;
Vector3 playerPos{0, 0, 0}; Vector3 playerPos{0, 0, 0};
Vector3 playerVelocity{0, 0, 0};
float playerYaw = 0.0f;
float playerPitch = 0.0f;
Texture2D terrainTexture; Texture2D terrainTexture;
// Login UI state // Movement tracking
GameState gameState = STATE_LOGIN; float lastMovementUpdate = 0.0f;
char usernameBuffer[32] = ""; const float movementUpdateInterval = 0.05f; // Send updates every 50ms
bool editMode = false;
std::string loginError = "";
std::string currentUsername = "";
// Heartbeat timing
float lastHeartbeatTime = 0.0f;
// Debug options // Debug options
bool showDebugAxes = false; bool showDebugAxes = false;
bool showWorldBounds = false; bool showWorldBounds = false;
// Player info
std::string currentUsername = "";
public: public:
Game(const GameConfig& cfg) : config(cfg) { Game(const GameConfig& cfg) : config(cfg), network(std::make_shared<NetworkManager>()) {
// Show login window first
LoginResult loginResult;
if (!ShowLoginWindow(network, loginResult)) {
std::cerr << "Login cancelled or failed\n";
return;
}
currentUsername = "Player" + std::to_string(loginResult.playerID);
// Connect to world server with auth token
network->connectToWorldServer(loginResult.worldHost, loginResult.worldPort);
network->sendAuth(loginResult.token);
// Wait for authentication
float authTimeout = 5.0f;
float authWaitTime = 0.0f;
while (!network->isAuthenticated() && authWaitTime < authTimeout) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
authWaitTime += 0.1f;
}
if (!network->isAuthenticated()) {
std::cerr << "Failed to authenticate with world server\n";
return;
}
// Now initialize game window
// Initialize network manager
network = std::make_shared<NetworkManager>();
// Show login window
LoginWindow loginWindow;
if (!loginWindow.Run(network)) {
std::cerr << "Login cancelled or failed\n";
return;
}
// Parse world server URL
std::string worldUrl = network->getWorldServerUrl();
std::string worldHost = "localhost";
uint16_t worldPort = 8082;
size_t colonPos = worldUrl.find(':');
if (colonPos != std::string::npos) {
worldHost = worldUrl.substr(0, colonPos);
worldPort = std::stoi(worldUrl.substr(colonPos + 1));
}
// Connect to world server
if (!network->connectToWorld(worldHost, worldPort, network->getAuthToken())) {
std::cerr << "Failed to connect to world server\n";
return;
}
// Initialize game window
InitWindow(config.windowWidth, config.windowHeight, config.windowTitle.c_str()); InitWindow(config.windowWidth, config.windowHeight, config.windowTitle.c_str());
SetTargetFPS(config.targetFPS); SetTargetFPS(config.targetFPS);
@ -87,69 +138,40 @@ public:
} }
~Game() { ~Game() {
// Send logout if we're connected // Disconnect from servers
if (gameState == STATE_PLAYING && network.isConnected()) { if (networknetwork)-> {
network.sendLogout(); network->disconnect->();
std::this_thread::sleep_for(std::chrono::milliseconds(100));
} }
if (IsWindowReady()) {
UnloadTexture(terrainTexture); UnloadTexture(terrainTexture);
UnloadModel(terrainModel); UnloadModel(terrainModel);
CloseWindow(); CloseWindow();
} }
}
void run() { void run() {
if (!IsWindowReady()) return;
while (!WindowShouldClose()) { while (!WindowShouldClose()) {
update(); update();
render(); render();
} }
// Clean logout when window is closing
if (gameState == STATE_PLAYING && network.isConnected()) {
network.sendLogout();
// Give the network time to send the packet
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
} }
private: private:
void update() { void update() {
switch (gameState) { if (!network->isConnected()) {
case STATE_LOGIN: // Lost connection - could show reconnect UI here
// Login screen - no game updates if (!network->isConnected()) {
break;
case STATE_CONNECTING:
// Check if we've received a response from the server
if (network.isConnected()) {
gameState = STATE_PLAYING;
currentUsername = std::string(usernameBuffer);
} else if (network.hasLoginError()) {
gameState = STATE_LOGIN;
loginError = network.getLoginError();
}
break;
case STATE_PLAYING:
updateGame();
break;
}
}
void updateGame() {
if (!network.isConnected()) {
// Player disconnected, go back to login
gameState = STATE_LOGIN;
loginError = "Disconnected from server";
return; return;
} }
// Update sky with server time if connected, otherwise use local time // Update sky with server time
if (network.isConnected()) { float serverTime = network->->getServerTimeOfDay();
float serverTime = network.getServerTimeOfDay();
sky->updateFromServerTime(serverTime); sky->updateFromServerTime(serverTime);
} else {
sky->update(GetFrameTime());
}
// Time of day controls (for testing) // Time of day controls (for testing)
if (IsKeyPressed(KEY_T)) { if (IsKeyPressed(KEY_T)) {
@ -163,144 +185,37 @@ private:
if (IsKeyPressed(KEY_F1)) showDebugAxes = !showDebugAxes; if (IsKeyPressed(KEY_F1)) showDebugAxes = !showDebugAxes;
if (IsKeyPressed(KEY_F2)) showWorldBounds = !showWorldBounds; if (IsKeyPressed(KEY_F2)) showWorldBounds = !showWorldBounds;
// Get server position and update player controller // Get server position
playerPos = network.getPosition(); playerPos = network->->getPosition();
// Clamp position to world bounds
playerPos = Coords::clampToWorldBounds(playerPos);
playerController.setPlayerPosition(playerPos); playerController.setPlayerPosition(playerPos);
-> float deltaTime = GetFrameTime();
// Clean up disconnected player models
auto remotePlayers = network.getRemotePlayers();
playerRenderer->cleanupDisconnectedPlayers(remotePlayers);
// Update player controller (handles camera)
float deltaTime = GetFrameTime();
playerController.update(deltaTime); playerController.update(deltaTime);
// Get movement input from player controller // Get movement input
Vector3 moveInput = playerController.getMoveInput(); Vector3 moveInput = playerController.getMoveInput();
// Send normalized movement direction to server (server handles speed) // Calculate velocity based on input
if (moveInput.x != 0 || moveInput.z != 0) { const float moveSpeed = 10.0f;
network.sendMove(moveInput.x, 0, moveInput.z); playerVelocity = Vector3Scale(moveInput, moveSpeed);
lastHeartbeatTime = GetTime(); // Reset heartbeat timer when moving
}
// Send periodic heartbeats when not moving // Get camera orientation for yaw/pitch
float currentTime = GetTime(); Camera camera = playerController.getCamera();
if (currentTime - lastHeartbeatTime >= config.heartbeatInterval) { Vector3 forward = Vector3Subtract(camera.target, camera.position);
network.sendHeartbeat(); playerYaw = atan2f(forward.x, forward.z);
lastHeartbeatTime = currentTime; playerPitch = atan2f(forward.y, sqrtf(forward.x * forward.x + forward.z * forward.z));
}
// Handle color change with arrow keys // Send movement updates at regular intervals or when moving
static int currentColorIndex = -1; -> float currentTime = GetTime();
if (IsKeyPressed(KEY_LEFT) || IsKeyPressed(KEY_RIGHT)) { if (currentTime - lastMovementUpdate >= movementUpdateInterval) {
// Get current color index if not set network->sendMovement->(playerPos, playerYaw, playerPitch, playerVelocity);
if (currentColorIndex == -1) { lastMovementUpdate = currentTime;
auto currentColor = network.getPlayerColor(); ->std::this_thread::sleep_for(std::chrono::milliseconds(100))break // Exit game loop }
for (size_t i = 0; i < NetworkManager::AVAILABLE_COLORS.size(); i++) {
if (NetworkManager::AVAILABLE_COLORS[i] == currentColor) {
currentColorIndex = i;
break;
}
}
if (currentColorIndex == -1) currentColorIndex = 0;
}
// Change color index
if (IsKeyPressed(KEY_LEFT)) {
currentColorIndex--;
if (currentColorIndex < 0) {
currentColorIndex = NetworkManager::AVAILABLE_COLORS.size() - 1;
}
} else if (IsKeyPressed(KEY_RIGHT)) {
currentColorIndex++;
if (currentColorIndex >= (int)NetworkManager::AVAILABLE_COLORS.size()) {
currentColorIndex = 0;
}
}
// Send color change to server
network.sendColorChange(NetworkManager::AVAILABLE_COLORS[currentColorIndex]);
}
// Handle logout
if (IsKeyPressed(KEY_ESCAPE)) {
network.sendLogout();
gameState = STATE_LOGIN;
loginError = "";
}
} }
void render() { void render() {
BeginDrawing(); BeginDrawing();
// Clear with a default color first
ClearBackground(SKYBLUE); ClearBackground(SKYBLUE);
switch (gameState) {
case STATE_LOGIN:
renderLoginScreen();
break;
case STATE_CONNECTING:
renderConnectingScreen();
break;
case STATE_PLAYING:
renderGame();
break;
}
EndDrawing();
}
void renderLoginScreen() {
// Center the login box
int boxWidth = 300;
int boxHeight = 200;
int boxX = (GetScreenWidth() - boxWidth) / 2;
int boxY = (GetScreenHeight() - boxHeight) / 2;
// Draw login panel
GuiPanel((Rectangle){(float)boxX, (float)boxY, (float)boxWidth, (float)boxHeight}, "Login");
// Username label and text box
GuiLabel((Rectangle){(float)(boxX + 20), (float)(boxY + 50), 80, 30}, "Username:");
if (GuiTextBox((Rectangle){(float)(boxX + 100), (float)(boxY + 50), 180, 30},
usernameBuffer, 32, editMode)) {
editMode = !editMode;
}
// Login button
if (GuiButton((Rectangle){(float)(boxX + 100), (float)(boxY + 100), 100, 30}, "Login")) {
if (strlen(usernameBuffer) > 0) {
// Send login with username
network.sendLoginWithUsername(usernameBuffer);
gameState = STATE_CONNECTING;
loginError = "";
} else {
loginError = "Please enter a username";
}
}
// Error message
if (!loginError.empty()) {
DrawText(loginError.c_str(), boxX + 20, boxY + 150, 20, RED);
}
// Instructions
DrawText("Enter your username to join the game", 10, 10, 20, WHITE);
}
void renderConnectingScreen() {
DrawText("Connecting to server...",
GetScreenWidth()/2 - 100, GetScreenHeight()/2 - 10, 20, WHITE);
}
void renderGame() {
// Begin 3D rendering with render context // Begin 3D rendering with render context
renderContext.begin3D(playerController.getCamera()); renderContext.begin3D(playerController.getCamera());
@ -315,21 +230,18 @@ private:
renderContext.submitModel(terrainModel, {0, 0, 0}, 1.0f, WHITE, renderContext.submitModel(terrainModel, {0, 0, 0}, 1.0f, WHITE,
RenderContext::RenderLayer::TERRAIN); RenderContext::RenderLayer::TERRAIN);
// Submit players if connected // Submit players
if (network.isConnected()) { ->// Local player
// Local player playerRenderer->renderLocalPlayer(renderContext, playerPos, "blue")"blue");
playerRenderer->renderLocalPlayer(renderContext, playerPos, network.getPlayerColor());
// Remote players // Remote players
auto remotePlayers = network.getRemotePlayers(); auto remotePlayers = network->->getRemotePlayers();
playerRenderer->renderRemotePlayers(renderContext, remotePlayers); playerRenderer->renderRemotePlayers(renderContext, remotePlayers);
}
// Debug rendering // Debug rendering
if (showDebugAxes) { if (showDebugAxes) {
renderContext.submitCustom([this]() { renderContext.submitCustom([this]() {
Coords::drawDebugAxes(playerPos, 2.0f); Coords::drawDebugAxes(playerPos, 2.0f);
// Draw world origin axes
Coords::drawDebugAxes({0, 0, 0}, 5.0f); Coords::drawDebugAxes({0, 0, 0}, 5.0f);
}, RenderContext::RenderLayer::TRANSPARENT); }, RenderContext::RenderLayer::TRANSPARENT);
} }
@ -344,28 +256,28 @@ private:
renderContext.end3D(); renderContext.end3D();
// UI // UI
DrawText(TextFormat("Logged in as: %s", currentUsername.c_str()), 10, 10, 20, WHITE); DrawText(TextFormat("PlayerPlayer IDID: %dd", network->getPlayerIDnetwork->getPlayerID()), 10, 10, 20, WHITE);
DrawText("WASD: Move | Q/E: Strafe | Right-Click: Rotate Camera", 10, 35, 20, WHITE); DrawText("WASD: Move | Q/E: Strafe | Right-Click: Rotate Camera", 10, 35, 20, WHITE);
DrawText("Left/Right Arrow: Change Color | Mouse Wheel: Zoom | ESC: Logout", 10, 60, 20, WHITE); DrawText("Mouse Wheel: Zoom | ESC: Exit", 10, 60, 20, WHITE);
DrawText("T: Change Time | F1: Debug Axes | F2: World Bounds", 10, 85, 20, WHITE); DrawText("T: Change Time | F1: Debug Axes | F2: World Bounds", 10, 85, 20, WHITE);
if (network.isConnected()) {
std::string colorText = "Your color: " + network.getPlayerColor();
DrawText(colorText.c_str(), 10, 110, 20, WHITE);
}
// Show time of day // Show time of day
float timeHour = sky->getTimeOfDay() * 24.0f; float timeHour = sky->getTimeOfDay() * 24.0f;
int hour = (int)timeHour; int hour = (int)timeHour;
int minute = (int)((timeHour - hour) * 60); int minute = (int)((timeHour - hour) * 60);
DrawText(TextFormat("Time: %02d:%02d", hour, minute), 10, 135, 20, WHITE); DrawText(TextFormat("Time: %02d:%02d", hour, minute), 10, 110110, 20, WHITE);
// Show position if debug is on // Show position if debug is on
if (showDebugAxes) { if (showDebugAxes) {
DrawText(TextFormat("Pos: %.1f, %.1f, %.1f", playerPos.x, playerPos.y, playerPos.z), DrawText(TextFormat("Pos: %.1f, %.1f, %.1f", playerPos.x, playerPos.y, playerPos.z),
10, 160, 20, YELLOW); 10, 135135, 20, YELLOW);
} }
DrawFPS(10, showDebugAxes ? 185 : 160); DrawFPS(10, showDebugAxes ? 160160 : 135);
EndDrawing(135);
EndDrawing();
} }
}; };

View File

@ -1,237 +1,135 @@
#include "NetworkManager.hpp" #include "NetworkManager.hpp"
#include <iostream> #include <iostream>
#include <cstring> #include <cstring>
#include <chrono>
const std::vector<std::string> NetworkManager::AVAILABLE_COLORS = { NetworkManager::NetworkManager() {loginSocketworldSocket.open(udp::v4());
"red", "green", "orange", "purple", "white" startLoginReceive();
}; startWorldReceive
NetworkManager::NetworkManager() : serverEndpoint(ip::make_address("127.0.0.1"), 9999) {
socket.open(udp::v4());
startReceive();
ioThread = std::thread([this] { ioContext.run(); }); ioThread = std::thread([this] { ioContext.run(); });
} }
NetworkManager::~NetworkManager() { NetworkManager::~NetworkManager() {
disconnect();
ioContext.stop(); ioContext.stop();
if (ioThread.joinable()) ioThread.join(); if (ioThread.joinable()) ioThread.join();
} }
void NetworkManager::startReceive() { bool NetworkManager::connectToLogin(const std::string& server, uint16_t port) {
socket.async_receive_from( try {
buffer(recvBuffer), serverEndpoint, tcp::resolver resolver(ioContext);
[this](std::error_code ec, std::size_t bytes) { auto endpoints = resolver.resolve(server, std::to_string(port));
if (!ec && bytes > 0) { boost::asio::connect(loginSocket, endpoints);
processMessage(recvBuffer.data(), bytes);
}
startReceive();
}
);
}
void NetworkManager::processMessage(const uint8_t* data, std::size_t size) { loginConnected = true;
if (size == 0) return; startLoginReceive();
return true;
auto msgType = static_cast<MessageType>(data[0]); } catch (std::exception& e) {
std::cerr << "Failed to connect to login server: " << e.what() << std::endl;
switch (msgType) { return false;
case MessageType::Spawn:
handleSpawn(data, size);
break;
case MessageType::Update:
handleUpdate(data, size);
break;
case MessageType::PlayerJoined:
handlePlayerJoined(data, size);
break;
case MessageType::PlayerLeft:
handlePlayerLeft(data, size);
break;
case MessageType::PlayerList:
handlePlayerList(data, size);
break;
case MessageType::ColorChanged:
handleColorChanged(data, size);
break;
case MessageType::LoginResponse:
handleLoginResponse(data, size);
break;
case MessageType::TimeSync:
handleTimeSync(data, size);
break;
default:
break;
} }
} }
void NetworkManager::handleTimeSync(const uint8_t* data, std::size_t size) { bool NetworkManager::connectToWorld(const std::string& server, uint16_t port, const std::string& token) {
// Message format: [type(1)][timeOfDay(4)] try {
if (size < 5) return; tcp::resolver resolver(ioContext);
auto endpoints = resolver.resolve(server, std::to_string(port));
boost::asio::connect(worldSocket, endpoints);
float timeOfDay; // Send authentication
std::memcpy(&timeOfDay, &data[1], sizeof(timeOfDay)); json authMsg;
authMsg["type"] = "auth";
authMsg["token"] = token;
serverTimeOfDay.store(timeOfDay); std::string msg = authMsg.dump() + "\n";
} boost::asio::write(worldSocket, boost::asio::buffer(msg));
void NetworkManager::handleSpawn(const uint8_t* data, std::size_t size) { // Wait for auth response
// Message format: [type(1)][id(4)][x(4)][y(4)][z(4)][colorLen(1)][color(colorLen)] boost::asio::streambuf response;
if (size < 18) return; boost::asio::read_until(worldSocket, response, '\n');
uint32_t id; std::istream response_stream(&response);
float x, y, z; std::string response_str;
std::memcpy(&id, &data[1], sizeof(id)); std::getline(response_stream, response_str);
std::memcpy(&x, &data[5], sizeof(x));
std::memcpy(&y, &data[9], sizeof(y));
std::memcpy(&z, &data[13], sizeof(z));
uint8_t colorLen = data[17]; json authResp = json::parse(response_str);
if (size >= 18u + colorLen) { if (authResp["success"] == true) {
playerColor = std::string(reinterpret_cast<const char*>(&data[18]), colorLen); worldConnected = true;
}
playerID = id;
{
std::lock_guard lock(positionMutex);
serverPosition = {x, y, z};
}
connected = true; connected = true;
std::cout << "Connected as player " << id << " with color " << playerColor << "\n"; startWorldReceive();
return true;
} }
void NetworkManager::handleUpdate(const uint8_t* data, std::size_t size) { return false;
if (size < 17) return; } catch (std::exception& e) {
std::cerr << "Failed to connect to world server: " << e.what() << std::endl;
uint32_t id; return false;
float x, y, z;
std::memcpy(&id, &data[1], sizeof(id));
std::memcpy(&x, &data[5], sizeof(x));
std::memcpy(&y, &data[9], sizeof(y));
std::memcpy(&z, &data[13], sizeof(z));
if (id == playerID) {
std::lock_guard lock(positionMutex);
serverPosition = {x, y, z};
} else {
std::lock_guard lock(remotePlayersMutex);
if (remotePlayers.find(id) != remotePlayers.end()) {
remotePlayers[id].position = {x, y, z};
remotePlayers[id].lastUpdate = GetTime();
}
}
}
void NetworkManager::handlePlayerJoined(const uint8_t* data, std::size_t size) {
// Message format: [type(1)][id(4)][x(4)][y(4)][z(4)][colorLen(1)][color(colorLen)]
if (size < 18) return;
uint32_t id;
float x, y, z;
std::memcpy(&id, &data[1], sizeof(id));
std::memcpy(&x, &data[5], sizeof(x));
std::memcpy(&y, &data[9], sizeof(y));
std::memcpy(&z, &data[13], sizeof(z));
uint8_t colorLen = data[17];
std::string color = "red";
if (size >= 18u + colorLen) {
color = std::string(reinterpret_cast<const char*>(&data[18]), colorLen);
}
if (id != playerID) {
std::lock_guard lock(remotePlayersMutex);
remotePlayers[id] = {id, {x, y, z}, color, static_cast<float>(GetTime())};
std::cout << "Player " << id << " joined with color " << color << "\n";
} }
} }
void NetworkManager::handlePlayerLeft(const uint8_t* data, std::size_t size) { void NetworkManager::startLoginReceive() {
if (size < 5) return; auto self(shared_from_this());
boost::asio::async_read_until(loginSocket, loginBuffer, '\n',
[this, self](boost::system::error_code ec, std::size_t length) {
if (!ec) {
std::istream is(&loginBuffer);
std::string line;
std::getline(is, line);
uint32_t id; try {
std::memcpy(&id, &data[1], sizeof(id)); json msg = json::parse(line);
processLoginMessage(msg);
std::lock_guard lock(remotePlayersMutex); } catch (json::exception& e) {
remotePlayers.erase(id); std::cerr << "JSON parse error: " << e.what() << std::endl;
std::cout << "Player " << id << " left\n";
} }
void NetworkManager::handlePlayerList(const uint8_t* data, std::size_t size) { if (loginConnected) {
if (size < 2) return; startLoginReceive();
}
uint8_t count = data[1]; void NetworkManager::connectToLoginServer(const std::string& host, uint16_t port) {
size_t offset = 2; loginEndpoint = udp::endpoint(ip::make_address(host), port);
std::cout << "Login server endpoint set to " << host << ":" << port << "\n";
std::lock_guard lock(remotePlayersMutex);
remotePlayers.clear();
for (uint8_t i = 0; i < count && offset + 17 < size; i++) {
uint32_t id;
float x, y, z;
std::memcpy(&id, &data[offset], sizeof(id));
std::memcpy(&x, &data[offset + 4], sizeof(x));
std::memcpy(&y, &data[offset + 8], sizeof(y));
std::memcpy(&z, &data[offset + 12], sizeof(z));
uint8_t colorLen = data[offset + 16];
std::string color = "red";
if (offset + 17 + colorLen <= size) {
color = std::string(reinterpret_cast<const char*>(&data[offset + 17]), colorLen);
} }
offset += 17 + colorLen; void NetworkManager::connectToWorldServer(const std::string& host, uint16_t port) {
worldEndpoint = udp::endpoint(ip::make_address(host), port);
if (id != playerID) { std::cout << "World server endpoint set to " << host << ":" << port << "\n";
remotePlayers[id] = {id, {x, y, z}, color, static_cast<float>(GetTime())};
}
} }
std::cout << "Received list of " << (int)count << " players\n"; void NetworkManager::sendLoginRequest(const std::string& username, const std::string& password) {
} std::vector<uint8_t> msg(2 + username.size() + 1 + password.size());
msg[0] = static_cast<uint8_t>(MessageType::LoginRequest);
void NetworkManager::sendLogin() {
std::array<uint8_t, 1> msg{static_cast<uint8_t>(MessageType::Login)};
socket.send_to(buffer(msg), serverEndpoint);
}
void NetworkManager::sendLoginWithUsername(const std::string& username) {
std::cout << "Attempting login with username: " << username << "\n";
loginErrorMsg = "";
currentUsername = username;
std::vector<uint8_t> msg(2 + username.size());
msg[0] = static_cast<uint8_t>(MessageType::Login);
msg[1] = static_cast<uint8_t>(username.size()); msg[1] = static_cast<uint8_t>(username.size());
std::memcpy(&msg[2], username.data(), username.size()); std::memcpy(&msg[2], username.data(), username.size());
socket.send_to(buffer(msg), serverEndpoint); msg[2 + username.size()] = static_cast<uint8_t>(password.size());
std::cout << "Login packet sent for username: " << username << "\n"; std::memcpy(&msg[3 + username.size()], password.data(), password.size());
loginSocket.send_to(buffer(msg), loginEndpoint);
loginResponseReceived = false;
} }
void NetworkManager::sendLogout() { bool NetworkManager::waitForLoginResponse(LoginResult& result, float timeout) {
if (!connected) { auto start = std::chrono::steady_clock::now();
std::cout << "Warning: sendLogout called but not connected\n"; auto timeoutDuration = std::chrono::milliseconds(static_cast<int>(timeout * 1000));
return;
while (!loginResponseReceived) {
if (std::chrono::steady_clock::now() - start > timeoutDuration) {
return false;
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
} }
std::cout << "Sending logout for player ID " << playerID << " (username: " << currentUsername << ")\n"; std::lock_guard<std::mutex> lock(loginMutex);
result = lastLoginResult;
std::array<uint8_t, 5> msg{}; return true;
msg[0] = static_cast<uint8_t>(MessageType::Logout);
uint32_t id = playerID;
std::memcpy(&msg[1], &id, sizeof(id));
socket.send_to(buffer(msg), serverEndpoint);
std::cout << "Logout packet sent, resetting client state\n";
// Reset state
connected = false;
playerID = 0;
currentUsername = "";
loginErrorMsg = ""; // Clear any error messages
{
std::lock_guard lock(remotePlayersMutex);
remotePlayers.clear();
} }
void NetworkManager::sendAuth(const std::array<uint8_t, 32>& token) {
std::array<uint8_t, 33> msg;
msg[0] = static_cast<uint8_t>(MessageType::Auth);
std::memcpy(&msg[1], token.data(), 32);
worldSocket.send_to(buffer(msg), worldEndpoint);
} }
void NetworkManager::sendMove(float dx, float dy, float dz) { void NetworkManager::sendMove(float dx, float dy, float dz) {
@ -246,21 +144,7 @@ void NetworkManager::sendMove(float dx, float dy, float dz) {
std::memcpy(&msg[9], &dy, sizeof(dy)); std::memcpy(&msg[9], &dy, sizeof(dy));
std::memcpy(&msg[13], &dz, sizeof(dz)); std::memcpy(&msg[13], &dz, sizeof(dz));
socket.send_to(buffer(msg), serverEndpoint); worldSocket.send_to(buffer(msg), worldEndpoint);
}
void NetworkManager::sendColorChange(const std::string& newColor) {
if (!connected) return;
std::vector<uint8_t> msg(6 + newColor.size());
msg[0] = static_cast<uint8_t>(MessageType::ChangeColor);
uint32_t id = playerID;
std::memcpy(&msg[1], &id, sizeof(id));
msg[5] = static_cast<uint8_t>(newColor.size());
std::memcpy(&msg[6], newColor.data(), newColor.size());
socket.send_to(buffer(msg), serverEndpoint);
} }
void NetworkManager::sendHeartbeat() { void NetworkManager::sendHeartbeat() {
@ -270,7 +154,246 @@ void NetworkManager::sendHeartbeat() {
msg[0] = static_cast<uint8_t>(MessageType::Heartbeat); msg[0] = static_cast<uint8_t>(MessageType::Heartbeat);
uint32_t id = playerID; uint32_t id = playerID;
std::memcpy(&msg[1], &id, sizeof(id)); std::memcpy(&msg[1], &id, sizeof(id));
socket.send_to(buffer(msg), serverEndpoint); worldSocket.send_to(buffer(msg), worldEndpoint);
}
void NetworkManager::sendLogout() {
if (!connected) return;
std::array<uint8_t, 5> msg{};
msg[0] = static_cast<uint8_t>(MessageType::Logout);
uint32_t id = playerID;
std::memcpy(&msg[1], &id, sizeof(id));
worldSocket.send_to(buffer(msg), worldEndpoint);
connected = false;
authenticated = false;
playerID = 0;
std::lock_guard lock(remotePlayersMutex);
remotePlayers.clear();
}
void NetworkManager::startLoginReceive() {
loginSocket.async_receive_from(
buffer(loginRecvBuffer), loginEndpoint,
processLoginMessageloginRecvBuffer }
});
}
void NetworkManager::startWorldReceive() {
auto self(shared_from_this());
boost::asio::async_read_until(worldSocket, worldBuffer, '\n',
[this, self](boost::system::error_code ec, std::size_t length) {
if (!ec) {
std::istream is(&worldBuffer);
std::string line;
std::getline(is, line);
try {
json msg = json::parse(line);
processWorldMessage(msg);
} catch (json::exception& e) {
std::cerr << "JSON parse error: " << e.what() << std::endl;
}
if (worldConnected) {
startWorldReceive();
}
}
});
}
void NetworkManager::processLoginMessage(const json& msg) {
std::string type = msg["type"];
if (type == "loginResponse") {
if (msg["success"] == true) {
authToken = msg["token"];
worldServerUrl = msg["worldUrl"];
playerID = msg["playerId"];
loginSuccess = true;
loginErrorMsg = "";
} else {
loginErrorMsg = msg.value("message", "Login failed");
loginSuccess = false;
startLoginReceive }
} else if (type == "registerResponse") {
if (msg["success"] == false) {
loginErrorMsg = msg.value("message", "Registration failed");
void NetworkManager::startWorldReceive() {
worldSocket.async_receive_from(
buffer(worldRecvBuffer), worldEndpoint,
[this](std::error_code ec, std::size_t bytes) {
if (!ec && bytes > 0) {
processWorldMessage(worldRecvBuffer.data(), bytes);
}
startWorldReceive();
}
);
}
void NetworkManager::processLoginMessage(const uint8_t* data, std::size_t size) {
if (size == 0) return;
auto msgType = static_cast<MessageType>(data[0]);
switch (msgType) {
case MessageType::LoginResponse:
handleLoginResponse(data, size);
break;
default:
break;
}
}
void NetworkManager::processWorldMessage(const uint8_t* data, std::size_t size) {
case MessageType::AuthResponse:
handleAuthResponse(data, size);
break;
handleLoginResponse3lock_guard<std::mutex>lockloginMutex) lastLoginResult.success = (data[1] == 1);
uint8_t msgLen = data[2];
if (size >= 3u + msgLen) {
lastLoginResult.message = std::string(reinterpret_cast<const char*>(&data[3]), msgLen);
}
if (lastLoginResult.success && size >= 3u + msgLen + 32 + 1) {
// Extract token
std::memcpy(lastLoginResult.token.data(), &data[3 + msgLen], 32);
// Extract world host
uint8_t hostLen = data[3 + msgLen + 32];
if (size >= 3u + msgLen + 32 + 1 + hostLen + 2 + 4) {
lastLoginResult.worldHost = std::string(reinterpret_cast<const char*>(&data[3 + msgLen + 32 + 1]), hostLen);
// Extract world port
std::memcpy(&lastLoginResult.worldPort, &data[3 + msgLen + 32 + 1 + hostLen], 2);
// Extract player ID
std::memcpy(&lastLoginResult.playerID, &data[3 + msgLen + 32 + 1 + hostLen + 2], 4);
}
}
loginResponseReceived = true;
if (lastLoginResult.success) {
std::cout << "Login successful! World: " << lastLoginResult.worldHost
<< ":" << lastLoginResult.worldPort << "\n";
} else {
std::cout << "Login failed: " << lastLoginResult.message << "\n";
}
}
void NetworkManager::handleAuthResponse(const uint8_t* data, std::size_t size) {
if (size < 3) return;
bool success = (data[1] == 1);
uint8_t msgLen = data[2];
if (size >= 3u + msgLen) {
std::string message(reinterpret_cast<const char*>(&data[3]), msgLen);
if (success) {
authenticated = true;
std::cout << "World authentication successful\n";
} else {
authenticated = false;
std::cout << "World authentication failed: " << message << "\n";
}
}
17Spawnedat(" << x << ",y, " << z << ") }
}
}
void NetworkManager::processWorldMessage(const json& msg) {
std::string type = msg["type"];
if (type == "init") {
playerID = msg["playerId"];
serverPosition = {msg["x"], msg["y"], msg["z"]};
serverTimeOfDay = msg["timeOfDay"];
connected = true;
std::cout << "Connected to world as player " << playerID << std::endl;
} else if (type == "playerMovement") {
uint32_t id = msg["playerId"];
if (id != playerID) {
std::lock_guard lock(remotePlayersMutex);
if (remotePlayers.find(id) != remotePlayers.end()) {
remotePlayers[id].position = {msg["x"], msg["y"], msg["z"]};
remotePlayers[id].lastUpdate = GetTime();
}
}
} else if (type == "playerJoined") {
uint32_t id = msg["playerId"];
if (id != playerID) {
std::lock_guard lock(remotePlayersMutex);
remotePlayers[id] = {
id,
{msg["x"], msg["y"], msg["z"]},
msg.value("username", "Player"),
static_cast<float>(GetTime())
};
std::cout << "Player " << msg["username"] << " joined" << std::endl;
}
} else if (type == "playerLeft") {
uint32_t id = msg["playerId"];
usernameLenusernamePlayerusernameLenusernameusernameLen std::lock_guard lock(remotePlayersMutex);
remotePlayers.erase(id);
} else if (type == "positionCorrection") {
serverPosition = {msg["x"], msg["y"], msg["z"]};
} else if (type == "timeUpdate") {
serverTimeOfDay = msg["timeOfDay"];
} else if (type == "chat") {
std::cout << "[" << msg["username"] << "]: " << msg["message"] << std::endl;
usernameusername << " (ID: " << ) }
}
void NetworkManager::sendLogin(const std::string& username, const std::string& password) {
if (!loginConnected) return;
json msg;
msg["type"] = "login";
msg["username"] = username;
msg["password"] = password;
std::string data = msg.dump() + "\n";
boost::asio::async_write(loginSocket, boost::asio::buffer(data),
[](boost::system::error_code ec, std::size_t) {
if (ec) std::cerr << "Send login error: " << ec.message() << std::endl;
});
}
void NetworkManager::sendMovement(const Vector3& position, float yaw, float pitch, const Vector3& velocity) {
if (!worldConnected) return;
json msg;
msg["type"] = "movement";
msg["x"] = position.x;
msg["y"] = position.y;
msg["z"] = position.z;
msg["yaw"] = yaw;
msg["pitch"] = pitch;
msg["velX"] = velocity.x;
msg["velY"] = velocity.y;
msg["velZ"] = velocity.z;
std::string data = msg.dump() + "\n";
boost::asio::async_write(worldSocket, boost::asio::buffer(data),
[](boost::system::error_code ec, std::size_t) {
if (ec) std::cerr << "Send movement error: " << ec.message() << std::endl;
});
}
void NetworkManager::handleTimeSync(const uint8_t* data, std::size_t size) {
if (size < 5) return;
float timeOfDay;
memcpy&timeOfDay,&data[1],sizeof(timeOfDay)) serverTimeOfDay.store(timeOfDay);
} }
Vector3 NetworkManager::getPosition() { Vector3 NetworkManager::getPosition() {
@ -278,53 +401,7 @@ Vector3 NetworkManager::getPosition() {
return serverPosition; return serverPosition;
} }
void NetworkManager::handleColorChanged(const uint8_t* data, std::size_t size) {
// Message format: [type(1)][id(4)][colorLen(1)][color(colorLen)]
if (size < 6) return;
uint32_t id;
std::memcpy(&id, &data[1], sizeof(id));
uint8_t colorLen = data[5];
if (size >= 6u + colorLen) {
std::string newColor(reinterpret_cast<const char*>(&data[6]), colorLen);
if (id == playerID) {
playerColor = newColor;
std::cout << "Your color changed to " << newColor << "\n";
} else {
std::lock_guard lock(remotePlayersMutex);
if (remotePlayers.find(id) != remotePlayers.end()) {
remotePlayers[id].color = newColor;
std::cout << "Player " << id << " changed color to " << newColor << "\n";
}
}
}
}
std::unordered_map<uint32_t, RemotePlayer> NetworkManager::getRemotePlayers() { std::unordered_map<uint32_t, RemotePlayer> NetworkManager::getRemotePlayers() {
std::lock_guard lock(remotePlayersMutex); std::lock_guard lock(remotePlayersMutex);
return remotePlayers; return remotePlayers;
} }}
void NetworkManager::handleLoginResponse(const uint8_t* data, std::size_t size) {
// Message format: [type(1)][success(1)][messageLen(1)][message(messageLen)]
if (size < 3) return;
uint8_t success = data[1];
uint8_t msgLen = data[2];
if (size >= 3u + msgLen) {
std::string message(reinterpret_cast<const char*>(&data[3]), msgLen);
if (success == 0) {
// Login failed
loginErrorMsg = message;
connected = false;
std::cout << "Login failed: " << message << "\n";
} else {
// Login succeeded, wait for spawn message
std::cout << "Login accepted, waiting for spawn...\n";
}
}
}

View File

@ -7,87 +7,125 @@
#include <mutex> #include <mutex>
#include <atomic> #include <atomic>
#include <unordered_map> #include <unordered_map>
#include <array> #include <memory>
using namespace boost::asio; using namespace boost::asio;
using ip::udp; using ip::tcp;
// messages
enum class MessageType : uint8_t { LoginRequest 0x20RegisterRequest0x21,
Login = 0x01, LoginResponse = 0x22,
Position = 0x02,
Spawn = 0x03,
Move = 0x04,
Update = 0x05,
PlayerJoined = 0x06,
PlayerLeft = 0x07,
PlayerList = 0x08,
ChangeColor = 0x09,
ColorChanged = 0x0A,
LoginResponse = 0x0B,
Logout = 0x0C,
Heartbeat = 0x0D,
TimeSync = 0x0E
};
// World messages
Auth = 0x30,
AuthResponse = 0x31,
Logout = 0x0C
struct RemotePlayer { struct RemotePlayer {
uint32_t id; uint32_t id;
Vector3 position; Vector3 position;
std::string color; std::string usernameusername;
float lastUpdate; float lastUpdate;
}; };
class NetworkManager { struct LoginResult {
bool success;
std::array<uint8_t, 32> token;
std::string worldHost;
uint16_t worldPort;
uint32_t playerID;
std::string message;
};
class NetworkManager : public std::enable_shared_from_this<NetworkManager> {
public: public:
NetworkManager(); NetworkManager();
~NetworkManager(); ~NetworkManager();
void sendLogin(); // Login server connection
void sendLoginWithUsername(const std::string& username); bool connectToLogin(const std::string& server, uint16_t port);
void sendLogout(); void sendLogin(const std::string& username, const std::string& password);
void sendMove(float dx, float dy, float dz);
void sendColorChange(const std::string& newColor);
void sendHeartbeat();
Vector3 getPosition(); // World server connection
bool isConnected() const { return connected; } bool connectToWorld(const std::string& server, uint16_t port, const std::string& token);
bool hasLoginError() const { return !loginErrorMsg.empty(); } void sendMovement(const Vector3& position, float yaw, float pitch, const Vector3& velocity);
std::string getLoginError() const { return loginErrorMsg; } void sendChat(const std::string& message);
// Disconnect and cleanup
void disconnect();
// State getters
// Login server operations
connectToLoginServerconst std::string& host, uint16_t portsendLoginRequest, const std::string& password);
bool waitForLoginResponse(LoginResult& result, float timeout = 5.0f);
// World server operations
void connectToWorldServer(const std::string& host, uint16_t portsendAuthconst std::array<uint8_t, 32>& token void sendLogout();
// State getters bool isConnected() const { return connected; }
bool isLoginSuccessisAuthenticated() const { return loginSuccessauthenticated; }
std::string getAuthToken() const { return authToken; }
std::string getWorldServerUrl() const { return worldServerUrl; }
uint32_t getPlayerID() const { return playerID; } uint32_t getPlayerID() const { return playerID; }
std::string getPlayerColor() const { return playerColor; }
std::unordered_map<uint32_t, RemotePlayer> getRemotePlayers(); std::unordered_map<uint32_t, RemotePlayer> getRemotePlayers();
float getServerTimeOfDay() const { return serverTimeOfDay.load(); } float getServerTimeOfDay() const { return serverTimeOfDay.load(); }
// Available colors for cycling
static const std::vector<std::string> AVAILABLE_COLORS;
private: private:
io_context ioContext; io_context ioContext;
udp::socket socket{ioContext}; tcp::socket loginSocket{ioContext};
udp::endpoint serverEndpoint; udp:: worldSocket;
tcp::socket worldSocketloginEndpoint;
udp::endpoint worldEndpoint;
std::thread ioThread; std::thread ioThread;
std::array<uint8_t, 1024> recvBuffer;
boost::asio::streambuf loginBuffer;
boost::asio::streambuf worldBuffer;
// Connection state
std::atomic<bool> loginConnected{false};
std::atomic<bool> worldConnected{false};
std::atomic<bool> connected{false};
std::atomic<bool> loginSuccess{false};
// Authentication
std::string authToken;
std::string worldServerUrl;
std::string loginErrorMsg;
// Player state
loginRecvBuffer
std::array<uint8_t, 1024> worldRecvBuffer;
std::atomic<uint32_t> playerID{0}; std::atomic<uint32_t> playerID{0};
std::string playerColor{"red"};
std::string currentUsername{""};
std::string loginErrorMsg{""};
std::mutex positionMutex; std::mutex positionMutex;
Vector3 serverPosition{0, 0, 0}; Vector3 serverPosition{0, 0, 0};
std::atomic<bool> connected{false};
Remote players
std::atomic<bool> authenticated{false};
std::atomic<bool> loginResponseReceived{false};
LoginResult lastLoginResult;
std::mutex loginMutex;
std::mutex remotePlayersMutex; std::mutex remotePlayersMutex;
std::unordered_map<uint32_t, RemotePlayer> remotePlayers; std::unordered_map<uint32_t, RemotePlayer> remotePlayers;
std::atomic<float> serverTimeOfDay{0.0f};
void startReceive(); // World state
void processMessage(const uint8_t* data, std::size_t size); std::atomic<float> serverTimeOfDay{12.0f};
void handleSpawn(const uint8_t* data, std::size_t size);
void handleUpdate(const uint8_t* data, std::size_t size); // Message processing
void handlePlayerJoined(const uint8_t* data, std::size_t size); void startLoginReceive();
void handlePlayerLeft(const uint8_t* data, std::size_t size); void startWorldReceive();
void handlePlayerList(const uint8_t* data, std::size_t size); void processLoginMessage(const nlohmann::json& msg);
void handleColorChanged(const uint8_t* data, std::size_t size); void processWorldMessage(const nlohmann::json& msg);
void handleLoginResponse(const uint8_t* data, std::size_t size);
void handleTimeSync(const uint8_t* data, std::size_t size);
}; };
std::atomic<float> serverTimeOfDay{12.0f};
void startLoginReceive();
void startWorldReceive();
void processLoginMessage(const uint8_t* data, std::size_t size);
void processWorldMessage(const uint8_t* data, std::size_t size);
// World message handlers
void handleAuthResponse(const uint8_t* data, std::size_t size);
// Login message handlers
void handleLoginResponse(const uint8_t* data, std::size_t size)};

View File

@ -3,56 +3,41 @@ package main
import ( import (
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"encoding/json"
"flag" "flag"
"fmt" "fmt"
"log" "log"
"net" "net"
"server/internal/db" "server/internal/db"
"server/internal/net/packets"
"sync"
"time" "time"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
type LoginServer struct { type LoginServer struct {
listener net.Listener conn *net.UDPConn
database *db.Database database *db.Database
worldURL string worldHost string
worldPort uint16
serverID string serverID string
clients map[string]*LoginClient // Map of addr string to client
clientsLock sync.RWMutex
} }
type LoginRequest struct { type LoginClient struct {
Type string `json:"type"` addr *net.UDPAddr
Username string `json:"username"` lastSeen time.Time
Password string `json:"password"` username string
} authed bool
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() { func main() {
var ( var (
port = flag.String("port", "8081", "Login server port") port = flag.String("port", "9998", "Login server port")
dbDSN = flag.String("db", "user:password@tcp(localhost:3306)/game", "Database DSN") dbDSN = flag.String("db", "user:password@tcp(localhost:3306)/game", "Database DSN")
worldURL = flag.String("world", "localhost:8082", "World server URL") worldHost = flag.String("worldhost", "localhost", "World server host")
worldPort = flag.Uint("worldport", 9999, "World server port")
) )
flag.Parse() flag.Parse()
@ -64,152 +49,154 @@ func main() {
server := &LoginServer{ server := &LoginServer{
database: database, database: database,
worldURL: *worldURL, worldHost: *worldHost,
worldPort: uint16(*worldPort),
serverID: generateServerID(), serverID: generateServerID(),
clients: make(map[string]*LoginClient),
} }
listener, err := net.Listen("tcp", ":"+*port) addr, err := net.ResolveUDPAddr("udp", ":"+*port)
if err != nil {
log.Fatalf("Failed to resolve address: %v", err)
}
conn, err := net.ListenUDP("udp", addr)
if err != nil { if err != nil {
log.Fatalf("Failed to start login server: %v", err) log.Fatalf("Failed to start login server: %v", err)
} }
server.listener = listener server.conn = conn
log.Printf("Login server started on port %s (ID: %s)", *port, server.serverID) log.Printf("Login server started on UDP port %s (ID: %s)", *port, server.serverID)
log.Printf("World server configured at %s:%d", server.worldHost, server.worldPort)
go server.cleanupSessions() go server.cleanupSessions()
go server.cleanupClients()
// Main packet processing loop
buffer := make([]byte, 1024)
for { for {
conn, err := listener.Accept() n, clientAddr, err := conn.ReadFromUDP(buffer)
if err != nil { if err != nil {
log.Printf("Accept error: %v", err) log.Printf("Read error: %v", err)
continue continue
} }
go server.handleConnection(conn)
go server.handlePacket(buffer[:n], clientAddr)
} }
} }
func (s *LoginServer) handleConnection(conn net.Conn) { func (s *LoginServer) handlePacket(data []byte, addr *net.UDPAddr) {
defer conn.Close() if len(data) == 0 {
decoder := json.NewDecoder(conn)
encoder := json.NewEncoder(conn)
for {
var msg json.RawMessage
if err := decoder.Decode(&msg); err != nil {
return return
} }
var baseMsg struct { msgType := data[0]
Type string `json:"type"`
}
if err := json.Unmarshal(msg, &baseMsg); err != nil {
continue
}
switch baseMsg.Type { switch msgType {
case "login": case packets.MSG_LOGIN_REQUEST:
var req LoginRequest s.handleLogin(data, addr)
if err := json.Unmarshal(msg, &req); err != nil { case packets.MSG_REGISTER_REQUEST:
continue s.handleRegister(data, addr)
}
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) { func (s *LoginServer) handleLogin(data []byte, addr *net.UDPAddr) {
player, err := s.database.GetPlayerByUsername(req.Username) username, password, ok := packets.DecodeLoginRequest(data)
if !ok {
s.sendLoginResponse(addr, false, nil, 0, "Invalid request")
return
}
// Update client tracking
s.clientsLock.Lock()
s.clients[addr.String()] = &LoginClient{
addr: addr,
lastSeen: time.Now(),
username: username,
authed: false,
}
s.clientsLock.Unlock()
player, err := s.database.GetPlayerByUsername(username)
if err != nil { if err != nil {
encoder.Encode(LoginResponse{ // Try to register if user doesn't exist
Type: "loginResponse", s.handleRegister(data, addr)
Success: false,
Message: "Invalid username or password",
})
return return
} }
if err := bcrypt.CompareHashAndPassword([]byte(player.PasswordHash), []byte(req.Password)); err != nil { if err := bcrypt.CompareHashAndPassword([]byte(player.PasswordHash), []byte(password)); err != nil {
encoder.Encode(LoginResponse{ s.sendLoginResponse(addr, false, nil, 0, "Invalid username or password")
Type: "loginResponse",
Success: false,
Message: "Invalid username or password",
})
return return
} }
token := generateToken() // Generate session token
if err := s.database.CreateSession(player.ID, token, "world", s.serverID, 24*time.Hour); err != nil { token := make([]byte, 32)
encoder.Encode(LoginResponse{ rand.Read(token)
Type: "loginResponse", tokenHex := hex.EncodeToString(token)
Success: false,
Message: "Failed to create session", if err := s.database.CreateSession(player.ID, tokenHex, "world", s.serverID, 24*time.Hour); err != nil {
}) s.sendLoginResponse(addr, false, nil, 0, "Failed to create session")
return return
} }
s.database.UpdateLastLogin(player.ID) s.database.UpdateLastLogin(player.ID)
encoder.Encode(LoginResponse{ // Mark client as authenticated
Type: "loginResponse", s.clientsLock.Lock()
Success: true, if client, exists := s.clients[addr.String()]; exists {
Token: token, client.authed = true
WorldURL: s.worldURL, }
PlayerID: player.ID, s.clientsLock.Unlock()
})
s.sendLoginResponse(addr, true, token, uint32(player.ID), "Login successful")
} }
func (s *LoginServer) handleRegister(req RegisterRequest, encoder *json.Encoder) { func (s *LoginServer) handleRegister(data []byte, addr *net.UDPAddr) {
if len(req.Username) < 3 || len(req.Username) > 20 { username, password, ok := packets.DecodeRegisterRequest(data)
encoder.Encode(RegisterResponse{ if !ok {
Type: "registerResponse", s.sendLoginResponse(addr, false, nil, 0, "Invalid request")
Success: false,
Message: "Username must be between 3 and 20 characters",
})
return return
} }
if len(req.Password) < 6 { if len(username) < 3 || len(username) > 20 {
encoder.Encode(RegisterResponse{ s.sendLoginResponse(addr, false, nil, 0, "Username must be 3-20 characters")
Type: "registerResponse",
Success: false,
Message: "Password must be at least 6 characters",
})
return return
} }
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if len(password) < 6 {
s.sendLoginResponse(addr, false, nil, 0, "Password must be at least 6 characters")
return
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil { if err != nil {
encoder.Encode(RegisterResponse{ s.sendLoginResponse(addr, false, nil, 0, "Failed to process password")
Type: "registerResponse",
Success: false,
Message: "Failed to process password",
})
return return
} }
_, err = s.database.CreatePlayer(req.Username, string(hashedPassword)) playerID, err := s.database.CreatePlayer(username, string(hashedPassword))
if err != nil { if err != nil {
encoder.Encode(RegisterResponse{ // Player might already exist, try login instead
Type: "registerResponse", s.handleLogin(data, addr)
Success: false,
Message: "Username already exists",
})
return return
} }
encoder.Encode(RegisterResponse{ // Auto-login after registration
Type: "registerResponse", token := make([]byte, 32)
Success: true, rand.Read(token)
Message: "Registration successful", tokenHex := hex.EncodeToString(token)
})
if err := s.database.CreateSession(playerID, tokenHex, "world", s.serverID, 24*time.Hour); err != nil {
s.sendLoginResponse(addr, false, nil, 0, "Registration successful but login failed")
return
}
s.sendLoginResponse(addr, true, token, uint32(playerID), "Registration successful")
}
func (s *LoginServer) sendLoginResponse(addr *net.UDPAddr, success bool, token []byte, playerID uint32, message string) {
response := packets.EncodeLoginResponse(success, token, s.worldHost, s.worldPort, playerID, message)
s.conn.WriteToUDP(response, addr)
} }
func (s *LoginServer) cleanupSessions() { func (s *LoginServer) cleanupSessions() {
@ -223,10 +210,20 @@ func (s *LoginServer) cleanupSessions() {
} }
} }
func generateToken() string { func (s *LoginServer) cleanupClients() {
b := make([]byte, 32) ticker := time.NewTicker(1 * time.Minute)
rand.Read(b) defer ticker.Stop()
return hex.EncodeToString(b)
for range ticker.C {
s.clientsLock.Lock()
now := time.Now()
for addr, client := range s.clients {
if now.Sub(client.lastSeen) > 5*time.Minute {
delete(s.clients, addr)
}
}
s.clientsLock.Unlock()
}
} }
func generateServerID() string { func generateServerID() string {

View File

@ -1,7 +1,7 @@
package main package main
import ( import (
"encoding/json" "encoding/hex"
"flag" "flag"
"fmt" "fmt"
"log" "log"
@ -14,44 +14,31 @@ import (
) )
type WorldServer struct { type WorldServer struct {
listener net.Listener conn *net.UDPConn
database *db.Database database *db.Database
serverID string serverID string
clients map[int]*Client clients map[uint32]*Client
clientLock sync.RWMutex clientLock sync.RWMutex
world *packets.WorldConfig world *packets.WorldConfig
heightmap [][]float32 heightmap [][]float32
ticker *time.Ticker
timeOfDay float32 timeOfDay float32
nextID uint32
} }
type Client struct { type Client struct {
ID int ID uint32
PlayerID int64 PlayerID int64
Username string Username string
Conn net.Conn Addr *net.UDPAddr
Encoder *json.Encoder
Decoder *json.Decoder
Position packets.Vec3 Position packets.Vec3
Rotation packets.Vec2
Velocity packets.Vec3 Velocity packets.Vec3
LastUpdate time.Time LastUpdate time.Time
} Authed bool
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() { func main() {
var ( var (
port = flag.String("port", "8082", "World server port") port = flag.String("port", "9999", "World server port")
dbDSN = flag.String("db", "user:password@tcp(localhost:3306)/game", "Database DSN") dbDSN = flag.String("db", "user:password@tcp(localhost:3306)/game", "Database DSN")
) )
flag.Parse() flag.Parse()
@ -68,285 +55,269 @@ func main() {
server := &WorldServer{ server := &WorldServer{
database: database, database: database,
serverID: generateServerID(), serverID: generateServerID(),
clients: make(map[int]*Client), clients: make(map[uint32]*Client),
world: worldConfig, world: worldConfig,
heightmap: heightmap, heightmap: heightmap,
timeOfDay: 12.0, timeOfDay: 12.0,
nextID: 1,
} }
listener, err := net.Listen("tcp", ":"+*port) addr, err := net.ResolveUDPAddr("udp", ":"+*port)
if err != nil {
log.Fatalf("Failed to resolve address: %v", err)
}
conn, err := net.ListenUDP("udp", addr)
if err != nil { if err != nil {
log.Fatalf("Failed to start world server: %v", err) log.Fatalf("Failed to start world server: %v", err)
} }
server.listener = listener server.conn = conn
log.Printf("World server started on port %s (ID: %s)", *port, server.serverID) log.Printf("World server started on UDP port %s (ID: %s)", *port, server.serverID)
log.Printf("World bounds: Min(%v), Max(%v)", server.world.MinBounds, server.world.MaxBounds) log.Printf("World bounds: Min(%v), Max(%v)", server.world.MinBounds, server.world.MaxBounds)
go server.gameLoop() go server.gameLoop()
go server.updateTimeOfDay() go server.updateTimeOfDay()
// Main packet processing loop
buffer := make([]byte, 1024)
for { for {
conn, err := listener.Accept() n, clientAddr, err := conn.ReadFromUDP(buffer)
if err != nil { if err != nil {
log.Printf("Accept error: %v", err) log.Printf("Read error: %v", err)
continue continue
} }
go server.handleConnection(conn)
go server.handlePacket(buffer[:n], clientAddr)
} }
} }
func (s *WorldServer) handleConnection(conn net.Conn) { func (s *WorldServer) handlePacket(data []byte, addr *net.UDPAddr) {
defer conn.Close() if len(data) == 0 {
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 return
} }
session, err := s.database.ValidateSession(authReq.Token) msgType := data[0]
switch msgType {
case packets.MSG_AUTH:
s.handleAuth(data, addr)
case packets.MSG_MOVE:
s.handleMove(data, addr)
case packets.MSG_HEARTBEAT:
s.handleHeartbeat(data, addr)
case packets.MSG_LOGOUT:
s.handleLogout(data, addr)
}
}
func (s *WorldServer) handleAuth(data []byte, addr *net.UDPAddr) {
tokenBytes, ok := packets.DecodeAuth(data)
if !ok {
response := packets.EncodeAuthResponse(false, "Invalid auth packet")
s.conn.WriteToUDP(response, addr)
return
}
tokenHex := hex.EncodeToString(tokenBytes)
session, err := s.database.ValidateSession(tokenHex)
if err != nil { if err != nil {
encoder.Encode(AuthResponse{ response := packets.EncodeAuthResponse(false, "Invalid or expired session")
Type: "authResponse", s.conn.WriteToUDP(response, addr)
Success: false,
Message: "Invalid or expired session",
})
return return
} }
player, err := s.database.GetPlayerByID(session.PlayerID) player, err := s.database.GetPlayerByID(session.PlayerID)
if err != nil { if err != nil {
encoder.Encode(AuthResponse{ response := packets.EncodeAuthResponse(false, "Player not found")
Type: "authResponse", s.conn.WriteToUDP(response, addr)
Success: false,
Message: "Player not found",
})
return return
} }
encoder.Encode(AuthResponse{ // Send auth success
Type: "authResponse", response := packets.EncodeAuthResponse(true, "Authenticated")
Success: true, s.conn.WriteToUDP(response, addr)
})
// Get saved position or default
position, _ := s.database.GetPlayerPosition(player.ID) position, _ := s.database.GetPlayerPosition(player.ID)
// Create client
s.clientLock.Lock()
clientID := s.nextID
s.nextID++
client := &Client{ client := &Client{
ID: s.getNextClientID(), ID: clientID,
PlayerID: player.ID, PlayerID: player.ID,
Username: player.Username, Username: player.Username,
Conn: conn, Addr: addr,
Encoder: encoder,
Decoder: decoder,
Position: packets.Vec3{X: position.X, Y: position.Y, Z: position.Z}, Position: packets.Vec3{X: position.X, Y: position.Y, Z: position.Z},
Rotation: packets.Vec2{X: position.Yaw, Y: position.Pitch},
LastUpdate: time.Now(), LastUpdate: time.Now(),
Authed: true,
} }
s.clients[clientID] = client
s.clientLock.Unlock()
s.addClient(client) // Send spawn packet
defer s.removeClient(client) spawnMsg := packets.EncodeSpawnPacket(clientID, client.Position)
s.conn.WriteToUDP(spawnMsg, addr)
s.sendInitialState(client) // Send time sync
timeMsg := packets.EncodeTimeSyncPacket(s.timeOfDay)
s.conn.WriteToUDP(timeMsg, addr)
// Send existing players to new client
s.sendPlayerList(client)
// Broadcast new player to others
s.broadcastPlayerJoined(client) s.broadcastPlayerJoined(client)
for { log.Printf("Player %s (ID: %d) joined the world", player.Username, clientID)
var msg json.RawMessage
if err := decoder.Decode(&msg); err != nil {
break
} }
var baseMsg struct { func (s *WorldServer) handleMove(data []byte, addr *net.UDPAddr) {
Type string `json:"type"` playerID, delta, ok := packets.DecodeMovePacket(data)
} if !ok {
if err := json.Unmarshal(msg, &baseMsg); err != nil { return
continue
} }
switch baseMsg.Type { s.clientLock.Lock()
case "movement": client, exists := s.clients[playerID]
var moveMsg packets.MovementMessage if !exists || !client.Authed {
if err := json.Unmarshal(msg, &moveMsg); err != nil { s.clientLock.Unlock()
continue return
}
s.handleMovement(client, moveMsg)
case "chat":
var chatMsg packets.ChatMessage
if err := json.Unmarshal(msg, &chatMsg); err != nil {
continue
}
s.handleChat(client, chatMsg)
}
} }
// Update position with delta (server authoritative)
moveSpeed := float32(10.0)
deltaTime := float32(0.016) // Assume 60 FPS
client.Position.X += delta.X * moveSpeed * deltaTime
client.Position.Z += delta.Z * moveSpeed * deltaTime
// Clamp to world bounds
client.Position = s.world.ClampPosition(client.Position)
// Check terrain height
terrainHeight := s.world.GetHeightAt(s.heightmap, client.Position.X, client.Position.Z)
if client.Position.Y < terrainHeight {
client.Position.Y = terrainHeight
}
client.LastUpdate = time.Now()
s.clientLock.Unlock()
// Send position update back to client
updateMsg := packets.EncodeUpdatePacket(playerID, client.Position)
s.conn.WriteToUDP(updateMsg, addr)
// Broadcast to other players
s.broadcastMovement(client)
}
func (s *WorldServer) handleHeartbeat(data []byte, addr *net.UDPAddr) {
playerID, ok := packets.DecodeHeartbeatPacket(data)
if !ok {
return
}
s.clientLock.Lock()
if client, exists := s.clients[playerID]; exists {
client.LastUpdate = time.Now()
}
s.clientLock.Unlock()
}
func (s *WorldServer) handleLogout(data []byte, addr *net.UDPAddr) {
playerID, ok := packets.DecodeLogoutPacket(data)
if !ok {
return
}
s.clientLock.Lock()
client, exists := s.clients[playerID]
if !exists {
s.clientLock.Unlock()
return
}
// Save position before removing
s.database.SavePlayerPosition(client.PlayerID, &db.Position{ s.database.SavePlayerPosition(client.PlayerID, &db.Position{
X: client.Position.X, X: client.Position.X,
Y: client.Position.Y, Y: client.Position.Y,
Z: client.Position.Z, Z: client.Position.Z,
Yaw: client.Rotation.X,
Pitch: client.Rotation.Y,
World: "main", World: "main",
}) })
delete(s.clients, playerID)
s.clientLock.Unlock()
// Broadcast player left
leftMsg := packets.EncodePlayerLeftPacket(playerID)
s.broadcast(leftMsg, playerID)
log.Printf("Player %s (ID: %d) logged out", client.Username, playerID)
} }
func (s *WorldServer) handleMovement(client *Client, msg packets.MovementMessage) { func (s *WorldServer) sendPlayerList(newClient *Client) {
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() s.clientLock.RLock()
defer s.clientLock.RUnlock() defer s.clientLock.RUnlock()
for _, client := range s.clients { for _, client := range s.clients {
client.Encoder.Encode(msg) if client.ID != newClient.ID {
msg := packets.EncodePlayerJoinedPacket(client.ID, client.Position, client.Username)
s.conn.WriteToUDP(msg, newClient.Addr)
}
} }
} }
func (s *WorldServer) broadcastExcept(msg interface{}, excludeID int) { func (s *WorldServer) broadcastPlayerJoined(newClient *Client) {
msg := packets.EncodePlayerJoinedPacket(newClient.ID, newClient.Position, newClient.Username)
s.broadcast(msg, newClient.ID)
}
func (s *WorldServer) broadcastMovement(movedClient *Client) {
msg := packets.EncodeUpdatePacket(movedClient.ID, movedClient.Position)
s.broadcast(msg, movedClient.ID)
}
func (s *WorldServer) broadcast(msg []byte, excludeID uint32) {
s.clientLock.RLock() s.clientLock.RLock()
defer s.clientLock.RUnlock() defer s.clientLock.RUnlock()
for _, client := range s.clients { for _, client := range s.clients {
if client.ID != excludeID { if client.ID != excludeID && client.Authed {
client.Encoder.Encode(msg) s.conn.WriteToUDP(msg, client.Addr)
} }
} }
} }
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() { func (s *WorldServer) gameLoop() {
ticker := time.NewTicker(50 * time.Millisecond) ticker := time.NewTicker(50 * time.Millisecond)
defer ticker.Stop() defer ticker.Stop()
for range ticker.C { for range ticker.C {
s.clientLock.RLock() now := time.Now()
for _, client := range s.clients { s.clientLock.Lock()
if time.Since(client.LastUpdate) > 30*time.Second { for id, client := range s.clients {
go func(c *Client) { if now.Sub(client.LastUpdate) > 30*time.Second {
c.Conn.Close() // Save position before timeout
}(client) s.database.SavePlayerPosition(client.PlayerID, &db.Position{
X: client.Position.X,
Y: client.Position.Y,
Z: client.Position.Z,
World: "main",
})
delete(s.clients, id)
// Broadcast player left
leftMsg := packets.EncodePlayerLeftPacket(id)
s.broadcast(leftMsg, id)
log.Printf("Player %s (ID: %d) timed out", client.Username, id)
} }
} }
s.clientLock.RUnlock() s.clientLock.Unlock()
} }
} }
@ -360,23 +331,15 @@ func (s *WorldServer) updateTimeOfDay() {
s.timeOfDay -= 24.0 s.timeOfDay -= 24.0
} }
s.broadcast(packets.TimeUpdateMessage{ // Broadcast time update
Type: "timeUpdate", msg := packets.EncodeTimeSyncPacket(s.timeOfDay)
TimeOfDay: s.timeOfDay, s.clientLock.RLock()
}) for _, client := range s.clients {
if client.Authed {
s.conn.WriteToUDP(msg, client.Addr)
} }
} }
s.clientLock.RUnlock()
func (s *WorldServer) getNextClientID() int {
s.clientLock.Lock()
defer s.clientLock.Unlock()
id := 1
for {
if _, exists := s.clients[id]; !exists {
return id
}
id++
} }
} }

View File

@ -0,0 +1,220 @@
package packets
import (
"encoding/binary"
"math"
)
// Message type constants
const (
// Login server messages
MSG_LOGIN_REQUEST = 0x20
MSG_REGISTER_REQUEST = 0x21
MSG_LOGIN_RESPONSE = 0x22
// World server messages
MSG_AUTH = 0x30
MSG_AUTH_RESPONSE = 0x31
MSG_SPAWN = 0x03
MSG_MOVE = 0x04
MSG_UPDATE = 0x05
MSG_PLAYER_JOINED = 0x06
MSG_PLAYER_LEFT = 0x07
MSG_PLAYER_LIST = 0x08
MSG_HEARTBEAT = 0x0D
MSG_TIME_SYNC = 0x0E
MSG_LOGOUT = 0x0C
)
// EncodeLoginResponse creates a login response packet
func EncodeLoginResponse(success bool, token []byte, worldHost string, worldPort uint16, playerID uint32, message string) []byte {
msgBytes := []byte(message)
hostBytes := []byte(worldHost)
// Calculate total size
size := 1 + 1 + 1 + len(msgBytes) // type + success + msgLen + message
if success {
size += 32 + 1 + len(hostBytes) + 2 + 4 // token + hostLen + host + port + playerID
}
msg := make([]byte, size)
offset := 0
msg[offset] = MSG_LOGIN_RESPONSE
offset++
if success {
msg[offset] = 1
} else {
msg[offset] = 0
}
offset++
msg[offset] = uint8(len(msgBytes))
offset++
copy(msg[offset:], msgBytes)
offset += len(msgBytes)
if success {
copy(msg[offset:offset+32], token)
offset += 32
msg[offset] = uint8(len(hostBytes))
offset++
copy(msg[offset:], hostBytes)
offset += len(hostBytes)
binary.LittleEndian.PutUint16(msg[offset:], worldPort)
offset += 2
binary.LittleEndian.PutUint32(msg[offset:], playerID)
}
return msg
}
// DecodeLoginRequest decodes a login request packet
func DecodeLoginRequest(data []byte) (username, password string, ok bool) {
if len(data) < 3 {
return "", "", false
}
offset := 1 // Skip message type
usernameLen := data[offset]
offset++
if len(data) < offset+int(usernameLen)+1 {
return "", "", false
}
username = string(data[offset : offset+int(usernameLen)])
offset += int(usernameLen)
passwordLen := data[offset]
offset++
if len(data) < offset+int(passwordLen) {
return "", "", false
}
password = string(data[offset : offset+int(passwordLen)])
return username, password, true
}
// DecodeRegisterRequest decodes a register request packet
func DecodeRegisterRequest(data []byte) (username, password string, ok bool) {
// Same format as login request
return DecodeLoginRequest(data)
}
// EncodeAuthResponse creates an auth response packet
func EncodeAuthResponse(success bool, message string) []byte {
msgBytes := []byte(message)
msg := make([]byte, 3+len(msgBytes))
msg[0] = MSG_AUTH_RESPONSE
if success {
msg[1] = 1
} else {
msg[1] = 0
}
msg[2] = uint8(len(msgBytes))
copy(msg[3:], msgBytes)
return msg
}
// DecodeAuth decodes an auth packet with token
func DecodeAuth(data []byte) (token []byte, ok bool) {
if len(data) < 33 { // 1 byte type + 32 byte token
return nil, false
}
token = make([]byte, 32)
copy(token, data[1:33])
return token, true
}
// EncodeSpawnPacket creates a spawn packet
func EncodeSpawnPacket(playerID uint32, position Vec3) []byte {
msg := make([]byte, 17)
msg[0] = MSG_SPAWN
binary.LittleEndian.PutUint32(msg[1:5], playerID)
binary.LittleEndian.PutUint32(msg[5:9], math.Float32bits(position.X))
binary.LittleEndian.PutUint32(msg[9:13], math.Float32bits(position.Y))
binary.LittleEndian.PutUint32(msg[13:17], math.Float32bits(position.Z))
return msg
}
// EncodeUpdatePacket creates an update packet
func EncodeUpdatePacket(playerID uint32, position Vec3) []byte {
msg := make([]byte, 17)
msg[0] = MSG_UPDATE
binary.LittleEndian.PutUint32(msg[1:5], playerID)
binary.LittleEndian.PutUint32(msg[5:9], math.Float32bits(position.X))
binary.LittleEndian.PutUint32(msg[9:13], math.Float32bits(position.Y))
binary.LittleEndian.PutUint32(msg[13:17], math.Float32bits(position.Z))
return msg
}
// EncodePlayerJoinedPacket creates a player joined packet
func EncodePlayerJoinedPacket(playerID uint32, position Vec3, username string) []byte {
usernameBytes := []byte(username)
msg := make([]byte, 18+len(usernameBytes))
msg[0] = MSG_PLAYER_JOINED
binary.LittleEndian.PutUint32(msg[1:5], playerID)
binary.LittleEndian.PutUint32(msg[5:9], math.Float32bits(position.X))
binary.LittleEndian.PutUint32(msg[9:13], math.Float32bits(position.Y))
binary.LittleEndian.PutUint32(msg[13:17], math.Float32bits(position.Z))
msg[17] = uint8(len(usernameBytes))
copy(msg[18:], usernameBytes)
return msg
}
// EncodePlayerLeftPacket creates a player left packet
func EncodePlayerLeftPacket(playerID uint32) []byte {
msg := make([]byte, 5)
msg[0] = MSG_PLAYER_LEFT
binary.LittleEndian.PutUint32(msg[1:5], playerID)
return msg
}
// EncodeTimeSyncPacket creates a time sync packet
func EncodeTimeSyncPacket(timeOfDay float32) []byte {
msg := make([]byte, 5)
msg[0] = MSG_TIME_SYNC
binary.LittleEndian.PutUint32(msg[1:5], math.Float32bits(timeOfDay))
return msg
}
// DecodeMovePacket decodes a move packet
func DecodeMovePacket(data []byte) (playerID uint32, delta Vec3, ok bool) {
if len(data) < 17 {
return 0, Vec3{}, false
}
playerID = binary.LittleEndian.Uint32(data[1:5])
delta.X = math.Float32frombits(binary.LittleEndian.Uint32(data[5:9]))
delta.Y = math.Float32frombits(binary.LittleEndian.Uint32(data[9:13]))
delta.Z = math.Float32frombits(binary.LittleEndian.Uint32(data[13:17]))
return playerID, delta, true
}
// DecodeHeartbeatPacket decodes a heartbeat packet
func DecodeHeartbeatPacket(data []byte) (playerID uint32, ok bool) {
if len(data) < 5 {
return 0, false
}
playerID = binary.LittleEndian.Uint32(data[1:5])
return playerID, true
}
// DecodeLogoutPacket decodes a logout packet
func DecodeLogoutPacket(data []byte) (playerID uint32, ok bool) {
if len(data) < 5 {
return 0, false
}
playerID = binary.LittleEndian.Uint32(data[1:5])
return playerID, true
}