1
0

Add login GUI and location/color persistence

This commit is contained in:
Sky Johnson 2025-09-08 19:14:46 -05:00
parent c05a27a621
commit 8d08958280
11 changed files with 6443 additions and 81 deletions

4
.gitignore vendored Normal file
View 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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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