first pass on bringing client up to date with new server structure
This commit is contained in:
parent
151ac6ab8e
commit
72062cfa2c
136
client/LoginWindow.cpp
Normal file
136
client/LoginWindow.cpp
Normal 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
25526
client/includes/json.hpp
Normal file
File diff suppressed because it is too large
Load Diff
332
client/main.cpp
332
client/main.cpp
@ -1,8 +1,6 @@
|
||||
#include <raylib.h>
|
||||
#include <raymath.h>
|
||||
|
||||
#define RAYGUI_IMPLEMENTATION
|
||||
#include "includes/raygui.h"
|
||||
#include <raygui.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
@ -10,21 +8,17 @@
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include "entity/player/PlayerController.hpp"
|
||||
#include "entity/player/PlayerRenderer.hpp"
|
||||
#include "net/NetworkManager.hpp"
|
||||
#include "ui/LoginWindow.hpp"
|
||||
#include "sky/Sky.hpp"
|
||||
#include "render/RenderContext.hpp"
|
||||
#include "utils/Coords.hpp"
|
||||
#include "terrain/Heightmap.hpp"
|
||||
#include "config.hpp"
|
||||
|
||||
enum GameState {
|
||||
STATE_LOGIN,
|
||||
STATE_CONNECTING,
|
||||
STATE_PLAYING
|
||||
};
|
||||
|
||||
//Forwarddeclaration of login window functionbool ShowLoginWindow(std::shared_ptr<NetworkManager> network, LoginResult& result)
|
||||
class Game {
|
||||
GameConfig config;
|
||||
PlayerController playerController;
|
||||
@ -32,27 +26,84 @@ class Game {
|
||||
RenderContext renderContext;
|
||||
Model terrainModel;
|
||||
Heightmap heightmap;
|
||||
NetworkManager network;
|
||||
std::shared_ptr<NetworkManager> network;
|
||||
std::unique_ptr<Sky> sky;
|
||||
Vector3 playerPos{0, 0, 0};
|
||||
Vector3 playerVelocity{0, 0, 0};
|
||||
float playerYaw = 0.0f;
|
||||
float playerPitch = 0.0f;
|
||||
Texture2D terrainTexture;
|
||||
|
||||
// Login UI state
|
||||
GameState gameState = STATE_LOGIN;
|
||||
char usernameBuffer[32] = "";
|
||||
bool editMode = false;
|
||||
std::string loginError = "";
|
||||
std::string currentUsername = "";
|
||||
// Movement tracking
|
||||
float lastMovementUpdate = 0.0f;
|
||||
const float movementUpdateInterval = 0.05f; // Send updates every 50ms
|
||||
|
||||
|
||||
// Heartbeat timing
|
||||
float lastHeartbeatTime = 0.0f;
|
||||
|
||||
// Debug options
|
||||
bool showDebugAxes = false;
|
||||
bool showWorldBounds = false;
|
||||
|
||||
// Player info
|
||||
std::string currentUsername = "";
|
||||
|
||||
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());
|
||||
SetTargetFPS(config.targetFPS);
|
||||
|
||||
@ -87,69 +138,40 @@ public:
|
||||
}
|
||||
|
||||
~Game() {
|
||||
// Send logout if we're connected
|
||||
if (gameState == STATE_PLAYING && network.isConnected()) {
|
||||
network.sendLogout();
|
||||
// Disconnect from servers
|
||||
if (networknetwork)-> {
|
||||
network->disconnect->();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
|
||||
UnloadTexture(terrainTexture);
|
||||
UnloadModel(terrainModel);
|
||||
CloseWindow();
|
||||
if (IsWindowReady()) {
|
||||
UnloadTexture(terrainTexture);
|
||||
UnloadModel(terrainModel);
|
||||
CloseWindow();
|
||||
}
|
||||
}
|
||||
|
||||
void run() {
|
||||
if (!IsWindowReady()) return;
|
||||
|
||||
while (!WindowShouldClose()) {
|
||||
update();
|
||||
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:
|
||||
void update() {
|
||||
switch (gameState) {
|
||||
case STATE_LOGIN:
|
||||
// Login screen - no game updates
|
||||
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";
|
||||
if (!network->isConnected()) {
|
||||
// Lost connection - could show reconnect UI here
|
||||
if (!network->isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update sky with server time if connected, otherwise use local time
|
||||
if (network.isConnected()) {
|
||||
float serverTime = network.getServerTimeOfDay();
|
||||
sky->updateFromServerTime(serverTime);
|
||||
} else {
|
||||
sky->update(GetFrameTime());
|
||||
}
|
||||
// Update sky with server time
|
||||
float serverTime = network->->getServerTimeOfDay();
|
||||
|
||||
sky->updateFromServerTime(serverTime);
|
||||
|
||||
// Time of day controls (for testing)
|
||||
if (IsKeyPressed(KEY_T)) {
|
||||
@ -163,144 +185,37 @@ private:
|
||||
if (IsKeyPressed(KEY_F1)) showDebugAxes = !showDebugAxes;
|
||||
if (IsKeyPressed(KEY_F2)) showWorldBounds = !showWorldBounds;
|
||||
|
||||
// Get server position and update player controller
|
||||
playerPos = network.getPosition();
|
||||
|
||||
// Clamp position to world bounds
|
||||
playerPos = Coords::clampToWorldBounds(playerPos);
|
||||
// Get server position
|
||||
playerPos = network->->getPosition();
|
||||
playerController.setPlayerPosition(playerPos);
|
||||
|
||||
// Clean up disconnected player models
|
||||
auto remotePlayers = network.getRemotePlayers();
|
||||
playerRenderer->cleanupDisconnectedPlayers(remotePlayers);
|
||||
|
||||
// Update player controller (handles camera)
|
||||
float deltaTime = GetFrameTime();
|
||||
-> float deltaTime = GetFrameTime();
|
||||
playerController.update(deltaTime);
|
||||
|
||||
// Get movement input from player controller
|
||||
// Get movement input
|
||||
Vector3 moveInput = playerController.getMoveInput();
|
||||
|
||||
// Send normalized movement direction to server (server handles speed)
|
||||
if (moveInput.x != 0 || moveInput.z != 0) {
|
||||
network.sendMove(moveInput.x, 0, moveInput.z);
|
||||
lastHeartbeatTime = GetTime(); // Reset heartbeat timer when moving
|
||||
}
|
||||
// Calculate velocity based on input
|
||||
const float moveSpeed = 10.0f;
|
||||
playerVelocity = Vector3Scale(moveInput, moveSpeed);
|
||||
|
||||
// Send periodic heartbeats when not moving
|
||||
float currentTime = GetTime();
|
||||
if (currentTime - lastHeartbeatTime >= config.heartbeatInterval) {
|
||||
network.sendHeartbeat();
|
||||
lastHeartbeatTime = currentTime;
|
||||
}
|
||||
// Get camera orientation for yaw/pitch
|
||||
Camera camera = playerController.getCamera();
|
||||
Vector3 forward = Vector3Subtract(camera.target, camera.position);
|
||||
playerYaw = atan2f(forward.x, forward.z);
|
||||
playerPitch = atan2f(forward.y, sqrtf(forward.x * forward.x + forward.z * forward.z));
|
||||
|
||||
// Handle color change with arrow keys
|
||||
static int currentColorIndex = -1;
|
||||
if (IsKeyPressed(KEY_LEFT) || IsKeyPressed(KEY_RIGHT)) {
|
||||
// Get current color index if not set
|
||||
if (currentColorIndex == -1) {
|
||||
auto currentColor = network.getPlayerColor();
|
||||
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 = "";
|
||||
}
|
||||
// Send movement updates at regular intervals or when moving
|
||||
-> float currentTime = GetTime();
|
||||
if (currentTime - lastMovementUpdate >= movementUpdateInterval) {
|
||||
network->sendMovement->(playerPos, playerYaw, playerPitch, playerVelocity);
|
||||
lastMovementUpdate = currentTime;
|
||||
->std::this_thread::sleep_for(std::chrono::milliseconds(100))break // Exit game loop }
|
||||
}
|
||||
|
||||
void render() {
|
||||
BeginDrawing();
|
||||
|
||||
// Clear with a default color first
|
||||
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
|
||||
renderContext.begin3D(playerController.getCamera());
|
||||
|
||||
@ -315,21 +230,18 @@ private:
|
||||
renderContext.submitModel(terrainModel, {0, 0, 0}, 1.0f, WHITE,
|
||||
RenderContext::RenderLayer::TERRAIN);
|
||||
|
||||
// Submit players if connected
|
||||
if (network.isConnected()) {
|
||||
// Local player
|
||||
playerRenderer->renderLocalPlayer(renderContext, playerPos, network.getPlayerColor());
|
||||
// Submit players
|
||||
->// Local player
|
||||
playerRenderer->renderLocalPlayer(renderContext, playerPos, "blue")"blue");
|
||||
|
||||
// Remote players
|
||||
auto remotePlayers = network.getRemotePlayers();
|
||||
playerRenderer->renderRemotePlayers(renderContext, remotePlayers);
|
||||
}
|
||||
// Remote players
|
||||
auto remotePlayers = network->->getRemotePlayers();
|
||||
playerRenderer->renderRemotePlayers(renderContext, remotePlayers);
|
||||
|
||||
// Debug rendering
|
||||
if (showDebugAxes) {
|
||||
renderContext.submitCustom([this]() {
|
||||
Coords::drawDebugAxes(playerPos, 2.0f);
|
||||
// Draw world origin axes
|
||||
Coords::drawDebugAxes({0, 0, 0}, 5.0f);
|
||||
}, RenderContext::RenderLayer::TRANSPARENT);
|
||||
}
|
||||
@ -344,28 +256,28 @@ private:
|
||||
renderContext.end3D();
|
||||
|
||||
// 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("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);
|
||||
if (network.isConnected()) {
|
||||
std::string colorText = "Your color: " + network.getPlayerColor();
|
||||
DrawText(colorText.c_str(), 10, 110, 20, WHITE);
|
||||
}
|
||||
|
||||
// Show time of day
|
||||
float timeHour = sky->getTimeOfDay() * 24.0f;
|
||||
int hour = (int)timeHour;
|
||||
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
|
||||
if (showDebugAxes) {
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -1,237 +1,135 @@
|
||||
#include "NetworkManager.hpp"
|
||||
#include <iostream>
|
||||
#include <cstring>
|
||||
#include <chrono>
|
||||
|
||||
const std::vector<std::string> NetworkManager::AVAILABLE_COLORS = {
|
||||
"red", "green", "orange", "purple", "white"
|
||||
};
|
||||
|
||||
NetworkManager::NetworkManager() : serverEndpoint(ip::make_address("127.0.0.1"), 9999) {
|
||||
socket.open(udp::v4());
|
||||
startReceive();
|
||||
NetworkManager::NetworkManager() {loginSocketworldSocket.open(udp::v4());
|
||||
startLoginReceive();
|
||||
startWorldReceive
|
||||
ioThread = std::thread([this] { ioContext.run(); });
|
||||
}
|
||||
|
||||
NetworkManager::~NetworkManager() {
|
||||
disconnect();
|
||||
ioContext.stop();
|
||||
if (ioThread.joinable()) ioThread.join();
|
||||
}
|
||||
|
||||
void NetworkManager::startReceive() {
|
||||
socket.async_receive_from(
|
||||
buffer(recvBuffer), serverEndpoint,
|
||||
[this](std::error_code ec, std::size_t bytes) {
|
||||
if (!ec && bytes > 0) {
|
||||
processMessage(recvBuffer.data(), bytes);
|
||||
}
|
||||
startReceive();
|
||||
}
|
||||
);
|
||||
}
|
||||
bool NetworkManager::connectToLogin(const std::string& server, uint16_t port) {
|
||||
try {
|
||||
tcp::resolver resolver(ioContext);
|
||||
auto endpoints = resolver.resolve(server, std::to_string(port));
|
||||
boost::asio::connect(loginSocket, endpoints);
|
||||
|
||||
void NetworkManager::processMessage(const uint8_t* data, std::size_t size) {
|
||||
if (size == 0) return;
|
||||
|
||||
auto msgType = static_cast<MessageType>(data[0]);
|
||||
|
||||
switch (msgType) {
|
||||
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;
|
||||
loginConnected = true;
|
||||
startLoginReceive();
|
||||
return true;
|
||||
} catch (std::exception& e) {
|
||||
std::cerr << "Failed to connect to login server: " << e.what() << std::endl;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkManager::handleTimeSync(const uint8_t* data, std::size_t size) {
|
||||
// Message format: [type(1)][timeOfDay(4)]
|
||||
if (size < 5) return;
|
||||
bool NetworkManager::connectToWorld(const std::string& server, uint16_t port, const std::string& token) {
|
||||
try {
|
||||
tcp::resolver resolver(ioContext);
|
||||
auto endpoints = resolver.resolve(server, std::to_string(port));
|
||||
boost::asio::connect(worldSocket, endpoints);
|
||||
|
||||
float timeOfDay;
|
||||
std::memcpy(&timeOfDay, &data[1], sizeof(timeOfDay));
|
||||
// Send authentication
|
||||
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) {
|
||||
// Message format: [type(1)][id(4)][x(4)][y(4)][z(4)][colorLen(1)][color(colorLen)]
|
||||
if (size < 18) return;
|
||||
// Wait for auth response
|
||||
boost::asio::streambuf response;
|
||||
boost::asio::read_until(worldSocket, response, '\n');
|
||||
|
||||
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));
|
||||
std::istream response_stream(&response);
|
||||
std::string response_str;
|
||||
std::getline(response_stream, response_str);
|
||||
|
||||
uint8_t colorLen = data[17];
|
||||
if (size >= 18u + colorLen) {
|
||||
playerColor = std::string(reinterpret_cast<const char*>(&data[18]), colorLen);
|
||||
}
|
||||
|
||||
playerID = id;
|
||||
{
|
||||
std::lock_guard lock(positionMutex);
|
||||
serverPosition = {x, y, z};
|
||||
}
|
||||
connected = true;
|
||||
std::cout << "Connected as player " << id << " with color " << playerColor << "\n";
|
||||
}
|
||||
|
||||
void NetworkManager::handleUpdate(const uint8_t* data, std::size_t size) {
|
||||
if (size < 17) 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));
|
||||
|
||||
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) {
|
||||
if (size < 5) return;
|
||||
|
||||
uint32_t id;
|
||||
std::memcpy(&id, &data[1], sizeof(id));
|
||||
|
||||
std::lock_guard lock(remotePlayersMutex);
|
||||
remotePlayers.erase(id);
|
||||
std::cout << "Player " << id << " left\n";
|
||||
}
|
||||
|
||||
void NetworkManager::handlePlayerList(const uint8_t* data, std::size_t size) {
|
||||
if (size < 2) return;
|
||||
|
||||
uint8_t count = data[1];
|
||||
size_t offset = 2;
|
||||
|
||||
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);
|
||||
json authResp = json::parse(response_str);
|
||||
if (authResp["success"] == true) {
|
||||
worldConnected = true;
|
||||
connected = true;
|
||||
startWorldReceive();
|
||||
return true;
|
||||
}
|
||||
|
||||
offset += 17 + colorLen;
|
||||
|
||||
if (id != playerID) {
|
||||
remotePlayers[id] = {id, {x, y, z}, color, static_cast<float>(GetTime())};
|
||||
}
|
||||
return false;
|
||||
} catch (std::exception& e) {
|
||||
std::cerr << "Failed to connect to world server: " << e.what() << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cout << "Received list of " << (int)count << " players\n";
|
||||
}
|
||||
|
||||
void NetworkManager::sendLogin() {
|
||||
std::array<uint8_t, 1> msg{static_cast<uint8_t>(MessageType::Login)};
|
||||
socket.send_to(buffer(msg), serverEndpoint);
|
||||
void NetworkManager::startLoginReceive() {
|
||||
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);
|
||||
|
||||
try {
|
||||
json msg = json::parse(line);
|
||||
processLoginMessage(msg);
|
||||
} catch (json::exception& e) {
|
||||
std::cerr << "JSON parse error: " << e.what() << std::endl;
|
||||
}
|
||||
|
||||
if (loginConnected) {
|
||||
startLoginReceive();
|
||||
}
|
||||
void NetworkManager::connectToLoginServer(const std::string& host, uint16_t port) {
|
||||
loginEndpoint = udp::endpoint(ip::make_address(host), port);
|
||||
std::cout << "Login server endpoint set to " << host << ":" << port << "\n";
|
||||
}
|
||||
|
||||
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);
|
||||
void NetworkManager::connectToWorldServer(const std::string& host, uint16_t port) {
|
||||
worldEndpoint = udp::endpoint(ip::make_address(host), port);
|
||||
std::cout << "World server endpoint set to " << host << ":" << port << "\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);
|
||||
msg[1] = static_cast<uint8_t>(username.size());
|
||||
std::memcpy(&msg[2], username.data(), username.size());
|
||||
socket.send_to(buffer(msg), serverEndpoint);
|
||||
std::cout << "Login packet sent for username: " << username << "\n";
|
||||
msg[2 + username.size()] = static_cast<uint8_t>(password.size());
|
||||
std::memcpy(&msg[3 + username.size()], password.data(), password.size());
|
||||
|
||||
loginSocket.send_to(buffer(msg), loginEndpoint);
|
||||
loginResponseReceived = false;
|
||||
}
|
||||
|
||||
void NetworkManager::sendLogout() {
|
||||
if (!connected) {
|
||||
std::cout << "Warning: sendLogout called but not connected\n";
|
||||
return;
|
||||
bool NetworkManager::waitForLoginResponse(LoginResult& result, float timeout) {
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
auto timeoutDuration = std::chrono::milliseconds(static_cast<int>(timeout * 1000));
|
||||
|
||||
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;
|
||||
return true;
|
||||
}
|
||||
|
||||
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));
|
||||
socket.send_to(buffer(msg), serverEndpoint);
|
||||
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);
|
||||
|
||||
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();
|
||||
}
|
||||
worldSocket.send_to(buffer(msg), worldEndpoint);
|
||||
}
|
||||
|
||||
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[13], &dz, sizeof(dz));
|
||||
|
||||
socket.send_to(buffer(msg), serverEndpoint);
|
||||
}
|
||||
|
||||
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);
|
||||
worldSocket.send_to(buffer(msg), worldEndpoint);
|
||||
}
|
||||
|
||||
void NetworkManager::sendHeartbeat() {
|
||||
@ -270,7 +154,246 @@ void NetworkManager::sendHeartbeat() {
|
||||
msg[0] = static_cast<uint8_t>(MessageType::Heartbeat);
|
||||
uint32_t id = playerID;
|
||||
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() {
|
||||
@ -278,53 +401,7 @@ Vector3 NetworkManager::getPosition() {
|
||||
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::lock_guard lock(remotePlayersMutex);
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
@ -7,87 +7,125 @@
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
#include <unordered_map>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
|
||||
using namespace boost::asio;
|
||||
using ip::udp;
|
||||
|
||||
enum class MessageType : uint8_t {
|
||||
Login = 0x01,
|
||||
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
|
||||
};
|
||||
using ip::tcp;
|
||||
// messages
|
||||
LoginRequest 0x20RegisterRequest0x21,
|
||||
LoginResponse = 0x22,
|
||||
|
||||
// World messages
|
||||
Auth = 0x30,
|
||||
AuthResponse = 0x31,
|
||||
Logout = 0x0C
|
||||
struct RemotePlayer {
|
||||
uint32_t id;
|
||||
Vector3 position;
|
||||
std::string color;
|
||||
std::string usernameusername;
|
||||
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:
|
||||
NetworkManager();
|
||||
~NetworkManager();
|
||||
|
||||
void sendLogin();
|
||||
void sendLoginWithUsername(const std::string& username);
|
||||
void sendLogout();
|
||||
void sendMove(float dx, float dy, float dz);
|
||||
void sendColorChange(const std::string& newColor);
|
||||
void sendHeartbeat();
|
||||
// Login server connection
|
||||
bool connectToLogin(const std::string& server, uint16_t port);
|
||||
void sendLogin(const std::string& username, const std::string& password);
|
||||
|
||||
Vector3 getPosition();
|
||||
bool isConnected() const { return connected; }
|
||||
bool hasLoginError() const { return !loginErrorMsg.empty(); }
|
||||
std::string getLoginError() const { return loginErrorMsg; }
|
||||
// World server connection
|
||||
bool connectToWorld(const std::string& server, uint16_t port, const std::string& token);
|
||||
void sendMovement(const Vector3& position, float yaw, float pitch, const Vector3& velocity);
|
||||
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; }
|
||||
std::string getPlayerColor() const { return playerColor; }
|
||||
|
||||
std::unordered_map<uint32_t, RemotePlayer> getRemotePlayers();
|
||||
float getServerTimeOfDay() const { return serverTimeOfDay.load(); }
|
||||
|
||||
// Available colors for cycling
|
||||
static const std::vector<std::string> AVAILABLE_COLORS;
|
||||
|
||||
private:
|
||||
io_context ioContext;
|
||||
udp::socket socket{ioContext};
|
||||
udp::endpoint serverEndpoint;
|
||||
tcp::socket loginSocket{ioContext};
|
||||
udp:: worldSocket;
|
||||
tcp::socket worldSocketloginEndpoint;
|
||||
udp::endpoint worldEndpoint;
|
||||
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::string playerColor{"red"};
|
||||
std::string currentUsername{""};
|
||||
std::string loginErrorMsg{""};
|
||||
std::mutex positionMutex;
|
||||
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::unordered_map<uint32_t, RemotePlayer> remotePlayers;
|
||||
std::atomic<float> serverTimeOfDay{0.0f};
|
||||
|
||||
void startReceive();
|
||||
void processMessage(const uint8_t* data, std::size_t size);
|
||||
void handleSpawn(const uint8_t* data, std::size_t size);
|
||||
void handleUpdate(const uint8_t* data, std::size_t size);
|
||||
void handlePlayerJoined(const uint8_t* data, std::size_t size);
|
||||
void handlePlayerLeft(const uint8_t* data, std::size_t size);
|
||||
void handlePlayerList(const uint8_t* data, std::size_t size);
|
||||
void handleColorChanged(const uint8_t* data, std::size_t size);
|
||||
void handleLoginResponse(const uint8_t* data, std::size_t size);
|
||||
void handleTimeSync(const uint8_t* data, std::size_t size);
|
||||
// World state
|
||||
std::atomic<float> serverTimeOfDay{12.0f};
|
||||
|
||||
// Message processing
|
||||
void startLoginReceive();
|
||||
void startWorldReceive();
|
||||
void processLoginMessage(const nlohmann::json& msg);
|
||||
void processWorldMessage(const nlohmann::json& msg);
|
||||
};
|
||||
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)};
|
||||
@ -3,56 +3,41 @@ package main
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"server/internal/db"
|
||||
"server/internal/net/packets"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type LoginServer struct {
|
||||
listener net.Listener
|
||||
database *db.Database
|
||||
worldURL string
|
||||
serverID string
|
||||
conn *net.UDPConn
|
||||
database *db.Database
|
||||
worldHost string
|
||||
worldPort uint16
|
||||
serverID string
|
||||
clients map[string]*LoginClient // Map of addr string to client
|
||||
clientsLock sync.RWMutex
|
||||
}
|
||||
|
||||
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"`
|
||||
type LoginClient struct {
|
||||
addr *net.UDPAddr
|
||||
lastSeen time.Time
|
||||
username string
|
||||
authed bool
|
||||
}
|
||||
|
||||
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")
|
||||
port = flag.String("port", "9998", "Login server port")
|
||||
dbDSN = flag.String("db", "user:password@tcp(localhost:3306)/game", "Database DSN")
|
||||
worldHost = flag.String("worldhost", "localhost", "World server host")
|
||||
worldPort = flag.Uint("worldport", 9999, "World server port")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
@ -63,153 +48,155 @@ func main() {
|
||||
defer database.Close()
|
||||
|
||||
server := &LoginServer{
|
||||
database: database,
|
||||
worldURL: *worldURL,
|
||||
serverID: generateServerID(),
|
||||
database: database,
|
||||
worldHost: *worldHost,
|
||||
worldPort: uint16(*worldPort),
|
||||
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 {
|
||||
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.cleanupClients()
|
||||
|
||||
// Main packet processing loop
|
||||
buffer := make([]byte, 1024)
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
n, clientAddr, err := conn.ReadFromUDP(buffer)
|
||||
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 {
|
||||
log.Printf("Read error: %v", err)
|
||||
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)
|
||||
}
|
||||
go server.handlePacket(buffer[:n], clientAddr)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LoginServer) handleLogin(req LoginRequest, encoder *json.Encoder) {
|
||||
player, err := s.database.GetPlayerByUsername(req.Username)
|
||||
func (s *LoginServer) handlePacket(data []byte, addr *net.UDPAddr) {
|
||||
if len(data) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
msgType := data[0]
|
||||
|
||||
switch msgType {
|
||||
case packets.MSG_LOGIN_REQUEST:
|
||||
s.handleLogin(data, addr)
|
||||
case packets.MSG_REGISTER_REQUEST:
|
||||
s.handleRegister(data, addr)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LoginServer) handleLogin(data []byte, addr *net.UDPAddr) {
|
||||
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 {
|
||||
encoder.Encode(LoginResponse{
|
||||
Type: "loginResponse",
|
||||
Success: false,
|
||||
Message: "Invalid username or password",
|
||||
})
|
||||
// Try to register if user doesn't exist
|
||||
s.handleRegister(data, addr)
|
||||
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",
|
||||
})
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(player.PasswordHash), []byte(password)); err != nil {
|
||||
s.sendLoginResponse(addr, false, nil, 0, "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",
|
||||
})
|
||||
// Generate session token
|
||||
token := make([]byte, 32)
|
||||
rand.Read(token)
|
||||
tokenHex := hex.EncodeToString(token)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
s.database.UpdateLastLogin(player.ID)
|
||||
|
||||
encoder.Encode(LoginResponse{
|
||||
Type: "loginResponse",
|
||||
Success: true,
|
||||
Token: token,
|
||||
WorldURL: s.worldURL,
|
||||
PlayerID: player.ID,
|
||||
})
|
||||
// Mark client as authenticated
|
||||
s.clientsLock.Lock()
|
||||
if client, exists := s.clients[addr.String()]; exists {
|
||||
client.authed = true
|
||||
}
|
||||
s.clientsLock.Unlock()
|
||||
|
||||
s.sendLoginResponse(addr, true, token, uint32(player.ID), "Login successful")
|
||||
}
|
||||
|
||||
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",
|
||||
})
|
||||
func (s *LoginServer) handleRegister(data []byte, addr *net.UDPAddr) {
|
||||
username, password, ok := packets.DecodeRegisterRequest(data)
|
||||
if !ok {
|
||||
s.sendLoginResponse(addr, false, nil, 0, "Invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Password) < 6 {
|
||||
encoder.Encode(RegisterResponse{
|
||||
Type: "registerResponse",
|
||||
Success: false,
|
||||
Message: "Password must be at least 6 characters",
|
||||
})
|
||||
if len(username) < 3 || len(username) > 20 {
|
||||
s.sendLoginResponse(addr, false, nil, 0, "Username must be 3-20 characters")
|
||||
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 {
|
||||
encoder.Encode(RegisterResponse{
|
||||
Type: "registerResponse",
|
||||
Success: false,
|
||||
Message: "Failed to process password",
|
||||
})
|
||||
s.sendLoginResponse(addr, false, nil, 0, "Failed to process password")
|
||||
return
|
||||
}
|
||||
|
||||
_, err = s.database.CreatePlayer(req.Username, string(hashedPassword))
|
||||
playerID, err := s.database.CreatePlayer(username, string(hashedPassword))
|
||||
if err != nil {
|
||||
encoder.Encode(RegisterResponse{
|
||||
Type: "registerResponse",
|
||||
Success: false,
|
||||
Message: "Username already exists",
|
||||
})
|
||||
// Player might already exist, try login instead
|
||||
s.handleLogin(data, addr)
|
||||
return
|
||||
}
|
||||
|
||||
encoder.Encode(RegisterResponse{
|
||||
Type: "registerResponse",
|
||||
Success: true,
|
||||
Message: "Registration successful",
|
||||
})
|
||||
// Auto-login after registration
|
||||
token := make([]byte, 32)
|
||||
rand.Read(token)
|
||||
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() {
|
||||
@ -223,10 +210,20 @@ func (s *LoginServer) cleanupSessions() {
|
||||
}
|
||||
}
|
||||
|
||||
func generateToken() string {
|
||||
b := make([]byte, 32)
|
||||
rand.Read(b)
|
||||
return hex.EncodeToString(b)
|
||||
func (s *LoginServer) cleanupClients() {
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
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 {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
@ -14,44 +14,31 @@ import (
|
||||
)
|
||||
|
||||
type WorldServer struct {
|
||||
listener net.Listener
|
||||
conn *net.UDPConn
|
||||
database *db.Database
|
||||
serverID string
|
||||
clients map[int]*Client
|
||||
clients map[uint32]*Client
|
||||
clientLock sync.RWMutex
|
||||
world *packets.WorldConfig
|
||||
heightmap [][]float32
|
||||
ticker *time.Ticker
|
||||
timeOfDay float32
|
||||
nextID uint32
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
ID int
|
||||
ID uint32
|
||||
PlayerID int64
|
||||
Username string
|
||||
Conn net.Conn
|
||||
Encoder *json.Encoder
|
||||
Decoder *json.Decoder
|
||||
Addr *net.UDPAddr
|
||||
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"`
|
||||
Authed bool
|
||||
}
|
||||
|
||||
func main() {
|
||||
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")
|
||||
)
|
||||
flag.Parse()
|
||||
@ -68,269 +55,241 @@ func main() {
|
||||
server := &WorldServer{
|
||||
database: database,
|
||||
serverID: generateServerID(),
|
||||
clients: make(map[int]*Client),
|
||||
clients: make(map[uint32]*Client),
|
||||
world: worldConfig,
|
||||
heightmap: heightmap,
|
||||
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 {
|
||||
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)
|
||||
|
||||
go server.gameLoop()
|
||||
go server.updateTimeOfDay()
|
||||
|
||||
// Main packet processing loop
|
||||
buffer := make([]byte, 1024)
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
n, clientAddr, err := conn.ReadFromUDP(buffer)
|
||||
if err != nil {
|
||||
log.Printf("Accept error: %v", err)
|
||||
log.Printf("Read error: %v", err)
|
||||
continue
|
||||
}
|
||||
go server.handleConnection(conn)
|
||||
|
||||
go server.handlePacket(buffer[:n], clientAddr)
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
})
|
||||
func (s *WorldServer) handlePacket(data []byte, addr *net.UDPAddr) {
|
||||
if len(data) == 0 {
|
||||
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 {
|
||||
encoder.Encode(AuthResponse{
|
||||
Type: "authResponse",
|
||||
Success: false,
|
||||
Message: "Invalid or expired session",
|
||||
})
|
||||
response := packets.EncodeAuthResponse(false, "Invalid or expired session")
|
||||
s.conn.WriteToUDP(response, addr)
|
||||
return
|
||||
}
|
||||
|
||||
player, err := s.database.GetPlayerByID(session.PlayerID)
|
||||
if err != nil {
|
||||
encoder.Encode(AuthResponse{
|
||||
Type: "authResponse",
|
||||
Success: false,
|
||||
Message: "Player not found",
|
||||
})
|
||||
response := packets.EncodeAuthResponse(false, "Player not found")
|
||||
s.conn.WriteToUDP(response, addr)
|
||||
return
|
||||
}
|
||||
|
||||
encoder.Encode(AuthResponse{
|
||||
Type: "authResponse",
|
||||
Success: true,
|
||||
})
|
||||
// Send auth success
|
||||
response := packets.EncodeAuthResponse(true, "Authenticated")
|
||||
s.conn.WriteToUDP(response, addr)
|
||||
|
||||
// Get saved position or default
|
||||
position, _ := s.database.GetPlayerPosition(player.ID)
|
||||
|
||||
// Create client
|
||||
s.clientLock.Lock()
|
||||
clientID := s.nextID
|
||||
s.nextID++
|
||||
|
||||
client := &Client{
|
||||
ID: s.getNextClientID(),
|
||||
ID: clientID,
|
||||
PlayerID: player.ID,
|
||||
Username: player.Username,
|
||||
Conn: conn,
|
||||
Encoder: encoder,
|
||||
Decoder: decoder,
|
||||
Addr: addr,
|
||||
Position: packets.Vec3{X: position.X, Y: position.Y, Z: position.Z},
|
||||
Rotation: packets.Vec2{X: position.Yaw, Y: position.Pitch},
|
||||
LastUpdate: time.Now(),
|
||||
Authed: true,
|
||||
}
|
||||
s.clients[clientID] = client
|
||||
s.clientLock.Unlock()
|
||||
|
||||
s.addClient(client)
|
||||
defer s.removeClient(client)
|
||||
// Send spawn packet
|
||||
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)
|
||||
|
||||
for {
|
||||
var msg json.RawMessage
|
||||
if err := decoder.Decode(&msg); err != nil {
|
||||
break
|
||||
}
|
||||
log.Printf("Player %s (ID: %d) joined the world", player.Username, clientID)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
func (s *WorldServer) handleMove(data []byte, addr *net.UDPAddr) {
|
||||
playerID, delta, ok := packets.DecodeMovePacket(data)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
s.clientLock.Lock()
|
||||
client, exists := s.clients[playerID]
|
||||
if !exists || !client.Authed {
|
||||
s.clientLock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// 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{
|
||||
X: client.Position.X,
|
||||
Y: client.Position.Y,
|
||||
Z: client.Position.Z,
|
||||
Yaw: client.Rotation.X,
|
||||
Pitch: client.Rotation.Y,
|
||||
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) {
|
||||
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{}) {
|
||||
func (s *WorldServer) sendPlayerList(newClient *Client) {
|
||||
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)
|
||||
if client.ID != newClient.ID {
|
||||
msg := packets.EncodePlayerJoinedPacket(client.ID, client.Position, client.Username)
|
||||
s.conn.WriteToUDP(msg, newClient.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) broadcastPlayerJoined(newClient *Client) {
|
||||
msg := packets.EncodePlayerJoinedPacket(newClient.ID, newClient.Position, newClient.Username)
|
||||
s.broadcast(msg, newClient.ID)
|
||||
}
|
||||
|
||||
func (s *WorldServer) removeClient(client *Client) {
|
||||
s.clientLock.Lock()
|
||||
delete(s.clients, client.ID)
|
||||
s.clientLock.Unlock()
|
||||
func (s *WorldServer) broadcastMovement(movedClient *Client) {
|
||||
msg := packets.EncodeUpdatePacket(movedClient.ID, movedClient.Position)
|
||||
s.broadcast(msg, movedClient.ID)
|
||||
}
|
||||
|
||||
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) broadcast(msg []byte, excludeID uint32) {
|
||||
s.clientLock.RLock()
|
||||
defer s.clientLock.RUnlock()
|
||||
|
||||
for _, client := range s.clients {
|
||||
if client.ID != excludeID && client.Authed {
|
||||
s.conn.WriteToUDP(msg, client.Addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WorldServer) gameLoop() {
|
||||
@ -338,15 +297,27 @@ func (s *WorldServer) gameLoop() {
|
||||
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)
|
||||
now := time.Now()
|
||||
s.clientLock.Lock()
|
||||
for id, client := range s.clients {
|
||||
if now.Sub(client.LastUpdate) > 30*time.Second {
|
||||
// Save position before timeout
|
||||
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.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
|
||||
// Broadcast time update
|
||||
msg := packets.EncodeTimeSyncPacket(s.timeOfDay)
|
||||
s.clientLock.RLock()
|
||||
for _, client := range s.clients {
|
||||
if client.Authed {
|
||||
s.conn.WriteToUDP(msg, client.Addr)
|
||||
}
|
||||
}
|
||||
id++
|
||||
s.clientLock.RUnlock()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
220
server/internal/net/packets/binary.go
Normal file
220
server/internal/net/packets/binary.go
Normal 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
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user