I forgot to open source this 💀

This commit is contained in:
ApfelTeeSaft
2025-11-26 12:30:40 +01:00
parent 1dfb212429
commit cab9abd3e1
215 changed files with 20340 additions and 0 deletions
+202
View File
@@ -0,0 +1,202 @@
cmake_minimum_required(VERSION 3.23)
# Auto-detect and configure vcpkg on Windows if available
if(WIN32 AND NOT DEFINED CMAKE_TOOLCHAIN_FILE)
# Common vcpkg locations
set(VCPKG_PATHS
"$ENV{VCPKG_ROOT}"
"C:/vcpkg"
"C:/src/vcpkg"
"$ENV{USERPROFILE}/vcpkg"
)
# Also check for vcpkg with date suffix (e.g., vcpkg-2025.06.13)
file(GLOB VCPKG_DATED "$ENV{USERPROFILE}/vcpkg-*")
list(APPEND VCPKG_PATHS ${VCPKG_DATED})
foreach(VCPKG_PATH ${VCPKG_PATHS})
if(EXISTS "${VCPKG_PATH}/scripts/buildsystems/vcpkg.cmake")
set(CMAKE_TOOLCHAIN_FILE "${VCPKG_PATH}/scripts/buildsystems/vcpkg.cmake" CACHE STRING "")
message(STATUS "Auto-detected vcpkg at: ${VCPKG_PATH}")
break()
endif()
endforeach()
if(NOT DEFINED CMAKE_TOOLCHAIN_FILE)
message(STATUS "vcpkg not found. Install packages manually or set VCPKG_ROOT environment variable.")
endif()
endif()
project(MinecraftBeta173Server
VERSION 0.1.0
DESCRIPTION "Modern C++ implementation of Minecraft Beta 1.7.3 Server"
LANGUAGES CXX
)
# C++23 required, C++20 minimum
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED OFF)
set(CMAKE_CXX_EXTENSIONS OFF)
# Fallback to C++20 if C++23 is not available
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
include(CheckCXXCompilerFlag)
check_cxx_compiler_flag("-std=c++23" COMPILER_SUPPORTS_CXX23)
if(NOT COMPILER_SUPPORTS_CXX23)
set(CMAKE_CXX_STANDARD 20)
message(STATUS "C++23 not available, falling back to C++20")
endif()
endif()
# Export compile commands for IDE support
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Build options
option(BUILD_TESTS "Build unit tests" ON)
option(USE_ASAN "Enable AddressSanitizer" OFF)
option(USE_UBSAN "Enable UndefinedBehaviorSanitizer" OFF)
option(PROFILE_BUILD "Enable profiling" OFF)
option(ALLOW_UNSAFE_PLUGINS "Allow unsafe plugin operations (disable sandbox)" OFF)
# Platform detection
if(WIN32)
set(PLATFORM_WINDOWS TRUE)
add_compile_definitions(PLATFORM_WINDOWS=1)
elseif(APPLE)
set(PLATFORM_MACOS TRUE)
add_compile_definitions(PLATFORM_MACOS=1)
elseif(UNIX)
set(PLATFORM_LINUX TRUE)
add_compile_definitions(PLATFORM_LINUX=1)
endif()
# Compiler-specific flags
if(MSVC)
add_compile_options(
/W4 # Warning level 4
/WX # Warnings as errors
/permissive- # Conformance mode
/Zc:__cplusplus # Enable updated __cplusplus macro
/MP # Multi-processor compilation
/EHsc # Exception handling
)
add_compile_definitions(
_CRT_SECURE_NO_WARNINGS
WIN32_LEAN_AND_MEAN
NOMINMAX
)
else()
add_compile_options(
-Wall
-Wextra
-Wpedantic
-Werror
-fno-exceptions # Disable exceptions (we use result types)
)
if(USE_ASAN)
add_compile_options(-fsanitize=address -fno-omit-frame-pointer)
add_link_options(-fsanitize=address)
endif()
if(USE_UBSAN)
add_compile_options(-fsanitize=undefined)
add_link_options(-fsanitize=undefined)
endif()
if(PROFILE_BUILD)
add_compile_options(-pg)
add_link_options(-pg)
endif()
endif()
# Configure unsafe plugins flag
if(ALLOW_UNSAFE_PLUGINS)
add_compile_definitions(ALLOW_UNSAFE_PLUGINS=1)
message(WARNING "ALLOW_UNSAFE_PLUGINS is enabled - sandbox checks are disabled!")
endif()
# Find required libraries
find_package(ZLIB REQUIRED)
# Helpful message if ZLIB is not found
if(NOT ZLIB_FOUND AND WIN32)
message(FATAL_ERROR
"ZLIB not found!\n"
"On Windows with vcpkg, install it with:\n"
" vcpkg install zlib:x64-windows\n"
"Or set VCPKG_ROOT environment variable to your vcpkg installation."
)
endif()
# Include directories
include_directories(${CMAKE_SOURCE_DIR}/src)
include_directories(${CMAKE_SOURCE_DIR}/sdk)
# Add subdirectories
add_subdirectory(src/platform)
add_subdirectory(src/util)
add_subdirectory(src/core)
add_subdirectory(src/net)
add_subdirectory(src/world)
add_subdirectory(src/entity)
add_subdirectory(src/storage)
add_subdirectory(src/admin)
add_subdirectory(src/plugin)
# Main server executable
add_executable(mcserver
src/main.cpp
)
target_link_libraries(mcserver PRIVATE
platform
util
core
net
world
entity
storage
admin
plugin
ZLIB::ZLIB
)
# Platform-specific libraries
if(PLATFORM_WINDOWS)
target_link_libraries(mcserver PRIVATE ws2_32 mswsock)
elseif(PLATFORM_LINUX)
target_link_libraries(mcserver PRIVATE pthread)
elseif(PLATFORM_MACOS)
target_link_libraries(mcserver PRIVATE pthread)
endif()
# Set output directories
set_target_properties(mcserver PROPERTIES
RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_SOURCE_DIR}/out/${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}/Debug"
RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_SOURCE_DIR}/out/${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}/Release"
RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_SOURCE_DIR}/out/${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}/RelWithDebInfo"
)
# Tests
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
# IDE support: Group projects in folders for Visual Studio
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
# Print configuration summary
message(STATUS "")
message(STATUS "=== Minecraft Beta 1.7.3 Server Configuration ===")
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
message(STATUS "C++ standard: ${CMAKE_CXX_STANDARD}")
message(STATUS "Platform: ${CMAKE_SYSTEM_NAME}")
message(STATUS "Compiler: ${CMAKE_CXX_COMPILER_ID}")
message(STATUS "Build tests: ${BUILD_TESTS}")
message(STATUS "AddressSanitizer: ${USE_ASAN}")
message(STATUS "UBSanitizer: ${USE_UBSAN}")
message(STATUS "Profiling: ${PROFILE_BUILD}")
message(STATUS "Allow unsafe plugins: ${ALLOW_UNSAFE_PLUGINS}")
message(STATUS "")
+146
View File
@@ -0,0 +1,146 @@
{
"version": 6,
"cmakeMinimumRequired": {
"major": 3,
"minor": 23,
"patch": 0
},
"configurePresets": [
{
"name": "windows-base",
"hidden": true,
"generator": "Visual Studio 17 2022",
"binaryDir": "${sourceDir}/build/windows",
"architecture": {
"value": "x64",
"strategy": "set"
},
"cacheVariables": {
"CMAKE_INSTALL_PREFIX": "${sourceDir}/install/windows"
}
},
{
"name": "windows-debug",
"displayName": "Windows Debug",
"inherits": "windows-base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"BUILD_TESTS": "ON"
}
},
{
"name": "windows-release",
"displayName": "Windows Release",
"inherits": "windows-base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"BUILD_TESTS": "OFF"
}
},
{
"name": "linux-base",
"hidden": true,
"generator": "Unix Makefiles",
"binaryDir": "${sourceDir}/build/linux",
"cacheVariables": {
"CMAKE_INSTALL_PREFIX": "${sourceDir}/install/linux"
}
},
{
"name": "linux-debug",
"displayName": "Linux Debug",
"inherits": "linux-base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"BUILD_TESTS": "ON"
}
},
{
"name": "linux-release",
"displayName": "Linux Release",
"inherits": "linux-base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"BUILD_TESTS": "OFF"
}
},
{
"name": "macos-base",
"hidden": true,
"generator": "Unix Makefiles",
"binaryDir": "${sourceDir}/build/macos",
"cacheVariables": {
"CMAKE_INSTALL_PREFIX": "${sourceDir}/install/macos"
}
},
{
"name": "macos-debug",
"displayName": "macOS Debug",
"inherits": "macos-base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"BUILD_TESTS": "ON"
}
},
{
"name": "macos-release",
"displayName": "macOS Release",
"inherits": "macos-base",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"BUILD_TESTS": "OFF"
}
}
],
"buildPresets": [
{
"name": "windows-debug",
"configurePreset": "windows-debug",
"configuration": "Debug"
},
{
"name": "windows-release",
"configurePreset": "windows-release",
"configuration": "Release"
},
{
"name": "linux-debug",
"configurePreset": "linux-debug"
},
{
"name": "linux-release",
"configurePreset": "linux-release"
},
{
"name": "macos-debug",
"configurePreset": "macos-debug"
},
{
"name": "macos-release",
"configurePreset": "macos-release"
}
],
"testPresets": [
{
"name": "windows-debug",
"configurePreset": "windows-debug",
"output": {
"outputOnFailure": true
}
},
{
"name": "linux-debug",
"configurePreset": "linux-debug",
"output": {
"outputOnFailure": true
}
},
{
"name": "macos-debug",
"configurePreset": "macos-debug",
"output": {
"outputOnFailure": true
}
}
]
}
+408
View File
@@ -0,0 +1,408 @@
# Plugin API Documentation
## Overview
The Minecraft Beta 1.7.3 Server implements a Bukkit-inspired plugin system that allows you to extend server functionality through dynamically loaded plugins written in C++.
## Features
- **Event System**: Register listeners for server events with configurable priorities
- **Cancellable Events**: Prevent default behavior by cancelling events
- **Dynamic Loading**: Load and unload plugins at runtime
- **Lifecycle Management**: Proper `on_enable()` and `on_disable()` hooks
- **Type-Safe**: Full C++ type safety with compile-time checking
- **Cross-Platform**: Works on Linux, macOS, and Windows
## Plugin Structure
### Basic Plugin Class
```cpp
#include "plugin/plugin.hpp"
#include "plugin/event/event_manager.hpp"
class MyPlugin : public mcserver::Plugin {
public:
MyPlugin() {
description_.name = "MyPlugin";
description_.version = "1.0.0";
description_.author = "YourName";
description_.description = "My awesome plugin";
}
void on_enable() override {
// Called when plugin is enabled
// Register event listeners here
}
void on_disable() override {
// Called when plugin is disabled
// Cleanup code here
}
const mcserver::PluginDescription& get_description() const override {
return description_;
}
private:
mcserver::PluginDescription description_;
};
// Required exports
extern "C" {
mcserver::Plugin* create_plugin() {
return new MyPlugin();
}
void destroy_plugin(mcserver::Plugin* plugin) {
delete plugin;
}
}
```
## Event System
### Event Priorities
Events are dispatched in order of priority:
- `LOWEST` - Runs first
- `LOW`
- `NORMAL` - Default priority
- `HIGH`
- `HIGHEST`
- `MONITOR` - Runs last, for monitoring only (should not modify event)
### Registering Event Listeners
```cpp
void on_enable() override {
auto* event_mgr = get_event_manager();
event_mgr->register_listener<PlayerJoinEvent>(
this, // Plugin pointer
EventPriority::NORMAL, // Priority
[this](PlayerJoinEvent& event) {
// Handle event
},
false // ignore_cancelled (optional)
);
}
```
### Cancellable Events
Some events can be cancelled to prevent their default behavior:
```cpp
event_mgr->register_listener<BlockPlaceEvent>(
this,
EventPriority::HIGH,
[](BlockPlaceEvent& event) {
if (event.get_block_type() == 7) { // Bedrock
event.set_cancelled(true); // Prevent placement
}
}
);
```
## Available Events
### Player Events (`player_events.hpp`)
- **PlayerJoinEvent**: When a player joins the server
- Methods: `get_player()`, `get/set_join_message()`
- Not cancellable
- **PlayerQuitEvent**: When a player leaves the server
- Methods: `get_player()`, `get/set_quit_message()`
- Not cancellable
- **PlayerChatEvent**: When a player sends a chat message
- Methods: `get_player()`, `get/set_message()`, `get/set_format()`
- Cancellable
- **PlayerMoveEvent**: When a player moves
- Methods: `get_player()`, `get_from_*()`, `get_to_*()`, `set_to()`
- Cancellable
- **PlayerInteractEvent**: When a player interacts with world
- Methods: `get_player()`, `get_action()`, `get_block_*()`
- Actions: `LEFT_CLICK_AIR`, `LEFT_CLICK_BLOCK`, `RIGHT_CLICK_AIR`, `RIGHT_CLICK_BLOCK`
- Cancellable
- **PlayerRespawnEvent**: When a player respawns
- Methods: `get_player()`, `get/set_respawn_location()`
- Not cancellable
### Block Events (`block_events.hpp`)
- **BlockPlaceEvent**: When a block is placed
- Methods: `get_x/y/z()`, `get/set_block_type()`, `get/set_metadata()`, `get_player()`
- Cancellable
- **BlockBreakEvent**: When a block is broken
- Methods: `get_x/y/z()`, `get_block_type()`, `get_player()`, `should_drop_items()`, `set_drop_items()`
- Cancellable
- **BlockInteractEvent**: When a player interacts with a block
- Methods: `get_x/y/z()`, `get_block_type()`, `get_player()`
- Cancellable
### Entity Events (`entity_events.hpp`)
- **EntitySpawnEvent**: When an entity spawns
- Methods: `get_entity()`
- Cancellable
- **EntityDeathEvent**: When an entity dies
- Methods: `get_entity()`, `get_killer()`, `get/set_dropped_exp()`, `should_drop_items()`
- Not cancellable
- **EntityDamageEvent**: When an entity takes damage
- Methods: `get_entity()`, `get_cause()`, `get/set_damage()`
- Damage causes: `CONTACT`, `ENTITY_ATTACK`, `PROJECTILE`, `FALL`, `FIRE`, `LAVA`, `DROWNING`, etc.
- Cancellable
- **EntityDamageByEntityEvent**: When an entity damages another entity
- Methods: `get_entity()`, `get_damager()`, `get/set_damage()`
- Cancellable
- **EntityTargetEvent**: When a mob targets something
- Methods: `get_entity()`, `get/set_target()`, `get_reason()`
- Reasons: `TARGET_ATTACKED_ENTITY`, `CLOSEST_PLAYER`, `FORGOT_TARGET`, etc.
- Cancellable
### Server Events (`server_events.hpp`)
- **ServerEnableEvent**: When server starts
- Methods: `get_server()`
- Not cancellable
- **ServerDisableEvent**: When server stops
- Methods: `get_server()`
- Not cancellable
- **ChunkLoadEvent**: When a chunk is loaded
- Methods: `get_chunk_x/z()`, `is_new_chunk()`
- Not cancellable
- **ChunkUnloadEvent**: When a chunk is unloaded
- Methods: `get_chunk_x/z()`
- Not cancellable
## Building Plugins
### Requirements
- C++23 compiler (or C++20 minimum)
- CMake 3.23 or higher
- Access to server headers
### CMake Example
```cmake
cmake_minimum_required(VERSION 3.23)
project(MyPlugin VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_library(my_plugin SHARED
my_plugin.cpp
)
target_include_directories(my_plugin PRIVATE
/path/to/server/src
)
set_target_properties(my_plugin PROPERTIES
PREFIX "" # Remove "lib" prefix
)
```
### Build Commands
```bash
mkdir build && cd build
cmake ..
cmake --build .
```
This will produce `my_plugin.so` (Linux), `my_plugin.dylib` (macOS), or `my_plugin.dll` (Windows).
## Loading Plugins
### Plugin Directory Structure
```
server_root/
├── mcserver (executable)
└── plugins/
├── my_plugin.so
├── another_plugin.so
└── ...
```
### Automatic Loading
Plugins in the `plugins/` directory are automatically loaded on server start.
### Manual Loading (via API)
```cpp
PluginLoader loader(&server, &event_manager);
// Load single plugin
auto result = loader.load_plugin("plugins/my_plugin.so");
if (result.is_ok()) {
Plugin* plugin = result.value();
loader.enable_plugin(plugin->get_description().name);
}
// Load all plugins from directory
loader.load_plugins_from_directory("plugins/");
loader.enable_all_plugins();
```
## Best Practices
### 1. Event Listener Management
Always use `ignore_cancelled = true` for MONITOR priority listeners:
```cpp
event_mgr->register_listener<BlockPlaceEvent>(
this,
EventPriority::MONITOR,
[](BlockPlaceEvent& event) {
// Just log, don't modify
log_block_placement(event);
},
true // Ignore cancelled events
);
```
### 2. Error Handling
Plugins should handle errors internally since exceptions are disabled:
```cpp
void on_enable() override {
if (!initialize_database()) {
LOG_ERROR_CAT("Failed to initialize database!", LogCategory::Plugin);
return;
}
// Continue initialization...
}
```
### 3. Resource Cleanup
Always clean up resources in `on_disable()`:
```cpp
void on_disable() override {
// Save data
save_plugin_data();
// Close connections
database_connection_.close();
// Event listeners are automatically unregistered
}
```
### 4. Thread Safety
Event handlers may be called from multiple threads. Use mutexes for shared data:
```cpp
class MyPlugin : public Plugin {
private:
std::mutex mutex_;
std::map<std::string, PlayerData> player_data_;
void handle_player_join(PlayerJoinEvent& event) {
std::lock_guard<std::mutex> lock(mutex_);
player_data_[event.get_player()->get_name()] = PlayerData();
}
};
```
## Example Plugin
See `examples/plugins/hello_world/` for a complete example plugin demonstrating:
- Plugin lifecycle management
- Event registration and handling
- Event cancellation
- Custom commands via chat events
- Block placement/breaking restrictions
## API Reference
### Plugin Interface
- `void on_enable()` - Called when plugin is enabled
- `void on_disable()` - Called when plugin is disabled
- `const PluginDescription& get_description() const` - Returns plugin metadata
- `Server* get_server() const` - Get server instance
- `EventManager* get_event_manager() const` - Get event manager
- `bool is_enabled() const` - Check if plugin is enabled
### EventManager
- `register_listener<EventType>(plugin, priority, handler, ignore_cancelled)` - Register event listener
- `call_event<EventType>(event)` - Dispatch event to listeners
- `unregister_plugin(plugin)` - Unregister all listeners for a plugin
- `unregister_all()` - Unregister all listeners
- `get_listener_count()` - Get total number of registered listeners
### PluginLoader
- `load_plugin(file_path)` - Load a single plugin
- `load_plugins_from_directory(directory)` - Load all plugins from directory
- `unload_plugin(plugin_name)` - Unload a plugin
- `enable_plugin(plugin_name)` - Enable a loaded plugin
- `disable_plugin(plugin_name)` - Disable a plugin
- `get_plugin(plugin_name)` - Get plugin by name
- `get_plugins()` - Get all loaded plugins
- `is_plugin_loaded(plugin_name)` - Check if plugin is loaded
## Troubleshooting
### Plugin Not Loading
1. **Check file extension**: Must be `.so` (Linux), `.dylib` (macOS), or `.dll` (Windows)
2. **Verify exports**: Ensure `create_plugin` and `destroy_plugin` are exported with `extern "C"`
3. **Check logs**: Look for error messages in server logs under `[Plugin]` category
4. **Dependencies**: Ensure all dependencies are available
### Events Not Firing
1. **Registration**: Verify event listener is registered in `on_enable()`
2. **Priority**: Check if higher priority listeners are cancelling the event
3. **ignore_cancelled**: If set to `true`, cancelled events won't reach your handler
4. **Event type**: Ensure you're using the correct event type
### Crash on Load/Unload
1. **Memory management**: Ensure proper cleanup in `on_disable()`
2. **Static variables**: Avoid global/static variables that aren't cleaned up
3. **Threading**: Use proper synchronization for shared data
4. **ABI compatibility**: Ensure plugin is built with same compiler/settings as server
## Contributing
To add new events:
1. Create event class inheriting from `Event` or `CancellableEvent`
2. Add to appropriate header in `src/plugin/event/`
3. Call `event_manager.call_event()` at the appropriate point in server code
4. Update this documentation
## License
See LICENSE file in repository root.
+109
View File
@@ -0,0 +1,109 @@
# Minecraft Beta 1.7.3 Server - Modern C++ Implementation
A high-performance, cross-platform reimplementation of the Minecraft Beta 1.7.3 server in modern C++23/C++20.
## Features
- **Cross-platform**: Windows, Linux, macOS
- **High performance**: Multithreaded architecture, data-oriented design
- **Low memory**: Arena allocators, object pools, minimal allocations
- **Modern C++**: C++23 preferred, C++20 minimum, RAII throughout
- **Beta 1.7.3 Protocol**: Full network protocol compatibility
- **Extensible**: Plugin SDK (Step 3)
## Quick Start
### Building on Windows
```batch
# Using Visual Studio 2022
scripts\build_windows.bat Release
# Or using PowerShell
powershell -ExecutionPolicy Bypass -File scripts\build_windows.ps1 -Configuration Release
```
The build will generate a Visual Studio solution at `build/windows/MinecraftBeta173Server.sln`.
### Building on Linux/macOS
```bash
# Debug build
./scripts/build_unix.sh debug
# Release build
./scripts/build_unix.sh release
```
### Running the Server
```bash
# Windows
out\Windows-x64\Release\mcserver.exe
# Linux/macOS
out/Linux-x86_64/Release/mcserver
```
The server will create a `server.properties` file on first run. Edit this file to configure the server.
## Configuration
Edit `server.properties` to configure server settings:
```properties
server-ip=
server-port=25565
level-name=world
level-seed=
online-mode=false
spawn-animals=true
spawn-monsters=true
pvp=true
allow-flight=false
allow-nether=true
max-players=20
```
## Development
### IDE Support
- **Visual Studio**: Open `build/windows/MinecraftBeta173Server.sln`
- **CLion**: Open project root (uses CMakePresets.json)
- **VSCode**: Install CMake Tools extension
### Running Tests
```bash
# After building
ctest --output-on-failure
# Or run directly
out/Linux-x86_64/Debug/tests_unit
```
### Build Options
```bash
cmake -DBUILD_TESTS=ON # Enable tests (default: ON)
cmake -DUSE_ASAN=ON # Enable AddressSanitizer
cmake -DUSE_UBSAN=ON # Enable UndefinedBehaviorSanitizer
cmake -DPROFILE_BUILD=ON # Enable profiling
cmake -DALLOW_UNSAFE_PLUGINS=ON # Disable plugin sandbox (Step 3)
```
## Performance
- **Target**: 20 TPS (50ms per tick)
- **Architecture**: Data-oriented design with structure-of-arrays for hot paths
- **Memory**: Arena allocators for per-tick temporaries, object pools for packets/entities
- **Parallelism**: Job system for chunk generation, packet encoding, I/O staging
## License
This is a clean-room reimplementation based on decompiled sources for educational purposes.
## Acknowledgments
- Minecraft Beta 1.7.3 decompiled sources
@@ -0,0 +1,3 @@
unit_tests 0 0
---
unit_tests
+44
View File
@@ -0,0 +1,44 @@
# Install script for directory: /home/user/mc-temp/src/core
# Set the install prefix
if(NOT DEFINED CMAKE_INSTALL_PREFIX)
set(CMAKE_INSTALL_PREFIX "/usr/local")
endif()
string(REGEX REPLACE "/$" "" CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}")
# Set the install configuration name.
if(NOT DEFINED CMAKE_INSTALL_CONFIG_NAME)
if(BUILD_TYPE)
string(REGEX REPLACE "^[^A-Za-z0-9_]+" ""
CMAKE_INSTALL_CONFIG_NAME "${BUILD_TYPE}")
else()
set(CMAKE_INSTALL_CONFIG_NAME "Debug")
endif()
message(STATUS "Install configuration: \"${CMAKE_INSTALL_CONFIG_NAME}\"")
endif()
# Set the component getting installed.
if(NOT CMAKE_INSTALL_COMPONENT)
if(COMPONENT)
message(STATUS "Install component: \"${COMPONENT}\"")
set(CMAKE_INSTALL_COMPONENT "${COMPONENT}")
else()
set(CMAKE_INSTALL_COMPONENT)
endif()
endif()
# Install shared libraries without execute permission?
if(NOT DEFINED CMAKE_INSTALL_SO_NO_EXE)
set(CMAKE_INSTALL_SO_NO_EXE "1")
endif()
# Is this installation the result of a crosscompile?
if(NOT DEFINED CMAKE_CROSSCOMPILING)
set(CMAKE_CROSSCOMPILING "FALSE")
endif()
# Set default install directory permissions.
if(NOT DEFINED CMAKE_OBJDUMP)
set(CMAKE_OBJDUMP "/usr/bin/objdump")
endif()
@@ -0,0 +1,44 @@
# Install script for directory: /home/user/mc-temp/src/entity
# Set the install prefix
if(NOT DEFINED CMAKE_INSTALL_PREFIX)
set(CMAKE_INSTALL_PREFIX "/usr/local")
endif()
string(REGEX REPLACE "/$" "" CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}")
# Set the install configuration name.
if(NOT DEFINED CMAKE_INSTALL_CONFIG_NAME)
if(BUILD_TYPE)
string(REGEX REPLACE "^[^A-Za-z0-9_]+" ""
CMAKE_INSTALL_CONFIG_NAME "${BUILD_TYPE}")
else()
set(CMAKE_INSTALL_CONFIG_NAME "Debug")
endif()
message(STATUS "Install configuration: \"${CMAKE_INSTALL_CONFIG_NAME}\"")
endif()
# Set the component getting installed.
if(NOT CMAKE_INSTALL_COMPONENT)
if(COMPONENT)
message(STATUS "Install component: \"${COMPONENT}\"")
set(CMAKE_INSTALL_COMPONENT "${COMPONENT}")
else()
set(CMAKE_INSTALL_COMPONENT)
endif()
endif()
# Install shared libraries without execute permission?
if(NOT DEFINED CMAKE_INSTALL_SO_NO_EXE)
set(CMAKE_INSTALL_SO_NO_EXE "1")
endif()
# Is this installation the result of a crosscompile?
if(NOT DEFINED CMAKE_CROSSCOMPILING)
set(CMAKE_CROSSCOMPILING "FALSE")
endif()
# Set default install directory permissions.
if(NOT DEFINED CMAKE_OBJDUMP)
set(CMAKE_OBJDUMP "/usr/bin/objdump")
endif()
+44
View File
@@ -0,0 +1,44 @@
# Install script for directory: /home/user/mc-temp/src/net
# Set the install prefix
if(NOT DEFINED CMAKE_INSTALL_PREFIX)
set(CMAKE_INSTALL_PREFIX "/usr/local")
endif()
string(REGEX REPLACE "/$" "" CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}")
# Set the install configuration name.
if(NOT DEFINED CMAKE_INSTALL_CONFIG_NAME)
if(BUILD_TYPE)
string(REGEX REPLACE "^[^A-Za-z0-9_]+" ""
CMAKE_INSTALL_CONFIG_NAME "${BUILD_TYPE}")
else()
set(CMAKE_INSTALL_CONFIG_NAME "Debug")
endif()
message(STATUS "Install configuration: \"${CMAKE_INSTALL_CONFIG_NAME}\"")
endif()
# Set the component getting installed.
if(NOT CMAKE_INSTALL_COMPONENT)
if(COMPONENT)
message(STATUS "Install component: \"${COMPONENT}\"")
set(CMAKE_INSTALL_COMPONENT "${COMPONENT}")
else()
set(CMAKE_INSTALL_COMPONENT)
endif()
endif()
# Install shared libraries without execute permission?
if(NOT DEFINED CMAKE_INSTALL_SO_NO_EXE)
set(CMAKE_INSTALL_SO_NO_EXE "1")
endif()
# Is this installation the result of a crosscompile?
if(NOT DEFINED CMAKE_CROSSCOMPILING)
set(CMAKE_CROSSCOMPILING "FALSE")
endif()
# Set default install directory permissions.
if(NOT DEFINED CMAKE_OBJDUMP)
set(CMAKE_OBJDUMP "/usr/bin/objdump")
endif()
@@ -0,0 +1,44 @@
# Install script for directory: /home/user/mc-temp/src/platform
# Set the install prefix
if(NOT DEFINED CMAKE_INSTALL_PREFIX)
set(CMAKE_INSTALL_PREFIX "/usr/local")
endif()
string(REGEX REPLACE "/$" "" CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}")
# Set the install configuration name.
if(NOT DEFINED CMAKE_INSTALL_CONFIG_NAME)
if(BUILD_TYPE)
string(REGEX REPLACE "^[^A-Za-z0-9_]+" ""
CMAKE_INSTALL_CONFIG_NAME "${BUILD_TYPE}")
else()
set(CMAKE_INSTALL_CONFIG_NAME "Debug")
endif()
message(STATUS "Install configuration: \"${CMAKE_INSTALL_CONFIG_NAME}\"")
endif()
# Set the component getting installed.
if(NOT CMAKE_INSTALL_COMPONENT)
if(COMPONENT)
message(STATUS "Install component: \"${COMPONENT}\"")
set(CMAKE_INSTALL_COMPONENT "${COMPONENT}")
else()
set(CMAKE_INSTALL_COMPONENT)
endif()
endif()
# Install shared libraries without execute permission?
if(NOT DEFINED CMAKE_INSTALL_SO_NO_EXE)
set(CMAKE_INSTALL_SO_NO_EXE "1")
endif()
# Is this installation the result of a crosscompile?
if(NOT DEFINED CMAKE_CROSSCOMPILING)
set(CMAKE_CROSSCOMPILING "FALSE")
endif()
# Set default install directory permissions.
if(NOT DEFINED CMAKE_OBJDUMP)
set(CMAKE_OBJDUMP "/usr/bin/objdump")
endif()
@@ -0,0 +1,44 @@
# Install script for directory: /home/user/mc-temp/src/storage
# Set the install prefix
if(NOT DEFINED CMAKE_INSTALL_PREFIX)
set(CMAKE_INSTALL_PREFIX "/usr/local")
endif()
string(REGEX REPLACE "/$" "" CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}")
# Set the install configuration name.
if(NOT DEFINED CMAKE_INSTALL_CONFIG_NAME)
if(BUILD_TYPE)
string(REGEX REPLACE "^[^A-Za-z0-9_]+" ""
CMAKE_INSTALL_CONFIG_NAME "${BUILD_TYPE}")
else()
set(CMAKE_INSTALL_CONFIG_NAME "Debug")
endif()
message(STATUS "Install configuration: \"${CMAKE_INSTALL_CONFIG_NAME}\"")
endif()
# Set the component getting installed.
if(NOT CMAKE_INSTALL_COMPONENT)
if(COMPONENT)
message(STATUS "Install component: \"${COMPONENT}\"")
set(CMAKE_INSTALL_COMPONENT "${COMPONENT}")
else()
set(CMAKE_INSTALL_COMPONENT)
endif()
endif()
# Install shared libraries without execute permission?
if(NOT DEFINED CMAKE_INSTALL_SO_NO_EXE)
set(CMAKE_INSTALL_SO_NO_EXE "1")
endif()
# Is this installation the result of a crosscompile?
if(NOT DEFINED CMAKE_CROSSCOMPILING)
set(CMAKE_CROSSCOMPILING "FALSE")
endif()
# Set default install directory permissions.
if(NOT DEFINED CMAKE_OBJDUMP)
set(CMAKE_OBJDUMP "/usr/bin/objdump")
endif()
+44
View File
@@ -0,0 +1,44 @@
# Install script for directory: /home/user/mc-temp/src/util
# Set the install prefix
if(NOT DEFINED CMAKE_INSTALL_PREFIX)
set(CMAKE_INSTALL_PREFIX "/usr/local")
endif()
string(REGEX REPLACE "/$" "" CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}")
# Set the install configuration name.
if(NOT DEFINED CMAKE_INSTALL_CONFIG_NAME)
if(BUILD_TYPE)
string(REGEX REPLACE "^[^A-Za-z0-9_]+" ""
CMAKE_INSTALL_CONFIG_NAME "${BUILD_TYPE}")
else()
set(CMAKE_INSTALL_CONFIG_NAME "Debug")
endif()
message(STATUS "Install configuration: \"${CMAKE_INSTALL_CONFIG_NAME}\"")
endif()
# Set the component getting installed.
if(NOT CMAKE_INSTALL_COMPONENT)
if(COMPONENT)
message(STATUS "Install component: \"${COMPONENT}\"")
set(CMAKE_INSTALL_COMPONENT "${COMPONENT}")
else()
set(CMAKE_INSTALL_COMPONENT)
endif()
endif()
# Install shared libraries without execute permission?
if(NOT DEFINED CMAKE_INSTALL_SO_NO_EXE)
set(CMAKE_INSTALL_SO_NO_EXE "1")
endif()
# Is this installation the result of a crosscompile?
if(NOT DEFINED CMAKE_CROSSCOMPILING)
set(CMAKE_CROSSCOMPILING "FALSE")
endif()
# Set default install directory permissions.
if(NOT DEFINED CMAKE_OBJDUMP)
set(CMAKE_OBJDUMP "/usr/bin/objdump")
endif()
+44
View File
@@ -0,0 +1,44 @@
# Install script for directory: /home/user/mc-temp/src/world
# Set the install prefix
if(NOT DEFINED CMAKE_INSTALL_PREFIX)
set(CMAKE_INSTALL_PREFIX "/usr/local")
endif()
string(REGEX REPLACE "/$" "" CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}")
# Set the install configuration name.
if(NOT DEFINED CMAKE_INSTALL_CONFIG_NAME)
if(BUILD_TYPE)
string(REGEX REPLACE "^[^A-Za-z0-9_]+" ""
CMAKE_INSTALL_CONFIG_NAME "${BUILD_TYPE}")
else()
set(CMAKE_INSTALL_CONFIG_NAME "Debug")
endif()
message(STATUS "Install configuration: \"${CMAKE_INSTALL_CONFIG_NAME}\"")
endif()
# Set the component getting installed.
if(NOT CMAKE_INSTALL_COMPONENT)
if(COMPONENT)
message(STATUS "Install component: \"${COMPONENT}\"")
set(CMAKE_INSTALL_COMPONENT "${COMPONENT}")
else()
set(CMAKE_INSTALL_COMPONENT)
endif()
endif()
# Install shared libraries without execute permission?
if(NOT DEFINED CMAKE_INSTALL_SO_NO_EXE)
set(CMAKE_INSTALL_SO_NO_EXE "1")
endif()
# Is this installation the result of a crosscompile?
if(NOT DEFINED CMAKE_CROSSCOMPILING)
set(CMAKE_CROSSCOMPILING "FALSE")
endif()
# Set default install directory permissions.
if(NOT DEFINED CMAKE_OBJDUMP)
set(CMAKE_OBJDUMP "/usr/bin/objdump")
endif()
+8
View File
@@ -0,0 +1,8 @@
# CMake generated Testfile for
# Source directory: /home/user/mc-temp/tests
# Build directory: /home/user/mc-temp/build/linux/tests
#
# This file includes the relevant testing commands required for
# testing this directory and lists subdirectories to be tested as well.
add_test([=[unit_tests]=] "/home/user/mc-temp/build/linux/tests/tests_unit")
set_tests_properties([=[unit_tests]=] PROPERTIES _BACKTRACE_TRIPLES "/home/user/mc-temp/tests/CMakeLists.txt;18;add_test;/home/user/mc-temp/tests/CMakeLists.txt;0;")
+44
View File
@@ -0,0 +1,44 @@
# Install script for directory: /home/user/mc-temp/tests
# Set the install prefix
if(NOT DEFINED CMAKE_INSTALL_PREFIX)
set(CMAKE_INSTALL_PREFIX "/usr/local")
endif()
string(REGEX REPLACE "/$" "" CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}")
# Set the install configuration name.
if(NOT DEFINED CMAKE_INSTALL_CONFIG_NAME)
if(BUILD_TYPE)
string(REGEX REPLACE "^[^A-Za-z0-9_]+" ""
CMAKE_INSTALL_CONFIG_NAME "${BUILD_TYPE}")
else()
set(CMAKE_INSTALL_CONFIG_NAME "Debug")
endif()
message(STATUS "Install configuration: \"${CMAKE_INSTALL_CONFIG_NAME}\"")
endif()
# Set the component getting installed.
if(NOT CMAKE_INSTALL_COMPONENT)
if(COMPONENT)
message(STATUS "Install component: \"${COMPONENT}\"")
set(CMAKE_INSTALL_COMPONENT "${COMPONENT}")
else()
set(CMAKE_INSTALL_COMPONENT)
endif()
endif()
# Install shared libraries without execute permission?
if(NOT DEFINED CMAKE_INSTALL_SO_NO_EXE)
set(CMAKE_INSTALL_SO_NO_EXE "1")
endif()
# Is this installation the result of a crosscompile?
if(NOT DEFINED CMAKE_CROSSCOMPILING)
set(CMAKE_CROSSCOMPILING "FALSE")
endif()
# Set default install directory permissions.
if(NOT DEFINED CMAKE_OBJDUMP)
set(CMAKE_OBJDUMP "/usr/bin/objdump")
endif()
Binary file not shown.
@@ -0,0 +1,47 @@
cmake_minimum_required(VERSION 3.23)
project(HelloWorldPlugin VERSION 1.0.0 LANGUAGES CXX)
# C++23 required
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Build as shared library (plugin)
add_library(hello_world SHARED
hello_world_plugin.cpp
)
# Include server headers
target_include_directories(hello_world PRIVATE
${CMAKE_SOURCE_DIR}/../../../src
)
# Link with plugin interface
# Note: In a real setup, you'd link against a plugin SDK library
# For now, we just need headers (header-only interfaces)
# Set output directory
set_target_properties(hello_world PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/../../../plugins"
PREFIX "" # Remove "lib" prefix on Unix
)
# Platform-specific settings
if(UNIX)
target_compile_options(hello_world PRIVATE
-Wall -Wextra -Wpedantic
-fvisibility=hidden
)
# Export only required symbols
target_link_options(hello_world PRIVATE
-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/exports.map
)
endif()
if(WIN32)
target_compile_options(hello_world PRIVATE
/W4
)
endif()
+7
View File
@@ -0,0 +1,7 @@
{
global:
create_plugin;
destroy_plugin;
local:
*;
};
@@ -0,0 +1,181 @@
/**
* Example Plugin: HelloWorld
*
* Demonstrates the basic plugin API including:
* - Plugin lifecycle (on_enable, on_disable)
* - Event registration and handling
* - Player events (join, quit, chat)
* - Block events (place, break)
*/
#include "plugin/plugin.hpp"
#include "plugin/event/event_manager.hpp"
#include "plugin/event/player_events.hpp"
#include "plugin/event/block_events.hpp"
#include "util/log/logger.hpp"
using namespace mcserver;
class HelloWorldPlugin : public Plugin {
public:
HelloWorldPlugin() {
description_.name = "HelloWorld";
description_.version = "1.0.0";
description_.author = "ExampleAuthor";
description_.description = "A simple example plugin demonstrating the plugin API";
}
void on_enable() override {
LOG_INFO_CAT("HelloWorld plugin enabled!", LogCategory::Plugin);
// Register event listeners
register_event_listeners();
}
void on_disable() override {
LOG_INFO_CAT("HelloWorld plugin disabled!", LogCategory::Plugin);
// Event listeners are automatically unregistered by the plugin loader
}
const PluginDescription& get_description() const override {
return description_;
}
private:
PluginDescription description_;
void register_event_listeners() {
auto* event_mgr = get_event_manager();
if (!event_mgr) return;
// Player join event - NORMAL priority
event_mgr->register_listener<PlayerJoinEvent>(
this,
EventPriority::NORMAL,
[this](PlayerJoinEvent& event) {
handle_player_join(event);
}
);
// Player quit event - NORMAL priority
event_mgr->register_listener<PlayerQuitEvent>(
this,
EventPriority::NORMAL,
[this](PlayerQuitEvent& event) {
handle_player_quit(event);
}
);
// Player chat event - HIGH priority (runs before other plugins)
event_mgr->register_listener<PlayerChatEvent>(
this,
EventPriority::HIGH,
[this](PlayerChatEvent& event) {
handle_player_chat(event);
}
);
// Block place event - NORMAL priority
event_mgr->register_listener<BlockPlaceEvent>(
this,
EventPriority::NORMAL,
[this](BlockPlaceEvent& event) {
handle_block_place(event);
},
true // Ignore cancelled events
);
// Block break event - NORMAL priority
event_mgr->register_listener<BlockBreakEvent>(
this,
EventPriority::NORMAL,
[this](BlockBreakEvent& event) {
handle_block_break(event);
},
true // Ignore cancelled events
);
LOG_INFO_CAT("Registered event listeners for HelloWorld plugin", LogCategory::Plugin);
}
void handle_player_join(PlayerJoinEvent& event) {
(void)event;
LOG_INFO_CAT("Player joined the server! Welcome!", LogCategory::Plugin);
// Could modify join message:
// event.set_join_message("§e[+] Player joined the game!");
}
void handle_player_quit(PlayerQuitEvent& event) {
(void)event;
LOG_INFO_CAT("Player left the server. Goodbye!", LogCategory::Plugin);
// Could modify quit message:
// event.set_quit_message("§e[-] Player left the game!");
}
void handle_player_chat(PlayerChatEvent& event) {
const std::string& message = event.get_message();
// Check for custom commands
if (message.starts_with("!hello")) {
LOG_INFO_CAT("Player said hello! Responding...", LogCategory::Plugin);
// Cancel the chat event and handle it ourselves
event.set_cancelled(true);
// In a proper implementation, you would send a message back to the player
// player->send_message("Hello to you too!");
} else if (message.starts_with("!help")) {
LOG_INFO_CAT("Player requested help", LogCategory::Plugin);
event.set_cancelled(true);
// Send help message
// player->send_message("§eAvailable commands:");
// player->send_message("§a!hello - Say hello");
// player->send_message("§a!help - Show this message");
}
}
void handle_block_place(BlockPlaceEvent& event) {
LOG_INFO_CAT("Block placed at (" +
std::to_string(event.get_x()) + ", " +
std::to_string(event.get_y()) + ", " +
std::to_string(event.get_z()) + ") - Type: " +
std::to_string(event.get_block_type()),
LogCategory::Plugin);
// Example: Prevent placing bedrock (block ID 7)
if (event.get_block_type() == 7) {
LOG_INFO_CAT("Preventing bedrock placement!", LogCategory::Plugin);
event.set_cancelled(true);
}
}
void handle_block_break(BlockBreakEvent& event) {
LOG_INFO_CAT("Block broken at (" +
std::to_string(event.get_x()) + ", " +
std::to_string(event.get_y()) + ", " +
std::to_string(event.get_z()) + ") - Type: " +
std::to_string(event.get_block_type()),
LogCategory::Plugin);
// Example: Prevent breaking bedrock
if (event.get_block_type() == 7) {
LOG_INFO_CAT("Preventing bedrock breakage!", LogCategory::Plugin);
event.set_cancelled(true);
}
}
};
// Plugin factory functions (required exports)
extern "C" {
Plugin* create_plugin() {
return new HelloWorldPlugin();
}
void destroy_plugin(Plugin* plugin) {
delete plugin;
}
}
+76
View File
@@ -0,0 +1,76 @@
#!/bin/bash
# Build script for Linux/macOS
# Usage: ./scripts/build_unix.sh [debug|release] [clean]
set -e
CONFIG="${1:-debug}"
CLEAN="${2}"
if [[ "$CONFIG" == "release" ]]; then
CMAKE_BUILD_TYPE="Release"
CONFIG_LOWER="release"
else
CMAKE_BUILD_TYPE="Debug"
CONFIG_LOWER="debug"
fi
echo "=== Minecraft Beta 1.7.3 Server - Unix Build ==="
echo "Configuration: $CMAKE_BUILD_TYPE"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
# Detect platform
if [[ "$OSTYPE" == "darwin"* ]]; then
PLATFORM="macos"
OS_NAME="macOS"
else
PLATFORM="linux"
OS_NAME="Linux"
fi
BUILD_DIR="$ROOT_DIR/build/$PLATFORM"
OUT_DIR="$ROOT_DIR/out/$OS_NAME-$(uname -m)/$CMAKE_BUILD_TYPE"
# Clean if requested
if [[ "$CLEAN" == "clean" ]]; then
echo "Cleaning build directory..."
rm -rf "$BUILD_DIR"
rm -rf "$OUT_DIR"
fi
# Create build directory
mkdir -p "$BUILD_DIR"
# Detect generator
if command -v ninja &> /dev/null; then
GENERATOR="Ninja"
echo "Using Ninja build system"
else
GENERATOR="Unix Makefiles"
echo "Using Make build system"
fi
# Configure CMake
echo "Configuring CMake..."
cd "$BUILD_DIR"
cmake -G "$GENERATOR" \
-DCMAKE_BUILD_TYPE="$CMAKE_BUILD_TYPE" \
-DBUILD_TESTS=ON \
"$ROOT_DIR"
# Build
echo "Building..."
cmake --build . --parallel "$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)"
echo ""
echo "=== Build completed successfully ==="
echo "Executable: $OUT_DIR/mcserver"
# Run tests if they exist
if [[ -f "$BUILD_DIR/tests/tests_unit" ]]; then
echo ""
echo "Running tests..."
ctest --output-on-failure
fi
+86
View File
@@ -0,0 +1,86 @@
@echo off
REM Build script for Windows
REM Usage: scripts\build_windows.bat [Debug|Release]
REM Note: Must be run from Windows CMD, not Git Bash/WSL
setlocal enabledelayedexpansion
set "CONFIG=%~1"
if "%CONFIG%"=="" set "CONFIG=Debug"
echo === Minecraft Beta 1.7.3 Server - Windows Build ===
echo Configuration: %CONFIG%
REM Get script directory and project root
set "SCRIPT_DIR=%~dp0"
REM Remove trailing backslash and go up one directory
for %%i in ("%SCRIPT_DIR%..") do set "ROOT_DIR=%%~fi"
echo Root directory: %ROOT_DIR%
REM Check if CMakeLists.txt exists
if not exist "%ROOT_DIR%\CMakeLists.txt" (
echo ERROR: CMakeLists.txt not found in %ROOT_DIR%
echo This script must be run from the scripts directory
exit /b 1
)
set "BUILD_DIR=%ROOT_DIR%\build\windows"
set "OUT_DIR=%ROOT_DIR%\out\Windows-x64\%CONFIG%"
REM Create build directory
if not exist "%BUILD_DIR%" mkdir "%BUILD_DIR%"
REM Detect generator
where ninja >nul 2>&1
if %ERRORLEVEL% EQU 0 (
echo Using Ninja build system
set "GENERATOR=Ninja"
) else (
where cl >nul 2>&1
if %ERRORLEVEL% EQU 0 (
echo Using Visual Studio 17 2022
set "GENERATOR=Visual Studio 17 2022"
) else (
echo ERROR: No suitable build system found
echo Please install Visual Studio 2022 or Ninja
exit /b 1
)
)
REM Configure CMake
echo.
echo Configuring CMake...
cd /d "%BUILD_DIR%"
if "%GENERATOR%"=="Ninja" (
cmake -G "Ninja" -DCMAKE_BUILD_TYPE=%CONFIG% -DBUILD_TESTS=ON "%ROOT_DIR%"
) else (
cmake -G "Visual Studio 17 2022" -A x64 -DCMAKE_BUILD_TYPE=%CONFIG% -DBUILD_TESTS=ON "%ROOT_DIR%"
)
if errorlevel 1 (
echo.
echo CMake configuration failed!
exit /b 1
)
REM Build
echo.
echo Building...
cmake --build . --config %CONFIG% --parallel
if errorlevel 1 (
echo.
echo Build failed!
exit /b 1
)
echo.
echo === Build completed successfully ===
echo Executable: %OUT_DIR%\mcserver.exe
if "%GENERATOR%"=="Visual Studio 17 2022" (
echo Visual Studio Solution: %BUILD_DIR%\MinecraftBeta173Server.sln
)
endlocal
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env pwsh
# Build script for Windows using PowerShell
# Usage: .\scripts\build_windows.ps1 [-Configuration Debug|Release] [-Clean]
param(
[ValidateSet("Debug", "Release", "RelWithDebInfo")]
[string]$Configuration = "Debug",
[switch]$Clean
)
$ErrorActionPreference = "Stop"
Write-Host "=== Minecraft Beta 1.7.3 Server - Windows Build ===" -ForegroundColor Cyan
Write-Host "Configuration: $Configuration" -ForegroundColor Green
$RootDir = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
$BuildDir = Join-Path $RootDir "build\windows"
$OutDir = Join-Path $RootDir "out\Windows-x64\$Configuration"
# Clean if requested
if ($Clean) {
Write-Host "Cleaning build directory..." -ForegroundColor Yellow
if (Test-Path $BuildDir) {
Remove-Item -Recurse -Force $BuildDir
}
if (Test-Path $OutDir) {
Remove-Item -Recurse -Force $OutDir
}
}
# Create build directory
New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null
# Configure CMake
Write-Host "Configuring CMake..." -ForegroundColor Yellow
Push-Location $BuildDir
try {
cmake -G "Visual Studio 17 2022" -A x64 `
-DCMAKE_BUILD_TYPE=$Configuration `
-DBUILD_TESTS=ON `
$RootDir
if ($LASTEXITCODE -ne 0) {
throw "CMake configuration failed"
}
# Build
Write-Host "Building..." -ForegroundColor Yellow
cmake --build . --config $Configuration --parallel
if ($LASTEXITCODE -ne 0) {
throw "Build failed"
}
Write-Host ""
Write-Host "=== Build completed successfully ===" -ForegroundColor Green
Write-Host "Executable: $OutDir\mcserver.exe" -ForegroundColor Cyan
Write-Host "Visual Studio Solution: $BuildDir\MinecraftBeta173Server.sln" -ForegroundColor Cyan
} finally {
Pop-Location
}
+12
View File
@@ -0,0 +1,12 @@
add_library(admin STATIC
admin_manager.cpp
admin_manager.hpp
)
target_include_directories(admin PUBLIC
${CMAKE_SOURCE_DIR}/src
)
target_link_libraries(admin PUBLIC util entity world)
set_target_properties(admin PROPERTIES FOLDER "Core")
+304
View File
@@ -0,0 +1,304 @@
#include "admin_manager.hpp"
#include "entity/player.hpp"
#include "entity/entity_manager.hpp"
#include "world/chunk/chunk_manager.hpp"
#include "util/log/logger.hpp"
#include <fstream>
#include <sstream>
#include <algorithm>
namespace mcserver {
AdminManager::AdminManager() {
// Auto-add default admin
add_admin("apfelteesaft_");
// Register built-in commands
register_builtin_commands();
}
void AdminManager::add_admin(const std::string& username) {
admins_.insert(username);
LOG_INFO_CAT("Added admin: " + username, LogCategory::General);
}
void AdminManager::remove_admin(const std::string& username) {
// Protect default admin
if (username == "apfelteesaft_") {
LOG_WARNING_CAT("Cannot remove default admin: " + username, LogCategory::General);
return;
}
admins_.erase(username);
LOG_INFO_CAT("Removed admin: " + username, LogCategory::General);
}
bool AdminManager::is_admin(const std::string& username) const {
return admins_.find(username) != admins_.end();
}
void AdminManager::save_admin_list(const std::string& file_path) {
std::ofstream file(file_path);
if (!file.is_open()) {
LOG_ERROR_CAT("Failed to save admin list to: " + file_path, LogCategory::General);
return;
}
for (const auto& admin : admins_) {
file << admin << "\n";
}
file.close();
LOG_INFO_CAT("Saved " + std::to_string(admins_.size()) + " admins to: " + file_path,
LogCategory::General);
}
void AdminManager::load_admin_list(const std::string& file_path) {
std::ifstream file(file_path);
if (!file.is_open()) {
LOG_INFO_CAT("No existing admin list found at: " + file_path, LogCategory::General);
return;
}
std::string username;
while (std::getline(file, username)) {
if (!username.empty()) {
admins_.insert(username);
}
}
file.close();
LOG_INFO_CAT("Loaded " + std::to_string(admins_.size()) + " admins from: " + file_path,
LogCategory::General);
}
void AdminManager::register_command(const std::string& name, CommandHandler handler, const std::string& usage) {
commands_[name] = handler;
if (!usage.empty()) {
command_usage_[name] = usage;
}
}
CommandResult AdminManager::execute_command(const std::string& command, Player* player) {
if (!player) {
return CommandResult::error("Invalid player");
}
// Check admin permission
if (!is_admin(player->get_username())) {
return CommandResult::error("§cYou don't have permission to use this command");
}
// Parse command
std::istringstream iss(command);
std::vector<std::string> tokens;
std::string token;
while (iss >> token) {
tokens.push_back(token);
}
if (tokens.empty()) {
return CommandResult::error("Empty command");
}
// Remove '/' prefix if present
std::string cmd_name = tokens[0];
if (cmd_name[0] == '/') {
cmd_name = cmd_name.substr(1);
}
// Get command arguments
std::vector<std::string> args(tokens.begin() + 1, tokens.end());
// Find and execute command
auto it = commands_.find(cmd_name);
if (it == commands_.end()) {
return CommandResult::error("§cUnknown command: /" + cmd_name);
}
return it->second(player, args);
}
void AdminManager::register_builtin_commands() {
register_command("fly", [this](Player* p, const std::vector<std::string>& args) {
return cmd_fly(p, args);
}, "/fly - Toggle flight mode");
register_command("give", [this](Player* p, const std::vector<std::string>& args) {
return cmd_give(p, args);
}, "/give <item_id> [amount] - Give yourself items");
register_command("tp", [this](Player* p, const std::vector<std::string>& args) {
return cmd_tp(p, args);
}, "/tp <x> <y> <z> - Teleport to coordinates");
register_command("gamemode", [this](Player* p, const std::vector<std::string>& args) {
return cmd_gamemode(p, args);
}, "/gamemode <0|1> - Change game mode (0=survival, 1=creative)");
register_command("time", [this](Player* p, const std::vector<std::string>& args) {
return cmd_time(p, args);
}, "/time <set|add> <value> - Change world time");
register_command("admin", [this](Player* p, const std::vector<std::string>& args) {
return cmd_admin(p, args);
}, "/admin <add|remove|list> [player] - Manage admins");
register_command("help", [this](Player* p, const std::vector<std::string>& args) {
return cmd_help(p, args);
}, "/help - Show available commands");
}
CommandResult AdminManager::cmd_fly(Player* player, const std::vector<std::string>& args) {
(void)player; // Unused for now
(void)args; // Unused for now
// Toggle flying ability
// Note: Beta 1.7.3 doesn't have native flying, but we can allow creative-style flying
// This would need to be implemented in the player movement handling
return CommandResult::ok("§aFlight mode toggled (not yet fully implemented)");
}
CommandResult AdminManager::cmd_give(Player* player, const std::vector<std::string>& args) {
if (args.empty()) {
return CommandResult::error("§cUsage: /give <item_id> [amount]");
}
// Simple validation - check if string is numeric
if (args[0].empty() || (!std::isdigit(args[0][0]) && args[0][0] != '-')) {
return CommandResult::error("§cInvalid item ID: " + args[0]);
}
i16 item_id = static_cast<i16>(std::atoi(args[0].c_str()));
i8 amount = 64;
if (args.size() >= 2) {
if (args[1].empty() || (!std::isdigit(args[1][0]) && args[1][0] != '-')) {
return CommandResult::error("§cInvalid amount: " + args[1]);
}
amount = static_cast<i8>(std::atoi(args[1].c_str()));
if (amount <= 0 || amount > 64) {
return CommandResult::error("§cAmount must be between 1 and 64");
}
}
// Add item to player inventory
Inventory* inv = player->get_inventory();
if (!inv) {
return CommandResult::error("§cInventory not available");
}
// Use the inventory's add_item method which correctly places items in slots 0-35
// (hotbar + main inventory), avoiding armor slots (36-39) and crafting slots (40-44)
auto item = std::make_unique<ItemStack>(item_id, amount, static_cast<i16>(0));
i8 remaining = inv->add_item(std::move(item));
if (remaining == 0) {
return CommandResult::ok("§aGave " + std::to_string(amount) + "x item " + std::to_string(item_id));
} else if (remaining < amount) {
i8 added = amount - remaining;
return CommandResult::ok("§aGave " + std::to_string(added) + "x item " + std::to_string(item_id) +
" (§c" + std::to_string(remaining) + " couldn't fit§a)");
} else {
return CommandResult::error("§cInventory is full");
}
}
CommandResult AdminManager::cmd_tp(Player* player, const std::vector<std::string>& args) {
if (args.size() < 3) {
return CommandResult::error("§cUsage: /tp <x> <y> <z>");
}
// Simple validation - just use atof which returns 0.0 on error (good enough for coordinates)
f64 x = std::atof(args[0].c_str());
f64 y = std::atof(args[1].c_str());
f64 z = std::atof(args[2].c_str());
player->set_position(x, y, z);
return CommandResult::ok("§aTeleported to " + std::to_string(x) + ", " +
std::to_string(y) + ", " + std::to_string(z));
}
CommandResult AdminManager::cmd_gamemode(Player* player, const std::vector<std::string>& args) {
(void)player; // Unused in Beta 1.7.3 (no gamemode state to set)
if (args.empty()) {
return CommandResult::error("§cUsage: /gamemode <0|1>");
}
// Beta 1.7.3 doesn't have official game modes, but we can track this for future features
if (args[0] == "0") {
return CommandResult::ok("§aSet game mode to Survival (not fully implemented in Beta 1.7.3)");
} else if (args[0] == "1") {
return CommandResult::ok("§aSet game mode to Creative (not fully implemented in Beta 1.7.3)");
} else {
return CommandResult::error("§cInvalid game mode. Use 0 for Survival or 1 for Creative");
}
}
CommandResult AdminManager::cmd_time(Player* player, const std::vector<std::string>& args) {
(void)player; // Unused - time is a world-level property
if (args.size() < 2) {
return CommandResult::error("§cUsage: /time <set|add> <value>");
}
// This would need to be implemented in the world/server time system
// For now, just return a placeholder
return CommandResult::ok("§aTime command received (world time system not yet implemented)");
}
CommandResult AdminManager::cmd_admin(Player* player, const std::vector<std::string>& args) {
(void)player; // Unused - admin management is global
if (args.empty()) {
return CommandResult::error("§cUsage: /admin <add|remove|list> [player]");
}
const std::string& subcmd = args[0];
if (subcmd == "list") {
std::string admin_list = "§aAdmins: ";
for (const auto& admin : admins_) {
admin_list += admin + ", ";
}
if (admin_list.size() > 10) {
admin_list = admin_list.substr(0, admin_list.size() - 2); // Remove trailing ", "
}
return CommandResult::ok(admin_list);
}
if (args.size() < 2) {
return CommandResult::error("§cUsage: /admin " + subcmd + " <player>");
}
const std::string& target = args[1];
if (subcmd == "add") {
add_admin(target);
save_admin_list("admins.txt");
return CommandResult::ok("§aAdded " + target + " to admins");
} else if (subcmd == "remove") {
if (target == "apfelteesaft_") {
return CommandResult::error("§cCannot remove default admin");
}
remove_admin(target);
save_admin_list("admins.txt");
return CommandResult::ok("§aRemoved " + target + " from admins");
} else {
return CommandResult::error("§cUnknown subcommand: " + subcmd);
}
}
CommandResult AdminManager::cmd_help(Player* player, const std::vector<std::string>& args) {
(void)player; // Unused
(void)args; // Unused
std::string help_text = "§aAvailable admin commands:\n";
for (const auto& [name, usage] : command_usage_) {
help_text += "§e" + usage + "\n";
}
return CommandResult::ok(help_text);
}
} // namespace mcserver
+79
View File
@@ -0,0 +1,79 @@
#pragma once
#include "util/types.hpp"
#include <string>
#include <unordered_set>
#include <functional>
namespace mcserver {
class Player;
class ChunkManager;
class EntityManager;
class MobManager;
// Admin command result
struct CommandResult {
bool success;
std::string message;
static CommandResult ok(const std::string& msg = "") {
return {true, msg};
}
static CommandResult error(const std::string& msg) {
return {false, msg};
}
};
// Admin command handler
using CommandHandler = std::function<CommandResult(Player*, const std::vector<std::string>&)>;
// Manages admin permissions and commands
class AdminManager {
public:
AdminManager();
// Admin management
void add_admin(const std::string& username);
void remove_admin(const std::string& username);
bool is_admin(const std::string& username) const;
void save_admin_list(const std::string& file_path);
void load_admin_list(const std::string& file_path);
// Command registration
void register_command(const std::string& name, CommandHandler handler, const std::string& usage = "");
// Command execution
CommandResult execute_command(const std::string& command, Player* player);
// Set manager references for commands
void set_chunk_manager(ChunkManager* manager) { chunk_manager_ = manager; }
void set_entity_manager(EntityManager* manager) { entity_manager_ = manager; }
void set_mob_manager(MobManager* manager) { mob_manager_ = manager; }
ChunkManager* get_chunk_manager() { return chunk_manager_; }
EntityManager* get_entity_manager() { return entity_manager_; }
MobManager* get_mob_manager() { return mob_manager_; }
private:
std::unordered_set<std::string> admins_;
std::unordered_map<std::string, CommandHandler> commands_;
std::unordered_map<std::string, std::string> command_usage_;
ChunkManager* chunk_manager_ = nullptr;
EntityManager* entity_manager_ = nullptr;
MobManager* mob_manager_ = nullptr;
// Built-in commands
void register_builtin_commands();
CommandResult cmd_fly(Player* player, const std::vector<std::string>& args);
CommandResult cmd_give(Player* player, const std::vector<std::string>& args);
CommandResult cmd_tp(Player* player, const std::vector<std::string>& args);
CommandResult cmd_gamemode(Player* player, const std::vector<std::string>& args);
CommandResult cmd_time(Player* player, const std::vector<std::string>& args);
CommandResult cmd_admin(Player* player, const std::vector<std::string>& args);
CommandResult cmd_help(Player* player, const std::vector<std::string>& args);
};
} // namespace mcserver
+18
View File
@@ -0,0 +1,18 @@
add_library(core STATIC
tick/tick_manager.cpp
tick/tick_manager.hpp
scheduler/job_system.cpp
scheduler/job_system.hpp
config/server_config.cpp
config/server_config.hpp
rng/random.cpp
rng/random.hpp
)
target_include_directories(core PUBLIC
${CMAKE_SOURCE_DIR}/src
)
target_link_libraries(core PUBLIC util platform)
set_target_properties(core PROPERTIES FOLDER "Core")
+152
View File
@@ -0,0 +1,152 @@
#include "server_config.hpp"
#include "platform/fs/file.hpp"
#include <sstream>
#include <algorithm>
#include <cstdlib>
namespace mcserver {
Result<void> ServerConfig::load(const std::string& path) {
auto content_result = File::read_all_text(path);
if (!content_result) {
// File doesn't exist, populate with defaults and save
set_defaults();
auto save_result = save(path);
if (!save_result) {
return save_result;
}
return Result<void>();
}
std::istringstream stream(content_result.value());
std::string line;
while (std::getline(stream, line)) {
line = trim(line);
// Skip comments and empty lines
if (line.empty() || line[0] == '#') {
continue;
}
// Parse key=value
auto equals_pos = line.find('=');
if (equals_pos != std::string::npos) {
std::string key = trim(line.substr(0, equals_pos));
std::string value = trim(line.substr(equals_pos + 1));
properties_[key] = value;
}
}
return Result<void>();
}
Result<void> ServerConfig::save(const std::string& path) const {
std::ostringstream oss;
oss << "# Minecraft Beta 1.7.3 Server Properties\n";
oss << "# Generated by Modern C++ Server\n\n";
for (const auto& [key, value] : properties_) {
oss << key << "=" << value << "\n";
}
return File::write_all_text(path, oss.str());
}
std::string ServerConfig::get_string(const std::string& key, const std::string& default_value) const {
auto it = properties_.find(key);
if (it != properties_.end()) {
return it->second;
}
return default_value;
}
i32 ServerConfig::get_int(const std::string& key, i32 default_value) const {
auto it = properties_.find(key);
if (it != properties_.end()) {
const char* str = it->second.c_str();
char* end;
long value = std::strtol(str, &end, 10);
if (end != str && *end == '\0') {
return static_cast<i32>(value);
}
}
return default_value;
}
bool ServerConfig::get_bool(const std::string& key, bool default_value) const {
auto it = properties_.find(key);
if (it != properties_.end()) {
std::string value = it->second;
std::transform(value.begin(), value.end(), value.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return value == "true" || value == "1" || value == "yes";
}
return default_value;
}
i64 ServerConfig::get_long(const std::string& key, i64 default_value) const {
auto it = properties_.find(key);
if (it != properties_.end()) {
const char* str = it->second.c_str();
char* end;
long long value = std::strtoll(str, &end, 10);
if (end != str && *end == '\0') {
return static_cast<i64>(value);
}
}
return default_value;
}
void ServerConfig::set_string(const std::string& key, const std::string& value) {
properties_[key] = value;
}
void ServerConfig::set_int(const std::string& key, i32 value) {
properties_[key] = std::to_string(value);
}
void ServerConfig::set_bool(const std::string& key, bool value) {
properties_[key] = value ? "true" : "false";
}
void ServerConfig::set_long(const std::string& key, i64 value) {
properties_[key] = std::to_string(value);
}
void ServerConfig::set_defaults() {
// Network settings
set_string("server-ip", "");
set_int("server-port", 25565);
// World settings
set_string("level-name", "world");
set_string("level-seed", "");
set_int("gamemode", 0); // 0 = Survival, 1 = Creative
set_bool("generate-structures", true);
// Server settings
set_bool("online-mode", false); // Offline mode for Beta 1.7.3
set_int("max-players", 20);
set_int("view-distance", 10);
set_string("motd", "A Minecraft Beta 1.7.3 Server");
// Gameplay settings
set_bool("spawn-animals", true);
set_bool("spawn-monsters", true);
set_bool("spawn-npcs", true);
set_bool("allow-nether", true);
set_bool("pvp", true);
set_bool("allow-flight", false);
// World generation settings
set_int("max-build-height", 128); // Beta 1.7.3 world height
}
std::string ServerConfig::trim(const std::string& str) {
auto start = std::find_if_not(str.begin(), str.end(), ::isspace);
auto end = std::find_if_not(str.rbegin(), str.rend(), ::isspace).base();
return (start < end) ? std::string(start, end) : std::string();
}
} // namespace mcserver
+55
View File
@@ -0,0 +1,55 @@
#pragma once
#include <string>
#include <map>
#include "util/types.hpp"
#include "util/result.hpp"
namespace mcserver {
class ServerConfig {
public:
ServerConfig() = default;
// Load from server.properties file
// If file doesn't exist, will populate with defaults and save
Result<void> load(const std::string& path);
// Save to server.properties file
Result<void> save(const std::string& path) const;
// Populate with default values
void set_defaults();
// Get properties
std::string get_string(const std::string& key, const std::string& default_value) const;
i32 get_int(const std::string& key, i32 default_value) const;
bool get_bool(const std::string& key, bool default_value) const;
i64 get_long(const std::string& key, i64 default_value) const;
// Set properties
void set_string(const std::string& key, const std::string& value);
void set_int(const std::string& key, i32 value);
void set_bool(const std::string& key, bool value);
void set_long(const std::string& key, i64 value);
// Common properties
std::string server_ip() const { return get_string("server-ip", ""); }
u16 server_port() const { return static_cast<u16>(get_int("server-port", 25565)); }
std::string level_name() const { return get_string("level-name", "world"); }
std::string level_seed() const { return get_string("level-seed", ""); }
bool online_mode() const { return get_bool("online-mode", true); }
bool spawn_animals() const { return get_bool("spawn-animals", true); }
bool spawn_monsters() const { return get_bool("spawn-monsters", true); }
bool pvp() const { return get_bool("pvp", true); }
bool allow_flight() const { return get_bool("allow-flight", false); }
bool allow_nether() const { return get_bool("allow-nether", true); }
i32 max_players() const { return get_int("max-players", 20); }
private:
std::map<std::string, std::string> properties_;
static std::string trim(const std::string& str);
};
} // namespace mcserver
+63
View File
@@ -0,0 +1,63 @@
#include "random.hpp"
namespace mcserver {
// Java LCG constants for compatibility
static constexpr i64 MULTIPLIER = 0x5DEECE66DLL;
static constexpr i64 ADDEND = 0xBLL;
static constexpr i64 MASK = (1LL << 48) - 1;
Random::Random(i64 seed) {
set_seed(seed);
}
void Random::set_seed(i64 seed) {
seed_ = (seed ^ MULTIPLIER) & MASK;
}
i64 Random::next_seed() {
seed_ = (seed_ * MULTIPLIER + ADDEND) & MASK;
return seed_;
}
i32 Random::next_int() {
return static_cast<i32>(next_seed() >> 16);
}
i32 Random::next_int(i32 bound) {
if (bound <= 0) {
return 0;
}
if ((bound & -bound) == bound) {
// Power of 2
return static_cast<i32>((bound * static_cast<i64>(next_seed() >> 16)) >> 31);
}
i32 bits, val;
do {
bits = static_cast<i32>(next_seed() >> 16);
val = bits % bound;
} while (bits - val + (bound - 1) < 0);
return val;
}
i64 Random::next_long() {
return (static_cast<i64>(next_int()) << 32) + next_int();
}
f32 Random::next_float() {
return static_cast<f32>(next_seed() >> 16) / static_cast<f32>(1 << 24);
}
f64 Random::next_double() {
return static_cast<f64>((static_cast<i64>(next_seed() >> 16) << 27) + (next_seed() >> 16)) /
static_cast<f64>(1LL << 53);
}
bool Random::next_bool() {
return next_int() >= 0;
}
} // namespace mcserver
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include "util/types.hpp"
#include <random>
namespace mcserver {
// Java-compatible random number generator for world generation parity
class Random {
public:
explicit Random(i64 seed = 0);
void set_seed(i64 seed);
i64 get_seed() const { return seed_; }
// Generate random integer
i32 next_int();
i32 next_int(i32 bound);
// Generate random long
i64 next_long();
// Generate random float [0, 1)
f32 next_float();
// Generate random double [0, 1)
f64 next_double();
// Generate random boolean
bool next_bool();
private:
i64 seed_;
i64 next_seed();
};
} // namespace mcserver
+96
View File
@@ -0,0 +1,96 @@
#include "job_system.hpp"
#include <condition_variable>
namespace mcserver {
JobSystem::JobSystem(u32 num_threads) {
if (num_threads == 0) {
num_threads = Thread::hardware_concurrency();
if (num_threads == 0) num_threads = 4;
}
workers_.reserve(num_threads);
}
JobSystem::~JobSystem() {
stop();
}
void JobSystem::start() {
if (running_) return;
running_ = true;
u32 count = static_cast<u32>(workers_.capacity());
for (u32 i = 0; i < count; ++i) {
workers_.emplace_back([this]() { worker_thread(); });
}
}
void JobSystem::stop() {
if (!running_) return;
{
std::lock_guard<std::mutex> lock(queue_mutex_);
running_ = false;
}
cv_.notify_all();
for (auto& worker : workers_) {
if (worker.joinable()) {
worker.join();
}
}
workers_.clear();
}
void JobSystem::submit(Job job) {
{
std::lock_guard<std::mutex> lock(queue_mutex_);
job_queue_.push(std::move(job));
++active_jobs_;
}
cv_.notify_one();
}
void JobSystem::wait_all() {
while (true) {
std::unique_lock<std::mutex> lock(queue_mutex_);
if (job_queue_.empty() && active_jobs_ == 0) {
break;
}
lock.unlock();
std::this_thread::yield();
}
}
void JobSystem::worker_thread() {
while (true) {
Job job;
{
std::unique_lock<std::mutex> lock(queue_mutex_);
cv_.wait(lock, [this]() { return !running_ || !job_queue_.empty(); });
if (!running_ && job_queue_.empty()) {
return;
}
if (!job_queue_.empty()) {
job = std::move(job_queue_.front());
job_queue_.pop();
}
}
if (job) {
job();
std::lock_guard<std::mutex> lock(queue_mutex_);
--active_jobs_;
}
}
}
} // namespace mcserver
+41
View File
@@ -0,0 +1,41 @@
#pragma once
#include <functional>
#include <vector>
#include <queue>
#include <mutex>
#include <condition_variable>
#include "platform/thread/thread.hpp"
#include "util/types.hpp"
namespace mcserver {
// Simple job system for parallel tasks
// Step 1: Basic implementation, Step 2 will add work-stealing
class JobSystem {
public:
using Job = std::function<void()>;
explicit JobSystem(u32 num_threads = 0);
~JobSystem();
void start();
void stop();
void submit(Job job);
void wait_all();
u32 thread_count() const { return static_cast<u32>(workers_.size()); }
private:
void worker_thread();
std::vector<Thread> workers_;
std::queue<Job> job_queue_;
std::mutex queue_mutex_;
std::condition_variable cv_;
bool running_ = false;
u32 active_jobs_ = 0;
};
} // namespace mcserver
+65
View File
@@ -0,0 +1,65 @@
#include "tick_manager.hpp"
#include <algorithm>
namespace mcserver {
TickManager::TickManager()
: tick_count_(0)
, accumulated_time_ms_(0)
, last_tick_time_ms_(0)
, avg_tick_time_ms_(0.0) {
reset();
}
void TickManager::reset() {
tick_count_ = 0;
accumulated_time_ms_ = 0;
last_update_time_ = Clock::now();
last_tick_time_ms_ = 0;
avg_tick_time_ms_ = 0.0;
}
bool TickManager::should_tick(i64& ticks_to_run) {
auto now = Clock::now();
i64 elapsed_ms = Clock::elapsed_ms(last_update_time_);
// Detect time going backwards
if (elapsed_ms < 0) {
last_update_time_ = now;
return false;
}
// Prevent runaway catchup
if (elapsed_ms > MAX_TICK_TIME_MS) {
elapsed_ms = MAX_TICK_TIME_MS;
}
accumulated_time_ms_ += elapsed_ms;
last_update_time_ = now;
ticks_to_run = accumulated_time_ms_ / TARGET_MS_PER_TICK;
if (ticks_to_run > 0) {
accumulated_time_ms_ -= ticks_to_run * TARGET_MS_PER_TICK;
return true;
}
return false;
}
void TickManager::tick_started() {
tick_start_time_ = Clock::now();
}
void TickManager::tick_finished() {
++tick_count_;
last_tick_time_ms_ = Clock::elapsed_ms(tick_start_time_);
// Moving average for tick time
constexpr f64 ALPHA = 0.1;
avg_tick_time_ms_ = ALPHA * static_cast<f64>(last_tick_time_ms_) +
(1.0 - ALPHA) * avg_tick_time_ms_;
}
} // namespace mcserver
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include "util/types.hpp"
#include "platform/time/clock.hpp"
#include <functional>
namespace mcserver {
// Manages deterministic server tick loop
// Beta 1.7.3 runs at 20 TPS (50ms per tick)
class TickManager {
public:
static constexpr i64 TARGET_MS_PER_TICK = 50; // 20 TPS
static constexpr i64 MAX_TICK_TIME_MS = 2000; // Max catchup time
TickManager();
void reset();
bool should_tick(i64& ticks_to_run);
i64 current_tick() const { return tick_count_; }
f64 average_tick_time_ms() const { return avg_tick_time_ms_; }
i64 last_tick_time_ms() const { return last_tick_time_ms_; }
void tick_started();
void tick_finished();
private:
i64 tick_count_;
i64 accumulated_time_ms_;
Clock::time_point last_update_time_;
Clock::time_point tick_start_time_;
i64 last_tick_time_ms_;
f64 avg_tick_time_ms_;
};
} // namespace mcserver
+41
View File
@@ -0,0 +1,41 @@
add_library(entity STATIC
entity.cpp
entity.hpp
entity_manager.cpp
entity_manager.hpp
player.cpp
player.hpp
inventory/item_stack.cpp
inventory/item_stack.hpp
inventory/inventory.cpp
inventory/inventory.hpp
item/item_entity.hpp
item/item_entity_manager.cpp
item/item_entity_manager.hpp
crafting/crafting_recipe.cpp
crafting/crafting_recipe.hpp
mob/mob_type.hpp
mob/mob_ai.hpp
mob/mob_metadata.cpp
mob/mob_metadata.hpp
mob/pathfinding.cpp
mob/pathfinding.hpp
mob/mob.cpp
mob/mob.hpp
mob/passive_mob.cpp
mob/passive_mob.hpp
mob/hostile_mob.cpp
mob/hostile_mob.hpp
mob/mob_manager.cpp
mob/mob_manager.hpp
mob/mob_spawner.cpp
mob/mob_spawner.hpp
)
target_include_directories(entity PUBLIC
${CMAKE_SOURCE_DIR}/src
)
target_link_libraries(entity PUBLIC core util platform)
set_target_properties(entity PROPERTIES FOLDER "Gameplay")
+182
View File
@@ -0,0 +1,182 @@
#include "crafting_recipe.hpp"
#include "world/chunk/chunk.hpp"
#include <algorithm>
#include <unordered_map>
namespace mcserver {
// Use BlockId from chunk.hpp
using BT = BlockId;
// ShapedRecipe implementation
ShapedRecipe::ShapedRecipe(const std::string& name, const std::vector<std::vector<i16>>& pattern,
i16 result_id, i32 result_count)
: name_(name)
, pattern_(pattern)
, result_id_(result_id)
, result_count_(result_count) {
pattern_height_ = static_cast<i32>(pattern_.size());
pattern_width_ = pattern_height_ > 0 ? static_cast<i32>(pattern_[0].size()) : 0;
}
bool ShapedRecipe::matches(const std::vector<ItemStack>& grid, i32 width, i32 height) const {
// Try to match pattern at different offsets in the grid
for (i32 offset_y = 0; offset_y <= height - pattern_height_; ++offset_y) {
for (i32 offset_x = 0; offset_x <= width - pattern_width_; ++offset_x) {
if (matches_at_offset(grid, width, height, offset_x, offset_y)) {
return true;
}
}
}
return false;
}
bool ShapedRecipe::matches_at_offset(const std::vector<ItemStack>& grid, i32 grid_width, i32 grid_height,
i32 offset_x, i32 offset_y) const {
// Check if all cells outside the pattern are empty
for (i32 y = 0; y < grid_height; ++y) {
for (i32 x = 0; x < grid_width; ++x) {
i32 grid_idx = y * grid_width + x;
bool in_pattern = (x >= offset_x && x < offset_x + pattern_width_ &&
y >= offset_y && y < offset_y + pattern_height_);
if (in_pattern) {
i32 pattern_x = x - offset_x;
i32 pattern_y = y - offset_y;
i16 required = pattern_[pattern_y][pattern_x];
// Check if the item matches the pattern
if (required == 0) {
// Must be empty
if (grid[grid_idx].get_item_id() != 0) {
return false;
}
} else if (required == -1) {
// Any item is fine
if (grid[grid_idx].get_item_id() == 0) {
return false;
}
} else {
// Must match specific item
if (grid[grid_idx].get_item_id() != required) {
return false;
}
}
} else {
// Outside pattern - must be empty
if (grid[grid_idx].get_item_id() != 0) {
return false;
}
}
}
}
return true;
}
ItemStack ShapedRecipe::get_result() const {
return ItemStack(result_id_, static_cast<i8>(result_count_));
}
// ShapelessRecipe implementation
ShapelessRecipe::ShapelessRecipe(const std::string& name, const std::vector<i16>& ingredients,
i16 result_id, i32 result_count)
: name_(name)
, ingredients_(ingredients)
, result_id_(result_id)
, result_count_(result_count) {
}
bool ShapelessRecipe::matches(const std::vector<ItemStack>& grid, i32 width, i32 height) const {
(void)width; // Unused for shapeless recipes
(void)height;
// Count items in grid
std::unordered_map<i16, i32> grid_counts;
for (const auto& stack : grid) {
if (stack.get_item_id() != 0) {
grid_counts[stack.get_item_id()]++;
}
}
// Count required ingredients
std::unordered_map<i16, i32> ingredient_counts;
for (i16 ingredient : ingredients_) {
ingredient_counts[ingredient]++;
}
// Check if grid has exactly the required ingredients
return grid_counts == ingredient_counts;
}
ItemStack ShapelessRecipe::get_result() const {
return ItemStack(result_id_, static_cast<i8>(result_count_));
}
// RecipeManager implementation
RecipeManager::RecipeManager() {
register_default_recipes();
}
const CraftingRecipe* RecipeManager::find_recipe(const std::vector<ItemStack>& grid,
i32 width, i32 height) const {
for (const auto& recipe : recipes_) {
if (recipe->matches(grid, width, height)) {
return recipe.get();
}
}
return nullptr;
}
void RecipeManager::add_recipe(std::unique_ptr<CraftingRecipe> recipe) {
recipes_.push_back(std::move(recipe));
}
void RecipeManager::add_shaped_recipe(const std::string& name,
const std::vector<std::vector<i16>>& pattern,
i16 result_id, i32 result_count) {
recipes_.push_back(std::make_unique<ShapedRecipe>(name, pattern, result_id, result_count));
}
void RecipeManager::add_shapeless_recipe(const std::string& name,
const std::vector<i16>& ingredients,
i16 result_id, i32 result_count) {
recipes_.push_back(std::make_unique<ShapelessRecipe>(name, ingredients, result_id, result_count));
}
void RecipeManager::register_default_recipes() {
// Planks from logs (shapeless)
add_shapeless_recipe("planks_from_log", {static_cast<i16>(BT::Wood)},
static_cast<i16>(BT::WoodPlanks), 4);
// Sticks from planks (2x1 shaped)
add_shaped_recipe("sticks", {
{static_cast<i16>(BT::WoodPlanks)},
{static_cast<i16>(BT::WoodPlanks)}
}, 280, 4); // Item ID 280 = Stick
// Crafting table (2x2 shaped)
add_shaped_recipe("crafting_table", {
{static_cast<i16>(BT::WoodPlanks), static_cast<i16>(BT::WoodPlanks)},
{static_cast<i16>(BT::WoodPlanks), static_cast<i16>(BT::WoodPlanks)}
}, 58, 1); // Block ID 58 = Crafting Table
// Torches (shapeless - 1 coal item + 1 stick)
add_shapeless_recipe("torch", {263, 280}, // 263 = Coal (item), 280 = Stick
50, 4); // Block ID 50 = Torch
// Wool from string (2x2 shaped)
add_shaped_recipe("wool_from_string", {
{287, 287}, // 287 = String
{287, 287}
}, 35, 1); // Block ID 35 = Wool
// Wooden tools (require 3x3 crafting table, but we can add 2x2 recipes)
// Note: Most tools require 3x3 crafting, but we can add some simple 2x2 recipes
// For full crafting support, players need to craft a crafting table first
}
} // namespace mcserver
+106
View File
@@ -0,0 +1,106 @@
#pragma once
#include "entity/inventory/item_stack.hpp"
#include "util/types.hpp"
#include <vector>
#include <memory>
#include <string>
namespace mcserver {
// Crafting recipe types
enum class RecipeType {
Shaped, // Pattern must match exactly (e.g., pickaxe)
Shapeless // Items can be in any position (e.g., flint and steel)
};
// Base crafting recipe class
class CraftingRecipe {
public:
virtual ~CraftingRecipe() = default;
// Check if the crafting grid matches this recipe
virtual bool matches(const std::vector<ItemStack>& grid, i32 width, i32 height) const = 0;
// Get the result of this recipe
virtual ItemStack get_result() const = 0;
// Get the recipe type
virtual RecipeType get_type() const = 0;
// Get recipe name (for debugging)
virtual std::string get_name() const = 0;
};
// Shaped recipe (pattern must match)
class ShapedRecipe : public CraftingRecipe {
public:
ShapedRecipe(const std::string& name, const std::vector<std::vector<i16>>& pattern,
i16 result_id, i32 result_count = 1);
bool matches(const std::vector<ItemStack>& grid, i32 width, i32 height) const override;
ItemStack get_result() const override;
RecipeType get_type() const override { return RecipeType::Shaped; }
std::string get_name() const override { return name_; }
private:
std::string name_;
std::vector<std::vector<i16>> pattern_; // -1 = any item, 0 = empty, >0 = specific item
i16 result_id_;
i32 result_count_;
i32 pattern_width_;
i32 pattern_height_;
// Check if pattern matches at a specific offset in the grid
bool matches_at_offset(const std::vector<ItemStack>& grid, i32 grid_width, i32 grid_height,
i32 offset_x, i32 offset_y) const;
};
// Shapeless recipe (items can be in any position)
class ShapelessRecipe : public CraftingRecipe {
public:
ShapelessRecipe(const std::string& name, const std::vector<i16>& ingredients,
i16 result_id, i32 result_count = 1);
bool matches(const std::vector<ItemStack>& grid, i32 width, i32 height) const override;
ItemStack get_result() const override;
RecipeType get_type() const override { return RecipeType::Shapeless; }
std::string get_name() const override { return name_; }
private:
std::string name_;
std::vector<i16> ingredients_;
i16 result_id_;
i32 result_count_;
};
// Recipe manager - holds all crafting recipes
class RecipeManager {
public:
RecipeManager();
// Find a matching recipe for the given crafting grid
const CraftingRecipe* find_recipe(const std::vector<ItemStack>& grid, i32 width, i32 height) const;
// Add a custom recipe
void add_recipe(std::unique_ptr<CraftingRecipe> recipe);
// Get all recipes (for recipe book, etc.)
const std::vector<std::unique_ptr<CraftingRecipe>>& get_all_recipes() const {
return recipes_;
}
private:
std::vector<std::unique_ptr<CraftingRecipe>> recipes_;
// Register default Beta 1.7.3 recipes
void register_default_recipes();
// Helper methods for adding recipes
void add_shaped_recipe(const std::string& name, const std::vector<std::vector<i16>>& pattern,
i16 result_id, i32 result_count = 1);
void add_shapeless_recipe(const std::string& name, const std::vector<i16>& ingredients,
i16 result_id, i32 result_count = 1);
};
} // namespace mcserver
+1
View File
@@ -0,0 +1 @@
// Stub for Step 1
+11
View File
@@ -0,0 +1,11 @@
#pragma once
// Placeholder for Step 2
namespace mcserver {
class Entity {
// Will be implemented in Step 2
};
} // namespace mcserver
+66
View File
@@ -0,0 +1,66 @@
#pragma once
#include "util/types.hpp"
#include <vector>
#include <mutex>
namespace mcserver {
// Manages entity IDs with recycling
// Thread-safe entity ID allocation and deallocation
class EntityIdManager {
public:
EntityIdManager() : next_id_(1) {}
// Allocate a new entity ID
i32 allocate() {
std::lock_guard<std::mutex> lock(mutex_);
// Try to reuse a freed ID first
if (!freed_ids_.empty()) {
i32 id = freed_ids_.back();
freed_ids_.pop_back();
return id;
}
// Otherwise, allocate a new ID
return next_id_++;
}
// Free an entity ID for reuse
void free(i32 id) {
std::lock_guard<std::mutex> lock(mutex_);
// Add to freed IDs list for reuse
// Only add if it's not already in the list (prevent duplicates)
if (id > 0 && id < next_id_) {
freed_ids_.push_back(id);
}
}
// Reset the manager (useful for tests or server restart)
void reset() {
std::lock_guard<std::mutex> lock(mutex_);
next_id_ = 1;
freed_ids_.clear();
}
// Get the total number of IDs allocated (including freed ones)
i32 get_total_allocated() const {
std::lock_guard<std::mutex> lock(mutex_);
return next_id_ - 1;
}
// Get the number of active (not freed) IDs
i32 get_active_count() const {
std::lock_guard<std::mutex> lock(mutex_);
return (next_id_ - 1) - static_cast<i32>(freed_ids_.size());
}
private:
mutable std::mutex mutex_;
i32 next_id_;
std::vector<i32> freed_ids_;
};
} // namespace mcserver
+168
View File
@@ -0,0 +1,168 @@
#include "entity_manager.hpp"
#include "net/session/client_session.hpp"
#include "util/log/logger.hpp"
#include <cmath>
namespace mcserver {
void EntityManager::add_player(Player* player, ClientSession* session) {
if (!player || !session) {
return;
}
i32 entity_id = player->get_entity_id();
players_[entity_id] = player;
player_sessions_[entity_id] = session;
// Set up health change callback for this player
if (health_change_callback_) {
player->set_health_change_callback(health_change_callback_);
}
// Set up death callback for this player
if (death_callback_) {
player->set_death_callback(death_callback_);
}
LOG_DEBUG_CAT("EntityManager: Added player " + player->get_username() +
" (entity ID: " + std::to_string(entity_id) + ")",
LogCategory::Entity);
}
void EntityManager::remove_player(i32 entity_id) {
auto it = players_.find(entity_id);
if (it != players_.end()) {
LOG_DEBUG_CAT("EntityManager: Removed player entity ID " + std::to_string(entity_id),
LogCategory::Entity);
players_.erase(it);
player_sessions_.erase(entity_id);
// Free the entity ID for reuse
id_manager_.free(entity_id);
}
}
Player* EntityManager::get_player(i32 entity_id) {
auto it = players_.find(entity_id);
if (it != players_.end()) {
return it->second;
}
return nullptr;
}
std::vector<Player*> EntityManager::get_other_players(i32 exclude_entity_id) {
std::vector<Player*> result;
for (auto& [entity_id, player] : players_) {
if (entity_id != exclude_entity_id) {
result.push_back(player);
}
}
return result;
}
std::vector<Player*> EntityManager::get_all_players() {
std::vector<Player*> result;
result.reserve(players_.size());
for (auto& [entity_id, player] : players_) {
result.push_back(player);
}
return result;
}
ClientSession* EntityManager::get_player_session(i32 entity_id) {
auto it = player_sessions_.find(entity_id);
if (it != player_sessions_.end()) {
return it->second;
}
return nullptr;
}
void EntityManager::spawn_existing_entities_for(ClientSession* new_client) {
if (!new_client || !spawn_player_callback_) {
return;
}
const Player* new_player = new_client->get_player();
if (!new_player) {
return;
}
// Spawn all existing players to the new client
for (auto& [entity_id, player] : players_) {
if (entity_id != new_player->get_entity_id()) {
// Check if in range (for now, spawn all - can add range check later)
if (is_in_range(new_player, player)) {
spawn_player_callback_(new_client, player);
}
}
}
LOG_DEBUG_CAT("EntityManager: Spawned " + std::to_string(players_.size() - 1) +
" existing entities for " + new_player->get_username(),
LogCategory::Entity);
}
void EntityManager::spawn_entity_for_nearby_players(Player* player, ClientSession* exclude_session) {
if (!player || !spawn_player_callback_) {
return;
}
// Spawn this player to all other connected players
for (auto& [entity_id, session] : player_sessions_) {
if (session != exclude_session) {
const Player* other_player = session->get_player();
if (other_player && other_player->get_entity_id() != player->get_entity_id()) {
// Check if in range
if (is_in_range(player, other_player)) {
spawn_player_callback_(session, player);
}
}
}
}
LOG_DEBUG_CAT("EntityManager: Spawned " + player->get_username() +
" for nearby players",
LogCategory::Entity);
}
void EntityManager::despawn_entity_for_all(i32 entity_id) {
if (!despawn_entity_callback_) {
return;
}
// Send despawn packet to all connected players
for (auto& [eid, session] : player_sessions_) {
if (eid != entity_id) {
despawn_entity_callback_(session, entity_id);
}
}
LOG_DEBUG_CAT("EntityManager: Despawned entity ID " + std::to_string(entity_id) +
" for all players",
LogCategory::Entity);
}
void EntityManager::tick() {
// Placeholder for future entity updates
// This could handle:
// - Periodic position broadcasts
// - Entity AI updates
// - Entity despawning when players move out of range
}
bool EntityManager::is_in_range(const Player* p1, const Player* p2, f64 range) const {
if (!p1 || !p2) {
return false;
}
f64 dx = p1->get_x() - p2->get_x();
f64 dy = p1->get_y() - p2->get_y();
f64 dz = p1->get_z() - p2->get_z();
f64 distance_squared = dx * dx + dy * dy + dz * dz;
f64 range_squared = range * range;
return distance_squared <= range_squared;
}
} // namespace mcserver
+89
View File
@@ -0,0 +1,89 @@
#pragma once
#include "util/types.hpp"
#include "entity/player.hpp"
#include "entity/entity_id_manager.hpp"
#include <vector>
#include <unordered_map>
#include <memory>
#include <functional>
namespace mcserver {
class ClientSession;
// Callback for spawning an entity to a specific client
using SpawnPlayerCallback = std::function<void(ClientSession* viewer, const Player* player)>;
// Callback for despawning an entity from a specific client
using DespawnEntityCallback = std::function<void(ClientSession* viewer, i32 entity_id)>;
// Callback for when player health changes (entity_id, new_health, took_damage)
using HealthChangeCallback = std::function<void(i32 entity_id, i16 health, bool took_damage)>;
// Callback for when player dies (entity_id)
using PlayerDeathCallback = std::function<void(i32 entity_id)>;
// EntityManager tracks all entities in the world and manages entity visibility
class EntityManager {
public:
EntityManager() = default;
// Get entity ID manager
EntityIdManager* get_id_manager() { return &id_manager_; }
// Set callbacks
void set_spawn_player_callback(SpawnPlayerCallback callback) {
spawn_player_callback_ = std::move(callback);
}
void set_despawn_entity_callback(DespawnEntityCallback callback) {
despawn_entity_callback_ = std::move(callback);
}
void set_health_change_callback(HealthChangeCallback callback) {
health_change_callback_ = std::move(callback);
}
void set_death_callback(PlayerDeathCallback callback) {
death_callback_ = std::move(callback);
}
// Player management
void add_player(Player* player, ClientSession* session);
void remove_player(i32 entity_id);
Player* get_player(i32 entity_id);
// Get all players except the specified one
std::vector<Player*> get_other_players(i32 exclude_entity_id);
// Get all players as a vector
std::vector<Player*> get_all_players();
// Get client session for a player
ClientSession* get_player_session(i32 entity_id);
// Entity visibility management
void spawn_existing_entities_for(ClientSession* new_client);
void spawn_entity_for_nearby_players(Player* player, ClientSession* exclude_session = nullptr);
void despawn_entity_for_all(i32 entity_id);
// Tick for entity updates
void tick();
private:
// Entity ID manager for allocation/deallocation
EntityIdManager id_manager_;
// Map of entity ID -> Player
std::unordered_map<i32, Player*> players_;
// Map of entity ID -> ClientSession (for sending packets)
std::unordered_map<i32, ClientSession*> player_sessions_;
SpawnPlayerCallback spawn_player_callback_;
DespawnEntityCallback despawn_entity_callback_;
HealthChangeCallback health_change_callback_;
PlayerDeathCallback death_callback_;
// Check if two entities are within visibility range
bool is_in_range(const Player* p1, const Player* p2, f64 range = 128.0) const;
};
} // namespace mcserver
+293
View File
@@ -0,0 +1,293 @@
#include "entity/inventory/inventory.hpp"
#include "entity/crafting/crafting_recipe.hpp"
#include <algorithm>
namespace mcserver {
Inventory::Inventory()
: current_slot_(0), dirty_(false) {
// Initialize all slots as empty
slots_.reserve(TOTAL_SIZE);
for (i32 i = 0; i < TOTAL_SIZE; ++i) {
slots_.push_back(std::make_unique<ItemStack>());
}
}
ItemStack* Inventory::get_slot(i32 slot) {
if (!is_valid_slot(slot)) {
return nullptr;
}
return slots_[slot].get();
}
const ItemStack* Inventory::get_slot(i32 slot) const {
if (!is_valid_slot(slot)) {
return nullptr;
}
return slots_[slot].get();
}
void Inventory::set_slot(i32 slot, std::unique_ptr<ItemStack> stack) {
if (!is_valid_slot(slot)) {
return;
}
slots_[slot] = std::move(stack);
mark_dirty();
}
void Inventory::clear_slot(i32 slot) {
if (!is_valid_slot(slot)) {
return;
}
slots_[slot] = std::make_unique<ItemStack>();
mark_dirty();
}
ItemStack* Inventory::get_held_item() {
if (current_slot_ < 0 || current_slot_ >= HOTBAR_SIZE) {
return nullptr;
}
return slots_[current_slot_].get();
}
const ItemStack* Inventory::get_held_item() const {
if (current_slot_ < 0 || current_slot_ >= HOTBAR_SIZE) {
return nullptr;
}
return slots_[current_slot_].get();
}
void Inventory::set_current_slot(i32 slot) {
if (slot >= 0 && slot < HOTBAR_SIZE) {
current_slot_ = slot;
mark_dirty();
}
}
i32 Inventory::find_item(i16 item_id) const {
for (i32 i = 0; i < TOTAL_SIZE; ++i) {
const auto* stack = slots_[i].get();
if (stack && !stack->is_empty() && stack->get_item_id() == item_id) {
return i;
}
}
return -1;
}
i32 Inventory::find_empty_slot() const {
// Only search hotbar (0-8) and main inventory (9-35), not armor/crafting slots
// This ensures items are placed in visible slots that get synced to the client
for (i32 i = 0; i < ARMOR_START; ++i) { // ARMOR_START = 36
const auto* stack = slots_[i].get();
if (stack && stack->is_empty()) {
return i;
}
}
return -1;
}
i32 Inventory::find_stackable_slot(const ItemStack& stack) const {
// Only search hotbar (0-8) and main inventory (9-35), not armor/crafting slots
// This ensures items are placed in visible slots that get synced to the client
for (i32 i = 0; i < ARMOR_START; ++i) { // ARMOR_START = 36
const auto* existing = slots_[i].get();
if (existing && !existing->is_empty() && existing->can_stack_with(stack)) {
// Check if there's room to add more
if (existing->get_count() < existing->get_max_stack_size()) {
return i;
}
}
}
return -1;
}
i8 Inventory::add_item(std::unique_ptr<ItemStack> stack) {
if (!stack || stack->is_empty()) {
return 0;
}
i8 remaining = stack->get_count();
i16 item_id = stack->get_item_id();
i16 damage = stack->get_damage();
// First, try to stack with existing items
while (remaining > 0) {
i32 slot = find_stackable_slot(*stack);
if (slot < 0) {
break;
}
auto* existing = slots_[slot].get();
i8 max_size = existing->get_max_stack_size();
i8 current = existing->get_count();
i8 can_add = std::min(remaining, static_cast<i8>(max_size - current));
existing->increase_count(can_add);
remaining -= can_add;
mark_dirty();
}
// Then, try to add to empty slots
while (remaining > 0) {
i32 slot = find_empty_slot();
if (slot < 0) {
break; // No more room
}
i8 max_size = stack->get_max_stack_size();
i8 to_add = std::min(remaining, max_size);
slots_[slot] = std::make_unique<ItemStack>(item_id, to_add, damage);
remaining -= to_add;
mark_dirty();
}
return remaining;
}
bool Inventory::can_add_item(const ItemStack& stack) const {
if (stack.is_empty()) {
return true; // Empty stack can always be "added"
}
i8 remaining = stack.get_count();
// Check if we can stack with existing items
for (const auto& slot : slots_) {
if (!slot || slot->is_empty()) {
continue;
}
if (slot->get_item_id() == stack.get_item_id() &&
slot->get_damage() == stack.get_damage()) {
i8 max_size = slot->get_max_stack_size();
i8 current = slot->get_count();
i8 can_add = max_size - current;
if (can_add > 0) {
remaining -= can_add;
if (remaining <= 0) {
return true; // Can fit entirely in existing stacks
}
}
}
}
// Check if we have empty slots
i32 empty_slots = 0;
for (const auto& slot : slots_) {
if (!slot || slot->is_empty()) {
empty_slots++;
}
}
// Calculate how many items can fit in empty slots
i8 max_size = stack.get_max_stack_size();
i8 can_fit_in_empty = static_cast<i8>(empty_slots * max_size);
return remaining <= can_fit_in_empty;
}
bool Inventory::remove_item(i16 item_id, i8 count) {
if (count <= 0) {
return true;
}
// First, check if we have enough
if (!contains_item(item_id, count)) {
return false;
}
i8 remaining = count;
// Remove from slots
for (i32 i = 0; i < TOTAL_SIZE && remaining > 0; ++i) {
auto* stack = slots_[i].get();
if (stack && !stack->is_empty() && stack->get_item_id() == item_id) {
i8 to_remove = std::min(remaining, stack->get_count());
stack->decrease_count(to_remove);
remaining -= to_remove;
mark_dirty();
}
}
return remaining == 0;
}
bool Inventory::contains_item(i16 item_id, i8 count) const {
i8 total = 0;
for (i32 i = 0; i < TOTAL_SIZE; ++i) {
const auto* stack = slots_[i].get();
if (stack && !stack->is_empty() && stack->get_item_id() == item_id) {
total += stack->get_count();
if (total >= count) {
return true;
}
}
}
return total >= count;
}
void Inventory::update_crafting_result(RecipeManager* recipe_manager) {
if (!recipe_manager) {
clear_slot(CRAFTING_OUTPUT);
return;
}
// Get crafting grid items
std::vector<ItemStack> grid = get_crafting_grid();
// Find matching recipe
const CraftingRecipe* recipe = recipe_manager->find_recipe(grid, 2, 2);
// Update output slot
if (recipe) {
ItemStack result = recipe->get_result();
slots_[CRAFTING_OUTPUT] = std::make_unique<ItemStack>(result);
} else {
clear_slot(CRAFTING_OUTPUT);
}
}
ItemStack* Inventory::get_crafting_result() {
return get_slot(CRAFTING_OUTPUT);
}
const ItemStack* Inventory::get_crafting_result() const {
return get_slot(CRAFTING_OUTPUT);
}
void Inventory::take_crafting_result() {
// Clear the output (player took it)
clear_slot(CRAFTING_OUTPUT);
// Consume one item from each crafting grid slot
for (i32 i = CRAFTING_START; i < CRAFTING_START + CRAFTING_GRID_SIZE; ++i) {
ItemStack* stack = get_slot(i);
if (stack && !stack->is_empty()) {
stack->decrease_count(1);
}
}
mark_dirty();
}
std::vector<ItemStack> Inventory::get_crafting_grid() const {
std::vector<ItemStack> grid;
grid.reserve(CRAFTING_GRID_SIZE);
for (i32 i = CRAFTING_START; i < CRAFTING_START + CRAFTING_GRID_SIZE; ++i) {
const ItemStack* stack = get_slot(i);
if (stack) {
grid.push_back(*stack);
} else {
grid.push_back(ItemStack()); // Empty slot
}
}
return grid;
}
} // namespace mcserver
+99
View File
@@ -0,0 +1,99 @@
#pragma once
#include "entity/inventory/item_stack.hpp"
#include "util/types.hpp"
#include <vector>
#include <memory>
namespace mcserver {
class RecipeManager;
// Inventory slot container
// Beta 1.7.3 player inventory structure:
// - Slots 0-8: Hotbar (quick access bar)
// - Slots 9-35: Main inventory (3 rows of 9)
// - Slots 36-39: Armor (boots, leggings, chestplate, helmet)
// - Slots 40-43: Crafting grid (2x2)
// - Slot 44: Crafting output
class Inventory {
public:
static constexpr i32 HOTBAR_SIZE = 9;
static constexpr i32 MAIN_SIZE = 27; // 3 rows x 9 columns
static constexpr i32 ARMOR_SIZE = 4;
static constexpr i32 CRAFTING_GRID_SIZE = 4; // 2x2
static constexpr i32 TOTAL_SIZE = 45; // 9 + 27 + 4 + 4 + 1
// Slot indices
static constexpr i32 HOTBAR_START = 0;
static constexpr i32 MAIN_START = 9;
static constexpr i32 ARMOR_START = 36;
static constexpr i32 CRAFTING_START = 40;
static constexpr i32 CRAFTING_OUTPUT = 44;
Inventory();
// Get/set item stack in slot
ItemStack* get_slot(i32 slot);
const ItemStack* get_slot(i32 slot) const;
void set_slot(i32 slot, std::unique_ptr<ItemStack> stack);
// Clear slot
void clear_slot(i32 slot);
// Get currently held item (from hotbar)
ItemStack* get_held_item();
const ItemStack* get_held_item() const;
// Get/set current hotbar slot (0-8)
i32 get_current_slot() const { return current_slot_; }
void set_current_slot(i32 slot);
// Find item in inventory
i32 find_item(i16 item_id) const;
// Add item to inventory (returns remaining count that couldn't be added)
i8 add_item(std::unique_ptr<ItemStack> stack);
// Check if item can be added to inventory (has space)
bool can_add_item(const ItemStack& stack) const;
// Remove item from inventory (returns true if successful)
bool remove_item(i16 item_id, i8 count);
// Check if inventory contains item
bool contains_item(i16 item_id, i8 count) const;
// Crafting operations
void update_crafting_result(RecipeManager* recipe_manager);
ItemStack* get_crafting_result();
const ItemStack* get_crafting_result() const;
void take_crafting_result(); // Take the result and consume ingredients
std::vector<ItemStack> get_crafting_grid() const;
// Get total size
i32 size() const { return TOTAL_SIZE; }
// Check if slot is valid
bool is_valid_slot(i32 slot) const {
return slot >= 0 && slot < TOTAL_SIZE;
}
// Mark inventory as changed (for network sync)
void mark_dirty() { dirty_ = true; }
bool is_dirty() const { return dirty_; }
void clear_dirty() { dirty_ = false; }
private:
std::vector<std::unique_ptr<ItemStack>> slots_;
i32 current_slot_; // Currently selected hotbar slot (0-8)
bool dirty_;
// Helper to find first empty slot
i32 find_empty_slot() const;
// Helper to find slot with stackable item
i32 find_stackable_slot(const ItemStack& stack) const;
};
} // namespace mcserver
+99
View File
@@ -0,0 +1,99 @@
#include "entity/inventory/item_stack.hpp"
#include <algorithm>
namespace mcserver {
ItemStack::ItemStack()
: item_id_(-1), count_(0), damage_(0) {
}
ItemStack::ItemStack(i16 item_id, i8 count, i16 damage)
: item_id_(item_id), count_(count), damage_(damage) {
}
bool ItemStack::can_stack_with(const ItemStack& other) const {
if (is_empty() || other.is_empty()) {
return false;
}
return item_id_ == other.item_id_ && damage_ == other.damage_;
}
i8 ItemStack::get_max_stack_size() const {
// Implement proper max stack sizes per item type (Beta 1.7.3)
// Items that stack to 16
if (item_id_ == 332 || // Snowball
item_id_ == 344 || // Egg
item_id_ == 368) { // Ender Pearl
return 16;
}
// Items that stack to 1 (tools, weapons, armor, special items)
// Tools (256-279)
if ((item_id_ >= 256 && item_id_ <= 259) || // Shovels (iron, wood, stone, diamond, gold would be 256-259)
(item_id_ >= 267 && item_id_ <= 279)) { // Swords, pickaxes, axes, hoes
return 1;
}
// Armor (298-317)
if (item_id_ >= 298 && item_id_ <= 317) {
return 1;
}
// Specific items that don't stack
if (item_id_ == 325 || // Bucket (empty)
item_id_ == 326 || // Water bucket
item_id_ == 327 || // Lava bucket
item_id_ == 335 || // Milk bucket
item_id_ == 323 || // Sign
item_id_ == 324 || // Door (wood)
item_id_ == 330 || // Door (iron)
item_id_ == 342 || // Minecart
item_id_ == 343 || // Boat
item_id_ == 345 || // Compass
item_id_ == 346 || // Clock
item_id_ == 347 || // Bed
item_id_ == 354 || // Cake
item_id_ == 355 || // Fishing rod
item_id_ == 259) { // Flint and steel
return 1;
}
// Most items and blocks stack to 64
return 64;
}
void ItemStack::decrease_count(i8 amount) {
count_ -= amount;
if (count_ < 0) {
count_ = 0;
}
if (count_ == 0) {
item_id_ = -1; // Mark as empty
}
}
void ItemStack::increase_count(i8 amount) {
count_ += amount;
i8 max_size = get_max_stack_size();
if (count_ > max_size) {
count_ = max_size;
}
}
std::unique_ptr<ItemStack> ItemStack::split(i8 amount) {
if (amount <= 0 || is_empty()) {
return std::make_unique<ItemStack>();
}
i8 split_amount = std::min(amount, count_);
decrease_count(split_amount);
return std::make_unique<ItemStack>(item_id_, split_amount, damage_);
}
std::unique_ptr<ItemStack> ItemStack::clone() const {
return std::make_unique<ItemStack>(item_id_, count_, damage_);
}
} // namespace mcserver
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include "util/types.hpp"
#include <memory>
namespace mcserver {
// Represents a stack of items in inventory
// Beta 1.7.3 item stack structure
class ItemStack {
public:
ItemStack();
ItemStack(i16 item_id, i8 count, i16 damage = 0);
// Item properties
i16 get_item_id() const { return item_id_; }
i8 get_count() const { return count_; }
i16 get_damage() const { return damage_; }
void set_item_id(i16 id) { item_id_ = id; }
void set_count(i8 count) { count_ = count; }
void set_damage(i16 damage) { damage_ = damage; }
// Stack operations
bool is_empty() const { return item_id_ < 0 || count_ <= 0; }
bool can_stack_with(const ItemStack& other) const;
i8 get_max_stack_size() const;
// Modify stack
void decrease_count(i8 amount);
void increase_count(i8 amount);
// Split stack (returns new stack with split amount)
std::unique_ptr<ItemStack> split(i8 amount);
// Clone
std::unique_ptr<ItemStack> clone() const;
private:
i16 item_id_; // Item/block ID (-1 = empty)
i8 count_; // Number of items (0-64 typically)
i16 damage_; // Durability damage or metadata
};
} // namespace mcserver
+111
View File
@@ -0,0 +1,111 @@
#pragma once
#include "util/types.hpp"
#include "entity/inventory/item_stack.hpp"
#include <memory>
namespace mcserver {
// ItemEntity represents a dropped item in the world
class ItemEntity {
public:
ItemEntity(i32 entity_id, const ItemStack& item, f64 x, f64 y, f64 z)
: entity_id_(entity_id)
, item_(std::make_unique<ItemStack>(item))
, x_(x)
, y_(y)
, z_(z)
, velocity_x_(0.0)
, velocity_y_(0.0)
, velocity_z_(0.0)
, age_(0)
, pickup_delay_(10) {} // 10 ticks = 0.5 seconds delay
// Getters
i32 get_entity_id() const { return entity_id_; }
const ItemStack* get_item() const { return item_.get(); }
f64 get_x() const { return x_; }
f64 get_y() const { return y_; }
f64 get_z() const { return z_; }
f64 get_velocity_x() const { return velocity_x_; }
f64 get_velocity_y() const { return velocity_y_; }
f64 get_velocity_z() const { return velocity_z_; }
i32 get_age() const { return age_; }
i32 get_pickup_delay() const { return pickup_delay_; }
// Setters
void set_position(f64 x, f64 y, f64 z) {
x_ = x;
y_ = y;
z_ = z;
}
void set_velocity(f64 vx, f64 vy, f64 vz) {
velocity_x_ = vx;
velocity_y_ = vy;
velocity_z_ = vz;
}
void set_pickup_delay(i32 delay) {
pickup_delay_ = delay;
}
// Check if item can be picked up
bool can_be_picked_up() const {
return pickup_delay_ <= 0;
}
// Check if item should despawn (5 minutes = 6000 ticks)
bool should_despawn() const {
return age_ >= 6000;
}
// Update item (called every tick)
void tick() {
age_++;
if (pickup_delay_ > 0) {
pickup_delay_--;
}
// Apply gravity
velocity_y_ -= 0.04; // Gravity acceleration
// Apply velocity
x_ += velocity_x_;
y_ += velocity_y_;
z_ += velocity_z_;
// Apply friction/drag
velocity_x_ *= 0.98;
velocity_y_ *= 0.98;
velocity_z_ *= 0.98;
// Ground collision (simple check)
if (y_ < 1.0) {
y_ = 1.0;
velocity_y_ = 0.0;
velocity_x_ *= 0.5; // Friction on ground
velocity_z_ *= 0.5;
}
}
// Check if entity is within pickup range of a position
bool is_in_pickup_range(f64 px, f64 py, f64 pz, f64 range = 1.0) const {
f64 dx = x_ - px;
f64 dy = y_ - py;
f64 dz = z_ - pz;
f64 dist_sq = dx * dx + dy * dy + dz * dz;
return dist_sq <= range * range;
}
private:
i32 entity_id_;
std::unique_ptr<ItemStack> item_;
f64 x_, y_, z_;
f64 velocity_x_, velocity_y_, velocity_z_;
i32 age_; // Ticks since spawn
i32 pickup_delay_; // Ticks until can be picked up
};
} // namespace mcserver
+148
View File
@@ -0,0 +1,148 @@
#include "item_entity_manager.hpp"
#include "entity/player.hpp"
#include "entity/entity_id_manager.hpp"
#include "entity/inventory/inventory.hpp"
#include "util/log/logger.hpp"
namespace mcserver {
ItemEntityManager::ItemEntityManager(EntityIdManager* id_manager)
: id_manager_(id_manager) {
}
ItemEntityManager::~ItemEntityManager() {
// Free all entity IDs on shutdown
for (const auto& [eid, item] : items_) {
if (id_manager_) {
id_manager_->free(eid);
}
}
}
i32 ItemEntityManager::spawn_item(const ItemStack& item, f64 x, f64 y, f64 z,
f64 velocity_x, f64 velocity_y, f64 velocity_z) {
// Allocate entity ID
i32 entity_id = id_manager_->allocate();
// Create item entity
auto item_entity = std::make_unique<ItemEntity>(entity_id, item, x, y, z);
item_entity->set_velocity(velocity_x, velocity_y, velocity_z);
LOG_DEBUG_CAT("Spawned item entity ID " + std::to_string(entity_id) +
" at (" + std::to_string(x) + ", " + std::to_string(y) + ", " + std::to_string(z) + ")",
LogCategory::Entity);
// Add to tracking
ItemEntity* item_ptr = item_entity.get();
items_[entity_id] = std::move(item_entity);
// Notify spawn callback
if (spawn_callback_) {
spawn_callback_(item_ptr);
}
return entity_id;
}
void ItemEntityManager::remove_item(i32 entity_id) {
auto it = items_.find(entity_id);
if (it != items_.end()) {
LOG_DEBUG_CAT("Removed item entity ID " + std::to_string(entity_id),
LogCategory::Entity);
// Notify despawn callback
if (despawn_callback_) {
despawn_callback_(entity_id);
}
// Free entity ID
if (id_manager_) {
id_manager_->free(entity_id);
}
items_.erase(it);
}
}
ItemEntity* ItemEntityManager::get_item(i32 entity_id) {
auto it = items_.find(entity_id);
if (it != items_.end()) {
return it->second.get();
}
return nullptr;
}
std::vector<ItemEntity*> ItemEntityManager::get_all_items() {
std::vector<ItemEntity*> result;
result.reserve(items_.size());
for (auto& [eid, item] : items_) {
result.push_back(item.get());
}
return result;
}
void ItemEntityManager::tick() {
// Update all items and collect despawn list
std::vector<i32> to_despawn;
for (auto& [entity_id, item] : items_) {
item->tick();
// Check if item should despawn
if (item->should_despawn()) {
to_despawn.push_back(entity_id);
}
}
// Remove despawned items
for (i32 entity_id : to_despawn) {
LOG_DEBUG_CAT("Item entity " + std::to_string(entity_id) + " despawned (age limit)",
LogCategory::Entity);
remove_item(entity_id);
}
}
void ItemEntityManager::check_pickups(const std::vector<Player*>& players) {
std::vector<std::pair<i32, i32>> pickups; // (item_eid, player_eid)
// Check each item against each player
for (auto& [entity_id, item] : items_) {
if (!item->can_be_picked_up()) {
continue; // Item has pickup delay
}
for (Player* player : players) {
if (!player || player->is_dead()) {
continue;
}
// Check if player is within pickup range
if (item->is_in_pickup_range(player->get_x(), player->get_y(), player->get_z(), 1.5)) {
// Try to add item to player's inventory
if (player->get_inventory()->can_add_item(*item->get_item())) {
// Create a copy of the item to add to inventory
auto item_copy = std::make_unique<ItemStack>(*item->get_item());
player->get_inventory()->add_item(std::move(item_copy));
pickups.push_back({entity_id, player->get_entity_id()});
LOG_DEBUG_CAT("Player " + player->get_username() + " picked up item entity " +
std::to_string(entity_id), LogCategory::Entity);
break; // Item picked up, stop checking other players
}
}
}
}
// Process pickups (collect callback + remove items)
for (const auto& [item_eid, player_eid] : pickups) {
// Notify collect callback
if (collect_callback_) {
collect_callback_(item_eid, player_eid);
}
// Remove the item entity
remove_item(item_eid);
}
}
} // namespace mcserver
+72
View File
@@ -0,0 +1,72 @@
#pragma once
#include "util/types.hpp"
#include "entity/item/item_entity.hpp"
#include "entity/inventory/item_stack.hpp"
#include <unordered_map>
#include <vector>
#include <functional>
#include <memory>
namespace mcserver {
class ClientSession;
class Player;
class EntityIdManager;
// Callbacks for item entity events
using ItemSpawnCallback = std::function<void(const ItemEntity* item)>;
using ItemDespawnCallback = std::function<void(i32 entity_id)>;
using ItemCollectCallback = std::function<void(i32 item_entity_id, i32 collector_entity_id)>;
// Manages all item entities in the world
class ItemEntityManager {
public:
explicit ItemEntityManager(EntityIdManager* id_manager);
~ItemEntityManager();
// Set callbacks
void set_spawn_callback(ItemSpawnCallback callback) {
spawn_callback_ = std::move(callback);
}
void set_despawn_callback(ItemDespawnCallback callback) {
despawn_callback_ = std::move(callback);
}
void set_collect_callback(ItemCollectCallback callback) {
collect_callback_ = std::move(callback);
}
// Spawn an item entity in the world
i32 spawn_item(const ItemStack& item, f64 x, f64 y, f64 z,
f64 velocity_x = 0.0, f64 velocity_y = 0.0, f64 velocity_z = 0.0);
// Remove an item entity
void remove_item(i32 entity_id);
// Get an item entity by ID
ItemEntity* get_item(i32 entity_id);
// Get all item entities
std::vector<ItemEntity*> get_all_items();
// Update all items (called every tick)
void tick();
// Check for item pickups by players
void check_pickups(const std::vector<Player*>& players);
// Get count of active items
usize get_item_count() const { return items_.size(); }
private:
EntityIdManager* id_manager_;
std::unordered_map<i32, std::unique_ptr<ItemEntity>> items_;
ItemSpawnCallback spawn_callback_;
ItemDespawnCallback despawn_callback_;
ItemCollectCallback collect_callback_;
};
} // namespace mcserver
+289
View File
@@ -0,0 +1,289 @@
#include "entity/mob/hostile_mob.hpp"
#include "entity/player.hpp"
#include "world/chunk/chunk_manager.hpp"
#include "util/log/logger.hpp"
#include <cmath>
#include <limits>
namespace mcserver {
HostileMob::HostileMob(i32 entity_id, MobType type)
: Mob(entity_id, type) {
}
void HostileMob::update_ai() {
// Reduce cooldowns
if (attack_cooldown_ > 0) {
attack_cooldown_--;
}
if (pathfind_cooldown_ > 0) {
pathfind_cooldown_--;
}
// Try to find a target player
Player* nearest = find_nearest_player(16.0); // 16 block detection range
if (nearest) {
target_player_ = nearest;
ai_state_ = MobAIState::Chasing;
chase_target(nearest);
// Check if close enough to attack (< 2 blocks)
f64 dx = nearest->get_x() - x_;
f64 dy = nearest->get_y() - y_;
f64 dz = nearest->get_z() - z_;
f64 dist_sq = dx * dx + dy * dy + dz * dz;
if (dist_sq < 4.0 && attack_cooldown_ <= 0) { // Within 2 blocks
ai_state_ = MobAIState::Attacking;
attack_target(nearest);
attack_cooldown_ = 20; // 1 second cooldown (20 ticks)
}
} else {
// No target, wander randomly
target_player_ = nullptr;
path_follower_.clear_path();
if (ai_state_ != MobAIState::Idle && ai_state_ != MobAIState::Wandering) {
ai_state_ = MobAIState::Idle;
}
wander_randomly();
}
}
Player* HostileMob::find_nearest_player(f64 max_range) {
if (!players_) {
return nullptr;
}
Player* nearest = nullptr;
f64 nearest_dist_sq = max_range * max_range;
for (Player* player : *players_) {
if (!player) continue;
f64 dx = player->get_x() - x_;
f64 dy = player->get_y() - y_;
f64 dz = player->get_z() - z_;
f64 dist_sq = dx * dx + dy * dy + dz * dz;
if (dist_sq < nearest_dist_sq) {
nearest_dist_sq = dist_sq;
nearest = player;
}
}
return nearest;
}
void HostileMob::chase_target(Player* target) {
if (!target) {
return;
}
f64 target_x, target_y, target_z;
// Try to use pathfinding if available and we don't have a path
if (chunk_manager_ && !path_follower_.has_path() && pathfind_cooldown_ <= 0) {
// Attempt pathfinding every 40 ticks (2 seconds) to avoid overhead
pathfind_cooldown_ = 40;
Pathfinder pathfinder(chunk_manager_);
PathNode start{static_cast<i32>(std::floor(x_)),
static_cast<i32>(std::floor(y_)),
static_cast<i32>(std::floor(z_))};
PathNode goal{static_cast<i32>(std::floor(target->get_x())),
static_cast<i32>(std::floor(target->get_y())),
static_cast<i32>(std::floor(target->get_z()))};
auto result = pathfinder.find_path(start, goal, 32.0, true, false);
if (result.success && !result.path.empty()) {
path_follower_.set_path(result.path);
}
}
// Follow path if we have one
if (path_follower_.get_next_waypoint(x_, y_, z_, target_x, target_y, target_z)) {
// Move towards waypoint
f64 dx = target_x - x_;
f64 dz = target_z - z_;
f64 angle = std::atan2(-dx, dz);
yaw_ = static_cast<f32>(angle * 180.0 / 3.14159265359);
// Normalize yaw
while (yaw_ < 0.0f) yaw_ += 360.0f;
while (yaw_ >= 360.0f) yaw_ -= 360.0f;
movement_.is_moving = true;
movement_.target_yaw = yaw_;
} else {
// No path, move directly towards target (simple chase)
f64 dx = target->get_x() - x_;
f64 dz = target->get_z() - z_;
f64 angle = std::atan2(-dx, dz);
yaw_ = static_cast<f32>(angle * 180.0 / 3.14159265359);
// Normalize yaw
while (yaw_ < 0.0f) yaw_ += 360.0f;
while (yaw_ >= 360.0f) yaw_ -= 360.0f;
movement_.is_moving = true;
movement_.target_yaw = yaw_;
}
}
void HostileMob::attack_target(Player* target) {
// Base implementation - will be overridden by specific mobs
if (target) {
LOG_DEBUG_CAT(get_name() + " attacks " + target->get_username(), LogCategory::Entity);
}
}
// Zombie implementation
MobZombie::MobZombie(i32 entity_id)
: HostileMob(entity_id, MobType::Zombie) {
health_ = 20;
max_health_ = 20;
}
void MobZombie::attack_target(Player* target) {
if (!target || target->is_dead()) return;
// Zombies deal 2-3 hearts of damage (4-6 HP) on normal difficulty
// In Beta 1.7.3, zombies deal damage when very close
i16 damage = 5; // 2.5 hearts
target->take_damage(damage);
LOG_DEBUG_CAT("Zombie attacks " + target->get_username() + " for " +
std::to_string(damage) + " damage (HP: " + std::to_string(target->get_health()) + "/20)",
LogCategory::Entity);
}
// Skeleton implementation
MobSkeleton::MobSkeleton(i32 entity_id)
: HostileMob(entity_id, MobType::Skeleton) {
health_ = 20;
max_health_ = 20;
}
void MobSkeleton::attack_target(Player* target) {
if (!target || target->is_dead()) return;
// Skeletons shoot arrows from a distance
// In Beta 1.7.3, skeletons have a range of ~15 blocks
f64 dx = target->get_x() - x_;
f64 dz = target->get_z() - z_;
f64 dist = std::sqrt(dx * dx + dz * dz);
if (dist > 4.0 && dist < 15.0) { // Shoot from 4-15 blocks away
// For now, apply instant damage (arrow damage)
// TODO: Spawn arrow entity when projectile system is implemented
i16 damage = 4; // 2 hearts
target->take_damage(damage);
LOG_DEBUG_CAT("Skeleton shoots arrow at " + target->get_username() + " for " +
std::to_string(damage) + " damage (HP: " + std::to_string(target->get_health()) + "/20)",
LogCategory::Entity);
}
}
// Creeper implementation
MobCreeper::MobCreeper(i32 entity_id)
: HostileMob(entity_id, MobType::Creeper) {
health_ = 20;
max_health_ = 20;
}
void MobCreeper::update_ai() {
// Creepers have special behavior - they explode when close to players
Player* nearest = find_nearest_player(16.0);
if (nearest) {
target_player_ = nearest;
f64 dx = nearest->get_x() - x_;
f64 dy = nearest->get_y() - y_;
f64 dz = nearest->get_z() - z_;
f64 dist_sq = dx * dx + dy * dy + dz * dz;
if (dist_sq < 9.0) { // Within 3 blocks - start fusing
if (!is_ignited_) {
is_ignited_ = true;
fuse_time_ = 30; // 1.5 seconds fuse (30 ticks)
LOG_DEBUG_CAT("Creeper ignited near " + nearest->get_username(), LogCategory::Entity);
}
// Stand still while fusing
movement_.is_moving = false;
ai_state_ = MobAIState::Attacking;
fuse_time_--;
if (fuse_time_ <= 0) {
// Explode! Deal damage to nearby player
if (nearest && !nearest->is_dead()) {
i16 damage = 17; // 8.5 hearts - very high damage
nearest->take_damage(damage);
LOG_INFO_CAT("Creeper exploded at (" + std::to_string(x_) + ", " +
std::to_string(y_) + ", " + std::to_string(z_) + ") dealing " +
std::to_string(damage) + " damage to " + nearest->get_username(),
LogCategory::Entity);
} else {
LOG_INFO_CAT("Creeper exploded at (" + std::to_string(x_) + ", " +
std::to_string(y_) + ", " + std::to_string(z_) + ")",
LogCategory::Entity);
}
// TODO: Create explosion block destruction
health_ = 0; // Creeper dies after exploding
}
} else if (dist_sq < 256.0) { // Within 16 blocks - chase
if (is_ignited_ && dist_sq > 16.0) {
// Player moved away, defuse
is_ignited_ = false;
fuse_time_ = 0;
}
ai_state_ = MobAIState::Chasing;
chase_target(nearest);
} else {
// Too far, wander
is_ignited_ = false;
fuse_time_ = 0;
wander_randomly();
}
} else {
// No target
is_ignited_ = false;
fuse_time_ = 0;
target_player_ = nullptr;
wander_randomly();
}
}
void MobCreeper::attack_target(Player* target) {
(void)target; // Unused - creepers explode instead of traditional attack
// Creepers don't have a traditional attack - they explode
// The explosion logic is handled in update_ai()
}
// Spider implementation
MobSpider::MobSpider(i32 entity_id)
: HostileMob(entity_id, MobType::Spider) {
health_ = 16;
max_health_ = 16;
}
void MobSpider::attack_target(Player* target) {
if (!target || target->is_dead()) return;
// Spiders are fast and deal poison damage in later versions
// In Beta 1.7.3, they just deal regular melee damage
i16 damage = 3; // 1.5 hearts
target->take_damage(damage);
LOG_DEBUG_CAT("Spider attacks " + target->get_username() + " for " +
std::to_string(damage) + " damage (HP: " + std::to_string(target->get_health()) + "/20)",
LogCategory::Entity);
}
} // namespace mcserver
+82
View File
@@ -0,0 +1,82 @@
#pragma once
#include "entity/mob/mob.hpp"
#include "entity/mob/pathfinding.hpp"
namespace mcserver {
class Player;
class ChunkManager;
// Base class for hostile mobs
class HostileMob : public Mob {
public:
HostileMob(i32 entity_id, MobType type);
bool is_hostile() const override { return true; }
f32 get_movement_speed() const override { return 0.25f; } // Slightly faster than passive
void update_ai() override;
// Set the player list for targeting
void set_player_list(const std::vector<Player*>* players) { players_ = players; }
// Set chunk manager for pathfinding
void set_chunk_manager(ChunkManager* chunk_manager) { chunk_manager_ = chunk_manager; }
protected:
// Find nearest player within range
Player* find_nearest_player(f64 max_range);
// Chase target player
void chase_target(Player* target);
// Attack target (to be implemented)
virtual void attack_target(Player* target);
const std::vector<Player*>* players_ = nullptr;
Player* target_player_ = nullptr;
i32 attack_cooldown_ = 0;
// Pathfinding
ChunkManager* chunk_manager_ = nullptr;
PathFollower path_follower_;
i32 pathfind_cooldown_ = 0; // Ticks until next pathfinding attempt
};
// Zombie mob (ID 54)
class MobZombie : public HostileMob {
public:
explicit MobZombie(i32 entity_id);
void attack_target(Player* target) override;
};
// Skeleton mob (ID 51)
class MobSkeleton : public HostileMob {
public:
explicit MobSkeleton(i32 entity_id);
f32 get_movement_speed() const override { return 0.23f; } // Slightly slower
void attack_target(Player* target) override;
};
// Creeper mob (ID 50)
class MobCreeper : public HostileMob {
public:
explicit MobCreeper(i32 entity_id);
void update_ai() override;
void attack_target(Player* target) override;
private:
i32 fuse_time_ = 0;
bool is_ignited_ = false;
};
// Spider mob (ID 52)
class MobSpider : public HostileMob {
public:
explicit MobSpider(i32 entity_id);
f32 get_movement_speed() const override { return 0.3f; } // Fastest hostile mob
void attack_target(Player* target) override;
};
} // namespace mcserver
+302
View File
@@ -0,0 +1,302 @@
#include "entity/mob/mob.hpp"
#include <algorithm>
#include <cmath>
#include <random>
namespace mcserver {
Mob::Mob(i32 entity_id, MobType type)
: entity_id_(entity_id), mob_type_(type) {
movement_.move_speed = get_movement_speed();
// Initialize required metadata for Beta 1.7.3
// Index 0: Entity flags (fire, crouched, riding, sprinting, eating)
metadata_.set_byte(0, 0x00); // No flags set by default
}
void Mob::set_position(f64 x, f64 y, f64 z) {
prev_x_ = x_;
prev_y_ = y_;
prev_z_ = z_;
x_ = x;
y_ = y;
z_ = z;
}
void Mob::set_rotation(f32 yaw, f32 pitch) {
yaw_ = yaw;
pitch_ = pitch;
}
void Mob::set_health(i16 health) {
health_ = std::clamp(health, static_cast<i16>(0), max_health_);
}
void Mob::update() {
age_++;
// Handle death timer
if (is_dead()) {
death_timer_--;
return; // Dead mobs don't move or update AI
}
// Decrement panic timer
if (panic_timer_ > 0) {
panic_timer_--;
if (panic_timer_ == 0) {
ai_state_ = MobAIState::Idle;
}
}
// Store previous position for movement detection
prev_x_ = x_;
prev_y_ = y_;
prev_z_ = z_;
// Update AI behavior
update_ai();
// Apply movement and physics
apply_movement();
// Check if mob moved significantly (> 0.01 blocks)
f64 dx = x_ - prev_x_;
f64 dy = y_ - prev_y_;
f64 dz = z_ - prev_z_;
f64 dist_sq = dx * dx + dy * dy + dz * dz;
// Broadcast movement if the mob moved or rotated
if (dist_sq > 0.0001 && move_callback_) {
move_callback_(entity_id_, prev_x_, prev_y_, prev_z_, x_, y_, z_, yaw_, pitch_);
}
}
std::string Mob::get_name() const {
switch (mob_type_) {
case MobType::Creeper: return "Creeper";
case MobType::Skeleton: return "Skeleton";
case MobType::Spider: return "Spider";
case MobType::Giant: return "Giant";
case MobType::Zombie: return "Zombie";
case MobType::Slime: return "Slime";
case MobType::Ghast: return "Ghast";
case MobType::PigZombie: return "PigZombie";
case MobType::Pig: return "Pig";
case MobType::Sheep: return "Sheep";
case MobType::Cow: return "Cow";
case MobType::Chicken: return "Chicken";
case MobType::Squid: return "Squid";
case MobType::Wolf: return "Wolf";
default: return "Unknown";
}
}
void Mob::update_ai() {
// Priority: Fleeing if in panic mode
if (panic_timer_ > 0 && !is_hostile()) {
ai_state_ = MobAIState::Fleeing;
movement_.is_moving = true;
// Calculate direction away from attacker
f64 dx = x_ - flee_from_x_;
f64 dz = z_ - flee_from_z_;
f64 dist = std::sqrt(dx * dx + dz * dz);
if (dist > 0.01) {
// Face away from attacker
f32 flee_yaw = static_cast<f32>(std::atan2(-dx, dz) * 180.0 / 3.14159265359);
yaw_ = flee_yaw;
movement_.target_yaw = flee_yaw;
}
// Run faster when panicking
movement_.move_speed = get_movement_speed() * 1.5f;
return;
}
// Reset movement speed to normal
movement_.move_speed = get_movement_speed();
// Base AI: Simple wandering for passive mobs
if (!is_hostile()) {
wander_randomly();
}
}
void Mob::wander_randomly() {
static std::random_device rd;
static std::mt19937 gen(rd());
static std::uniform_real_distribution<> change_dir_dist(0.0, 1.0);
static std::uniform_real_distribution<> angle_dist(0.0, 6.28318530718); // 0 to 2*PI
// Random chance to change state
if (change_dir_dist(gen) < 0.01) { // 1% chance per tick to change state
if (ai_state_ == MobAIState::Idle) {
ai_state_ = MobAIState::Wandering;
movement_.wander_ticks = 20 + static_cast<i32>(change_dir_dist(gen) * 60); // 1-4 seconds
movement_.target_yaw = static_cast<f32>(angle_dist(gen) * 180.0 / 3.14159265359); // Random direction
} else {
ai_state_ = MobAIState::Idle;
movement_.idle_ticks = 20 + static_cast<i32>(change_dir_dist(gen) * 80); // 1-5 seconds
movement_.is_moving = false;
}
}
// Update state-specific behavior
if (ai_state_ == MobAIState::Wandering) {
movement_.is_moving = true;
yaw_ = movement_.target_yaw;
movement_.wander_ticks--;
if (movement_.wander_ticks <= 0) {
ai_state_ = MobAIState::Idle;
movement_.is_moving = false;
}
} else if (ai_state_ == MobAIState::Idle) {
movement_.is_moving = false;
movement_.idle_ticks--;
}
}
void Mob::apply_movement() {
// Apply knockback/external velocity first
x_ += movement_.velocity_x;
y_ += movement_.velocity_y;
z_ += movement_.velocity_z;
// Apply friction to knockback velocity
movement_.velocity_x *= 0.6;
movement_.velocity_y *= 0.98;
movement_.velocity_z *= 0.6;
// Stop very small velocities
if (std::abs(movement_.velocity_x) < 0.001) movement_.velocity_x = 0.0;
if (std::abs(movement_.velocity_y) < 0.001) movement_.velocity_y = 0.0;
if (std::abs(movement_.velocity_z) < 0.001) movement_.velocity_z = 0.0;
// Apply movement velocity if moving
if (movement_.is_moving) {
// Convert yaw to radians
f64 yaw_rad = yaw_ * 3.14159265359 / 180.0;
// Calculate velocity based on yaw and speed
// Minecraft uses Z-forward coordinate system
f64 speed = movement_.move_speed / 20.0; // Convert blocks/second to blocks/tick
f64 move_x = -std::sin(yaw_rad) * speed;
f64 move_z = std::cos(yaw_rad) * speed;
// Add to position
x_ += move_x;
z_ += move_z;
}
// Simple bounds check - keep mobs within reasonable range of spawn
constexpr f64 max_distance = 100.0;
if (std::abs(x_) > max_distance) x_ = std::copysign(max_distance, x_);
if (std::abs(z_) > max_distance) z_ = std::copysign(max_distance, z_);
}
void Mob::apply_knockback(f64 source_x, f64 source_z, f32 strength) {
// Implement original Minecraft knockback algorithm
// Calculate direction away from source
f64 dx = x_ - source_x;
f64 dz = z_ - source_z;
f64 dist = std::sqrt(dx * dx + dz * dz);
if (dist < 0.01) {
// Too close, use small value to avoid division by zero
dist = 0.01;
}
// Step 1: Halve all existing motion
movement_.velocity_x /= 2.0;
movement_.velocity_y /= 2.0;
movement_.velocity_z /= 2.0;
// Step 2: Apply knockback in horizontal direction (subtract because we want to push away)
// Original uses strength = 0.4F
movement_.velocity_x -= (dx / dist) * static_cast<f64>(strength);
movement_.velocity_z -= (dz / dist) * static_cast<f64>(strength);
// Step 3: Add upward boost
movement_.velocity_y += 0.4;
// Step 4: Cap upward velocity
if (movement_.velocity_y > 0.4) {
movement_.velocity_y = 0.4;
}
}
void Mob::on_attacked_by(f64 attacker_x, f64 attacker_z) {
// Set panic mode for passive mobs
if (!is_hostile()) {
panic_timer_ = 120; // Panic for 6 seconds (120 ticks)
flee_from_x_ = attacker_x;
flee_from_z_ = attacker_z;
}
}
std::vector<std::pair<i16, i8>> Mob::get_death_drops() const {
std::vector<std::pair<i16, i8>> drops;
static std::random_device rd;
static std::mt19937 gen(rd());
static std::uniform_int_distribution<> drop_count(0, 2);
switch (mob_type_) {
case MobType::Pig:
// Pigs drop 0-2 raw porkchops (item ID 319)
drops.push_back({static_cast<i16>(319), static_cast<i8>(drop_count(gen))});
break;
case MobType::Cow:
// Cows drop 0-2 leather (item ID 334) and 0-2 raw beef (item ID 363)
drops.push_back({static_cast<i16>(334), static_cast<i8>(drop_count(gen))});
drops.push_back({static_cast<i16>(363), static_cast<i8>(drop_count(gen))});
break;
case MobType::Chicken:
// Chickens drop 0-2 feathers (item ID 288) and 0-1 raw chicken (item ID 365)
drops.push_back({static_cast<i16>(288), static_cast<i8>(drop_count(gen))});
{
std::uniform_int_distribution<> chicken_drop(0, 1);
drops.push_back({static_cast<i16>(365), static_cast<i8>(chicken_drop(gen))});
}
break;
case MobType::Sheep:
// Sheep drop 1 wool (item ID 35) - color depends on metadata
// Default to white wool
drops.push_back({static_cast<i16>(35), static_cast<i8>(1)});
break;
case MobType::Zombie:
// Zombies drop 0-2 feathers (should be rotten flesh but using feathers for Beta 1.7.3)
drops.push_back({static_cast<i16>(288), static_cast<i8>(drop_count(gen))});
break;
case MobType::Skeleton:
// Skeletons drop 0-2 arrows (item ID 262) and 0-2 bones (item ID 352)
drops.push_back({static_cast<i16>(262), static_cast<i8>(drop_count(gen))});
drops.push_back({static_cast<i16>(352), static_cast<i8>(drop_count(gen))});
break;
case MobType::Spider:
// Spiders drop 0-2 string (item ID 287)
drops.push_back({static_cast<i16>(287), static_cast<i8>(drop_count(gen))});
break;
case MobType::Creeper:
// Creepers drop 0-2 gunpowder (item ID 289)
drops.push_back({static_cast<i16>(289), static_cast<i8>(drop_count(gen))});
break;
default:
break;
}
return drops;
}
} // namespace mcserver
+115
View File
@@ -0,0 +1,115 @@
#pragma once
#include "entity/mob/mob_type.hpp"
#include "entity/mob/mob_ai.hpp"
#include "entity/mob/mob_metadata.hpp"
#include "util/types.hpp"
#include <string>
#include <functional>
namespace mcserver {
// Callback for mob movement broadcasting
using MobMoveCallback = std::function<void(i32 entity_id, f64 old_x, f64 old_y, f64 old_z,
f64 new_x, f64 new_y, f64 new_z,
f32 yaw, f32 pitch)>;
// Base mob entity class for all NPCs/mobs
class Mob {
public:
Mob(i32 entity_id, MobType type);
virtual ~Mob() = default;
// Getters
i32 get_entity_id() const { return entity_id_; }
MobType get_mob_type() const { return mob_type_; }
f64 get_x() const { return x_; }
f64 get_y() const { return y_; }
f64 get_z() const { return z_; }
f32 get_yaw() const { return yaw_; }
f32 get_pitch() const { return pitch_; }
i16 get_health() const { return health_; }
i16 get_max_health() const { return max_health_; }
bool is_dead() const { return health_ <= 0; }
MobAIState get_ai_state() const { return ai_state_; }
i32 get_death_timer() const { return death_timer_; }
// Setters
void set_position(f64 x, f64 y, f64 z);
void set_rotation(f32 yaw, f32 pitch);
void set_health(i16 health);
void set_move_callback(MobMoveCallback callback) { move_callback_ = callback; }
// Combat methods
void apply_knockback(f64 source_x, f64 source_z, f32 strength = 0.4f);
void on_attacked_by(f64 attacker_x, f64 attacker_z);
// Death handling
bool should_despawn() const { return is_dead() && death_timer_ <= 0; }
// Get items this mob drops on death
virtual std::vector<std::pair<i16, i8>> get_death_drops() const;
// Update tick (called every server tick)
virtual void update();
// Get the name of this mob type (for logging/debugging)
virtual std::string get_name() const;
// Metadata access
MobMetadata* get_metadata() { return &metadata_; }
const MobMetadata* get_metadata() const { return &metadata_; }
// AI behavior methods (can be overridden by subclasses)
virtual void update_ai();
virtual bool is_hostile() const { return false; }
virtual f32 get_movement_speed() const { return 0.2f; } // Blocks per second
protected:
// Apply movement and velocity
void apply_movement();
// Random wandering behavior
void wander_randomly();
i32 entity_id_;
MobType mob_type_;
// Position
f64 x_ = 0.0;
f64 y_ = 64.0;
f64 z_ = 0.0;
f64 prev_x_ = 0.0;
f64 prev_y_ = 64.0;
f64 prev_z_ = 0.0;
f32 yaw_ = 0.0f;
f32 pitch_ = 0.0f;
// Stats
i16 health_ = 20;
i16 max_health_ = 20;
// Timers
i32 age_ = 0; // Age in ticks
i32 panic_timer_ = 0; // Ticks remaining in panic mode
i32 death_timer_ = 40; // Ticks before despawn after death (2 seconds)
// Panic mode (fleeing when attacked)
f64 flee_from_x_ = 0.0;
f64 flee_from_z_ = 0.0;
// AI state
MobAIState ai_state_ = MobAIState::Idle;
MobMovement movement_;
// Metadata
MobMetadata metadata_;
// Callback for movement
MobMoveCallback move_callback_;
};
} // namespace mcserver
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include "util/types.hpp"
namespace mcserver {
// AI states for mob behavior
enum class MobAIState {
Idle, // Standing still
Wandering, // Random movement
Chasing, // Following a target
Attacking, // In combat
Fleeing // Running away
};
// AI goals and behaviors
struct MobAIGoal {
virtual ~MobAIGoal() = default;
virtual bool should_execute() = 0;
virtual void start() = 0;
virtual void update() = 0;
virtual void stop() = 0;
};
// Movement parameters for mobs
struct MobMovement {
f64 velocity_x = 0.0;
f64 velocity_y = 0.0;
f64 velocity_z = 0.0;
f32 move_speed = 0.0f; // Blocks per second
f32 target_yaw = 0.0f; // Desired yaw
bool is_moving = false;
bool on_ground = true;
i32 idle_ticks = 0; // Ticks spent idle
i32 wander_ticks = 0; // Ticks spent wandering
void reset() {
velocity_x = velocity_y = velocity_z = 0.0;
is_moving = false;
idle_ticks = wander_ticks = 0;
}
};
} // namespace mcserver
+250
View File
@@ -0,0 +1,250 @@
#include "entity/mob/mob_manager.hpp"
#include "entity/mob/passive_mob.hpp"
#include "entity/mob/hostile_mob.hpp"
#include "entity/mob/mob_spawner.hpp"
#include "net/session/client_session.hpp"
#include "net/protocol/packets/mob_spawn.hpp"
#include "world/chunk/chunk.hpp"
#include "world/chunk/chunk_manager.hpp"
#include "util/log/logger.hpp"
#include <random>
#include <cmath>
namespace mcserver {
MobManager::MobManager(ChunkManager* chunk_manager)
: chunk_manager_(chunk_manager)
, spawner_(std::make_unique<MobSpawner>(this, chunk_manager)) {
}
Mob* MobManager::spawn_mob(MobType type, f64 x, f64 y, f64 z) {
i32 entity_id = get_next_entity_id();
// Create the mob based on type
std::unique_ptr<Mob> mob;
switch (type) {
// Passive mobs
case MobType::Pig:
mob = std::make_unique<MobPig>(entity_id);
break;
case MobType::Sheep:
mob = std::make_unique<MobSheep>(entity_id);
break;
case MobType::Cow:
mob = std::make_unique<MobCow>(entity_id);
break;
case MobType::Chicken:
mob = std::make_unique<MobChicken>(entity_id);
break;
// Hostile mobs
case MobType::Zombie:
mob = std::make_unique<MobZombie>(entity_id);
break;
case MobType::Skeleton:
mob = std::make_unique<MobSkeleton>(entity_id);
break;
case MobType::Creeper:
mob = std::make_unique<MobCreeper>(entity_id);
break;
case MobType::Spider:
mob = std::make_unique<MobSpider>(entity_id);
break;
default:
LOG_WARNING_CAT("Unsupported mob type: " + std::to_string(static_cast<i8>(type)),
LogCategory::Entity);
return nullptr;
}
mob->set_position(x, y, z);
// Set up movement callback for this mob
mob->set_move_callback([this](i32 entity_id, f64 old_x, f64 old_y, f64 old_z,
f64 new_x, f64 new_y, f64 new_z,
f32 yaw, f32 pitch) {
this->broadcast_mob_movement(entity_id, old_x, old_y, old_z, new_x, new_y, new_z, yaw, pitch);
});
// If this is a hostile mob, give it access to the player list and chunk manager
if (mob->is_hostile()) {
auto* hostile = dynamic_cast<HostileMob*>(mob.get());
if (hostile) {
if (players_) {
hostile->set_player_list(players_);
}
if (chunk_manager_) {
hostile->set_chunk_manager(chunk_manager_);
}
}
}
// Broadcast spawn to all clients
if (spawn_callback_) {
spawn_callback_(mob.get());
}
LOG_INFO_CAT("Spawned " + mob->get_name() + " at (" +
std::to_string(x) + ", " + std::to_string(y) + ", " + std::to_string(z) + ")",
LogCategory::Entity);
Mob* mob_ptr = mob.get();
mobs_[entity_id] = std::move(mob);
return mob_ptr;
}
void MobManager::remove_mob(i32 entity_id) {
auto it = mobs_.find(entity_id);
if (it != mobs_.end()) {
LOG_INFO_CAT("Removed mob " + it->second->get_name() + " (ID: " +
std::to_string(entity_id) + ")", LogCategory::Entity);
mobs_.erase(it);
}
}
void MobManager::update_all() {
// Collect mobs ready to despawn (can't modify map while iterating)
std::vector<i32> mobs_to_remove;
for (auto& [id, mob] : mobs_) {
// Always update mobs (even dead ones, for death timer)
mob->update();
// Check if dead mob should despawn after animation
if (mob->should_despawn()) {
mobs_to_remove.push_back(id);
}
}
// Remove mobs that finished death animation
for (i32 entity_id : mobs_to_remove) {
auto it = mobs_.find(entity_id);
if (it != mobs_.end()) {
LOG_INFO_CAT("Removed mob: " + it->second->get_name() + " (ID: " +
std::to_string(entity_id) + ")", LogCategory::Entity);
// Broadcast despawn to all clients
if (despawn_callback_) {
despawn_callback_(entity_id);
}
// Remove from manager
mobs_.erase(it);
}
}
}
Mob* MobManager::get_mob(i32 entity_id) {
auto it = mobs_.find(entity_id);
return (it != mobs_.end()) ? it->second.get() : nullptr;
}
const Mob* MobManager::get_mob(i32 entity_id) const {
auto it = mobs_.find(entity_id);
return (it != mobs_.end()) ? it->second.get() : nullptr;
}
void MobManager::spawn_existing_mobs_for(ClientSession* session) {
if (!session) {
return;
}
for (const auto& [id, mob] : mobs_) {
PacketMobSpawn spawn_packet(mob.get());
session->send_packet(spawn_packet);
}
LOG_DEBUG_CAT("Sent " + std::to_string(mobs_.size()) + " existing mobs to " +
session->get_username(), LogCategory::Entity);
}
void MobManager::spawn_test_mobs(f64 spawn_x, f64 spawn_z) {
// Spawn a few test mobs in a circle around the spawn point
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<> angle_dist(0.0, 6.28318530718); // 0 to 2*PI
std::uniform_real_distribution<> radius_dist(10.0, 30.0);
// Spawn 2 of each passive mob type
MobType types[] = {MobType::Pig, MobType::Sheep, MobType::Cow, MobType::Chicken};
for (MobType type : types) {
for (int i = 0; i < 2; ++i) {
f64 angle = angle_dist(gen);
f64 radius = radius_dist(gen);
f64 x = spawn_x + radius * std::cos(angle);
f64 z = spawn_z + radius * std::sin(angle);
f64 y = get_ground_level(x, z); // Get actual ground level from terrain
spawn_mob(type, x, y, z);
}
}
LOG_INFO_CAT("Spawned " + std::to_string(mobs_.size()) + " passive test mobs", LogCategory::Entity);
}
void MobManager::spawn_test_hostile_mobs(f64 spawn_x, f64 spawn_z) {
// Spawn hostile mobs in a circle around spawn
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<> angle_dist(0.0, 6.28318530718); // 0 to 2*PI
std::uniform_real_distribution<> radius_dist(15.0, 40.0); // Further away than passive mobs
// Spawn 2 of each hostile mob type
MobType types[] = {MobType::Zombie, MobType::Skeleton, MobType::Creeper, MobType::Spider};
for (MobType type : types) {
for (int i = 0; i < 2; ++i) {
f64 angle = angle_dist(gen);
f64 radius = radius_dist(gen);
f64 x = spawn_x + radius * std::cos(angle);
f64 z = spawn_z + radius * std::sin(angle);
f64 y = get_ground_level(x, z); // Get actual ground level from terrain
spawn_mob(type, x, y, z);
}
}
LOG_INFO_CAT("Spawned " + std::to_string(8) + " hostile test mobs", LogCategory::Entity);
}
void MobManager::broadcast_mob_movement(i32 entity_id, f64 old_x, f64 old_y, f64 old_z,
f64 new_x, f64 new_y, f64 new_z, f32 yaw, f32 pitch) {
if (movement_callback_) {
movement_callback_(entity_id, old_x, old_y, old_z, new_x, new_y, new_z, yaw, pitch);
}
}
f64 MobManager::get_ground_level(f64 x, f64 z) {
if (!chunk_manager_) {
return 64.0; // Fallback if no chunk manager
}
// Convert world coordinates to chunk coordinates
i32 world_x = static_cast<i32>(std::floor(x));
i32 world_z = static_cast<i32>(std::floor(z));
i32 chunk_x = world_x >> 4; // Divide by 16
i32 chunk_z = world_z >> 4;
i32 local_x = world_x & 0xF; // Modulo 16
i32 local_z = world_z & 0xF;
// Get chunk
Chunk* chunk = chunk_manager_->get_chunk(chunk_x, chunk_z);
if (!chunk) {
return 64.0; // Fallback if chunk not loaded
}
// Find top solid block (start from top, search downward)
for (i32 y = 127; y > 0; y--) {
u8 block = chunk->get_block(local_x, y, local_z);
if (block != 0) { // Found first non-air block
return static_cast<f64>(y) + 1.0; // Spawn 1 block above
}
}
return 64.0; // Fallback if no solid block found
}
} // namespace mcserver
+104
View File
@@ -0,0 +1,104 @@
#pragma once
#include "entity/mob/mob.hpp"
#include "entity/mob/mob_type.hpp"
#include "entity/mob/mob_spawner.hpp"
#include "util/types.hpp"
#include <memory>
#include <unordered_map>
#include <vector>
#include <functional>
namespace mcserver {
class ClientSession;
class ChunkManager;
class Player;
class MobSpawner;
// Callback for broadcasting mob spawn packets
using MobSpawnCallback = std::function<void(const Mob* mob)>;
// Callback for broadcasting mob movement packets
using MobMovementCallback = std::function<void(i32 entity_id, f64 old_x, f64 old_y, f64 old_z,
f64 new_x, f64 new_y, f64 new_z,
f32 yaw, f32 pitch)>;
// Callback for broadcasting mob despawn packets
using MobDespawnCallback = std::function<void(i32 entity_id)>;
// Manages all mobs in the world
class MobManager {
public:
explicit MobManager(ChunkManager* chunk_manager);
// Spawn a mob at the given position
Mob* spawn_mob(MobType type, f64 x, f64 y, f64 z);
// Remove a mob by entity ID
void remove_mob(i32 entity_id);
// Update all mobs (called every server tick)
void update_all();
// Get a mob by entity ID
Mob* get_mob(i32 entity_id);
const Mob* get_mob(i32 entity_id) const;
// Get all mobs
const std::unordered_map<i32, std::unique_ptr<Mob>>& get_all_mobs() const {
return mobs_;
}
// Spawn existing mobs for a new player
void spawn_existing_mobs_for(ClientSession* session);
// Set callback for mob spawn events
void set_spawn_callback(MobSpawnCallback callback) {
spawn_callback_ = callback;
}
// Set callback for mob movement events
void set_movement_callback(MobMovementCallback callback) {
movement_callback_ = callback;
}
// Set callback for mob despawn events
void set_despawn_callback(MobDespawnCallback callback) {
despawn_callback_ = callback;
}
// Simple mob spawning around spawn point (for testing)
void spawn_test_mobs(f64 spawn_x, f64 spawn_z);
// Spawn test hostile mobs for testing
void spawn_test_hostile_mobs(f64 spawn_x, f64 spawn_z);
// Set the player list for hostile mob targeting
void set_player_list(const std::vector<Player*>* players) { players_ = players; }
// Get natural spawner
MobSpawner* get_spawner() { return spawner_.get(); }
private:
ChunkManager* chunk_manager_;
std::unordered_map<i32, std::unique_ptr<Mob>> mobs_;
std::unique_ptr<MobSpawner> spawner_;
MobSpawnCallback spawn_callback_;
MobMovementCallback movement_callback_;
MobDespawnCallback despawn_callback_;
const std::vector<Player*>* players_ = nullptr;
i32 next_entity_id_ = 1000; // Start at 1000 to avoid conflict with players
// Get next available entity ID
i32 get_next_entity_id() { return next_entity_id_++; }
// Broadcast mob movement to all clients
void broadcast_mob_movement(i32 entity_id, f64 old_x, f64 old_y, f64 old_z,
f64 new_x, f64 new_y, f64 new_z, f32 yaw, f32 pitch);
// Get ground level at given position (finds top solid block)
f64 get_ground_level(f64 x, f64 z);
};
} // namespace mcserver
+69
View File
@@ -0,0 +1,69 @@
#include "mob_metadata.hpp"
namespace mcserver {
void MobMetadata::set_byte(u8 index, i8 value) {
entries_[index] = MetadataEntry{index, MetadataType::Byte, value};
}
void MobMetadata::set_short(u8 index, i16 value) {
entries_[index] = MetadataEntry{index, MetadataType::Short, value};
}
void MobMetadata::set_int(u8 index, i32 value) {
entries_[index] = MetadataEntry{index, MetadataType::Int, value};
}
void MobMetadata::set_float(u8 index, f32 value) {
entries_[index] = MetadataEntry{index, MetadataType::Float, value};
}
void MobMetadata::set_string(u8 index, const std::string& value) {
entries_[index] = MetadataEntry{index, MetadataType::String, value};
}
i8 MobMetadata::get_byte(u8 index, i8 default_value) const {
auto it = entries_.find(index);
if (it == entries_.end() || it->second.type != MetadataType::Byte) {
return default_value;
}
return std::get<i8>(it->second.value);
}
i16 MobMetadata::get_short(u8 index, i16 default_value) const {
auto it = entries_.find(index);
if (it == entries_.end() || it->second.type != MetadataType::Short) {
return default_value;
}
return std::get<i16>(it->second.value);
}
i32 MobMetadata::get_int(u8 index, i32 default_value) const {
auto it = entries_.find(index);
if (it == entries_.end() || it->second.type != MetadataType::Int) {
return default_value;
}
return std::get<i32>(it->second.value);
}
f32 MobMetadata::get_float(u8 index, f32 default_value) const {
auto it = entries_.find(index);
if (it == entries_.end() || it->second.type != MetadataType::Float) {
return default_value;
}
return std::get<f32>(it->second.value);
}
std::string MobMetadata::get_string(u8 index, const std::string& default_value) const {
auto it = entries_.find(index);
if (it == entries_.end() || it->second.type != MetadataType::String) {
return default_value;
}
return std::get<std::string>(it->second.value);
}
bool MobMetadata::has_metadata(u8 index) const {
return entries_.find(index) != entries_.end();
}
} // namespace mcserver
+98
View File
@@ -0,0 +1,98 @@
#pragma once
#include "util/types.hpp"
#include <unordered_map>
#include <variant>
#include <string>
#include <vector>
namespace mcserver {
// Beta 1.7.3 metadata types
enum class MetadataType : u8 {
Byte = 0,
Short = 1,
Int = 2,
Float = 3,
String = 4,
ItemStack = 5,
BlockPos = 6
};
// Metadata value variant
using MetadataValue = std::variant<i8, i16, i32, f32, std::string>;
// Metadata entry with index and typed value
struct MetadataEntry {
u8 index;
MetadataType type;
MetadataValue value;
};
// Mob metadata manager (DataWatcher in original)
class MobMetadata {
public:
MobMetadata() = default;
// Set metadata values
void set_byte(u8 index, i8 value);
void set_short(u8 index, i16 value);
void set_int(u8 index, i32 value);
void set_float(u8 index, f32 value);
void set_string(u8 index, const std::string& value);
// Get metadata values
i8 get_byte(u8 index, i8 default_value = 0) const;
i16 get_short(u8 index, i16 default_value = 0) const;
i32 get_int(u8 index, i32 default_value = 0) const;
f32 get_float(u8 index, f32 default_value = 0.0f) const;
std::string get_string(u8 index, const std::string& default_value = "") const;
// Check if metadata exists
bool has_metadata(u8 index) const;
// Get all metadata entries for packet serialization
const std::unordered_map<u8, MetadataEntry>& get_all() const { return entries_; }
// Clear all metadata
void clear() { entries_.clear(); }
private:
std::unordered_map<u8, MetadataEntry> entries_;
};
// Sheep color constants (index 16, lower 4 bits)
enum class SheepColor : i8 {
White = 0,
Orange = 1,
Magenta = 2,
LightBlue = 3,
Yellow = 4,
Lime = 5,
Pink = 6,
Gray = 7,
LightGray = 8,
Cyan = 9,
Purple = 10,
Blue = 11,
Brown = 12,
Green = 13,
Red = 14,
Black = 15
};
// Wolf metadata indices
constexpr u8 WOLF_FLAGS_INDEX = 16;
constexpr u8 WOLF_OWNER_INDEX = 17;
constexpr u8 WOLF_HEALTH_INDEX = 18;
// Wolf flags (index 16)
constexpr i8 WOLF_FLAG_SITTING = 0x01;
constexpr i8 WOLF_FLAG_ANGRY = 0x02;
constexpr i8 WOLF_FLAG_TAMED = 0x04;
// Sheep metadata index
constexpr u8 SHEEP_COLOR_INDEX = 16;
constexpr i8 SHEEP_FLAG_SHEARED = 0x10;
} // namespace mcserver
+275
View File
@@ -0,0 +1,275 @@
#include "mob_spawner.hpp"
#include "mob_manager.hpp"
#include "entity/player.hpp"
#include "world/chunk/chunk_manager.hpp"
#include "world/chunk/chunk.hpp"
#include "util/log/logger.hpp"
#include <algorithm>
namespace mcserver {
// Hostile mob spawn groups (spawn in darkness, light level <= 7)
const std::vector<SpawnGroup> MobSpawner::hostile_spawns_ = {
{MobType::Zombie, 4, 4, 100}, // Common, groups of 4
{MobType::Skeleton, 4, 4, 100}, // Common, groups of 4
{MobType::Spider, 4, 4, 100}, // Common, groups of 4
{MobType::Creeper, 4, 4, 100}, // Common, groups of 4
};
// Passive mob spawn groups (spawn in light, light level >= 9)
const std::vector<SpawnGroup> MobSpawner::passive_spawns_ = {
{MobType::Pig, 4, 4, 100}, // Common, groups of 4
{MobType::Sheep, 4, 4, 100}, // Common, groups of 4
{MobType::Cow, 4, 4, 100}, // Common, groups of 4
{MobType::Chicken, 4, 4, 100}, // Common, groups of 4
};
MobSpawner::MobSpawner(MobManager* mob_manager, ChunkManager* chunk_manager)
: mob_manager_(mob_manager)
, chunk_manager_(chunk_manager) {
std::random_device rd;
random_gen_.seed(rd());
}
void MobSpawner::tick(const std::vector<Player*>& players) {
if (!enabled_ || players.empty()) {
return;
}
// Spawn cycle: attempt spawns every spawn_cycle_interval_ ticks
spawn_cycle_counter_++;
if (spawn_cycle_counter_ < spawn_cycle_interval_) {
return;
}
spawn_cycle_counter_ = 0;
// Check mob cap
i32 current_mob_count = count_mobs();
if (current_mob_count >= spawn_limit_) {
return;
}
// Attempt spawns for each player
for (const Player* player : players) {
if (!player) continue;
// Make multiple spawn attempts per cycle
for (i32 i = 0; i < SPAWN_ATTEMPTS_PER_CYCLE; ++i) {
attempt_spawn_near_player(player);
}
}
}
void MobSpawner::attempt_spawn_near_player(const Player* player) {
if (!player) return;
// Get player position
f64 player_x = player->get_x();
f64 player_z = player->get_z();
// Random angle and distance from player
std::uniform_real_distribution<f64> angle_dist(0.0, 6.28318530718); // 0 to 2*PI
std::uniform_real_distribution<f64> distance_dist(MIN_SPAWN_DISTANCE, MAX_SPAWN_DISTANCE);
std::uniform_int_distribution<i32> height_dist(MIN_SPAWN_HEIGHT, MAX_SPAWN_HEIGHT);
f64 angle = angle_dist(random_gen_);
f64 distance = distance_dist(random_gen_);
// Calculate spawn position
f64 spawn_x = player_x + distance * std::cos(angle);
f64 spawn_z = player_z + distance * std::sin(angle);
// Try multiple heights to find valid spawn
for (i32 attempt = 0; attempt < 5; ++attempt) {
i32 spawn_y = height_dist(random_gen_);
// Decide hostile or passive based on light level
u8 light = get_light_level(static_cast<i32>(spawn_x), spawn_y, static_cast<i32>(spawn_z));
const SpawnGroup* group = nullptr;
if (light <= 7) {
// Dark area - spawn hostile mobs
group = get_random_spawn_group(hostile_spawns_);
} else if (light >= 9) {
// Bright area - spawn passive mobs
group = get_random_spawn_group(passive_spawns_);
} else {
// Twilight zone - 50/50 chance
std::uniform_int_distribution<i32> coin_flip(0, 1);
if (coin_flip(random_gen_) == 0) {
group = get_random_spawn_group(hostile_spawns_);
} else {
group = get_random_spawn_group(passive_spawns_);
}
}
if (!group) continue;
// Try to spawn a group
std::uniform_int_distribution<i32> group_size_dist(group->min_group_size, group->max_group_size);
i32 group_size = group_size_dist(random_gen_);
i32 spawned = 0;
for (i32 i = 0; i < group_size; ++i) {
// Slight offset for each mob in group
std::uniform_real_distribution<f64> offset_dist(-2.0, 2.0);
f64 offset_x = offset_dist(random_gen_);
f64 offset_z = offset_dist(random_gen_);
f64 mob_x = spawn_x + offset_x;
f64 mob_z = spawn_z + offset_z;
if (try_spawn_mob(group->mob_type, mob_x, spawn_y, mob_z)) {
spawned++;
}
}
if (spawned > 0) {
// Successfully spawned some mobs, done for this attempt
return;
}
}
}
bool MobSpawner::try_spawn_mob(MobType type, f64 x, f64 y, f64 z) {
i32 ix = static_cast<i32>(std::floor(x));
i32 iy = static_cast<i32>(std::floor(y));
i32 iz = static_cast<i32>(std::floor(z));
// Check if location is valid
if (!is_valid_spawn_location(type, ix, iy, iz)) {
return false;
}
// Spawn the mob
mob_manager_->spawn_mob(type, x, y, z);
return true;
}
bool MobSpawner::is_valid_spawn_location(MobType type, i32 x, i32 y, i32 z) {
// Get the chunk
i32 chunk_x = x >> 4;
i32 chunk_z = z >> 4;
Chunk* chunk = chunk_manager_->get_chunk_if_loaded(chunk_x, chunk_z);
if (!chunk) {
return false; // Chunk not loaded
}
// Convert to local chunk coordinates
i32 local_x = x & 0xF;
i32 local_z = z & 0xF;
// Check bounds
if (y < 0 || y >= CHUNK_SIZE_Y - 2) {
return false;
}
// Get blocks at spawn location
u8 block_below = chunk->get_block(local_x, y - 1, local_z);
u8 block_at = chunk->get_block(local_x, y, local_z);
u8 block_above = chunk->get_block(local_x, y + 1, local_z);
// Need solid block below
if (!is_solid_block(block_below)) {
return false;
}
// Need air (or non-solid) at spawn position and above
if (block_at != static_cast<u8>(BlockId::Air) || block_above != static_cast<u8>(BlockId::Air)) {
return false;
}
// Don't spawn in liquids
if (is_liquid_block(block_below)) {
return false;
}
// Check light level
u8 light = get_light_level(x, y, z);
// Hostile mobs need darkness (light <= 7)
bool is_hostile = (type == MobType::Zombie || type == MobType::Skeleton ||
type == MobType::Creeper || type == MobType::Spider);
if (is_hostile && light > 7) {
return false;
}
// Passive mobs need light (light >= 9)
if (!is_hostile && light < 9) {
return false;
}
return true;
}
u8 MobSpawner::get_light_level(i32 x, i32 y, i32 z) {
// Get the chunk
i32 chunk_x = x >> 4;
i32 chunk_z = z >> 4;
Chunk* chunk = chunk_manager_->get_chunk_if_loaded(chunk_x, chunk_z);
if (!chunk) {
return 15; // Assume full light if chunk not loaded
}
// Convert to local chunk coordinates
i32 local_x = x & 0xF;
i32 local_z = z & 0xF;
// Check bounds
if (y < 0 || y >= CHUNK_SIZE_Y) {
return 15;
}
// Get max of block light and sky light
u8 block_light = chunk->get_block_light(local_x, y, local_z);
u8 sky_light = chunk->get_sky_light(local_x, y, local_z);
return std::max(block_light, sky_light);
}
bool MobSpawner::is_solid_block(u8 block_id) {
// Air and transparent blocks are not solid
return block_id != static_cast<u8>(BlockId::Air) &&
block_id != static_cast<u8>(BlockId::Glass) &&
block_id != static_cast<u8>(BlockId::Sapling) &&
!is_liquid_block(block_id);
}
bool MobSpawner::is_liquid_block(u8 block_id) {
return block_id == static_cast<u8>(BlockId::WaterFlowing) ||
block_id == static_cast<u8>(BlockId::WaterStill) ||
block_id == static_cast<u8>(BlockId::LavaFlowing) ||
block_id == static_cast<u8>(BlockId::LavaStill);
}
const SpawnGroup* MobSpawner::get_random_spawn_group(const std::vector<SpawnGroup>& groups) {
if (groups.empty()) {
return nullptr;
}
// Calculate total weight
i32 total_weight = 0;
for (const auto& group : groups) {
total_weight += group.weight;
}
// Random weighted selection
std::uniform_int_distribution<i32> weight_dist(0, total_weight - 1);
i32 random_weight = weight_dist(random_gen_);
i32 cumulative_weight = 0;
for (const auto& group : groups) {
cumulative_weight += group.weight;
if (random_weight < cumulative_weight) {
return &group;
}
}
return &groups[0]; // Fallback
}
i32 MobSpawner::count_mobs() {
return static_cast<i32>(mob_manager_->get_all_mobs().size());
}
} // namespace mcserver
+84
View File
@@ -0,0 +1,84 @@
#pragma once
#include "mob_type.hpp"
#include "util/types.hpp"
#include <vector>
#include <random>
namespace mcserver {
class MobManager;
class ChunkManager;
class Player;
// Spawn group: defines what mobs can spawn together
struct SpawnGroup {
MobType mob_type;
i32 min_group_size;
i32 max_group_size;
i32 weight; // Spawn weight (higher = more common)
};
// Natural mob spawning system for Beta 1.7.3
class MobSpawner {
public:
explicit MobSpawner(MobManager* mob_manager, ChunkManager* chunk_manager);
// Update spawning (called every tick)
void tick(const std::vector<Player*>& players);
// Set spawn limits
void set_spawn_limit(i32 limit) { spawn_limit_ = limit; }
i32 get_spawn_limit() const { return spawn_limit_; }
// Enable/disable natural spawning
void set_enabled(bool enabled) { enabled_ = enabled; }
bool is_enabled() const { return enabled_; }
private:
MobManager* mob_manager_;
ChunkManager* chunk_manager_;
std::mt19937 random_gen_;
bool enabled_ = true;
i32 spawn_limit_ = 70; // Beta 1.7.3 default mob cap
i32 spawn_cycle_counter_ = 0;
i32 spawn_cycle_interval_ = 20; // Spawn every 20 ticks (1 second)
// Spawn attempt constants
static constexpr i32 SPAWN_ATTEMPTS_PER_CYCLE = 3;
static constexpr i32 MIN_SPAWN_DISTANCE = 24; // Min blocks from player
static constexpr i32 MAX_SPAWN_DISTANCE = 128; // Max blocks from player (chunk load distance)
static constexpr i32 MAX_SPAWN_HEIGHT = 120; // Don't spawn near world height
static constexpr i32 MIN_SPAWN_HEIGHT = 1; // Don't spawn at bedrock
// Spawn groups for different mob types
static const std::vector<SpawnGroup> hostile_spawns_;
static const std::vector<SpawnGroup> passive_spawns_;
// Attempt to spawn mobs near a player
void attempt_spawn_near_player(const Player* player);
// Try to spawn a mob at a specific location
bool try_spawn_mob(MobType type, f64 x, f64 y, f64 z);
// Check if location is valid for spawning
bool is_valid_spawn_location(MobType type, i32 x, i32 y, i32 z);
// Check light level at location
u8 get_light_level(i32 x, i32 y, i32 z);
// Check if block is solid
bool is_solid_block(u8 block_id);
// Check if block is liquid
bool is_liquid_block(u8 block_id);
// Get random spawn group based on weights
const SpawnGroup* get_random_spawn_group(const std::vector<SpawnGroup>& groups);
// Count current mobs
i32 count_mobs();
};
} // namespace mcserver
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include "util/types.hpp"
namespace mcserver {
// Mob types from Beta 1.7.3 EntityList
enum class MobType : i8 {
// Generic base types
Mob = 48,
Monster = 49,
// Hostile mobs
Creeper = 50,
Skeleton = 51,
Spider = 52,
Giant = 53,
Zombie = 54,
Slime = 55,
Ghast = 56,
PigZombie = 57,
// Passive mobs
Pig = 90,
Sheep = 91,
Cow = 92,
Chicken = 93,
Squid = 94,
Wolf = 95
};
// Helper to check if a mob type is hostile
inline bool is_hostile_mob(MobType type) {
return static_cast<i8>(type) >= 49 && static_cast<i8>(type) <= 57;
}
// Helper to check if a mob type is passive
inline bool is_passive_mob(MobType type) {
return static_cast<i8>(type) >= 90 && static_cast<i8>(type) <= 95;
}
} // namespace mcserver
+103
View File
@@ -0,0 +1,103 @@
#include "entity/mob/passive_mob.hpp"
#include <random>
namespace mcserver {
// Pig implementation
MobPig::MobPig(i32 entity_id)
: Mob(entity_id, MobType::Pig) {
health_ = 10;
max_health_ = 10;
}
void MobPig::update() {
Mob::update();
// TODO: Add wandering AI, sounds, etc.
}
// Sheep implementation
MobSheep::MobSheep(i32 entity_id)
: Mob(entity_id, MobType::Sheep) {
health_ = 8;
max_health_ = 8;
// Random sheep color (weighted towards common colors)
static std::random_device rd;
static std::mt19937 gen(rd());
static std::discrete_distribution<> color_dist({
81, // White (most common)
1, // Orange
1, // Magenta
1, // Light Blue
1, // Yellow
1, // Lime
8, // Pink
10, // Gray
10, // Light Gray
1, // Cyan
1, // Purple
1, // Blue
7, // Brown
1, // Green
1, // Red
10 // Black
});
i32 color_index = color_dist(gen);
set_color(static_cast<SheepColor>(color_index));
}
void MobSheep::update() {
Mob::update();
// TODO: Add wandering AI, sounds, etc.
}
void MobSheep::set_color(SheepColor color) {
i8 current = metadata_.get_byte(SHEEP_COLOR_INDEX, 0);
i8 color_value = static_cast<i8>(color) & 0x0F; // Lower 4 bits
i8 sheared_bit = current & SHEEP_FLAG_SHEARED; // Preserve sheared flag
metadata_.set_byte(SHEEP_COLOR_INDEX, color_value | sheared_bit);
}
SheepColor MobSheep::get_color() const {
i8 value = metadata_.get_byte(SHEEP_COLOR_INDEX, 0);
return static_cast<SheepColor>(value & 0x0F);
}
void MobSheep::set_sheared(bool sheared) {
i8 current = metadata_.get_byte(SHEEP_COLOR_INDEX, 0);
i8 color_bits = current & 0x0F; // Preserve color
i8 sheared_bit = sheared ? SHEEP_FLAG_SHEARED : 0;
metadata_.set_byte(SHEEP_COLOR_INDEX, color_bits | sheared_bit);
}
bool MobSheep::is_sheared() const {
i8 value = metadata_.get_byte(SHEEP_COLOR_INDEX, 0);
return (value & SHEEP_FLAG_SHEARED) != 0;
}
// Cow implementation
MobCow::MobCow(i32 entity_id)
: Mob(entity_id, MobType::Cow) {
health_ = 10;
max_health_ = 10;
}
void MobCow::update() {
Mob::update();
// TODO: Add wandering AI, sounds, etc.
}
// Chicken implementation
MobChicken::MobChicken(i32 entity_id)
: Mob(entity_id, MobType::Chicken) {
health_ = 4;
max_health_ = 4;
}
void MobChicken::update() {
Mob::update();
// TODO: Add wandering AI, sounds, etc.
}
} // namespace mcserver
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include "entity/mob/mob.hpp"
namespace mcserver {
// Pig mob (ID 90)
class MobPig : public Mob {
public:
explicit MobPig(i32 entity_id);
void update() override;
};
// Sheep mob (ID 91)
class MobSheep : public Mob {
public:
explicit MobSheep(i32 entity_id);
void update() override;
// Set sheep color (0-15)
void set_color(SheepColor color);
SheepColor get_color() const;
// Sheared status
void set_sheared(bool sheared);
bool is_sheared() const;
};
// Cow mob (ID 92)
class MobCow : public Mob {
public:
explicit MobCow(i32 entity_id);
void update() override;
};
// Chicken mob (ID 93)
class MobChicken : public Mob {
public:
explicit MobChicken(i32 entity_id);
void update() override;
};
} // namespace mcserver
+294
View File
@@ -0,0 +1,294 @@
#include "pathfinding.hpp"
#include "world/chunk/chunk_manager.hpp"
#include "world/chunk/chunk.hpp"
#include <algorithm>
namespace mcserver {
Pathfinder::Pathfinder(ChunkManager* chunk_manager)
: chunk_manager_(chunk_manager) {
}
PathfindingResult Pathfinder::find_path(const PathNode& start, const PathNode& goal,
f64 max_distance, bool can_jump, bool can_swim) {
PathfindingResult result;
result.success = false;
result.nodes_evaluated = 0;
// Quick distance check
if (start.distance_to(goal) > max_distance) {
return result;
}
// Check if start and goal are walkable
if (!is_walkable(start.x, start.y, start.z, can_swim) ||
!is_walkable(goal.x, goal.y, goal.z, can_swim)) {
return result;
}
// A* algorithm
std::priority_queue<AStarNode, std::vector<AStarNode>, std::greater<AStarNode>> open_set;
std::unordered_map<PathNode, PathNode, PathNode::Hash> came_from;
std::unordered_map<PathNode, f64, PathNode::Hash> g_score;
std::unordered_map<PathNode, bool, PathNode::Hash> closed_set;
// Initialize start node
AStarNode start_node;
start_node.position = start;
start_node.g_cost = 0.0;
start_node.h_cost = heuristic(start, goal);
start_node.parent = start;
open_set.push(start_node);
g_score[start] = 0.0;
// Maximum nodes to evaluate (prevent infinite loops)
constexpr i32 MAX_NODES = 1000;
while (!open_set.empty() && result.nodes_evaluated < MAX_NODES) {
AStarNode current = open_set.top();
open_set.pop();
// Skip if already processed
if (closed_set[current.position]) {
continue;
}
closed_set[current.position] = true;
result.nodes_evaluated++;
// Check if we reached the goal
if (current.position == goal) {
result.path = reconstruct_path(came_from, start, goal);
result.success = true;
return result;
}
// Get neighbors
auto neighbors = get_neighbors(current.position, can_jump, can_swim);
for (const auto& neighbor : neighbors) {
// Skip if already in closed set
if (closed_set[neighbor]) {
continue;
}
// Calculate tentative g_score
f64 tentative_g = current.g_cost + current.position.distance_to(neighbor);
// Skip if this path is worse than a previously found one
auto g_it = g_score.find(neighbor);
if (g_it != g_score.end() && tentative_g >= g_it->second) {
continue;
}
// This path is better, record it
came_from[neighbor] = current.position;
g_score[neighbor] = tentative_g;
// Add to open set
AStarNode neighbor_node;
neighbor_node.position = neighbor;
neighbor_node.g_cost = tentative_g;
neighbor_node.h_cost = heuristic(neighbor, goal);
neighbor_node.parent = current.position;
open_set.push(neighbor_node);
}
}
// No path found
return result;
}
bool Pathfinder::is_walkable(i32 x, i32 y, i32 z, bool can_swim) const {
// Check bounds
if (y < 0 || y >= CHUNK_SIZE_Y - 1) {
return false;
}
// Get chunk
i32 chunk_x = x >> 4;
i32 chunk_z = z >> 4;
Chunk* chunk = chunk_manager_->get_chunk_if_loaded(chunk_x, chunk_z);
if (!chunk) {
return false; // Chunk not loaded
}
i32 local_x = x & 0xF;
i32 local_z = z & 0xF;
// Check blocks at feet and head level
u8 block_below = chunk->get_block(local_x, y - 1, local_z);
u8 block_feet = chunk->get_block(local_x, y, local_z);
u8 block_head = chunk->get_block(local_x, y + 1, local_z);
// Need solid ground below (unless swimming)
bool is_liquid_below = (block_below == static_cast<u8>(BlockId::WaterFlowing) ||
block_below == static_cast<u8>(BlockId::WaterStill) ||
block_below == static_cast<u8>(BlockId::LavaFlowing) ||
block_below == static_cast<u8>(BlockId::LavaStill));
if (!can_swim && is_liquid_below) {
return false; // Can't walk on liquid
}
if (!is_liquid_below && is_solid(x, y - 1, z) == false) {
return false; // No ground below
}
// Feet and head level must be passable
bool feet_passable = (block_feet == static_cast<u8>(BlockId::Air)) ||
(can_swim && (block_feet == static_cast<u8>(BlockId::WaterFlowing) ||
block_feet == static_cast<u8>(BlockId::WaterStill)));
bool head_passable = (block_head == static_cast<u8>(BlockId::Air)) ||
(can_swim && (block_head == static_cast<u8>(BlockId::WaterFlowing) ||
block_head == static_cast<u8>(BlockId::WaterStill)));
return feet_passable && head_passable;
}
bool Pathfinder::is_solid(i32 x, i32 y, i32 z) const {
if (y < 0 || y >= CHUNK_SIZE_Y) {
return false;
}
i32 chunk_x = x >> 4;
i32 chunk_z = z >> 4;
Chunk* chunk = chunk_manager_->get_chunk_if_loaded(chunk_x, chunk_z);
if (!chunk) {
return false;
}
i32 local_x = x & 0xF;
i32 local_z = z & 0xF;
u8 block = chunk->get_block(local_x, y, local_z);
return block != static_cast<u8>(BlockId::Air) &&
block != static_cast<u8>(BlockId::WaterFlowing) &&
block != static_cast<u8>(BlockId::WaterStill) &&
block != static_cast<u8>(BlockId::LavaFlowing) &&
block != static_cast<u8>(BlockId::LavaStill);
}
bool Pathfinder::has_ground_below(i32 x, i32 y, i32 z) const {
return is_solid(x, y - 1, z);
}
f64 Pathfinder::heuristic(const PathNode& from, const PathNode& to) const {
// Manhattan distance with diagonal cost
i32 dx = std::abs(from.x - to.x);
i32 dy = std::abs(from.y - to.y);
i32 dz = std::abs(from.z - to.z);
// Diagonal move cost
i32 straight = std::abs(dx - dz);
i32 diagonal = std::max(dx, dz) - straight;
return static_cast<f64>(straight) + diagonal * 1.414 + dy * 1.5; // Y movement is more costly
}
std::vector<PathNode> Pathfinder::get_neighbors(const PathNode& node, bool can_jump, bool can_swim) const {
std::vector<PathNode> neighbors;
// 8 horizontal directions + up/down
const i32 dx[] = {-1, 0, 1, -1, 1, -1, 0, 1, 0, 0};
const i32 dy[] = { 0, 0, 0, 0, 0, 0, 0, 0, 1,-1};
const i32 dz[] = { 0,-1, 0, 1, 1, -1,-1,-1, 0, 0};
for (usize i = 0; i < 10; ++i) {
PathNode neighbor;
neighbor.x = node.x + dx[i];
neighbor.y = node.y + dy[i];
neighbor.z = node.z + dz[i];
// Check if neighbor is walkable
if (is_walkable(neighbor.x, neighbor.y, neighbor.z, can_swim)) {
neighbors.push_back(neighbor);
}
// Try jumping up 1 block (if moving horizontally and can_jump)
if (can_jump && dy[i] == 0 && (dx[i] != 0 || dz[i] != 0)) {
PathNode jump_neighbor;
jump_neighbor.x = node.x + dx[i];
jump_neighbor.y = node.y + 1;
jump_neighbor.z = node.z + dz[i];
if (is_walkable(jump_neighbor.x, jump_neighbor.y, jump_neighbor.z, can_swim)) {
neighbors.push_back(jump_neighbor);
}
}
}
return neighbors;
}
std::vector<PathNode> Pathfinder::reconstruct_path(
const std::unordered_map<PathNode, PathNode, PathNode::Hash>& came_from,
const PathNode& start, const PathNode& goal) const {
std::vector<PathNode> path;
PathNode current = goal;
// Build path backwards from goal to start
while (current != start) {
path.push_back(current);
auto it = came_from.find(current);
if (it == came_from.end()) {
// Path broken, shouldn't happen
break;
}
current = it->second;
}
// Reverse to get path from start to goal
std::reverse(path.begin(), path.end());
return path;
}
// PathFollower implementation
PathFollower::PathFollower() = default;
void PathFollower::set_path(const std::vector<PathNode>& path) {
path_ = path;
current_waypoint_ = 0;
}
bool PathFollower::get_next_waypoint(f64 current_x, f64 current_y, f64 current_z,
f64& target_x, f64& target_y, f64& target_z) {
if (is_path_complete()) {
return false;
}
const PathNode& waypoint = path_[current_waypoint_];
// Check if we're close enough to the current waypoint to move to the next one
f64 dx = current_x - static_cast<f64>(waypoint.x);
f64 dy = current_y - static_cast<f64>(waypoint.y);
f64 dz = current_z - static_cast<f64>(waypoint.z);
f64 distance_squared = dx * dx + dy * dy + dz * dz;
// If within 0.5 blocks, move to next waypoint
if (distance_squared < 0.25) {
current_waypoint_++;
if (is_path_complete()) {
return false;
}
return get_next_waypoint(current_x, current_y, current_z, target_x, target_y, target_z);
}
// Return current waypoint as target
target_x = static_cast<f64>(waypoint.x) + 0.5; // Center of block
target_y = static_cast<f64>(waypoint.y);
target_z = static_cast<f64>(waypoint.z) + 0.5; // Center of block
return true;
}
void PathFollower::clear_path() {
path_.clear();
current_waypoint_ = 0;
}
} // namespace mcserver
+134
View File
@@ -0,0 +1,134 @@
#pragma once
#include "util/types.hpp"
#include <vector>
#include <unordered_map>
#include <queue>
#include <cmath>
namespace mcserver {
class ChunkManager;
// 3D position for pathfinding
struct PathNode {
i32 x, y, z;
bool operator==(const PathNode& other) const {
return x == other.x && y == other.y && z == other.z;
}
bool operator!=(const PathNode& other) const {
return !(*this == other);
}
f64 distance_to(const PathNode& other) const {
f64 dx = static_cast<f64>(x - other.x);
f64 dy = static_cast<f64>(y - other.y);
f64 dz = static_cast<f64>(z - other.z);
return std::sqrt(dx * dx + dy * dy + dz * dz);
}
// Hash function for unordered_map
struct Hash {
usize operator()(const PathNode& node) const {
// Simple hash combining x, y, z coordinates
usize h1 = std::hash<i32>{}(node.x);
usize h2 = std::hash<i32>{}(node.y);
usize h3 = std::hash<i32>{}(node.z);
return h1 ^ (h2 << 1) ^ (h3 << 2);
}
};
};
// Pathfinding result
struct PathfindingResult {
std::vector<PathNode> path;
bool success;
i32 nodes_evaluated;
};
// A* pathfinding for mobs
class Pathfinder {
public:
explicit Pathfinder(ChunkManager* chunk_manager);
// Find a path from start to goal
// max_distance: maximum path length to search
// can_jump: whether the mob can jump up 1 block
// can_swim: whether the mob can move through water
PathfindingResult find_path(const PathNode& start, const PathNode& goal,
f64 max_distance = 32.0,
bool can_jump = true,
bool can_swim = false);
// Check if a block is walkable
bool is_walkable(i32 x, i32 y, i32 z, bool can_swim = false) const;
// Check if a block is solid
bool is_solid(i32 x, i32 y, i32 z) const;
// Check if there's a solid block below (ground check)
bool has_ground_below(i32 x, i32 y, i32 z) const;
private:
ChunkManager* chunk_manager_;
// A* node for priority queue
struct AStarNode {
PathNode position;
f64 g_cost; // Cost from start
f64 h_cost; // Heuristic cost to goal
f64 f_cost() const { return g_cost + h_cost; }
PathNode parent;
// For priority queue (lower f_cost has higher priority)
bool operator>(const AStarNode& other) const {
return f_cost() > other.f_cost();
}
};
// Heuristic function (Manhattan distance with diagonal cost)
f64 heuristic(const PathNode& from, const PathNode& to) const;
// Get neighboring positions (includes jumping)
std::vector<PathNode> get_neighbors(const PathNode& node, bool can_jump, bool can_swim) const;
// Reconstruct path from parent map
std::vector<PathNode> reconstruct_path(const std::unordered_map<PathNode, PathNode, PathNode::Hash>& came_from,
const PathNode& start, const PathNode& goal) const;
};
// Path follower for mobs
class PathFollower {
public:
PathFollower();
// Set a new path to follow
void set_path(const std::vector<PathNode>& path);
// Get the next waypoint to move towards
// Returns false if no more waypoints
bool get_next_waypoint(f64 current_x, f64 current_y, f64 current_z,
f64& target_x, f64& target_y, f64& target_z);
// Check if the path is complete
bool is_path_complete() const { return current_waypoint_ >= path_.size(); }
// Clear the current path
void clear_path();
// Check if there's an active path
bool has_path() const { return !path_.empty() && !is_path_complete(); }
// Get remaining waypoints
usize remaining_waypoints() const {
return path_.size() > current_waypoint_ ? path_.size() - current_waypoint_ : 0;
}
private:
std::vector<PathNode> path_;
usize current_waypoint_ = 0;
};
} // namespace mcserver
+89
View File
@@ -0,0 +1,89 @@
#include "player.hpp"
#include <algorithm>
namespace mcserver {
Player::Player(std::string username, i32 entity_id)
: username_(std::move(username))
, entity_id_(entity_id)
, uuid_(UUID::from_string(username_)) {}
void Player::set_position(f64 x, f64 y, f64 z) {
x_ = x;
y_ = y;
z_ = z;
}
void Player::set_rotation(f32 yaw, f32 pitch) {
yaw_ = yaw;
pitch_ = pitch;
}
void Player::set_health(i16 health) {
health_ = std::clamp(health, static_cast<i16>(0), static_cast<i16>(20));
if (health_change_callback_) {
health_change_callback_(entity_id_, health_, false);
}
}
void Player::take_damage(i16 damage) {
if (damage <= 0 || health_ <= 0) {
return; // Already dead or invalid damage
}
health_ -= damage;
bool just_died = false;
if (health_ <= 0) {
health_ = 0;
just_died = true;
}
// Notify about health change (damage taken)
if (health_change_callback_) {
health_change_callback_(entity_id_, health_, true);
}
// Notify about death
if (just_died && death_callback_) {
death_callback_(entity_id_);
}
}
void Player::heal(i16 amount) {
if (amount <= 0 || health_ <= 0) {
return; // Dead or invalid amount
}
health_ += amount;
if (health_ > 20) {
health_ = 20;
}
// Notify about health change (healing)
if (health_change_callback_) {
health_change_callback_(entity_id_, health_, false);
}
}
void Player::respawn(f64 spawn_x, f64 spawn_y, f64 spawn_z) {
// Reset position to spawn
x_ = spawn_x;
y_ = spawn_y;
z_ = spawn_z;
// Reset rotation
yaw_ = 0.0f;
pitch_ = 0.0f;
on_ground_ = false;
// Reset health and food
health_ = 20;
food_ = 20;
// Notify about health change (respawn healing)
if (health_change_callback_) {
health_change_callback_(entity_id_, health_, false);
}
}
} // namespace mcserver
+95
View File
@@ -0,0 +1,95 @@
#pragma once
#include "entity/inventory/inventory.hpp"
#include "util/types.hpp"
#include "util/uuid.hpp"
#include <string>
#include <functional>
namespace mcserver {
// Callback for when player health changes
using PlayerHealthChangeCallback = std::function<void(i32 entity_id, i16 health, bool took_damage)>;
// Callback for when player dies (entity_id)
using PlayerDeathCallback = std::function<void(i32 entity_id)>;
// Player entity for tracking player state
class Player {
public:
Player(std::string username, i32 entity_id);
// Getters
const std::string& get_username() const { return username_; }
i32 get_entity_id() const { return entity_id_; }
const UUID& get_uuid() const { return uuid_; }
f64 get_x() const { return x_; }
f64 get_y() const { return y_; }
f64 get_z() const { return z_; }
f32 get_yaw() const { return yaw_; }
f32 get_pitch() const { return pitch_; }
bool is_on_ground() const { return on_ground_; }
i16 get_health() const { return health_; }
i16 get_food() const { return food_; }
bool is_dead() const { return health_ <= 0; }
bool is_sneaking() const { return sneaking_; }
bool is_sprinting() const { return sprinting_; }
// Setters
void set_position(f64 x, f64 y, f64 z);
void set_rotation(f32 yaw, f32 pitch);
void set_on_ground(bool on_ground) { on_ground_ = on_ground; }
void set_sneaking(bool sneaking) { sneaking_ = sneaking; }
void set_sprinting(bool sprinting) { sprinting_ = sprinting; }
void set_health(i16 health);
void set_food(i16 food) { food_ = food; }
// Damage and combat
void take_damage(i16 damage);
void heal(i16 amount);
void respawn(f64 spawn_x, f64 spawn_y, f64 spawn_z);
// Callbacks
void set_health_change_callback(PlayerHealthChangeCallback callback) {
health_change_callback_ = callback;
}
void set_death_callback(PlayerDeathCallback callback) {
death_callback_ = callback;
}
// Inventory access
Inventory* get_inventory() { return &inventory_; }
const Inventory* get_inventory() const { return &inventory_; }
private:
std::string username_;
i32 entity_id_;
UUID uuid_; // Unique identifier for this player
// Position
f64 x_ = 0.0;
f64 y_ = 64.0;
f64 z_ = 0.0;
f32 yaw_ = 0.0f;
f32 pitch_ = 0.0f;
bool on_ground_ = false;
// Stats
i16 health_ = 20; // 20 = full health (10 hearts)
i16 food_ = 20; // 20 = full food
// Actions
bool sneaking_ = false;
bool sprinting_ = false;
// Callbacks
PlayerHealthChangeCallback health_change_callback_;
PlayerDeathCallback death_callback_;
// Inventory
Inventory inventory_;
};
} // namespace mcserver
+247
View File
@@ -0,0 +1,247 @@
#include "util/log/logger.hpp"
#include "util/types.hpp"
#include "platform/net/socket.hpp"
#include "core/config/server_config.hpp"
#include "core/tick/tick_manager.hpp"
#include "core/scheduler/job_system.hpp"
#include "net/transport/network_manager.hpp"
#include "world/chunk/chunk_manager.hpp"
#include "world/generation/world_generator.hpp"
#include "storage/chunk/chunk_storage.hpp"
#include "entity/entity_manager.hpp"
#include <iostream>
#include <filesystem>
#include <csignal>
#include <atomic>
#include <cstdlib>
#include <ctime>
#include <fstream>
#include <random>
using namespace mcserver;
static std::atomic<bool> g_running{true};
void signal_handler(int signal) {
(void)signal;
g_running = false;
}
int main(int argc, char** argv) {
(void)argc;
(void)argv;
// Initialize logging
Logger::instance().init("server.log");
Logger::instance().set_min_level(LogLevel::Debug);
LOG_INFO("=== Minecraft Beta 1.7.3 Server - Modern C++ Implementation ===");
LOG_INFO("Starting server...");
// Initialize platform networking
auto net_init_result = init_networking();
if (!net_init_result) {
LOG_FATAL("Failed to initialize networking");
return 1;
}
// Load server configuration
ServerConfig config;
auto config_result = config.load("server.properties");
if (!config_result) {
LOG_WARNING("Failed to load server.properties, using defaults");
}
// Save default config if it doesn't exist
config.save("server.properties");
// Log configuration
LOG_INFO(std::string("Server IP: ") + (config.server_ip().empty() ? "*" : config.server_ip()));
LOG_INFO(std::string("Server Port: ") + std::to_string(config.server_port()));
LOG_INFO(std::string("Level Name: ") + config.level_name());
LOG_INFO(std::string("Online Mode: ") + (config.online_mode() ? "true" : "false"));
LOG_INFO(std::string("Max Players: ") + std::to_string(config.max_players()));
// Initialize job system
JobSystem job_system;
job_system.start();
LOG_INFO(std::string("Job system started with ") + std::to_string(job_system.thread_count()) + " threads");
// Create world directory structure
std::filesystem::path world_path = config.level_name();
std::filesystem::create_directories(world_path);
LOG_INFO(std::string("World directory: ") + world_path.string());
// Create necessary subdirectories
std::filesystem::path players_dir = world_path / "players";
std::filesystem::create_directories(players_dir);
LOG_INFO(std::string("Players directory: ") + players_dir.string());
// Create plugins directory
std::filesystem::path plugins_dir = "plugins";
std::filesystem::create_directories(plugins_dir);
LOG_INFO(std::string("Plugins directory: ") + plugins_dir.string());
// Initialize world seed with persistence
i64 seed = 0;
std::filesystem::path seed_file = world_path / "seed.txt";
bool seed_from_config = !config.level_seed().empty();
bool seed_file_exists = std::filesystem::exists(seed_file);
if (seed_from_config) {
// Seed specified in config - use it
const char* str = config.level_seed().c_str();
char* end;
long long value = std::strtoll(str, &end, 10);
if (end != str && *end == '\0') {
seed = static_cast<i64>(value);
} else {
// Use string hash as seed
std::hash<std::string> hasher;
seed = static_cast<i64>(hasher(config.level_seed()));
}
LOG_INFO(std::string("Using seed from config: ") + std::to_string(seed));
} else if (seed_file_exists) {
// Load existing seed from file
std::ifstream seed_input(seed_file);
if (seed_input >> seed) {
LOG_INFO(std::string("Loaded existing world seed: ") + std::to_string(seed));
} else {
LOG_WARNING("Failed to read seed file, generating new seed");
seed_file_exists = false; // Force new seed generation
}
}
if (!seed_from_config && !seed_file_exists) {
// Generate new random seed using random_device for better randomness
std::random_device rd;
std::mt19937_64 gen(rd());
std::uniform_int_distribution<i64> dis;
seed = dis(gen);
LOG_INFO(std::string("Generated new world seed: ") + std::to_string(seed));
// Save seed to file for persistence
std::ofstream seed_output(seed_file);
if (seed_output) {
seed_output << seed << std::endl;
LOG_INFO("Saved seed to seed.txt");
} else {
LOG_WARNING("Failed to save seed to seed.txt");
}
}
LOG_INFO(std::string("World seed: ") + std::to_string(seed));
// Initialize world storage
ChunkStorage chunk_storage(world_path.string());
// Initialize world generator and chunk manager
WorldGenerator world_gen(seed);
ChunkManager chunk_manager(&world_gen, &chunk_storage);
EntityManager entity_manager;
// Start network listening
NetworkManager network(&chunk_manager, world_path.string());
auto network_result = network.start(config.server_ip(), config.server_port());
if (!network_result) {
LOG_FATAL("Failed to bind to port");
shutdown_networking();
return 1;
}
LOG_INFO("Server started successfully!");
// Natural mob spawning is now enabled
// (Test mob spawning disabled - mobs will spawn naturally based on light level)
// Uncomment below to spawn test mobs at startup:
// network.get_mob_manager()->spawn_test_mobs(0.0, 0.0);
// network.get_mob_manager()->spawn_test_hostile_mobs(0.0, 0.0);
if (network.get_mob_manager()->get_spawner()) {
LOG_INFO("Natural mob spawning enabled (spawn limit: " +
std::to_string(network.get_mob_manager()->get_spawner()->get_spawn_limit()) + ")");
}
LOG_INFO("Ready to accept connections");
// Set up signal handling
std::signal(SIGINT, signal_handler);
std::signal(SIGTERM, signal_handler);
// Main server loop
TickManager tick_manager;
tick_manager.reset();
i64 tick_count = 0;
constexpr i64 auto_save_interval = 6000; // Auto-save every 5 minutes (6000 ticks)
while (g_running) {
i64 ticks_to_run = 0;
if (tick_manager.should_tick(ticks_to_run)) {
for (i64 i = 0; i < ticks_to_run && g_running; ++i) {
tick_manager.tick_started();
// Network tick
network.tick();
// World tick
chunk_manager.tick();
// Entity tick
entity_manager.tick();
// Mob tick
network.get_mob_manager()->update_all();
tick_manager.tick_finished();
++tick_count;
// Auto-save world every 5 minutes
if (tick_count % auto_save_interval == 0) {
LOG_INFO("Auto-saving world...");
chunk_manager.save_all_dirty();
LOG_INFO("World saved successfully");
}
// Log status every 20 seconds (400 ticks)
if (tick_count % 400 == 0) {
LOG_INFO_CAT(
std::string("Tick: ") + std::to_string(tick_count) +
" | Clients: " + std::to_string(network.client_count()) +
" | Chunks: " + std::to_string(chunk_manager.get_loaded_chunk_count()) +
" | Avg tick: " + std::to_string(tick_manager.average_tick_time_ms()) + "ms",
LogCategory::Performance
);
}
}
} else {
// Sleep a bit to avoid busy waiting
Clock::sleep_ms(1);
}
}
LOG_INFO("Shutting down server...");
// Save all chunks before shutdown
LOG_INFO("Saving world...");
chunk_manager.save_all();
chunk_storage.close_all();
LOG_INFO("World saved successfully");
// Stop network
network.stop();
// Stop job system
job_system.stop();
// Shutdown networking
shutdown_networking();
LOG_INFO("Server shut down cleanly");
Logger::instance().shutdown();
return 0;
}
+76
View File
@@ -0,0 +1,76 @@
add_library(net STATIC
transport/network_manager.cpp
transport/network_manager.hpp
transport/chunk_streaming_manager.cpp
transport/chunk_streaming_manager.hpp
session/client_session.cpp
session/client_session.hpp
protocol/packet.cpp
protocol/packet.hpp
protocol/packet_handler.cpp
protocol/packet_handler.hpp
protocol/packets/handshake.cpp
protocol/packets/handshake.hpp
protocol/packets/login.cpp
protocol/packets/login.hpp
protocol/packets/keepalive.cpp
protocol/packets/keepalive.hpp
protocol/packets/spawn_position.cpp
protocol/packets/spawn_position.hpp
protocol/packets/update_time.cpp
protocol/packets/update_time.hpp
protocol/packets/player_position_look.cpp
protocol/packets/player_position_look.hpp
protocol/packets/pre_chunk.cpp
protocol/packets/pre_chunk.hpp
protocol/packets/map_chunk.cpp
protocol/packets/map_chunk.hpp
protocol/packets/chat.cpp
protocol/packets/chat.hpp
protocol/packets/update_health.cpp
protocol/packets/update_health.hpp
protocol/packets/respawn.cpp
protocol/packets/respawn.hpp
protocol/packets/kick.cpp
protocol/packets/kick.hpp
protocol/packets/player_flying.cpp
protocol/packets/player_flying.hpp
protocol/packets/player_position.cpp
protocol/packets/player_position.hpp
protocol/packets/player_look.cpp
protocol/packets/player_look.hpp
protocol/packets/named_entity_spawn.cpp
protocol/packets/named_entity_spawn.hpp
protocol/packets/destroy_entity.cpp
protocol/packets/destroy_entity.hpp
protocol/packets/entity_relative_move.cpp
protocol/packets/entity_relative_move.hpp
protocol/packets/entity_look.cpp
protocol/packets/entity_look.hpp
protocol/packets/entity_look_move.cpp
protocol/packets/entity_look_move.hpp
protocol/packets/block_dig.cpp
protocol/packets/block_dig.hpp
protocol/packets/place.cpp
protocol/packets/place.hpp
protocol/packets/block_change.cpp
protocol/packets/block_change.hpp
protocol/packets/block_item_switch.cpp
protocol/packets/block_item_switch.hpp
protocol/packets/set_slot.cpp
protocol/packets/set_slot.hpp
protocol/packets/window_items.cpp
protocol/packets/window_items.hpp
protocol/packets/mob_spawn.cpp
protocol/packets/mob_spawn.hpp
protocol/packets/entity_status.cpp
protocol/packets/entity_status.hpp
)
target_include_directories(net PUBLIC
${CMAKE_SOURCE_DIR}/src
)
target_link_libraries(net PUBLIC core util platform ZLIB::ZLIB)
set_target_properties(net PROPERTIES FOLDER "Network")
+218
View File
@@ -0,0 +1,218 @@
#include "packet.hpp"
#include <cstring>
#include <algorithm>
namespace mcserver {
PacketBuffer::PacketBuffer(std::vector<byte> data)
: data_(std::move(data)), position_(0) {}
bool PacketBuffer::ensure_available(usize bytes) {
return position_ + bytes <= data_.size();
}
Result<u8> PacketBuffer::read_u8() {
if (!ensure_available(1)) {
return ErrorCode::ParseError;
}
u8 value = static_cast<u8>(data_[position_++]);
return value;
}
Result<i8> PacketBuffer::read_i8() {
auto result = read_u8();
if (!result) return result.error();
return static_cast<i8>(result.value());
}
Result<u16> PacketBuffer::read_u16() {
if (!ensure_available(2)) {
return ErrorCode::ParseError;
}
u16 value = (static_cast<u16>(data_[position_]) << 8) |
static_cast<u16>(data_[position_ + 1]);
position_ += 2;
return value;
}
Result<i16> PacketBuffer::read_i16() {
auto result = read_u16();
if (!result) return result.error();
return static_cast<i16>(result.value());
}
Result<u32> PacketBuffer::read_u32() {
if (!ensure_available(4)) {
return ErrorCode::ParseError;
}
u32 value = (static_cast<u32>(data_[position_]) << 24) |
(static_cast<u32>(data_[position_ + 1]) << 16) |
(static_cast<u32>(data_[position_ + 2]) << 8) |
static_cast<u32>(data_[position_ + 3]);
position_ += 4;
return value;
}
Result<i32> PacketBuffer::read_i32() {
auto result = read_u32();
if (!result) return result.error();
return static_cast<i32>(result.value());
}
Result<u64> PacketBuffer::read_u64() {
if (!ensure_available(8)) {
return ErrorCode::ParseError;
}
u64 value = (static_cast<u64>(data_[position_]) << 56) |
(static_cast<u64>(data_[position_ + 1]) << 48) |
(static_cast<u64>(data_[position_ + 2]) << 40) |
(static_cast<u64>(data_[position_ + 3]) << 32) |
(static_cast<u64>(data_[position_ + 4]) << 24) |
(static_cast<u64>(data_[position_ + 5]) << 16) |
(static_cast<u64>(data_[position_ + 6]) << 8) |
static_cast<u64>(data_[position_ + 7]);
position_ += 8;
return value;
}
Result<i64> PacketBuffer::read_i64() {
auto result = read_u64();
if (!result) return result.error();
return static_cast<i64>(result.value());
}
Result<f32> PacketBuffer::read_f32() {
auto result = read_u32();
if (!result) return result.error();
f32 value;
std::memcpy(&value, &result.value(), sizeof(f32));
return value;
}
Result<f64> PacketBuffer::read_f64() {
auto result = read_u64();
if (!result) return result.error();
f64 value;
std::memcpy(&value, &result.value(), sizeof(f64));
return value;
}
Result<bool> PacketBuffer::read_bool() {
auto result = read_u8();
if (!result) return result.error();
return result.value() != 0;
}
Result<std::string> PacketBuffer::read_string(usize max_length) {
auto length_result = read_i16();
if (!length_result) {
return length_result.error();
}
i16 length = length_result.value();
if (length < 0) {
return ErrorCode::ParseError;
}
if (static_cast<usize>(length) > max_length) {
return ErrorCode::ParseError;
}
// Beta 1.7.3 uses UTF-16 encoding (2 bytes per character)
usize bytes_needed = static_cast<usize>(length) * 2;
if (!ensure_available(bytes_needed)) {
return ErrorCode::ParseError;
}
std::string result;
result.reserve(length);
for (i16 i = 0; i < length; ++i) {
u16 ch = (static_cast<u16>(data_[position_]) << 8) |
static_cast<u16>(data_[position_ + 1]);
position_ += 2;
// Simple conversion (ASCII only for now, proper UTF-16 would be more complex)
if (ch < 128) {
result.push_back(static_cast<char>(ch));
} else {
result.push_back('?'); // Replace non-ASCII with placeholder
}
}
return result;
}
void PacketBuffer::write_u8(u8 value) {
data_.push_back(static_cast<byte>(value));
}
void PacketBuffer::write_i8(i8 value) {
write_u8(static_cast<u8>(value));
}
void PacketBuffer::write_u16(u16 value) {
data_.push_back(static_cast<byte>(value >> 8));
data_.push_back(static_cast<byte>(value & 0xFF));
}
void PacketBuffer::write_i16(i16 value) {
write_u16(static_cast<u16>(value));
}
void PacketBuffer::write_u32(u32 value) {
data_.push_back(static_cast<byte>(value >> 24));
data_.push_back(static_cast<byte>((value >> 16) & 0xFF));
data_.push_back(static_cast<byte>((value >> 8) & 0xFF));
data_.push_back(static_cast<byte>(value & 0xFF));
}
void PacketBuffer::write_i32(i32 value) {
write_u32(static_cast<u32>(value));
}
void PacketBuffer::write_u64(u64 value) {
data_.push_back(static_cast<byte>(value >> 56));
data_.push_back(static_cast<byte>((value >> 48) & 0xFF));
data_.push_back(static_cast<byte>((value >> 40) & 0xFF));
data_.push_back(static_cast<byte>((value >> 32) & 0xFF));
data_.push_back(static_cast<byte>((value >> 24) & 0xFF));
data_.push_back(static_cast<byte>((value >> 16) & 0xFF));
data_.push_back(static_cast<byte>((value >> 8) & 0xFF));
data_.push_back(static_cast<byte>(value & 0xFF));
}
void PacketBuffer::write_i64(i64 value) {
write_u64(static_cast<u64>(value));
}
void PacketBuffer::write_f32(f32 value) {
u32 bits;
std::memcpy(&bits, &value, sizeof(f32));
write_u32(bits);
}
void PacketBuffer::write_f64(f64 value) {
u64 bits;
std::memcpy(&bits, &value, sizeof(f64));
write_u64(bits);
}
void PacketBuffer::write_bool(bool value) {
write_u8(value ? 1 : 0);
}
void PacketBuffer::write_string(const std::string& str) {
if (str.length() > 32767) {
return; // String too long, should return error
}
write_i16(static_cast<i16>(str.length()));
// Beta 1.7.3 uses UTF-16 encoding (2 bytes per character)
for (char ch : str) {
write_u16(static_cast<u16>(static_cast<unsigned char>(ch)));
}
}
} // namespace mcserver
+131
View File
@@ -0,0 +1,131 @@
#pragma once
#include "util/types.hpp"
#include "util/result.hpp"
#include "util/span_util.hpp"
#include <string>
#include <vector>
#include <memory>
namespace mcserver {
// Packet IDs for Beta 1.7.3
enum class PacketId : u8 {
KeepAlive = 0,
Login = 1,
Handshake = 2,
Chat = 3,
UpdateTime = 4,
PlayerInventory = 5,
SpawnPosition = 6,
UseEntity = 7,
UpdateHealth = 8,
Respawn = 9,
Flying = 10,
PlayerPosition = 11,
PlayerLook = 12,
PlayerLookMove = 13,
BlockDig = 14,
Place = 15,
BlockItemSwitch = 16,
Sleep = 17,
Animation = 18,
EntityAction = 19,
NamedEntitySpawn = 20,
PickupSpawn = 21,
Collect = 22,
VehicleSpawn = 23,
MobSpawn = 24,
EntityPainting = 25,
Position = 27,
EntityVelocity = 28,
DestroyEntity = 29,
Entity = 30,
RelEntityMove = 31,
EntityLook = 32,
RelEntityMoveLook = 33,
EntityTeleport = 34,
EntityStatus = 38,
AttachEntity = 39,
EntityMetadata = 40,
PreChunk = 50,
MapChunk = 51,
MultiBlockChange = 52,
BlockChange = 53,
PlayNoteBlock = 54,
Explosion = 60,
DoorChange = 61,
Bed = 70,
Weather = 71,
OpenWindow = 100,
CloseWindow = 101,
WindowClick = 102,
SetSlot = 103,
WindowItems = 104,
UpdateProgressbar = 105,
Transaction = 106,
UpdateSign = 130,
MapData = 131,
Statistic = 200,
Kick = 255
};
// Packet reader/writer for Beta 1.7.3 format
class PacketBuffer {
public:
explicit PacketBuffer(std::vector<byte> data = {});
// Read operations
Result<u8> read_u8();
Result<i8> read_i8();
Result<u16> read_u16();
Result<i16> read_i16();
Result<u32> read_u32();
Result<i32> read_i32();
Result<u64> read_u64();
Result<i64> read_i64();
Result<f32> read_f32();
Result<f64> read_f64();
Result<bool> read_bool();
Result<std::string> read_string(usize max_length = 32767);
// Write operations
void write_u8(u8 value);
void write_i8(i8 value);
void write_u16(u16 value);
void write_i16(i16 value);
void write_u32(u32 value);
void write_i32(i32 value);
void write_u64(u64 value);
void write_i64(i64 value);
void write_f32(f32 value);
void write_f64(f64 value);
void write_bool(bool value);
void write_string(const std::string& str);
// Buffer management
const std::vector<byte>& data() const { return data_; }
std::vector<byte>&& take_data() { return std::move(data_); }
usize size() const { return data_.size(); }
usize position() const { return position_; }
void reset_position() { position_ = 0; }
private:
std::vector<byte> data_;
usize position_ = 0;
bool ensure_available(usize bytes);
};
// Base packet class
class Packet {
public:
virtual ~Packet() = default;
virtual PacketId get_id() const = 0;
virtual Result<void> read(PacketBuffer& buffer) = 0;
virtual Result<void> write(PacketBuffer& buffer) const = 0;
virtual usize estimated_size() const = 0;
};
} // namespace mcserver
+2
View File
@@ -0,0 +1,2 @@
// Placeholder implementation
// Will be expanded in Step 2
+13
View File
@@ -0,0 +1,13 @@
#pragma once
// Packet handler will be expanded in Step 2
// For Step 1, packet handling is done in ClientSession
namespace mcserver {
class PacketHandler {
public:
// Placeholder for Step 2 expansion
};
} // namespace mcserver
+60
View File
@@ -0,0 +1,60 @@
#pragma once
#include "net/protocol/packet.hpp"
#include "util/types.hpp"
namespace mcserver {
// Animation types
enum class AnimationType : i8 {
NoAnimation = 0,
SwingArm = 1,
Damage = 2,
LeaveBed = 3,
EatFood = 5,
Unknown = 102,
Crouch = 104,
UnCrouch = 105
};
// Packet 18 - Animation
// Sent when a player animates (e.g., arm swing)
// Bidirectional - can be sent by client or server
class PacketAnimation : public Packet {
public:
i32 entity_id;
AnimationType animation;
PacketAnimation() : entity_id(0), animation(AnimationType::NoAnimation) {}
PacketAnimation(i32 eid, AnimationType anim)
: entity_id(eid), animation(anim) {}
PacketId get_id() const override {
return PacketId::Animation;
}
usize estimated_size() const override {
return 5; // 1 byte ID + 4 bytes entity_id + 1 byte animation
}
Result<void> read(PacketBuffer& buffer) override {
auto eid_result = buffer.read_i32();
if (!eid_result) return eid_result.error();
entity_id = eid_result.value();
auto anim_result = buffer.read_i8();
if (!anim_result) return anim_result.error();
animation = static_cast<AnimationType>(anim_result.value());
return {};
}
Result<void> write(PacketBuffer& buffer) const override {
buffer.write_i32(entity_id);
buffer.write_i8(static_cast<i8>(animation));
return {};
}
};
} // namespace mcserver
+56
View File
@@ -0,0 +1,56 @@
#include "block_change.hpp"
namespace mcserver {
PacketBlockChange::PacketBlockChange(i32 x, i8 y, i32 z, u8 block_type, u8 block_metadata)
: x(x)
, y(y)
, z(z)
, block_type(block_type)
, block_metadata(block_metadata) {}
Result<void> PacketBlockChange::read(PacketBuffer& buffer) {
auto x_result = buffer.read_i32();
if (!x_result) {
return Result<void>(x_result.error());
}
x = x_result.value();
auto y_result = buffer.read_i8();
if (!y_result) {
return Result<void>(y_result.error());
}
y = y_result.value();
auto z_result = buffer.read_i32();
if (!z_result) {
return Result<void>(z_result.error());
}
z = z_result.value();
auto type_result = buffer.read_u8();
if (!type_result) {
return Result<void>(type_result.error());
}
block_type = type_result.value();
auto meta_result = buffer.read_u8();
if (!meta_result) {
return Result<void>(meta_result.error());
}
block_metadata = meta_result.value();
return Result<void>();
}
Result<void> PacketBlockChange::write(PacketBuffer& buffer) const {
buffer.write_i32(x);
buffer.write_i8(y);
buffer.write_i32(z);
buffer.write_u8(block_type);
buffer.write_u8(block_metadata);
return Result<void>();
}
} // namespace mcserver
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include "net/protocol/packet.hpp"
namespace mcserver {
// Packet 53: BlockChange
// Server -> Client
// Sent when a single block is changed
class PacketBlockChange : public Packet {
public:
PacketBlockChange() = default;
PacketBlockChange(i32 x, i8 y, i32 z, u8 block_type, u8 block_metadata);
PacketId get_id() const override { return PacketId::BlockChange; }
Result<void> read(PacketBuffer& buffer) override;
Result<void> write(PacketBuffer& buffer) const override;
usize estimated_size() const override { return 11; } // 4+1+4+1+1
i32 x = 0;
i8 y = 0;
i32 z = 0;
u8 block_type = 0; // Block ID (0 = air)
u8 block_metadata = 0; // Block data/metadata
};
} // namespace mcserver
+56
View File
@@ -0,0 +1,56 @@
#include "block_dig.hpp"
namespace mcserver {
PacketBlockDig::PacketBlockDig(DigStatus status, i32 x, i8 y, i32 z, i8 face)
: status(status)
, x(x)
, y(y)
, z(z)
, face(face) {}
Result<void> PacketBlockDig::read(PacketBuffer& buffer) {
auto status_result = buffer.read_u8();
if (!status_result) {
return Result<void>(status_result.error());
}
status = static_cast<DigStatus>(status_result.value());
auto x_result = buffer.read_i32();
if (!x_result) {
return Result<void>(x_result.error());
}
x = x_result.value();
auto y_result = buffer.read_i8();
if (!y_result) {
return Result<void>(y_result.error());
}
y = y_result.value();
auto z_result = buffer.read_i32();
if (!z_result) {
return Result<void>(z_result.error());
}
z = z_result.value();
auto face_result = buffer.read_i8();
if (!face_result) {
return Result<void>(face_result.error());
}
face = face_result.value();
return Result<void>();
}
Result<void> PacketBlockDig::write(PacketBuffer& buffer) const {
buffer.write_u8(static_cast<u8>(status));
buffer.write_i32(x);
buffer.write_i8(y);
buffer.write_i32(z);
buffer.write_i8(face);
return Result<void>();
}
} // namespace mcserver
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include "net/protocol/packet.hpp"
namespace mcserver {
// Digging status values
enum class DigStatus : u8 {
Started = 0, // Started digging
Cancelled = 1, // Cancelled digging
Finished = 2, // Finished digging
DropItemStack = 3, // Drop item stack (Q key)
DropItem = 4, // Drop single item (Ctrl+Q)
ShootArrow = 5 // Shoot arrow / finish eating
};
// Packet 14: BlockDig
// Client -> Server
// Sent when player digs/breaks a block
class PacketBlockDig : public Packet {
public:
PacketBlockDig() = default;
PacketBlockDig(DigStatus status, i32 x, i8 y, i32 z, i8 face);
PacketId get_id() const override { return PacketId::BlockDig; }
Result<void> read(PacketBuffer& buffer) override;
Result<void> write(PacketBuffer& buffer) const override;
usize estimated_size() const override { return 11; } // 1+4+1+4+1
DigStatus status = DigStatus::Started;
i32 x = 0;
i8 y = 0;
i32 z = 0;
i8 face = 0; // Face being hit (0-5: -Y, +Y, -Z, +Z, -X, +X)
};
} // namespace mcserver
@@ -0,0 +1,19 @@
#include "net/protocol/packets/block_item_switch.hpp"
namespace mcserver {
PacketBlockItemSwitch::PacketBlockItemSwitch(i16 slot) : slot(slot) {}
Result<void> PacketBlockItemSwitch::read(PacketBuffer& buffer) {
auto slot_result = buffer.read_i16();
if (!slot_result) return slot_result.error();
slot = slot_result.value();
return Result<void>();
}
Result<void> PacketBlockItemSwitch::write(PacketBuffer& buffer) const {
buffer.write_i16(slot);
return Result<void>();
}
} // namespace mcserver
@@ -0,0 +1,23 @@
#pragma once
#include "net/protocol/packet.hpp"
#include "util/types.hpp"
namespace mcserver {
// Packet 16: BlockItemSwitch (Client→Server)
// Sent when player switches their currently held item (hotbar slot 0-8)
class PacketBlockItemSwitch : public Packet {
public:
PacketBlockItemSwitch() = default;
explicit PacketBlockItemSwitch(i16 slot);
PacketId get_id() const override { return PacketId::BlockItemSwitch; }
Result<void> read(PacketBuffer& buffer) override;
Result<void> write(PacketBuffer& buffer) const override;
usize estimated_size() const override { return 2; }
i16 slot = 0; // Hotbar slot (0-8)
};
} // namespace mcserver
+24
View File
@@ -0,0 +1,24 @@
#include "chat.hpp"
namespace mcserver {
PacketChat::PacketChat(std::string message) : message(std::move(message)) {}
Result<void> PacketChat::read(PacketBuffer& buffer) {
auto msg_result = buffer.read_string(119); // Max length in Beta 1.7.3
if (!msg_result) return msg_result.error();
message = msg_result.value();
return Result<void>();
}
Result<void> PacketChat::write(PacketBuffer& buffer) const {
buffer.write_string(message);
return Result<void>();
}
usize PacketChat::estimated_size() const {
// 2 bytes for string length + (string length * 2 for UTF-16)
return 2 + (message.length() * 2);
}
} // namespace mcserver
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include "net/protocol/packet.hpp"
namespace mcserver {
// Packet 3: Chat
// Bidirectional - server can send messages, client can send messages
class PacketChat : public Packet {
public:
PacketChat() = default;
explicit PacketChat(std::string message);
PacketId get_id() const override { return PacketId::Chat; }
Result<void> read(PacketBuffer& buffer) override;
Result<void> write(PacketBuffer& buffer) const override;
usize estimated_size() const override;
std::string message;
};
} // namespace mcserver
+40
View File
@@ -0,0 +1,40 @@
#pragma once
#include "net/protocol/packet.hpp"
#include "util/types.hpp"
namespace mcserver {
// Packet 101 - Close Window
// Sent when a player closes a window (inventory, chest, etc.)
// Server-bound only
class PacketCloseWindow : public Packet {
public:
i8 window_id; // 0 for player inventory
PacketCloseWindow() : window_id(0) {}
explicit PacketCloseWindow(i8 wid) : window_id(wid) {}
PacketId get_id() const override {
return PacketId::CloseWindow;
}
usize estimated_size() const override {
return 1; // 1 byte window_id
}
Result<void> read(PacketBuffer& buffer) override {
auto wid_result = buffer.read_i8();
if (!wid_result) return wid_result.error();
window_id = wid_result.value();
return {};
}
Result<void> write(PacketBuffer& buffer) const override {
buffer.write_i8(window_id);
return {};
}
};
} // namespace mcserver
+51
View File
@@ -0,0 +1,51 @@
#pragma once
#include "net/protocol/packet.hpp"
#include "util/types.hpp"
namespace mcserver {
// Packet 22 - Collect Item
// Sent when a player picks up an item
// Server->Client only
class PacketCollect : public Packet {
public:
i32 collected_entity_id; // The item entity that was picked up
i32 collector_entity_id; // The player who picked it up
PacketCollect()
: collected_entity_id(0)
, collector_entity_id(0) {}
PacketCollect(i32 item_eid, i32 player_eid)
: collected_entity_id(item_eid)
, collector_entity_id(player_eid) {}
PacketId get_id() const override {
return PacketId::Collect;
}
usize estimated_size() const override {
return 8; // 4+4 = 8 bytes
}
Result<void> read(PacketBuffer& buffer) override {
auto collected_result = buffer.read_i32();
if (!collected_result) return collected_result.error();
collected_entity_id = collected_result.value();
auto collector_result = buffer.read_i32();
if (!collector_result) return collector_result.error();
collector_entity_id = collector_result.value();
return {};
}
Result<void> write(PacketBuffer& buffer) const override {
buffer.write_i32(collected_entity_id);
buffer.write_i32(collector_entity_id);
return {};
}
};
} // namespace mcserver
@@ -0,0 +1,23 @@
#include "destroy_entity.hpp"
namespace mcserver {
PacketDestroyEntity::PacketDestroyEntity(i32 entity_id)
: entity_id(entity_id) {}
Result<void> PacketDestroyEntity::read(PacketBuffer& buffer) {
auto entity_id_result = buffer.read_i32();
if (!entity_id_result) {
return Result<void>(entity_id_result.error());
}
entity_id = entity_id_result.value();
return Result<void>();
}
Result<void> PacketDestroyEntity::write(PacketBuffer& buffer) const {
buffer.write_i32(entity_id);
return Result<void>();
}
} // namespace mcserver
@@ -0,0 +1,23 @@
#pragma once
#include "net/protocol/packet.hpp"
namespace mcserver {
// Packet 29: DestroyEntity
// Server -> Client
// Sent when an entity is destroyed/removed
class PacketDestroyEntity : public Packet {
public:
PacketDestroyEntity() = default;
explicit PacketDestroyEntity(i32 entity_id);
PacketId get_id() const override { return PacketId::DestroyEntity; }
Result<void> read(PacketBuffer& buffer) override;
Result<void> write(PacketBuffer& buffer) const override;
usize estimated_size() const override { return 4; } // Entity ID only
i32 entity_id = 0;
};
} // namespace mcserver
@@ -0,0 +1,57 @@
#pragma once
#include "net/protocol/packet.hpp"
#include "util/types.hpp"
namespace mcserver {
// Entity action states
enum class EntityActionState : i8 {
Crouch = 1, // Start sneaking
Uncrouch = 2, // Stop sneaking
LeaveBed = 3, // Wake up from bed
StartSprinting = 4,
StopSprinting = 5
};
// Packet 19 - Entity Action
// Sent when a player performs an action (sneak, wake up, etc.)
// Server-bound only
class PacketEntityAction : public Packet {
public:
i32 entity_id;
EntityActionState state;
PacketEntityAction() : entity_id(0), state(EntityActionState::Crouch) {}
PacketEntityAction(i32 eid, EntityActionState s)
: entity_id(eid), state(s) {}
PacketId get_id() const override {
return PacketId::EntityAction;
}
usize estimated_size() const override {
return 5; // 1 byte ID + 4 bytes entity_id + 1 byte state
}
Result<void> read(PacketBuffer& buffer) override {
auto eid_result = buffer.read_i32();
if (!eid_result) return eid_result.error();
entity_id = eid_result.value();
auto state_result = buffer.read_i8();
if (!state_result) return state_result.error();
state = static_cast<EntityActionState>(state_result.value());
return {};
}
Result<void> write(PacketBuffer& buffer) const override {
buffer.write_i32(entity_id);
buffer.write_i8(static_cast<i8>(state));
return {};
}
};
} // namespace mcserver
+40
View File
@@ -0,0 +1,40 @@
#include "entity_look.hpp"
namespace mcserver {
PacketEntityLook::PacketEntityLook(i32 entity_id, i8 yaw, i8 pitch)
: entity_id(entity_id)
, yaw(yaw)
, pitch(pitch) {}
Result<void> PacketEntityLook::read(PacketBuffer& buffer) {
auto entity_id_result = buffer.read_i32();
if (!entity_id_result) {
return Result<void>(entity_id_result.error());
}
entity_id = entity_id_result.value();
auto yaw_result = buffer.read_i8();
if (!yaw_result) {
return Result<void>(yaw_result.error());
}
yaw = yaw_result.value();
auto pitch_result = buffer.read_i8();
if (!pitch_result) {
return Result<void>(pitch_result.error());
}
pitch = pitch_result.value();
return Result<void>();
}
Result<void> PacketEntityLook::write(PacketBuffer& buffer) const {
buffer.write_i32(entity_id);
buffer.write_i8(yaw);
buffer.write_i8(pitch);
return Result<void>();
}
} // namespace mcserver
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include "net/protocol/packet.hpp"
namespace mcserver {
// Packet 32: EntityLook
// Server -> Client
// Sent when an entity rotates
class PacketEntityLook : public Packet {
public:
PacketEntityLook() = default;
PacketEntityLook(i32 entity_id, i8 yaw, i8 pitch);
PacketId get_id() const override { return PacketId::EntityLook; }
Result<void> read(PacketBuffer& buffer) override;
Result<void> write(PacketBuffer& buffer) const override;
usize estimated_size() const override { return 6; } // 4 + 1 + 1
i32 entity_id = 0;
i8 yaw = 0; // Rotation: angle * 256 / 360
i8 pitch = 0;
};
} // namespace mcserver
@@ -0,0 +1,64 @@
#include "entity_look_move.hpp"
namespace mcserver {
PacketEntityLookMove::PacketEntityLookMove(i32 entity_id, i8 dx, i8 dy, i8 dz, i8 yaw, i8 pitch)
: entity_id(entity_id)
, dx(dx)
, dy(dy)
, dz(dz)
, yaw(yaw)
, pitch(pitch) {}
Result<void> PacketEntityLookMove::read(PacketBuffer& buffer) {
auto entity_id_result = buffer.read_i32();
if (!entity_id_result) {
return Result<void>(entity_id_result.error());
}
entity_id = entity_id_result.value();
auto dx_result = buffer.read_i8();
if (!dx_result) {
return Result<void>(dx_result.error());
}
dx = dx_result.value();
auto dy_result = buffer.read_i8();
if (!dy_result) {
return Result<void>(dy_result.error());
}
dy = dy_result.value();
auto dz_result = buffer.read_i8();
if (!dz_result) {
return Result<void>(dz_result.error());
}
dz = dz_result.value();
auto yaw_result = buffer.read_i8();
if (!yaw_result) {
return Result<void>(yaw_result.error());
}
yaw = yaw_result.value();
auto pitch_result = buffer.read_i8();
if (!pitch_result) {
return Result<void>(pitch_result.error());
}
pitch = pitch_result.value();
return Result<void>();
}
Result<void> PacketEntityLookMove::write(PacketBuffer& buffer) const {
buffer.write_i32(entity_id);
buffer.write_i8(dx);
buffer.write_i8(dy);
buffer.write_i8(dz);
buffer.write_i8(yaw);
buffer.write_i8(pitch);
return Result<void>();
}
} // namespace mcserver
@@ -0,0 +1,28 @@
#pragma once
#include "net/protocol/packet.hpp"
namespace mcserver {
// Packet 33: RelEntityMoveLook
// Server -> Client
// Sent when an entity moves and rotates at the same time
class PacketEntityLookMove : public Packet {
public:
PacketEntityLookMove() = default;
PacketEntityLookMove(i32 entity_id, i8 dx, i8 dy, i8 dz, i8 yaw, i8 pitch);
PacketId get_id() const override { return PacketId::RelEntityMoveLook; }
Result<void> read(PacketBuffer& buffer) override;
Result<void> write(PacketBuffer& buffer) const override;
usize estimated_size() const override { return 9; } // 4 + 1 + 1 + 1 + 1 + 1
i32 entity_id = 0;
i8 dx = 0; // Relative movement in fixed-point (actual * 32)
i8 dy = 0;
i8 dz = 0;
i8 yaw = 0; // Rotation: angle * 256 / 360
i8 pitch = 0;
};
} // namespace mcserver
@@ -0,0 +1,48 @@
#include "entity_relative_move.hpp"
namespace mcserver {
PacketEntityRelativeMove::PacketEntityRelativeMove(i32 entity_id, i8 dx, i8 dy, i8 dz)
: entity_id(entity_id)
, dx(dx)
, dy(dy)
, dz(dz) {}
Result<void> PacketEntityRelativeMove::read(PacketBuffer& buffer) {
auto entity_id_result = buffer.read_i32();
if (!entity_id_result) {
return Result<void>(entity_id_result.error());
}
entity_id = entity_id_result.value();
auto dx_result = buffer.read_i8();
if (!dx_result) {
return Result<void>(dx_result.error());
}
dx = dx_result.value();
auto dy_result = buffer.read_i8();
if (!dy_result) {
return Result<void>(dy_result.error());
}
dy = dy_result.value();
auto dz_result = buffer.read_i8();
if (!dz_result) {
return Result<void>(dz_result.error());
}
dz = dz_result.value();
return Result<void>();
}
Result<void> PacketEntityRelativeMove::write(PacketBuffer& buffer) const {
buffer.write_i32(entity_id);
buffer.write_i8(dx);
buffer.write_i8(dy);
buffer.write_i8(dz);
return Result<void>();
}
} // namespace mcserver
@@ -0,0 +1,26 @@
#pragma once
#include "net/protocol/packet.hpp"
namespace mcserver {
// Packet 31: EntityRelativeMove
// Server -> Client
// Sent when an entity moves by a small amount (< 4 blocks)
class PacketEntityRelativeMove : public Packet {
public:
PacketEntityRelativeMove() = default;
PacketEntityRelativeMove(i32 entity_id, i8 dx, i8 dy, i8 dz);
PacketId get_id() const override { return PacketId::RelEntityMove; }
Result<void> read(PacketBuffer& buffer) override;
Result<void> write(PacketBuffer& buffer) const override;
usize estimated_size() const override { return 7; } // 4 + 1 + 1 + 1
i32 entity_id = 0;
i8 dx = 0; // Relative movement in fixed-point (actual * 32)
i8 dy = 0;
i8 dz = 0;
};
} // namespace mcserver
@@ -0,0 +1,27 @@
#include "net/protocol/packets/entity_status.hpp"
namespace mcserver {
PacketEntityStatus::PacketEntityStatus(i32 entity_id, i8 status)
: entity_id(entity_id), status(status) {
}
Result<void> PacketEntityStatus::read(PacketBuffer& buffer) {
auto entity_id_result = buffer.read_i32();
if (!entity_id_result) return entity_id_result.error();
entity_id = entity_id_result.value();
auto status_result = buffer.read_i8();
if (!status_result) return status_result.error();
status = status_result.value();
return Result<void>();
}
Result<void> PacketEntityStatus::write(PacketBuffer& buffer) const {
buffer.write_i32(entity_id);
buffer.write_i8(status);
return Result<void>();
}
} // namespace mcserver
@@ -0,0 +1,24 @@
#pragma once
#include "net/protocol/packet.hpp"
#include "util/types.hpp"
namespace mcserver {
// Packet 38: EntityStatus (Server→Client)
// Sent when an entity's status changes (damage, death, etc.)
class PacketEntityStatus : public Packet {
public:
PacketEntityStatus() = default;
PacketEntityStatus(i32 entity_id, i8 status);
PacketId get_id() const override { return PacketId::EntityStatus; }
Result<void> read(PacketBuffer& buffer) override;
Result<void> write(PacketBuffer& buffer) const override;
usize estimated_size() const override { return 5; } // 4 + 1
i32 entity_id = 0;
i8 status = 0; // 2 = hurt, 3 = dead, etc.
};
} // namespace mcserver
+27
View File
@@ -0,0 +1,27 @@
#include "handshake.hpp"
namespace mcserver {
PacketHandshake::PacketHandshake(std::string username)
: username(std::move(username)) {}
Result<void> PacketHandshake::read(PacketBuffer& buffer) {
auto username_result = buffer.read_string(32);
if (!username_result) {
return username_result.error();
}
username = username_result.value();
return Result<void>();
}
Result<void> PacketHandshake::write(PacketBuffer& buffer) const {
buffer.write_string(username);
return Result<void>();
}
usize PacketHandshake::estimated_size() const {
return 4 + username.length() * 2 + 4;
}
} // namespace mcserver
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include "net/protocol/packet.hpp"
namespace mcserver {
// Packet 2: Handshake
class PacketHandshake : public Packet {
public:
PacketHandshake() = default;
explicit PacketHandshake(std::string username);
PacketId get_id() const override { return PacketId::Handshake; }
Result<void> read(PacketBuffer& buffer) override;
Result<void> write(PacketBuffer& buffer) const override;
usize estimated_size() const override;
std::string username;
};
} // namespace mcserver
+17
View File
@@ -0,0 +1,17 @@
#include "keepalive.hpp"
namespace mcserver {
Result<void> PacketKeepAlive::read(PacketBuffer& buffer) {
// KeepAlive packet has no data
(void)buffer;
return Result<void>();
}
Result<void> PacketKeepAlive::write(PacketBuffer& buffer) const {
// KeepAlive packet has no data
(void)buffer;
return Result<void>();
}
} // namespace mcserver
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include "net/protocol/packet.hpp"
namespace mcserver {
// Packet 0: KeepAlive
class PacketKeepAlive : public Packet {
public:
PacketKeepAlive() = default;
PacketId get_id() const override { return PacketId::KeepAlive; }
Result<void> read(PacketBuffer& buffer) override;
Result<void> write(PacketBuffer& buffer) const override;
usize estimated_size() const override { return 0; }
};
} // namespace mcserver
+24
View File
@@ -0,0 +1,24 @@
#include "kick.hpp"
namespace mcserver {
PacketKick::PacketKick(std::string reason) : reason(std::move(reason)) {}
Result<void> PacketKick::read(PacketBuffer& buffer) {
auto reason_result = buffer.read_string(256);
if (!reason_result) return reason_result.error();
reason = reason_result.value();
return Result<void>();
}
Result<void> PacketKick::write(PacketBuffer& buffer) const {
buffer.write_string(reason);
return Result<void>();
}
usize PacketKick::estimated_size() const {
// 2 bytes for string length + (string length * 2 for UTF-16)
return 2 + (reason.length() * 2);
}
} // namespace mcserver
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include "net/protocol/packet.hpp"
namespace mcserver {
// Packet 255: Kick/Disconnect
// Server -> Client
// Sent to disconnect a client with a reason
class PacketKick : public Packet {
public:
PacketKick() = default;
explicit PacketKick(std::string reason);
PacketId get_id() const override { return PacketId::Kick; }
Result<void> read(PacketBuffer& buffer) override;
Result<void> write(PacketBuffer& buffer) const override;
usize estimated_size() const override;
std::string reason;
};
} // namespace mcserver

Some files were not shown because too many files have changed in this diff Show More