mirror of
https://github.com/ApfelTeeSaft/Minecraft-B1.7.3-CPP.git
synced 2026-08-26 19:33:26 +00:00
I forgot to open source this 💀
This commit is contained in:
+202
@@ -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 "")
|
||||
@@ -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
@@ -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.
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;")
|
||||
@@ -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()
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
// Stub for Step 1
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
// Placeholder for Step 2
|
||||
|
||||
namespace mcserver {
|
||||
|
||||
class Entity {
|
||||
// Will be implemented in Step 2
|
||||
};
|
||||
|
||||
} // namespace mcserver
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
// Placeholder implementation
|
||||
// Will be expanded in Step 2
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user