From 8e7dfd85b81f302746094d7f41730145b0446551 Mon Sep 17 00:00:00 2001 From: TheBrokenRail Date: Thu, 5 Nov 2020 23:05:37 -0500 Subject: [PATCH] Save Player Data --- README.md | 2 +- mods/CMakeLists.txt | 4 +- mods/src/server/playerdata.cpp | 252 ++++++++++++++++++++++++++++++ mods/src/server/playerdata.h | 8 + mods/src/server/server.cpp | 36 ++++- mods/src/server/server_internal.h | 13 ++ 6 files changed, 307 insertions(+), 8 deletions(-) create mode 100644 mods/src/server/playerdata.cpp create mode 100644 mods/src/server/playerdata.h create mode 100644 mods/src/server/server_internal.h diff --git a/README.md b/README.md index 753f39bdf..5c28fbf50 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ To use, install the ``minecraft-pi-server`` package and run ``minecraft-pi-serve This is also compatible with MCPE 0.6.1. ### Limitations -- Player data is not saved because of limitations with MCPE LAN worlds +- Player inventories are not saved because of limitations with MCPE LAN worlds - An easy workaround is to place your inventory in a chest before logging off - Survival mode servers are only compatible with ``minecraft-pi-docker`` clients diff --git a/mods/CMakeLists.txt b/mods/CMakeLists.txt index c315265d8..0b1d5f690 100644 --- a/mods/CMakeLists.txt +++ b/mods/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.13.0) project(mods) -add_compile_options(-Wall -Wextra -Werror) +add_compile_options(-g -Wall -Wextra -Werror) add_link_options(-Wl,--no-undefined) add_definitions(-D_GLIBCXX_USE_CXX11_ABI=0) @@ -13,7 +13,7 @@ include_directories(include) add_library(core SHARED src/core.c) target_link_libraries(core dl) -add_library(server SHARED src/server/server.cpp src/server/server_properties.cpp) +add_library(server SHARED src/server/server.cpp src/server/server_properties.cpp src/server/playerdata.cpp) target_link_libraries(server core dl SDL pthread) add_library(extra SHARED src/extra.c src/extra.cpp src/cxx11_util.cpp) diff --git a/mods/src/server/playerdata.cpp b/mods/src/server/playerdata.cpp new file mode 100644 index 000000000..4de955c09 --- /dev/null +++ b/mods/src/server/playerdata.cpp @@ -0,0 +1,252 @@ +#include + +#include "playerdata.h" +#include "server_internal.h" + +struct RakNet_BitStream { + unsigned char data[273]; +}; +typedef unsigned char *(*RakNet_BitStream_constructor_t)(RakNet_BitStream *stream); +static RakNet_BitStream_constructor_t RakNet_BitStream_constructor = (RakNet_BitStream_constructor_t) 0xd3b84; +typedef void (*RakNet_BitStream_destructor_t)(RakNet_BitStream *stream); +static RakNet_BitStream_destructor_t RakNet_BitStream_destructor = (RakNet_BitStream_destructor_t) 0xd3ce8; + +struct RakDataOutput { + unsigned char data[8]; +}; +static unsigned char *RakDataOutput_vtable = (unsigned char *) 0x109628; + +typedef unsigned char *(*CompoundTag_t)(unsigned char *tag); +static CompoundTag_t CompoundTag = (CompoundTag_t) 0xb9920; + +typedef void (*Tag_writeNamedTag_t)(unsigned char *tag, RakDataOutput *output); +static Tag_writeNamedTag_t Tag_writeNamedTag = (Tag_writeNamedTag_t) 0x6850c; + +typedef void (*Entity_saveWithoutId_t)(unsigned char *entity, unsigned char *tag); + +typedef void (*Tag_deleteChildren_t)(unsigned char *tag); +typedef void (*Tag_destructor_t)(unsigned char *tag); + +// Execute Command Without LD_PRELOAD +static void exec_without_preload(std::string str) { + std::string preload = getenv("LD_PRELOAD"); + unsetenv("LD_PRELOAD"); + system(str.c_str()); + setenv("LD_PRELOAD", preload.c_str(), 1); +} + +// Get Player Data NBT Path +static std::string get_player_data_path(std::string username) { + std::string path = std::string(getenv("HOME")) + "/.minecraft/games/com.mojang/minecraftWorlds/" + server_internal_get_world_name() + "/playerdata"; + exec_without_preload("mkdir -p " + path); + return path + '/' + username + ".dat"; +} + +// Destruct Tag +static void destruct_tag(unsigned char *tag) { + unsigned char *tag_vtable = *(unsigned char **) tag; + Tag_deleteChildren_t Tag_deleteChildren = *(Tag_deleteChildren_t *) (tag_vtable + 0x8); + (*Tag_deleteChildren)(tag); + Tag_destructor_t Tag_destructor = *(Tag_destructor_t *) (tag_vtable + 0x4); + (*Tag_destructor)(tag); +} + +// Save Player Callback +static void save_player_callback(std::string username, unsigned char *player) { + std::string nbt_file = get_player_data_path(username); + + // Open File + FILE *file = fopen(nbt_file.c_str(), "wb"); + + // Write Storage Version + uint32_t storage_version = 3; + fwrite(&storage_version, 4, 1, file); + + // Create Tag + unsigned char *tag = (unsigned char *) ::operator new(0x38); + (*CompoundTag)(tag); + + // Allocate + RakNet_BitStream stream; + RakDataOutput output; + + // Set VTable + *(unsigned char **) &output.data[0] = RakDataOutput_vtable; + + // Construct BitStream + (*RakNet_BitStream_constructor)(&stream); + + // Set BitStream + *(RakNet_BitStream **) &output.data[4] = &stream; + + // Save NBT + unsigned char *player_vtable = *(unsigned char **) player; + Entity_saveWithoutId_t Entity_saveWithoutId = *(Entity_saveWithoutId_t *) (player_vtable + 0xcc); + (*Entity_saveWithoutId)(player, tag); + + // Write NBT + (*Tag_writeNamedTag)(tag, &output); + + // Destruct Tag + destruct_tag(tag); + + // Write To File + uint32_t numberOfBitsUsed = *(uint32_t *) &stream.data[0]; + uint32_t size = (numberOfBitsUsed + 7) >> 3; + fwrite(&size, 4, 1, file); + unsigned char *data = *(unsigned char **) &stream.data[12]; + fwrite(data, 1, size, file); + + // Destruct BitStream + (*RakNet_BitStream_destructor)(&stream); + + // Close File + fclose(file); +} + +struct RakDataInput { + unsigned char data[8]; +}; +static unsigned char *RakDataInput_vtable = (unsigned char *) 0x1095c8; + +typedef long int (*getRemainingFileSize_t)(FILE *file); +static getRemainingFileSize_t getRemainingFileSize = (getRemainingFileSize_t) 0xba520; + +typedef RakNet_BitStream *(*RakNet_BitStream_constructor_with_data_t)(RakNet_BitStream *stream, unsigned char *data, uint32_t size, bool copyData); +static RakNet_BitStream_constructor_with_data_t RakNet_BitStream_constructor_with_data = (RakNet_BitStream_constructor_with_data_t) 0xd3c30; + +typedef unsigned char *(*NbtIo_read_t)(RakDataInput *input); +static NbtIo_read_t NbtIo_read = (NbtIo_read_t) 0xb98cc; + +typedef void (*Entity_load_t)(unsigned char *entity, unsigned char *tag); + +// Load Player Callback +static void load_player_callback(std::string username, unsigned char *player) { + std::string nbt_file = get_player_data_path(username); + + // Open File + FILE *file = fopen(nbt_file.c_str(), "rb"); + + if (file) { + uint32_t storage_version; + size_t data_read = fread(&storage_version, 4, 1, file); + + // Check File + if (data_read == 1 && storage_version == 3) { + // Read Expected File Size + uint32_t expected_size; + data_read = fread(&expected_size, 4, 1, file); + + // Get Actual Size + long int remaining = (*getRemainingFileSize)(file); + + if (data_read == 1 && expected_size > 0 && ((uint32_t) remaining) == expected_size) { + // Read File + unsigned char *data = (unsigned char *) malloc(expected_size); + data_read = fread(data, 1, expected_size, file); + + if (data_read == expected_size) { + // Allocate + RakNet_BitStream stream; + RakDataInput input; + + // Set VTable + *(unsigned char **) &input.data[0] = RakDataInput_vtable; + + // Construct BitStream + (*RakNet_BitStream_constructor_with_data)(&stream, data, expected_size, false); + + // Set BitStream + *(RakNet_BitStream **) &input.data[4] = &stream; + + // Create Tag + unsigned char *tag = (*NbtIo_read)(&input); + + if (tag != NULL) { + // Load Data + unsigned char *player_vtable = *(unsigned char **) player; + Entity_load_t Entity_load = *(Entity_load_t *) (player_vtable + 0xd0); + (*Entity_load)(player, tag); + + // Destruct tag + destruct_tag(tag); + } + + // Destruct BitStream + (*RakNet_BitStream_destructor)(&stream); + } + + // Free Data + free(data); + } + } + + // Close File + fclose(file); + } +} + +typedef void (*Entity_moveTo_t)(unsigned char *entity, float param_1, float param_2, float param_3, float param_4, float param_5); +static Entity_moveTo_t Entity_moveTo = (Entity_moveTo_t) 0x7a834; + +static uint32_t get_entity_id(unsigned char *entity) { + return *(uint32_t *) (entity + 0x1c); +} +static void ServerPlayer_moveTo_injection(unsigned char *player, float param_1, float param_2, float param_3, float param_4, float param_5) { + // Call Original Method + (*Entity_moveTo)(player, param_1, param_2, param_3, param_4, param_5); + + // Check If Player Is Spawned + unsigned char *minecraft = *(unsigned char **) (player + 0xc8c); + unsigned char *level = server_internal_get_level(minecraft); + std::vector players = server_internal_get_players(level); + bool spawned = false; + uint32_t player_id = get_entity_id(player); + for (std::size_t i = 0; i < players.size(); i++) { + if (player_id == get_entity_id(players[i])) { + spawned = true; + break; + } + } + + // Load Data + if (!spawned) { + load_player_callback(server_internal_get_player_username(player), player); + } +} + +void playerdata_save(unsigned char *level) { + // Save Players + std::vector players = server_internal_get_players(level); + for (std::size_t i = 0; i < players.size(); i++) { + // Iterate Players + unsigned char *player = players[i]; + std::string username = server_internal_get_player_username(player); + save_player_callback(username, player); + } +} + +typedef void (*ServerSideNetworkHandler_onDisconnect_t)(unsigned char *server_side_network_handler, unsigned char *guid); +static ServerSideNetworkHandler_onDisconnect_t ServerSideNetworkHandler_onDisconnect = (ServerSideNetworkHandler_onDisconnect_t) 0x75164; + +typedef unsigned char *(*ServerSideNetworkHandler_getPlayer_t)(unsigned char *server_side_network_handler, unsigned char *guid); +static ServerSideNetworkHandler_getPlayer_t ServerSideNetworkHandler_getPlayer = (ServerSideNetworkHandler_getPlayer_t) 0x75464; + +static void ServerSideNetworkHandler_onDisconnect_injection(unsigned char *server_side_network_handler, unsigned char *guid) { + // Save Player Data + unsigned char *player = (*ServerSideNetworkHandler_getPlayer)(server_side_network_handler, guid); + if (player != NULL) { + std::string username = server_internal_get_player_username(player); + save_player_callback(username, player); + } + + // Call Original Method + (*ServerSideNetworkHandler_onDisconnect)(server_side_network_handler, guid); +} + +void playerdata_init() { + // Load Player NBT + patch_address((void *) 0x109e54, (void *) ServerPlayer_moveTo_injection); + // Save On Logout + patch_address((void *) 0x109bb0, (void *) ServerSideNetworkHandler_onDisconnect_injection); +} \ No newline at end of file diff --git a/mods/src/server/playerdata.h b/mods/src/server/playerdata.h new file mode 100644 index 000000000..06e7a1171 --- /dev/null +++ b/mods/src/server/playerdata.h @@ -0,0 +1,8 @@ +#ifndef PLAYERDATA_H + +#define PLAYERDATA_H + +void playerdata_save(unsigned char *level); +void playerdata_init(); + +#endif \ No newline at end of file diff --git a/mods/src/server/server.cpp b/mods/src/server/server.cpp index b85093205..1defab76c 100644 --- a/mods/src/server/server.cpp +++ b/mods/src/server/server.cpp @@ -15,7 +15,9 @@ #include #include "server.h" +#include "server_internal.h" #include "server_properties.h" +#include "playerdata.h" // Server Properties static ServerProperties &get_server_properties() { @@ -79,6 +81,11 @@ static ProgressScreen_t ProgressScreen = (ProgressScreen_t) 0x37044; typedef void (*Minecraft_setScreen_t)(unsigned char *minecraft, unsigned char *screen); static Minecraft_setScreen_t Minecraft_setScreen = (Minecraft_setScreen_t) 0x15d6c; +// Get World Name +std::string server_internal_get_world_name() { + return get_server_properties().get_string("world-name", DEFAULT_WORLD_NAME); +} + // Create/Start World static void start_world(unsigned char *minecraft) { INFO("%s", "Starting Minecraft: Pi Edition Dedicated Server"); @@ -89,7 +96,7 @@ static void start_world(unsigned char *minecraft) { int32_t seed = seed_str.length() > 0 ? std::stoi(seed_str) : time(NULL); settings.seed = seed; - std::string world_name = get_server_properties().get_string("world-name", DEFAULT_WORLD_NAME); + std::string world_name = server_internal_get_world_name(); (*Minecraft_selectLevel)(minecraft, world_name, world_name, settings); int port = get_server_properties().get_int("port", DEFAULT_PORT); @@ -175,15 +182,28 @@ static std::string get_banned_ips_file() { typedef void (*player_callback_t)(unsigned char *minecraft, std::string username, unsigned char *player); +// Get Vector Of Players In Level +std::vector server_internal_get_players(unsigned char *level) { + return *(std::vector *) (level + 0x60); +} +// Get Player's Username +std::string server_internal_get_player_username(unsigned char *player) { + return *(char **) (player + 0xbf4); +} +// Get Level From Minecraft +unsigned char *server_internal_get_level(unsigned char *minecraft) { + return *(unsigned char **) (minecraft + 0x188); +} + // Find Players With Username And Run Callback static void find_players(unsigned char *minecraft, std::string target_username, player_callback_t callback, bool all_players) { - unsigned char *level = *(unsigned char **) (minecraft + 0x188); - std::vector players = *(std::vector *) (level + 0x60); + unsigned char *level = server_internal_get_level(minecraft); + std::vector players = server_internal_get_players(level); bool found_player = false; for (std::size_t i = 0; i < players.size(); i++) { // Iterate Players unsigned char *player = players[i]; - std::string username = *(std::string *) (player + 0xbf4); + std::string username = server_internal_get_player_username(player); if (all_players || username == target_username) { // Run Callback (*callback)(minecraft, username, player); @@ -263,6 +283,9 @@ static void Level_saveLevelData_injection(unsigned char *level) { revert_overwrite((void *) Level_saveLevelData, Level_saveLevelData_original); (*Level_saveLevelData)(level); revert_overwrite((void *) Level_saveLevelData, Level_saveLevelData_original); + + // Save Player Data + playerdata_save(level); } typedef void (*Minecraft_leaveGame_t)(unsigned char *minecraft, bool save_remote_level); @@ -277,7 +300,7 @@ static void handle_server_stop(unsigned char *minecraft) { if (exit_requested) { INFO("%s", "Stopping Server"); // Save And Exit - unsigned char *level = *(unsigned char **) (minecraft + 0x188); + unsigned char *level = server_internal_get_level(minecraft); (*Level_saveLevelData)(level); (*Minecraft_leaveGame)(minecraft, false); // Stop Game @@ -518,6 +541,9 @@ void server_init() { // Custom Banned IP List RakNet_RakPeer_IsBanned_original = overwrite((void *) RakNet_RakPeer_IsBanned, (void *) RakNet_RakPeer_IsBanned_injection); + // Load Player Data + playerdata_init(); + if (get_server_properties().get_bool("show-minecon-icon", DEFAULT_SHOW_MINECON_ICON)) { // Show The MineCon Icon Next To MOTD In Server List unsigned char minecon_icon_patch[4] = {0x04, 0x1a, 0x9f, 0xe5}; diff --git a/mods/src/server/server_internal.h b/mods/src/server/server_internal.h new file mode 100644 index 000000000..1210502f3 --- /dev/null +++ b/mods/src/server/server_internal.h @@ -0,0 +1,13 @@ +#ifndef SERVER_INTERNAL_H + +#define SERVER_INTERNAL_H + +#include +#include + +std::string server_internal_get_world_name(); +unsigned char *server_internal_get_level(unsigned char *minecraft); +std::vector server_internal_get_players(unsigned char *level); +std::string server_internal_get_player_username(unsigned char *player); + +#endif \ No newline at end of file