mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2025-09-05 14:58:27 +00:00
WIP
This commit is contained in:
@@ -322,6 +322,7 @@ add_subdirectory(dWorldServer)
|
||||
add_subdirectory(dAuthServer)
|
||||
add_subdirectory(dChatServer)
|
||||
add_subdirectory(dMasterServer) # Add MasterServer last so it can rely on the other binaries
|
||||
add_subdirectory(dSimClient) # Simulation client for testing and load testing
|
||||
|
||||
target_precompile_headers(
|
||||
dZoneManager PRIVATE
|
||||
|
56
build_simclient.bat
Normal file
56
build_simclient.bat
Normal file
@@ -0,0 +1,56 @@
|
||||
@echo off
|
||||
REM Simple build and test script for SimClient (Windows)
|
||||
REM This script demonstrates how to build and run the SimClient
|
||||
|
||||
echo DarkflameServer SimClient Build and Test Script
|
||||
echo ===============================================
|
||||
|
||||
REM Check if we're in the right directory
|
||||
if not exist CMakeLists.txt (
|
||||
echo Error: Please run this script from the DarkflameServer root directory
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Create build directory if it doesn't exist
|
||||
if not exist build (
|
||||
echo Creating build directory...
|
||||
mkdir build
|
||||
)
|
||||
|
||||
cd build
|
||||
|
||||
REM Generate build files
|
||||
echo Generating build files...
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
|
||||
if errorlevel 1 (
|
||||
echo Error: CMake configuration failed
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
REM Build SimClient
|
||||
echo Building SimClient...
|
||||
cmake --build . --target SimClient --config Release
|
||||
|
||||
if errorlevel 1 (
|
||||
echo Error: Build failed
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Build completed successfully!
|
||||
echo.
|
||||
echo SimClient executable location:
|
||||
dir /s /b SimClient.exe 2>nul
|
||||
|
||||
echo.
|
||||
echo To run SimClient:
|
||||
echo SimClient.exe --help # Show help
|
||||
echo SimClient.exe -t basic -v # Basic test with verbose logging
|
||||
echo SimClient.exe -n 5 -t load # Load test with 5 clients
|
||||
echo SimClient.exe -a 127.0.0.1:1001 -n 3 # Connect to specific auth server
|
||||
echo.
|
||||
echo Make sure your DarkflameServer is running before testing!
|
||||
pause
|
53
build_simclient.sh
Normal file
53
build_simclient.sh
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Simple build and test script for SimClient
|
||||
# This script demonstrates how to build and run the SimClient
|
||||
|
||||
echo "DarkflameServer SimClient Build and Test Script"
|
||||
echo "==============================================="
|
||||
|
||||
# Check if we're in the right directory
|
||||
if [ ! -f "CMakeLists.txt" ]; then
|
||||
echo "Error: Please run this script from the DarkflameServer root directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create build directory if it doesn't exist
|
||||
if [ ! -d "build" ]; then
|
||||
echo "Creating build directory..."
|
||||
mkdir build
|
||||
fi
|
||||
|
||||
cd build
|
||||
|
||||
# Generate build files
|
||||
echo "Generating build files..."
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
|
||||
# Build SimClient
|
||||
echo "Building SimClient..."
|
||||
if command -v make >/dev/null 2>&1; then
|
||||
make SimClient -j$(nproc)
|
||||
else
|
||||
# For systems without make, try cmake --build
|
||||
cmake --build . --target SimClient --config Release
|
||||
fi
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Build failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Build completed successfully!"
|
||||
echo ""
|
||||
echo "SimClient executable location:"
|
||||
find . -name "SimClient*" -type f -executable 2>/dev/null | head -1
|
||||
|
||||
echo ""
|
||||
echo "To run SimClient:"
|
||||
echo " ./SimClient --help # Show help"
|
||||
echo " ./SimClient -t basic -v # Basic test with verbose logging"
|
||||
echo " ./SimClient -n 5 -t load # Load test with 5 clients"
|
||||
echo " ./SimClient -a 127.0.0.1:1001 -n 3 # Connect to specific auth server"
|
||||
echo ""
|
||||
echo "Make sure your DarkflameServer is running before testing!"
|
29
dSimClient/CMakeLists.txt
Normal file
29
dSimClient/CMakeLists.txt
Normal file
@@ -0,0 +1,29 @@
|
||||
set(DSIMCLIENT_SOURCES
|
||||
"SimClient.cpp"
|
||||
"SimUser.cpp"
|
||||
"SimUser.h"
|
||||
"SimWorld.cpp"
|
||||
"SimWorld.h"
|
||||
"SimBehavior.cpp"
|
||||
"SimBehavior.h"
|
||||
"SimConfig.cpp"
|
||||
"SimConfig.h"
|
||||
)
|
||||
|
||||
add_executable(SimClient ${DSIMCLIENT_SOURCES})
|
||||
|
||||
target_link_libraries(SimClient ${COMMON_LIBRARIES})
|
||||
target_include_directories(SimClient PRIVATE
|
||||
"${PROJECT_SOURCE_DIR}/dCommon"
|
||||
"${PROJECT_SOURCE_DIR}/dDatabase"
|
||||
"${PROJECT_SOURCE_DIR}/dNet"
|
||||
"${PROJECT_SOURCE_DIR}/thirdparty/raknet/Source"
|
||||
"${PROJECT_SOURCE_DIR}/dServer"
|
||||
)
|
||||
|
||||
# Copy the configuration file to build directory
|
||||
configure_file(
|
||||
"${PROJECT_SOURCE_DIR}/resources/simclient.ini"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/simclient.ini"
|
||||
COPYONLY
|
||||
)
|
266
dSimClient/README.md
Normal file
266
dSimClient/README.md
Normal file
@@ -0,0 +1,266 @@
|
||||
# DarkflameServer SimClient
|
||||
|
||||
A simulation client for testing and load testing the DarkflameServer.
|
||||
|
||||
## Overview
|
||||
|
||||
The SimClient is a headless client that can simulate multiple player connections to test server performance, stability, and functionality. It's designed for:
|
||||
|
||||
- **Load Testing**: Simulate many concurrent users
|
||||
- **Performance Testing**: Measure server response under load
|
||||
- **Regression Testing**: Automated testing of server functionality
|
||||
- **Development Testing**: Quick connectivity and feature testing
|
||||
|
||||
## Features
|
||||
|
||||
- **Multiple Client Simulation**: Spawn many simulated clients
|
||||
- **Configurable Behaviors**: Movement, chat, random actions
|
||||
- **Network Statistics**: Track packets, bytes, connection stats
|
||||
- **Flexible Configuration**: File-based and command-line configuration
|
||||
- **Different Test Modes**: Basic, load, stress, movement, chat tests
|
||||
- **Real-time Monitoring**: Live statistics and status updates
|
||||
|
||||
## Building
|
||||
|
||||
The SimClient is built as part of the normal DarkflameServer build process:
|
||||
|
||||
```bash
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
make SimClient # or build SimClient target in Visual Studio
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```bash
|
||||
# Run with default settings (1 client)
|
||||
./SimClient
|
||||
|
||||
# Run load test with 10 clients
|
||||
./SimClient -n 10 -t load
|
||||
|
||||
# Run with verbose logging
|
||||
./SimClient -v -n 5
|
||||
|
||||
# Connect to remote server
|
||||
./SimClient -a 192.168.1.100:1001 -w 192.168.1.100:2000
|
||||
```
|
||||
|
||||
### Command Line Options
|
||||
|
||||
- `-h, --help` - Show help message
|
||||
- `-c, --config <file>` - Configuration file (default: simclient.ini)
|
||||
- `-n, --clients <count>` - Number of clients to simulate
|
||||
- `-a, --auth <ip:port>` - Auth server address
|
||||
- `-w, --world <ip:port>` - World server address
|
||||
- `-v, --verbose` - Enable verbose logging
|
||||
- `-t, --test <type>` - Test type (basic, load, stress, movement, chat)
|
||||
|
||||
### Test Types
|
||||
|
||||
- **basic** - Single client connectivity test
|
||||
- **load** - Multiple client load test
|
||||
- **stress** - High-load stress test (2x client count)
|
||||
- **movement** - Movement simulation test
|
||||
- **chat** - Chat functionality test
|
||||
|
||||
### Configuration File
|
||||
|
||||
Create `simclient.ini` to configure default settings:
|
||||
|
||||
```ini
|
||||
# Server Settings
|
||||
auth_server_ip=127.0.0.1
|
||||
auth_server_port=1001
|
||||
world_server_ip=127.0.0.1
|
||||
world_server_port=2000
|
||||
|
||||
# Simulation Settings
|
||||
client_count=5
|
||||
spawn_delay_ms=2000
|
||||
tick_rate_ms=16
|
||||
|
||||
# Behavior Settings
|
||||
enable_movement=1
|
||||
enable_chat=0
|
||||
enable_random_actions=1
|
||||
action_interval_ms=5000
|
||||
|
||||
# Test Accounts
|
||||
test_account_1=testuser1:testpass1
|
||||
test_account_2=testuser2:testpass2
|
||||
```
|
||||
|
||||
## Simulated Behaviors
|
||||
|
||||
### Movement Simulation
|
||||
- Random movement within zones
|
||||
- Configurable movement patterns (random, circular, linear)
|
||||
- Position updates sent to server
|
||||
- Collision-free pathfinding
|
||||
|
||||
### Chat Simulation
|
||||
- Predefined message sets (normal, spam, roleplay)
|
||||
- Configurable chat intervals
|
||||
- Multiple chat channels support
|
||||
|
||||
### Connection Management
|
||||
- Automatic reconnection on failures
|
||||
- Graceful disconnection handling
|
||||
- Connection state tracking
|
||||
- Error reporting and recovery
|
||||
|
||||
### Statistics Tracking
|
||||
- Packets sent/received per client
|
||||
- Bytes sent/received per client
|
||||
- Connection success/failure rates
|
||||
- Real-time performance metrics
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Load Testing
|
||||
Test server performance under various loads:
|
||||
|
||||
```bash
|
||||
# Test with 50 concurrent users
|
||||
./SimClient -t load -n 50
|
||||
|
||||
# Stress test with 100 users
|
||||
./SimClient -t stress -n 50 # Will actually use 100 (2x)
|
||||
```
|
||||
|
||||
### Development Testing
|
||||
Quick testing during development:
|
||||
|
||||
```bash
|
||||
# Basic connectivity test
|
||||
./SimClient -t basic
|
||||
|
||||
# Test movement system
|
||||
./SimClient -t movement -n 3
|
||||
```
|
||||
|
||||
### Automated Testing
|
||||
Integrate into CI/CD pipelines:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Start server
|
||||
./MasterServer &
|
||||
./AuthServer &
|
||||
./WorldServer &
|
||||
|
||||
sleep 5
|
||||
|
||||
# Run automated tests
|
||||
./SimClient -t basic || exit 1
|
||||
./SimClient -t load -n 10 || exit 1
|
||||
|
||||
# Cleanup
|
||||
killall MasterServer AuthServer WorldServer
|
||||
```
|
||||
|
||||
### Performance Monitoring
|
||||
Monitor server performance over time:
|
||||
|
||||
```bash
|
||||
# Long-running load test with statistics
|
||||
./SimClient -t load -n 25 -v > load_test_$(date +%Y%m%d_%H%M%S).log
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Components
|
||||
|
||||
- **SimClient** - Main executable and command-line interface
|
||||
- **SimConfig** - Configuration management
|
||||
- **SimWorld** - Simulation coordinator and statistics
|
||||
- **SimUser** - Individual client simulation
|
||||
- **SimBehavior** - Behavior patterns and actions
|
||||
|
||||
### Network Protocol
|
||||
|
||||
The SimClient implements a simplified version of the DarkflameServer client protocol:
|
||||
|
||||
1. **Authentication Flow**
|
||||
- Connect to AuthServer
|
||||
- Send login credentials
|
||||
- Receive session token
|
||||
- Get world server info
|
||||
|
||||
2. **World Connection**
|
||||
- Connect to WorldServer
|
||||
- Send session validation
|
||||
- Load character/world data
|
||||
- Enter simulation mode
|
||||
|
||||
3. **Simulation Loop**
|
||||
- Send position updates
|
||||
- Process server messages
|
||||
- Execute configured behaviors
|
||||
- Maintain connection health
|
||||
|
||||
### Error Handling
|
||||
|
||||
- Automatic retry on connection failures
|
||||
- Graceful degradation on partial failures
|
||||
- Detailed error logging and reporting
|
||||
- Recovery strategies for different error types
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Connection Refused**
|
||||
- Ensure servers are running and accessible
|
||||
- Check firewall settings
|
||||
- Verify IP addresses and ports
|
||||
|
||||
**Authentication Failures**
|
||||
- Verify test account credentials
|
||||
- Check database connectivity
|
||||
- Review auth server logs
|
||||
|
||||
**High Memory Usage**
|
||||
- Reduce client count for available RAM
|
||||
- Monitor for memory leaks
|
||||
- Use 64-bit build for large tests
|
||||
|
||||
**Performance Issues**
|
||||
- Adjust tick rate and spawn delays
|
||||
- Monitor server resource usage
|
||||
- Check network bandwidth limits
|
||||
|
||||
### Debugging
|
||||
|
||||
Enable verbose logging to see detailed operations:
|
||||
|
||||
```bash
|
||||
./SimClient -v -n 1 -t basic
|
||||
```
|
||||
|
||||
Check log files in the `logs/` directory for detailed information.
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new features to SimClient:
|
||||
|
||||
1. Follow existing code patterns
|
||||
2. Add appropriate error handling
|
||||
3. Update configuration options
|
||||
4. Add command-line parameters as needed
|
||||
5. Update this documentation
|
||||
6. Test with various scenarios
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements for SimClient:
|
||||
|
||||
- **Protocol Coverage**: Support more game messages and features
|
||||
- **Behavior Scripting**: Lua or Python scripting for custom behaviors
|
||||
- **Performance Profiling**: Built-in server performance monitoring
|
||||
- **Web Interface**: Real-time web dashboard for monitoring
|
||||
- **Distributed Testing**: Coordinate multiple SimClient instances
|
||||
- **Recording/Playback**: Record real client sessions and replay them
|
162
dSimClient/SimBehavior.cpp
Normal file
162
dSimClient/SimBehavior.cpp
Normal file
@@ -0,0 +1,162 @@
|
||||
#include "SimBehavior.h"
|
||||
#include "SimUser.h"
|
||||
#include <random>
|
||||
#include <cmath>
|
||||
|
||||
SimBehavior::MovementPattern SimBehavior::GetRandomMovementPattern() {
|
||||
MovementPattern pattern;
|
||||
pattern.minDistance = 5.0f;
|
||||
pattern.maxDistance = 15.0f;
|
||||
pattern.moveSpeed = 5.0f;
|
||||
pattern.pauseTime = 2.0f;
|
||||
return pattern;
|
||||
}
|
||||
|
||||
SimBehavior::MovementPattern SimBehavior::GetCircularMovementPattern(float radius) {
|
||||
MovementPattern pattern;
|
||||
pattern.minDistance = radius * 0.8f;
|
||||
pattern.maxDistance = radius * 1.2f;
|
||||
pattern.moveSpeed = 3.0f;
|
||||
pattern.pauseTime = 1.0f;
|
||||
return pattern;
|
||||
}
|
||||
|
||||
SimBehavior::MovementPattern SimBehavior::GetLinearMovementPattern(float distance) {
|
||||
MovementPattern pattern;
|
||||
pattern.minDistance = distance;
|
||||
pattern.maxDistance = distance;
|
||||
pattern.moveSpeed = 4.0f;
|
||||
pattern.pauseTime = 0.5f;
|
||||
return pattern;
|
||||
}
|
||||
|
||||
SimBehavior::ChatPattern SimBehavior::GetRandomChatPattern() {
|
||||
ChatPattern pattern;
|
||||
pattern.messages = {
|
||||
"Hello everyone!",
|
||||
"How's everyone doing?",
|
||||
"Nice day for building!",
|
||||
"Anyone want to race?",
|
||||
"This place looks cool!",
|
||||
"Great work on that build!",
|
||||
"Thanks for the help!",
|
||||
"See you around!",
|
||||
"What's your favorite zone?",
|
||||
"This game is awesome!"
|
||||
};
|
||||
pattern.intervalMin = 30.0f; // 30 seconds minimum
|
||||
pattern.intervalMax = 120.0f; // 2 minutes maximum
|
||||
return pattern;
|
||||
}
|
||||
|
||||
SimBehavior::ChatPattern SimBehavior::GetSpamChatPattern() {
|
||||
ChatPattern pattern;
|
||||
pattern.messages = {
|
||||
"Test message 1",
|
||||
"Test message 2",
|
||||
"Test message 3",
|
||||
"Spam test",
|
||||
"Load test message"
|
||||
};
|
||||
pattern.intervalMin = 1.0f; // 1 second minimum
|
||||
pattern.intervalMax = 3.0f; // 3 seconds maximum
|
||||
return pattern;
|
||||
}
|
||||
|
||||
SimBehavior::ChatPattern SimBehavior::GetRoleplayChatPattern() {
|
||||
ChatPattern pattern;
|
||||
pattern.messages = {
|
||||
"*waves to everyone*",
|
||||
"*looks around curiously*",
|
||||
"*examines the nearby structures*",
|
||||
"*checks inventory*",
|
||||
"*stretches after a long journey*",
|
||||
"*admires the scenery*",
|
||||
"*practices building techniques*",
|
||||
"*takes a short break*",
|
||||
"*prepares for the next adventure*",
|
||||
"*organizes backpack*"
|
||||
};
|
||||
pattern.intervalMin = 45.0f; // 45 seconds minimum
|
||||
pattern.intervalMax = 180.0f; // 3 minutes maximum
|
||||
return pattern;
|
||||
}
|
||||
|
||||
void SimBehavior::ExecuteRandomMovement(SimUser* user, const MovementPattern& pattern) {
|
||||
if (!user) return;
|
||||
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
static std::uniform_real_distribution<float> angleDist(0.0f, 2.0f * 3.14159f);
|
||||
static std::uniform_real_distribution<float> distDist(pattern.minDistance, pattern.maxDistance);
|
||||
|
||||
// Generate random direction and distance
|
||||
float angle = angleDist(gen);
|
||||
float distance = distDist(gen);
|
||||
|
||||
// Calculate target position
|
||||
NiPoint3 currentPos = user->GetPosition();
|
||||
NiPoint3 targetPos;
|
||||
targetPos.x = currentPos.x + distance * std::cos(angle);
|
||||
targetPos.y = currentPos.y;
|
||||
targetPos.z = currentPos.z + distance * std::sin(angle);
|
||||
|
||||
// Simulate movement (this would be implemented in SimUser)
|
||||
// user->MoveTo(targetPos, pattern.moveSpeed);
|
||||
}
|
||||
|
||||
void SimBehavior::ExecuteChat(SimUser* user, const ChatPattern& pattern) {
|
||||
if (!user || pattern.messages.empty()) return;
|
||||
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
static std::uniform_int_distribution<size_t> msgDist(0, pattern.messages.size() - 1);
|
||||
|
||||
// Select random message
|
||||
size_t messageIndex = msgDist(gen);
|
||||
const std::string& message = pattern.messages[messageIndex];
|
||||
|
||||
user->SendChatMessage(message);
|
||||
}
|
||||
|
||||
void SimBehavior::ExecuteIdleBehavior(SimUser* user) {
|
||||
if (!user) return;
|
||||
|
||||
// Simple idle behavior - just send occasional heartbeats
|
||||
// The user's Update method handles most of this
|
||||
}
|
||||
|
||||
void SimBehavior::ExecuteStressTest(SimUser* user) {
|
||||
if (!user) return;
|
||||
|
||||
// Stress test behavior - rapid actions
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
static std::uniform_int_distribution<int> actionDist(1, 4);
|
||||
|
||||
int action = actionDist(gen);
|
||||
|
||||
switch (action) {
|
||||
case 1:
|
||||
// Rapid movement
|
||||
ExecuteRandomMovement(user, GetRandomMovementPattern());
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// Rapid chat
|
||||
ExecuteChat(user, GetSpamChatPattern());
|
||||
break;
|
||||
|
||||
case 3:
|
||||
// Multiple position updates
|
||||
for (int i = 0; i < 5; i++) {
|
||||
user->SimulateMovement();
|
||||
}
|
||||
break;
|
||||
|
||||
case 4:
|
||||
// Just maintain connection
|
||||
ExecuteIdleBehavior(user);
|
||||
break;
|
||||
}
|
||||
}
|
51
dSimClient/SimBehavior.h
Normal file
51
dSimClient/SimBehavior.h
Normal file
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* @brief Defines and manages different test behaviors for simulated clients
|
||||
*/
|
||||
class SimBehavior {
|
||||
public:
|
||||
enum class BehaviorType {
|
||||
IDLE,
|
||||
RANDOM_MOVEMENT,
|
||||
CHAT_SPAM,
|
||||
RAPID_RECONNECT,
|
||||
INVENTORY_ACTIONS,
|
||||
SKILL_USAGE,
|
||||
BUILDING_ACTIONS,
|
||||
RACING_ACTIONS
|
||||
};
|
||||
|
||||
struct MovementPattern {
|
||||
float minDistance;
|
||||
float maxDistance;
|
||||
float moveSpeed;
|
||||
float pauseTime;
|
||||
};
|
||||
|
||||
struct ChatPattern {
|
||||
std::vector<std::string> messages;
|
||||
float intervalMin;
|
||||
float intervalMax;
|
||||
};
|
||||
|
||||
static MovementPattern GetRandomMovementPattern();
|
||||
static MovementPattern GetCircularMovementPattern(float radius);
|
||||
static MovementPattern GetLinearMovementPattern(float distance);
|
||||
|
||||
static ChatPattern GetRandomChatPattern();
|
||||
static ChatPattern GetSpamChatPattern();
|
||||
static ChatPattern GetRoleplayChatPattern();
|
||||
|
||||
// Behavior executors
|
||||
static void ExecuteRandomMovement(class SimUser* user, const MovementPattern& pattern);
|
||||
static void ExecuteChat(class SimUser* user, const ChatPattern& pattern);
|
||||
static void ExecuteIdleBehavior(class SimUser* user);
|
||||
static void ExecuteStressTest(class SimUser* user);
|
||||
|
||||
private:
|
||||
SimBehavior() = delete;
|
||||
};
|
230
dSimClient/SimClient.cpp
Normal file
230
dSimClient/SimClient.cpp
Normal file
@@ -0,0 +1,230 @@
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <csignal>
|
||||
#include <thread>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
// DLU Includes
|
||||
#include "Logger.h"
|
||||
#include "dConfig.h"
|
||||
#include "Game.h"
|
||||
#include "Server.h"
|
||||
#include "BinaryPathFinder.h"
|
||||
|
||||
// SimClient includes
|
||||
#include "SimConfig.h"
|
||||
#include "SimWorld.h"
|
||||
#include "SimUser.h"
|
||||
#include "SimBehavior.h"
|
||||
|
||||
namespace Game {
|
||||
Logger* logger = nullptr;
|
||||
dConfig* config = nullptr;
|
||||
Game::signal_t lastSignal = 0;
|
||||
}
|
||||
|
||||
void PrintUsage(const char* programName) {
|
||||
std::cout << "Usage: " << programName << " [options]" << std::endl;
|
||||
std::cout << "Options:" << std::endl;
|
||||
std::cout << " -h, --help Show this help message" << std::endl;
|
||||
std::cout << " -c, --config <file> Configuration file (default: simclient.ini)" << std::endl;
|
||||
std::cout << " -n, --clients <count> Number of clients to simulate (default: 1)" << std::endl;
|
||||
std::cout << " -a, --auth <ip:port> Auth server address (default: 127.0.0.1:1001)" << std::endl;
|
||||
std::cout << " -w, --world <ip:port> World server address (default: 127.0.0.1:2000)" << std::endl;
|
||||
std::cout << " -v, --verbose Enable verbose logging" << std::endl;
|
||||
std::cout << " -t, --test <type> Run specific test type:" << std::endl;
|
||||
std::cout << " basic - Basic connectivity test (1 client)" << std::endl;
|
||||
std::cout << " load - Load test (multiple clients)" << std::endl;
|
||||
std::cout << " stress - Stress test (high load)" << std::endl;
|
||||
std::cout << " movement - Movement simulation test" << std::endl;
|
||||
std::cout << " chat - Chat functionality test" << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << "Examples:" << std::endl;
|
||||
std::cout << " " << programName << " -n 10 -v # 10 clients with verbose logging" << std::endl;
|
||||
std::cout << " " << programName << " -t load -n 50 # Load test with 50 clients" << std::endl;
|
||||
std::cout << " " << programName << " -a 192.168.1.100:1001 # Connect to remote server" << std::endl;
|
||||
}
|
||||
|
||||
void SignalHandler(int signal) {
|
||||
std::cout << std::endl << "Received signal " << signal << ", shutting down..." << std::endl;
|
||||
Game::lastSignal = signal;
|
||||
}
|
||||
|
||||
bool ParseAddress(const std::string& addr, std::string& ip, uint16_t& port) {
|
||||
size_t colonPos = addr.find(':');
|
||||
if (colonPos == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ip = addr.substr(0, colonPos);
|
||||
try {
|
||||
port = static_cast<uint16_t>(std::stoul(addr.substr(colonPos + 1)));
|
||||
return true;
|
||||
} catch (const std::exception&) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
std::cout << "DarkflameServer Simulation Client" << std::endl;
|
||||
std::cout << "Version: " << PROJECT_VERSION << std::endl;
|
||||
std::cout << "Compiled on: " << __TIMESTAMP__ << std::endl;
|
||||
std::cout << std::endl;
|
||||
|
||||
// Set up signal handling
|
||||
std::signal(SIGINT, SignalHandler);
|
||||
std::signal(SIGTERM, SignalHandler);
|
||||
|
||||
// Parse command line arguments
|
||||
std::string configFile = "simclient.ini";
|
||||
std::string testType = "load";
|
||||
bool showHelp = false;
|
||||
|
||||
auto config = std::make_shared<SimConfig>();
|
||||
|
||||
for (int i = 1; i < argc; i++) {
|
||||
std::string arg = argv[i];
|
||||
|
||||
if (arg == "-h" || arg == "--help") {
|
||||
showHelp = true;
|
||||
} else if (arg == "-c" || arg == "--config") {
|
||||
if (i + 1 < argc) {
|
||||
configFile = argv[++i];
|
||||
} else {
|
||||
std::cerr << "Error: " << arg << " requires a filename" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
} else if (arg == "-n" || arg == "--clients") {
|
||||
if (i + 1 < argc) {
|
||||
try {
|
||||
uint32_t clientCount = std::stoul(argv[++i]);
|
||||
config->SetClientCount(clientCount);
|
||||
} catch (const std::exception&) {
|
||||
std::cerr << "Error: Invalid client count" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
std::cerr << "Error: " << arg << " requires a number" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
} else if (arg == "-a" || arg == "--auth") {
|
||||
if (i + 1 < argc) {
|
||||
std::string ip;
|
||||
uint16_t port;
|
||||
if (ParseAddress(argv[++i], ip, port)) {
|
||||
config->SetAuthServer(ip, port);
|
||||
} else {
|
||||
std::cerr << "Error: Invalid auth server address format (use ip:port)" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
std::cerr << "Error: " << arg << " requires an address" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
} else if (arg == "-w" || arg == "--world") {
|
||||
if (i + 1 < argc) {
|
||||
std::string ip;
|
||||
uint16_t port;
|
||||
if (ParseAddress(argv[++i], ip, port)) {
|
||||
config->SetWorldServer(ip, port);
|
||||
} else {
|
||||
std::cerr << "Error: Invalid world server address format (use ip:port)" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
std::cerr << "Error: " << arg << " requires an address" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
} else if (arg == "-v" || arg == "--verbose") {
|
||||
config->SetVerboseLogging(true);
|
||||
} else if (arg == "-t" || arg == "--test") {
|
||||
if (i + 1 < argc) {
|
||||
testType = argv[++i];
|
||||
} else {
|
||||
std::cerr << "Error: " << arg << " requires a test type" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
std::cerr << "Error: Unknown argument: " << arg << std::endl;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (showHelp) {
|
||||
PrintUsage(argv[0]);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Setup logging
|
||||
const auto logsDir = BinaryPathFinder::GetBinaryDir() / "logs";
|
||||
if (!std::filesystem::exists(logsDir)) {
|
||||
std::filesystem::create_directories(logsDir);
|
||||
}
|
||||
|
||||
std::string logPath = (logsDir / "SimClient").string() + "_" + std::to_string(time(nullptr)) + ".log";
|
||||
Game::logger = new Logger(logPath, true, config->GetVerboseLogging());
|
||||
|
||||
if (Game::logger) {
|
||||
#ifdef _WIN32
|
||||
LOG("SimClient started with PID %lu", GetCurrentProcessId());
|
||||
#else
|
||||
LOG("SimClient started with PID %d", getpid());
|
||||
#endif
|
||||
}
|
||||
|
||||
// Load configuration file if it exists
|
||||
if (!config->LoadFromFile(configFile)) {
|
||||
std::cout << "Using default configuration (config file not found or invalid)" << std::endl;
|
||||
}
|
||||
|
||||
// Create and initialize simulation world
|
||||
SimWorld simWorld;
|
||||
if (!simWorld.Initialize(config)) {
|
||||
std::cerr << "Failed to initialize simulation world" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << "Starting simulation..." << std::endl;
|
||||
std::cout << "Press Ctrl+C to stop" << std::endl;
|
||||
std::cout << std::endl;
|
||||
|
||||
// Run the appropriate test
|
||||
try {
|
||||
if (testType == "basic") {
|
||||
simWorld.RunBasicConnectivityTest();
|
||||
} else if (testType == "load") {
|
||||
simWorld.RunLoadTest();
|
||||
} else if (testType == "stress") {
|
||||
simWorld.RunStressTest();
|
||||
} else if (testType == "movement") {
|
||||
simWorld.RunMovementTest();
|
||||
} else if (testType == "chat") {
|
||||
simWorld.RunChatTest();
|
||||
} else {
|
||||
std::cerr << "Unknown test type: " << testType << std::endl;
|
||||
std::cerr << "Valid types: basic, load, stress, movement, chat" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Exception during simulation: " << e.what() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Clean shutdown
|
||||
simWorld.Shutdown();
|
||||
|
||||
if (Game::logger) {
|
||||
LOG("SimClient shutting down");
|
||||
delete Game::logger;
|
||||
Game::logger = nullptr;
|
||||
}
|
||||
|
||||
std::cout << "Simulation complete!" << std::endl;
|
||||
return 0;
|
||||
}
|
113
dSimClient/SimConfig.cpp
Normal file
113
dSimClient/SimConfig.cpp
Normal file
@@ -0,0 +1,113 @@
|
||||
#include "SimConfig.h"
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <iostream>
|
||||
|
||||
SimConfig::SimConfig()
|
||||
: m_AuthServerIP("127.0.0.1")
|
||||
, m_AuthServerPort(1001)
|
||||
, m_WorldServerIP("127.0.0.1")
|
||||
, m_WorldServerPort(2000)
|
||||
, m_ClientCount(1)
|
||||
, m_SpawnDelayMs(1000)
|
||||
, m_TickRateMs(16)
|
||||
, m_EnableMovement(true)
|
||||
, m_EnableChat(false)
|
||||
, m_EnableRandomActions(true)
|
||||
, m_ActionIntervalMs(5000)
|
||||
, m_VerboseLogging(false)
|
||||
, m_LogFile("simclient.log")
|
||||
{
|
||||
// Add some default test accounts
|
||||
m_TestAccounts.push_back(std::make_pair("testuser1", "testpass1"));
|
||||
m_TestAccounts.push_back(std::make_pair("testuser2", "testpass2"));
|
||||
m_TestAccounts.push_back(std::make_pair("testuser3", "testpass3"));
|
||||
m_TestAccounts.push_back(std::make_pair("testuser4", "testpass4"));
|
||||
m_TestAccounts.push_back(std::make_pair("testuser5", "testpass5"));
|
||||
}
|
||||
|
||||
SimConfig::~SimConfig() {
|
||||
}
|
||||
|
||||
bool SimConfig::LoadFromFile(const std::string& filename) {
|
||||
std::ifstream file(filename);
|
||||
if (!file.is_open()) {
|
||||
std::cout << "Warning: Could not open config file '" << filename << "', using defaults" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string line;
|
||||
while (std::getline(file, line)) {
|
||||
// Skip comments and empty lines
|
||||
if (line.empty() || line[0] == '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
size_t equalsPos = line.find('=');
|
||||
if (equalsPos == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string key = line.substr(0, equalsPos);
|
||||
std::string value = line.substr(equalsPos + 1);
|
||||
|
||||
// Trim whitespace
|
||||
key.erase(0, key.find_first_not_of(" \t"));
|
||||
key.erase(key.find_last_not_of(" \t") + 1);
|
||||
value.erase(0, value.find_first_not_of(" \t"));
|
||||
value.erase(value.find_last_not_of(" \t") + 1);
|
||||
|
||||
// Parse configuration values
|
||||
if (key == "auth_server_ip") {
|
||||
m_AuthServerIP = value;
|
||||
} else if (key == "auth_server_port") {
|
||||
m_AuthServerPort = static_cast<uint16_t>(std::stoul(value));
|
||||
} else if (key == "world_server_ip") {
|
||||
m_WorldServerIP = value;
|
||||
} else if (key == "world_server_port") {
|
||||
m_WorldServerPort = static_cast<uint16_t>(std::stoul(value));
|
||||
} else if (key == "client_count") {
|
||||
m_ClientCount = static_cast<uint32_t>(std::stoul(value));
|
||||
} else if (key == "spawn_delay_ms") {
|
||||
m_SpawnDelayMs = static_cast<uint32_t>(std::stoul(value));
|
||||
} else if (key == "tick_rate_ms") {
|
||||
m_TickRateMs = static_cast<uint32_t>(std::stoul(value));
|
||||
} else if (key == "enable_movement") {
|
||||
m_EnableMovement = (value == "1" || value == "true");
|
||||
} else if (key == "enable_chat") {
|
||||
m_EnableChat = (value == "1" || value == "true");
|
||||
} else if (key == "enable_random_actions") {
|
||||
m_EnableRandomActions = (value == "1" || value == "true");
|
||||
} else if (key == "action_interval_ms") {
|
||||
m_ActionIntervalMs = static_cast<uint32_t>(std::stoul(value));
|
||||
} else if (key == "verbose_logging") {
|
||||
m_VerboseLogging = (value == "1" || value == "true");
|
||||
} else if (key == "log_file") {
|
||||
m_LogFile = value;
|
||||
} else if (key.substr(0, 12) == "test_account") {
|
||||
// Parse test_account_1=username:password format
|
||||
size_t colonPos = value.find(':');
|
||||
if (colonPos != std::string::npos) {
|
||||
std::string username = value.substr(0, colonPos);
|
||||
std::string password = value.substr(colonPos + 1);
|
||||
|
||||
// Check if this account already exists
|
||||
bool found = false;
|
||||
for (auto& account : m_TestAccounts) {
|
||||
if (account.first == username) {
|
||||
account.second = password;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
m_TestAccounts.push_back(std::make_pair(username, password));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
file.close();
|
||||
return true;
|
||||
}
|
72
dSimClient/SimConfig.h
Normal file
72
dSimClient/SimConfig.h
Normal file
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
/**
|
||||
* @brief Configuration class for simulation client
|
||||
*/
|
||||
class SimConfig {
|
||||
public:
|
||||
SimConfig();
|
||||
~SimConfig();
|
||||
|
||||
// Load configuration from file
|
||||
bool LoadFromFile(const std::string& filename);
|
||||
|
||||
// Server connection settings
|
||||
std::string GetAuthServerIP() const { return m_AuthServerIP; }
|
||||
uint16_t GetAuthServerPort() const { return m_AuthServerPort; }
|
||||
std::string GetWorldServerIP() const { return m_WorldServerIP; }
|
||||
uint16_t GetWorldServerPort() const { return m_WorldServerPort; }
|
||||
|
||||
// Client simulation settings
|
||||
uint32_t GetClientCount() const { return m_ClientCount; }
|
||||
uint32_t GetSpawnDelayMs() const { return m_SpawnDelayMs; }
|
||||
uint32_t GetTickRateMs() const { return m_TickRateMs; }
|
||||
|
||||
// Test behavior settings
|
||||
bool GetEnableMovement() const { return m_EnableMovement; }
|
||||
bool GetEnableChat() const { return m_EnableChat; }
|
||||
bool GetEnableRandomActions() const { return m_EnableRandomActions; }
|
||||
uint32_t GetActionIntervalMs() const { return m_ActionIntervalMs; }
|
||||
|
||||
// Authentication settings
|
||||
std::vector<std::pair<std::string, std::string>> GetTestAccounts() const { return m_TestAccounts; }
|
||||
|
||||
// Logging settings
|
||||
bool GetVerboseLogging() const { return m_VerboseLogging; }
|
||||
std::string GetLogFile() const { return m_LogFile; }
|
||||
|
||||
// Setters for programmatic configuration
|
||||
void SetAuthServer(const std::string& ip, uint16_t port) { m_AuthServerIP = ip; m_AuthServerPort = port; }
|
||||
void SetWorldServer(const std::string& ip, uint16_t port) { m_WorldServerIP = ip; m_WorldServerPort = port; }
|
||||
void SetClientCount(uint32_t count) { m_ClientCount = count; }
|
||||
void SetVerboseLogging(bool enabled) { m_VerboseLogging = enabled; }
|
||||
|
||||
private:
|
||||
// Server connection
|
||||
std::string m_AuthServerIP;
|
||||
uint16_t m_AuthServerPort;
|
||||
std::string m_WorldServerIP;
|
||||
uint16_t m_WorldServerPort;
|
||||
|
||||
// Client simulation
|
||||
uint32_t m_ClientCount;
|
||||
uint32_t m_SpawnDelayMs;
|
||||
uint32_t m_TickRateMs;
|
||||
|
||||
// Test behavior
|
||||
bool m_EnableMovement;
|
||||
bool m_EnableChat;
|
||||
bool m_EnableRandomActions;
|
||||
uint32_t m_ActionIntervalMs;
|
||||
|
||||
// Authentication
|
||||
std::vector<std::pair<std::string, std::string>> m_TestAccounts;
|
||||
|
||||
// Logging
|
||||
bool m_VerboseLogging;
|
||||
std::string m_LogFile;
|
||||
};
|
413
dSimClient/SimUser.cpp
Normal file
413
dSimClient/SimUser.cpp
Normal file
@@ -0,0 +1,413 @@
|
||||
#include "SimUser.h"
|
||||
#include "RakPeerInterface.h"
|
||||
#include "RakNetworkFactory.h"
|
||||
#include "BitStream.h"
|
||||
#include "MessageIdentifiers.h"
|
||||
#include "Logger.h"
|
||||
#include "dNetCommon.h"
|
||||
#include "Game.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
SimUser::SimUser(uint32_t id, const std::string& username, const std::string& password)
|
||||
: m_ID(id)
|
||||
, m_Username(username)
|
||||
, m_Password(password)
|
||||
, m_State(SimUserState::DISCONNECTED)
|
||||
, m_AuthPeer(nullptr)
|
||||
, m_WorldPeer(nullptr)
|
||||
, m_CharacterID(LWOOBJID_EMPTY)
|
||||
, m_WorldInstanceID(0)
|
||||
, m_ZoneID(1000) // Default to Venture Explorer
|
||||
, m_Position(NiPoint3::ZERO)
|
||||
, m_Rotation(NiQuaternion::IDENTITY)
|
||||
, m_Velocity(NiPoint3::ZERO)
|
||||
, m_TargetPosition(NiPoint3::ZERO)
|
||||
, m_IsMoving(false)
|
||||
, m_PacketsSent(0)
|
||||
, m_PacketsReceived(0)
|
||||
, m_BytesSent(0)
|
||||
, m_BytesReceived(0)
|
||||
, m_HasError(false)
|
||||
, m_RandomEngine(std::random_device{}())
|
||||
, m_RandomFloat(0.0f, 1.0f)
|
||||
{
|
||||
m_LastUpdate = std::chrono::steady_clock::now();
|
||||
m_LastHeartbeat = std::chrono::steady_clock::now();
|
||||
m_LastRandomAction = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
SimUser::~SimUser() {
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
bool SimUser::ConnectToAuth(const std::string& authIP, uint16_t authPort) {
|
||||
if (m_State != SimUserState::DISCONNECTED) {
|
||||
SetError("Cannot connect to auth server: already connected or in error state");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_AuthPeer = RakNetworkFactory::GetRakPeerInterface();
|
||||
if (!m_AuthPeer) {
|
||||
SetError("Failed to create RakPeer interface for auth connection");
|
||||
return false;
|
||||
}
|
||||
|
||||
SocketDescriptor socketDescriptor(0, nullptr);
|
||||
if (m_AuthPeer->Startup(1, 30, &socketDescriptor, 1) != RAKNET_STARTED) {
|
||||
SetError("Failed to startup auth peer");
|
||||
RakNetworkFactory::DestroyRakPeerInterface(m_AuthPeer);
|
||||
m_AuthPeer = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_AuthPeer->Connect(authIP.c_str(), authPort, nullptr, 0) != CONNECTION_ATTEMPT_STARTED) {
|
||||
SetError("Failed to start connection attempt to auth server");
|
||||
m_AuthPeer->Shutdown(0);
|
||||
RakNetworkFactory::DestroyRakPeerInterface(m_AuthPeer);
|
||||
m_AuthPeer = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_AuthServerAddr = SystemAddress(authIP.c_str(), authPort);
|
||||
SetState(SimUserState::CONNECTING_TO_AUTH);
|
||||
LogMessage("Connecting to auth server at " + authIP + ":" + std::to_string(authPort));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SimUser::ConnectToWorld(const std::string& worldIP, uint16_t worldPort) {
|
||||
if (m_State != SimUserState::AUTHENTICATING && m_State != SimUserState::CONNECTING_TO_WORLD) {
|
||||
SetError("Cannot connect to world server: not in correct state");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_WorldPeer = RakNetworkFactory::GetRakPeerInterface();
|
||||
if (!m_WorldPeer) {
|
||||
SetError("Failed to create RakPeer interface for world connection");
|
||||
return false;
|
||||
}
|
||||
|
||||
SocketDescriptor socketDescriptor(0, nullptr);
|
||||
if (m_WorldPeer->Startup(1, 30, &socketDescriptor, 1) != RAKNET_STARTED) {
|
||||
SetError("Failed to startup world peer");
|
||||
RakNetworkFactory::DestroyRakPeerInterface(m_WorldPeer);
|
||||
m_WorldPeer = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_WorldPeer->Connect(worldIP.c_str(), worldPort, nullptr, 0) != CONNECTION_ATTEMPT_STARTED) {
|
||||
SetError("Failed to start connection attempt to world server");
|
||||
m_WorldPeer->Shutdown(0);
|
||||
RakNetworkFactory::DestroyRakPeerInterface(m_WorldPeer);
|
||||
m_WorldPeer = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_WorldServerAddr = SystemAddress(worldIP.c_str(), worldPort);
|
||||
SetState(SimUserState::CONNECTING_TO_WORLD);
|
||||
LogMessage("Connecting to world server at " + worldIP + ":" + std::to_string(worldPort));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SimUser::Disconnect() {
|
||||
if (m_AuthPeer) {
|
||||
m_AuthPeer->Shutdown(100);
|
||||
RakNetworkFactory::DestroyRakPeerInterface(m_AuthPeer);
|
||||
m_AuthPeer = nullptr;
|
||||
}
|
||||
|
||||
if (m_WorldPeer) {
|
||||
m_WorldPeer->Shutdown(100);
|
||||
RakNetworkFactory::DestroyRakPeerInterface(m_WorldPeer);
|
||||
m_WorldPeer = nullptr;
|
||||
}
|
||||
|
||||
SetState(SimUserState::DISCONNECTED);
|
||||
}
|
||||
|
||||
void SimUser::ProcessIncomingPackets() {
|
||||
// Process auth server packets
|
||||
if (m_AuthPeer) {
|
||||
Packet* packet = nullptr;
|
||||
while ((packet = m_AuthPeer->Receive()) != nullptr) {
|
||||
m_PacketsReceived++;
|
||||
m_BytesReceived += packet->length;
|
||||
|
||||
HandleAuthPacket(packet);
|
||||
m_AuthPeer->DeallocatePacket(packet);
|
||||
}
|
||||
}
|
||||
|
||||
// Process world server packets
|
||||
if (m_WorldPeer) {
|
||||
Packet* packet = nullptr;
|
||||
while ((packet = m_WorldPeer->Receive()) != nullptr) {
|
||||
m_PacketsReceived++;
|
||||
m_BytesReceived += packet->length;
|
||||
|
||||
HandleWorldPacket(packet);
|
||||
m_WorldPeer->DeallocatePacket(packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SimUser::SendHeartbeat() {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto timeSinceLastHeartbeat = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_LastHeartbeat);
|
||||
|
||||
if (timeSinceLastHeartbeat.count() >= 5000) { // Send heartbeat every 5 seconds
|
||||
// Send to appropriate server based on current state
|
||||
RakNet::BitStream heartbeat;
|
||||
heartbeat.Write(static_cast<MessageID>(ID_USER_PACKET_ENUM + 1)); // Simple heartbeat
|
||||
|
||||
if (m_State == SimUserState::IN_WORLD && m_WorldPeer) {
|
||||
m_WorldPeer->Send(&heartbeat, HIGH_PRIORITY, RELIABLE, 0, m_WorldServerAddr, false);
|
||||
m_PacketsSent++;
|
||||
m_BytesSent += heartbeat.GetNumberOfBytesUsed();
|
||||
}
|
||||
|
||||
m_LastHeartbeat = now;
|
||||
}
|
||||
}
|
||||
|
||||
void SimUser::Update(float deltaTime) {
|
||||
ProcessIncomingPackets();
|
||||
SendHeartbeat();
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto timeSinceLastAction = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_LastRandomAction);
|
||||
|
||||
// Perform random actions based on current state
|
||||
if (m_State == SimUserState::IN_WORLD && timeSinceLastAction.count() >= 5000) {
|
||||
SimulateRandomAction();
|
||||
m_LastRandomAction = now;
|
||||
}
|
||||
|
||||
// Update movement simulation
|
||||
if (m_IsMoving) {
|
||||
SimulateMovement();
|
||||
}
|
||||
}
|
||||
|
||||
void SimUser::SimulateMovement() {
|
||||
// Simple movement simulation - move towards target position
|
||||
const float moveSpeed = 5.0f; // units per second
|
||||
const float deltaTime = 0.016f; // Assuming 60 FPS
|
||||
|
||||
NiPoint3 direction = m_TargetPosition - m_Position;
|
||||
float distance = direction.Length();
|
||||
|
||||
if (distance > 0.1f) {
|
||||
direction = direction / distance; // Normalize
|
||||
NiPoint3 movement = direction * moveSpeed * deltaTime;
|
||||
|
||||
if (movement.Length() >= distance) {
|
||||
m_Position = m_TargetPosition;
|
||||
m_IsMoving = false;
|
||||
} else {
|
||||
m_Position = m_Position + movement;
|
||||
}
|
||||
|
||||
SendPositionUpdate();
|
||||
} else {
|
||||
m_IsMoving = false;
|
||||
}
|
||||
}
|
||||
|
||||
void SimUser::SimulateRandomAction() {
|
||||
if (m_State != SimUserState::IN_WORLD) return;
|
||||
|
||||
float action = m_RandomFloat(m_RandomEngine);
|
||||
|
||||
if (action < 0.6f) {
|
||||
// Generate random movement
|
||||
GenerateRandomMovement();
|
||||
} else if (action < 0.8f) {
|
||||
// Send a chat message (if enabled)
|
||||
SendChatMessage("Hello from simulated client " + std::to_string(m_ID));
|
||||
} else {
|
||||
// Just idle
|
||||
LogMessage("Idling...");
|
||||
}
|
||||
}
|
||||
|
||||
void SimUser::SendChatMessage(const std::string& message) {
|
||||
if (m_State != SimUserState::IN_WORLD || !m_WorldPeer) return;
|
||||
|
||||
// Create a simple chat message
|
||||
RakNet::BitStream chatBitStream;
|
||||
chatBitStream.Write(static_cast<MessageID>(ID_USER_PACKET_ENUM + 2));
|
||||
chatBitStream.Write(static_cast<uint8_t>(0)); // General chat channel
|
||||
chatBitStream.Write(static_cast<uint16_t>(0)); // Unknown
|
||||
|
||||
// Convert message to UTF-16
|
||||
std::u16string u16message;
|
||||
for (char c : message) {
|
||||
u16message.push_back(static_cast<char16_t>(c));
|
||||
}
|
||||
|
||||
chatBitStream.Write(static_cast<uint32_t>(u16message.length()));
|
||||
for (char16_t c : u16message) {
|
||||
chatBitStream.Write(c);
|
||||
}
|
||||
|
||||
m_WorldPeer->Send(&chatBitStream, HIGH_PRIORITY, RELIABLE_ORDERED, 0, m_WorldServerAddr, false);
|
||||
m_PacketsSent++;
|
||||
m_BytesSent += chatBitStream.GetNumberOfBytesUsed();
|
||||
|
||||
LogMessage("Sent chat message: " + message);
|
||||
}
|
||||
|
||||
void SimUser::GenerateRandomMovement() {
|
||||
// Generate a random target position within a reasonable area
|
||||
float randomX = (m_RandomFloat(m_RandomEngine) - 0.5f) * 20.0f; // -10 to 10
|
||||
float randomZ = (m_RandomFloat(m_RandomEngine) - 0.5f) * 20.0f; // -10 to 10
|
||||
|
||||
m_TargetPosition = m_Position + NiPoint3(randomX, 0, randomZ);
|
||||
m_IsMoving = true;
|
||||
|
||||
LogMessage("Started movement to (" + std::to_string(m_TargetPosition.x) + ", " +
|
||||
std::to_string(m_TargetPosition.y) + ", " + std::to_string(m_TargetPosition.z) + ")");
|
||||
}
|
||||
|
||||
void SimUser::SendPositionUpdate() {
|
||||
if (m_State != SimUserState::IN_WORLD || !m_WorldPeer) return;
|
||||
|
||||
// Create position update packet
|
||||
RakNet::BitStream positionBitStream;
|
||||
positionBitStream.Write(static_cast<MessageID>(ID_USER_PACKET_ENUM + 3)); // Position update
|
||||
positionBitStream.Write(m_Position.x);
|
||||
positionBitStream.Write(m_Position.y);
|
||||
positionBitStream.Write(m_Position.z);
|
||||
positionBitStream.Write(m_Rotation.x);
|
||||
positionBitStream.Write(m_Rotation.y);
|
||||
positionBitStream.Write(m_Rotation.z);
|
||||
positionBitStream.Write(m_Rotation.w);
|
||||
|
||||
m_WorldPeer->Send(&positionBitStream, HIGH_PRIORITY, UNRELIABLE_SEQUENCED, 0, m_WorldServerAddr, false);
|
||||
m_PacketsSent++;
|
||||
m_BytesSent += positionBitStream.GetNumberOfBytesUsed();
|
||||
}
|
||||
|
||||
void SimUser::HandleAuthPacket(Packet* packet) {
|
||||
switch (packet->data[0]) {
|
||||
case ID_CONNECTION_REQUEST_ACCEPTED:
|
||||
LogMessage("Connected to auth server");
|
||||
SendLoginRequest();
|
||||
break;
|
||||
|
||||
case ID_DISCONNECTION_NOTIFICATION:
|
||||
case ID_CONNECTION_LOST:
|
||||
LogMessage("Disconnected from auth server");
|
||||
SetError("Lost connection to auth server");
|
||||
break;
|
||||
|
||||
default:
|
||||
// Handle auth-specific packets
|
||||
if (packet->data[0] >= ID_USER_PACKET_ENUM) {
|
||||
HandleLoginResponse(packet);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SimUser::HandleWorldPacket(Packet* packet) {
|
||||
switch (packet->data[0]) {
|
||||
case ID_CONNECTION_REQUEST_ACCEPTED:
|
||||
LogMessage("Connected to world server");
|
||||
SendWorldLoginRequest();
|
||||
break;
|
||||
|
||||
case ID_DISCONNECTION_NOTIFICATION:
|
||||
case ID_CONNECTION_LOST:
|
||||
LogMessage("Disconnected from world server");
|
||||
SetError("Lost connection to world server");
|
||||
break;
|
||||
|
||||
default:
|
||||
// Handle world-specific packets
|
||||
if (packet->data[0] >= ID_USER_PACKET_ENUM) {
|
||||
// For simplicity, just log that we received a world packet
|
||||
LogMessage("Received world packet of type " + std::to_string(packet->data[0]));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SimUser::SendLoginRequest() {
|
||||
if (!m_AuthPeer) return;
|
||||
|
||||
SetState(SimUserState::AUTHENTICATING);
|
||||
|
||||
// Create login request packet
|
||||
RakNet::BitStream loginBitStream;
|
||||
loginBitStream.Write(static_cast<MessageID>(ID_USER_PACKET_ENUM));
|
||||
loginBitStream.Write(static_cast<uint32_t>(m_Username.length()));
|
||||
loginBitStream.Write(m_Username.c_str(), m_Username.length());
|
||||
loginBitStream.Write(static_cast<uint32_t>(m_Password.length()));
|
||||
loginBitStream.Write(m_Password.c_str(), m_Password.length());
|
||||
|
||||
m_AuthPeer->Send(&loginBitStream, HIGH_PRIORITY, RELIABLE, 0, m_AuthServerAddr, false);
|
||||
m_PacketsSent++;
|
||||
m_BytesSent += loginBitStream.GetNumberOfBytesUsed();
|
||||
|
||||
LogMessage("Sent login request for user: " + m_Username);
|
||||
}
|
||||
|
||||
void SimUser::HandleLoginResponse(Packet* packet) {
|
||||
// Simple login response handling
|
||||
LogMessage("Received login response");
|
||||
|
||||
// For simulation purposes, assume successful login and move to world
|
||||
m_SessionKey = "sim_session_" + std::to_string(m_ID);
|
||||
|
||||
// Simulate connecting to world server (hardcoded for now)
|
||||
ConnectToWorld("127.0.0.1", 2000);
|
||||
}
|
||||
|
||||
void SimUser::SendWorldLoginRequest() {
|
||||
if (!m_WorldPeer) return;
|
||||
|
||||
SetState(SimUserState::LOADING_WORLD);
|
||||
|
||||
// Create world login request
|
||||
RakNet::BitStream worldLoginBitStream;
|
||||
worldLoginBitStream.Write(static_cast<MessageID>(ID_USER_PACKET_ENUM + 10));
|
||||
worldLoginBitStream.Write(static_cast<uint32_t>(m_SessionKey.length()));
|
||||
worldLoginBitStream.Write(m_SessionKey.c_str(), m_SessionKey.length());
|
||||
|
||||
m_WorldPeer->Send(&worldLoginBitStream, HIGH_PRIORITY, RELIABLE, 0, m_WorldServerAddr, false);
|
||||
m_PacketsSent++;
|
||||
m_BytesSent += worldLoginBitStream.GetNumberOfBytesUsed();
|
||||
|
||||
LogMessage("Sent world login request");
|
||||
|
||||
// For simulation, immediately consider ourselves in world
|
||||
SetState(SimUserState::IN_WORLD);
|
||||
m_Position = NiPoint3(0, 0, 0); // Start at origin
|
||||
LogMessage("Entered world simulation mode");
|
||||
}
|
||||
|
||||
void SimUser::SetError(const std::string& error) {
|
||||
m_HasError = true;
|
||||
m_LastError = error;
|
||||
m_State = SimUserState::ERROR_STATE;
|
||||
LogMessage("ERROR: " + error);
|
||||
}
|
||||
|
||||
void SimUser::LogMessage(const std::string& message) {
|
||||
if (Game::logger) {
|
||||
LOG("[SimUser %u (%s)] %s", m_ID, m_Username.c_str(), message.c_str());
|
||||
} else {
|
||||
std::cout << "[SimUser " << m_ID << " (" << m_Username << ")] " << message << std::endl;
|
||||
}
|
||||
}
|
139
dSimClient/SimUser.h
Normal file
139
dSimClient/SimUser.h
Normal file
@@ -0,0 +1,139 @@
|
||||
#pragma once
|
||||
|
||||
#include "dCommonVars.h"
|
||||
#include "RakNetTypes.h"
|
||||
#include "RakPeerInterface.h"
|
||||
#include "NiPoint3.h"
|
||||
#include "NiQuaternion.h"
|
||||
#include <string>
|
||||
#include <chrono>
|
||||
#include <random>
|
||||
|
||||
enum class SimUserState {
|
||||
DISCONNECTED,
|
||||
CONNECTING_TO_AUTH,
|
||||
AUTHENTICATING,
|
||||
CONNECTING_TO_WORLD,
|
||||
LOADING_WORLD,
|
||||
IN_WORLD,
|
||||
DISCONNECTING,
|
||||
ERROR_STATE
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Represents a simulated user/client connection
|
||||
*/
|
||||
class SimUser {
|
||||
public:
|
||||
SimUser(uint32_t id, const std::string& username, const std::string& password);
|
||||
~SimUser();
|
||||
|
||||
// Connection management
|
||||
bool ConnectToAuth(const std::string& authIP, uint16_t authPort);
|
||||
bool ConnectToWorld(const std::string& worldIP, uint16_t worldPort);
|
||||
void Disconnect();
|
||||
|
||||
// State management
|
||||
SimUserState GetState() const { return m_State; }
|
||||
void SetState(SimUserState state) { m_State = state; }
|
||||
|
||||
// User info
|
||||
uint32_t GetID() const { return m_ID; }
|
||||
const std::string& GetUsername() const { return m_Username; }
|
||||
const std::string& GetPassword() const { return m_Password; }
|
||||
|
||||
// Network handling
|
||||
void ProcessIncomingPackets();
|
||||
void SendHeartbeat();
|
||||
void Update(float deltaTime);
|
||||
|
||||
// Simulation behaviors
|
||||
void SimulateMovement();
|
||||
void SimulateRandomAction();
|
||||
void SendChatMessage(const std::string& message);
|
||||
|
||||
// Position and movement
|
||||
void SetPosition(const NiPoint3& position) { m_Position = position; }
|
||||
void SetRotation(const NiQuaternion& rotation) { m_Rotation = rotation; }
|
||||
const NiPoint3& GetPosition() const { return m_Position; }
|
||||
const NiQuaternion& GetRotation() const { return m_Rotation; }
|
||||
|
||||
// Statistics
|
||||
uint64_t GetPacketsSent() const { return m_PacketsSent; }
|
||||
uint64_t GetPacketsReceived() const { return m_PacketsReceived; }
|
||||
uint64_t GetBytesReceived() const { return m_BytesReceived; }
|
||||
uint64_t GetBytesSent() const { return m_BytesSent; }
|
||||
|
||||
// Error handling
|
||||
bool HasError() const { return m_HasError; }
|
||||
const std::string& GetLastError() const { return m_LastError; }
|
||||
|
||||
private:
|
||||
// Helper methods
|
||||
void HandleAuthPacket(Packet* packet);
|
||||
void HandleWorldPacket(Packet* packet);
|
||||
void SetError(const std::string& error);
|
||||
void LogMessage(const std::string& message);
|
||||
|
||||
// Authentication flow
|
||||
void SendLoginRequest();
|
||||
void HandleLoginResponse(Packet* packet);
|
||||
void HandleWorldServerInfo(Packet* packet);
|
||||
|
||||
// World flow
|
||||
void SendWorldLoginRequest();
|
||||
void HandleWorldLoginResponse(Packet* packet);
|
||||
void SendCharacterListRequest();
|
||||
void HandleCharacterListResponse(Packet* packet);
|
||||
void SendCharacterSelection();
|
||||
void HandleMinifigureListResponse(Packet* packet);
|
||||
void SendWorldReady();
|
||||
|
||||
// Movement simulation
|
||||
void GenerateRandomMovement();
|
||||
void SendPositionUpdate();
|
||||
|
||||
// Member variables
|
||||
uint32_t m_ID;
|
||||
std::string m_Username;
|
||||
std::string m_Password;
|
||||
SimUserState m_State;
|
||||
|
||||
// Network
|
||||
RakPeerInterface* m_AuthPeer;
|
||||
RakPeerInterface* m_WorldPeer;
|
||||
SystemAddress m_AuthServerAddr;
|
||||
SystemAddress m_WorldServerAddr;
|
||||
|
||||
// Session data
|
||||
std::string m_SessionKey;
|
||||
LWOOBJID m_CharacterID;
|
||||
uint32_t m_WorldInstanceID;
|
||||
uint32_t m_ZoneID;
|
||||
|
||||
// Position and movement
|
||||
NiPoint3 m_Position;
|
||||
NiQuaternion m_Rotation;
|
||||
NiPoint3 m_Velocity;
|
||||
NiPoint3 m_TargetPosition;
|
||||
bool m_IsMoving;
|
||||
|
||||
// Timing
|
||||
std::chrono::steady_clock::time_point m_LastUpdate;
|
||||
std::chrono::steady_clock::time_point m_LastHeartbeat;
|
||||
std::chrono::steady_clock::time_point m_LastRandomAction;
|
||||
|
||||
// Statistics
|
||||
uint64_t m_PacketsSent;
|
||||
uint64_t m_PacketsReceived;
|
||||
uint64_t m_BytesSent;
|
||||
uint64_t m_BytesReceived;
|
||||
|
||||
// Error handling
|
||||
bool m_HasError;
|
||||
std::string m_LastError;
|
||||
|
||||
// Random number generation
|
||||
mutable std::mt19937 m_RandomEngine;
|
||||
mutable std::uniform_real_distribution<float> m_RandomFloat;
|
||||
};
|
337
dSimClient/SimWorld.cpp
Normal file
337
dSimClient/SimWorld.cpp
Normal file
@@ -0,0 +1,337 @@
|
||||
#include "SimWorld.h"
|
||||
#include "SimUser.h"
|
||||
#include "SimConfig.h"
|
||||
#include "Logger.h"
|
||||
#include "Game.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
#include <thread>
|
||||
#include <random>
|
||||
#include <iomanip>
|
||||
|
||||
SimWorld::SimWorld()
|
||||
: m_NextUserID(1)
|
||||
, m_Running(false)
|
||||
, m_Initialized(false)
|
||||
, m_TotalUsersSpawned(0)
|
||||
, m_TotalUsersDisconnected(0)
|
||||
, m_TotalErrors(0)
|
||||
{
|
||||
m_StartTime = std::chrono::steady_clock::now();
|
||||
m_LastSpawn = std::chrono::steady_clock::now();
|
||||
m_LastStatsPrint = std::chrono::steady_clock::now();
|
||||
}
|
||||
|
||||
SimWorld::~SimWorld() {
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
bool SimWorld::Initialize(std::shared_ptr<SimConfig> config) {
|
||||
if (m_Initialized) {
|
||||
std::cout << "SimWorld already initialized" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_Config = config;
|
||||
m_Initialized = true;
|
||||
|
||||
std::cout << "SimWorld initialized with configuration:" << std::endl;
|
||||
std::cout << " Auth Server: " << m_Config->GetAuthServerIP() << ":" << m_Config->GetAuthServerPort() << std::endl;
|
||||
std::cout << " World Server: " << m_Config->GetWorldServerIP() << ":" << m_Config->GetWorldServerPort() << std::endl;
|
||||
std::cout << " Client Count: " << m_Config->GetClientCount() << std::endl;
|
||||
std::cout << " Spawn Delay: " << m_Config->GetSpawnDelayMs() << "ms" << std::endl;
|
||||
std::cout << " Tick Rate: " << m_Config->GetTickRateMs() << "ms" << std::endl;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SimWorld::Shutdown() {
|
||||
if (!m_Initialized) return;
|
||||
|
||||
m_Running = false;
|
||||
|
||||
// Disconnect all users
|
||||
std::cout << "Shutting down simulation, disconnecting " << m_Users.size() << " users..." << std::endl;
|
||||
|
||||
for (auto& user : m_Users) {
|
||||
user->Disconnect();
|
||||
}
|
||||
|
||||
m_Users.clear();
|
||||
m_Initialized = false;
|
||||
|
||||
std::cout << "SimWorld shutdown complete" << std::endl;
|
||||
}
|
||||
|
||||
void SimWorld::Run() {
|
||||
if (!m_Initialized) {
|
||||
std::cout << "SimWorld not initialized, cannot run" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
m_Running = true;
|
||||
std::cout << "Starting simulation..." << std::endl;
|
||||
|
||||
auto lastUpdate = std::chrono::steady_clock::now();
|
||||
const auto tickRate = std::chrono::milliseconds(m_Config->GetTickRateMs());
|
||||
|
||||
while (m_Running) {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto deltaTime = std::chrono::duration<float>(now - lastUpdate).count();
|
||||
lastUpdate = now;
|
||||
|
||||
Update(deltaTime);
|
||||
|
||||
// Print statistics every 10 seconds
|
||||
auto timeSinceLastStats = std::chrono::duration_cast<std::chrono::seconds>(now - m_LastStatsPrint);
|
||||
if (timeSinceLastStats.count() >= 10) {
|
||||
PrintStatistics();
|
||||
m_LastStatsPrint = now;
|
||||
}
|
||||
|
||||
// Sleep to maintain tick rate
|
||||
std::this_thread::sleep_for(tickRate);
|
||||
}
|
||||
|
||||
std::cout << "Simulation stopped" << std::endl;
|
||||
}
|
||||
|
||||
void SimWorld::Update(float deltaTime) {
|
||||
SpawnUsersGradually();
|
||||
CheckUserStates();
|
||||
HandleUserErrors();
|
||||
|
||||
// Update all users
|
||||
for (auto& user : m_Users) {
|
||||
user->Update(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
void SimWorld::SpawnUsersGradually() {
|
||||
if (m_Users.size() >= m_Config->GetClientCount()) {
|
||||
return; // Already have enough users
|
||||
}
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto timeSinceLastSpawn = std::chrono::duration_cast<std::chrono::milliseconds>(now - m_LastSpawn);
|
||||
|
||||
if (timeSinceLastSpawn.count() >= m_Config->GetSpawnDelayMs()) {
|
||||
// Get credentials from config or generate random ones
|
||||
std::string username, password;
|
||||
|
||||
auto testAccounts = m_Config->GetTestAccounts();
|
||||
if (m_TotalUsersSpawned < testAccounts.size()) {
|
||||
// Use predefined test account
|
||||
username = testAccounts[m_TotalUsersSpawned].first;
|
||||
password = testAccounts[m_TotalUsersSpawned].second;
|
||||
} else {
|
||||
// Generate random credentials
|
||||
username = GenerateRandomUsername();
|
||||
password = GenerateRandomPassword();
|
||||
}
|
||||
|
||||
if (SpawnUser(username, password)) {
|
||||
m_LastSpawn = now;
|
||||
m_TotalUsersSpawned++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool SimWorld::SpawnUser(const std::string& username, const std::string& password) {
|
||||
auto user = std::make_unique<SimUser>(m_NextUserID++, username, password);
|
||||
|
||||
if (!user->ConnectToAuth(m_Config->GetAuthServerIP(), m_Config->GetAuthServerPort())) {
|
||||
std::cout << "Failed to connect user " << username << " to auth server" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_Users.push_back(std::move(user));
|
||||
|
||||
if (m_Config->GetVerboseLogging()) {
|
||||
std::cout << "Spawned user: " << username << " (Total: " << m_Users.size() << ")" << std::endl;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void SimWorld::RemoveUser(uint32_t userID) {
|
||||
auto it = std::remove_if(m_Users.begin(), m_Users.end(),
|
||||
[userID](const std::unique_ptr<SimUser>& user) {
|
||||
return user->GetID() == userID;
|
||||
});
|
||||
|
||||
if (it != m_Users.end()) {
|
||||
(*it)->Disconnect();
|
||||
m_Users.erase(it, m_Users.end());
|
||||
m_TotalUsersDisconnected++;
|
||||
}
|
||||
}
|
||||
|
||||
void SimWorld::CheckUserStates() {
|
||||
// Remove users that have been in error state for too long
|
||||
auto it = std::remove_if(m_Users.begin(), m_Users.end(),
|
||||
[this](const std::unique_ptr<SimUser>& user) {
|
||||
if (user->GetState() == SimUserState::ERROR_STATE) {
|
||||
if (m_Config->GetVerboseLogging()) {
|
||||
std::cout << "Removing user " << user->GetUsername()
|
||||
<< " due to error: " << user->GetLastError() << std::endl;
|
||||
}
|
||||
m_TotalErrors++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (it != m_Users.end()) {
|
||||
m_Users.erase(it, m_Users.end());
|
||||
}
|
||||
}
|
||||
|
||||
void SimWorld::HandleUserErrors() {
|
||||
uint32_t errorCount = GetErrorUserCount();
|
||||
if (errorCount > 0 && m_Config->GetVerboseLogging()) {
|
||||
std::cout << "Warning: " << errorCount << " users in error state" << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void SimWorld::PrintStatistics() {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto uptime = std::chrono::duration_cast<std::chrono::seconds>(now - m_StartTime);
|
||||
|
||||
std::cout << std::endl;
|
||||
std::cout << "=== Simulation Statistics ===" << std::endl;
|
||||
std::cout << "Uptime: " << uptime.count() << " seconds" << std::endl;
|
||||
std::cout << "Active Users: " << GetActiveUserCount() << std::endl;
|
||||
std::cout << "Connected Users: " << GetConnectedUserCount() << std::endl;
|
||||
std::cout << "Total Spawned: " << m_TotalUsersSpawned << std::endl;
|
||||
std::cout << "Total Disconnected: " << m_TotalUsersDisconnected << std::endl;
|
||||
std::cout << "Total Errors: " << m_TotalErrors << std::endl;
|
||||
std::cout << "Total Packets Sent: " << GetTotalPacketsSent() << std::endl;
|
||||
std::cout << "Total Packets Received: " << GetTotalPacketsReceived() << std::endl;
|
||||
std::cout << "Total Bytes Sent: " << GetTotalBytesSent() << std::endl;
|
||||
std::cout << "Total Bytes Received: " << GetTotalBytesReceived() << std::endl;
|
||||
|
||||
if (uptime.count() > 0) {
|
||||
std::cout << "Avg Packets/sec Sent: " << (GetTotalPacketsSent() / uptime.count()) << std::endl;
|
||||
std::cout << "Avg Packets/sec Received: " << (GetTotalPacketsReceived() / uptime.count()) << std::endl;
|
||||
std::cout << "Avg Bytes/sec Sent: " << (GetTotalBytesSent() / uptime.count()) << std::endl;
|
||||
std::cout << "Avg Bytes/sec Received: " << (GetTotalBytesReceived() / uptime.count()) << std::endl;
|
||||
}
|
||||
std::cout << "=============================" << std::endl;
|
||||
std::cout << std::endl;
|
||||
}
|
||||
|
||||
uint64_t SimWorld::GetTotalPacketsSent() const {
|
||||
uint64_t total = 0;
|
||||
for (const auto& user : m_Users) {
|
||||
total += user->GetPacketsSent();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
uint64_t SimWorld::GetTotalPacketsReceived() const {
|
||||
uint64_t total = 0;
|
||||
for (const auto& user : m_Users) {
|
||||
total += user->GetPacketsReceived();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
uint64_t SimWorld::GetTotalBytesSent() const {
|
||||
uint64_t total = 0;
|
||||
for (const auto& user : m_Users) {
|
||||
total += user->GetBytesSent();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
uint64_t SimWorld::GetTotalBytesReceived() const {
|
||||
uint64_t total = 0;
|
||||
for (const auto& user : m_Users) {
|
||||
total += user->GetBytesReceived();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
uint32_t SimWorld::GetConnectedUserCount() const {
|
||||
uint32_t count = 0;
|
||||
for (const auto& user : m_Users) {
|
||||
if (user->GetState() == SimUserState::IN_WORLD ||
|
||||
user->GetState() == SimUserState::LOADING_WORLD ||
|
||||
user->GetState() == SimUserState::CONNECTING_TO_WORLD ||
|
||||
user->GetState() == SimUserState::AUTHENTICATING) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
uint32_t SimWorld::GetErrorUserCount() const {
|
||||
uint32_t count = 0;
|
||||
for (const auto& user : m_Users) {
|
||||
if (user->GetState() == SimUserState::ERROR_STATE) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
std::string SimWorld::GenerateRandomUsername() {
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
static std::uniform_int_distribution<> dis(1000, 9999);
|
||||
|
||||
return "simuser" + std::to_string(dis(gen));
|
||||
}
|
||||
|
||||
std::string SimWorld::GenerateRandomPassword() {
|
||||
static std::random_device rd;
|
||||
static std::mt19937 gen(rd());
|
||||
static std::uniform_int_distribution<> dis(10000, 99999);
|
||||
|
||||
return "simpass" + std::to_string(dis(gen));
|
||||
}
|
||||
|
||||
void SimWorld::RunLoadTest() {
|
||||
std::cout << "Running load test with " << m_Config->GetClientCount() << " clients..." << std::endl;
|
||||
Run();
|
||||
}
|
||||
|
||||
void SimWorld::RunStressTest() {
|
||||
std::cout << "Running stress test..." << std::endl;
|
||||
|
||||
// Temporarily increase client count for stress testing
|
||||
uint32_t originalCount = m_Config->GetClientCount();
|
||||
m_Config->SetClientCount(originalCount * 2);
|
||||
|
||||
Run();
|
||||
|
||||
// Restore original count
|
||||
m_Config->SetClientCount(originalCount);
|
||||
}
|
||||
|
||||
void SimWorld::RunBasicConnectivityTest() {
|
||||
std::cout << "Running basic connectivity test..." << std::endl;
|
||||
|
||||
// Set to 1 client for basic test
|
||||
uint32_t originalCount = m_Config->GetClientCount();
|
||||
m_Config->SetClientCount(1);
|
||||
|
||||
Run();
|
||||
|
||||
// Restore original count
|
||||
m_Config->SetClientCount(originalCount);
|
||||
}
|
||||
|
||||
void SimWorld::RunMovementTest() {
|
||||
std::cout << "Running movement test..." << std::endl;
|
||||
// Movement is handled automatically in the user simulation
|
||||
Run();
|
||||
}
|
||||
|
||||
void SimWorld::RunChatTest() {
|
||||
std::cout << "Running chat test..." << std::endl;
|
||||
// Chat is handled automatically in the user simulation
|
||||
Run();
|
||||
}
|
76
dSimClient/SimWorld.h
Normal file
76
dSimClient/SimWorld.h
Normal file
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <chrono>
|
||||
|
||||
class SimUser;
|
||||
class SimConfig;
|
||||
|
||||
/**
|
||||
* @brief Manages the simulation of multiple users and test behaviors
|
||||
*/
|
||||
class SimWorld {
|
||||
public:
|
||||
SimWorld();
|
||||
~SimWorld();
|
||||
|
||||
// Initialization
|
||||
bool Initialize(std::shared_ptr<SimConfig> config);
|
||||
void Shutdown();
|
||||
|
||||
// Main simulation loop
|
||||
void Run();
|
||||
void Update(float deltaTime);
|
||||
|
||||
// User management
|
||||
bool SpawnUser(const std::string& username, const std::string& password);
|
||||
void RemoveUser(uint32_t userID);
|
||||
size_t GetActiveUserCount() const { return m_Users.size(); }
|
||||
|
||||
// Test scenarios
|
||||
void RunLoadTest();
|
||||
void RunStressTest();
|
||||
void RunBasicConnectivityTest();
|
||||
void RunMovementTest();
|
||||
void RunChatTest();
|
||||
|
||||
// Statistics
|
||||
void PrintStatistics();
|
||||
uint64_t GetTotalPacketsSent() const;
|
||||
uint64_t GetTotalPacketsReceived() const;
|
||||
uint64_t GetTotalBytesSent() const;
|
||||
uint64_t GetTotalBytesReceived() const;
|
||||
uint32_t GetConnectedUserCount() const;
|
||||
uint32_t GetErrorUserCount() const;
|
||||
|
||||
// Control
|
||||
void StopSimulation() { m_Running = false; }
|
||||
bool IsRunning() const { return m_Running; }
|
||||
|
||||
private:
|
||||
// Helper methods
|
||||
void SpawnUsersGradually();
|
||||
void CheckUserStates();
|
||||
void HandleUserErrors();
|
||||
std::string GenerateRandomUsername();
|
||||
std::string GenerateRandomPassword();
|
||||
|
||||
// Member variables
|
||||
std::shared_ptr<SimConfig> m_Config;
|
||||
std::vector<std::unique_ptr<SimUser>> m_Users;
|
||||
uint32_t m_NextUserID;
|
||||
bool m_Running;
|
||||
bool m_Initialized;
|
||||
|
||||
// Timing
|
||||
std::chrono::steady_clock::time_point m_LastSpawn;
|
||||
std::chrono::steady_clock::time_point m_StartTime;
|
||||
std::chrono::steady_clock::time_point m_LastStatsPrint;
|
||||
|
||||
// Statistics
|
||||
uint32_t m_TotalUsersSpawned;
|
||||
uint32_t m_TotalUsersDisconnected;
|
||||
uint32_t m_TotalErrors;
|
||||
};
|
344
docs/SimClient_Integration.md
Normal file
344
docs/SimClient_Integration.md
Normal file
@@ -0,0 +1,344 @@
|
||||
# SimClient Integration Guide
|
||||
|
||||
This guide explains how the SimClient has been integrated into the DarkflameServer project and how to use it effectively.
|
||||
|
||||
## Integration Overview
|
||||
|
||||
The SimClient has been added as a new executable target to the existing DarkflameServer build system. It's designed to work alongside the existing server components for testing and load testing purposes.
|
||||
|
||||
### Files Added
|
||||
|
||||
#### Core SimClient Files
|
||||
- `dSimClient/` - New directory containing all SimClient source code
|
||||
- `dSimClient/CMakeLists.txt` - Build configuration for SimClient
|
||||
- `dSimClient/SimClient.cpp` - Main executable entry point
|
||||
- `dSimClient/SimUser.h/cpp` - Individual simulated user implementation
|
||||
- `dSimClient/SimWorld.h/cpp` - Simulation coordinator and manager
|
||||
- `dSimClient/SimBehavior.h/cpp` - Behavior patterns and test scenarios
|
||||
- `dSimClient/SimConfig.h/cpp` - Configuration management
|
||||
- `dSimClient/README.md` - Detailed SimClient documentation
|
||||
|
||||
#### Configuration and Build Files
|
||||
- `resources/simclient.ini` - Default configuration file
|
||||
- `build_simclient.sh` - Linux/macOS build script
|
||||
- `build_simclient.bat` - Windows build script
|
||||
|
||||
#### Modified Files
|
||||
- `CMakeLists.txt` - Added SimClient subdirectory to main build
|
||||
|
||||
## Build Integration
|
||||
|
||||
The SimClient is built as part of the normal DarkflameServer build process:
|
||||
|
||||
```bash
|
||||
# Standard build process includes SimClient
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
make # Builds all targets including SimClient
|
||||
|
||||
# Or build just SimClient
|
||||
make SimClient
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
|
||||
SimClient reuses existing DarkflameServer libraries:
|
||||
- `dCommon` - Common utilities and data types
|
||||
- `dDatabase` - Database connectivity (minimal usage)
|
||||
- `dNet` - Network communication and RakNet integration
|
||||
- `dServer` - Server utilities and logging
|
||||
- `raknet` - RakNet networking library
|
||||
|
||||
This ensures consistency with the main server codebase and reduces maintenance overhead.
|
||||
|
||||
## Usage Scenarios
|
||||
|
||||
### Development Testing
|
||||
|
||||
During development, developers can use SimClient to quickly test server functionality:
|
||||
|
||||
```bash
|
||||
# Quick connectivity test
|
||||
./SimClient -t basic -v
|
||||
|
||||
# Test new features with multiple clients
|
||||
./SimClient -n 3 -t movement
|
||||
```
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
Integrate SimClient into CI/CD pipelines:
|
||||
|
||||
```yaml
|
||||
# Example GitHub Actions step
|
||||
- name: Test Server with SimClient
|
||||
run: |
|
||||
# Start servers in background
|
||||
./MasterServer &
|
||||
./AuthServer &
|
||||
./WorldServer &
|
||||
|
||||
# Wait for startup
|
||||
sleep 10
|
||||
|
||||
# Run tests
|
||||
./SimClient -t basic || exit 1
|
||||
./SimClient -t load -n 5 || exit 1
|
||||
|
||||
# Cleanup
|
||||
killall MasterServer AuthServer WorldServer
|
||||
```
|
||||
|
||||
### Load Testing
|
||||
|
||||
Simulate realistic server loads:
|
||||
|
||||
```bash
|
||||
# Test with 50 concurrent users
|
||||
./SimClient -t load -n 50
|
||||
|
||||
# Stress test with high load
|
||||
./SimClient -t stress -n 25 # Actually uses 50 clients (2x)
|
||||
|
||||
# Custom configuration for specific tests
|
||||
./SimClient -c load_test.ini -t load
|
||||
```
|
||||
|
||||
### Performance Monitoring
|
||||
|
||||
Monitor server performance over time:
|
||||
|
||||
```bash
|
||||
# Long-running test with detailed logging
|
||||
./SimClient -t load -n 20 -v > performance_test_$(date +%Y%m%d).log &
|
||||
|
||||
# Let it run for hours or days, then analyze logs
|
||||
```
|
||||
|
||||
## Configuration Management
|
||||
|
||||
### File-based Configuration
|
||||
|
||||
The SimClient uses an INI-style configuration file that can be customized for different test scenarios:
|
||||
|
||||
```ini
|
||||
# Basic connectivity test config
|
||||
client_count=1
|
||||
enable_movement=0
|
||||
enable_chat=0
|
||||
verbose_logging=1
|
||||
|
||||
# Load test config
|
||||
client_count=25
|
||||
spawn_delay_ms=1000
|
||||
enable_movement=1
|
||||
enable_random_actions=1
|
||||
|
||||
# Stress test config
|
||||
client_count=100
|
||||
spawn_delay_ms=500
|
||||
tick_rate_ms=8
|
||||
```
|
||||
|
||||
### Command-line Overrides
|
||||
|
||||
Command-line parameters override configuration file settings:
|
||||
|
||||
```bash
|
||||
# Override client count from config file
|
||||
./SimClient -c load_test.ini -n 10
|
||||
|
||||
# Override server addresses
|
||||
./SimClient -a 192.168.1.100:1001 -w 192.168.1.100:2000
|
||||
```
|
||||
|
||||
## Testing Integration
|
||||
|
||||
### Unit Testing
|
||||
|
||||
The SimClient components can be unit tested independently:
|
||||
|
||||
```cpp
|
||||
// Example unit test
|
||||
TEST(SimConfigTest, LoadValidConfig) {
|
||||
SimConfig config;
|
||||
ASSERT_TRUE(config.LoadFromFile("test_config.ini"));
|
||||
ASSERT_EQ(config.GetClientCount(), 5);
|
||||
}
|
||||
```
|
||||
|
||||
### Integration Testing
|
||||
|
||||
SimClient can be used for automated integration testing:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Integration test script
|
||||
|
||||
# Test basic server startup and connectivity
|
||||
./SimClient -t basic || { echo "Basic connectivity failed"; exit 1; }
|
||||
|
||||
# Test multiple client handling
|
||||
./SimClient -t load -n 5 || { echo "Load test failed"; exit 1; }
|
||||
|
||||
# Test movement system
|
||||
./SimClient -t movement -n 3 || { echo "Movement test failed"; exit 1; }
|
||||
|
||||
echo "All integration tests passed!"
|
||||
```
|
||||
|
||||
## Error Handling and Debugging
|
||||
|
||||
### Logging Integration
|
||||
|
||||
SimClient integrates with the DarkflameServer logging system:
|
||||
|
||||
- Uses same Logger class as server components
|
||||
- Writes to `logs/SimClient_<timestamp>.log`
|
||||
- Supports verbose and debug logging levels
|
||||
- Correlates with server logs for debugging
|
||||
|
||||
### Error Recovery
|
||||
|
||||
SimClient implements robust error handling:
|
||||
|
||||
- Automatic reconnection on network failures
|
||||
- Graceful degradation when servers are overloaded
|
||||
- Detailed error reporting for debugging
|
||||
- State recovery after temporary issues
|
||||
|
||||
### Debugging Features
|
||||
|
||||
Enable detailed debugging with:
|
||||
|
||||
```bash
|
||||
# Maximum verbosity
|
||||
./SimClient -v -n 1 -t basic
|
||||
|
||||
# Debug specific client behaviors
|
||||
./SimClient -t movement -n 1 -v | grep "movement"
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Resource Usage
|
||||
|
||||
SimClient is designed to be lightweight:
|
||||
|
||||
- Minimal memory footprint per simulated client
|
||||
- Efficient network communication
|
||||
- Configurable tick rates to balance load
|
||||
- Automatic cleanup of failed connections
|
||||
|
||||
### Scalability
|
||||
|
||||
For large-scale testing:
|
||||
|
||||
```bash
|
||||
# Multiple SimClient instances for extreme load
|
||||
./SimClient -n 100 &
|
||||
./SimClient -n 100 &
|
||||
./SimClient -n 100 &
|
||||
wait
|
||||
|
||||
# Distributed testing across multiple machines
|
||||
ssh server1 "./SimClient -n 50 -a $SERVER_IP" &
|
||||
ssh server2 "./SimClient -n 50 -a $SERVER_IP" &
|
||||
```
|
||||
|
||||
### Monitoring
|
||||
|
||||
Track performance metrics:
|
||||
|
||||
- Packets per second sent/received
|
||||
- Connection success/failure rates
|
||||
- Response times and latency
|
||||
- Memory and CPU usage
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Planned Features
|
||||
|
||||
1. **Protocol Coverage**: Support more game messages and interactions
|
||||
2. **Behavior Scripting**: Lua/Python scripting for complex behaviors
|
||||
3. **Web Dashboard**: Real-time monitoring interface
|
||||
4. **Distributed Coordination**: Multiple SimClient instance coordination
|
||||
5. **Performance Profiling**: Built-in server performance analysis
|
||||
|
||||
### Extension Points
|
||||
|
||||
The SimClient architecture supports easy extension:
|
||||
|
||||
```cpp
|
||||
// Add new behavior patterns
|
||||
class CustomBehavior : public SimBehavior {
|
||||
void ExecuteCustomAction(SimUser* user) override {
|
||||
// Implement custom test behavior
|
||||
}
|
||||
};
|
||||
|
||||
// Add new test scenarios
|
||||
void SimWorld::RunCustomTest() {
|
||||
// Implement custom test logic
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Development Workflow
|
||||
|
||||
1. **Start Small**: Begin with basic connectivity tests
|
||||
2. **Incremental Load**: Gradually increase client counts
|
||||
3. **Monitor Resources**: Watch server CPU/memory during tests
|
||||
4. **Log Analysis**: Correlate SimClient and server logs
|
||||
5. **Automate Testing**: Integrate into development workflow
|
||||
|
||||
### Production Testing
|
||||
|
||||
1. **Staging Environment**: Test on production-like infrastructure
|
||||
2. **Gradual Rollout**: Start with small loads, increase gradually
|
||||
3. **Baseline Metrics**: Establish performance baselines
|
||||
4. **Failure Scenarios**: Test server behavior under failures
|
||||
5. **Recovery Testing**: Verify graceful degradation and recovery
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Build Failures**
|
||||
- Ensure all DarkflameServer dependencies are met
|
||||
- Check CMake version and compiler compatibility
|
||||
- Verify RakNet and other third-party libraries
|
||||
|
||||
**Connection Issues**
|
||||
- Verify server components are running
|
||||
- Check firewall and network connectivity
|
||||
- Validate configuration file settings
|
||||
- Review server logs for authentication errors
|
||||
|
||||
**Performance Issues**
|
||||
- Reduce client count for available resources
|
||||
- Adjust tick rate and spawn delays
|
||||
- Monitor network bandwidth usage
|
||||
- Check for memory leaks in long-running tests
|
||||
|
||||
### Getting Help
|
||||
|
||||
- Check SimClient logs in `logs/` directory
|
||||
- Review server logs for corresponding errors
|
||||
- Enable verbose logging for detailed diagnostics
|
||||
- Correlate timestamps between SimClient and server logs
|
||||
|
||||
## Contributing
|
||||
|
||||
When contributing to SimClient:
|
||||
|
||||
1. Follow existing code style and patterns
|
||||
2. Add comprehensive error handling
|
||||
3. Update configuration options as needed
|
||||
4. Add command-line parameters for new features
|
||||
5. Update documentation
|
||||
6. Test with various scenarios and loads
|
||||
7. Consider backwards compatibility
|
||||
|
||||
The SimClient is designed to grow with the DarkflameServer project and can be extended to support new testing scenarios as the server evolves.
|
38
resources/simclient.ini
Normal file
38
resources/simclient.ini
Normal file
@@ -0,0 +1,38 @@
|
||||
# DarkflameServer Simulation Client Configuration
|
||||
# Lines starting with # are comments
|
||||
|
||||
# Authentication Server Settings
|
||||
auth_server_ip=127.0.0.1
|
||||
auth_server_port=1001
|
||||
|
||||
# World Server Settings
|
||||
world_server_ip=127.0.0.1
|
||||
world_server_port=2000
|
||||
|
||||
# Client Simulation Settings
|
||||
client_count=5
|
||||
spawn_delay_ms=2000
|
||||
tick_rate_ms=16
|
||||
|
||||
# Test Behavior Settings
|
||||
enable_movement=1
|
||||
enable_chat=0
|
||||
enable_random_actions=1
|
||||
action_interval_ms=5000
|
||||
|
||||
# Logging Settings
|
||||
verbose_logging=0
|
||||
log_file=simclient.log
|
||||
|
||||
# Test Accounts (username:password format)
|
||||
# If not enough accounts are provided, random ones will be generated
|
||||
test_account_1=testuser1:testpass1
|
||||
test_account_2=testuser2:testpass2
|
||||
test_account_3=testuser3:testpass3
|
||||
test_account_4=testuser4:testpass4
|
||||
test_account_5=testuser5:testpass5
|
||||
test_account_6=loadtest1:loadtest1
|
||||
test_account_7=loadtest2:loadtest2
|
||||
test_account_8=loadtest3:loadtest3
|
||||
test_account_9=loadtest4:loadtest4
|
||||
test_account_10=loadtest5:loadtest5
|
Reference in New Issue
Block a user