Add login GUI and location/color persistence
This commit is contained in:
parent
c05a27a621
commit
8d08958280
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
*.o
|
||||||
|
|
||||||
|
server/players.json
|
||||||
|
client/game
|
||||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
@ -2,7 +2,7 @@ CXX = g++
|
|||||||
CXXFLAGS = -std=c++20 -Wall -Wextra -O2 -I.
|
CXXFLAGS = -std=c++20 -Wall -Wextra -O2 -I.
|
||||||
LDFLAGS = -lraylib -lboost_system -lpthread -lGL -lm -ldl -lrt -lX11
|
LDFLAGS = -lraylib -lboost_system -lpthread -lGL -lm -ldl -lrt -lX11
|
||||||
|
|
||||||
TARGET = game_client
|
TARGET = game
|
||||||
SOURCES = main.cpp PlayerController.cpp net/NetworkManager.cpp
|
SOURCES = main.cpp PlayerController.cpp net/NetworkManager.cpp
|
||||||
OBJECTS = $(SOURCES:.cpp=.o)
|
OBJECTS = $(SOURCES:.cpp=.o)
|
||||||
|
|
||||||
|
|||||||
5993
client/includes/raygui.h
Normal file
5993
client/includes/raygui.h
Normal file
File diff suppressed because it is too large
Load Diff
138
client/main.cpp
138
client/main.cpp
@ -1,10 +1,17 @@
|
|||||||
#include <raylib.h>
|
#include <raylib.h>
|
||||||
#include <raymath.h>
|
#include <raymath.h>
|
||||||
|
|
||||||
|
#define RAYGUI_IMPLEMENTATION
|
||||||
|
#include "includes/raygui.h"
|
||||||
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <thread>
|
||||||
|
#include <chrono>
|
||||||
#include "PlayerController.hpp"
|
#include "PlayerController.hpp"
|
||||||
#include "net/NetworkManager.hpp"
|
#include "net/NetworkManager.hpp"
|
||||||
|
|
||||||
@ -93,7 +100,11 @@ struct Heightmap {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum GameState {
|
||||||
|
STATE_LOGIN,
|
||||||
|
STATE_CONNECTING,
|
||||||
|
STATE_PLAYING
|
||||||
|
};
|
||||||
|
|
||||||
class Game {
|
class Game {
|
||||||
PlayerController playerController;
|
PlayerController playerController;
|
||||||
@ -106,6 +117,13 @@ class Game {
|
|||||||
std::unordered_map<std::string, Texture2D> playerTextures;
|
std::unordered_map<std::string, Texture2D> playerTextures;
|
||||||
std::unordered_map<uint32_t, Model> remotePlayerModels;
|
std::unordered_map<uint32_t, Model> remotePlayerModels;
|
||||||
|
|
||||||
|
// Login UI state
|
||||||
|
GameState gameState = STATE_LOGIN;
|
||||||
|
char usernameBuffer[32] = "";
|
||||||
|
bool editMode = false;
|
||||||
|
std::string loginError = "";
|
||||||
|
std::string currentUsername = "";
|
||||||
|
|
||||||
public:
|
public:
|
||||||
Game() {
|
Game() {
|
||||||
InitWindow(1280, 720, "Multiplayer Terrain Game");
|
InitWindow(1280, 720, "Multiplayer Terrain Game");
|
||||||
@ -134,12 +152,14 @@ public:
|
|||||||
// Create player cube (texture will be set when we know our color)
|
// Create player cube (texture will be set when we know our color)
|
||||||
auto cubeMesh = GenMeshCube(1.0f, 2.0f, 1.0f);
|
auto cubeMesh = GenMeshCube(1.0f, 2.0f, 1.0f);
|
||||||
playerModel = LoadModelFromMesh(cubeMesh);
|
playerModel = LoadModelFromMesh(cubeMesh);
|
||||||
|
|
||||||
// Connect to server
|
|
||||||
network.sendLogin();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
~Game() {
|
~Game() {
|
||||||
|
// Send logout if we're connected
|
||||||
|
if (gameState == STATE_PLAYING && network.isConnected()) {
|
||||||
|
network.sendLogout();
|
||||||
|
}
|
||||||
|
|
||||||
UnloadTexture(terrainTexture);
|
UnloadTexture(terrainTexture);
|
||||||
for (auto& [color, texture] : playerTextures) {
|
for (auto& [color, texture] : playerTextures) {
|
||||||
UnloadTexture(texture);
|
UnloadTexture(texture);
|
||||||
@ -157,14 +177,44 @@ public:
|
|||||||
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) {
|
||||||
|
case STATE_LOGIN:
|
||||||
|
// Login screen - no game updates
|
||||||
|
break;
|
||||||
|
|
||||||
|
case STATE_CONNECTING:
|
||||||
|
// Check if we've received a response from the server
|
||||||
|
if (network.isConnected()) {
|
||||||
|
gameState = STATE_PLAYING;
|
||||||
|
currentUsername = std::string(usernameBuffer);
|
||||||
|
} else if (network.hasLoginError()) {
|
||||||
|
gameState = STATE_LOGIN;
|
||||||
|
loginError = network.getLoginError();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case STATE_PLAYING:
|
||||||
|
updateGame();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateGame() {
|
||||||
if (!network.isConnected()) {
|
if (!network.isConnected()) {
|
||||||
if (IsKeyPressed(KEY_SPACE)) {
|
// Player disconnected, go back to login
|
||||||
network.sendLogin();
|
gameState = STATE_LOGIN;
|
||||||
}
|
loginError = "Disconnected from server";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -244,12 +294,80 @@ private:
|
|||||||
// Send color change to server
|
// Send color change to server
|
||||||
network.sendColorChange(NetworkManager::AVAILABLE_COLORS[currentColorIndex]);
|
network.sendColorChange(NetworkManager::AVAILABLE_COLORS[currentColorIndex]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle logout
|
||||||
|
if (IsKeyPressed(KEY_ESCAPE)) {
|
||||||
|
network.sendLogout();
|
||||||
|
gameState = STATE_LOGIN;
|
||||||
|
loginError = "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void render() {
|
void render() {
|
||||||
BeginDrawing();
|
BeginDrawing();
|
||||||
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() {
|
||||||
BeginMode3D(playerController.getCamera());
|
BeginMode3D(playerController.getCamera());
|
||||||
|
|
||||||
// Draw terrain
|
// Draw terrain
|
||||||
@ -271,16 +389,14 @@ private:
|
|||||||
EndMode3D();
|
EndMode3D();
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
DrawText(network.isConnected() ? "Connected" : "Press SPACE to connect", 10, 10, 20, WHITE);
|
DrawText(TextFormat("Logged in as: %s", currentUsername.c_str()), 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", 10, 60, 20, WHITE);
|
DrawText("Left/Right Arrow: Change Color | Mouse Wheel: Zoom | ESC: Logout", 10, 60, 20, WHITE);
|
||||||
if (network.isConnected()) {
|
if (network.isConnected()) {
|
||||||
std::string colorText = "Your color: " + network.getPlayerColor();
|
std::string colorText = "Your color: " + network.getPlayerColor();
|
||||||
DrawText(colorText.c_str(), 10, 85, 20, WHITE);
|
DrawText(colorText.c_str(), 10, 85, 20, WHITE);
|
||||||
}
|
}
|
||||||
DrawFPS(10, 110);
|
DrawFPS(10, 110);
|
||||||
|
|
||||||
EndDrawing();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -53,6 +53,9 @@ void NetworkManager::processMessage(const uint8_t* data, std::size_t size) {
|
|||||||
case MessageType::ColorChanged:
|
case MessageType::ColorChanged:
|
||||||
handleColorChanged(data, size);
|
handleColorChanged(data, size);
|
||||||
break;
|
break;
|
||||||
|
case MessageType::LoginResponse:
|
||||||
|
handleLoginResponse(data, size);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -179,6 +182,45 @@ void NetworkManager::sendLogin() {
|
|||||||
socket.send_to(buffer(msg), serverEndpoint);
|
socket.send_to(buffer(msg), serverEndpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NetworkManager::sendLoginWithUsername(const std::string& username) {
|
||||||
|
std::cout << "Attempting login with username: " << username << "\n";
|
||||||
|
loginErrorMsg = "";
|
||||||
|
currentUsername = username;
|
||||||
|
std::vector<uint8_t> msg(2 + username.size());
|
||||||
|
msg[0] = static_cast<uint8_t>(MessageType::Login);
|
||||||
|
msg[1] = static_cast<uint8_t>(username.size());
|
||||||
|
std::memcpy(&msg[2], username.data(), username.size());
|
||||||
|
socket.send_to(buffer(msg), serverEndpoint);
|
||||||
|
std::cout << "Login packet sent for username: " << username << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkManager::sendLogout() {
|
||||||
|
if (!connected) {
|
||||||
|
std::cout << "Warning: sendLogout called but not connected\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "Sending logout for player ID " << playerID << " (username: " << currentUsername << ")\n";
|
||||||
|
|
||||||
|
std::array<uint8_t, 5> msg{};
|
||||||
|
msg[0] = static_cast<uint8_t>(MessageType::Logout);
|
||||||
|
uint32_t id = playerID;
|
||||||
|
std::memcpy(&msg[1], &id, sizeof(id));
|
||||||
|
socket.send_to(buffer(msg), serverEndpoint);
|
||||||
|
|
||||||
|
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) {
|
||||||
if (!connected) return;
|
if (!connected) return;
|
||||||
|
|
||||||
@ -241,3 +283,25 @@ 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 >= 3 + 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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -22,7 +22,9 @@ enum class MessageType : uint8_t {
|
|||||||
PlayerLeft = 0x07,
|
PlayerLeft = 0x07,
|
||||||
PlayerList = 0x08,
|
PlayerList = 0x08,
|
||||||
ChangeColor = 0x09,
|
ChangeColor = 0x09,
|
||||||
ColorChanged = 0x0A
|
ColorChanged = 0x0A,
|
||||||
|
LoginResponse = 0x0B,
|
||||||
|
Logout = 0x0C
|
||||||
};
|
};
|
||||||
|
|
||||||
struct RemotePlayer {
|
struct RemotePlayer {
|
||||||
@ -38,11 +40,15 @@ public:
|
|||||||
~NetworkManager();
|
~NetworkManager();
|
||||||
|
|
||||||
void sendLogin();
|
void sendLogin();
|
||||||
|
void sendLoginWithUsername(const std::string& username);
|
||||||
|
void sendLogout();
|
||||||
void sendMove(float dx, float dy, float dz);
|
void sendMove(float dx, float dy, float dz);
|
||||||
void sendColorChange(const std::string& newColor);
|
void sendColorChange(const std::string& newColor);
|
||||||
|
|
||||||
Vector3 getPosition();
|
Vector3 getPosition();
|
||||||
bool isConnected() const { return connected; }
|
bool isConnected() const { return connected; }
|
||||||
|
bool hasLoginError() const { return !loginErrorMsg.empty(); }
|
||||||
|
std::string getLoginError() const { return loginErrorMsg; }
|
||||||
uint32_t getPlayerID() const { return playerID; }
|
uint32_t getPlayerID() const { return playerID; }
|
||||||
std::string getPlayerColor() const { return playerColor; }
|
std::string getPlayerColor() const { return playerColor; }
|
||||||
|
|
||||||
@ -60,6 +66,8 @@ private:
|
|||||||
|
|
||||||
std::atomic<uint32_t> playerID{0};
|
std::atomic<uint32_t> playerID{0};
|
||||||
std::string playerColor{"red"};
|
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};
|
std::atomic<bool> connected{false};
|
||||||
@ -75,4 +83,5 @@ private:
|
|||||||
void handlePlayerLeft(const uint8_t* data, std::size_t size);
|
void handlePlayerLeft(const uint8_t* data, std::size_t size);
|
||||||
void handlePlayerList(const uint8_t* data, std::size_t size);
|
void handlePlayerList(const uint8_t* data, std::size_t size);
|
||||||
void handleColorChanged(const uint8_t* data, std::size_t size);
|
void handleColorChanged(const uint8_t* data, std::size_t size);
|
||||||
|
void handleLoginResponse(const uint8_t* data, std::size_t size);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -7,16 +7,18 @@ import (
|
|||||||
|
|
||||||
// Message type constants
|
// Message type constants
|
||||||
const (
|
const (
|
||||||
MSG_LOGIN = 0x01
|
MSG_LOGIN = 0x01
|
||||||
MSG_POSITION = 0x02
|
MSG_POSITION = 0x02
|
||||||
MSG_SPAWN = 0x03
|
MSG_SPAWN = 0x03
|
||||||
MSG_MOVE = 0x04
|
MSG_MOVE = 0x04
|
||||||
MSG_UPDATE = 0x05
|
MSG_UPDATE = 0x05
|
||||||
MSG_PLAYER_JOINED = 0x06
|
MSG_PLAYER_JOINED = 0x06
|
||||||
MSG_PLAYER_LEFT = 0x07
|
MSG_PLAYER_LEFT = 0x07
|
||||||
MSG_PLAYER_LIST = 0x08
|
MSG_PLAYER_LIST = 0x08
|
||||||
MSG_CHANGE_COLOR = 0x09
|
MSG_CHANGE_COLOR = 0x09
|
||||||
MSG_COLOR_CHANGED = 0x0A
|
MSG_COLOR_CHANGED = 0x0A
|
||||||
|
MSG_LOGIN_RESPONSE = 0x0B
|
||||||
|
MSG_LOGOUT = 0x0C
|
||||||
)
|
)
|
||||||
|
|
||||||
// Vec3 represents a 3D vector
|
// Vec3 represents a 3D vector
|
||||||
@ -142,3 +144,43 @@ func DecodeColorChangePacket(data []byte) (playerID uint32, color string, ok boo
|
|||||||
color = string(data[6 : 6+colorLen])
|
color = string(data[6 : 6+colorLen])
|
||||||
return playerID, color, true
|
return playerID, color, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EncodeLoginResponsePacket creates a login response packet
|
||||||
|
func EncodeLoginResponsePacket(success bool, message string) []byte {
|
||||||
|
msgBytes := []byte(message)
|
||||||
|
msg := make([]byte, 3+len(msgBytes))
|
||||||
|
msg[0] = MSG_LOGIN_RESPONSE
|
||||||
|
if success {
|
||||||
|
msg[1] = 1
|
||||||
|
} else {
|
||||||
|
msg[1] = 0
|
||||||
|
}
|
||||||
|
msg[2] = uint8(len(msgBytes))
|
||||||
|
copy(msg[3:], msgBytes)
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeLoginPacket decodes a login packet with username
|
||||||
|
func DecodeLoginPacket(data []byte) (username string, ok bool) {
|
||||||
|
if len(data) < 2 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
usernameLen := data[1]
|
||||||
|
if len(data) < 2+int(usernameLen) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
username = string(data[2 : 2+usernameLen])
|
||||||
|
return username, 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
|
||||||
|
}
|
||||||
@ -16,6 +16,7 @@ import (
|
|||||||
// Player represents a connected player
|
// Player represents a connected player
|
||||||
type Player struct {
|
type Player struct {
|
||||||
ID uint32
|
ID uint32
|
||||||
|
Username string
|
||||||
Position Vec3
|
Position Vec3
|
||||||
Velocity Vec3
|
Velocity Vec3
|
||||||
Color string
|
Color string
|
||||||
@ -23,13 +24,22 @@ type Player struct {
|
|||||||
LastSeen time.Time
|
LastSeen time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserData represents persistent user data
|
||||||
|
type UserData struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Color string `json:"color"`
|
||||||
|
Position Vec3 `json:"position"`
|
||||||
|
}
|
||||||
|
|
||||||
// Server manages the game state and networking
|
// Server manages the game state and networking
|
||||||
type Server struct {
|
type Server struct {
|
||||||
conn *net.UDPConn
|
conn *net.UDPConn
|
||||||
players map[uint32]*Player
|
players map[uint32]*Player
|
||||||
heightmap [][]float32
|
usersByName map[string]*Player // Track by username for preventing duplicates
|
||||||
mutex sync.RWMutex
|
userData map[string]*UserData // Persistent user data
|
||||||
nextID uint32
|
heightmap [][]float32
|
||||||
|
mutex sync.RWMutex
|
||||||
|
nextID uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer creates a new game server
|
// NewServer creates a new game server
|
||||||
@ -45,13 +55,15 @@ func NewServer(port string, heightmap [][]float32) (*Server, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
server := &Server{
|
server := &Server{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
players: make(map[uint32]*Player),
|
players: make(map[uint32]*Player),
|
||||||
heightmap: heightmap,
|
usersByName: make(map[string]*Player),
|
||||||
nextID: 0,
|
userData: make(map[string]*UserData),
|
||||||
|
heightmap: heightmap,
|
||||||
|
nextID: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
server.loadPlayerPositions()
|
server.loadUserData()
|
||||||
|
|
||||||
return server, nil
|
return server, nil
|
||||||
}
|
}
|
||||||
@ -65,7 +77,7 @@ func (s *Server) Run() error {
|
|||||||
ticker := time.NewTicker(10 * time.Second)
|
ticker := time.NewTicker(10 * time.Second)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
s.savePlayerPositions()
|
s.saveUserData()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -96,34 +108,69 @@ func (s *Server) Run() error {
|
|||||||
|
|
||||||
switch msgType {
|
switch msgType {
|
||||||
case MSG_LOGIN:
|
case MSG_LOGIN:
|
||||||
s.handleLogin(addr)
|
s.handleLogin(buffer[:n], addr)
|
||||||
case MSG_MOVE:
|
case MSG_MOVE:
|
||||||
s.handleMove(buffer[:n], addr)
|
s.handleMove(buffer[:n], addr)
|
||||||
case MSG_CHANGE_COLOR:
|
case MSG_CHANGE_COLOR:
|
||||||
s.handleColorChange(buffer[:n], addr)
|
s.handleColorChange(buffer[:n], addr)
|
||||||
|
case MSG_LOGOUT:
|
||||||
|
s.handleLogout(buffer[:n], addr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleLogin(addr *net.UDPAddr) {
|
func (s *Server) handleLogin(data []byte, addr *net.UDPAddr) {
|
||||||
|
username, ok := DecodeLoginPacket(data)
|
||||||
|
if !ok || username == "" {
|
||||||
|
// Invalid login packet
|
||||||
|
log.Printf("Received invalid login packet from %s", addr)
|
||||||
|
responseMsg := EncodeLoginResponsePacket(false, "Invalid username")
|
||||||
|
s.conn.WriteToUDP(responseMsg, addr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Login attempt for username: %s from %s", username, addr)
|
||||||
|
|
||||||
s.mutex.Lock()
|
s.mutex.Lock()
|
||||||
|
|
||||||
|
// Check if username is already logged in
|
||||||
|
if existingPlayer, exists := s.usersByName[username]; exists {
|
||||||
|
s.mutex.Unlock()
|
||||||
|
responseMsg := EncodeLoginResponsePacket(false, "User already logged in")
|
||||||
|
s.conn.WriteToUDP(responseMsg, addr)
|
||||||
|
log.Printf("Login rejected for %s: already logged in (ID: %d)", username, existingPlayer.ID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
s.nextID++
|
s.nextID++
|
||||||
playerID := s.nextID
|
playerID := s.nextID
|
||||||
|
|
||||||
// Assign color based on player ID
|
// Load saved user data or create new
|
||||||
colors := []string{"red", "green", "orange", "purple", "white"}
|
var userData *UserData
|
||||||
colorIndex := (playerID - 1) % uint32(len(colors))
|
var exists bool
|
||||||
color := colors[colorIndex]
|
if userData, exists = s.userData[username]; !exists {
|
||||||
|
// New user - assign default color and random position
|
||||||
|
colors := []string{"red", "green", "orange", "purple", "white"}
|
||||||
|
colorIndex := (playerID - 1) % uint32(len(colors))
|
||||||
|
color := colors[colorIndex]
|
||||||
|
|
||||||
// Spawn at random position on heightmap
|
x := rand.Float32()*100 - 50
|
||||||
x := rand.Float32()*100 - 50
|
z := rand.Float32()*100 - 50
|
||||||
z := rand.Float32()*100 - 50
|
y := s.getHeightAt(x, z) + 1.0
|
||||||
y := s.getHeightAt(x, z) + 1.0
|
|
||||||
|
userData = &UserData{
|
||||||
|
Username: username,
|
||||||
|
Color: color,
|
||||||
|
Position: Vec3{x, y, z},
|
||||||
|
}
|
||||||
|
s.userData[username] = userData
|
||||||
|
}
|
||||||
|
|
||||||
player := &Player{
|
player := &Player{
|
||||||
ID: playerID,
|
ID: playerID,
|
||||||
Position: Vec3{x, y, z},
|
Username: username,
|
||||||
Color: color,
|
Position: userData.Position,
|
||||||
|
Color: userData.Color,
|
||||||
Address: addr,
|
Address: addr,
|
||||||
LastSeen: time.Now(),
|
LastSeen: time.Now(),
|
||||||
}
|
}
|
||||||
@ -137,10 +184,15 @@ func (s *Server) handleLogin(addr *net.UDPAddr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.players[playerID] = player
|
s.players[playerID] = player
|
||||||
|
s.usersByName[username] = player
|
||||||
s.mutex.Unlock()
|
s.mutex.Unlock()
|
||||||
|
|
||||||
// Send spawn message with color
|
// Send login success response
|
||||||
spawnMsg := EncodeSpawnPacket(playerID, player.Position, color)
|
responseMsg := EncodeLoginResponsePacket(true, "Login successful")
|
||||||
|
s.conn.WriteToUDP(responseMsg, addr)
|
||||||
|
|
||||||
|
// Send spawn message with saved position and color
|
||||||
|
spawnMsg := EncodeSpawnPacket(playerID, userData.Position, userData.Color)
|
||||||
s.conn.WriteToUDP(spawnMsg, addr)
|
s.conn.WriteToUDP(spawnMsg, addr)
|
||||||
|
|
||||||
// Send player list to new player
|
// Send player list to new player
|
||||||
@ -152,10 +204,10 @@ func (s *Server) handleLogin(addr *net.UDPAddr) {
|
|||||||
// Notify other players about new player
|
// Notify other players about new player
|
||||||
s.broadcastPlayerJoined(player)
|
s.broadcastPlayerJoined(player)
|
||||||
|
|
||||||
log.Printf("Player %d logged in at (%.2f, %.2f, %.2f) with color %s",
|
log.Printf("Player %s (ID %d) logged in at (%.2f, %.2f, %.2f) with color %s",
|
||||||
playerID, x, y, z, color)
|
username, playerID, userData.Position.X, userData.Position.Y, userData.Position.Z, userData.Color)
|
||||||
|
|
||||||
s.savePlayerPositions()
|
s.saveUserData()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleMove(data []byte, _ *net.UDPAddr) {
|
func (s *Server) handleMove(data []byte, _ *net.UDPAddr) {
|
||||||
@ -190,12 +242,53 @@ func (s *Server) handleMove(data []byte, _ *net.UDPAddr) {
|
|||||||
player.Position.Z = newZ
|
player.Position.Z = newZ
|
||||||
player.LastSeen = time.Now()
|
player.LastSeen = time.Now()
|
||||||
|
|
||||||
|
// Update persistent user data
|
||||||
|
if userData, exists := s.userData[player.Username]; exists {
|
||||||
|
userData.Position = player.Position
|
||||||
|
}
|
||||||
|
|
||||||
s.mutex.Unlock()
|
s.mutex.Unlock()
|
||||||
|
|
||||||
// Broadcast position update to all players
|
// Broadcast position update to all players
|
||||||
s.broadcastUpdate(player)
|
s.broadcastUpdate(player)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleLogout(data []byte, _ *net.UDPAddr) {
|
||||||
|
playerID, ok := DecodeLogoutPacket(data)
|
||||||
|
if !ok {
|
||||||
|
log.Printf("Failed to decode logout packet")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Received logout request for player ID %d", playerID)
|
||||||
|
|
||||||
|
s.mutex.Lock()
|
||||||
|
player, exists := s.players[playerID]
|
||||||
|
if !exists {
|
||||||
|
s.mutex.Unlock()
|
||||||
|
log.Printf("Player ID %d not found in active players", playerID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save final position
|
||||||
|
if userData, exists := s.userData[player.Username]; exists {
|
||||||
|
userData.Position = player.Position
|
||||||
|
userData.Color = player.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from active players
|
||||||
|
username := player.Username
|
||||||
|
delete(s.players, playerID)
|
||||||
|
delete(s.usersByName, username)
|
||||||
|
s.mutex.Unlock()
|
||||||
|
|
||||||
|
// Notify other players
|
||||||
|
s.broadcastPlayerLeft(playerID)
|
||||||
|
|
||||||
|
log.Printf("Player %s (ID %d) successfully logged out", username, playerID)
|
||||||
|
s.saveUserData()
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleColorChange(data []byte, _ *net.UDPAddr) {
|
func (s *Server) handleColorChange(data []byte, _ *net.UDPAddr) {
|
||||||
playerID, newColor, ok := DecodeColorChangePacket(data)
|
playerID, newColor, ok := DecodeColorChangePacket(data)
|
||||||
if !ok {
|
if !ok {
|
||||||
@ -218,6 +311,12 @@ func (s *Server) handleColorChange(data []byte, _ *net.UDPAddr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
player.Color = newColor
|
player.Color = newColor
|
||||||
|
|
||||||
|
// Update persistent user data
|
||||||
|
if userData, exists := s.userData[player.Username]; exists {
|
||||||
|
userData.Color = newColor
|
||||||
|
}
|
||||||
|
|
||||||
s.mutex.Unlock()
|
s.mutex.Unlock()
|
||||||
|
|
||||||
// Broadcast color change to all players
|
// Broadcast color change to all players
|
||||||
@ -312,42 +411,58 @@ func (s *Server) checkTimeouts() {
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
for id, player := range s.players {
|
for id, player := range s.players {
|
||||||
if now.Sub(player.LastSeen) > 30*time.Second {
|
if now.Sub(player.LastSeen) > 30*time.Second {
|
||||||
|
// Save final position before removing
|
||||||
|
if userData, exists := s.userData[player.Username]; exists {
|
||||||
|
userData.Position = player.Position
|
||||||
|
userData.Color = player.Color
|
||||||
|
}
|
||||||
|
|
||||||
delete(s.players, id)
|
delete(s.players, id)
|
||||||
|
delete(s.usersByName, player.Username)
|
||||||
go s.broadcastPlayerLeft(id)
|
go s.broadcastPlayerLeft(id)
|
||||||
log.Printf("Player %d timed out", id)
|
log.Printf("Player %s (ID %d) timed out", player.Username, id)
|
||||||
|
go s.saveUserData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) loadPlayerPositions() {
|
func (s *Server) loadUserData() {
|
||||||
data, err := os.ReadFile("players.json")
|
data, err := os.ReadFile("players.json")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf("No existing user data found: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var savedPlayers map[uint32]Vec3
|
var savedUsers map[string]*UserData
|
||||||
json.Unmarshal(data, &savedPlayers)
|
if err := json.Unmarshal(data, &savedUsers); err != nil {
|
||||||
|
log.Printf("Failed to parse user data: %v", err)
|
||||||
for id, pos := range savedPlayers {
|
return
|
||||||
if id > s.nextID {
|
|
||||||
s.nextID = id
|
|
||||||
}
|
|
||||||
s.players[id] = &Player{
|
|
||||||
ID: id,
|
|
||||||
Position: pos,
|
|
||||||
LastSeen: time.Now(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.userData = savedUsers
|
||||||
|
log.Printf("Loaded data for %d users", len(savedUsers))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) savePlayerPositions() {
|
func (s *Server) saveUserData() {
|
||||||
s.mutex.RLock()
|
s.mutex.RLock()
|
||||||
savedPlayers := make(map[uint32]Vec3)
|
// Deep copy userData to avoid holding lock during file I/O
|
||||||
for id, player := range s.players {
|
savedUsers := make(map[string]*UserData)
|
||||||
savedPlayers[id] = player.Position
|
for username, data := range s.userData {
|
||||||
|
savedUsers[username] = &UserData{
|
||||||
|
Username: data.Username,
|
||||||
|
Color: data.Color,
|
||||||
|
Position: data.Position,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
s.mutex.RUnlock()
|
s.mutex.RUnlock()
|
||||||
|
|
||||||
data, _ := json.Marshal(savedPlayers)
|
data, err := json.MarshalIndent(savedUsers, "", " ")
|
||||||
os.WriteFile("players.json", data, 0644)
|
if err != nil {
|
||||||
|
log.Printf("Failed to marshal user data: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile("players.json", data, 0644); err != nil {
|
||||||
|
log.Printf("Failed to save user data: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1 +1,20 @@
|
|||||||
{"1":{"X":-13.046684,"Y":-0.008753866,"Z":15.9760895}}
|
{
|
||||||
|
"sky": {
|
||||||
|
"username": "sky",
|
||||||
|
"color": "purple",
|
||||||
|
"position": {
|
||||||
|
"X": 3.9765263,
|
||||||
|
"Y": -5.033655,
|
||||||
|
"Z": 19.824585
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"username": "test",
|
||||||
|
"color": "red",
|
||||||
|
"position": {
|
||||||
|
"X": 14.141681,
|
||||||
|
"Y": 2.2383373,
|
||||||
|
"Z": -0.06184849
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user