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
346
client/main.cpp
346
client/main.cpp
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
UnloadTexture(terrainTexture);
|
if (IsWindowReady()) {
|
||||||
UnloadModel(terrainModel);
|
UnloadTexture(terrainTexture);
|
||||||
CloseWindow();
|
UnloadModel(terrainModel);
|
||||||
|
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
|
|
||||||
}
|
// Get camera orientation for yaw/pitch
|
||||||
|
Camera camera = playerController.getCamera();
|
||||||
// Send periodic heartbeats when not moving
|
Vector3 forward = Vector3Subtract(camera.target, camera.position);
|
||||||
float currentTime = GetTime();
|
playerYaw = atan2f(forward.x, forward.z);
|
||||||
if (currentTime - lastHeartbeatTime >= config.heartbeatInterval) {
|
playerPitch = atan2f(forward.y, sqrtf(forward.x * forward.x + forward.z * forward.z));
|
||||||
network.sendHeartbeat();
|
|
||||||
lastHeartbeatTime = currentTime;
|
// Send movement updates at regular intervals or when moving
|
||||||
}
|
-> float currentTime = GetTime();
|
||||||
|
if (currentTime - lastMovementUpdate >= movementUpdateInterval) {
|
||||||
// Handle color change with arrow keys
|
network->sendMovement->(playerPos, playerYaw, playerPitch, playerVelocity);
|
||||||
static int currentColorIndex = -1;
|
lastMovementUpdate = currentTime;
|
||||||
if (IsKeyPressed(KEY_LEFT) || IsKeyPressed(KEY_RIGHT)) {
|
->std::this_thread::sleep_for(std::chrono::milliseconds(100))break // Exit game loop }
|
||||||
// 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 = "";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -383,4 +295,4 @@ int main(int argc, char* argv[]) {
|
|||||||
Game game(config);
|
Game game(config);
|
||||||
game.run();
|
game.run();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -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));
|
||||||
float timeOfDay;
|
boost::asio::connect(worldSocket, endpoints);
|
||||||
std::memcpy(&timeOfDay, &data[1], sizeof(timeOfDay));
|
|
||||||
|
|
||||||
serverTimeOfDay.store(timeOfDay);
|
|
||||||
}
|
|
||||||
|
|
||||||
void NetworkManager::handleSpawn(const uint8_t* data, std::size_t size) {
|
// Send authentication
|
||||||
// Message format: [type(1)][id(4)][x(4)][y(4)][z(4)][colorLen(1)][color(colorLen)]
|
json authMsg;
|
||||||
if (size < 18) return;
|
authMsg["type"] = "auth";
|
||||||
|
authMsg["token"] = token;
|
||||||
|
|
||||||
uint32_t id;
|
std::string msg = authMsg.dump() + "\n";
|
||||||
float x, y, z;
|
boost::asio::write(worldSocket, boost::asio::buffer(msg));
|
||||||
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];
|
// Wait for auth response
|
||||||
if (size >= 18u + colorLen) {
|
boost::asio::streambuf response;
|
||||||
playerColor = std::string(reinterpret_cast<const char*>(&data[18]), colorLen);
|
boost::asio::read_until(worldSocket, response, '\n');
|
||||||
}
|
|
||||||
|
|
||||||
playerID = id;
|
std::istream response_stream(&response);
|
||||||
{
|
std::string response_str;
|
||||||
std::lock_guard lock(positionMutex);
|
std::getline(response_stream, response_str);
|
||||||
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) {
|
json authResp = json::parse(response_str);
|
||||||
if (size < 17) return;
|
if (authResp["success"] == true) {
|
||||||
|
worldConnected = true;
|
||||||
uint32_t id;
|
connected = true;
|
||||||
float x, y, z;
|
startWorldReceive();
|
||||||
std::memcpy(&id, &data[1], sizeof(id));
|
return true;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
offset += 17 + colorLen;
|
return false;
|
||||||
|
} catch (std::exception& e) {
|
||||||
if (id != playerID) {
|
std::cerr << "Failed to connect to world server: " << e.what() << std::endl;
|
||||||
remotePlayers[id] = {id, {x, y, z}, color, static_cast<float>(GetTime())};
|
return false;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cout << "Received list of " << (int)count << " players\n";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void NetworkManager::sendLogin() {
|
void NetworkManager::startLoginReceive() {
|
||||||
std::array<uint8_t, 1> msg{static_cast<uint8_t>(MessageType::Login)};
|
auto self(shared_from_this());
|
||||||
socket.send_to(buffer(msg), serverEndpoint);
|
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) {
|
void NetworkManager::connectToWorldServer(const std::string& host, uint16_t port) {
|
||||||
std::cout << "Attempting login with username: " << username << "\n";
|
worldEndpoint = udp::endpoint(ip::make_address(host), port);
|
||||||
loginErrorMsg = "";
|
std::cout << "World server endpoint set to " << host << ":" << port << "\n";
|
||||||
currentUsername = username;
|
}
|
||||||
std::vector<uint8_t> msg(2 + username.size());
|
|
||||||
msg[0] = static_cast<uint8_t>(MessageType::Login);
|
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());
|
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;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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::array<uint8_t, 5> msg{};
|
worldSocket.send_to(buffer(msg), worldEndpoint);
|
||||||
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::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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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,
|
// World messages
|
||||||
Move = 0x04,
|
Auth = 0x30,
|
||||||
Update = 0x05,
|
AuthResponse = 0x31,
|
||||||
PlayerJoined = 0x06,
|
Logout = 0x0C
|
||||||
PlayerLeft = 0x07,
|
|
||||||
PlayerList = 0x08,
|
|
||||||
ChangeColor = 0x09,
|
|
||||||
ColorChanged = 0x0A,
|
|
||||||
LoginResponse = 0x0B,
|
|
||||||
Logout = 0x0C,
|
|
||||||
Heartbeat = 0x0D,
|
|
||||||
TimeSync = 0x0E
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
// World server connection
|
||||||
void sendHeartbeat();
|
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);
|
||||||
Vector3 getPosition();
|
void sendChat(const std::string& message);
|
||||||
bool isConnected() const { return connected; }
|
|
||||||
bool hasLoginError() const { return !loginErrorMsg.empty(); }
|
// Disconnect and cleanup
|
||||||
std::string getLoginError() const { return loginErrorMsg; }
|
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};
|
|
||||||
|
// World state
|
||||||
void startReceive();
|
std::atomic<float> serverTimeOfDay{12.0f};
|
||||||
void processMessage(const uint8_t* data, std::size_t size);
|
|
||||||
void handleSpawn(const uint8_t* data, std::size_t size);
|
// Message processing
|
||||||
void handleUpdate(const uint8_t* data, std::size_t size);
|
void startLoginReceive();
|
||||||
void handlePlayerJoined(const uint8_t* data, std::size_t size);
|
void startWorldReceive();
|
||||||
void handlePlayerLeft(const uint8_t* data, std::size_t size);
|
void processLoginMessage(const nlohmann::json& msg);
|
||||||
void handlePlayerList(const uint8_t* data, std::size_t size);
|
void processWorldMessage(const nlohmann::json& msg);
|
||||||
void handleColorChanged(const uint8_t* data, std::size_t size);
|
};
|
||||||
void handleLoginResponse(const uint8_t* data, std::size_t size);
|
std::atomic<float> serverTimeOfDay{12.0f};
|
||||||
void handleTimeSync(const uint8_t* data, std::size_t size);
|
|
||||||
};
|
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 (
|
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
|
||||||
serverID string
|
worldPort uint16
|
||||||
|
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()
|
||||||
|
|
||||||
@ -63,153 +48,155 @@ func main() {
|
|||||||
defer database.Close()
|
defer database.Close()
|
||||||
|
|
||||||
server := &LoginServer{
|
server := &LoginServer{
|
||||||
database: database,
|
database: database,
|
||||||
worldURL: *worldURL,
|
worldHost: *worldHost,
|
||||||
serverID: generateServerID(),
|
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 {
|
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
|
|
||||||
}
|
|
||||||
go server.handleConnection(conn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *LoginServer) handleConnection(conn net.Conn) {
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
decoder := json.NewDecoder(conn)
|
|
||||||
encoder := json.NewEncoder(conn)
|
|
||||||
|
|
||||||
for {
|
|
||||||
var msg json.RawMessage
|
|
||||||
if err := decoder.Decode(&msg); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var baseMsg struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(msg, &baseMsg); err != nil {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
switch baseMsg.Type {
|
go server.handlePacket(buffer[:n], clientAddr)
|
||||||
case "login":
|
|
||||||
var req LoginRequest
|
|
||||||
if err := json.Unmarshal(msg, &req); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
s.handleLogin(req, encoder)
|
|
||||||
|
|
||||||
case "register":
|
|
||||||
var req RegisterRequest
|
|
||||||
if err := json.Unmarshal(msg, &req); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
s.handleRegister(req, encoder)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *LoginServer) handleLogin(req LoginRequest, encoder *json.Encoder) {
|
func (s *LoginServer) handlePacket(data []byte, addr *net.UDPAddr) {
|
||||||
player, err := s.database.GetPlayerByUsername(req.Username)
|
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 {
|
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 {
|
||||||
|
|||||||
@ -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,269 +55,241 @@ 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 {
|
|
||||||
case "movement":
|
|
||||||
var moveMsg packets.MovementMessage
|
|
||||||
if err := json.Unmarshal(msg, &moveMsg); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
s.handleMovement(client, moveMsg)
|
|
||||||
|
|
||||||
case "chat":
|
|
||||||
var chatMsg packets.ChatMessage
|
|
||||||
if err := json.Unmarshal(msg, &chatMsg); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
s.handleChat(client, chatMsg)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.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{
|
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) {
|
|
||||||
s.clientLock.RLock()
|
|
||||||
defer s.clientLock.RUnlock()
|
|
||||||
|
|
||||||
for _, client := range s.clients {
|
|
||||||
if client.ID != excludeID {
|
|
||||||
client.Encoder.Encode(msg)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *WorldServer) addClient(client *Client) {
|
func (s *WorldServer) broadcastPlayerJoined(newClient *Client) {
|
||||||
s.clientLock.Lock()
|
msg := packets.EncodePlayerJoinedPacket(newClient.ID, newClient.Position, newClient.Username)
|
||||||
s.clients[client.ID] = client
|
s.broadcast(msg, newClient.ID)
|
||||||
s.clientLock.Unlock()
|
|
||||||
log.Printf("Player %s (ID: %d) joined the world", client.Username, client.ID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *WorldServer) removeClient(client *Client) {
|
func (s *WorldServer) broadcastMovement(movedClient *Client) {
|
||||||
s.clientLock.Lock()
|
msg := packets.EncodeUpdatePacket(movedClient.ID, movedClient.Position)
|
||||||
delete(s.clients, client.ID)
|
s.broadcast(msg, movedClient.ID)
|
||||||
s.clientLock.Unlock()
|
}
|
||||||
|
|
||||||
s.broadcast(packets.PlayerLeftMessage{
|
func (s *WorldServer) broadcast(msg []byte, excludeID uint32) {
|
||||||
Type: "playerLeft",
|
s.clientLock.RLock()
|
||||||
PlayerID: client.ID,
|
defer s.clientLock.RUnlock()
|
||||||
})
|
|
||||||
log.Printf("Player %s (ID: %d) left the world", client.Username, client.ID)
|
for _, client := range s.clients {
|
||||||
|
if client.ID != excludeID && client.Authed {
|
||||||
|
s.conn.WriteToUDP(msg, client.Addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *WorldServer) gameLoop() {
|
func (s *WorldServer) gameLoop() {
|
||||||
@ -338,15 +297,27 @@ func (s *WorldServer) gameLoop() {
|
|||||||
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)
|
||||||
|
}
|
||||||
func (s *WorldServer) getNextClientID() int {
|
|
||||||
s.clientLock.Lock()
|
|
||||||
defer s.clientLock.Unlock()
|
|
||||||
|
|
||||||
id := 1
|
|
||||||
for {
|
|
||||||
if _, exists := s.clients[id]; !exists {
|
|
||||||
return id
|
|
||||||
}
|
}
|
||||||
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