diff --git a/Minecraft.Client/ArchiveFile.cpp b/Minecraft.Client/ArchiveFile.cpp index bf41ec97..defef24e 100644 --- a/Minecraft.Client/ArchiveFile.cpp +++ b/Minecraft.Client/ArchiveFile.cpp @@ -104,7 +104,7 @@ byteArray ArchiveFile::getFile(const wstring &filename) app.DebugPrintf("Couldn't find file in archive\n"); app.DebugPrintf("Failed to find file '%ls' in archive\n", filename.c_str()); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif app.FatalLoadError(); } diff --git a/Minecraft.Client/Chunk.cpp b/Minecraft.Client/Chunk.cpp index bd12adbf..d64e6243 100644 --- a/Minecraft.Client/Chunk.cpp +++ b/Minecraft.Client/Chunk.cpp @@ -1036,7 +1036,7 @@ bool Chunk::isEmpty() void Chunk::setDirty() { // 4J - not used, but if this starts being used again then we'll need to investigate how best to handle it. - __debugbreak(); + DEBUG_BREAK(); levelRenderer->setGlobalChunkFlag(x, y, z, level, LevelRenderer::CHUNK_FLAG_DIRTY); } diff --git a/Minecraft.Client/Common/Audio/SoundEngine.cpp b/Minecraft.Client/Common/Audio/SoundEngine.cpp index 0bc4c831..2828713d 100644 --- a/Minecraft.Client/Common/Audio/SoundEngine.cpp +++ b/Minecraft.Client/Common/Audio/SoundEngine.cpp @@ -698,23 +698,26 @@ void SoundEngine::playUI(int iSound, float volume, float pitch) { U8 szSoundName[256]; wstring name; + const char* soundDir; if (iSound >= eSFX_MAX) { strcpy((char*)szSoundName, "Minecraft/"); name = wchSoundNames[iSound]; + soundDir = "Minecraft"; } else { strcpy((char*)szSoundName, "Minecraft/UI/"); name = wchUISoundNames[iSound]; + soundDir = "Minecraft/UI"; } char* SoundName = (char*)ConvertSoundPathToName(name); strcat((char*)szSoundName, SoundName); char basePath[256]; - sprintf_s(basePath, "Windows64Media/Sound/Minecraft/UI/%s", ConvertSoundPathToName(name)); + sprintf_s(basePath, "Windows64Media/Sound/%s/%s", soundDir, ConvertSoundPathToName(name)); char finalPath[256]; sprintf_s(finalPath, "%s.wav", basePath); diff --git a/Minecraft.Client/Common/Consoles_App.cpp b/Minecraft.Client/Common/Consoles_App.cpp index 043f40d0..b92a4782 100644 --- a/Minecraft.Client/Common/Consoles_App.cpp +++ b/Minecraft.Client/Common/Consoles_App.cpp @@ -96,7 +96,7 @@ CMinecraftApp::CMinecraftApp() // 4J Stu - See comment for GAME_SETTINGS_PROFILE_DATA_BYTES in Xbox_App.h DebugPrintf("WARNING: The size of the profile GAME_SETTINGS struct has changed, so all stat data is likely incorrect. Is: %d, Should be: %d\n",sizeof(GAME_SETTINGS),GAME_SETTINGS_PROFILE_DATA_BYTES); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif } @@ -1415,9 +1415,6 @@ int CMinecraftApp::OldProfileVersionCallback(LPVOID pParam,unsigned char *pucDat { // This might be from a version during testing of new profile updates app.DebugPrintf("Don't know what to do with this profile version!\n"); -#ifndef _CONTENT_PACKAGE - // __debugbreak(); -#endif GAME_SETTINGS *pGameSettings=(GAME_SETTINGS *)pucData; pGameSettings->ucMenuSensitivity=100; //eGameSetting_Sensitivity_InMenu @@ -6647,7 +6644,7 @@ void CMinecraftApp::InitialiseTips() { // the m_TriviaTipA or the m_GameTipA are out of sync #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif } } @@ -6900,6 +6897,8 @@ wstring CMinecraftApp::FormatChatMessage(const wstring& desc, bool applyStyling) results = replaceAll(results, L"§f", replacements); results = replaceAll(results, L"§r", replacements); //we only support color so reset is the same as white color + results = replaceAll(results, L"'", L"\u2019"); + if (applyStyling) { std::wsmatch match; while (std::regex_search(results, match, IDS_Pattern)) { diff --git a/Minecraft.Client/Common/DLC/DLCManager.cpp b/Minecraft.Client/Common/DLC/DLCManager.cpp index 90614a97..8f35b1d1 100644 --- a/Minecraft.Client/Common/DLC/DLCManager.cpp +++ b/Minecraft.Client/Common/DLC/DLCManager.cpp @@ -171,7 +171,7 @@ DLCPack *DLCManager::getPack(DWORD index, EDLCType type /*= e_DLCType_All*/) if(index >= m_packs.size()) { app.DebugPrintf("DLCManager: Trying to access a DLC pack beyond the range of valid packs\n"); - __debugbreak(); + DEBUG_BREAK(); } pack = m_packs[index]; } @@ -186,7 +186,6 @@ DWORD DLCManager::getPackIndex(DLCPack *pack, bool &found, EDLCType type /*= e_D if(pack == nullptr) { app.DebugPrintf("DLCManager: Attempting to find the index for a nullptr pack\n"); - //__debugbreak(); return foundIndex; } if( type != e_DLCType_All ) diff --git a/Minecraft.Client/Common/DLC/DLCPack.cpp b/Minecraft.Client/Common/DLC/DLCPack.cpp index 9247c6b8..5f3874d0 100644 --- a/Minecraft.Client/Common/DLC/DLCPack.cpp +++ b/Minecraft.Client/Common/DLC/DLCPack.cpp @@ -107,7 +107,7 @@ void DLCPack::addChildPack(DLCPack *childPack) #ifndef _CONTENT_PACKAGE if(packId < 0 || packId > 15) { - __debugbreak(); + DEBUG_BREAK(); } #endif childPack->SetPackId( (packId<<24) | m_packId ); @@ -362,7 +362,7 @@ DWORD DLCPack::getFileIndexAt(DLCManager::EDLCType type, const wstring &path, bo { app.DebugPrintf("Unimplemented\n"); #ifndef __CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif return 0; } @@ -392,9 +392,6 @@ bool DLCPack::hasPurchasedFile(DLCManager::EDLCType type, const wstring &path) /*if(type == DLCManager::e_DLCType_All) { app.DebugPrintf("Unimplemented\n"); -#ifndef _CONTENT_PACKAGE - __debugbreak(); -#endif return false; } #ifndef _CONTENT_PACKAGE diff --git a/Minecraft.Client/Common/GameRules/ConsoleSchematicFile.cpp b/Minecraft.Client/Common/GameRules/ConsoleSchematicFile.cpp index 4990dd4a..25ca419c 100644 --- a/Minecraft.Client/Common/GameRules/ConsoleSchematicFile.cpp +++ b/Minecraft.Client/Common/GameRules/ConsoleSchematicFile.cpp @@ -122,7 +122,7 @@ void ConsoleSchematicFile::load(DataInputStream *dis) { #ifndef _CONTENT_PACKAGE app.DebugPrintf("ConsoleSchematicFile has read a nullptr tile entity\n"); - __debugbreak(); + DEBUG_BREAK(); #endif } else @@ -635,7 +635,7 @@ void ConsoleSchematicFile::generateSchematicFile(DataOutputStream *dos, Level *l } #ifndef _CONTENT_PACKAGE - if(p!=blockCount) __debugbreak(); + if(p!=blockCount) DEBUG_BREAK(); #endif // We don't know how this will compress - just make a fixed length buffer to initially decompress into diff --git a/Minecraft.Client/Common/GameRules/GameRule.cpp b/Minecraft.Client/Common/GameRules/GameRule.cpp index b37df84d..cae35f16 100644 --- a/Minecraft.Client/Common/GameRules/GameRule.cpp +++ b/Minecraft.Client/Common/GameRules/GameRule.cpp @@ -24,7 +24,7 @@ GameRule::ValueType GameRule::getParameter(const wstring ¶meterName) { #ifndef _CONTENT_PACKAGE wprintf(L"WARNING: Parameter %ls was not set before being fetched\n", parameterName.c_str()); - __debugbreak(); + DEBUG_BREAK(); #endif } return m_parameters[parameterName]; diff --git a/Minecraft.Client/Common/Leaderboards/SonyLeaderboardManager.cpp b/Minecraft.Client/Common/Leaderboards/SonyLeaderboardManager.cpp index f4e00ab3..377d85e0 100644 --- a/Minecraft.Client/Common/Leaderboards/SonyLeaderboardManager.cpp +++ b/Minecraft.Client/Common/Leaderboards/SonyLeaderboardManager.cpp @@ -238,7 +238,7 @@ HRESULT SonyLeaderboardManager::fillByIdsQuery(const SceNpId &myNpId, SceNpId* & { // 4J-JEV: Something terrible must have happend, // 'getFriendslist' was supposed to be a synchronous operation. - __debugbreak(); + DEBUG_BREAK(); // 4J-JEV: We can at least fall-back to just the players score. len = 1; diff --git a/Minecraft.Client/Common/Network/Sony/PlatformNetworkManagerSony.cpp b/Minecraft.Client/Common/Network/Sony/PlatformNetworkManagerSony.cpp index cf009aa0..aa27f544 100644 --- a/Minecraft.Client/Common/Network/Sony/PlatformNetworkManagerSony.cpp +++ b/Minecraft.Client/Common/Network/Sony/PlatformNetworkManagerSony.cpp @@ -247,8 +247,6 @@ void CPlatformNetworkManagerSony::HandlePlayerJoined(SQRNetworkPlayer * void CPlatformNetworkManagerSony::HandlePlayerLeaving(SQRNetworkPlayer *pSQRPlayer) { - //__debugbreak(); - app.DebugPrintf( "Player 0x%p leaving.\n", pSQRPlayer ); diff --git a/Minecraft.Client/Common/UI/UIScene.cpp b/Minecraft.Client/Common/UI/UIScene.cpp index 12b059c8..e696b1ab 100644 --- a/Minecraft.Client/Common/UI/UIScene.cpp +++ b/Minecraft.Client/Common/UI/UIScene.cpp @@ -328,7 +328,7 @@ void UIScene::loadMovie() { app.DebugPrintf("ERROR: Could not find any iggy movie for %ls!\n", moviePath.c_str()); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif app.FatalLoadError(); } @@ -344,7 +344,7 @@ void UIScene::loadMovie() { app.DebugPrintf("ERROR: Failed to load iggy scene!\n"); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif app.FatalLoadError(); } @@ -969,9 +969,6 @@ void UIScene::_customDrawSlotControl(CustomDrawData *region, int iPad, shared_pt // if(m_parentLayer == nullptr) // { // app.DebugPrintf("A scene is trying to navigate forwards, but it's parent layer is nullptr!\n"); -//#ifndef _CONTENT_PACKAGE -// __debugbreak(); -//#endif // } // else // { @@ -988,10 +985,6 @@ void UIScene::navigateBack() if(m_parentLayer == nullptr) { -// app.DebugPrintf("A scene is trying to navigate back, but it's parent layer is nullptr!\n"); -#ifndef _CONTENT_PACKAGE -// __debugbreak(); -#endif } else { @@ -1222,7 +1215,7 @@ void UIScene::externalCallback(IggyExternalFunctionCallUTF16 * call) { app.DebugPrintf("Callback for handlePress did not have the correct number of arguments\n"); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif return; } @@ -1230,7 +1223,7 @@ void UIScene::externalCallback(IggyExternalFunctionCallUTF16 * call) { app.DebugPrintf("Arguments for handlePress were not of the correct type\n"); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif return; } @@ -1242,7 +1235,7 @@ void UIScene::externalCallback(IggyExternalFunctionCallUTF16 * call) { app.DebugPrintf("Callback for handleFocusChange did not have the correct number of arguments\n"); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif return; } @@ -1250,7 +1243,7 @@ void UIScene::externalCallback(IggyExternalFunctionCallUTF16 * call) { app.DebugPrintf("Arguments for handleFocusChange were not of the correct type\n"); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif return; } @@ -1262,7 +1255,7 @@ void UIScene::externalCallback(IggyExternalFunctionCallUTF16 * call) { app.DebugPrintf("Callback for handleInitFocus did not have the correct number of arguments\n"); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif return; } @@ -1270,7 +1263,7 @@ void UIScene::externalCallback(IggyExternalFunctionCallUTF16 * call) { app.DebugPrintf("Arguments for handleInitFocus were not of the correct type\n"); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif return; } @@ -1282,7 +1275,7 @@ void UIScene::externalCallback(IggyExternalFunctionCallUTF16 * call) { app.DebugPrintf("Callback for handleCheckboxToggled did not have the correct number of arguments\n"); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif return; } @@ -1290,7 +1283,7 @@ void UIScene::externalCallback(IggyExternalFunctionCallUTF16 * call) { app.DebugPrintf("Arguments for handleCheckboxToggled were not of the correct type\n"); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif return; } @@ -1302,7 +1295,7 @@ void UIScene::externalCallback(IggyExternalFunctionCallUTF16 * call) { app.DebugPrintf("Callback for handleSliderMove did not have the correct number of arguments\n"); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif return; } @@ -1310,7 +1303,7 @@ void UIScene::externalCallback(IggyExternalFunctionCallUTF16 * call) { app.DebugPrintf("Arguments for handleSliderMove were not of the correct type\n"); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif return; } @@ -1322,7 +1315,7 @@ void UIScene::externalCallback(IggyExternalFunctionCallUTF16 * call) { app.DebugPrintf("Callback for handleAnimationEnd did not have the correct number of arguments\n"); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif return; } @@ -1334,7 +1327,7 @@ void UIScene::externalCallback(IggyExternalFunctionCallUTF16 * call) { app.DebugPrintf("Callback for handleSelectionChanged did not have the correct number of arguments\n"); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif return; } @@ -1342,7 +1335,7 @@ void UIScene::externalCallback(IggyExternalFunctionCallUTF16 * call) { app.DebugPrintf("Arguments for handleSelectionChanged were not of the correct type\n"); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif return; } @@ -1360,7 +1353,7 @@ void UIScene::externalCallback(IggyExternalFunctionCallUTF16 * call) { app.DebugPrintf("Callback for handleRequestMoreData did not have the correct number of arguments\n"); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif return; } @@ -1368,7 +1361,7 @@ void UIScene::externalCallback(IggyExternalFunctionCallUTF16 * call) { app.DebugPrintf("Arguments for handleRequestMoreData were not of the correct type\n"); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif return; } diff --git a/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp b/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp index d159a164..c62dcf46 100644 --- a/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_MainMenu.cpp @@ -409,7 +409,7 @@ void UIScene_MainMenu::handlePress(F64 controlId, F64 childId) break; #endif - default: __debugbreak(); + default: DEBUG_BREAK(); } bool confirmUser = false; diff --git a/Minecraft.Client/DLCTexturePack.cpp b/Minecraft.Client/DLCTexturePack.cpp index ab1b5221..3cb68521 100644 --- a/Minecraft.Client/DLCTexturePack.cpp +++ b/Minecraft.Client/DLCTexturePack.cpp @@ -127,7 +127,7 @@ wstring DLCTexturePack::getResource(const wstring& name) { // 4J Stu - We should never call this function #ifndef __CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif return L""; } @@ -136,7 +136,7 @@ InputStream *DLCTexturePack::getResourceImplementation(const wstring &name) //th { // 4J Stu - We should never call this function #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); if(hasFile(name)) return nullptr; #endif return nullptr; //resource; diff --git a/Minecraft.Client/Durango/Durango_App.cpp b/Minecraft.Client/Durango/Durango_App.cpp index 1c2ffbe2..fb3f20ec 100644 --- a/Minecraft.Client/Durango/Durango_App.cpp +++ b/Minecraft.Client/Durango/Durango_App.cpp @@ -641,7 +641,7 @@ int CConsoleMinecraftApp::Callback_TMSPPReadDLCFile(void *pParam,int iPad, int i { DWORD error = GetLastError(); app.DebugPrintf("Failed to open DLCXbox1.cmp with error code %d (%x)\n", error, error); - __debugbreak(); + DEBUG_BREAK(); return 0; } diff --git a/Minecraft.Client/Durango/Durango_UIController.cpp b/Minecraft.Client/Durango/Durango_UIController.cpp index e27d91d2..63bc75ad 100644 --- a/Minecraft.Client/Durango/Durango_UIController.cpp +++ b/Minecraft.Client/Durango/Durango_UIController.cpp @@ -20,7 +20,7 @@ void ConsoleUIController::init(Microsoft::WRL::ComPtr dev, Microso { app.DebugPrintf("Failed to initialise GDraw!\n"); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif app.FatalLoadError(); } diff --git a/Minecraft.Client/Durango/Iggy/include/rrCore.h b/Minecraft.Client/Durango/Iggy/include/rrCore.h index 17ebee3a..d3f4f41e 100644 --- a/Minecraft.Client/Durango/Iggy/include/rrCore.h +++ b/Minecraft.Client/Durango/Iggy/include/rrCore.h @@ -1619,7 +1619,7 @@ RADDEFSTART #define RR_BREAK() __builtin_trap() #define RR_CACHE_LINE_SIZE 32 #elif defined(__RADXENON__) - #define RR_BREAK() __debugbreak() + #define RR_BREAK() DEBUG_BREAK() #define RR_CACHE_LINE_SIZE 128 #elif defined(__RADANDROID__) #define RR_BREAK() __builtin_trap() diff --git a/Minecraft.Client/Durango/Miles/include/rrCore.h b/Minecraft.Client/Durango/Miles/include/rrCore.h index 17ebee3a..d3f4f41e 100644 --- a/Minecraft.Client/Durango/Miles/include/rrCore.h +++ b/Minecraft.Client/Durango/Miles/include/rrCore.h @@ -1619,7 +1619,7 @@ RADDEFSTART #define RR_BREAK() __builtin_trap() #define RR_CACHE_LINE_SIZE 32 #elif defined(__RADXENON__) - #define RR_BREAK() __debugbreak() + #define RR_BREAK() DEBUG_BREAK() #define RR_CACHE_LINE_SIZE 128 #elif defined(__RADANDROID__) #define RR_BREAK() __builtin_trap() diff --git a/Minecraft.Client/Durango/Network/PlatformNetworkManagerDurango.cpp b/Minecraft.Client/Durango/Network/PlatformNetworkManagerDurango.cpp index 9e8d1fc6..8b663ea3 100644 --- a/Minecraft.Client/Durango/Network/PlatformNetworkManagerDurango.cpp +++ b/Minecraft.Client/Durango/Network/PlatformNetworkManagerDurango.cpp @@ -187,8 +187,6 @@ void CPlatformNetworkManagerDurango::HandlePlayerJoined(DQRNetworkPlayer *pDQRPl void CPlatformNetworkManagerDurango::HandlePlayerLeaving(DQRNetworkPlayer *pDQRPlayer) { - //__debugbreak(); - app.DebugPrintf( "Player 0x%p leaving.\n", pDQRPlayer ); diff --git a/Minecraft.Client/EntityRenderDispatcher.cpp b/Minecraft.Client/EntityRenderDispatcher.cpp index 7ce3b35a..95a806f0 100644 --- a/Minecraft.Client/EntityRenderDispatcher.cpp +++ b/Minecraft.Client/EntityRenderDispatcher.cpp @@ -204,7 +204,7 @@ EntityRenderer *EntityRenderDispatcher::getRenderer(eINSTANCEOF e) { app.DebugPrintf("Couldn't find renderer for entity of type %d\n", e); // New renderer mapping required in above table - __debugbreak(); + DEBUG_BREAK(); } /* 4J - not doing this hierarchical search anymore. We need to explicitly add renderers for any eINSTANCEOF type that we want to be able to render if (it == renderers.end() && e != Entity::_class) diff --git a/Minecraft.Client/EntityTracker.cpp b/Minecraft.Client/EntityTracker.cpp index 469e3966..c20bf0ba 100644 --- a/Minecraft.Client/EntityTracker.cpp +++ b/Minecraft.Client/EntityTracker.cpp @@ -88,7 +88,7 @@ void EntityTracker::addEntity(shared_ptr e, int range, int updateInterva } if( e->entityId >= 16384 ) { - __debugbreak(); + DEBUG_BREAK(); } shared_ptr te = std::make_shared(e, range, updateInterval, trackDeltas); entities.insert(te); diff --git a/Minecraft.Client/GameRenderer.cpp b/Minecraft.Client/GameRenderer.cpp index 484b2323..d57d2c75 100644 --- a/Minecraft.Client/GameRenderer.cpp +++ b/Minecraft.Client/GameRenderer.cpp @@ -2219,7 +2219,7 @@ void GameRenderer::setupFog(int i, float alpha) if (i == 999) { - __debugbreak(); + DEBUG_BREAK(); // 4J TODO /* glFog(GL_FOG_COLOR, getBuffer(0, 0, 0, 1)); diff --git a/Minecraft.Client/Gui.cpp b/Minecraft.Client/Gui.cpp index 3c1ff3c5..8c30b1ff 100644 --- a/Minecraft.Client/Gui.cpp +++ b/Minecraft.Client/Gui.cpp @@ -697,8 +697,8 @@ void Gui::render(float a, bool mouseFree, int xMouse, int yMouse) glEnable(GL_COLOR_MATERIAL); // 4J - TomK now using safe zone values directly instead of the magic number calculation that lived here before (which only worked for medium scale, the other two were off!) - int xo = iSafezoneXHalf + 10; - int yo = iSafezoneTopYHalf + 10; + int xo = iSafezoneXHalf + 10; // TODO: fix relative scaling for atrocious aspect ratios + int yo = iSafezoneTopYHalf + 10; // TODO: fix relative scaling for atrocious aspect ratios #ifdef __PSVITA__ // align directly with corners, there are no safe zones on vita @@ -708,8 +708,29 @@ void Gui::render(float a, bool mouseFree, int xMouse, int yMouse) glPushMatrix(); glTranslatef(static_cast(xo), static_cast(yo), 50); - float ss = 12; - glScalef(-ss, ss, ss); + + // correct paper doll aspect ratio + float ss = 12.0f; + float aspectScaleX = 1.0f; + float aspectScaleY = 1.0f; + + extern int g_rScreenWidth; + extern int g_rScreenHeight; + + if (g_rScreenWidth > 0 && g_rScreenHeight > 0) { + float screenAspect = (float)g_rScreenWidth / (float)g_rScreenHeight; + const float targetAspect = 16.0f / 9.0f; + + // apply correction if window is not already at a 16:9 aspect ratio + if (fabs(screenAspect - targetAspect) > 0.01f) { + if (screenAspect > targetAspect) + aspectScaleX = targetAspect / screenAspect; + else + aspectScaleY = screenAspect / targetAspect; + } + } + + glScalef(-ss * aspectScaleX, ss * aspectScaleY, ss); glRotatef(180, 0, 0, 1); float oyr = minecraft->player->yRot; diff --git a/Minecraft.Client/LocalPlayer.cpp b/Minecraft.Client/LocalPlayer.cpp index bd58d398..91ed1ec2 100644 --- a/Minecraft.Client/LocalPlayer.cpp +++ b/Minecraft.Client/LocalPlayer.cpp @@ -573,7 +573,7 @@ void LocalPlayer::changeDimension(int i) //minecraft.setScreen(new WinScreen()); #ifndef _CONTENT_PACKAGE app.DebugPrintf("LocalPlayer::changeDimension from 1 to 1 but WinScreen has not been implemented.\n"); - __debugbreak(); + DEBUG_BREAK(); #endif } else diff --git a/Minecraft.Client/Minecraft.cpp b/Minecraft.Client/Minecraft.cpp index df0f7b7e..eeedcd2e 100644 --- a/Minecraft.Client/Minecraft.cpp +++ b/Minecraft.Client/Minecraft.cpp @@ -4991,7 +4991,7 @@ void Minecraft::main() app.DebugPrintf("%ls\n", i, app.GetString( Tile::tiles[i]->getDescriptionId() )); } } - __debugbreak(); + DEBUG_BREAK(); #endif // 4J-PB - Can't call this for the first 5 seconds of a game - MS rule diff --git a/Minecraft.Client/MinecraftServer.cpp b/Minecraft.Client/MinecraftServer.cpp index 747d7ca6..3e15b344 100644 --- a/Minecraft.Client/MinecraftServer.cpp +++ b/Minecraft.Client/MinecraftServer.cpp @@ -85,6 +85,9 @@ vector MinecraftServer::s_sentTo; int MinecraftServer::s_slowQueuePlayerIndex = 0; int MinecraftServer::s_slowQueueLastTime = 0; bool MinecraftServer::s_slowQueuePacketSent = false; +#ifdef MINECRAFT_SERVER_BUILD +int MinecraftServer::s_dedicatedChunkSendsThisTick = 0; +#endif #endif unordered_map MinecraftServer::ironTimers; @@ -1772,6 +1775,12 @@ void MinecraftServer::run(int64_t seed, void *lpParameter) int64_t unprocessedTime = 0; while (running && !s_bServerHalted) { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + // Full wall-clock cost of one run loop iteration (catch-up ticks + // + setTime handlers + XUI delayed actions + Sleep). + int64_t outerIterStart = getCurrentTimeMillis(); + int64_t outerIterTickWork = 0; +#endif int64_t now = getCurrentTimeMillis(); // 4J Stu - When we pause the server, we don't want to count that as time passed @@ -1809,14 +1818,39 @@ void MinecraftServer::run(int64_t seed, void *lpParameter) while (unprocessedTime > MS_PER_TICK) { unprocessedTime -= MS_PER_TICK; +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + // Per-iteration pre/tick/post timing. + int64_t iter_t0 = System::currentTimeMillis(); +#endif chunkPacketManagement_PreTick(); - // int64_t before = System::currentTimeMillis(); +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + int64_t iter_t1 = System::currentTimeMillis(); +#endif tick(); - // int64_t after = System::currentTimeMillis(); - // PIXReportCounter(L"Server time",(float)(after-before)); - +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + int64_t iter_t2 = System::currentTimeMillis(); +#endif chunkPacketManagement_PostTick(); +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + int64_t iter_t3 = System::currentTimeMillis(); + int64_t iter_total = iter_t3 - iter_t0; + outerIterTickWork += iter_total; + if (iter_total > 60) + { + ServerRuntime::LogInfof("perf", + "iter total=%lldms pre=%lld tick=%lld post=%lld", + (long long)iter_total, + (long long)(iter_t1 - iter_t0), + (long long)(iter_t2 - iter_t1), + (long long)(iter_t3 - iter_t2)); + } +#endif } + // Do NOT reset lastTime here. Resetting discards the wall + // time spent in the catch-up so passedTime restarts from + // post-tick, capping effective TPS at 1000 / (MS_PER_TICK + // + avgTickBody). Runaway after a real freeze is bounded + // by the passedTime > MS_PER_TICK * 40 cap above. // int64_t afterall = System::currentTimeMillis(); // PIXReportCounter(L"Server time all",(float)(afterall-beforeall)); // PIXReportCounter(L"Server ticks",(float)tickcount); @@ -2102,6 +2136,73 @@ void MinecraftServer::run(int64_t seed, void *lpParameter) } Sleep(1); +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + int64_t outerIterTotal = getCurrentTimeMillis() - outerIterStart; + + // Distribution histogram (gated). Buckets every outer iter, dumps + // the bucket counts + a self-computed TPS every ~10 seconds. + if (ServerRuntime::g_serverPerfTrace) + { + static const int kBucketCount = 14; + static int64_t s_bucketEdges[kBucketCount] = { + 2, 5, 10, 20, 30, 40, 50, 60, 80, 100, 200, 500, 1000, INT64_MAX + }; + static unsigned int s_buckets[kBucketCount] = {0}; + static int64_t s_histWindowStartMs = 0; + static int s_histWindowStartTick = 0; + static int64_t s_histTotalIterMs = 0; + static unsigned int s_histTotalIters = 0; + static unsigned int s_histTickIters = 0; + int64_t nowMsForHist = getCurrentTimeMillis(); + if (s_histWindowStartMs == 0) + { + s_histWindowStartMs = nowMsForHist; + s_histWindowStartTick = (int)tickCount; + } + for (int b = 0; b < kBucketCount; b++) + { + if (outerIterTotal <= s_bucketEdges[b]) + { + s_buckets[b]++; + break; + } + } + s_histTotalIterMs += outerIterTotal; + s_histTotalIters++; + if (outerIterTickWork > 0) s_histTickIters++; + int ticksThisWindow = (int)tickCount - s_histWindowStartTick; + if (ticksThisWindow >= 200) + { + int64_t windowMs = nowMsForHist - s_histWindowStartMs; + double calcTps = windowMs > 0 ? (ticksThisWindow * 1000.0) / windowMs : 0.0; + double avgIterMs = s_histTotalIters > 0 ? (double)s_histTotalIterMs / s_histTotalIters : 0.0; + ServerRuntime::LogInfof("perf", + "histogram window: %d ticks in %lldms calcTps=%.2f iters=%u tickIters=%u avgIter=%.2fms | " + "<=2:%u <=5:%u <=10:%u <=20:%u <=30:%u <=40:%u <=50:%u <=60:%u <=80:%u <=100:%u <=200:%u <=500:%u <=1000:%u >1000:%u", + ticksThisWindow, (long long)windowMs, calcTps, + s_histTotalIters, s_histTickIters, avgIterMs, + s_buckets[0], s_buckets[1], s_buckets[2], s_buckets[3], + s_buckets[4], s_buckets[5], s_buckets[6], s_buckets[7], + s_buckets[8], s_buckets[9], s_buckets[10], s_buckets[11], + s_buckets[12], s_buckets[13]); + for (int b = 0; b < kBucketCount; b++) s_buckets[b] = 0; + s_histWindowStartMs = nowMsForHist; + s_histWindowStartTick = (int)tickCount; + s_histTotalIterMs = 0; + s_histTotalIters = 0; + s_histTickIters = 0; + } + } + + if (outerIterTotal > 60) + { + ServerRuntime::LogInfof("perf", + "outerIter total=%lldms tickWork=%lld postTickOverhead=%lld", + (long long)outerIterTotal, + (long long)outerIterTickWork, + (long long)(outerIterTotal - outerIterTickWork)); + } +#endif } } //else @@ -2156,6 +2257,17 @@ void MinecraftServer::broadcastStopSavingPacket() void MinecraftServer::tick() { + // Per-substep wall-clock timing. Logs one summary line when total tick + // exceeds TICK_SLOW_THRESHOLD_MS. + const int64_t TICK_SLOW_THRESHOLD_MS = 60; + const int kMaxLevelsRecorded = 8; + int64_t tickStart = System::currentTimeMillis(); + int64_t lvlTickMs[kMaxLevelsRecorded] = {0}; + int64_t lvlEntMs[kMaxLevelsRecorded] = {0}; + int64_t lvlTrkMs[kMaxLevelsRecorded] = {0}; + int lvlDimId[kMaxLevelsRecorded] = {0}; + unsigned int recordedLevels = 0; + vector toRemove; for ( auto& it : ironTimers ) { @@ -2219,11 +2331,8 @@ void MinecraftServer::tick() int64_t st2 = System::currentTimeMillis(); PIXEndNamedEvent(); PIXBeginNamedEvent(0,"Entity tick %d",i); - // 4J added to stop ticking entities in levels when players are not in those levels. - // Note: now changed so that we also tick if there are entities to be removed, as this also happens as a result of calling tickEntities. If we don't do this, then the - // entities get removed at the first point that there is a player count in the level - this has been causing a problem when going from normal dimension -> nether -> normal, - // as the player is getting flagged as to be removed (from the normal dimension) when going to the nether, but Actually gets removed only when it returns - if( ( players->getPlayerCount(level) > 0) || ( level->hasEntitiesToRemove() ) ) + // 4J added: do not tick entities in empty dimensions. + if ((players->getPlayerCount(level) > 0) || level->hasEntitiesToRemove()) { #ifdef __PSVITA__ // AP - the PlayerList->viewDistance initially starts out at 3 to make starting a level speedy @@ -2240,6 +2349,8 @@ void MinecraftServer::tick() } PIXEndNamedEvent(); + int64_t stEntDone = System::currentTimeMillis(); + PIXBeginNamedEvent(0,"Entity tracker tick"); level->getTracker()->tick(); PIXEndNamedEvent(); @@ -2248,9 +2359,21 @@ void MinecraftServer::tick() // printf(">>>>>>>>>>>>>>>>>>>>>> Tick %d %d %d : %d\n", st1 - st0, st2 - st1, st3 - st2, st0 - stc ); stc = st0; // #endif// __PS3__ + + // Record per-level breakdown for the slow-tick summary. + if (i < kMaxLevelsRecorded) + { + lvlTickMs[i] = st1 - st0; // Level::tick (mob spawner, chunk source, tile ticks, etc.) + lvlEntMs[i] = stEntDone - st2; // tickEntities (per-entity AI/physics) + lvlTrkMs[i] = st3 - stEntDone; // EntityTracker::tick (visibility & broadcasts) + lvlDimId[i] = level->dimension->id; + recordedLevels = i + 1; + } } } + int64_t afterLevels = System::currentTimeMillis(); Entity::tickExtraWandering(); // 4J added + int64_t afterExtraW = System::currentTimeMillis(); // Process player disconnect/kick queue BEFORE ticking connections. // PendingConnection::handleLogin rejects duplicate XUIDs, so the old @@ -2259,9 +2382,11 @@ void MinecraftServer::tick() PIXBeginNamedEvent(0,"Players tick"); players->tick(); PIXEndNamedEvent(); + int64_t afterPlayers = System::currentTimeMillis(); PIXBeginNamedEvent(0,"Connection tick"); connection->tick(); PIXEndNamedEvent(); + int64_t afterConn = System::currentTimeMillis(); // 4J - removed #if 0 @@ -2275,6 +2400,35 @@ void MinecraftServer::tick() // } catch (Exception e) { // logger.log(Level.WARNING, "Unexpected exception while parsing console command", e); // } + + int64_t totalMs = System::currentTimeMillis() - tickStart; +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + if (totalMs > TICK_SLOW_THRESHOLD_MS) + { + // Build a single one-line breakdown so it greps cleanly. Per-level: + // Level::tick / tickEntities / tracker tick. Then global subsystems. + char buf[512]; + int n = 0; + for (unsigned int i = 0; i < recordedLevels && n >= 0 && n < (int)sizeof(buf); i++) + { + n += snprintf(buf + n, sizeof(buf) - n, + " L%d:tick=%lld ent=%lld trk=%lld", + lvlDimId[i], + (long long)lvlTickMs[i], + (long long)lvlEntMs[i], + (long long)lvlTrkMs[i]); + } + ServerRuntime::LogInfof("perf", + "slow tick total=%lldms%s | extraW=%lld players=%lld conn=%lld", + (long long)totalMs, + buf, + (long long)(afterExtraW - afterLevels), + (long long)(afterPlayers - afterExtraW), + (long long)(afterConn - afterPlayers)); + } +#else + (void)totalMs; +#endif } void MinecraftServer::handleConsoleInput(const wstring& msg, ConsoleInputSource *source) @@ -2418,7 +2572,9 @@ bool MinecraftServer::chunkPacketManagement_CanSendTo(INetworkPlayer *player) if( player == nullptr ) return false; #ifdef MINECRAFT_SERVER_BUILD - return true; + // Cap chunk-data sends per tick. Other players are served on later ticks + // via the per-tick rotation in ServerConnection::tick. + return s_dedicatedChunkSendsThisTick < DEDICATED_MAX_CHUNK_SENDS_PER_TICK; #else int time = GetTickCount(); DWORD currentPlayerCount = g_NetworkManager.GetPlayerCount(); @@ -2437,10 +2593,16 @@ bool MinecraftServer::chunkPacketManagement_CanSendTo(INetworkPlayer *player) void MinecraftServer::chunkPacketManagement_DidSendTo(INetworkPlayer *player) { s_slowQueuePacketSent = true; +#ifdef MINECRAFT_SERVER_BUILD + s_dedicatedChunkSendsThisTick++; +#endif } void MinecraftServer::chunkPacketManagement_PreTick() { +#ifdef MINECRAFT_SERVER_BUILD + s_dedicatedChunkSendsThisTick = 0; +#endif } void MinecraftServer::chunkPacketManagement_PostTick() diff --git a/Minecraft.Client/MinecraftServer.h b/Minecraft.Client/MinecraftServer.h index aa0c4a37..6b76c194 100644 --- a/Minecraft.Client/MinecraftServer.h +++ b/Minecraft.Client/MinecraftServer.h @@ -258,6 +258,12 @@ private: static int s_slowQueuePlayerIndex; static int s_slowQueueLastTime; static bool s_slowQueuePacketSent; +#ifdef MINECRAFT_SERVER_BUILD + // Cap on chunk-data packet sends per tick. Paired with per-tick rotation + // in ServerConnection::tick so every player gets a turn even when bound. + static int s_dedicatedChunkSendsThisTick; + static const int DEDICATED_MAX_CHUNK_SENDS_PER_TICK = 10; +#endif #endif bool IsServerPaused() { return m_isServerPaused; } diff --git a/Minecraft.Client/Orbis/Iggy/include/rrCore.h b/Minecraft.Client/Orbis/Iggy/include/rrCore.h index 17ebee3a..d3f4f41e 100644 --- a/Minecraft.Client/Orbis/Iggy/include/rrCore.h +++ b/Minecraft.Client/Orbis/Iggy/include/rrCore.h @@ -1619,7 +1619,7 @@ RADDEFSTART #define RR_BREAK() __builtin_trap() #define RR_CACHE_LINE_SIZE 32 #elif defined(__RADXENON__) - #define RR_BREAK() __debugbreak() + #define RR_BREAK() DEBUG_BREAK() #define RR_CACHE_LINE_SIZE 128 #elif defined(__RADANDROID__) #define RR_BREAK() __builtin_trap() diff --git a/Minecraft.Client/Orbis/Leaderboards/OrbisLeaderboardManager.cpp b/Minecraft.Client/Orbis/Leaderboards/OrbisLeaderboardManager.cpp index feede12a..6437a90e 100644 --- a/Minecraft.Client/Orbis/Leaderboards/OrbisLeaderboardManager.cpp +++ b/Minecraft.Client/Orbis/Leaderboards/OrbisLeaderboardManager.cpp @@ -245,7 +245,7 @@ bool OrbisLeaderboardManager::getScoreByIds() { // 4J-JEV: Something terrible must have happend, // 'getFriendslist' was supposed to be a synchronous operation. - __debugbreak(); + DEBUG_BREAK(); // 4J-JEV: We can at least fall-back to just the players score. num = 1; diff --git a/Minecraft.Client/Orbis/Miles/include/rrCore.h b/Minecraft.Client/Orbis/Miles/include/rrCore.h index 17ebee3a..d3f4f41e 100644 --- a/Minecraft.Client/Orbis/Miles/include/rrCore.h +++ b/Minecraft.Client/Orbis/Miles/include/rrCore.h @@ -1619,7 +1619,7 @@ RADDEFSTART #define RR_BREAK() __builtin_trap() #define RR_CACHE_LINE_SIZE 32 #elif defined(__RADXENON__) - #define RR_BREAK() __debugbreak() + #define RR_BREAK() DEBUG_BREAK() #define RR_CACHE_LINE_SIZE 128 #elif defined(__RADANDROID__) #define RR_BREAK() __builtin_trap() diff --git a/Minecraft.Client/PS3/Iggy/include/rrCore.h b/Minecraft.Client/PS3/Iggy/include/rrCore.h index 17ebee3a..d3f4f41e 100644 --- a/Minecraft.Client/PS3/Iggy/include/rrCore.h +++ b/Minecraft.Client/PS3/Iggy/include/rrCore.h @@ -1619,7 +1619,7 @@ RADDEFSTART #define RR_BREAK() __builtin_trap() #define RR_CACHE_LINE_SIZE 32 #elif defined(__RADXENON__) - #define RR_BREAK() __debugbreak() + #define RR_BREAK() DEBUG_BREAK() #define RR_CACHE_LINE_SIZE 128 #elif defined(__RADANDROID__) #define RR_BREAK() __builtin_trap() diff --git a/Minecraft.Client/PS3/Miles/include/rrCore.h b/Minecraft.Client/PS3/Miles/include/rrCore.h index 17ebee3a..d3f4f41e 100644 --- a/Minecraft.Client/PS3/Miles/include/rrCore.h +++ b/Minecraft.Client/PS3/Miles/include/rrCore.h @@ -1619,7 +1619,7 @@ RADDEFSTART #define RR_BREAK() __builtin_trap() #define RR_CACHE_LINE_SIZE 32 #elif defined(__RADXENON__) - #define RR_BREAK() __debugbreak() + #define RR_BREAK() DEBUG_BREAK() #define RR_CACHE_LINE_SIZE 128 #elif defined(__RADANDROID__) #define RR_BREAK() __builtin_trap() diff --git a/Minecraft.Client/PSVita/Iggy/include/rrCore.h b/Minecraft.Client/PSVita/Iggy/include/rrCore.h index 17ebee3a..d3f4f41e 100644 --- a/Minecraft.Client/PSVita/Iggy/include/rrCore.h +++ b/Minecraft.Client/PSVita/Iggy/include/rrCore.h @@ -1619,7 +1619,7 @@ RADDEFSTART #define RR_BREAK() __builtin_trap() #define RR_CACHE_LINE_SIZE 32 #elif defined(__RADXENON__) - #define RR_BREAK() __debugbreak() + #define RR_BREAK() DEBUG_BREAK() #define RR_CACHE_LINE_SIZE 128 #elif defined(__RADANDROID__) #define RR_BREAK() __builtin_trap() diff --git a/Minecraft.Client/PSVita/Miles/include/rrCore.h b/Minecraft.Client/PSVita/Miles/include/rrCore.h index 17ebee3a..d3f4f41e 100644 --- a/Minecraft.Client/PSVita/Miles/include/rrCore.h +++ b/Minecraft.Client/PSVita/Miles/include/rrCore.h @@ -1619,7 +1619,7 @@ RADDEFSTART #define RR_BREAK() __builtin_trap() #define RR_CACHE_LINE_SIZE 32 #elif defined(__RADXENON__) - #define RR_BREAK() __debugbreak() + #define RR_BREAK() DEBUG_BREAK() #define RR_CACHE_LINE_SIZE 128 #elif defined(__RADANDROID__) #define RR_BREAK() __builtin_trap() diff --git a/Minecraft.Client/Particle.cpp b/Minecraft.Client/Particle.cpp index 5747286f..80dac72e 100644 --- a/Minecraft.Client/Particle.cpp +++ b/Minecraft.Client/Particle.cpp @@ -218,7 +218,7 @@ void Particle::setTex(Textures *textures, Icon *icon) { #ifndef _CONTENT_PACKAGE printf("Invalid call to Particle.setTex, use coordinate methods\n"); - __debugbreak(); + DEBUG_BREAK(); #endif //throw new RuntimeException("Invalid call to Particle.setTex, use coordinate methods"); } @@ -230,7 +230,7 @@ void Particle::setMiscTex(int slotIndex) { #ifndef _CONTENT_PACKAGE printf("Invalid call to Particle.setMixTex\n"); - __debugbreak(); + DEBUG_BREAK(); //throw new RuntimeException("Invalid call to Particle.setMiscTex"); #endif } diff --git a/Minecraft.Client/PlayerChunkMap.cpp b/Minecraft.Client/PlayerChunkMap.cpp index be8143d8..9ac67927 100644 --- a/Minecraft.Client/PlayerChunkMap.cpp +++ b/Minecraft.Client/PlayerChunkMap.cpp @@ -13,6 +13,14 @@ #include "../Minecraft.World/System.h" #include "PlayerList.h" #include +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) +#include "../Minecraft.Server/ServerLogger.h" + +// Per-tick accumulators for tickAddRequests timing. +static int64_t g_findUsAccum = 0; +static int64_t g_addUsAccum = 0; +static int g_addCount = 0; +#endif PlayerChunkMap::PlayerChunk::PlayerChunk(int x, int z, PlayerChunkMap *pcm) : pos(x,z) { @@ -74,7 +82,7 @@ void PlayerChunkMap::PlayerChunk::add(shared_ptr player, bool send players.push_back(player); - player->chunksToSend.push_back(pos); + player->chunksToSend.insert(pos); #ifdef _LARGE_WORLDS parent->getLevel()->cache->dontDrop(pos.x, pos.z); // 4J Added; @@ -118,7 +126,7 @@ void PlayerChunkMap::PlayerChunk::remove(shared_ptr player) parent->getLevel()->cache->drop(pos.x, pos.z); } - player->chunksToSend.remove(pos); + player->chunksToSend.erase(pos); // 4J - I don't think there's any point sending these anymore, as we don't need to unload chunks with fixed sized maps // 4J - We do need to send these to unload entities in chunks when players are dead. If we do not and the entity is removed // while they are dead, that entity will remain in the clients world @@ -369,6 +377,15 @@ ServerLevel *PlayerChunkMap::getLevel() void PlayerChunkMap::tick() { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + // Substep timing for slow chunkMap diagnostics. + const int64_t CHUNKMAP_SLOW_THRESHOLD_MS = 50; + int64_t cm_t0 = System::currentTimeMillis(); + int cm_addReqStart = (int)addRequests.size(); + g_findUsAccum = 0; + g_addUsAccum = 0; + g_addCount = 0; +#endif int64_t time = level->getGameTime(); if (time - lastInhabitedUpdate > Level::TICKS_PER_DAY / 3) @@ -385,6 +402,9 @@ void PlayerChunkMap::tick() chunk->updateInhabitedTime(); } } +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + int64_t cm_t1 = System::currentTimeMillis(); +#endif // 4J - some changes here so that we only send one region update per tick. The chunks themselves also // limit their resend rate to once every MIN_TICKS_BETWEEN_REGION_UPDATE ticks @@ -404,11 +424,35 @@ void PlayerChunkMap::tick() i++; } } +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + int64_t cm_t2 = System::currentTimeMillis(); +#endif for( unsigned int i = 0; i < players.size(); i++ ) { tickAddRequests(players[i]); } +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + int64_t cm_t3 = System::currentTimeMillis(); + int64_t cm_total = cm_t3 - cm_t0; + if (cm_total > CHUNKMAP_SLOW_THRESHOLD_MS) + { + ServerRuntime::LogInfof("perf", + "L%d chunkMap total=%lldms inhabited=%lld changed=%lld addReq=%lld | players=%d addReqQueue=%d->%d | adds=%d findUs=%lld addUs=%lld avgAddUs=%lld", + this->dimension, + (long long)cm_total, + (long long)(cm_t1 - cm_t0), + (long long)(cm_t2 - cm_t1), + (long long)(cm_t3 - cm_t2), + (int)players.size(), + cm_addReqStart, + (int)addRequests.size(), + g_addCount, + (long long)g_findUsAccum, + (long long)g_addUsAccum, + g_addCount > 0 ? (long long)(g_addUsAccum / g_addCount) : 0LL); + } +#endif // 4J Stu - Added 1.1 but not relevant to us as we never no 0 players anyway, and don't think we should be dropping stuff //if (players.isEmpty()) { @@ -490,7 +534,11 @@ void PlayerChunkMap::getChunkAndRemovePlayer(int x, int z, shared_ptr player) for (int processed = 0; processed < CHUNKS_PER_PLAYER_PER_TICK; processed++) { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + LARGE_INTEGER qpcFreq, qpcA, qpcB, qpcC; + QueryPerformanceFrequency(&qpcFreq); + QueryPerformanceCounter(&qpcA); +#endif // Find the nearest chunk request to the player int minDistSq = -1; auto itNearest = addRequests.end(); @@ -522,12 +575,21 @@ void PlayerChunkMap::tickAddRequests(shared_ptr player) } } } +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + QueryPerformanceCounter(&qpcB); + g_findUsAccum += (qpcB.QuadPart - qpcA.QuadPart) * 1000000 / qpcFreq.QuadPart; +#endif // If we found one, process it and continue; otherwise done if( itNearest != addRequests.end() ) { getChunk(itNearest->x, itNearest->z, true)->add(itNearest->player); addRequests.erase(itNearest); +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + QueryPerformanceCounter(&qpcC); + g_addUsAccum += (qpcC.QuadPart - qpcB.QuadPart) * 1000000 / qpcFreq.QuadPart; + g_addCount++; +#endif } else { @@ -781,7 +843,7 @@ bool PlayerChunkMap::isPlayerIn(shared_ptr player, int xChunk, int else { auto it1 = find(chunk->players.begin(), chunk->players.end(), player); - auto it2 = find(player->chunksToSend.begin(), player->chunksToSend.end(), chunk->pos); + auto it2 = player->chunksToSend.find(chunk->pos); return it1 != chunk->players.end() && it2 == player->chunksToSend.end(); } diff --git a/Minecraft.Client/PlayerList.cpp b/Minecraft.Client/PlayerList.cpp index 4c0f6bf7..8814401d 100644 --- a/Minecraft.Client/PlayerList.cpp +++ b/Minecraft.Client/PlayerList.cpp @@ -329,7 +329,9 @@ bool PlayerList::placeNewPlayer(Connection *connection, shared_ptr // 4J-PB - removed, since it needs to be localised in the language the client is in //server->players->broadcastAll( shared_ptr( new ChatPacket(L"�e" + playerEntity->name + L" joined the game.") ) ); +#if !(defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD)) broadcastAll(std::make_shared(player->name, ChatPacket::e_ChatPlayerJoinedGame)); +#endif MemSect(14); add(player); diff --git a/Minecraft.Client/PreStitchedTextureMap.cpp b/Minecraft.Client/PreStitchedTextureMap.cpp index cbc5ef84..ff8d024c 100644 --- a/Minecraft.Client/PreStitchedTextureMap.cpp +++ b/Minecraft.Client/PreStitchedTextureMap.cpp @@ -227,7 +227,6 @@ void PreStitchedTextureMap::makeTextureAnimated(TexturePack *texturePack, Stitch if(first->getWidth() != tex->getWidth() || first->getHeight() != tex->getHeight()) { app.DebugPrintf("%ls - first w - %d, h - %d, tex w - %d, h - %d\n",textureFileName.c_str(),first->getWidth(),tex->getWidth(),first->getHeight(),tex->getHeight()); - //__debugbreak(); } #endif @@ -246,7 +245,7 @@ StitchedTexture *PreStitchedTextureMap::getTexture(const wstring &name) { #ifndef _CONTENT_PACKAGE app.DebugPrintf("Not implemented!\n"); - __debugbreak(); + DEBUG_BREAK(); #endif return nullptr; #if 0 @@ -277,7 +276,7 @@ Icon *PreStitchedTextureMap::registerIcon(const wstring &name) { app.DebugPrintf("Don't register nullptr\n"); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif result = missingPosition; //new RuntimeException("Don't register null!").printStackTrace(); @@ -290,7 +289,7 @@ Icon *PreStitchedTextureMap::registerIcon(const wstring &name) { #ifndef _CONTENT_PACKAGE app.DebugPrintf("Could not find uv data for icon %ls\n", name.c_str() ); - __debugbreak(); + DEBUG_BREAK(); #endif result = missingPosition; } diff --git a/Minecraft.Client/ServerChunkCache.cpp b/Minecraft.Client/ServerChunkCache.cpp index 881acabf..6262512a 100644 --- a/Minecraft.Client/ServerChunkCache.cpp +++ b/Minecraft.Client/ServerChunkCache.cpp @@ -12,6 +12,9 @@ #include "../Minecraft.World/compression.h" #include "../Minecraft.World/OldChunkStorage.h" #include "../Minecraft.World/Tile.h" +#ifdef MINECRAFT_SERVER_BUILD +#include "../Minecraft.Server/FourKitBridge.h" +#endif ServerChunkCache::ServerChunkCache(ServerLevel *level, ChunkStorage *storage, ChunkSource *source) { @@ -125,7 +128,10 @@ LevelChunk *ServerChunkCache::create(int x, int z, bool asyncPostProcess) // 4J { EnterCriticalSection(&m_csLoadCreate); chunk = load(x, z); - if (chunk == nullptr) +#ifdef MINECRAFT_SERVER_BUILD + bool isNewChunk = (chunk == nullptr); +#endif + if (chunk == nullptr) { if (source == nullptr) { @@ -204,6 +210,10 @@ LevelChunk *ServerChunkCache::create(int x, int z, bool asyncPostProcess) // 4J if( hasChunk( x - 1, z ) && hasChunk( x + 1, z ) && hasChunk ( x, z - 1 ) && hasChunk( x, z + 1 ) ) chunk->checkChests( this, x, z ); LeaveCriticalSection(&m_csLoadCreate); + +#ifdef MINECRAFT_SERVER_BUILD + FourKitBridge::FireChunkLoad(level->dimension->id, x, z, isNewChunk); +#endif } else { @@ -351,6 +361,38 @@ void ServerChunkCache::overwriteHellLevelChunkFromSource(int x, int z, int minVa #endif +#ifdef MINECRAFT_SERVER_BUILD +void ServerChunkCache::regenerateChunk(int x, int z) +{ + if (!source) + return; + + LevelChunk *freshChunk = source->getChunk(x, z); + if (!freshChunk) + return; + + LevelChunk *cachedChunk = nullptr; + if (hasChunk(x, z)) + cachedChunk = getChunk(x, z); + + if (cachedChunk && cachedChunk != emptyChunk) + { + for (int lx = 0; lx < 16; lx++) + for (int ly = 0; ly < 128; ly++) + for (int lz = 0; lz < 16; lz++) + cachedChunk->setTileAndData(lx, ly, lz, freshChunk->getTile(lx, ly, lz), freshChunk->getData(lx, ly, lz)); + save(cachedChunk); + } + else + { + save(freshChunk); + } + + freshChunk->unload(false); + delete freshChunk; +} +#endif + // 4J Added // #ifdef _LARGE_WORLDS void ServerChunkCache::dontDrop(int x, int z) @@ -914,15 +956,19 @@ bool ServerChunkCache::tick() // player's tick is called to remove them from the chunk they used to be in, and add them to their current chunk. This will only be a temporary state and // we should be able to unload the chunk on the next call to this tick. if( !chunk->containsPlayer() ) - { + { +#ifdef MINECRAFT_SERVER_BUILD + if (!FourKitBridge::FireChunkUnload(level->dimension->id, chunk->x, chunk->z)) + { +#endif save(chunk); saveEntities(chunk); chunk->unload(true); //loadedChunks.remove(cp); //loadedChunkList.remove(chunk); - auto it = std::find(m_loadedChunkList.begin(), m_loadedChunkList.end(), chunk); - if(it != m_loadedChunkList.end()) m_loadedChunkList.erase(it); + auto it = std::find(m_loadedChunkList.begin(), m_loadedChunkList.end(), chunk); + if(it != m_loadedChunkList.end()) m_loadedChunkList.erase(it); int ix = chunk->x + XZOFFSET; int iz = chunk->z + XZOFFSET; @@ -930,6 +976,9 @@ bool ServerChunkCache::tick() delete m_unloadedCache[idx]; m_unloadedCache[idx] = chunk; cache[idx] = nullptr; +#ifdef MINECRAFT_SERVER_BUILD + } +#endif } else { diff --git a/Minecraft.Client/ServerChunkCache.h b/Minecraft.Client/ServerChunkCache.h index 97138e76..559217ed 100644 --- a/Minecraft.Client/ServerChunkCache.h +++ b/Minecraft.Client/ServerChunkCache.h @@ -53,6 +53,9 @@ public: #endif virtual LevelChunk **getCache() { return cache; } // 4J added +#ifdef MINECRAFT_SERVER_BUILD + void regenerateChunk(int x, int z); +#endif // 4J-JEV Added; Remove chunk from the toDrop queue. #ifdef _LARGE_WORLDS diff --git a/Minecraft.Client/ServerConnection.cpp b/Minecraft.Client/ServerConnection.cpp index 7bf05a9b..9ebf7259 100644 --- a/Minecraft.Client/ServerConnection.cpp +++ b/Minecraft.Client/ServerConnection.cpp @@ -13,6 +13,8 @@ #if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) #include "../Minecraft.Server/Security/SecurityConfig.h" #include "../Minecraft.Server/ServerLogManager.h" +#include "../Minecraft.Server/ServerLogger.h" +#include "../Minecraft.World/System.h" #endif ServerConnection::ServerConnection(MinecraftServer *server) @@ -129,16 +131,45 @@ void ServerConnection::tick() vector< shared_ptr > tempPlayers = players; LeaveCriticalSection(&players_cs); - for (unsigned int i = 0; i < tempPlayers.size(); i++) +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + // Substep timing for chunk / tick / flush across all players. + LARGE_INTEGER scFreq, scA, scB, scC, scD; + QueryPerformanceFrequency(&scFreq); + int64_t sc_chunkUs = 0; + int64_t sc_tickUs = 0; + int64_t sc_flushUs = 0; + int64_t sc_loopT0 = System::currentTimeMillis(); +#endif + + // Rotate the per-tick start offset so the chunk-send cap doesn't + // starve players at the back of the vector. + static unsigned int s_chunkRotationOffset = 0; + s_chunkRotationOffset++; + size_t playerCount = tempPlayers.size(); + size_t startIdx = playerCount > 0 ? (s_chunkRotationOffset % playerCount) : 0; + + for (unsigned int k = 0; k < tempPlayers.size(); k++) { + unsigned int i = (unsigned int)((startIdx + k) % playerCount); shared_ptr player = tempPlayers[i]; shared_ptr serverPlayer = player->getPlayer(); +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + QueryPerformanceCounter(&scA); +#endif if( serverPlayer ) { serverPlayer->updateFrameTick(); serverPlayer->doChunkSendingTick(false); } +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + QueryPerformanceCounter(&scB); + sc_chunkUs += (scB.QuadPart - scA.QuadPart) * 1000000 / scFreq.QuadPart; +#endif player->tick(); +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + QueryPerformanceCounter(&scC); + sc_tickUs += (scC.QuadPart - scB.QuadPart) * 1000000 / scFreq.QuadPart; +#endif if (player->done) { EnterCriticalSection(&players_cs); @@ -150,7 +181,28 @@ void ServerConnection::tick() { player->connection->flush(); } +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + QueryPerformanceCounter(&scD); + sc_flushUs += (scD.QuadPart - scC.QuadPart) * 1000000 / scFreq.QuadPart; +#endif } + +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + int64_t sc_loopTotal = System::currentTimeMillis() - sc_loopT0; + if (sc_loopTotal > 50) + { + ServerRuntime::LogInfof("perf", + "conn playerLoop total=%lldms players=%d chunkUs=%lld tickUs=%lld flushUs=%lld | avg chunk=%lld tick=%lld flush=%lld", + (long long)sc_loopTotal, + (int)tempPlayers.size(), + (long long)sc_chunkUs, + (long long)sc_tickUs, + (long long)sc_flushUs, + tempPlayers.size() > 0 ? (long long)(sc_chunkUs / tempPlayers.size()) : 0LL, + tempPlayers.size() > 0 ? (long long)(sc_tickUs / tempPlayers.size()) : 0LL, + tempPlayers.size() > 0 ? (long long)(sc_flushUs / tempPlayers.size()) : 0LL); + } +#endif } } diff --git a/Minecraft.Client/ServerLevel.cpp b/Minecraft.Client/ServerLevel.cpp index ff905d26..507bfbc7 100644 --- a/Minecraft.Client/ServerLevel.cpp +++ b/Minecraft.Client/ServerLevel.cpp @@ -41,6 +41,7 @@ #include "PlayerChunkMap.h" #if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) #include "../Minecraft.Server/FourKitBridge.h" +#include "../Minecraft.Server/ServerLogger.h" #endif WeighedTreasureArray ServerLevel::RANDOM_BONUS_ITEMS; @@ -197,6 +198,14 @@ ServerLevel::~ServerLevel() void ServerLevel::tick() { +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + // ts[N] = wall-clock at substep N. Logged when level total exceeds + // LEVEL_SLOW_THRESHOLD_MS to pinpoint the dominant substep. + const int64_t LEVEL_SLOW_THRESHOLD_MS = 50; + int64_t ts[13]; + ts[0] = System::currentTimeMillis(); +#endif + Level::tick(); if (getLevelData()->isHardcore() && difficulty < 3) { @@ -218,6 +227,9 @@ void ServerLevel::tick() } awakenAllPlayers(); } +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + ts[1] = System::currentTimeMillis(); +#endif PIXBeginNamedEvent(0,"Mob spawner tick"); // for Minecraft 1.8, spawn friendlies really rarely - 4J - altered from once every 400 ticks to 40 ticks as we depend on this a more than the original since we don't have chunk post-process spawning @@ -233,6 +245,9 @@ void ServerLevel::tick() mobSpawner->tick(this, finalSpawnEnemies, finalSpawnFriendlies, finalSpawnPersistent); } PIXEndNamedEvent(); +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + ts[2] = System::currentTimeMillis(); +#endif PIXBeginNamedEvent(0,"Chunk source tick"); chunkSource->tick(); PIXEndNamedEvent(); @@ -248,6 +263,9 @@ void ServerLevel::tick() } } } +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + ts[3] = System::currentTimeMillis(); +#endif //4J - temporarily disabling saves as they are causing gameplay to generally stutter quite a lot @@ -263,6 +281,9 @@ void ServerLevel::tick() save(false, nullptr); PIXEndNamedEvent(); } +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + ts[4] = System::currentTimeMillis(); +#endif // 4J : WESTY : Changed so that time update goes through stats tracking update code. //levelData->setTime(time); @@ -278,19 +299,31 @@ void ServerLevel::tick() setDayTime(levelData->getDayTime() + 1); } } +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + ts[5] = System::currentTimeMillis(); +#endif PIXBeginNamedEvent(0,"Tick pending ticks"); // if (tickCount % 5 == 0) { tickPendingTicks(false); PIXEndNamedEvent(); +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + ts[6] = System::currentTimeMillis(); +#endif PIXBeginNamedEvent(0,"Tick tiles"); MemSect(18); tickTiles(); MemSect(0); PIXEndNamedEvent(); +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + ts[7] = System::currentTimeMillis(); +#endif chunkMap->tick(); +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + ts[8] = System::currentTimeMillis(); +#endif PIXBeginNamedEvent(0,"Tick villages"); //MemSect(18); @@ -298,18 +331,50 @@ void ServerLevel::tick() villageSiege->tick(); //MemSect(0); PIXEndNamedEvent(); +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + ts[9] = System::currentTimeMillis(); +#endif PIXBeginNamedEvent(0,"Tick portal forcer"); portalForcer->tick(getGameTime()); PIXEndNamedEvent(); +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + ts[10] = System::currentTimeMillis(); +#endif // repeat after tile ticks PIXBeginNamedEvent(0,"runTileEvents"); runTileEvents(); PIXEndNamedEvent(); +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + ts[11] = System::currentTimeMillis(); +#endif // 4J Added runQueuedSendTileUpdates(); +#if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) + ts[12] = System::currentTimeMillis(); + int64_t levelTotal = ts[12] - ts[0]; + if (levelTotal > LEVEL_SLOW_THRESHOLD_MS) + { + ServerRuntime::LogInfof("perf", + "L%d substep total=%lldms base=%lld spawn=%lld chunkSrc=%lld save=%lld time=%lld pending=%lld tiles=%lld chunkMap=%lld villages=%lld portal=%lld evt=%lld sendTU=%lld", + dimension->id, + (long long)levelTotal, + (long long)(ts[1] - ts[0]), + (long long)(ts[2] - ts[1]), + (long long)(ts[3] - ts[2]), + (long long)(ts[4] - ts[3]), + (long long)(ts[5] - ts[4]), + (long long)(ts[6] - ts[5]), + (long long)(ts[7] - ts[6]), + (long long)(ts[8] - ts[7]), + (long long)(ts[9] - ts[8]), + (long long)(ts[10] - ts[9]), + (long long)(ts[11] - ts[10]), + (long long)(ts[12] - ts[11])); + } +#endif } Biome::MobSpawnerData *ServerLevel::getRandomMobSpawnAt(MobCategory *mobCategory, int x, int y, int z) diff --git a/Minecraft.Client/ServerPlayer.cpp b/Minecraft.Client/ServerPlayer.cpp index 5ff71075..04f5691c 100644 --- a/Minecraft.Client/ServerPlayer.cpp +++ b/Minecraft.Client/ServerPlayer.cpp @@ -8,6 +8,9 @@ #include "Settings.h" #include "PlayerList.h" #include "MultiPlayerLevel.h" +#include "Minecraft.h" +#include "Common/Audio/SoundEngine.h" +#include "../Minecraft.World/SoundTypes.h" #include "../Minecraft.World/net.minecraft.network.packet.h" #include "../Minecraft.World/net.minecraft.world.damagesource.h" @@ -474,28 +477,82 @@ void ServerPlayer::doTickA() } // 4J - split off the chunk sending bit of the tick here from ::doTick so we can do this exactly once per player per server tick +// +// Find-nearest uses spiral iteration from the player's chunk position with +// O(1) hash lookups against chunksToSend. A bounded fallback walk covers +// the rare case of a stale entry outside the spiral radius. void ServerPlayer::doChunkSendingTick(bool dontDelayChunks) { // printf("[%d] %s: sendChunks: %d, empty: %d\n",tickCount, connection->getNetworkPlayer()->GetUID().getOnlineID(),sendChunks,chunksToSend.empty()); if (!chunksToSend.empty()) { - ChunkPos nearest = chunksToSend.front(); + ChunkPos nearest(0, 0); bool nearestValid = false; - - // 4J - reinstated and optimised some code that was commented out in the original, to make sure that we always - // send the nearest chunk to the player. The original uses the bukkit sorting thing to try and avoid doing this, but - // the player can quickly wander away from the centre of the spiral of chunks that that method creates, long before transmission - // of them is complete. double dist = DBL_MAX; - for(ChunkPos chunk : chunksToSend) + + const int px = (int)floor(x) >> 4; + const int pz = (int)floor(z) >> 4; + // Bound on spiral radius. Configured view distance is much smaller. + const int kMaxSpiralRadius = 32; + + // Inline distance: ChunkPos::distanceToSqr is non-const so it can't + // be called on the const refs we get when iterating an unordered_set. + auto chunkDistSq = [&](int cx, int cz) -> double { + double xPos = cx * 16.0 + 8.0; + double zPos = cz * 16.0 + 8.0; + double xd = xPos - this->x; + double zd = zPos - this->z; + return xd * xd + zd * zd; + }; + + // Ring r=0: the player's own chunk. { - if( level->isChunkFinalised(chunk.x, chunk.z) ) + ChunkPos cp(px, pz); + auto it = chunksToSend.find(cp); + if (it != chunksToSend.end() && level->isChunkFinalised(cp.x, cp.z)) { - double newDist = chunk.distanceToSqr(x, z); - if ( (!nearestValid) || (newDist < dist) ) + nearest = cp; + dist = chunkDistSq(cp.x, cp.z); + nearestValid = true; + } + } + + // Rings r>=1: Chebyshev perimeter. First ring with a match wins; + // within it pick the closest by true Euclidean distance. + for (int r = 1; r <= kMaxSpiralRadius && !nearestValid; r++) + { + for (int dx = -r; dx <= r; dx++) + { + for (int dz = -r; dz <= r; dz++) { - nearest = chunk; - dist = chunk.distanceToSqr(x, z); + int adx = dx < 0 ? -dx : dx; + int adz = dz < 0 ? -dz : dz; + if ((adx > adz ? adx : adz) != r) continue; // perimeter only + ChunkPos cp(px + dx, pz + dz); + if (chunksToSend.find(cp) == chunksToSend.end()) continue; + if (!level->isChunkFinalised(cp.x, cp.z)) continue; + double d = chunkDistSq(cp.x, cp.z); + if (!nearestValid || d < dist) + { + nearest = cp; + dist = d; + nearestValid = true; + } + } + } + } + + // Fallback for chunks outside the spiral radius (rare). + if (!nearestValid) + { + for (const ChunkPos& cp : chunksToSend) + { + if (!level->isChunkFinalised(cp.x, cp.z)) continue; + double d = chunkDistSq(cp.x, cp.z); + if (!nearestValid || d < dist) + { + nearest = cp; + dist = d; nearestValid = true; } } @@ -564,7 +621,7 @@ void ServerPlayer::doChunkSendingTick(bool dontDelayChunks) { ServerLevel *level = server->getLevel(dimension); int flagIndex = getFlagIndexForChunk(nearest,this->level->dimension->id); - chunksToSend.remove(nearest); + chunksToSend.erase(nearest); bool chunkDataSent = false; @@ -1036,6 +1093,13 @@ void ServerPlayer::changeDimension(int i) // 4J: Removed on the advice of the mighty King of Achievments (JV) // awardStat(GenericStats::portal(), GenericStats::param_portal()); } + // play the travel whoosh right before the actual dimension swap + Minecraft *mc = Minecraft::GetInstance(); + if (mc != nullptr && mc->soundEngine != nullptr) + { + mc->soundEngine->playUI(eSoundType_PORTAL_TRAVEL, 1, 1.0f); + } + server->getPlayers()->toggleDimension( dynamic_pointer_cast(shared_from_this()), i); #if defined(_WINDOWS64) && defined(MINECRAFT_SERVER_BUILD) if (portalDestModified) diff --git a/Minecraft.Client/ServerPlayer.h b/Minecraft.Client/ServerPlayer.h index 171f5dc7..12022b48 100644 --- a/Minecraft.Client/ServerPlayer.h +++ b/Minecraft.Client/ServerPlayer.h @@ -25,7 +25,9 @@ public: MinecraftServer *server; ServerPlayerGameMode *gameMode; double lastMoveX, lastMoveZ; - list chunksToSend; + // Hash set for O(1) membership/erase. Find-nearest is done via + // spiral iteration in ServerPlayer::doChunkSendingTick, not a sweep. + unordered_set chunksToSend; vector entitiesToRemove; unordered_set seenChunks; int spewTimer; diff --git a/Minecraft.Client/Stitcher.cpp b/Minecraft.Client/Stitcher.cpp index 450c8026..3243889a 100644 --- a/Minecraft.Client/Stitcher.cpp +++ b/Minecraft.Client/Stitcher.cpp @@ -87,7 +87,7 @@ void Stitcher::stitch() { app.DebugPrintf("Stitcher exception!\n"); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif //throw new StitcherException(textureHolder); } diff --git a/Minecraft.Client/TextureMap.cpp b/Minecraft.Client/TextureMap.cpp index a1eb6f8a..e329a532 100644 --- a/Minecraft.Client/TextureMap.cpp +++ b/Minecraft.Client/TextureMap.cpp @@ -214,7 +214,7 @@ Icon *TextureMap::registerIcon(const wstring &name) { app.DebugPrintf("Don't register nullptr\n"); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif //new RuntimeException("Don't register null!").printStackTrace(); } diff --git a/Minecraft.Client/TexturePackRepository.cpp b/Minecraft.Client/TexturePackRepository.cpp index 12604701..dec1b368 100644 --- a/Minecraft.Client/TexturePackRepository.cpp +++ b/Minecraft.Client/TexturePackRepository.cpp @@ -339,7 +339,6 @@ bool TexturePackRepository::selectTexturePackById(DWORD id) app.DebugPrintf("Failed to select texture pack %d as it is not in the list\n", id); #ifndef _CONTENT_PACKAGE // TODO - 4J Stu: We should report this to the player in some way - //__debugbreak(); #endif // Fail safely if( selectSkin( DEFAULT_TEXTURE_PACK ) ) diff --git a/Minecraft.Client/Windows64/Iggy/include/rrCore.h b/Minecraft.Client/Windows64/Iggy/include/rrCore.h index 17ebee3a..d3f4f41e 100644 --- a/Minecraft.Client/Windows64/Iggy/include/rrCore.h +++ b/Minecraft.Client/Windows64/Iggy/include/rrCore.h @@ -1619,7 +1619,7 @@ RADDEFSTART #define RR_BREAK() __builtin_trap() #define RR_CACHE_LINE_SIZE 32 #elif defined(__RADXENON__) - #define RR_BREAK() __debugbreak() + #define RR_BREAK() DEBUG_BREAK() #define RR_CACHE_LINE_SIZE 128 #elif defined(__RADANDROID__) #define RR_BREAK() __builtin_trap() diff --git a/Minecraft.Client/Windows64/Windows64_UIController.cpp b/Minecraft.Client/Windows64/Windows64_UIController.cpp index cd47154c..3f5285ae 100644 --- a/Minecraft.Client/Windows64/Windows64_UIController.cpp +++ b/Minecraft.Client/Windows64/Windows64_UIController.cpp @@ -24,7 +24,7 @@ void ConsoleUIController::init(ID3D11Device *dev, ID3D11DeviceContext *ctx, ID3D { app.DebugPrintf("Failed to initialise GDraw!\n"); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif app.FatalLoadError(); } diff --git a/Minecraft.Client/Xbox/Audio/SoundEngine.cpp b/Minecraft.Client/Xbox/Audio/SoundEngine.cpp index 5587ce81..92421d20 100644 --- a/Minecraft.Client/Xbox/Audio/SoundEngine.cpp +++ b/Minecraft.Client/Xbox/Audio/SoundEngine.cpp @@ -487,7 +487,7 @@ void SoundEngine::play(int iSound, float x, float y, float z, float volume, floa { #ifndef _CONTENT_PACKAGE #ifdef _DEBUG - __debugbreak(); + DEBUG_BREAK(); #endif //wprintf(L"WARNING: Sound cue not found - %ls\n", name.c_str() ); app.DebugPrintf("Not found: %s\n",xboxName); diff --git a/Minecraft.Client/Xbox/Leaderboards/XboxLeaderboardManager.cpp b/Minecraft.Client/Xbox/Leaderboards/XboxLeaderboardManager.cpp index e7aebf2a..535ce55e 100644 --- a/Minecraft.Client/Xbox/Leaderboards/XboxLeaderboardManager.cpp +++ b/Minecraft.Client/Xbox/Leaderboards/XboxLeaderboardManager.cpp @@ -183,7 +183,7 @@ bool XboxLeaderboardManager::WriteStats(unsigned int viewCount, ViewIn views) // some debug code to catch the leaderboard write with 7 views #ifndef _CONTENT_PACKAGE - if(viewCount>5) __debugbreak(); + if(viewCount>5) DEBUG_BREAK(); #endif // 4J Stu - If we are online we already have a session, so use that diff --git a/Minecraft.Client/Xbox/Network/PlatformNetworkManagerXbox.cpp b/Minecraft.Client/Xbox/Network/PlatformNetworkManagerXbox.cpp index 89a42d53..62dccf31 100644 --- a/Minecraft.Client/Xbox/Network/PlatformNetworkManagerXbox.cpp +++ b/Minecraft.Client/Xbox/Network/PlatformNetworkManagerXbox.cpp @@ -211,8 +211,6 @@ VOID CPlatformNetworkManagerXbox::NotifyPlayerLeaving( __in IQNetPlayer * pQNetPlayer ) { - //__debugbreak(); - app.DebugPrintf( "Player 0x%p \"%ls\" leaving.\n", pQNetPlayer, pQNetPlayer->GetGamertag() ); diff --git a/Minecraft.Client/Xbox/Xbox_Minecraft.cpp b/Minecraft.Client/Xbox/Xbox_Minecraft.cpp index bfe08456..a3d87268 100644 --- a/Minecraft.Client/Xbox/Xbox_Minecraft.cpp +++ b/Minecraft.Client/Xbox/Xbox_Minecraft.cpp @@ -315,8 +315,6 @@ int __cdecl main() HRESULT hr; static bool bTrialTimerDisplayed=true; - //__debugbreak(); - #ifdef MEMORY_TRACKING ResetMem(); MEMORYSTATUS memStat; diff --git a/Minecraft.Client/stdafx.h b/Minecraft.Client/stdafx.h index f5acd11f..a4bb0aab 100644 --- a/Minecraft.Client/stdafx.h +++ b/Minecraft.Client/stdafx.h @@ -148,6 +148,7 @@ typedef XUID GameSessionUID; #endif #include "../Minecraft.World/Definitions.h" +#include "../Minecraft.World/Debug.h" #include "../Minecraft.World/Class.h" #include "../Minecraft.World/ArrayWithLength.h" #include "../Minecraft.World/SharedConstants.h" diff --git a/Minecraft.Server.FourKit/Block/Biome.cs b/Minecraft.Server.FourKit/Block/Biome.cs new file mode 100644 index 00000000..82c49f64 --- /dev/null +++ b/Minecraft.Server.FourKit/Block/Biome.cs @@ -0,0 +1,109 @@ +namespace Minecraft.Server.FourKit.Block; + + +public enum Biome +{ + OCEAN = 0, + PLAINS = 1, + DESERT = 2, + EXTREME_HILLS = 3, + FOREST = 4, + TAIGA = 5, + SWAMPLAND = 6, + RIVER = 7, + HELL = 8, + SKY = 9, + FROZEN_OCEAN = 10, + FROZEN_RIVER = 11, + ICE_PLAINS = 12, + ICE_MOUNTAINS = 13, + MUSHROOM_ISLAND = 14, + MUSHROOM_SHORE = 15, + BEACH = 16, + DESERT_HILLS = 17, + FOREST_HILLS = 18, + TAIGA_HILLS = 19, + SMALL_MOUNTAINS = 20, + JUNGLE = 21, + JUNGLE_HILLS = 22, +} + + +// more for internal +// eliminates unnecessary overhead +internal static class BiomeHelper +{ + private static readonly double[] _temperatures = new double[23]; + private static readonly double[] _rainfalls = new double[23]; + + static BiomeHelper() + { + _temperatures[(int)Biome.OCEAN] = 0.5; + _temperatures[(int)Biome.PLAINS] = 0.8; + _temperatures[(int)Biome.DESERT] = 2.0; + _temperatures[(int)Biome.EXTREME_HILLS] = 0.2; + _temperatures[(int)Biome.FOREST] = 0.7; + _temperatures[(int)Biome.TAIGA] = 0.05; + _temperatures[(int)Biome.SWAMPLAND] = 0.8; + _temperatures[(int)Biome.RIVER] = 0.5; + _temperatures[(int)Biome.HELL] = 2.0; + _temperatures[(int)Biome.SKY] = 0.5; + _temperatures[(int)Biome.FROZEN_OCEAN] = 0.0; + _temperatures[(int)Biome.FROZEN_RIVER] = 0.0; + _temperatures[(int)Biome.ICE_PLAINS] = 0.0; + _temperatures[(int)Biome.ICE_MOUNTAINS] = 0.0; + _temperatures[(int)Biome.MUSHROOM_ISLAND] = 0.9; + _temperatures[(int)Biome.MUSHROOM_SHORE] = 0.9; + _temperatures[(int)Biome.BEACH] = 0.8; + _temperatures[(int)Biome.DESERT_HILLS] = 2.0; + _temperatures[(int)Biome.FOREST_HILLS] = 0.7; + _temperatures[(int)Biome.TAIGA_HILLS] = 0.05; + _temperatures[(int)Biome.SMALL_MOUNTAINS] = 0.2; + _temperatures[(int)Biome.JUNGLE] = 1.2; + _temperatures[(int)Biome.JUNGLE_HILLS] = 1.2; + + _rainfalls[(int)Biome.OCEAN] = 0.5; + _rainfalls[(int)Biome.PLAINS] = 0.4; + _rainfalls[(int)Biome.DESERT] = 0.0; + _rainfalls[(int)Biome.EXTREME_HILLS] = 0.3; + _rainfalls[(int)Biome.FOREST] = 0.8; + _rainfalls[(int)Biome.TAIGA] = 0.8; + _rainfalls[(int)Biome.SWAMPLAND] = 0.9; + _rainfalls[(int)Biome.RIVER] = 0.5; + _rainfalls[(int)Biome.HELL] = 0.0; + _rainfalls[(int)Biome.SKY] = 0.5; + _rainfalls[(int)Biome.FROZEN_OCEAN] = 0.5; + _rainfalls[(int)Biome.FROZEN_RIVER] = 0.5; + _rainfalls[(int)Biome.ICE_PLAINS] = 0.5; + _rainfalls[(int)Biome.ICE_MOUNTAINS] = 0.5; + _rainfalls[(int)Biome.MUSHROOM_ISLAND] = 1.0; + _rainfalls[(int)Biome.MUSHROOM_SHORE] = 1.0; + _rainfalls[(int)Biome.BEACH] = 0.4; + _rainfalls[(int)Biome.DESERT_HILLS] = 0.0; + _rainfalls[(int)Biome.FOREST_HILLS] = 0.8; + _rainfalls[(int)Biome.TAIGA_HILLS] = 0.8; + _rainfalls[(int)Biome.SMALL_MOUNTAINS] = 0.3; + _rainfalls[(int)Biome.JUNGLE] = 0.9; + _rainfalls[(int)Biome.JUNGLE_HILLS] = 0.9; + } + + public static double getTemperature(this Biome biome) + { + int id = (int)biome; + if (id >= 0 && id < _temperatures.Length) return _temperatures[id]; + return 0.5; + } + + public static double getRainfall(this Biome biome) + { + int id = (int)biome; + if (id >= 0 && id < _rainfalls.Length) return _rainfalls[id]; + return 0.5; + } + + public static Biome fromId(int id) + { + if (Enum.IsDefined(typeof(Biome), id)) return (Biome)id; + return Biome.PLAINS; + } +} diff --git a/Minecraft.Server.FourKit/Block/Block.cs b/Minecraft.Server.FourKit/Block/Block.cs index 1a31081a..8c19e6bc 100644 --- a/Minecraft.Server.FourKit/Block/Block.cs +++ b/Minecraft.Server.FourKit/Block/Block.cs @@ -89,7 +89,19 @@ public class Block /// Whether the change was successful. public bool setTypeId(int type) { - NativeBridge.SetTile?.Invoke(_world.getDimensionId(), _x, _y, _z, type, 0); + return setTypeId(type, true); + } + + /// + /// Sets the type ID of this block. + /// + /// Type ID to change this block to. + /// False to cancel physics on the changed block. + /// Whether the block was changed. + public bool setTypeId(int type, bool applyPhysics) + { + int flags = applyPhysics ? 3 : 2; + NativeBridge.SetTile?.Invoke(_world.getDimensionId(), _x, _y, _z, type, 0, flags); return true; } @@ -108,7 +120,43 @@ public class Block /// New block specific metadata. public void setData(byte data) { - NativeBridge.SetTileData?.Invoke(_world.getDimensionId(), _x, _y, _z, data); + setData(data, true); + } + + /// + /// Sets the metadata for this block. + /// + /// New block specific metadata. + /// False to cancel physics from the changed block. + public void setData(byte data, bool applyPhysics) + { + int flags = applyPhysics ? 3 : 2; + NativeBridge.SetTileData?.Invoke(_world.getDimensionId(), _x, _y, _z, data, flags); + } + + /// + /// Sets the type ID and data of this block. + /// + /// Type ID to change this block to. + /// The data value to change this block to. + /// Whether the block was changed. + public bool setTypeIdAndData(int type, byte data) + { + return setTypeIdAndData(type, data, true); + } + + /// + /// Sets the type ID and data of this block. + /// + /// Type ID to change this block to. + /// The data value to change this block to. + /// False to cancel physics on the changed block. + /// Whether the block was changed. + public bool setTypeIdAndData(int type, byte data, bool applyPhysics) + { + int flags = applyPhysics ? 3 : 2; + NativeBridge.SetTile?.Invoke(_world.getDimensionId(), _x, _y, _z, type, data, flags); + return true; } /// @@ -134,6 +182,15 @@ public class Block return getWorld().getBlockAt(getX() + modX, getY() + modY, getZ() + modZ); } + /// + /// Gets the chunk which contains this block. + /// + /// Containing Chunk. + public Chunk.Chunk getChunk() + { + return getWorld().getChunkAt(getX() >> 4, getZ() >> 4); + } + /// /// Gets the block at the given face /// This method is equal to getRelative(face, 1) @@ -162,5 +219,93 @@ public class Block { return getRelative(face.getModX() * distance, face.getModY() * distance, face.getModZ() * distance); } - + + /// + /// Returns the biome that this block resides in. + /// + /// Biome type containing this block. + public Biome getBiome() + { + if (NativeBridge.GetBiomeId != null) + return BiomeHelper.fromId(NativeBridge.GetBiomeId(_world.getDimensionId(), _x, _z)); + return Biome.PLAINS; + } + + /// + /// Sets the biome that this block resides in. + /// + /// New Biome type for this block. + public void setBiome(Biome bio) + { + NativeBridge.SetBiomeId?.Invoke(_world.getDimensionId(), _x, _z, (int)bio); + } + + /// + /// Gets the humidity of the biome of this block. + /// + /// Humidity of this block. + public double getHumidity() + { + return getBiome().getRainfall(); + } + + /// + /// Gets the temperature of the biome of this block. + /// + /// Temperature of this block. + public double getTemperature() + { + return getBiome().getTemperature(); + } + + /// + /// Checks if this block is liquid. + /// A block is considered liquid when returns + /// , , + /// or . + /// + /// true if this block is liquid. + public bool isLiquid() + { + Material type = getType(); + return type == Material.WATER || type == Material.STATIONARY_WATER || + type == Material.LAVA || type == Material.STATIONARY_LAVA; + } + + + /// + /// Gets the light level between 0-15. + /// + /// Light level. + public byte getLightLevel() + { + int sky = getLightFromSky(); + int block = getLightFromBlocks(); + return (byte)(sky > block ? sky : block); + } + + /// + /// Get the amount of light at this block from the sky. + /// Any light given from other sources (such as blocks like torches) will be ignored. + /// + /// Sky light level. + public byte getLightFromSky() + { + if (NativeBridge.GetSkyLight != null) + return (byte)NativeBridge.GetSkyLight(_world.getDimensionId(), _x, _y, _z); + return 0; + } + + /// + /// Get the amount of light at this block from nearby blocks. + /// Any light given from other sources (such as the sun) will be ignored. + /// + /// Block light level. + public byte getLightFromBlocks() + { + if (NativeBridge.GetBlockLight != null) + return (byte)NativeBridge.GetBlockLight(_world.getDimensionId(), _x, _y, _z); + return 0; + } + } diff --git a/Minecraft.Server.FourKit/Block/BlockState.cs b/Minecraft.Server.FourKit/Block/BlockState.cs index 343abbb1..33d3493a 100644 --- a/Minecraft.Server.FourKit/Block/BlockState.cs +++ b/Minecraft.Server.FourKit/Block/BlockState.cs @@ -92,6 +92,15 @@ public class BlockState /// Z-coordinate. public int getZ() => _z; + /// + /// Gets the chunk which contains this block. + /// + /// Containing Chunk. + public Chunk.Chunk getChunk() + { + return _world.getChunkAt(_x >> 4, _z >> 4); + } + /// /// Gets the location of this block. /// @@ -191,7 +200,16 @@ public class BlockState if (!force && currentType != _typeId) return false; - NativeBridge.SetTile(_world.getDimensionId(), _x, _y, _z, _typeId, _data); + NativeBridge.SetTile(_world.getDimensionId(), _x, _y, _z, _typeId, _data, applyPhysics ? 3 : 2); return true; } + + /// + /// Gets the light level between 0-15. + /// + /// Light level. + public byte getLightLevel() + { + return getBlock().getLightLevel(); + } } diff --git a/Minecraft.Server.FourKit/ChatColor.cs b/Minecraft.Server.FourKit/ChatColor.cs new file mode 100644 index 00000000..cbfb066a --- /dev/null +++ b/Minecraft.Server.FourKit/ChatColor.cs @@ -0,0 +1,173 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace Minecraft.Server.FourKit; + +/// +/// All supported color values for chat. +/// +public class ChatColor +{ + /// + /// The special character which prefixes all chat colour codes. + /// + public static readonly char COLOR_CHAR = '\u00A7'; // F + + private static readonly Regex STRIP_COLOR_PATTERN = + new Regex("(?i)" + COLOR_CHAR + "[0-9A-FK-OR]", RegexOptions.Compiled); + + private static readonly Dictionary BY_CHAR = new(); + + /// Represents black. + public static readonly ChatColor BLACK = new('0', false); + /// Represents dark blue. + public static readonly ChatColor DARK_BLUE = new('1', false); + /// Represents dark green. + public static readonly ChatColor DARK_GREEN = new('2', false); + /// Represents dark blue (aqua). + public static readonly ChatColor DARK_AQUA = new('3', false); + /// Represents dark red. + public static readonly ChatColor DARK_RED = new('4', false); + /// Represents dark purple. + public static readonly ChatColor DARK_PURPLE = new('5', false); + /// Represents gold. + public static readonly ChatColor GOLD = new('6', false); + /// Represents gray. + public static readonly ChatColor GRAY = new('7', false); + /// Represents dark gray. + public static readonly ChatColor DARK_GRAY = new('8', false); + /// Represents blue. + public static readonly ChatColor BLUE = new('9', false); + /// Represents green. + public static readonly ChatColor GREEN = new('a', false); + /// Represents aqua. + public static readonly ChatColor AQUA = new('b', false); + /// Represents red. + public static readonly ChatColor RED = new('c', false); + /// Represents light purple. + public static readonly ChatColor LIGHT_PURPLE = new('d', false); + /// Represents yellow. + public static readonly ChatColor YELLOW = new('e', false); + /// Represents white. + public static readonly ChatColor WHITE = new('f', false); + /// Resets all previous chat colors or formats. + public static readonly ChatColor RESET = new('r', false); + + private readonly char _code; + private readonly bool _isFormat; + private readonly string _toString; + + private ChatColor(char code, bool isFormat) + { + _code = code; + _isFormat = isFormat; + _toString = new string(new[] { COLOR_CHAR, code }); + BY_CHAR[code] = this; + } + + /// + /// Gets the char value associated with this color. + /// + /// A char value of this color code. + public char getChar() => _code; + + /// + /// Checks if this code is a format code as opposed to a color code. + /// + /// true if this is a format code. + public bool isFormat() => _isFormat; + + /// + /// Checks if this code is a color code as opposed to a format code. + /// + /// true if this is a color code. + public bool isColor() => !_isFormat && this != RESET; + + /// + /// Gets the color represented by the specified color code. + /// + /// Code to check. + /// Associative ChatColor with the given code, or null if it doesn't exist. + public static ChatColor? getByChar(char code) + { + return BY_CHAR.TryGetValue(char.ToLower(code), out var color) ? color : null; + } + + /// + /// Gets the color represented by the specified color code. + /// + /// Code to check. + /// Associative ChatColor with the given code, or null if it doesn't exist. + public static ChatColor? getByChar(string code) + { + if (string.IsNullOrEmpty(code)) return null; + return getByChar(code[0]); + } + + /// + /// Strips the given message of all color codes. + /// + /// String to strip of color. + /// A copy of the input string, without any coloring. + public static string? stripColor(string? input) + { + if (input == null) return null; + return STRIP_COLOR_PATTERN.Replace(input, ""); + } + + /// + /// Translates a string using an alternate color code character into a string + /// that uses the internal color code character. + /// The alternate color code character will only be replaced if it is immediately + /// followed by 0-9, A-F, a-f, K-O, k-o, R or r. + /// + /// The alternate color code character to replace. Ex: & + /// Text containing the alternate color code character. + /// Text containing the color code character. + public static string translateAlternateColorCodes(char altColorChar, string textToTranslate) + { + char[] b = textToTranslate.ToCharArray(); + for (int i = 0; i < b.Length - 1; i++) + { + if (b[i] == altColorChar && "0123456789AaBbCcDdEeFfKkLlMmNnOoRr".IndexOf(b[i + 1]) > -1) + { + b[i] = COLOR_CHAR; + } + } + return new string(b); + } + + /// + /// Gets the ChatColors used at the end of the given input string. + /// + /// Input string to retrieve the colors from. + /// Any remaining ChatColors to pass onto the next line. + public static string getLastColors(string input) + { + var result = new StringBuilder(); + int length = input.Length; + + for (int index = length - 1; index > -1; index--) + { + char section = input[index]; + if (section == COLOR_CHAR && index < length - 1) + { + char c = input[index + 1]; + ChatColor? color = getByChar(c); + if (color != null) + { + result.Insert(0, color.ToString()); + if (color.isColor() || color == RESET) + break; + } + } + } + + return result.ToString(); + } + + public static string operator +(ChatColor color, string text) => color.ToString() + text; + + /// + public override string ToString() => _toString; +} diff --git a/Minecraft.Server.FourKit/Chunk/Chunk.cs b/Minecraft.Server.FourKit/Chunk/Chunk.cs new file mode 100644 index 00000000..32e1f0aa --- /dev/null +++ b/Minecraft.Server.FourKit/Chunk/Chunk.cs @@ -0,0 +1,311 @@ +using Minecraft.Server.FourKit.Block; +using Minecraft.Server.FourKit.Entity; +using System.Runtime.InteropServices; + +namespace Minecraft.Server.FourKit.Chunk; + + +/// +/// Represents a chunk of blocks. +/// +public class Chunk +{ + private readonly World _world; + private readonly int _chunkX; + private readonly int _chunkZ; + + internal Chunk(World world, int chunkX, int chunkZ) + { + _world = world; + _chunkX = chunkX; + _chunkZ = chunkZ; + } + + /// + /// Gets the X-coordinate of this chunk. + /// + /// X-coordinate. + public int getX() => _chunkX; + + /// + /// Gets the Z-coordinate of this chunk. + /// + /// Z-coordinate. + public int getZ() => _chunkZ; + + /// + /// Gets the world containing this chunk. + /// + /// Parent World. + public World getWorld() => _world; + + /// + /// Gets a block from this chunk. + /// + /// 0-15 + /// 0-127 + /// 0-15 + /// The Block. + public Block.Block getBlock(int x, int y, int z) + { + return _world.getBlockAt((_chunkX << 4) + x, y, (_chunkZ << 4) + z); + } + + /// + /// Capture thread-safe read-only snapshot of chunk data. + /// + /// ChunkSnapshot. + public ChunkSnapshot getChunkSnapshot() + { + return getChunkSnapshot(false, false); + } + + /// + /// Capture thread-safe read-only snapshot of chunk data. + /// + /// If true, snapshot includes per-coordinate biome type. + /// If true, snapshot includes per-coordinate raw biome temperature and rainfall. + /// ChunkSnapshot. + public ChunkSnapshot getChunkSnapshot(bool includeBiome, bool includeBiomeTempRain) + { + // this has a lot of overhead + // (SYLV)todo: clean this up + int dimId = _world.getDimensionId(); + int[] blockIds = new int[16 * 128 * 16]; + int[] blockData = new int[16 * 128 * 16]; + int[] maxBlockY = new int[16 * 16]; + int[] skyLight = new int[16 * 128 * 16]; + int[] blockLight = new int[16 * 128 * 16]; + int[]? biomeIds = includeBiome ? new int[16 * 16] : null; + double[]? biomeTemp = includeBiomeTempRain ? new double[16 * 16] : null; + double[]? biomeRainfall = includeBiomeTempRain ? new double[16 * 16] : null; + + if (NativeBridge.GetChunkSnapshot != null) + { + var hIds = GCHandle.Alloc(blockIds, GCHandleType.Pinned); + var hData = GCHandle.Alloc(blockData, GCHandleType.Pinned); + var hMaxY = GCHandle.Alloc(maxBlockY, GCHandleType.Pinned); + try + { + NativeBridge.GetChunkSnapshot(dimId, _chunkX, _chunkZ, + hIds.AddrOfPinnedObject(), + hData.AddrOfPinnedObject(), + hMaxY.AddrOfPinnedObject()); + } + finally + { + hIds.Free(); + hData.Free(); + hMaxY.Free(); + } + } + else + { + for (int lx = 0; lx < 16; lx++) + { + for (int lz = 0; lz < 16; lz++) + { + int worldX = (_chunkX << 4) + lx; + int worldZ = (_chunkZ << 4) + lz; + int highest = 0; + for (int ly = 0; ly < 128; ly++) + { + int idx = (lx * 128 * 16) + (ly * 16) + lz; + if (NativeBridge.GetTileId != null) + blockIds[idx] = NativeBridge.GetTileId(dimId, worldX, ly, worldZ); + if (NativeBridge.GetTileData != null) + blockData[idx] = NativeBridge.GetTileData(dimId, worldX, ly, worldZ); + if (blockIds[idx] != 0) + highest = ly; + } + maxBlockY[lx * 16 + lz] = highest; + } + } + } + + for (int lx = 0; lx < 16; lx++) + { + for (int lz = 0; lz < 16; lz++) + { + int worldX = (_chunkX << 4) + lx; + int worldZ = (_chunkZ << 4) + lz; + for (int ly = 0; ly < 128; ly++) + { + int idx = (lx * 128 * 16) + (ly * 16) + lz; + if (NativeBridge.GetSkyLight != null) + skyLight[idx] = NativeBridge.GetSkyLight(dimId, worldX, ly, worldZ); + if (NativeBridge.GetBlockLight != null) + blockLight[idx] = NativeBridge.GetBlockLight(dimId, worldX, ly, worldZ); + } + if (includeBiome && NativeBridge.GetBiomeId != null) + { + int colIdx = lx * 16 + lz; + int biomeId = NativeBridge.GetBiomeId(dimId, worldX, worldZ); + biomeIds![colIdx] = biomeId; + if (includeBiomeTempRain) + { + var biome = Block.BiomeHelper.fromId(biomeId); + biomeTemp![colIdx] = biome.getTemperature(); + biomeRainfall![colIdx] = biome.getRainfall(); + } + } + } + } + + long captureTime = 0; + if (NativeBridge.GetWorldInfo != null) + { + double[] info = new double[7]; + var hInfo = GCHandle.Alloc(info, GCHandleType.Pinned); + try + { + NativeBridge.GetWorldInfo(dimId, hInfo.AddrOfPinnedObject()); + } + finally + { + hInfo.Free(); + } + captureTime = (long)info[4]; + } + + return new ChunkSnapshot(_chunkX, _chunkZ, _world.getName(), captureTime, + blockIds, blockData, maxBlockY, + skyLight, blockLight, biomeIds, biomeTemp, biomeRainfall); + } + + /// + /// Capture thread-safe read-only snapshot of chunk data. + /// + /// (NONFUNCTIONAL) Only here for parity. + /// If true, snapshot includes per-coordinate biome type. + /// If true, snapshot includes per-coordinate raw biome temperature and rainfall. + /// ChunkSnapshot. + public ChunkSnapshot getChunkSnapshot(bool includeMaxblocky, bool includeBiome, bool includeBiomeTempRain) + { + return getChunkSnapshot(includeBiome, includeBiomeTempRain); + } + + /// + /// Get a list of all entities in the chunk. + /// + /// The entities. + public Entity.Entity[] getEntities() + { + if (NativeBridge.GetChunkEntities == null) return Array.Empty(); + + int dimId = _world.getDimensionId(); + int count = NativeBridge.GetChunkEntities(dimId, _chunkX, _chunkZ, out IntPtr buf); + if (count <= 0 || buf == IntPtr.Zero) return Array.Empty(); + + var result = new Entity.Entity[count]; + try + { + int[] data = new int[count * 3]; + Marshal.Copy(buf, data, 0, count * 3); + + for (int i = 0; i < count; i++) + { + int entityId = data[i * 3]; + int mappedType = data[i * 3 + 1]; + int isLiving = data[i * 3 + 2]; + + var entityType = Enum.IsDefined(typeof(Entity.EntityType), mappedType) + ? (Entity.EntityType)mappedType + : Entity.EntityType.UNKNOWN; + + if (entityType == Entity.EntityType.PLAYER) + { + var player = FourKit.GetPlayerByEntityId(entityId); + if (player != null) + { + result[i] = player; + continue; + } + } + + if (isLiving == 1) + { + result[i] = new Entity.LivingEntity(entityId, entityType, dimId, 0, 0, 0); + } + else + { + var entity = new Entity.Entity(); + entity.SetEntityIdInternal(entityId); + entity.SetEntityTypeInternal(entityType); + entity.SetDimensionInternal(dimId); + result[i] = entity; + } + } + } + finally + { + Marshal.FreeCoTaskMem(buf); + } + + return result; + } + + /// + /// Checks if the chunk is loaded. + /// + /// True if it is loaded. + public bool isLoaded() + { + if (NativeBridge.IsChunkLoaded != null) + return NativeBridge.IsChunkLoaded(_world.getDimensionId(), _chunkX, _chunkZ) != 0; + return false; + } + + /// + /// Loads the chunk. + /// + /// Whether or not to generate a chunk if it doesn't already exist. + /// true if the chunk has loaded successfully, otherwise false. + public bool load(bool generate) + { + if (NativeBridge.LoadChunk != null) + return NativeBridge.LoadChunk(_world.getDimensionId(), _chunkX, _chunkZ, generate ? 1 : 0) != 0; + return false; + } + + /// + /// Loads the chunk. + /// + /// true if the chunk has loaded successfully, otherwise false. + public bool load() + { + return load(true); + } + + /// + /// Unloads and optionally saves the Chunk. + /// + /// Controls whether the chunk is saved. + /// Controls whether to unload the chunk when players are nearby. + /// true if the chunk has unloaded successfully, otherwise false. + public bool unload(bool save, bool safe) + { + if (NativeBridge.UnloadChunk != null) + return NativeBridge.UnloadChunk(_world.getDimensionId(), _chunkX, _chunkZ, save ? 1 : 0, safe ? 1 : 0) != 0; + return false; + } + + /// + /// Unloads and optionally saves the Chunk. + /// + /// Controls whether the chunk is saved. + /// true if the chunk has unloaded successfully, otherwise false. + public bool unload(bool save) + { + return unload(save, true); + } + + /// + /// Unloads and optionally saves the Chunk. + /// + /// true if the chunk has unloaded successfully, otherwise false. + public bool unload() + { + return unload(true, true); + } +} diff --git a/Minecraft.Server.FourKit/Chunk/ChunkSnapshot.cs b/Minecraft.Server.FourKit/Chunk/ChunkSnapshot.cs new file mode 100644 index 00000000..fc116925 --- /dev/null +++ b/Minecraft.Server.FourKit/Chunk/ChunkSnapshot.cs @@ -0,0 +1,209 @@ +namespace Minecraft.Server.FourKit.Chunk; + +using Minecraft.Server.FourKit.Block; + +/// +/// Represents a static, thread-safe snapshot of chunk of blocks. +/// Purpose is to allow clean, efficient copy of a chunk data to be made, and +/// then handed off for processing in another thread (e.g. map rendering). +/// +public class ChunkSnapshot +{ + private readonly int _chunkX; + private readonly int _chunkZ; + private readonly string _worldName; + private readonly long _captureFullTime; + private readonly int[] _blockIds; + private readonly int[] _blockData; + private readonly int[] _maxBlockY; + private readonly int[]? _skyLight; + private readonly int[]? _blockLight; + private readonly int[]? _biome; + private readonly double[]? _biomeTemp; + private readonly double[]? _biomeRainfall; + + internal ChunkSnapshot(int chunkX, int chunkZ, string worldName, long captureFullTime, + int[] blockIds, int[] blockData, int[] maxBlockY, + int[]? skyLight = null, int[]? blockLight = null, + int[]? biome = null, double[]? biomeTemp = null, double[]? biomeRainfall = null) + { + _chunkX = chunkX; + _chunkZ = chunkZ; + _worldName = worldName; + _captureFullTime = captureFullTime; + _blockIds = blockIds; + _blockData = blockData; + _maxBlockY = maxBlockY; + _skyLight = skyLight; + _blockLight = blockLight; + _biome = biome; + _biomeTemp = biomeTemp; + _biomeRainfall = biomeRainfall; + } + + /// + /// Gets the X-coordinate of this chunk. + /// + /// X-coordinate. + public int getX() => _chunkX; + + /// + /// Gets the Z-coordinate of this chunk. + /// + /// Z-coordinate. + public int getZ() => _chunkZ; + + /// + /// Gets name of the world containing this chunk. + /// + /// Parent World Name. + public string getWorldName() => _worldName; + + /// + /// Get block type for block at corresponding coordinate in the chunk. + /// + /// 0-15 + /// 0-127 + /// 0-15 + /// 0-255 + public int getBlockTypeId(int x, int y, int z) + { + int idx = (x * 128 * 16) + (y * 16) + z; + if (idx < 0 || idx >= _blockIds.Length) return 0; + return _blockIds[idx]; + } + + /// + /// Get block data for block at corresponding coordinate in the chunk. + /// + /// 0-15 + /// 0-127 + /// 0-15 + /// 0-15 + public int getBlockData(int x, int y, int z) + { + int idx = (x * 128 * 16) + (y * 16) + z; + if (idx < 0 || idx >= _blockData.Length) return 0; + return _blockData[idx]; + } + + /// + /// Gets the highest non-air coordinate at the given coordinates. + /// + /// X-coordinate of the blocks. + /// Z-coordinate of the blocks. + /// Y-coordinate of the highest non-air block. + public int getHighestBlockYAt(int x, int z) + { + int idx = x * 16 + z; + if (idx < 0 || idx >= _maxBlockY.Length) return 0; + return _maxBlockY[idx]; + } + + /// + /// Get world full time when chunk snapshot was captured. + /// + /// Time in ticks. + public long getCaptureFullTime() => _captureFullTime; + + /// + /// Test if section is empty. + /// + /// Section Y coordinate (block Y / 16). + /// true if empty, false if not. + public bool isSectionEmpty(int sy) + { + int startY = sy * 16; + int endY = startY + 16; + if (endY > 128) endY = 128; + for (int x = 0; x < 16; x++) + { + for (int z = 0; z < 16; z++) + { + for (int y = startY; y < endY; y++) + { + int idx = (x * 128 * 16) + (y * 16) + z; + if (idx >= 0 && idx < _blockIds.Length && _blockIds[idx] != 0) + return false; + } + } + } + return true; + } + + /// + /// Get sky light level for block at corresponding coordinate in the chunk. + /// + /// 0-15 + /// 0-127 + /// 0-15 + /// 0-15 + public int getBlockSkyLight(int x, int y, int z) + { + if (_skyLight == null) return 0; + int idx = (x * 128 * 16) + (y * 16) + z; + if (idx < 0 || idx >= _skyLight.Length) return 0; + return _skyLight[idx]; + } + + /// + /// Get light level emitted by block at corresponding coordinate in the chunk. + /// + /// 0-15 + /// 0-127 + /// 0-15 + /// 0-15 + public int getBlockEmittedLight(int x, int y, int z) + { + if (_blockLight == null) return 0; + int idx = (x * 128 * 16) + (y * 16) + z; + if (idx < 0 || idx >= _blockLight.Length) return 0; + return _blockLight[idx]; + } + + /// + /// Get biome at given coordinates. + /// + /// X-coordinate (0-15) + /// Z-coordinate (0-15) + /// Biome at given coordinate. + public Biome getBiome(int x, int z) + { + if (_biome == null) return Biome.PLAINS; + int idx = x * 16 + z; + if (idx < 0 || idx >= _biome.Length) return Biome.PLAINS; + return BiomeHelper.fromId(_biome[idx]); + } + + /// + /// Get raw biome temperature (0.0-1.0) at given coordinate. + /// + /// X-coordinate (0-15) + /// Z-coordinate (0-15) + /// Temperature at given coordinate. + public double getRawBiomeTemperature(int x, int z) + { + if (_biomeTemp != null) + { + int idx = x * 16 + z; + if (idx >= 0 && idx < _biomeTemp.Length) return _biomeTemp[idx]; + } + return getBiome(x, z).getTemperature(); + } + + /// + /// Get raw biome rainfall (0.0-1.0) at given coordinate. + /// + /// X-coordinate (0-15) + /// Z-coordinate (0-15) + /// Rainfall at given coordinate. + public double getRawBiomeRainfall(int x, int z) + { + if (_biomeRainfall != null) + { + int idx = x * 16 + z; + if (idx >= 0 && idx < _biomeRainfall.Length) return _biomeRainfall[idx]; + } + return getBiome(x, z).getRainfall(); + } +} diff --git a/Minecraft.Server.FourKit/Entity/HumanEntity.cs b/Minecraft.Server.FourKit/Entity/HumanEntity.cs index 497ce7bc..a74db061 100644 --- a/Minecraft.Server.FourKit/Entity/HumanEntity.cs +++ b/Minecraft.Server.FourKit/Entity/HumanEntity.cs @@ -11,7 +11,7 @@ public abstract class HumanEntity : LivingEntity, InventoryHolder private GameMode _gameMode = GameMode.SURVIVAL; private string _name = string.Empty; internal PlayerInventory _playerInventory = new(); - internal Inventory _enderChestInventory = new("Ender Chest", InventoryType.ENDER_CHEST, 27); + internal EnderChestInventory? _enderChestInventory; private ItemStack? _cursorItem; private bool _sleeping; private int _sleepTicks; @@ -59,6 +59,8 @@ public abstract class HumanEntity : LivingEntity, InventoryHolder /// The EnderChest of the player. public Inventory getEnderChest() { + // AAAAAH + _enderChestInventory ??= new EnderChestInventory(getEntityId()); return _enderChestInventory; } @@ -86,14 +88,44 @@ public abstract class HumanEntity : LivingEntity, InventoryHolder /// Will always be empty if the player currently has no open window. /// /// The ItemStack of the item you are currently moving around. - public ItemStack? getItemOnCursor() => _cursorItem; + public ItemStack? getItemOnCursor() + { + if (NativeBridge.GetCarriedItem != null) + { + int[] buf = new int[3]; + var gh = System.Runtime.InteropServices.GCHandle.Alloc(buf, System.Runtime.InteropServices.GCHandleType.Pinned); + try + { + NativeBridge.GetCarriedItem(getEntityId(), gh.AddrOfPinnedObject()); + } + finally + { + gh.Free(); + } + int id = buf[0]; + int aux = buf[1]; + int count = buf[2]; + if (id > 0 && count > 0) + _cursorItem = new ItemStack(id, count, (short)aux); + else + _cursorItem = null; + } + return _cursorItem; + } /// /// Sets the item to the given ItemStack, this will replace whatever the /// user was moving. Will always be empty if the player currently has no open window. /// /// The ItemStack which will end up in the hand. - public void setItemOnCursor(ItemStack? item) => _cursorItem = item; + public void setItemOnCursor(ItemStack? item) + { + _cursorItem = item; + NativeBridge.SetCarriedItem?.Invoke(getEntityId(), + item?.getTypeId() ?? 0, + item?.getAmount() ?? 0, + item?.getDurability() ?? 0); + } /// /// If the player currently has an inventory window open, this method will diff --git a/Minecraft.Server.FourKit/Event/World/ChunkEvent.cs b/Minecraft.Server.FourKit/Event/World/ChunkEvent.cs new file mode 100644 index 00000000..debc24b7 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/World/ChunkEvent.cs @@ -0,0 +1,22 @@ +namespace Minecraft.Server.FourKit.Event.World; + +using Minecraft.Server.FourKit.Chunk; + +/// +/// Represents a Chunk related event. +/// +public abstract class ChunkEvent : WorldEvent +{ + protected Chunk chunk; + + protected ChunkEvent(Chunk chunk) : base(chunk.getWorld()) + { + this.chunk = chunk; + } + + /// + /// Gets the chunk being loaded/unloaded. + /// + /// Chunk that triggered this event. + public Chunk getChunk() => chunk; +} diff --git a/Minecraft.Server.FourKit/Event/World/ChunkLoadEvent.cs b/Minecraft.Server.FourKit/Event/World/ChunkLoadEvent.cs new file mode 100644 index 00000000..14f72b15 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/World/ChunkLoadEvent.cs @@ -0,0 +1,23 @@ +namespace Minecraft.Server.FourKit.Event.World; + +using Minecraft.Server.FourKit.Chunk; + +/// +/// Called when a chunk is loaded. +/// +public class ChunkLoadEvent : ChunkEvent +{ + private readonly bool _newChunk; + + internal ChunkLoadEvent(Chunk chunk, bool newChunk) : base(chunk) + { + _newChunk = newChunk; + } + + /// + /// Gets if this chunk was newly created or not. Note that if this chunk is + /// new, it will not be populated at this time. + /// + /// true if the chunk is new, otherwise false. + public bool isNewChunk() => _newChunk; +} diff --git a/Minecraft.Server.FourKit/Event/World/ChunkUnloadEvent.cs b/Minecraft.Server.FourKit/Event/World/ChunkUnloadEvent.cs new file mode 100644 index 00000000..e6272e59 --- /dev/null +++ b/Minecraft.Server.FourKit/Event/World/ChunkUnloadEvent.cs @@ -0,0 +1,33 @@ +namespace Minecraft.Server.FourKit.Event.World; + +using Minecraft.Server.FourKit.Chunk; + +/// +/// Called when a chunk is unloaded. +/// +public class ChunkUnloadEvent : ChunkEvent, Cancellable +{ + private bool _cancel; + + internal ChunkUnloadEvent(Chunk chunk) : base(chunk) + { + _cancel = false; + } + + /// + /// Gets the cancellation state of this event. A cancelled event will not + /// be executed in the server, but will still pass to other plugins. + /// + /// true if this event is cancelled. + public bool isCancelled() => _cancel; + + /// + /// Sets the cancellation state of this event. A cancelled event will not + /// be executed in the server, but will still pass to other plugins. + /// + /// true if you wish to cancel this event. + public void setCancelled(bool cancel) + { + _cancel = cancel; + } +} diff --git a/Minecraft.Server.FourKit/EventDispatcher.cs b/Minecraft.Server.FourKit/EventDispatcher.cs index 9c5bc2c1..4938a541 100644 --- a/Minecraft.Server.FourKit/EventDispatcher.cs +++ b/Minecraft.Server.FourKit/EventDispatcher.cs @@ -22,60 +22,80 @@ internal sealed class EventDispatcher public int CompareTo(RegisteredHandler other) => Priority.CompareTo(other.Priority); } - private readonly Dictionary> _handlers = new(); - private readonly object _lock = new(); + // Snapshot-on-write: writers swap _handlers atomically; Fire reads it lock-free. + private volatile Dictionary _handlers = new(); + private readonly object _writeLock = new(); + + // Fired when an event type gains its first handler. + internal Action? OnSubscriptionChanged; + public void Register(Listener listener) { var methods = listener.GetType().GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - lock (_lock) + List<(Type eventType, RegisteredHandler handler)>? pending = null; + foreach (var method in methods) { - foreach (var method in methods) + var attr = method.GetCustomAttribute(); + if (attr == null) + continue; + + var parameters = method.GetParameters(); + if (parameters.Length != 1) { - var attr = method.GetCustomAttribute(); - if (attr == null) - continue; - - var parameters = method.GetParameters(); - if (parameters.Length != 1) - { - Console.WriteLine($"[FourKit] Warning: @EventHandler method {method.Name} must have exactly 1 parameter, skipping."); - continue; - } - - var eventType = parameters[0].ParameterType; - if (!typeof(Event.Event).IsAssignableFrom(eventType)) - { - Console.WriteLine($"[FourKit] Warning: @EventHandler method {method.Name} parameter must extend Event, skipping."); - continue; - } - - if (!_handlers.TryGetValue(eventType, out var list)) - { - list = new List(); - _handlers[eventType] = list; - } - - list.Add(new RegisteredHandler(listener, method, attr.Priority, attr.IgnoreCancelled)); - _handlers[eventType] = list.OrderBy(h => h.Priority).ToList(); + Console.WriteLine($"[FourKit] Warning: @EventHandler method {method.Name} must have exactly 1 parameter, skipping."); + continue; } + + var eventType = parameters[0].ParameterType; + if (!typeof(Event.Event).IsAssignableFrom(eventType)) + { + Console.WriteLine($"[FourKit] Warning: @EventHandler method {method.Name} parameter must extend Event, skipping."); + continue; + } + + pending ??= new List<(Type, RegisteredHandler)>(); + pending.Add((eventType, new RegisteredHandler(listener, method, attr.Priority, attr.IgnoreCancelled))); + } + + if (pending == null) return; + + HashSet newlySubscribed = new(); + lock (_writeLock) + { + var newDict = new Dictionary(_handlers); + foreach (var (eventType, handler) in pending) + { + bool hadAny = newDict.TryGetValue(eventType, out var existing); + existing ??= Array.Empty(); + + // OrderBy is stable; Array.Sort is not. + var combined = existing.Append(handler).OrderBy(h => h.Priority).ToArray(); + newDict[eventType] = combined; + + if (!hadAny) newlySubscribed.Add(eventType); + } + _handlers = newDict; + } + + if (OnSubscriptionChanged != null) + { + foreach (var t in newlySubscribed) + OnSubscriptionChanged(t); } } + public void Fire(Event.Event evt) { - List? handlers; - lock (_lock) - { - if (!_handlers.TryGetValue(evt.GetType(), out handlers)) - return; - - handlers = new List(handlers); - } + var snapshot = _handlers; + if (!snapshot.TryGetValue(evt.GetType(), out var handlers)) + return; var cancellable = evt as Cancellable; - foreach (var handler in handlers) + for (int i = 0; i < handlers.Length; i++) { + ref readonly var handler = ref handlers[i]; if (handler.IgnoreCancelled && cancellable != null && cancellable.isCancelled()) continue; @@ -89,4 +109,6 @@ internal sealed class EventDispatcher } } } + + internal bool IsSubscribed(Type eventType) => _handlers.ContainsKey(eventType); } diff --git a/Minecraft.Server.FourKit/FourKit.cs b/Minecraft.Server.FourKit/FourKit.cs index 7da6bcae..062e886b 100644 --- a/Minecraft.Server.FourKit/FourKit.cs +++ b/Minecraft.Server.FourKit/FourKit.cs @@ -11,11 +11,68 @@ using Minecraft.Server.FourKit.Plugin; /// public static class FourKit { - private static readonly EventDispatcher _dispatcher = new(); + private static readonly EventDispatcher _dispatcher; private static readonly Dictionary _players = new(StringComparer.OrdinalIgnoreCase); private static readonly Dictionary _playersByEntityId = new(); private static readonly object _playerLock = new(); + // Must match HandlerKind in FourKitNatives.h. + private enum HandlerKind + { + ChunkLoad = 0, + ChunkUnload = 1, + PlayerMove = 2, + } + + private static uint _handlerMask; + private static readonly object _handlerMaskLock = new(); + + static FourKit() + { + _dispatcher = new EventDispatcher(); + _dispatcher.OnSubscriptionChanged = OnEventSubscribed; + } + + private static HandlerKind? MapEventTypeToHandlerKind(Type eventType) + { + if (eventType == typeof(Event.World.ChunkLoadEvent)) return HandlerKind.ChunkLoad; + if (eventType == typeof(Event.World.ChunkUnloadEvent)) return HandlerKind.ChunkUnload; + if (eventType == typeof(Event.Player.PlayerMoveEvent)) return HandlerKind.PlayerMove; + return null; + } + + private static void OnEventSubscribed(Type eventType) + { + var kind = MapEventTypeToHandlerKind(eventType); + if (kind == null) return; + + lock (_handlerMaskLock) + { + uint newMask = _handlerMask | (1u << (int)kind.Value); + if (newMask == _handlerMask) return; + _handlerMask = newMask; + NativeBridge.SetHandlerMask?.Invoke(_handlerMask); + } + } + + internal static void ResyncHandlerMask() + { + lock (_handlerMaskLock) + { + NativeBridge.SetHandlerMask?.Invoke(_handlerMask); + } + } + + /// + /// Gets the current server tick count. Increments once per server tick + /// (~20 per second under nominal load). Useful for measuring TPS by + /// sampling the delta against wall clock time. + /// + public static int getServerTick() + { + return NativeBridge.GetServerTickCount?.Invoke() ?? 0; + } + internal const int MAX_CHAT_LENGTH = 123; private static readonly Dictionary _worldsByDimId = new(); diff --git a/Minecraft.Server.FourKit/FourKitHost.Callbacks.cs b/Minecraft.Server.FourKit/FourKitHost.Callbacks.cs index f4e17ed1..d035813a 100644 --- a/Minecraft.Server.FourKit/FourKitHost.Callbacks.cs +++ b/Minecraft.Server.FourKit/FourKitHost.Callbacks.cs @@ -58,11 +58,11 @@ public static partial class FourKitHost } [UnmanagedCallersOnly] - public static void SetInventoryCallbacks(IntPtr getPlayerInventory, IntPtr setPlayerInventorySlot, IntPtr getContainerContents, IntPtr setContainerSlot, IntPtr getContainerViewerEntityIds, IntPtr closeContainer, IntPtr openVirtualContainer, IntPtr getItemMeta, IntPtr setItemMeta, IntPtr setHeldItemSlot) + public static void SetInventoryCallbacks(IntPtr getPlayerInventory, IntPtr setPlayerInventorySlot, IntPtr getContainerContents, IntPtr setContainerSlot, IntPtr getContainerViewerEntityIds, IntPtr closeContainer, IntPtr openVirtualContainer, IntPtr getItemMeta, IntPtr setItemMeta, IntPtr setHeldItemSlot, IntPtr getCarriedItem, IntPtr setCarriedItem, IntPtr getEnderChestContents, IntPtr setEnderChestSlot) { try { - NativeBridge.SetInventoryCallbacks(getPlayerInventory, setPlayerInventorySlot, getContainerContents, setContainerSlot, getContainerViewerEntityIds, closeContainer, openVirtualContainer, getItemMeta, setItemMeta, setHeldItemSlot); + NativeBridge.SetInventoryCallbacks(getPlayerInventory, setPlayerInventorySlot, getContainerContents, setContainerSlot, getContainerViewerEntityIds, closeContainer, openVirtualContainer, getItemMeta, setItemMeta, setHeldItemSlot, getCarriedItem, setCarriedItem, getEnderChestContents, setEnderChestSlot); } catch (Exception ex) { @@ -121,4 +121,71 @@ public static partial class FourKitHost ServerLog.Error("fourkit", $"SetVehicleCallbacks error: {ex}"); } } + + [UnmanagedCallersOnly] + public static void SetChunkCallbacks(IntPtr isChunkLoaded, IntPtr loadChunk, IntPtr unloadChunk, IntPtr getLoadedChunks, IntPtr isChunkInUse, IntPtr getChunkSnapshot, IntPtr unloadChunkRequest, IntPtr regenerateChunk, IntPtr refreshChunk) + { + try + { + NativeBridge.SetChunkCallbacks(isChunkLoaded, loadChunk, unloadChunk, getLoadedChunks, isChunkInUse, getChunkSnapshot, unloadChunkRequest, regenerateChunk, refreshChunk); + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"SetChunkCallbacks error: {ex}"); + } + } + + [UnmanagedCallersOnly] + public static void SetBlockInfoCallbacks(IntPtr getSkyLight, IntPtr getBlockLight, IntPtr getBiomeId, IntPtr setBiomeId) + { + try + { + NativeBridge.SetBlockInfoCallbacks(getSkyLight, getBlockLight, getBiomeId, setBiomeId); + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"SetBlockInfoCallbacks error: {ex}"); + } + } + + [UnmanagedCallersOnly] + public static void SetWorldEntityCallbacks(IntPtr getWorldEntities, IntPtr getChunkEntities) + { + try + { + NativeBridge.SetWorldEntityCallbacks(getWorldEntities, getChunkEntities); + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"SetWorldEntityCallbacks error: {ex}"); + } + } + + [UnmanagedCallersOnly] + public static void SetSubscriptionCallbacks(IntPtr setHandlerMask) + { + try + { + NativeBridge.SetSubscriptionCallbacks(setHandlerMask); + // Flush the mask accumulated during plugin onEnable. + FourKit.ResyncHandlerMask(); + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"SetSubscriptionCallbacks error: {ex}"); + } + } + + [UnmanagedCallersOnly] + public static void SetServerCallbacks(IntPtr getServerTickCount) + { + try + { + NativeBridge.SetServerCallbacks(getServerTickCount); + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"SetServerCallbacks error: {ex}"); + } + } } diff --git a/Minecraft.Server.FourKit/FourKitHost.Events.cs b/Minecraft.Server.FourKit/FourKitHost.Events.cs index 08329ebd..f6c6ef25 100644 --- a/Minecraft.Server.FourKit/FourKitHost.Events.cs +++ b/Minecraft.Server.FourKit/FourKitHost.Events.cs @@ -1250,4 +1250,38 @@ public static partial class FourKitHost return 0; } } + + [UnmanagedCallersOnly] + public static void FireChunkLoad(int dimId, int chunkX, int chunkZ, int isNewChunk) + { + try + { + var world = FourKit.getWorld(dimId); + var chunk = new Chunk.Chunk(world, chunkX, chunkZ); + var evt = new Event.World.ChunkLoadEvent(chunk, isNewChunk != 0); + FourKit.FireEvent(evt); + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FireChunkLoad error: {ex}"); + } + } + + [UnmanagedCallersOnly] + public static int FireChunkUnload(int dimId, int chunkX, int chunkZ) + { + try + { + var world = FourKit.getWorld(dimId); + var chunk = new Chunk.Chunk(world, chunkX, chunkZ); + var evt = new Event.World.ChunkUnloadEvent(chunk); + FourKit.FireEvent(evt); + return evt.isCancelled() ? 1 : 0; + } + catch (Exception ex) + { + ServerLog.Error("fourkit", $"FireChunkUnload error: {ex}"); + return 0; + } + } } diff --git a/Minecraft.Server.FourKit/Inventory/EnderChestInventory.cs b/Minecraft.Server.FourKit/Inventory/EnderChestInventory.cs new file mode 100644 index 00000000..7d74f13f --- /dev/null +++ b/Minecraft.Server.FourKit/Inventory/EnderChestInventory.cs @@ -0,0 +1,85 @@ +namespace Minecraft.Server.FourKit.Inventory; + +using System.Runtime.InteropServices; + +// todo: this needs to be removed at some point + +internal class EnderChestInventory : Inventory +{ + private readonly int _ownerEntityId; + + internal EnderChestInventory(int ownerEntityId) + : base("Ender Chest", InventoryType.ENDER_CHEST, 27) + { + _ownerEntityId = ownerEntityId; + } + + protected internal override void EnsureSynced() + { + if (NativeBridge.GetEnderChestContents == null) + return; + + int[] buf = new int[27 * 3]; + var gh = GCHandle.Alloc(buf, GCHandleType.Pinned); + try + { + NativeBridge.GetEnderChestContents(_ownerEntityId, gh.AddrOfPinnedObject()); + } + finally + { + gh.Free(); + } + + for (int i = 0; i < 27; i++) + { + int id = buf[i * 3 + 0]; + int aux = buf[i * 3 + 1]; + int packed = buf[i * 3 + 2]; + + ushort count = (ushort)((packed >> 8) & 0xFFFF); + + _items[i]?.UnbindFromInventory(); + if (id > 0 && count > 0) + { + if (_items[i] == null) + { + _items[i] = new ItemStack(id, count, (short)aux); + } + else + { + _items[i]!.setTypeId(id); + _items[i]!.setAmount(count); + _items[i]!.setDurability((short)aux); + } + _items[i]!.BindToInventory(this, i); + } + else + { + _items[i] = null; + } + } + } + + public override void setItem(int index, ItemStack? item) + { + if (index >= 0 && index < _items.Length) + { + var old = _items[index]; + if (old != item) + { + old?.UnbindFromInventory(); + item?.BindToInventory(this, index); + } + _items[index] = item; + _slotModifiedByPlugin = true; + } + + if (NativeBridge.SetEnderChestSlot != null && index >= 0 && index < _items.Length) + { + int id = item?.getTypeId() ?? 0; + int count = item?.getAmount() ?? 0; + int aux = item?.getDurability() ?? 0; + NativeBridge.SetEnderChestSlot(_ownerEntityId, index, id, count, aux); + } + } +} diff --git a/Minecraft.Server.FourKit/Inventory/Inventory.cs b/Minecraft.Server.FourKit/Inventory/Inventory.cs index 52fda5fc..ad5b188e 100644 --- a/Minecraft.Server.FourKit/Inventory/Inventory.cs +++ b/Minecraft.Server.FourKit/Inventory/Inventory.cs @@ -174,7 +174,9 @@ public class Inventory : IEnumerable if (_items[slot] == null) { int added = Math.Min(64, remaining); - setItem(slot, new ItemStack(toAdd.getType(), added, toAdd.getDurability())); + var newItem = new ItemStack(toAdd.getType(), added, toAdd.getDurability()); + newItem.setItemMetaInternal(toAdd.getItemMetaInternal()?.clone()); + setItem(slot, newItem); remaining -= added; } } diff --git a/Minecraft.Server.FourKit/Inventory/Meta/ItemMeta.cs b/Minecraft.Server.FourKit/Inventory/Meta/ItemMeta.cs index 60ba224c..d9913a42 100644 --- a/Minecraft.Server.FourKit/Inventory/Meta/ItemMeta.cs +++ b/Minecraft.Server.FourKit/Inventory/Meta/ItemMeta.cs @@ -154,7 +154,7 @@ public class ItemMeta /// The enchantments to set. public void setEnchants(Dictionary? enchants) { - enchants = enchants != null ? new Dictionary(enchants) : null; + _enchants = enchants != null ? new Dictionary(enchants) : null; } diff --git a/Minecraft.Server.FourKit/Location.cs b/Minecraft.Server.FourKit/Location.cs index 6b8e314a..be513b67 100644 --- a/Minecraft.Server.FourKit/Location.cs +++ b/Minecraft.Server.FourKit/Location.cs @@ -186,6 +186,15 @@ public class Location public Location clone() => new Location(LocationWorld, X, Y, Z, Yaw, Pitch); + /// + /// Gets the chunk at the represented location. + /// + /// Chunk at the represented location. + public Chunk.Chunk getChunk() + { + return getWorld().getChunkAt(getBlockX() >> 4, getBlockZ() >> 4); + } + /// public override string ToString() => $"Location(world={LocationWorld.getName()}, x={X}, y={Y}, z={Z}, yaw={Yaw}, pitch={Pitch})"; } diff --git a/Minecraft.Server.FourKit/Minecraft.Server.FourKit.csproj b/Minecraft.Server.FourKit/Minecraft.Server.FourKit.csproj index ac1fff0a..3c12cecd 100644 --- a/Minecraft.Server.FourKit/Minecraft.Server.FourKit.csproj +++ b/Minecraft.Server.FourKit/Minecraft.Server.FourKit.csproj @@ -7,5 +7,7 @@ Minecraft.Server.FourKit true bin + true + true diff --git a/Minecraft.Server.FourKit/NativeBridge.cs b/Minecraft.Server.FourKit/NativeBridge.cs index 2a38c788..ec53e14d 100644 --- a/Minecraft.Server.FourKit/NativeBridge.cs +++ b/Minecraft.Server.FourKit/NativeBridge.cs @@ -43,10 +43,10 @@ internal static class NativeBridge internal delegate int NativeGetTileDataDelegate(int dimId, int x, int y, int z); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - internal delegate void NativeSetTileDelegate(int dimId, int x, int y, int z, int tileId, int data); + internal delegate void NativeSetTileDelegate(int dimId, int x, int y, int z, int tileId, int data, int flags); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - internal delegate void NativeSetTileDataDelegate(int dimId, int x, int y, int z, int data); + internal delegate void NativeSetTileDataDelegate(int dimId, int x, int y, int z, int data, int flags); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate int NativeBreakBlockDelegate(int dimId, int x, int y, int z); @@ -123,6 +123,18 @@ internal static class NativeBridge [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void NativeSetHeldItemSlotDelegate(int entityId, int slot); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void NativeGetCarriedItemDelegate(int entityId, IntPtr outData); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void NativeSetCarriedItemDelegate(int entityId, int itemId, int count, int aux); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void NativeGetEnderChestContentsDelegate(int entityId, IntPtr outData); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void NativeSetEnderChestSlotDelegate(int entityId, int slot, int itemId, int count, int aux); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void NativeSetSneakingDelegate(int entityId, int sneak); @@ -180,6 +192,58 @@ internal static class NativeBridge [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate void NativeGetEntityInfoDelegate(int entityId, IntPtr outBuf); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int NativeIsChunkLoadedDelegate(int dimId, int chunkX, int chunkZ); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int NativeLoadChunkDelegate(int dimId, int chunkX, int chunkZ, int generate); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int NativeUnloadChunkDelegate(int dimId, int chunkX, int chunkZ, int save, int safe); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int NativeGetLoadedChunksDelegate(int dimId, out IntPtr coordBuf); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int NativeIsChunkInUseDelegate(int dimId, int chunkX, int chunkZ); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void NativeGetChunkSnapshotDelegate(int dimId, int chunkX, int chunkZ, IntPtr blockIds, IntPtr blockData, IntPtr maxBlockY); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int NativeUnloadChunkRequestDelegate(int dimId, int chunkX, int chunkZ, int safe); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int NativeRegenerateChunkDelegate(int dimId, int chunkX, int chunkZ); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int NativeRefreshChunkDelegate(int dimId, int chunkX, int chunkZ); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int NativeGetWorldEntitiesDelegate(int dimId, out IntPtr outBuf); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int NativeGetChunkEntitiesDelegate(int dimId, int chunkX, int chunkZ, out IntPtr outBuf); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int NativeGetSkyLightDelegate(int dimId, int x, int y, int z); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int NativeGetBlockLightDelegate(int dimId, int x, int y, int z); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int NativeGetBiomeIdDelegate(int dimId, int x, int z); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void NativeSetBiomeIdDelegate(int dimId, int x, int z, int biomeId); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate void NativeSetHandlerMaskDelegate(uint mask); + internal static NativeSetHandlerMaskDelegate? SetHandlerMask; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int NativeGetServerTickCountDelegate(); + internal static NativeGetServerTickCountDelegate? GetServerTickCount; internal static NativeDamageDelegate? DamagePlayer; internal static NativeSetHealthDelegate? SetPlayerHealth; @@ -221,6 +285,10 @@ internal static class NativeBridge internal static NativeGetItemMetaDelegate? GetItemMeta; internal static NativeSetItemMetaDelegate? SetItemMeta; internal static NativeSetHeldItemSlotDelegate? SetHeldItemSlot; + internal static NativeGetCarriedItemDelegate? GetCarriedItem; + internal static NativeSetCarriedItemDelegate? SetCarriedItem; + internal static NativeGetEnderChestContentsDelegate? GetEnderChestContents; + internal static NativeSetEnderChestSlotDelegate? SetEnderChestSlot; internal static NativeSetSneakingDelegate? SetSneaking; internal static NativeSetVelocityDelegate? SetVelocity; internal static NativeSetAllowFlightDelegate? SetAllowFlight; @@ -240,6 +308,21 @@ internal static class NativeBridge internal static NativeGetVehicleIdDelegate? GetVehicleId; internal static NativeGetPassengerIdDelegate? GetPassengerId; internal static NativeGetEntityInfoDelegate? GetEntityInfo; + internal static NativeIsChunkLoadedDelegate? IsChunkLoaded; + internal static NativeLoadChunkDelegate? LoadChunk; + internal static NativeUnloadChunkDelegate? UnloadChunk; + internal static NativeGetLoadedChunksDelegate? GetLoadedChunks; + internal static NativeIsChunkInUseDelegate? IsChunkInUse; + internal static NativeGetChunkSnapshotDelegate? GetChunkSnapshot; + internal static NativeUnloadChunkRequestDelegate? UnloadChunkRequest; + internal static NativeRegenerateChunkDelegate? RegenerateChunk; + internal static NativeRefreshChunkDelegate? RefreshChunk; + internal static NativeGetWorldEntitiesDelegate? GetWorldEntities; + internal static NativeGetChunkEntitiesDelegate? GetChunkEntities; + internal static NativeGetSkyLightDelegate? GetSkyLight; + internal static NativeGetBlockLightDelegate? GetBlockLight; + internal static NativeGetBiomeIdDelegate? GetBiomeId; + internal static NativeSetBiomeIdDelegate? SetBiomeId; internal static void SetCallbacks(IntPtr damage, IntPtr setHealth, IntPtr teleport, IntPtr setGameMode, IntPtr broadcastMessage, IntPtr setFallDistance, IntPtr getPlayerSnapshot, IntPtr sendMessage, IntPtr setWalkSpeed, IntPtr teleportEntity) { @@ -286,7 +369,7 @@ internal static class NativeBridge SendRaw = Marshal.GetDelegateForFunctionPointer(sendRaw); } - internal static void SetInventoryCallbacks(IntPtr getPlayerInventory, IntPtr setPlayerInventorySlot, IntPtr getContainerContents, IntPtr setContainerSlot, IntPtr getContainerViewerEntityIds, IntPtr closeContainer, IntPtr openVirtualContainer, IntPtr getItemMeta, IntPtr setItemMeta, IntPtr setHeldItemSlot) + internal static void SetInventoryCallbacks(IntPtr getPlayerInventory, IntPtr setPlayerInventorySlot, IntPtr getContainerContents, IntPtr setContainerSlot, IntPtr getContainerViewerEntityIds, IntPtr closeContainer, IntPtr openVirtualContainer, IntPtr getItemMeta, IntPtr setItemMeta, IntPtr setHeldItemSlot, IntPtr getCarriedItem, IntPtr setCarriedItem, IntPtr getEnderChestContents, IntPtr setEnderChestSlot) { GetPlayerInventory = Marshal.GetDelegateForFunctionPointer(getPlayerInventory); SetPlayerInventorySlot = Marshal.GetDelegateForFunctionPointer(setPlayerInventorySlot); @@ -298,6 +381,10 @@ internal static class NativeBridge GetItemMeta = Marshal.GetDelegateForFunctionPointer(getItemMeta); SetItemMeta = Marshal.GetDelegateForFunctionPointer(setItemMeta); SetHeldItemSlot = Marshal.GetDelegateForFunctionPointer(setHeldItemSlot); + GetCarriedItem = Marshal.GetDelegateForFunctionPointer(getCarriedItem); + SetCarriedItem = Marshal.GetDelegateForFunctionPointer(setCarriedItem); + GetEnderChestContents = Marshal.GetDelegateForFunctionPointer(getEnderChestContents); + SetEnderChestSlot = Marshal.GetDelegateForFunctionPointer(setEnderChestSlot); } internal static void SetEntityCallbacks(IntPtr setSneaking, IntPtr setVelocity, IntPtr setAllowFlight, IntPtr playSound, IntPtr setSleepingIgnored) @@ -334,4 +421,41 @@ internal static class NativeBridge GetPassengerId = Marshal.GetDelegateForFunctionPointer(getPassengerId); GetEntityInfo = Marshal.GetDelegateForFunctionPointer(getEntityInfo); } + + internal static void SetChunkCallbacks(IntPtr isChunkLoaded, IntPtr loadChunk, IntPtr unloadChunk, IntPtr getLoadedChunks, IntPtr isChunkInUse, IntPtr getChunkSnapshot, IntPtr unloadChunkRequest, IntPtr regenerateChunk, IntPtr refreshChunk) + { + IsChunkLoaded = Marshal.GetDelegateForFunctionPointer(isChunkLoaded); + LoadChunk = Marshal.GetDelegateForFunctionPointer(loadChunk); + UnloadChunk = Marshal.GetDelegateForFunctionPointer(unloadChunk); + GetLoadedChunks = Marshal.GetDelegateForFunctionPointer(getLoadedChunks); + IsChunkInUse = Marshal.GetDelegateForFunctionPointer(isChunkInUse); + GetChunkSnapshot = Marshal.GetDelegateForFunctionPointer(getChunkSnapshot); + UnloadChunkRequest = Marshal.GetDelegateForFunctionPointer(unloadChunkRequest); + RegenerateChunk = Marshal.GetDelegateForFunctionPointer(regenerateChunk); + RefreshChunk = Marshal.GetDelegateForFunctionPointer(refreshChunk); + } + + internal static void SetWorldEntityCallbacks(IntPtr getWorldEntities, IntPtr getChunkEntities) + { + GetWorldEntities = Marshal.GetDelegateForFunctionPointer(getWorldEntities); + GetChunkEntities = Marshal.GetDelegateForFunctionPointer(getChunkEntities); + } + + internal static void SetBlockInfoCallbacks(IntPtr getSkyLight, IntPtr getBlockLight, IntPtr getBiomeId, IntPtr setBiomeId) + { + GetSkyLight = Marshal.GetDelegateForFunctionPointer(getSkyLight); + GetBlockLight = Marshal.GetDelegateForFunctionPointer(getBlockLight); + GetBiomeId = Marshal.GetDelegateForFunctionPointer(getBiomeId); + SetBiomeId = Marshal.GetDelegateForFunctionPointer(setBiomeId); + } + + internal static void SetSubscriptionCallbacks(IntPtr setHandlerMask) + { + SetHandlerMask = Marshal.GetDelegateForFunctionPointer(setHandlerMask); + } + + internal static void SetServerCallbacks(IntPtr getServerTickCount) + { + GetServerTickCount = Marshal.GetDelegateForFunctionPointer(getServerTickCount); + } } diff --git a/Minecraft.Server.FourKit/World.cs b/Minecraft.Server.FourKit/World.cs index 1d85fe9c..4abec4ca 100644 --- a/Minecraft.Server.FourKit/World.cs +++ b/Minecraft.Server.FourKit/World.cs @@ -1,4 +1,5 @@ using System.Runtime.InteropServices; +using Minecraft.Server.FourKit.Chunk; using Minecraft.Server.FourKit.Entity; using Minecraft.Server.FourKit.Inventory; @@ -245,6 +246,115 @@ public class World return result; } + /// + /// Get a list of all entities in this World. + /// + /// A list of all Entities currently residing in this world. + public List getEntities() + { + var result = new List(); + if (NativeBridge.GetWorldEntities == null) return result; + + int count = NativeBridge.GetWorldEntities(_dimensionId, out IntPtr buf); + if (count <= 0 || buf == IntPtr.Zero) return result; + + try + { + int[] data = new int[count * 3]; + Marshal.Copy(buf, data, 0, count * 3); + + for (int i = 0; i < count; i++) + { + int entityId = data[i * 3]; + int mappedType = data[i * 3 + 1]; + int isLiving = data[i * 3 + 2]; + + var entityType = Enum.IsDefined(typeof(Entity.EntityType), mappedType) + ? (Entity.EntityType)mappedType + : Entity.EntityType.UNKNOWN; + + if (entityType == Entity.EntityType.PLAYER) + { + var player = FourKit.GetPlayerByEntityId(entityId); + if (player != null) + { + result.Add(player); + continue; + } + } + + if (isLiving == 1) + { + result.Add(new Entity.LivingEntity(entityId, entityType, _dimensionId, 0, 0, 0)); + } + else + { + var entity = new Entity.Entity(); + entity.SetEntityIdInternal(entityId); + entity.SetEntityTypeInternal(entityType); + entity.SetDimensionInternal(_dimensionId); + result.Add(entity); + } + } + } + finally + { + Marshal.FreeCoTaskMem(buf); + } + + return result; + } + + /// + /// Get a list of all living entities in this World. + /// + /// A list of all LivingEntities currently residing in this world. + public List getLivingEntities() + { + var result = new List(); + if (NativeBridge.GetWorldEntities == null) return result; + + int count = NativeBridge.GetWorldEntities(_dimensionId, out IntPtr buf); + if (count <= 0 || buf == IntPtr.Zero) return result; + + try + { + int[] data = new int[count * 3]; + Marshal.Copy(buf, data, 0, count * 3); + + for (int i = 0; i < count; i++) + { + int entityId = data[i * 3]; + int mappedType = data[i * 3 + 1]; + int isLiving = data[i * 3 + 2]; + + if (isLiving != 1) continue; + + var entityType = Enum.IsDefined(typeof(Entity.EntityType), mappedType) + ? (Entity.EntityType)mappedType + : Entity.EntityType.UNKNOWN; + + if (entityType == Entity.EntityType.PLAYER) + { + var player = FourKit.GetPlayerByEntityId(entityId); + if (player != null) + { + result.Add(player); + continue; + } + } + + result.Add(new Entity.LivingEntity(entityId, entityType, _dimensionId, 0, 0, 0)); + } + } + finally + { + Marshal.FreeCoTaskMem(buf); + } + + return result; + } + /// /// Creates explosion at given coordinates with given power. /// @@ -374,4 +484,240 @@ public class World { NativeBridge.DropItem?.Invoke(_dimensionId, location.X, location.Y, location.Z, item.getTypeId(), item.getAmount(), item.getDurability(), 1); } + + /// + /// Gets the Chunk at the given coordinates. + /// + /// X-coordinate of the chunk. + /// Z-coordinate of the chunk. + /// Chunk at the given coordinates. + public Chunk.Chunk getChunkAt(int x, int z) + { + return new Chunk.Chunk(this, x, z); + } + + /// + /// Gets the Chunk at the given Location. + /// + /// Location of the chunk. + /// Chunk at the given location. + public Chunk.Chunk getChunkAt(Location location) + { + return getChunkAt(location.getBlockX() >> 4, location.getBlockZ() >> 4); + } + + /// + /// Gets the Chunk that contains the given Block. + /// + /// Block to get the containing chunk from. + /// The chunk that contains the given block. + public Chunk.Chunk getChunkAt(Block.Block block) + { + return getChunkAt(block.getX() >> 4, block.getZ() >> 4); + } + + /// + /// Checks if the specified Chunk is loaded. + /// + /// The chunk to check. + /// true if the chunk is loaded, otherwise false. + public bool isChunkLoaded(Chunk.Chunk chunk) + { + return isChunkLoaded(chunk.getX(), chunk.getZ()); + } + + /// + /// Checks if the Chunk at the specified coordinates is loaded. + /// + /// X-coordinate of the chunk. + /// Z-coordinate of the chunk. + /// true if the chunk is loaded, otherwise false. + public bool isChunkLoaded(int x, int z) + { + if (NativeBridge.IsChunkLoaded != null) + return NativeBridge.IsChunkLoaded(_dimensionId, x, z) != 0; + return false; + } + + /// + /// Gets an array of all loaded Chunks. + /// + /// Chunk[] containing all loaded chunks. + public Chunk.Chunk[] getLoadedChunks() + { + if (NativeBridge.GetLoadedChunks == null) + return Array.Empty(); + + int count = NativeBridge.GetLoadedChunks(_dimensionId, out IntPtr buf); + if (count <= 0 || buf == IntPtr.Zero) + return Array.Empty(); + + try + { + int[] coords = new int[count * 2]; + Marshal.Copy(buf, coords, 0, count * 2); + var chunks = new Chunk.Chunk[count]; + for (int i = 0; i < count; i++) + chunks[i] = new Chunk.Chunk(this, coords[i * 2], coords[i * 2 + 1]); + return chunks; + } + finally + { + Marshal.FreeCoTaskMem(buf); + } + } + + /// + /// Loads the specified Chunk. + /// + /// The chunk to load. + public void loadChunk(Chunk.Chunk chunk) + { + loadChunk(chunk.getX(), chunk.getZ()); + } + + /// + /// Loads the Chunk at the specified coordinates. + /// If the chunk does not exist, it will be generated. This method is + /// analogous to loadChunk(int, int, boolean) where generate is true. + /// + /// X-coordinate of the chunk. + /// Z-coordinate of the chunk. + public void loadChunk(int x, int z) + { + loadChunk(x, z, true); + } + + /// + /// Loads the Chunk at the specified coordinates. + /// + /// X-coordinate of the chunk. + /// Z-coordinate of the chunk. + /// Whether or not to generate a chunk if it doesn't already exist. + /// true if the chunk has loaded successfully, otherwise false. + public bool loadChunk(int x, int z, bool generate) + { + if (NativeBridge.LoadChunk != null) + return NativeBridge.LoadChunk(_dimensionId, x, z, generate ? 1 : 0) != 0; + return false; + } + + /// + /// Checks if the Chunk at the specified coordinates is loaded and in use + /// by one or more players. + /// + /// X-coordinate of the chunk. + /// Z-coordinate of the chunk. + /// true if the chunk is loaded and in use by one or more players, otherwise false. + public bool isChunkInUse(int x, int z) + { + if (NativeBridge.IsChunkInUse != null) + return NativeBridge.IsChunkInUse(_dimensionId, x, z) != 0; + return false; + } + + /// + /// Safely unloads and saves the Chunk at the specified coordinates. + /// This method is analogous to unloadChunk(int, int, boolean, boolean) + /// where safe and save is true. + /// + /// The chunk to unload. + /// true if the chunk has unloaded successfully, otherwise false. + public bool unloadChunk(Chunk.Chunk chunk) + { + return unloadChunk(chunk.getX(), chunk.getZ()); + } + + /// + /// Safely unloads and saves the Chunk at the specified coordinates. + /// This method is analogous to unloadChunk(int, int, boolean, boolean) + /// where safe and save is true. + /// + /// X-coordinate of the chunk. + /// Z-coordinate of the chunk. + /// true if the chunk has unloaded successfully, otherwise false. + public bool unloadChunk(int x, int z) + { + return unloadChunk(x, z, true, true); + } + + /// + /// Safely unloads and optionally saves the Chunk at the specified coordinates. + /// + /// X-coordinate of the chunk. + /// Z-coordinate of the chunk. + /// Whether or not to save the chunk. + /// true if the chunk has unloaded successfully, otherwise false. + public bool unloadChunk(int x, int z, bool save) + { + return unloadChunk(x, z, save, true); + } + + /// + /// Unloads and optionally saves the Chunk at the specified coordinates. + /// + /// X-coordinate of the chunk. + /// Z-coordinate of the chunk. + /// Controls whether the chunk is saved. + /// Controls whether to unload the chunk when players are nearby. + /// true if the chunk has unloaded successfully, otherwise false. + public bool unloadChunk(int x, int z, bool save, bool safe) + { + if (NativeBridge.UnloadChunk != null) + return NativeBridge.UnloadChunk(_dimensionId, x, z, save ? 1 : 0, safe ? 1 : 0) != 0; + return false; + } + + /// + /// Safely queues the Chunk at the specified coordinates for unloading. + /// This method is analogous to unloadChunkRequest(int, int, boolean) + /// where safe is true. + /// + /// X-coordinate of the chunk. + /// Z-coordinate of the chunk. + /// true is the queue attempt was successful, otherwise false. + public bool unloadChunkRequest(int x, int z) + { + return unloadChunkRequest(x, z, true); + } + + /// + /// Queues the Chunk at the specified coordinates for unloading. + /// + /// X-coordinate of the chunk. + /// Z-coordinate of the chunk. + /// Controls whether to queue the chunk when players are nearby. + /// Whether the chunk was actually queued. + public bool unloadChunkRequest(int x, int z, bool safe) + { + if (NativeBridge.UnloadChunkRequest != null) + return NativeBridge.UnloadChunkRequest(_dimensionId, x, z, safe ? 1 : 0) != 0; + return false; + } + + /// + /// Regenerates the Chunk at the specified coordinates. + /// + /// X-coordinate of the chunk. + /// Z-coordinate of the chunk. + /// Whether the chunk was actually regenerated. + public bool regenerateChunk(int x, int z) + { + if (NativeBridge.RegenerateChunk != null) + return NativeBridge.RegenerateChunk(_dimensionId, x, z) != 0; + return false; + } + + /// + /// Resends the Chunk to all clients. + /// + /// X-coordinate of the chunk. + /// Z-coordinate of the chunk. + /// Whether the chunk was actually refreshed. + public bool refreshChunk(int x, int z) + { + if (NativeBridge.RefreshChunk != null) + return NativeBridge.RefreshChunk(_dimensionId, x, z) != 0; + return false; + } } diff --git a/Minecraft.Server.FourKit/docs/usage-of-all-events.md b/Minecraft.Server.FourKit/docs/usage-of-all-events.md index 66c27212..234ede22 100644 --- a/Minecraft.Server.FourKit/docs/usage-of-all-events.md +++ b/Minecraft.Server.FourKit/docs/usage-of-all-events.md @@ -1132,4 +1132,60 @@ public void onClick(InventoryClickEvent e) > **Cancellable:** Yes + +--- + +@section chunk_events Chunk Events + +@subsection chunkloadevent ChunkLoadEvent + +\ref Minecraft.Server.FourKit.Event.World.ChunkLoadEvent "ChunkLoadEvent" is fired when a chunk is loaded. If the chunk is newly generated it will not yet be populated when this event fires. + +```csharp +[EventHandler] +public void onChunkLoad(ChunkLoadEvent e) +{ + if (e.isNewChunk()) + { + Console.WriteLine($"New chunk generated at {e.getChunk().getX()}, {e.getChunk().getZ()}"); + } +} +``` + +| Method | Description | +|--------|-------------| +| `getChunk()` | The `Chunk` that was loaded. | +| `isNewChunk()` | True if this chunk was newly generated. Note that new chunks will not yet be populated at this time. | + +> **Cancellable:** No + +--- + +@subsection chunkunloadevent ChunkUnloadEvent + +\ref Minecraft.Server.FourKit.Event.World.ChunkUnloadEvent "ChunkUnloadEvent" is fired when a chunk is about to be unloaded. You can cancel it to prevent the chunk from being unloaded. + +```csharp +[EventHandler] +public void onChunkUnload(ChunkUnloadEvent e) +{ + // keep chunks near world origin loaded + Chunk chunk = e.getChunk(); + if (Math.Abs(chunk.getX()) <= 2 && Math.Abs(chunk.getZ()) <= 2) + { + e.setCancelled(true); + } +} +``` + +| Method | Description | +|--------|-------------| +| `getChunk()` | The `Chunk` that is about to be unloaded. | +| `isCancelled()` | Whether the unload is cancelled. | +| `setCancelled(bool)` | Cancel or allow the chunk unload. | + +> **Cancellable:** Yes + +--- +

Page currently under construction

\ No newline at end of file diff --git a/Minecraft.Server/FourKitBridge.cpp b/Minecraft.Server/FourKitBridge.cpp index 4d1fab80..dfe90c4b 100644 --- a/Minecraft.Server/FourKitBridge.cpp +++ b/Minecraft.Server/FourKitBridge.cpp @@ -62,7 +62,7 @@ typedef void(__stdcall *fn_set_player_connection_callbacks)(void *sendRaw); typedef long long(__stdcall *fn_fire_player_drop_item)(int entityId, int itemId, int itemCount, int itemAux, int *outItemId, int *outItemCount, int *outItemAux); -typedef void(__stdcall *fn_set_inventory_callbacks)(void *getPlayerInventory, void *setPlayerInventorySlot, void *getContainerContents, void *setContainerSlot, void *getContainerViewerEntityIds, void *closeContainer, void *openVirtualContainer, void *getItemMeta, void *setItemMeta, void *setHeldItemSlot); +typedef void(__stdcall *fn_set_inventory_callbacks)(void *getPlayerInventory, void *setPlayerInventorySlot, void *getContainerContents, void *setContainerSlot, void *getContainerViewerEntityIds, void *closeContainer, void *openVirtualContainer, void *getItemMeta, void *setItemMeta, void *setHeldItemSlot, void *getCarriedItem, void *setCarriedItem, void *getEnderChestContents, void *setEnderChestSlot); typedef int(__stdcall *fn_fire_player_interact)(int entityId, int action, int itemId, int itemCount, int itemAux, int clickedX, int clickedY, int clickedZ, @@ -103,6 +103,13 @@ typedef int(__stdcall *fn_fire_piston_extend)(int dimId, int x, int y, int z, in typedef int(__stdcall *fn_fire_piston_retract)(int dimId, int x, int y, int z, int direction); typedef int(__stdcall *fn_fire_command_preprocess)(int entityId, const char *cmdUtf8, int cmdByteLen, char *outBuf, int outBufSize, int *outLen); typedef int(__stdcall *fn_fire_block_from_to)(int dimId, int fromX, int fromY, int fromZ, int toX, int toY, int toZ, int face); +typedef void(__stdcall *fn_set_chunk_callbacks)(void *isChunkLoaded, void *loadChunk, void *unloadChunk, void *getLoadedChunks, void *isChunkInUse, void *getChunkSnapshot, void *unloadChunkRequest, void *regenerateChunk, void *refreshChunk); +typedef void(__stdcall *fn_set_block_info_callbacks)(void *getSkyLight, void *getBlockLight, void *getBiomeId, void *setBiomeId); +typedef void(__stdcall *fn_set_world_entity_callbacks)(void *getWorldEntities, void *getChunkEntities); +typedef void(__stdcall *fn_set_subscription_callbacks)(void *setHandlerMask); +typedef void(__stdcall *fn_set_server_callbacks)(void *getServerTickCount); +typedef void(__stdcall *fn_fire_chunk_load)(int dimId, int chunkX, int chunkZ, int isNewChunk); +typedef int(__stdcall *fn_fire_chunk_unload)(int dimId, int chunkX, int chunkZ); struct OpenContainerInfo { @@ -160,6 +167,13 @@ static fn_fire_piston_extend s_managedFirePistonExtend = nullptr; static fn_fire_piston_retract s_managedFirePistonRetract = nullptr; static fn_fire_command_preprocess s_managedFireCommandPreprocess = nullptr; static fn_fire_block_from_to s_managedFireBlockFromTo = nullptr; +static fn_set_chunk_callbacks s_managedSetChunkCallbacks = nullptr; +static fn_set_block_info_callbacks s_managedSetBlockInfoCallbacks = nullptr; +static fn_set_world_entity_callbacks s_managedSetWorldEntityCallbacks = nullptr; +static fn_set_subscription_callbacks s_managedSetSubscriptionCallbacks = nullptr; +static fn_set_server_callbacks s_managedSetServerCallbacks = nullptr; +static fn_fire_chunk_load s_managedFireChunkLoad = nullptr; +static fn_fire_chunk_unload s_managedFireChunkUnload = nullptr; static bool s_initialized = false; @@ -242,6 +256,13 @@ void Initialize() {L"FirePistonRetract", (void **)&s_managedFirePistonRetract}, {L"FireCommandPreprocess", (void **)&s_managedFireCommandPreprocess}, {L"FireBlockFromTo", (void **)&s_managedFireBlockFromTo}, + {L"SetChunkCallbacks", (void **)&s_managedSetChunkCallbacks}, + {L"SetBlockInfoCallbacks", (void **)&s_managedSetBlockInfoCallbacks}, + {L"SetWorldEntityCallbacks", (void **)&s_managedSetWorldEntityCallbacks}, + {L"SetSubscriptionCallbacks", (void **)&s_managedSetSubscriptionCallbacks}, + {L"SetServerCallbacks", (void **)&s_managedSetServerCallbacks}, + {L"FireChunkLoad", (void **)&s_managedFireChunkLoad}, + {L"FireChunkUnload", (void **)&s_managedFireChunkUnload}, }; bool ok = true; @@ -307,7 +328,11 @@ void Initialize() (void *)&NativeOpenVirtualContainer, (void *)&NativeGetItemMeta, (void *)&NativeSetItemMeta, - (void *)&NativeSetHeldItemSlot); + (void *)&NativeSetHeldItemSlot, + (void *)&NativeGetCarriedItem, + (void *)&NativeSetCarriedItem, + (void *)&NativeGetEnderChestContents, + (void *)&NativeSetEnderChestSlot); s_managedSetEntityCallbacks( (void *)&NativeSetSneaking, @@ -336,6 +361,33 @@ void Initialize() (void *)&NativeGetPassengerId, (void *)&NativeGetEntityInfo); + s_managedSetChunkCallbacks( + (void *)&NativeIsChunkLoaded, + (void *)&NativeLoadChunk, + (void *)&NativeUnloadChunk, + (void *)&NativeGetLoadedChunks, + (void *)&NativeIsChunkInUse, + (void *)&NativeGetChunkSnapshot, + (void *)&NativeUnloadChunkRequest, + (void *)&NativeRegenerateChunk, + (void *)&NativeRefreshChunk); + + s_managedSetBlockInfoCallbacks( + (void *)&NativeGetSkyLight, + (void *)&NativeGetBlockLight, + (void *)&NativeGetBiomeId, + (void *)&NativeSetBiomeId); + + s_managedSetWorldEntityCallbacks( + (void *)&NativeGetWorldEntities, + (void *)&NativeGetChunkEntities); + + s_managedSetSubscriptionCallbacks( + (void *)&NativeSetHandlerMask); + + s_managedSetServerCallbacks( + (void *)&NativeGetServerTickCount); + LogInfo("fourkit", "FourKit initialized successfully."); } @@ -481,8 +533,12 @@ bool FirePlayerMove(int entityId, double toX, double toY, double toZ, double *outToX, double *outToY, double *outToZ) { - if (!s_initialized || !s_managedFireMove) + // Caller reads outTo* unconditionally; init on every early-return. + if (!s_initialized || !s_managedFireMove || !HasHandlers(kHandlerKind_PlayerMove)) { + *outToX = toX; + *outToY = toY; + *outToZ = toZ; return false; } @@ -1014,4 +1070,22 @@ bool FireBlockFromTo(int dimId, int fromX, int fromY, int fromZ, int toX, int to return false; return s_managedFireBlockFromTo(dimId, fromX, fromY, fromZ, toX, toY, toZ, face) != 0; } + +void FireChunkLoad(int dimId, int chunkX, int chunkZ, bool isNewChunk) +{ + if (!s_initialized || !s_managedFireChunkLoad) + return; + if (!HasHandlers(kHandlerKind_ChunkLoad)) + return; + s_managedFireChunkLoad(dimId, chunkX, chunkZ, isNewChunk ? 1 : 0); +} + +bool FireChunkUnload(int dimId, int chunkX, int chunkZ) +{ + if (!s_initialized || !s_managedFireChunkUnload) + return false; + if (!HasHandlers(kHandlerKind_ChunkUnload)) + return false; + return s_managedFireChunkUnload(dimId, chunkX, chunkZ) != 0; +} } // namespace FourKitBridge diff --git a/Minecraft.Server/FourKitBridge.h b/Minecraft.Server/FourKitBridge.h index 3349cc64..3623c330 100644 --- a/Minecraft.Server/FourKitBridge.h +++ b/Minecraft.Server/FourKitBridge.h @@ -105,6 +105,8 @@ namespace FourKitBridge bool FirePistonRetract(int dimId, int x, int y, int z, int direction); bool FireCommandPreprocess(int entityId, const std::wstring &commandLine, std::wstring &outCommand); bool FireBlockFromTo(int dimId, int fromX, int fromY, int fromZ, int toX, int toY, int toZ, int face); + void FireChunkLoad(int dimId, int chunkX, int chunkZ, bool isNewChunk); + bool FireChunkUnload(int dimId, int chunkX, int chunkZ); #else // Standalone build: every hook is an inline no-op. Cancellable hooks // return false so vanilla code paths run unmodified, AND every out @@ -211,5 +213,7 @@ namespace FourKitBridge inline bool FirePistonRetract(int, int, int, int, int) { return false; } inline bool FireCommandPreprocess(int, const std::wstring &commandLine, std::wstring &outCommand) { outCommand = commandLine; return false; } inline bool FireBlockFromTo(int, int, int, int, int, int, int, int) { return false; } + inline void FireChunkLoad(int, int, int, bool) {} + inline bool FireChunkUnload(int, int, int) { return false; } #endif } diff --git a/Minecraft.Server/FourKitNatives.cpp b/Minecraft.Server/FourKitNatives.cpp index 758aea7e..e19a1423 100644 --- a/Minecraft.Server/FourKitNatives.cpp +++ b/Minecraft.Server/FourKitNatives.cpp @@ -3,6 +3,7 @@ #include "Common/StringUtils.h" #include "stdafx.h" +#include #include #include @@ -13,6 +14,10 @@ #include "../Minecraft.Client/ServerLevel.h" #include "../Minecraft.Client/ServerPlayer.h" #include "../Minecraft.Client/ServerPlayerGameMode.h" +#include "../Minecraft.Client/ServerChunkCache.h" +#include "../Minecraft.World/LevelChunk.h" +#include "../Minecraft.World/Biome.h" +#include "../Minecraft.World/LightLayer.h" #include "../Minecraft.Client/Windows64/Network/WinsockNetLayer.h" #include "../Minecraft.World/AbstractContainerMenu.h" #include "../Minecraft.World/AddGlobalEntityPacket.h" @@ -29,6 +34,7 @@ #include "../Minecraft.World/Player.h" #include "../Minecraft.World/PlayerAbilitiesPacket.h" #include "../Minecraft.World/SetCarriedItemPacket.h" +#include "../Minecraft.World/BlockRegionUpdatePacket.h" #include "../Minecraft.World/SetExperiencePacket.h" #include "../Minecraft.World/SetHealthPacket.h" #include "../Minecraft.World/LevelSoundPacket.h" @@ -47,6 +53,8 @@ namespace { +std::atomic g_handlerMask{0}; + static shared_ptr FindPlayer(int entityId) { PlayerList *list = MinecraftServer::getPlayerList(); @@ -105,6 +113,24 @@ class VirtualContainer : public SimpleContainer namespace FourKitBridge { + +void __cdecl NativeSetHandlerMask(uint32_t mask) +{ + g_handlerMask.store(mask, std::memory_order_release); +} + +bool HasHandlers(int kind) +{ + if (kind < 0 || kind >= 32) return false; + return (g_handlerMask.load(std::memory_order_acquire) & (1u << kind)) != 0; +} + +int __cdecl NativeGetServerTickCount() +{ + MinecraftServer *srv = MinecraftServer::getInstance(); + return srv ? srv->tickCount : 0; +} + void __cdecl NativeDamagePlayer(int entityId, float amount) { auto player = FindPlayer(entityId); @@ -305,20 +331,20 @@ int __cdecl NativeGetTileData(int dimId, int x, int y, int z) return level->getData(x, y, z); } -void __cdecl NativeSetTile(int dimId, int x, int y, int z, int tileId, int data) +void __cdecl NativeSetTile(int dimId, int x, int y, int z, int tileId, int data, int flags) { ServerLevel *level = GetLevel(dimId); if (!level) return; - level->setTileAndData(x, y, z, tileId, data, Tile::UPDATE_ALL); + level->setTileAndData(x, y, z, tileId, data, flags); } -void __cdecl NativeSetTileData(int dimId, int x, int y, int z, int data) +void __cdecl NativeSetTileData(int dimId, int x, int y, int z, int data, int flags) { ServerLevel *level = GetLevel(dimId); if (!level) return; - level->setData(x, y, z, data, Tile::UPDATE_ALL); + level->setData(x, y, z, data, flags); } int __cdecl NativeBreakBlock(int dimId, int x, int y, int z) @@ -634,6 +660,9 @@ int __cdecl NativeSendRaw(int entityId, unsigned char *bufferData, int bufferSiz void WriteInventoryItemData(std::shared_ptr item, int index, int* outBuffer) { if (item) { + //ItemFlags Key: + // 0x1 = hasMetadata (has data that needs to be gotten from "ReadMetaFromNative") + uint8_t itemFlags = 0; if (item->getTag() == nullptr) goto doneWithMetadataFlag; CompoundTag* itemTag = item->getTag(); @@ -642,7 +671,7 @@ void WriteInventoryItemData(std::shared_ptr item, int index, int* itemFlags |= 0x1; goto doneWithMetadataFlag; } - else { + else { //we just want to check one tag for metadata and return for this flag, not all of them CompoundTag* displayTag = itemTag->getCompound(L"display"); if (displayTag->contains(L"Name") || displayTag->contains(L"Lore")) { itemFlags |= 0x1; @@ -650,7 +679,9 @@ void WriteInventoryItemData(std::shared_ptr item, int index, int* } } + doneWithMetadataFlag: + outBuffer[(index * 3) + 0] = item->id; outBuffer[(index * 3) + 1] = item->getAuxValue(); outBuffer[(index * 3) + 2] = (((int)itemFlags << 24) | ((int)item->count << 8)); @@ -659,6 +690,9 @@ void WriteInventoryItemData(std::shared_ptr item, int index, int* void __cdecl NativeGetPlayerInventory(int entityId, int *outData) { + // 9 slots per row, 3 slots in the inventory and the hotbar, 4 armor slots, 1 hand slot + // (((slotsPerRow * Rows) + ArmorSlots) * AmountOfIntsPerSlot) + hand slot + // (((9 * 4) + 4) * 3) + 1 = 121 memset(outData, 0, 121 * sizeof(int)); auto player = FindPlayer(entityId); @@ -799,7 +833,8 @@ void __cdecl NativeOpenVirtualContainer(int entityId, int nativeType, const char player->openContainer(container); } - +//didnt update this for enchants +// [nameLen:int32][nameUTF8:bytes][loreCount:int32][lore0Len:int32][lore0UTF8:bytes] int __cdecl NativeGetItemMeta(int entityId, int slot, char *outBuf, int bufSize) { auto player = FindPlayer(entityId); @@ -815,15 +850,14 @@ int __cdecl NativeGetItemMeta(int entityId, int slot, char *outBuf, int bufSize) return 0; CompoundTag *tag = item->getTag(); - if (!tag || !tag->contains(L"display")) - return 0; - CompoundTag *display = tag->getCompound(L"display"); - bool hasName = display->contains(L"Name"); - bool hasLore = display->contains(L"Lore"); bool hasEnchantments = item->isEnchanted(); - if (!hasName && !hasLore) + CompoundTag *display = (tag && tag->contains(L"display")) ? tag->getCompound(L"display") : nullptr; + bool hasName = display && display->contains(L"Name"); + bool hasLore = display && display->contains(L"Lore"); + + if (!hasName && !hasLore && !hasEnchantments) return 0; int offset = 0; @@ -879,14 +913,18 @@ int __cdecl NativeGetItemMeta(int entityId, int slot, char *outBuf, int bufSize) ListTag* list = item->getEnchantmentTags(); if (list != nullptr) { int listSize = list->size(); + if ((offset + 4 + (listSize * (4 + 4))) > bufSize) return 0; + memcpy(outBuf + offset, &listSize, 4); offset += 4; for (int i = 0; i < listSize; i++) { int type = list->get(i)->getShort((wchar_t*)ItemInstance::TAG_ENCH_ID); int level = list->get(i)->getShort((wchar_t*)ItemInstance::TAG_ENCH_LEVEL); + memcpy(outBuf + offset, &type, 4); offset += 4; + memcpy(outBuf + offset, &level, 4); offset += 4; } @@ -933,8 +971,11 @@ void __cdecl NativeSetItemMeta(int entityId, int slot, const char *inBuf, int bu item->setTag(nullptr); } } + if (tag && tag->contains(L"ench")) + { tag->remove(L"ench"); + } } return; } @@ -1014,12 +1055,15 @@ void __cdecl NativeSetItemMeta(int entityId, int slot, const char *inBuf, int bu for (int i = 0; i < enchantCount; i++) { if (offset + (4 + 4) > bufSize) break; + int type = 0; memcpy(&type, inBuf + offset, 4); offset += 4; + int level = 0; memcpy(&level, inBuf + offset, 4); offset += 4; + CompoundTag* ench = new CompoundTag(); ench->putShort((wchar_t*)ItemInstance::TAG_ENCH_ID, static_cast(type)); ench->putShort((wchar_t*)ItemInstance::TAG_ENCH_LEVEL, static_cast(level)); @@ -1032,7 +1076,9 @@ void __cdecl NativeSetItemMeta(int entityId, int slot, const char *inBuf, int bu { CompoundTag* tag = item->getTag(); if (tag && tag->contains(L"ench")) + { tag->remove(L"ench"); + } } } } @@ -1047,6 +1093,68 @@ void __cdecl NativeSetHeldItemSlot(int entityId, int slot) player->connection->queueSend(std::make_shared(slot)); } +void __cdecl NativeGetCarriedItem(int entityId, int *outData) +{ + outData[0] = 0; + outData[1] = 0; + outData[2] = 0; + auto player = FindPlayer(entityId); + if (!player || !player->inventory) + return; + auto item = player->inventory->getCarried(); + if (item) + { + outData[0] = item->id; + outData[1] = item->getAuxValue(); + outData[2] = (int)item->count; + } +} + +void __cdecl NativeSetCarriedItem(int entityId, int itemId, int count, int aux) +{ + auto player = FindPlayer(entityId); + if (!player || !player->inventory) + return; + if (itemId <= 0 || count <= 0) + player->inventory->setCarried(nullptr); + else + player->inventory->setCarried(std::make_shared(itemId, count, aux)); +} + +void __cdecl NativeGetEnderChestContents(int entityId, int *outData) +{ + memset(outData, 0, 27 * 3 * sizeof(int)); + auto player = FindPlayer(entityId); + if (!player) + return; + auto ec = player->getEnderChestInventory(); + if (!ec) + return; + unsigned int size = ec->getContainerSize(); + if (size > 27) + size = 27; + for (unsigned int i = 0; i < size; i++) + { + WriteInventoryItemData(ec->getItem(i), i, outData); + } +} + +void __cdecl NativeSetEnderChestSlot(int entityId, int slot, int itemId, int count, int aux) +{ + auto player = FindPlayer(entityId); + if (!player) + return; + auto ec = player->getEnderChestInventory(); + if (!ec) + return; + if (slot < 0 || slot >= (int)ec->getContainerSize()) + return; + if (itemId <= 0 || count <= 0) + ec->setItem(slot, nullptr); + else + ec->setItem(slot, std::make_shared(itemId, count, aux)); +} + void __cdecl NativeSetSneaking(int entityId, int sneak) { auto player = FindPlayer(entityId); @@ -1169,7 +1277,6 @@ void __cdecl NativeSetExhaustion(int entityId, float exhaustion) fd->setExhaustion(exhaustion); } - void __cdecl NativeSpawnParticle(int entityId, int particleId, float x, float y, float z, float offsetX, float offsetY, float offsetZ, float speed, int count) { auto player = FindPlayer(entityId); @@ -1249,4 +1356,284 @@ void __cdecl NativeGetEntityInfo(int entityId, double *outData) outData[4] = (double)entity->dimension; } +int __cdecl NativeGetWorldEntities(int dimId, int **outBuf) +{ + *outBuf = nullptr; + ServerLevel *level = GetLevel(dimId); + if (!level) + return 0; + + EnterCriticalSection(&level->m_entitiesCS); + int total = (int)level->entities.size(); + int *buf = (int *)CoTaskMemAlloc(total * 3 * sizeof(int)); + int count = 0; + if (buf) + { + for (auto &entity : level->entities) + { + if (!entity) + continue; + int idx = count * 3; + buf[idx] = entity->entityId; + buf[idx + 1] = MapEntityType((int)entity->GetType()); + buf[idx + 2] = entity->instanceof(eTYPE_LIVINGENTITY) ? 1 : 0; + count++; + } + } + LeaveCriticalSection(&level->m_entitiesCS); + *outBuf = buf; + return count; +} + +int __cdecl NativeGetChunkEntities(int dimId, int chunkX, int chunkZ, int **outBuf) +{ + *outBuf = nullptr; + ServerLevel *level = GetLevel(dimId); + if (!level) + return 0; + + EnterCriticalSection(&level->m_entitiesCS); + int total = (int)level->entities.size(); + int *buf = (int *)CoTaskMemAlloc(total * 3 * sizeof(int)); + int count = 0; + if (buf) + { + for (auto &entity : level->entities) + { + if (!entity) + continue; + int ecx = Mth::floor(entity->x / 16.0); + int ecz = Mth::floor(entity->z / 16.0); + if (ecx != chunkX || ecz != chunkZ) + continue; + int idx = count * 3; + buf[idx] = entity->entityId; + buf[idx + 1] = MapEntityType((int)entity->GetType()); + buf[idx + 2] = entity->instanceof(eTYPE_LIVINGENTITY) ? 1 : 0; + count++; + } + } + LeaveCriticalSection(&level->m_entitiesCS); + *outBuf = buf; + return count; +} + +int __cdecl NativeIsChunkLoaded(int dimId, int chunkX, int chunkZ) +{ + ServerLevel *level = GetLevel(dimId); + if (!level || !level->cache) + return 0; + return level->cache->hasChunk(chunkX, chunkZ) ? 1 : 0; +} + +int __cdecl NativeLoadChunk(int dimId, int chunkX, int chunkZ, int generate) +{ + ServerLevel *level = GetLevel(dimId); + if (!level || !level->cache) + return 0; + LevelChunk *chunk = level->cache->create(chunkX, chunkZ); + return (chunk != nullptr) ? 1 : 0; +} + +int __cdecl NativeUnloadChunk(int dimId, int chunkX, int chunkZ, int save, int safe) +{ + ServerLevel *level = GetLevel(dimId); + if (!level || !level->cache) + return 0; + if (safe) + { + if (!level->cache->hasChunk(chunkX, chunkZ)) + return 0; + LevelChunk *chunk = level->cache->getChunk(chunkX, chunkZ); + if (chunk && chunk->containsPlayer()) + return 0; + } + level->cache->drop(chunkX, chunkZ); + return 1; +} + +int __cdecl NativeGetLoadedChunks(int dimId, int **coordBuf) +{ + // wow gay + *coordBuf = nullptr; + ServerLevel *level = GetLevel(dimId); + if (!level || !level->cache) + return 0; + + std::vector *list = level->cache->getLoadedChunkList(); + + if (!list) + return 0; + + + + int total = (int)list->size(); + int *buf = (int *)CoTaskMemAlloc(total * 2 * sizeof(int)); + int count = 0; + + if (buf) + { + for (auto *chunk : *list) + { + if (chunk) + { + buf[count * 2] = chunk->x; + buf[count * 2 + 1] = chunk->z; + count++; + } + } + } + + *coordBuf = buf; + return count; +} + +int __cdecl NativeIsChunkInUse(int dimId, int chunkX, int chunkZ) +{ + PlayerList *list = MinecraftServer::getPlayerList(); + if (!list) + return 0; + for (auto &p : list->players) + { + if (p && p->dimension == dimId) + { + int px = (int)floor(p->x) >> 4; + int pz = (int)floor(p->z) >> 4; + if (px == chunkX && pz == chunkZ) + return 1; + } + } + return 0; +} + +void __cdecl NativeGetChunkSnapshot(int dimId, int chunkX, int chunkZ, int *blockIds, int *blockData, int *maxBlockY) +{ + ServerLevel *level = GetLevel(dimId); + if (!level || !level->cache) + { + memset(blockIds, 0, 16 * 128 * 16 * sizeof(int)); + memset(blockData, 0, 16 * 128 * 16 * sizeof(int)); + memset(maxBlockY, 0, 16 * 16 * sizeof(int)); + return; + } + if (!level->cache->hasChunk(chunkX, chunkZ)) + { + memset(blockIds, 0, 16 * 128 * 16 * sizeof(int)); + memset(blockData, 0, 16 * 128 * 16 * sizeof(int)); + memset(maxBlockY, 0, 16 * 16 * sizeof(int)); + return; + } + LevelChunk *chunk = level->cache->getChunk(chunkX, chunkZ); + if (!chunk) + { + memset(blockIds, 0, 16 * 128 * 16 * sizeof(int)); + memset(blockData, 0, 16 * 128 * 16 * sizeof(int)); + memset(maxBlockY, 0, 16 * 16 * sizeof(int)); + return; + } + for (int lx = 0; lx < 16; lx++) + { + for (int lz = 0; lz < 16; lz++) + { + int highest = 0; + for (int ly = 0; ly < 128; ly++) + { + int idx = (lx * 128 * 16) + (ly * 16) + lz; + blockIds[idx] = chunk->getTile(lx, ly, lz); + blockData[idx] = chunk->getData(lx, ly, lz); + if (blockIds[idx] != 0) + highest = ly; + } + maxBlockY[lx * 16 + lz] = highest; + } + } +} + +int __cdecl NativeUnloadChunkRequest(int dimId, int chunkX, int chunkZ, int safe) +{ + ServerLevel *level = GetLevel(dimId); + if (!level || !level->cache) + return 0; + if (safe) + { + if (!level->cache->hasChunk(chunkX, chunkZ)) + return 0; + LevelChunk *chunk = level->cache->getChunk(chunkX, chunkZ); + if (chunk && chunk->containsPlayer()) + return 0; + } + level->cache->drop(chunkX, chunkZ); + return 1; +} + +int __cdecl NativeRegenerateChunk(int dimId, int chunkX, int chunkZ) +{ + ServerLevel *level = GetLevel(dimId); + if (!level || !level->cache) + return 0; + level->cache->regenerateChunk(chunkX, chunkZ); + return 1; +} + +int __cdecl NativeRefreshChunk(int dimId, int chunkX, int chunkZ) +{ + ServerLevel *level = GetLevel(dimId); + if (!level) + return 0; + + PlayerList *list = MinecraftServer::getPlayerList(); + if (!list) + return 0; + + auto packet = std::make_shared(chunkX * 16, 0, chunkZ * 16, 16, Level::maxBuildHeight, 16, level); + for (auto &p : list->players) + { + if (!p || p->dimension != dimId || !p->connection || p->connection->isLocal()) + continue; + p->connection->send(packet); + } + return 1; +} + +int __cdecl NativeGetSkyLight(int dimId, int x, int y, int z) +{ + ServerLevel *level = GetLevel(dimId); + if (!level) + return 0; + return level->getBrightness(LightLayer::Sky, x, y, z); +} + +int __cdecl NativeGetBlockLight(int dimId, int x, int y, int z) +{ + ServerLevel *level = GetLevel(dimId); + if (!level) + return 0; + return level->getBrightness(LightLayer::Block, x, y, z); +} + +int __cdecl NativeGetBiomeId(int dimId, int x, int z) +{ + ServerLevel *level = GetLevel(dimId); + if (!level) + return 1; + Biome *biome = level->getBiome(x, z); + return biome ? biome->id : 1; +} + +void __cdecl NativeSetBiomeId(int dimId, int x, int z, int biomeId) +{ + ServerLevel *level = GetLevel(dimId); + if (!level) + return; + LevelChunk *chunk = level->getChunk(x >> 4, z >> 4); + if (!chunk) + return; + byteArray biomes = chunk->getBiomes(); + if (biomes.data == nullptr) + return; + int lx = x & 0xf; + int lz = z & 0xf; + biomes.data[(lz << 4) | lx] = static_cast(biomeId & 0xff); +} + } // namespace FourKitBridge \ No newline at end of file diff --git a/Minecraft.Server/FourKitNatives.h b/Minecraft.Server/FourKitNatives.h index df96d918..0b77a396 100644 --- a/Minecraft.Server/FourKitNatives.h +++ b/Minecraft.Server/FourKitNatives.h @@ -1,8 +1,21 @@ #pragma once +#include namespace FourKitBridge { + // Must match HandlerKind in FourKit.cs. + enum HandlerKind : int { + kHandlerKind_ChunkLoad = 0, + kHandlerKind_ChunkUnload = 1, + kHandlerKind_PlayerMove = 2, + }; + + void __cdecl NativeSetHandlerMask(uint32_t mask); + bool HasHandlers(int kind); + + int __cdecl NativeGetServerTickCount(); + // core void __cdecl NativeDamagePlayer(int entityId, float amount); void __cdecl NativeSetPlayerHealth(int entityId, float health); @@ -18,8 +31,8 @@ namespace FourKitBridge // World int __cdecl NativeGetTileId(int dimId, int x, int y, int z); int __cdecl NativeGetTileData(int dimId, int x, int y, int z); - void __cdecl NativeSetTile(int dimId, int x, int y, int z, int tileId, int data); - void __cdecl NativeSetTileData(int dimId, int x, int y, int z, int data); + void __cdecl NativeSetTile(int dimId, int x, int y, int z, int tileId, int data, int flags); + void __cdecl NativeSetTileData(int dimId, int x, int y, int z, int data, int flags); int __cdecl NativeBreakBlock(int dimId, int x, int y, int z); int __cdecl NativeGetHighestBlockY(int dimId, int x, int z); void __cdecl NativeGetWorldInfo(int dimId, double *outBuf); @@ -52,6 +65,12 @@ namespace FourKitBridge void __cdecl NativeSetItemMeta(int entityId, int slot, const char *inBuf, int bufSize); void __cdecl NativeSetHeldItemSlot(int entityId, int slot); + // carried item (cursor) & ender chest + void __cdecl NativeGetCarriedItem(int entityId, int *outData); + void __cdecl NativeSetCarriedItem(int entityId, int itemId, int count, int aux); + void __cdecl NativeGetEnderChestContents(int entityId, int *outData); + void __cdecl NativeSetEnderChestSlot(int entityId, int slot, int itemId, int count, int aux); + // ent void __cdecl NativeSetSneaking(int entityId, int sneak); void __cdecl NativeSetVelocity(int entityId, double x, double y, double z); @@ -78,4 +97,25 @@ namespace FourKitBridge int __cdecl NativeGetVehicleId(int entityId); int __cdecl NativeGetPassengerId(int entityId); void __cdecl NativeGetEntityInfo(int entityId, double *outData); + + // chunk + int __cdecl NativeIsChunkLoaded(int dimId, int chunkX, int chunkZ); + int __cdecl NativeLoadChunk(int dimId, int chunkX, int chunkZ, int generate); + int __cdecl NativeUnloadChunk(int dimId, int chunkX, int chunkZ, int save, int safe); + int __cdecl NativeGetLoadedChunks(int dimId, int **coordBuf); + int __cdecl NativeIsChunkInUse(int dimId, int chunkX, int chunkZ); + void __cdecl NativeGetChunkSnapshot(int dimId, int chunkX, int chunkZ, int *blockIds, int *blockData, int *maxBlockY); + int __cdecl NativeUnloadChunkRequest(int dimId, int chunkX, int chunkZ, int safe); + int __cdecl NativeRegenerateChunk(int dimId, int chunkX, int chunkZ); + int __cdecl NativeRefreshChunk(int dimId, int chunkX, int chunkZ); + + // world entity bs + int __cdecl NativeGetWorldEntities(int dimId, int **outBuf); + int __cdecl NativeGetChunkEntities(int dimId, int chunkX, int chunkZ, int **outBuf); + + // block info (light, biome) + int __cdecl NativeGetSkyLight(int dimId, int x, int y, int z); + int __cdecl NativeGetBlockLight(int dimId, int x, int y, int z); + int __cdecl NativeGetBiomeId(int dimId, int x, int z); + void __cdecl NativeSetBiomeId(int dimId, int x, int z, int biomeId); } diff --git a/Minecraft.Server/ServerLogger.cpp b/Minecraft.Server/ServerLogger.cpp index 175b7fea..f8d29c7c 100644 --- a/Minecraft.Server/ServerLogger.cpp +++ b/Minecraft.Server/ServerLogger.cpp @@ -15,6 +15,8 @@ static volatile LONG g_minLogLevel = (LONG)eServerLogLevel_Info; static FILE *g_logFile = NULL; static std::once_flag g_logFileOnce; +bool g_serverPerfTrace = false; + static void OpenLogFile() { if (g_logFile != NULL) diff --git a/Minecraft.Server/ServerLogger.h b/Minecraft.Server/ServerLogger.h index 89b820e6..d5d35bce 100644 --- a/Minecraft.Server/ServerLogger.h +++ b/Minecraft.Server/ServerLogger.h @@ -41,4 +41,9 @@ namespace ServerRuntime void LogStartupStep(const char *message); void LogWorldIO(const char *message); void LogWorldName(const char *prefix, const std::wstring &name); + + // When true, noisy [perf] sampling output (histograms, per-iter samples) + // is enabled. Threshold-fired warnings remain always-on. Toggled via the + // -perftrace CLI flag. + extern bool g_serverPerfTrace; } diff --git a/Minecraft.Server/ServerProperties.cpp b/Minecraft.Server/ServerProperties.cpp index 711ebe10..1b6c5c0a 100644 --- a/Minecraft.Server/ServerProperties.cpp +++ b/Minecraft.Server/ServerProperties.cpp @@ -888,6 +888,14 @@ ServerPropertiesConfig LoadServerPropertiesConfig() config.hardcore = ReadNormalizedBoolProperty(&merged, "hardcore", false, &shouldWrite); config.hardcoreBanIp = ReadNormalizedBoolProperty(&merged, "hardcore-ban-ip", false, &shouldWrite); + config.maxMonsters = ReadNormalizedIntProperty(&merged, "max-monsters", 50, 0, 1000, &shouldWrite); + config.maxAnimals = ReadNormalizedIntProperty(&merged, "max-animals", 50, 0, 1000, &shouldWrite); + config.maxAmbient = ReadNormalizedIntProperty(&merged, "max-ambient", 20, 0, 1000, &shouldWrite); + config.maxWaterAnimals = ReadNormalizedIntProperty(&merged, "max-water-animals", 5, 0, 1000, &shouldWrite); + config.maxWolves = ReadNormalizedIntProperty(&merged, "max-wolves", 8, 0, 1000, &shouldWrite); + config.maxChickens = ReadNormalizedIntProperty(&merged, "max-chickens", 8, 0, 1000, &shouldWrite); + config.maxMushroomCows = ReadNormalizedIntProperty(&merged, "max-mushroom-cows", 2, 0, 1000, &shouldWrite); + config.maxBuildHeight = ReadNormalizedIntProperty(&merged, "max-build-height", 256, 64, 256, &shouldWrite); config.motd = ReadNormalizedStringProperty(&merged, "motd", "A Minecraft Server", 255, &shouldWrite); diff --git a/Minecraft.Server/ServerProperties.h b/Minecraft.Server/ServerProperties.h index 0e6f8813..fd251fc3 100644 --- a/Minecraft.Server/ServerProperties.h +++ b/Minecraft.Server/ServerProperties.h @@ -80,6 +80,21 @@ namespace ServerRuntime /** `hardcore-ban-ip` — whether hardcore death bans include IP bans */ bool hardcoreBanIp; + /** `max-monsters` natural spawn cap for monsters (zombies, skeletons, creepers, etc.) */ + int maxMonsters; + /** `max-animals` natural spawn cap for animals (cows, sheep, pigs) */ + int maxAnimals; + /** `max-ambient` natural spawn cap for ambient mobs (bats) */ + int maxAmbient; + /** `max-water-animals` natural spawn cap for water mobs (squid) */ + int maxWaterAnimals; + /** `max-wolves` natural spawn cap for wolves */ + int maxWolves; + /** `max-chickens` natural spawn cap for chickens */ + int maxChickens; + /** `max-mushroom-cows` natural spawn cap for mooshrooms */ + int maxMushroomCows; + /** security settings */ /** `hide-player-list-prelogin` — strip XUIDs from PreLoginPacket response */ bool hidePlayerListPreLogin; diff --git a/Minecraft.Server/Windows64/ServerMain.cpp b/Minecraft.Server/Windows64/ServerMain.cpp index cd1119a6..b5d5402c 100644 --- a/Minecraft.Server/Windows64/ServerMain.cpp +++ b/Minecraft.Server/Windows64/ServerMain.cpp @@ -37,6 +37,7 @@ #include "../../Minecraft.World/ConsoleSaveFileOriginal.h" #include "../../Minecraft.World/net.minecraft.world.level.tile.h" #include "../../Minecraft.World/Random.h" +#include "../../Minecraft.World/MobCategory.h" #include #include @@ -165,6 +166,7 @@ static void PrintUsage() ServerRuntime::LogInfo("usage", " -maxplayers <1-8> Public slots (default: server.properties:max-players)"); ServerRuntime::LogInfo("usage", " -seed World seed (overrides server.properties:level-seed)"); ServerRuntime::LogInfo("usage", " -loglevel debug|info|warn|error (default: server.properties:log-level)"); + ServerRuntime::LogInfo("usage", " -perftrace Enable noisy [perf] sampling output (histograms, per-iter samples)"); ServerRuntime::LogInfo("usage", " -help Show this help"); } @@ -271,6 +273,10 @@ static bool ParseCommandLine(int argc, char **argv, DedicatedServerConfig *confi return false; } } + else if (_stricmp(arg, "-perftrace") == 0) + { + ServerRuntime::g_serverPerfTrace = true; + } else { LogErrorf("startup", "Unknown or incomplete argument: %s", arg); @@ -557,10 +563,18 @@ int main(int argc, char **argv) { LogError("startup", "Minecraft initialization failed."); CleanupDevice(); - + return 3; } + MobCategory::monster->setMaxInstancesPerLevel(serverProperties.maxMonsters); + MobCategory::creature->setMaxInstancesPerLevel(serverProperties.maxAnimals); + MobCategory::ambient->setMaxInstancesPerLevel(serverProperties.maxAmbient); + MobCategory::waterCreature->setMaxInstancesPerLevel(serverProperties.maxWaterAnimals); + MobCategory::creature_wolf->setMaxInstancesPerLevel(serverProperties.maxWolves); + MobCategory::creature_chicken->setMaxInstancesPerLevel(serverProperties.maxChickens); + MobCategory::creature_mushroomcow->setMaxInstancesPerLevel(serverProperties.maxMushroomCows); + app.InitGameSettings(); MinecraftServer::resetFlags(); @@ -737,7 +751,7 @@ int main(int argc, char **argv) break; } - if (autosaveRequested && app.GetXuiServerAction(kServerActionPad) == eXuiServerAction_Idle) + if (autosaveRequested && app.GetXuiServerAction(kServerActionPad) == eXuiServerAction_Idle && !ConsoleSaveFileOriginal::hasPendingBackgroundSave()) { LogWorldIO("autosave completed"); autosaveRequested = false; @@ -749,7 +763,7 @@ int main(int argc, char **argv) } DWORD now = GetTickCount(); - if ((LONG)(now - nextAutosaveTick) >= 0) + if ((LONG)(now - nextAutosaveTick) >= 0 && !IsShutdownRequested() && !app.m_bShutdown) { if (app.GetXuiServerAction(kServerActionPad) == eXuiServerAction_Idle) { @@ -772,12 +786,28 @@ int main(int argc, char **argv) MinecraftServer *server = MinecraftServer::getInstance(); if (server != NULL) { + // Drain any in-flight autosave before requesting the exit save so the + // async autosave can't overwrite the exit save with an older snapshot, + // and so m_saveOnExit gets set (prior logic skipped it when a save was + // pending, causing silent data loss on restart). + if (ConsoleSaveFileOriginal::hasPendingBackgroundSave()) + { + LogWorldIO("Draining pending autosave before exit save..."); + const DWORD kDrainTimeoutMs = 30000; + DWORD drainStart = GetTickCount(); + while (ConsoleSaveFileOriginal::hasPendingBackgroundSave()) + { + if ((LONG)(GetTickCount() - drainStart) > (LONG)kDrainTimeoutMs) + { + LogWorldIO("Autosave drain timed out; continuing with exit save"); + break; + } + TickCoreSystems(); + Sleep(10); + } + } server->setSaveOnExit(true); - } - if (server != NULL) - { - LogWorldIO("requesting save before shutdown"); - LogWorldIO("using saveOnExit for shutdown"); + LogWorldIO("requesting exit save"); } MinecraftServer::HaltServer(); diff --git a/Minecraft.Server/cmake/sources/Common.cmake b/Minecraft.Server/cmake/sources/Common.cmake index 43f3d8a8..64ef9f02 100644 --- a/Minecraft.Server/cmake/sources/Common.cmake +++ b/Minecraft.Server/cmake/sources/Common.cmake @@ -416,6 +416,7 @@ set(_MINECRAFT_SERVER_COMMON_ROOT "${_MS_SRC}/../Minecraft.Client/ScrolledSelectionList.cpp" "${_MS_SRC}/../Minecraft.Client/SelectWorldScreen.cpp" "${_MS_SRC}/../Minecraft.Client/ServerChunkCache.cpp" + "${_MS_SRC}/../Minecraft.Client/ServerChunkCache.h" "${_MS_SRC}/../Minecraft.Client/ServerCommandDispatcher.cpp" "${_MS_SRC}/../Minecraft.Client/ServerConnection.cpp" "${_MS_SRC}/../Minecraft.Client/ServerLevel.cpp" diff --git a/Minecraft.World/ArmorItem.cpp b/Minecraft.World/ArmorItem.cpp index 4b5955c6..c5fc4c02 100644 --- a/Minecraft.World/ArmorItem.cpp +++ b/Minecraft.World/ArmorItem.cpp @@ -244,7 +244,7 @@ void ArmorItem::setColor(shared_ptr item, int color) { #ifndef _CONTENT_PACKAGE printf("Can't dye non-leather!"); - __debugbreak(); + DEBUG_BREAK(); #endif //throw new UnsupportedOperationException("Can't dye non-leather!"); } diff --git a/Minecraft.World/BiomeDecorator.cpp b/Minecraft.World/BiomeDecorator.cpp index 0ebadeed..f6577572 100644 --- a/Minecraft.World/BiomeDecorator.cpp +++ b/Minecraft.World/BiomeDecorator.cpp @@ -27,8 +27,8 @@ void BiomeDecorator::decorate(Level *level, Random *random, int xo, int zo) { app.DebugPrintf("BiomeDecorator::decorate - Already decorating!!\n"); #ifndef _CONTENT_PACKAGE - __debugbreak(); - //throw new RuntimeException("Already decorating!!"); + DEBUG_BREAK(); + //throw new RuntimeException("Already decorating!!"); #endif } this->level = level; diff --git a/Minecraft.World/BiomeOverrideLayer.cpp b/Minecraft.World/BiomeOverrideLayer.cpp index 84605740..39747bc0 100644 --- a/Minecraft.World/BiomeOverrideLayer.cpp +++ b/Minecraft.World/BiomeOverrideLayer.cpp @@ -32,7 +32,7 @@ BiomeOverrideLayer::BiomeOverrideLayer(int seedMixup) : Layer(seedMixup) { #ifdef _DURANGO - __debugbreak(); // TODO + DEBUG_BREAK(); // TODO DWORD bytesRead,dwFileSize = 0; #else DWORD bytesRead,dwFileSize = GetFileSize(file,nullptr); @@ -40,7 +40,7 @@ BiomeOverrideLayer::BiomeOverrideLayer(int seedMixup) : Layer(seedMixup) if(dwFileSize > m_biomeOverride.length) { app.DebugPrintf("Biomemap binary is too large!!\n"); - __debugbreak(); + DEBUG_BREAK(); } BOOL bSuccess = ReadFile(file,m_biomeOverride.data,dwFileSize,&bytesRead,nullptr); diff --git a/Minecraft.World/BiomeSource.cpp b/Minecraft.World/BiomeSource.cpp index e010b4d2..864a098e 100644 --- a/Minecraft.World/BiomeSource.cpp +++ b/Minecraft.World/BiomeSource.cpp @@ -180,11 +180,11 @@ void BiomeSource::getRawBiomeBlock(BiomeArray &biomes, int x, int z, int w, int { biomes[i] = Biome::biomes[result[i]]; #ifndef _CONTENT_PACKAGE - if(biomes[i] == nullptr) - { - app.DebugPrintf("Tried to assign null biome %d\n", result[i]); - __debugbreak(); - } + if(biomes[i] == nullptr) + { + app.DebugPrintf("Tried to assign null biome %d\n", result[i]); + DEBUG_BREAK(); + } #endif } } diff --git a/Minecraft.World/Class.h b/Minecraft.World/Class.h index 71b9df55..5e197e3f 100644 --- a/Minecraft.World/Class.h +++ b/Minecraft.World/Class.h @@ -1,4 +1,5 @@ #pragma once +#include "Debug.h" using namespace std; class InputStream; @@ -611,7 +612,7 @@ public: if ( (m_falsePositives.size() > 0) || (m_falseNegatives.size() > 0) ) { - __debugbreak(); + DEBUG_BREAK(); } } }; diff --git a/Minecraft.World/CompoundTag.h b/Minecraft.World/CompoundTag.h index 5ea9bca5..83d0bfb2 100644 --- a/Minecraft.World/CompoundTag.h +++ b/Minecraft.World/CompoundTag.h @@ -37,7 +37,7 @@ public: { #ifndef _CONTENT_PACKAGE printf("Tried to read NBT tag with too high complexity, depth > %d" , MAX_DEPTH); - __debugbreak(); + DEBUG_BREAK(); #endif return; } diff --git a/Minecraft.World/CompressedTileStorage.cpp b/Minecraft.World/CompressedTileStorage.cpp index 7cdfdbce..c6ea725b 100644 --- a/Minecraft.World/CompressedTileStorage.cpp +++ b/Minecraft.World/CompressedTileStorage.cpp @@ -1065,7 +1065,7 @@ void CompressedTileStorage::compress(int upgradeBlock/*=-1*/) #ifndef _DURANGO MEMORYSTATUS memStatus; GlobalMemoryStatus(&memStatus); - __debugbreak(); + DEBUG_BREAK(); #endif } unsigned char *pucData = newIndicesAndData + 1024; diff --git a/Minecraft.World/ConsoleSaveFileOriginal.cpp b/Minecraft.World/ConsoleSaveFileOriginal.cpp index 8488c8cd..4c8bf83a 100644 --- a/Minecraft.World/ConsoleSaveFileOriginal.cpp +++ b/Minecraft.World/ConsoleSaveFileOriginal.cpp @@ -90,7 +90,7 @@ ConsoleSaveFileOriginal::ConsoleSaveFileOriginal(const wstring &fileName, LPVOID if( pagesCommitted != 0 ) { #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif } @@ -101,7 +101,7 @@ ConsoleSaveFileOriginal::ConsoleSaveFileOriginal(const wstring &fileName, LPVOID { #ifndef _CONTENT_PACKAGE // Out of physical memory - __debugbreak(); + DEBUG_BREAK(); #endif } pagesCommitted = pagesRequired; @@ -206,7 +206,7 @@ ConsoleSaveFileOriginal::ConsoleSaveFileOriginal(const wstring &fileName, LPVOID if( pvRet == nullptr ) { // Out of physical memory - __debugbreak(); + DEBUG_BREAK(); } pagesCommitted = pagesRequired; } @@ -503,7 +503,7 @@ void ConsoleSaveFileOriginal::finalizeWrite() void *pvRet = VirtualAlloc(pvHeap, pagesRequired * CSF_PAGE_SIZE, COMMIT_ALLOCATION, PAGE_READWRITE); if( pvRet == NULL ) { - __debugbreak(); + DEBUG_BREAK(); } pagesCommitted = pagesRequired; } @@ -537,7 +537,7 @@ void ConsoleSaveFileOriginal::MoveDataBeyond(FileEntry *file, DWORD nNumberOfByt if( pvRet == nullptr ) { // Out of physical memory - __debugbreak(); + DEBUG_BREAK(); } pagesCommitted = pagesRequired; } @@ -739,6 +739,7 @@ void ConsoleSaveFileOriginal::Flush(bool autosave, bool updateThumbnail ) s_bgSaveActive.store(true, std::memory_order_release); std::thread([snap, fileSize, thumb, thumbSz, meta, metaLen, this]() { + Compression::UseDefaultThreadStorage(); unsigned int compLen = fileSize + 8; byte *buf = static_cast(StorageManager.AllocateSaveData(compLen)); if (!buf) diff --git a/Minecraft.World/CustomLevelSource.cpp b/Minecraft.World/CustomLevelSource.cpp index a63802bd..de6923e8 100644 --- a/Minecraft.World/CustomLevelSource.cpp +++ b/Minecraft.World/CustomLevelSource.cpp @@ -41,7 +41,7 @@ CustomLevelSource::CustomLevelSource(Level *level, int64_t seed, bool generateSt { #ifdef _DURANGO - __debugbreak(); // TODO + DEBUG_BREAK(); // TODO DWORD bytesRead,dwFileSize = 0; #else DWORD bytesRead,dwFileSize = GetFileSize(file,nullptr); @@ -49,7 +49,7 @@ CustomLevelSource::CustomLevelSource(Level *level, int64_t seed, bool generateSt if(dwFileSize > m_heightmapOverride.length) { app.DebugPrintf("Heightmap binary is too large!!\n"); - __debugbreak(); + DEBUG_BREAK(); } BOOL bSuccess = ReadFile(file,m_heightmapOverride.data,dwFileSize,&bytesRead,nullptr); @@ -83,7 +83,7 @@ CustomLevelSource::CustomLevelSource(Level *level, int64_t seed, bool generateSt { #ifdef _DURANGO - __debugbreak(); // TODO + DEBUG_BREAK(); // TODO DWORD bytesRead,dwFileSize = 0; #else DWORD bytesRead,dwFileSize = GetFileSize(file,nullptr); @@ -91,7 +91,7 @@ CustomLevelSource::CustomLevelSource(Level *level, int64_t seed, bool generateSt if(dwFileSize > m_waterheightOverride.length) { app.DebugPrintf("waterheight binary is too large!!\n"); - __debugbreak(); + DEBUG_BREAK(); } BOOL bSuccess = ReadFile(file,m_waterheightOverride.data,dwFileSize,&bytesRead,nullptr); diff --git a/Minecraft.World/CustomPayloadPacket.cpp b/Minecraft.World/CustomPayloadPacket.cpp index 4c23f237..e6dacfeb 100644 --- a/Minecraft.World/CustomPayloadPacket.cpp +++ b/Minecraft.World/CustomPayloadPacket.cpp @@ -49,7 +49,7 @@ CustomPayloadPacket::CustomPayloadPacket(const wstring &identifier, byteArray da { app.DebugPrintf("Payload may not be larger than 32K\n"); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif //throw new IllegalArgumentException("Payload may not be larger than 32k"); } diff --git a/Minecraft.World/Debug.h b/Minecraft.World/Debug.h new file mode 100644 index 00000000..d1d82836 --- /dev/null +++ b/Minecraft.World/Debug.h @@ -0,0 +1,12 @@ +#pragma once +#include + +#if defined(_MSC_VER) + #define DEBUG_BREAK() __debugbreak() +#elif defined(__GNUC__) || defined(__clang__) + #define DEBUG_BREAK() __builtin_trap() +#elif defined(SIGTRAP) + #define DEBUG_BREAK() std::raise(SIGTRAP) +#else + #define DEBUG_BREAK() ((void)0) +#endif \ No newline at end of file diff --git a/Minecraft.World/DirectoryLevelStorage.cpp b/Minecraft.World/DirectoryLevelStorage.cpp index 30ee52f0..142f7359 100644 --- a/Minecraft.World/DirectoryLevelStorage.cpp +++ b/Minecraft.World/DirectoryLevelStorage.cpp @@ -44,7 +44,7 @@ int _MapDataMappings::getDimension(int id) default: #ifndef _CONTENT_PACKAGE printf("Read invalid dimension from MapDataMapping\n"); - __debugbreak(); + DEBUG_BREAK(); #endif break; } @@ -73,7 +73,7 @@ void _MapDataMappings::setMapping(int id, PlayerUID xuid, int dimension) default: #ifndef _CONTENT_PACKAGE printf("Trinyg to set a MapDataMapping for an invalid dimension.\n"); - __debugbreak(); + DEBUG_BREAK(); #endif break; } diff --git a/Minecraft.World/Enchantment.cpp b/Minecraft.World/Enchantment.cpp index 5d0743cc..944f03d7 100644 --- a/Minecraft.World/Enchantment.cpp +++ b/Minecraft.World/Enchantment.cpp @@ -94,7 +94,7 @@ void Enchantment::_init(int id) { app.DebugPrintf("Duplicate enchantment id!"); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif //throw new IllegalArgumentException("Duplicate enchantment id!"); } diff --git a/Minecraft.World/Entity.cpp b/Minecraft.World/Entity.cpp index 6e3ef44c..1b42ba6e 100644 --- a/Minecraft.World/Entity.cpp +++ b/Minecraft.World/Entity.cpp @@ -111,7 +111,7 @@ int Entity::getSmallId() return fallbackId; #else app.DebugPrintf("Out of small entity Ids... possible leak?\n"); - __debugbreak(); + DEBUG_BREAK(); return -1; #endif } diff --git a/Minecraft.World/File.cpp b/Minecraft.World/File.cpp index 11871e42..23e1051c 100644 --- a/Minecraft.World/File.cpp +++ b/Minecraft.World/File.cpp @@ -239,7 +239,7 @@ bool File::renameTo(File dest) std::string sourcePath = wstringtofilename(getPath()); const char *destPath = wstringtofilename(dest.getPath()); #ifdef _DURANGO - __debugbreak(); // TODO + DEBUG_BREAK(); // TODO BOOL result = false; #else BOOL result = MoveFile(sourcePath.c_str(), destPath); diff --git a/Minecraft.World/FileHeader.cpp b/Minecraft.World/FileHeader.cpp index a7bdbc90..b2ebe083 100644 --- a/Minecraft.World/FileHeader.cpp +++ b/Minecraft.World/FileHeader.cpp @@ -298,7 +298,7 @@ void FileHeader::ReadHeader( LPVOID saveMem, ESavePlatform plat /*= SAVE_FILE_PL default: #ifndef _CONTENT_PACKAGE app.DebugPrintf("********** Invalid save version %d\n",m_saveVersion); - __debugbreak(); + DEBUG_BREAK(); #endif break; } diff --git a/Minecraft.World/FileInputStream.cpp b/Minecraft.World/FileInputStream.cpp index 7c34a22f..a1846440 100644 --- a/Minecraft.World/FileInputStream.cpp +++ b/Minecraft.World/FileInputStream.cpp @@ -44,7 +44,6 @@ FileInputStream::FileInputStream(const File &file) if( m_fileHandle == INVALID_HANDLE_VALUE ) { // TODO 4J Stu - Any form of error/exception handling - //__debugbreak(); app.FatalLoadError(); } } diff --git a/Minecraft.World/GameCommandPacket.cpp b/Minecraft.World/GameCommandPacket.cpp index ada5b04c..d7922293 100644 --- a/Minecraft.World/GameCommandPacket.cpp +++ b/Minecraft.World/GameCommandPacket.cpp @@ -23,7 +23,7 @@ GameCommandPacket::GameCommandPacket(EGameCommand command, byteArray data) { app.DebugPrintf("Payload may not be larger than 32K\n"); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif //throw new IllegalArgumentException("Payload may not be larger than 32k"); } diff --git a/Minecraft.World/Level.cpp b/Minecraft.World/Level.cpp index 184d84e5..8478535b 100644 --- a/Minecraft.World/Level.cpp +++ b/Minecraft.World/Level.cpp @@ -2411,7 +2411,7 @@ void Level::tickEntities() { if(isClientSide) { - __debugbreak(); + DEBUG_BREAK(); } it = tileEntityList.erase(it); } @@ -4699,19 +4699,19 @@ bool Level::canCreateMore(eINSTANCEOF type, ESPAWN_TYPE spawnType) break; case eTYPE_CHICKEN: count = countInstanceOf( eTYPE_CHICKEN, true); - max = MobCategory::MAX_XBOX_CHICKENS_WITH_SPAWN_EGG; + max = MobCategory::maxChickensWithSpawnEgg(); break; case eTYPE_WOLF: count = countInstanceOf( eTYPE_WOLF, true); - max = MobCategory::MAX_XBOX_WOLVES_WITH_SPAWN_EGG; + max = MobCategory::maxWolvesWithSpawnEgg(); break; case eTYPE_MUSHROOMCOW: count = countInstanceOf( eTYPE_MUSHROOMCOW, true); - max = MobCategory::MAX_XBOX_MUSHROOMCOWS_WITH_SPAWN_EGG; + max = MobCategory::maxMushroomCowsWithSpawnEgg(); break; case eTYPE_SQUID: count = countInstanceOf( eTYPE_SQUID, true); - max = MobCategory::MAX_XBOX_SQUIDS_WITH_SPAWN_EGG; + max = MobCategory::maxSquidsWithSpawnEgg(); break; case eTYPE_SNOWMAN: count = countInstanceOf( eTYPE_SNOWMAN, true); @@ -4729,18 +4729,18 @@ bool Level::canCreateMore(eINSTANCEOF type, ESPAWN_TYPE spawnType) if((type & eTYPE_ANIMALS_SPAWN_LIMIT_CHECK) == eTYPE_ANIMALS_SPAWN_LIMIT_CHECK) { count = countInstanceOf( eTYPE_ANIMALS_SPAWN_LIMIT_CHECK, false); - max = MobCategory::MAX_XBOX_ANIMALS_WITH_SPAWN_EGG; + max = MobCategory::maxAnimalsWithSpawnEgg(); } // 4J: Use eTYPE_ENEMY instead of monster (slimes and ghasts aren't monsters) else if(Entity::instanceof(type, eTYPE_ENEMY)) { count = countInstanceOf(eTYPE_ENEMY, false); - max = MobCategory::MAX_XBOX_MONSTERS_WITH_SPAWN_EGG; + max = MobCategory::maxMonstersWithSpawnEgg(); } else if( (type & eTYPE_AMBIENT) == eTYPE_AMBIENT) { count = countInstanceOf( eTYPE_AMBIENT, false); - max = MobCategory::MAX_AMBIENT_WITH_SPAWN_EGG; + max = MobCategory::maxAmbientWithSpawnEgg(); } // 4J: Added minecart and boats else if (Entity::instanceof(type, eTYPE_MINECART)) @@ -4765,21 +4765,21 @@ bool Level::canCreateMore(eINSTANCEOF type, ESPAWN_TYPE spawnType) break; case eTYPE_CHICKEN: count = countInstanceOf( eTYPE_CHICKEN, true); - max = MobCategory::MAX_XBOX_CHICKENS_WITH_BREEDING; + max = MobCategory::maxChickensWithBreeding(); break; case eTYPE_WOLF: count = countInstanceOf( eTYPE_WOLF, true); - max = MobCategory::MAX_XBOX_WOLVES_WITH_BREEDING; + max = MobCategory::maxWolvesWithBreeding(); break; case eTYPE_MUSHROOMCOW: count = countInstanceOf( eTYPE_MUSHROOMCOW, true); - max = MobCategory::MAX_XBOX_MUSHROOMCOWS_WITH_BREEDING; + max = MobCategory::maxMushroomCowsWithBreeding(); break; default: if((type & eTYPE_ANIMALS_SPAWN_LIMIT_CHECK) == eTYPE_ANIMALS_SPAWN_LIMIT_CHECK) { count = countInstanceOf( eTYPE_ANIMALS_SPAWN_LIMIT_CHECK, false); - max = MobCategory::MAX_XBOX_ANIMALS_WITH_BREEDING; + max = MobCategory::maxAnimalsWithBreeding(); } else if( (type & eTYPE_MONSTER) == eTYPE_MONSTER) { diff --git a/Minecraft.World/MobCategory.cpp b/Minecraft.World/MobCategory.cpp index 8c6d3b44..cde98dc4 100644 --- a/Minecraft.World/MobCategory.cpp +++ b/Minecraft.World/MobCategory.cpp @@ -58,6 +58,24 @@ int MobCategory::getMaxInstancesPerLevel() // 4J added return m_maxPerLevel; } +void MobCategory::setMaxInstancesPerLevel(int max) +{ + m_maxPerLevel = max; +} + +int MobCategory::maxAnimalsWithBreeding() { return creature->getMaxInstancesPerLevel() + 20; } +int MobCategory::maxChickensWithBreeding() { return creature_chicken->getMaxInstancesPerLevel() + 8; } +int MobCategory::maxMushroomCowsWithBreeding() { return creature_mushroomcow->getMaxInstancesPerLevel() + 20; } +int MobCategory::maxWolvesWithBreeding() { return creature_wolf->getMaxInstancesPerLevel() + 8; } + +int MobCategory::maxAnimalsWithSpawnEgg() { return maxAnimalsWithBreeding() + 20; } +int MobCategory::maxChickensWithSpawnEgg() { return maxChickensWithBreeding() + 10; } +int MobCategory::maxWolvesWithSpawnEgg() { return maxWolvesWithBreeding() + 10; } +int MobCategory::maxMonstersWithSpawnEgg() { return monster->getMaxInstancesPerLevel() + 20; } +int MobCategory::maxMushroomCowsWithSpawnEgg() { return maxMushroomCowsWithBreeding() + 8; } +int MobCategory::maxSquidsWithSpawnEgg() { return waterCreature->getMaxInstancesPerLevel() + 8; } +int MobCategory::maxAmbientWithSpawnEgg() { return ambient->getMaxInstancesPerLevel() + 8; } + Material *MobCategory::getSpawnPositionMaterial() { return (Material *) spawnPositionMaterial; diff --git a/Minecraft.World/MobCategory.h b/Minecraft.World/MobCategory.h index 4fe5c826..d5eab0c3 100644 --- a/Minecraft.World/MobCategory.h +++ b/Minecraft.World/MobCategory.h @@ -19,20 +19,25 @@ public: static const int CONSOLE_SQUID_HARD_LIMIT = 5; static const int MAX_CONSOLE_BOSS = 1; // Max number of bosses (enderdragon/wither) - static const int MAX_XBOX_ANIMALS_WITH_BREEDING = CONSOLE_ANIMALS_HARD_LIMIT + 20; // Max number of animals that we can produce (in total), when breeding - static const int MAX_XBOX_CHICKENS_WITH_BREEDING = MAX_XBOX_CHICKENS + 8; // Max number of chickens that we can produce (in total), when breeding/hatching - static const int MAX_XBOX_MUSHROOMCOWS_WITH_BREEDING = MAX_XBOX_MUSHROOMCOWS + 20; // Max number of mushroom cows that we can produce (in total), when breeding - static const int MAX_XBOX_WOLVES_WITH_BREEDING = MAX_XBOX_WOLVES + 8; // Max number of wolves that we can produce (in total), when breeding + // 4J Villager breeding/egg limits - villagers are not a MobCategory so these stay hardcoded static const int MAX_VILLAGERS_WITH_BREEDING = 35; + static const int MAX_XBOX_VILLAGERS_WITH_SPAWN_EGG = MAX_VILLAGERS_WITH_BREEDING + 15; - static const int MAX_XBOX_ANIMALS_WITH_SPAWN_EGG = MAX_XBOX_ANIMALS_WITH_BREEDING + 20; - static const int MAX_XBOX_CHICKENS_WITH_SPAWN_EGG = MAX_XBOX_CHICKENS_WITH_BREEDING + 10; - static const int MAX_XBOX_WOLVES_WITH_SPAWN_EGG = MAX_XBOX_WOLVES_WITH_BREEDING + 10; - static const int MAX_XBOX_MONSTERS_WITH_SPAWN_EGG = CONSOLE_MONSTERS_HARD_LIMIT + 20; - static const int MAX_XBOX_VILLAGERS_WITH_SPAWN_EGG = MAX_VILLAGERS_WITH_BREEDING + 15; // 4J-PB - increased this limit due to player requests - static const int MAX_XBOX_MUSHROOMCOWS_WITH_SPAWN_EGG = MAX_XBOX_MUSHROOMCOWS_WITH_BREEDING + 8; - static const int MAX_XBOX_SQUIDS_WITH_SPAWN_EGG = CONSOLE_SQUID_HARD_LIMIT + 8; - static const int MAX_AMBIENT_WITH_SPAWN_EGG = CONSOLE_AMBIENT_HARD_LIMIT + 8; + // Breeding headroom above the natural spawn cap. Read at call time so these + // respect max-* overrides from server.properties. + static int maxAnimalsWithBreeding(); + static int maxChickensWithBreeding(); + static int maxMushroomCowsWithBreeding(); + static int maxWolvesWithBreeding(); + + // Spawn-egg headroom above the natural (or breeding) cap. + static int maxAnimalsWithSpawnEgg(); + static int maxChickensWithSpawnEgg(); + static int maxWolvesWithSpawnEgg(); + static int maxMonstersWithSpawnEgg(); + static int maxMushroomCowsWithSpawnEgg(); + static int maxSquidsWithSpawnEgg(); + static int maxAmbientWithSpawnEgg(); /* Maximum animals = 50 + 20 + 20 = 90 @@ -65,7 +70,7 @@ public: private: const int m_max; - const int m_maxPerLevel; + int m_maxPerLevel; const Material *spawnPositionMaterial; const bool m_isFriendly; const bool m_isPersistent; @@ -79,6 +84,7 @@ public: const eINSTANCEOF getEnumBaseClass(); // 4J added int getMaxInstancesPerChunk(); int getMaxInstancesPerLevel(); // 4J added + void setMaxInstancesPerLevel(int max); // 4J added Material *getSpawnPositionMaterial(); bool isFriendly(); bool isSingleType(); diff --git a/Minecraft.World/MoveEntityPacket.cpp b/Minecraft.World/MoveEntityPacket.cpp index aef9e621..a2b79db3 100644 --- a/Minecraft.World/MoveEntityPacket.cpp +++ b/Minecraft.World/MoveEntityPacket.cpp @@ -38,7 +38,7 @@ void MoveEntityPacket::write(DataOutputStream *dos) //throws IOException if( (id < 0 ) || (id >= 16384 ) ) { // We shouln't be tracking an entity that doesn't have a short type of id - __debugbreak(); + DEBUG_BREAK(); } dos->writeShort(static_cast(id)); } diff --git a/Minecraft.World/MoveEntityPacketSmall.cpp b/Minecraft.World/MoveEntityPacketSmall.cpp index 7d91a15d..3e1b922c 100644 --- a/Minecraft.World/MoveEntityPacketSmall.cpp +++ b/Minecraft.World/MoveEntityPacketSmall.cpp @@ -22,7 +22,7 @@ MoveEntityPacketSmall::MoveEntityPacketSmall(int id) if( (id < 0 ) || (id >= 16384 ) ) { // We shouln't be tracking an entity that doesn't have a short type of id - __debugbreak(); + DEBUG_BREAK(); } this->id = id; @@ -45,7 +45,7 @@ void MoveEntityPacketSmall::write(DataOutputStream *dos) //throws IOException if( (id < 0 ) || (id >= 16384 ) ) { // We shouln't be tracking an entity that doesn't have a short type of id - __debugbreak(); + DEBUG_BREAK(); } dos->writeShort(static_cast(id)); } @@ -102,7 +102,7 @@ void MoveEntityPacketSmall::PosRot::write(DataOutputStream *dos) //throws IOExce if( (id < 0 ) || (id >= 16384 ) ) { // We shouln't be tracking an entity that doesn't have a short type of id - __debugbreak(); + DEBUG_BREAK(); } short idAndRot = id | yRot << 11; dos->writeShort(idAndRot); @@ -141,7 +141,7 @@ void MoveEntityPacketSmall::Pos::write(DataOutputStream *dos) //throws IOExcepti if( (id < 0 ) || (id >= 16384 ) ) { // We shouln't be tracking an entity that doesn't have a short type of id - __debugbreak(); + DEBUG_BREAK(); } short idAndY = id | ya << 11; dos->writeShort(idAndY); @@ -179,7 +179,7 @@ void MoveEntityPacketSmall::Rot::write(DataOutputStream *dos) //throws IOExcepti if( (id < 0 ) || (id >= 16384 ) ) { // We shouln't be tracking an entity that doesn't have a short type of id - __debugbreak(); + DEBUG_BREAK(); } short idAndRot = id | yRot << 11; dos->writeShort(idAndRot); diff --git a/Minecraft.World/Packet.cpp b/Minecraft.World/Packet.cpp index 963fe5de..8329db1f 100644 --- a/Minecraft.World/Packet.cpp +++ b/Minecraft.World/Packet.cpp @@ -288,7 +288,7 @@ byteArray Packet::readBytes(DataInputStream *datainputstream) { app.DebugPrintf("Key was smaller than nothing! Weird key!"); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif return byteArray(); //throw new IOException("Key was smaller than nothing! Weird key!"); diff --git a/Minecraft.World/PressurePlateTile.cpp b/Minecraft.World/PressurePlateTile.cpp index 650fbf9f..4a61cf27 100644 --- a/Minecraft.World/PressurePlateTile.cpp +++ b/Minecraft.World/PressurePlateTile.cpp @@ -28,7 +28,7 @@ int PressurePlateTile::getSignalStrength(Level *level, int x, int y, int z) if (sensitivity == everything) entities = level->getEntities(nullptr, getSensitiveAABB(x, y, z)); else if (sensitivity == mobs) entities = level->getEntitiesOfClass(typeid(LivingEntity), getSensitiveAABB(x, y, z)); else if (sensitivity == players) entities = level->getEntitiesOfClass(typeid(Player), getSensitiveAABB(x, y, z)); - else __debugbreak(); // 4J-JEV: We're going to delete something at a random location. + else DEBUG_BREAK(); // 4J-JEV: We're going to delete something at a random location. if (entities != nullptr && !entities->empty()) { diff --git a/Minecraft.World/RegionFile.cpp b/Minecraft.World/RegionFile.cpp index 175ea6c1..9ef17fd4 100644 --- a/Minecraft.World/RegionFile.cpp +++ b/Minecraft.World/RegionFile.cpp @@ -299,7 +299,7 @@ void RegionFile::write(int x, int z, byte *data, int length) // TODO - was sync #ifndef _CONTENT_PACKAGE if(sectorNumber < 0) { - __debugbreak(); + DEBUG_BREAK(); } #endif diff --git a/Minecraft.World/SavedDataStorage.cpp b/Minecraft.World/SavedDataStorage.cpp index 049a19fc..36c79b6f 100644 --- a/Minecraft.World/SavedDataStorage.cpp +++ b/Minecraft.World/SavedDataStorage.cpp @@ -50,7 +50,7 @@ shared_ptr SavedDataStorage::get(const type_info& clazz, const wstrin else { // Handling of new SavedData class required - __debugbreak(); + DEBUG_BREAK(); } ConsoleSaveFileInputStream fis = ConsoleSaveFileInputStream(levelStorage->getSaveFile(), file); diff --git a/Minecraft.World/Scoreboard.cpp b/Minecraft.World/Scoreboard.cpp index bab0b213..2af268cc 100644 --- a/Minecraft.World/Scoreboard.cpp +++ b/Minecraft.World/Scoreboard.cpp @@ -14,9 +14,6 @@ Objective *Scoreboard::addObjective(const wstring &name, ObjectiveCriteria *crit // Objective *objective = getObjective(name); // if (objective != nullptr) // { -//#indef _CONTENT_PACKAGE -// __debugbreak(); -//#endif // //throw new IllegalArgumentException("An objective with the name '" + name + "' already exists!"); // } // diff --git a/Minecraft.World/SetPlayerTeamPacket.cpp b/Minecraft.World/SetPlayerTeamPacket.cpp index a574067d..b5fc825b 100644 --- a/Minecraft.World/SetPlayerTeamPacket.cpp +++ b/Minecraft.World/SetPlayerTeamPacket.cpp @@ -39,14 +39,14 @@ SetPlayerTeamPacket::SetPlayerTeamPacket(PlayerTeam *team, vector *play { app.DebugPrintf("Method must be join or leave for player constructor"); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif } if (playerNames == nullptr || playerNames->empty()) { app.DebugPrintf("Players cannot be null/empty"); #ifndef _CONTENT_PACKAGE - __debugbreak(); + DEBUG_BREAK(); #endif } diff --git a/Minecraft.World/StrongholdFeature.cpp b/Minecraft.World/StrongholdFeature.cpp index cdd36571..cced8116 100644 --- a/Minecraft.World/StrongholdFeature.cpp +++ b/Minecraft.World/StrongholdFeature.cpp @@ -158,7 +158,7 @@ bool StrongholdFeature::isFeatureChunk(int x, int z,bool bIsSuperflat) #ifndef _CONTENT_PACKAGE if(position->x > 2560 || position->x < -2560 || position->z > 2560 || position->z < -2560) { - __debugbreak(); + DEBUG_BREAK(); } #endif diff --git a/Minecraft.World/WeighedRandom.cpp b/Minecraft.World/WeighedRandom.cpp index 777ab92f..ff75be74 100644 --- a/Minecraft.World/WeighedRandom.cpp +++ b/Minecraft.World/WeighedRandom.cpp @@ -15,7 +15,7 @@ WeighedRandomItem *WeighedRandom::getRandomItem(Random *random, vectornextInt(totalWeight); @@ -50,7 +50,7 @@ WeighedRandomItem *WeighedRandom::getRandomItem(Random *random, WeighedRandomIte { if (totalWeight <= 0) { - __debugbreak(); + DEBUG_BREAK(); } int selection = random->nextInt(totalWeight); diff --git a/Minecraft.World/cmake/sources/Common.cmake b/Minecraft.World/cmake/sources/Common.cmake index 60573cbb..fe119720 100644 --- a/Minecraft.World/cmake/sources/Common.cmake +++ b/Minecraft.World/cmake/sources/Common.cmake @@ -106,6 +106,7 @@ set(_MINECRAFT_WORLD_COMMON_CONSOLEJAVALIBS_INPUTOUTPUTSTREAM source_group("ConsoleJavaLibs/InputOutputStream" FILES ${_MINECRAFT_WORLD_COMMON_CONSOLEJAVALIBS_INPUTOUTPUTSTREAM}) set(_MINECRAFT_WORLD_COMMON_HEADER_FILES + "${CMAKE_CURRENT_SOURCE_DIR}/Debug.h" "${CMAKE_CURRENT_SOURCE_DIR}/LevelObjectInputStream.h" "${CMAKE_CURRENT_SOURCE_DIR}/Minecraft.World.h" "${CMAKE_CURRENT_SOURCE_DIR}/ParticleTypes.h" @@ -432,8 +433,8 @@ set(_MINECRAFT_WORLD_COMMON_NET_MINECRAFT_NETWORK_PACKET "${CMAKE_CURRENT_SOURCE_DIR}/UseItemPacket.h" "${CMAKE_CURRENT_SOURCE_DIR}/XZPacket.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/XZPacket.h" - "${CMAKE_CURRENT_SOURCE_DIR}/net.minecraft.network.packet.h" - "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ParticleType.h" + "${CMAKE_CURRENT_SOURCE_DIR}/net.minecraft.network.packet.h" + "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ParticleType.h" "${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/ParticleType.cpp" ) source_group("net/minecraft/network/packet" FILES ${_MINECRAFT_WORLD_COMMON_NET_MINECRAFT_NETWORK_PACKET}) @@ -852,9 +853,8 @@ set(_MINECRAFT_WORLD_COMMON_NET_MINECRAFT_WORLD_ENTITY_ITEM "${CMAKE_CURRENT_SOURCE_DIR}/MinecartTNT.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/MinecartTNT.h" "${CMAKE_CURRENT_SOURCE_DIR}/PrimedTnt.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/PrimedTnt.h" - - "${CMAKE_CURRENT_SOURCE_DIR}/ArmorStand.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/PrimedTnt.h" + "${CMAKE_CURRENT_SOURCE_DIR}/ArmorStand.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/ArmorStand.h" "${CMAKE_CURRENT_SOURCE_DIR}/net.minecraft.world.entity.item.h" ) diff --git a/Minecraft.World/stdafx.h b/Minecraft.World/stdafx.h index 81e2fcc6..e233116a 100644 --- a/Minecraft.World/stdafx.h +++ b/Minecraft.World/stdafx.h @@ -126,6 +126,7 @@ typedef XUID GameSessionUID; #include "TilePos.h" #include "ChunkPos.h" #include "compression.h" +#include "Debug.h" #include "PerformanceTimer.h" diff --git a/samples/FourKitTestPlugin/FourKitTestPlugin.cs b/samples/FourKitTestPlugin/FourKitTestPlugin.cs new file mode 100644 index 00000000..18432020 --- /dev/null +++ b/samples/FourKitTestPlugin/FourKitTestPlugin.cs @@ -0,0 +1,658 @@ +using Minecraft.Server.FourKit; +using Minecraft.Server.FourKit.Block; +using Minecraft.Server.FourKit.Command; +using Minecraft.Server.FourKit.Enchantments; +using Minecraft.Server.FourKit.Entity; +using Minecraft.Server.FourKit.Event; +using Minecraft.Server.FourKit.Event.World; +using Minecraft.Server.FourKit.Inventory; +using Minecraft.Server.FourKit.Inventory.Meta; +using Minecraft.Server.FourKit.Plugin; + +namespace FourKitTestPlugin; + +/// +/// Exercises the new FourKit APIs added by the recent upstream sync: +/// chunk additions, chunk events, ender chest inventory, and the +/// disenchant fix on ItemStack.setDurability. +/// +/// Use the /fktest command in-game (or from the console) to run +/// each subtest. Run /fktest help for the full menu. +/// +public class FourKitTestPlugin : ServerPlugin +{ + public override string name => "FourKitTestPlugin"; + public override string version => "1.0.0"; + public override string author => "LCE-Revelations"; + + private static int _chunkLoadCount; + private static int _chunkUnloadCount; + + private static string? _logPath; + private static readonly object _logLock = new(); + + public override void onEnable() + { + _logPath = ResolveLogPath(serverDirectory, dataDirectory); + Log("FourKitTestPlugin enabled."); + Log($"Plugin log file: {_logPath}"); + // ChunkEventLogger is intentionally NOT registered here. Subscribing + // to chunk events flips the C++ HasHandlers mask bit, which disables + // the no-listener fast-path. Use /fktest hookchunks to register it + // when you want to measure dispatch overhead specifically. + TpsProbe.Start(); + + var cmd = FourKit.getCommand("fktest"); + cmd.setDescription("FourKit API smoke tests."); + cmd.setUsage("/fktest "); + cmd.setExecutor(new TestExecutor()); + } + + public override void onDisable() + { + TpsProbe.Stop(); + Log("FourKitTestPlugin disabled."); + } + + internal static int ChunkLoadCount => _chunkLoadCount; + internal static int ChunkUnloadCount => _chunkUnloadCount; + internal static void IncChunkLoad() => Interlocked.Increment(ref _chunkLoadCount); + internal static void IncChunkUnload() => Interlocked.Increment(ref _chunkUnloadCount); + + internal static volatile bool WatchChunks = false; + internal static volatile bool ChunkListenerHooked = false; + + /// + /// Writes a line both to the live server console and to a persistent log + /// file so test results are recoverable after the server window closes. + /// Tries server.log first using shared-write mode; falls back to a + /// sibling fkplugin.log in the server root if the C++ host has the + /// main log opened exclusively. + /// + internal static void Log(string message) + { + string line = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}][INFO][fkplugin] {message}"; + Console.WriteLine(line); + + if (_logPath == null) return; + try + { + lock (_logLock) + { + using var fs = new FileStream(_logPath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite); + using var sw = new StreamWriter(fs); + sw.WriteLine(line); + } + } + catch (Exception ex) + { + Console.WriteLine($"[fkplugin] WARN: failed to append to {_logPath}: {ex.Message}"); + } + } + + private static string ResolveLogPath(string serverDir, string dataDir) + { + string serverLog = Path.Combine(serverDir, "server.log"); + try + { + using var fs = new FileStream(serverLog, FileMode.Append, FileAccess.Write, FileShare.ReadWrite); + return serverLog; + } + catch (IOException) + { + string fallback = Path.Combine(serverDir, "fkplugin.log"); + Console.WriteLine($"[fkplugin] server.log is locked; writing to {fallback} instead."); + return fallback; + } + catch (UnauthorizedAccessException) + { + string fallback = Path.Combine(dataDir, "fkplugin.log"); + Console.WriteLine($"[fkplugin] server.log not writable; writing to {fallback} instead."); + return fallback; + } + } +} + +internal static class TpsProbe +{ + private static readonly object _lock = new(); + private static readonly LinkedList<(double elapsed, int tick)> _samples = new(); + private static System.Threading.Timer? _timer; + private static System.Diagnostics.Stopwatch? _sw; + + public static void Start() + { + _sw = System.Diagnostics.Stopwatch.StartNew(); + // Seed an initial sample so a /fktest tps in the first second is meaningful. + Sample(); + _timer = new System.Threading.Timer(_ => Sample(), null, 1000, 1000); + } + + public static void Stop() + { + _timer?.Dispose(); + _timer = null; + _sw?.Stop(); + _sw = null; + lock (_lock) _samples.Clear(); + } + + private static void Sample() + { + var sw = _sw; + if (sw == null) return; + try + { + int tick = FourKit.getServerTick(); + double elapsed = sw.Elapsed.TotalSeconds; + lock (_lock) + { + _samples.AddLast((elapsed, tick)); + // Keep a 65s window so the 60s average has full data. + while (_samples.Count > 66) _samples.RemoveFirst(); + } + } + catch (Exception ex) + { + Console.WriteLine($"[fkplugin] TpsProbe sample error: {ex.Message}"); + } + } + + public static (int samples, double tps1, double tps5, double tps30, double tps60) Read() + { + (double elapsed, int tick)[] arr; + lock (_lock) + { + if (_samples.Count < 2) return (_samples.Count, 0, 0, 0, 0); + arr = _samples.ToArray(); + } + + var last = arr[^1]; + + double Window(double seconds) + { + for (int j = arr.Length - 2; j >= 0; j--) + { + if (last.elapsed - arr[j].elapsed >= seconds) + { + var first = arr[j]; + double dt = last.elapsed - first.elapsed; + return dt > 0 ? (last.tick - first.tick) / dt : 0; + } + } + // Not enough history yet: report what we have. + var oldest = arr[0]; + double dt0 = last.elapsed - oldest.elapsed; + return dt0 > 0 ? (last.tick - oldest.tick) / dt0 : 0; + } + + return (arr.Length, Window(1), Window(5), Window(30), Window(60)); + } +} + +// Chunk events fire at high frequency under load (16 chunks/player/tick on +// the dedicated server). Doing disk I/O per event tanks server TPS. Default +// to counter-only; /fktest watchchunks toggles verbose disk logging on. +internal sealed class ChunkEventLogger : Listener +{ + [EventHandler(Priority = EventPriority.Monitor)] + public void onChunkLoad(ChunkLoadEvent e) + { + FourKitTestPlugin.IncChunkLoad(); + if (!FourKitTestPlugin.WatchChunks) return; + var chunk = e.getChunk(); + FourKitTestPlugin.Log($"ChunkLoadEvent dim={chunk.getWorld().getDimensionId()} ({chunk.getX()},{chunk.getZ()}) new={e.isNewChunk()}"); + } + + [EventHandler(Priority = EventPriority.Monitor)] + public void onChunkUnload(ChunkUnloadEvent e) + { + FourKitTestPlugin.IncChunkUnload(); + if (!FourKitTestPlugin.WatchChunks) return; + var chunk = e.getChunk(); + FourKitTestPlugin.Log($"ChunkUnloadEvent dim={chunk.getWorld().getDimensionId()} ({chunk.getX()},{chunk.getZ()})"); + } +} + +internal sealed class TestExecutor : CommandExecutor +{ + private static void Reply(CommandSender sender, string message) + { + sender.sendMessage(message); + FourKitTestPlugin.Log($"[/fktest] {message}"); + } + + public bool onCommand(CommandSender sender, Command command, string label, string[] args) + { + if (args.Length == 0) + { + Reply(sender,"Usage: /fktest "); + return true; + } + + try + { + switch (args[0].ToLowerInvariant()) + { + case "help": SendHelp(sender); return true; + case "world": return TestWorld(sender); + case "chunks": return TestLoadedChunks(sender); + case "snapshot": return TestChunkSnapshot(sender, args); + case "entities": return TestChunkEntities(sender, args); + case "loadchunk": return TestLoadUnloadChunk(sender, args); + case "enderchest": return TestEnderChest(sender); + case "disenchant": return TestDisenchant(sender); + case "setblock": return TestSetBlock(sender); + case "chatcolor": return TestChatColor(sender); + case "tps": return TestTps(sender); + case "scatter": return TestScatter(sender, args); + case "hookchunks": + if (FourKitTestPlugin.ChunkListenerHooked) + { + Reply(sender, "Chunk listener already hooked. EventDispatcher has no unregister, so this is one-way for the session."); + return true; + } + FourKit.addListener(new ChunkEventLogger()); + FourKitTestPlugin.ChunkListenerHooked = true; + Reply(sender, "Chunk listener registered. HasHandlers fast-path now off; chunk events will dispatch."); + return true; + case "watchchunks": + FourKitTestPlugin.WatchChunks = !FourKitTestPlugin.WatchChunks; + Reply(sender, $"Verbose chunk-event disk logging {(FourKitTestPlugin.WatchChunks ? "ON" : "OFF")} (only effective once /fktest hookchunks is run)"); + return true; + case "events": + Reply(sender,$"Chunk loads observed: {FourKitTestPlugin.ChunkLoadCount}"); + Reply(sender,$"Chunk unloads observed: {FourKitTestPlugin.ChunkUnloadCount}"); + return true; + default: + Reply(sender,$"Unknown subcommand '{args[0]}'. Try /fktest help"); + return true; + } + } + catch (Exception ex) + { + Reply(sender, $"Test threw: {ex.GetType().Name}: {ex.Message}"); + FourKitTestPlugin.Log($"Exception: {ex}"); + return true; + } + } + + private static void SendHelp(CommandSender sender) + { + Reply(sender,"FourKit test commands:"); + Reply(sender,"/fktest world - World/spawn/seed lookup"); + Reply(sender,"/fktest chunks - List loaded chunks in your world"); + Reply(sender,"/fktest snapshot - Snapshot of chunk under your feet"); + Reply(sender,"/fktest entities - Entities in chunk under your feet"); + Reply(sender,"/fktest loadchunk [dx dz] - Load/unload a chunk relative to you"); + Reply(sender,"/fktest enderchest - Probe ender chest inventory"); + Reply(sender,"/fktest disenchant - Verify setDurability preserves enchants"); + Reply(sender,"/fktest events - Show observed chunk-event counters"); + Reply(sender,"/fktest setblock - Place wool 3 above head via setTypeIdAndData, read back"); + Reply(sender,"/fktest chatcolor - Verify ChatColor parsing/strip/translate"); + Reply(sender,"/fktest tps - Show server TPS over 1s/5s/30s/60s windows"); + Reply(sender,"/fktest scatter [N] - Teleport every online player to a random point within +-N blocks (default 1500)"); + Reply(sender,"/fktest hookchunks - Register the chunk listener (turns OFF the no-listener fast-path)"); + Reply(sender,"/fktest watchchunks - Toggle per-event disk logging (off by default; expensive)"); + } + + private static Player? RequirePlayer(CommandSender sender) + { + if (sender is Player p) return p; + Reply(sender,"This subcommand must be run by a player."); + return null; + } + + private static bool TestScatter(CommandSender sender, string[] args) + { + int range = 1500; + if (args.Length > 1 && int.TryParse(args[1], out int parsed) && parsed > 0) + range = parsed; + + var world = FourKit.getWorld(0); + if (world == null) + { + Reply(sender, "Could not resolve overworld."); + return true; + } + + var rng = new Random(); + int count = 0; + foreach (var p in FourKit.getOnlinePlayers()) + { + int x = rng.Next(-range, range + 1); + int z = rng.Next(-range, range + 1); + int y = world.getHighestBlockYAt(x, z) + 1; + try + { + p.teleport(new Location(world, x, y, z)); + count++; + } + catch (Exception ex) + { + FourKitTestPlugin.Log($"scatter: failed to teleport {p.getName()}: {ex.Message}"); + } + } + Reply(sender, $"Scattered {count} player(s) within ±{range} blocks."); + return true; + } + + private static bool TestTps(CommandSender sender) + { + var (samples, t1, t5, t30, t60) = TpsProbe.Read(); + if (samples < 2) + { + Reply(sender, $"TPS probe warming up ({samples}/2 samples). Try again in a few seconds."); + return true; + } + int tick = FourKit.getServerTick(); + Reply(sender, $"TPS 1s={t1:F2} 5s={t5:F2} 30s={t30:F2} 60s={t60:F2}"); + Reply(sender, $"server tick={tick} samples={samples}"); + return true; + } + + private static bool TestWorld(CommandSender sender) + { + var player = RequirePlayer(sender); + if (player == null) return true; + + var loc = player.getLocation(); + var world = loc?.getWorld(); + if (world == null) + { + Reply(sender,"Could not resolve player world."); + return true; + } + + var spawn = world.getSpawnLocation(); + Reply(sender,$"World name={world.getName()} dim={world.getDimensionId()}"); + Reply(sender,$"Spawn = ({spawn.getBlockX()},{spawn.getBlockY()},{spawn.getBlockZ()}) seed={world.getSeed()} time={world.getTime()}"); + Reply(sender,$"Players in world: {world.getPlayers().Count}"); + return true; + } + + private static bool TestLoadedChunks(CommandSender sender) + { + var player = RequirePlayer(sender); + if (player == null) return true; + + var world = player.getLocation()?.getWorld(); + if (world == null) { Reply(sender,"No world."); return true; } + + var chunks = world.getLoadedChunks(); + Reply(sender,$"Loaded chunks in {world.getName()}: {chunks.Length}"); + int sample = Math.Min(5, chunks.Length); + for (int i = 0; i < sample; i++) + { + var c = chunks[i]; + bool inUse = world.isChunkInUse(c.getX(), c.getZ()); + Reply(sender,$" ({c.getX()},{c.getZ()}) loaded={c.isLoaded()} inUse={inUse}"); + } + return true; + } + + private static bool TestChunkSnapshot(CommandSender sender, string[] args) + { + var player = RequirePlayer(sender); + if (player == null) return true; + + var loc = player.getLocation(); + var world = loc?.getWorld(); + if (world == null || loc == null) { Reply(sender,"No world."); return true; } + + bool includeBiome = args.Length > 1 && args[1].Equals("biome", StringComparison.OrdinalIgnoreCase); + + var chunk = world.getChunkAt(loc); + var snap = chunk.getChunkSnapshot(includeBiome, includeBiome); + + int blocksUnderFeet = 0; + int lx = loc.getBlockX() & 0xF; + int lz = loc.getBlockZ() & 0xF; + for (int ly = 0; ly < 128; ly++) + { + if (snap.getBlockTypeId(lx, ly, lz) != 0) blocksUnderFeet++; + } + + Reply(sender,$"Snapshot of chunk ({chunk.getX()},{chunk.getZ()}) in '{snap.getWorldName()}' captured at tick {snap.getCaptureFullTime()}."); + Reply(sender,$"Non-air column at ({lx},{lz}): {blocksUnderFeet} blocks. Highest = y{snap.getHighestBlockYAt(lx, lz)}."); + if (includeBiome) + { + var biome = snap.getBiome(lx, lz); + Reply(sender,$"Biome at ({lx},{lz}) = {biome}, temp={snap.getRawBiomeTemperature(lx, lz):0.00}, rain={snap.getRawBiomeRainfall(lx, lz):0.00}"); + } + return true; + } + + private static bool TestChunkEntities(CommandSender sender, string[] args) + { + var player = RequirePlayer(sender); + if (player == null) return true; + + var loc = player.getLocation(); + var world = loc?.getWorld(); + if (world == null || loc == null) { Reply(sender,"No world."); return true; } + + var chunk = world.getChunkAt(loc); + var entities = chunk.getEntities(); + Reply(sender,$"Entities in chunk ({chunk.getX()},{chunk.getZ()}): {entities.Length}"); + int sample = Math.Min(8, entities.Length); + for (int i = 0; i < sample; i++) + { + var ent = entities[i]; + Reply(sender,$" id={ent.getEntityId()} type={ent.GetType()}"); + } + return true; + } + + private static bool TestLoadUnloadChunk(CommandSender sender, string[] args) + { + var player = RequirePlayer(sender); + if (player == null) return true; + + var loc = player.getLocation(); + var world = loc?.getWorld(); + if (world == null || loc == null) { Reply(sender,"No world."); return true; } + + int dx = 0, dz = 0; + if (args.Length >= 3 && int.TryParse(args[1], out var px) && int.TryParse(args[2], out var pz)) + { + dx = px; dz = pz; + } + + int cx = (loc.getBlockX() >> 4) + dx; + int cz = (loc.getBlockZ() >> 4) + dz; + + bool wasLoaded = world.isChunkLoaded(cx, cz); + Reply(sender,$"Chunk ({cx},{cz}) loaded? {wasLoaded}"); + + if (!wasLoaded) + { + bool ok = world.loadChunk(cx, cz, true); + Reply(sender,$"loadChunk -> {ok}; nowLoaded={world.isChunkLoaded(cx, cz)}"); + } + else + { + bool inUse = world.isChunkInUse(cx, cz); + if (inUse) + { + Reply(sender,$"Chunk in use by a player; requesting unsafe unload would refuse. Trying unloadChunkRequest(safe=true)."); + bool queued = world.unloadChunkRequest(cx, cz, true); + Reply(sender,$"unloadChunkRequest -> {queued}"); + } + else + { + bool unloaded = world.unloadChunk(cx, cz, true, true); + Reply(sender,$"unloadChunk -> {unloaded}"); + } + } + return true; + } + + private static bool TestEnderChest(CommandSender sender) + { + var player = RequirePlayer(sender); + if (player == null) return true; + + var ender = player.getEnderChest(); + Reply(sender,$"Ender chest size = {ender.getSize()} type={ender.getType()} name='{ender.getName()}'"); + + int filled = 0; + for (int i = 0; i < ender.getSize(); i++) + { + var item = ender.getItem(i); + if (item != null && item.getAmount() > 0) + { + filled++; + if (filled <= 4) + { + Reply(sender,$" slot {i}: {item.getType()} x{item.getAmount()} dur={item.getDurability()}"); + } + } + } + Reply(sender,$"Non-empty slots: {filled}"); + return true; + } + + private static bool TestDisenchant(CommandSender sender) + { + var player = RequirePlayer(sender); + if (player == null) return true; + + var pickaxe = new ItemStack(Material.DIAMOND_PICKAXE, 1, 0); + var meta = pickaxe.getItemMeta(); + meta.addEnchant(EnchantmentType.DIG_SPEAD, 5, true); + meta.addEnchant(EnchantmentType.DURABILITY, 3, true); + pickaxe.setItemMeta(meta); + + int beforeCount = pickaxe.getItemMeta().getEnchants().Count; + Reply(sender,$"Before setDurability: {beforeCount} enchants."); + + pickaxe.setDurability(123); + + var after = pickaxe.getItemMeta().getEnchants(); + Reply(sender,$"After setDurability(123): {after.Count} enchants, durability={pickaxe.getDurability()}"); + foreach (var kv in after) + { + Reply(sender,$" {kv.Key} lvl {kv.Value}"); + } + + bool ok = after.Count == beforeCount; + Reply(sender,ok + ? "PASS: setDurability preserved enchantments." + : "FAIL: setDurability dropped enchantments."); + return true; + } + + private static bool TestSetBlock(CommandSender sender) + { + var player = RequirePlayer(sender); + if (player == null) return true; + + var loc = player.getLocation(); + var world = loc?.getWorld(); + if (world == null || loc == null) { Reply(sender, "No world."); return true; } + + int bx = loc.getBlockX(); + int by = loc.getBlockY() + 3; + int bz = loc.getBlockZ(); + + var block = world.getBlockAt(bx, by, bz); + int originalType = block.getTypeId(); + byte originalData = block.getData(); + Reply(sender, $"Target ({bx},{by},{bz}) before: typeId={originalType} data={originalData}"); + + const int WOOL_ID = 35; + const byte RED_WOOL_DATA = 14; + bool wrote = block.setTypeIdAndData(WOOL_ID, RED_WOOL_DATA, true); + Reply(sender, $"setTypeIdAndData(35, 14, true) -> {wrote}"); + + int afterType = block.getTypeId(); + byte afterData = block.getData(); + Reply(sender, $"After: typeId={afterType} data={afterData}"); + + bool typeOk = afterType == WOOL_ID; + bool dataOk = afterData == RED_WOOL_DATA; + + bool restored = block.setTypeId(originalType, false); + Reply(sender, $"Restore setTypeId({originalType}, false) -> {restored}; typeId now={block.getTypeId()}"); + + Reply(sender, (typeOk && dataOk) + ? "PASS: setTypeIdAndData wrote correct type and data." + : $"FAIL: expected type={WOOL_ID} data={RED_WOOL_DATA}, got type={afterType} data={afterData}."); + return true; + } + + private static bool TestChatColor(CommandSender sender) + { + int pass = 0, fail = 0; + + void Check(string name, bool condition, string detail) + { + if (condition) { pass++; Reply(sender, $" PASS {name}"); } + else { fail++; Reply(sender, $" FAIL {name}: {detail}"); } + } + + Check("COLOR_CHAR is section sign", + ChatColor.COLOR_CHAR == '\u00A7', + $"got U+{(int)ChatColor.COLOR_CHAR:X4}"); + + var red = ChatColor.getByChar('c'); + Check("getByChar('c') -> RED", + red == ChatColor.RED, + $"got {red}"); + + var redUpper = ChatColor.getByChar('C'); + Check("getByChar('C') case-insensitive -> RED", + redUpper == ChatColor.RED, + $"got {redUpper}"); + + var garbage = ChatColor.getByChar('z'); + Check("getByChar('z') -> null", + garbage == null, + $"got {garbage}"); + + Check("RED.getChar() == 'c'", + ChatColor.RED.getChar() == 'c', + $"got '{ChatColor.RED.getChar()}'"); + + Check("RED.isColor() true", + ChatColor.RED.isColor(), + "isColor returned false"); + + Check("RESET.isColor() false", + !ChatColor.RESET.isColor(), + "isColor returned true on RESET"); + + string translated = ChatColor.translateAlternateColorCodes('&', "&chello&r world"); + string expected = $"{ChatColor.COLOR_CHAR}chello{ChatColor.COLOR_CHAR}r world"; + Check("translateAlternateColorCodes('&', ...)", + translated == expected, + $"got '{translated}' expected '{expected}'"); + + string coloredInput = $"{ChatColor.COLOR_CHAR}ahello{ChatColor.COLOR_CHAR}r world"; + string? stripped = ChatColor.stripColor(coloredInput); + Check("stripColor removes color codes", + stripped == "hello world", + $"got '{stripped}'"); + + string? strippedNull = ChatColor.stripColor(null); + Check("stripColor(null) returns null", + strippedNull == null, + "got non-null"); + + string lastColors = ChatColor.getLastColors($"{ChatColor.COLOR_CHAR}ahello {ChatColor.COLOR_CHAR}bworld"); + Check("getLastColors finds trailing color", + lastColors == ChatColor.AQUA.ToString(), + $"got '{lastColors}'"); + + string composed = ChatColor.GREEN + "hi"; + Check("operator + builds prefix", + composed == $"{ChatColor.COLOR_CHAR}ahi", + $"got '{composed}'"); + + sender.sendMessage(ChatColor.GREEN + "green " + ChatColor.RED + "red " + ChatColor.RESET + "plain"); + + Reply(sender, $"ChatColor checks: {pass} passed, {fail} failed."); + return true; + } +} diff --git a/samples/FourKitTestPlugin/FourKitTestPlugin.csproj b/samples/FourKitTestPlugin/FourKitTestPlugin.csproj new file mode 100644 index 00000000..e9d453ed --- /dev/null +++ b/samples/FourKitTestPlugin/FourKitTestPlugin.csproj @@ -0,0 +1,17 @@ + + + net10.0 + enable + enable + FourKitTestPlugin + FourKitTestPlugin + true + + + + + false + runtime + + + diff --git a/samples/TpsPlugin/TpsPlugin.cs b/samples/TpsPlugin/TpsPlugin.cs new file mode 100644 index 00000000..8291623c --- /dev/null +++ b/samples/TpsPlugin/TpsPlugin.cs @@ -0,0 +1,123 @@ +using Minecraft.Server.FourKit; +using Minecraft.Server.FourKit.Command; +using Minecraft.Server.FourKit.Plugin; + +namespace TpsPlugin; + +/// +/// Minimal plugin exposing /tps. Samples the server tick counter once per +/// second and reports tick-rate averages over 1s/5s/30s/60s windows. +/// +public class TpsPlugin : ServerPlugin +{ + public override string name => "TpsPlugin"; + public override string version => "1.0.0"; + public override string author => "LCE-Revelations"; + + public override void onEnable() + { + TpsProbe.Start(); + + var cmd = FourKit.getCommand("tps"); + cmd.setDescription("Show server TPS over 1s/5s/30s/60s windows."); + cmd.setUsage("/tps"); + cmd.setExecutor(new TpsExecutor()); + } + + public override void onDisable() + { + TpsProbe.Stop(); + } +} + +internal static class TpsProbe +{ + private static readonly object _lock = new(); + private static readonly LinkedList<(double elapsed, int tick)> _samples = new(); + private static System.Threading.Timer? _timer; + private static System.Diagnostics.Stopwatch? _sw; + + public static void Start() + { + _sw = System.Diagnostics.Stopwatch.StartNew(); + Sample(); + _timer = new System.Threading.Timer(_ => Sample(), null, 1000, 1000); + } + + public static void Stop() + { + _timer?.Dispose(); + _timer = null; + _sw?.Stop(); + _sw = null; + lock (_lock) _samples.Clear(); + } + + private static void Sample() + { + var sw = _sw; + if (sw == null) return; + try + { + int tick = FourKit.getServerTick(); + double elapsed = sw.Elapsed.TotalSeconds; + lock (_lock) + { + _samples.AddLast((elapsed, tick)); + // 65s window so the 60s average has full data. + while (_samples.Count > 66) _samples.RemoveFirst(); + } + } + catch + { + // Probe is best-effort; never let sampling errors take down the host. + } + } + + public static (int samples, double tps1, double tps5, double tps30, double tps60) Read() + { + (double elapsed, int tick)[] arr; + lock (_lock) + { + if (_samples.Count < 2) return (_samples.Count, 0, 0, 0, 0); + arr = _samples.ToArray(); + } + + var last = arr[^1]; + + double Window(double seconds) + { + for (int j = arr.Length - 2; j >= 0; j--) + { + if (last.elapsed - arr[j].elapsed >= seconds) + { + var first = arr[j]; + double dt = last.elapsed - first.elapsed; + return dt > 0 ? (last.tick - first.tick) / dt : 0; + } + } + var oldest = arr[0]; + double dt0 = last.elapsed - oldest.elapsed; + return dt0 > 0 ? (last.tick - oldest.tick) / dt0 : 0; + } + + return (arr.Length, Window(1), Window(5), Window(30), Window(60)); + } +} + +internal sealed class TpsExecutor : CommandExecutor +{ + public bool onCommand(CommandSender sender, Command command, string label, string[] args) + { + var (samples, t1, t5, t30, t60) = TpsProbe.Read(); + if (samples < 2) + { + sender.sendMessage($"TPS probe warming up ({samples}/2 samples). Try again in a few seconds."); + return true; + } + int tick = FourKit.getServerTick(); + sender.sendMessage($"TPS 1s={t1:F2} 5s={t5:F2} 30s={t30:F2} 60s={t60:F2}"); + sender.sendMessage($"server tick={tick} samples={samples}"); + return true; + } +} diff --git a/samples/TpsPlugin/TpsPlugin.csproj b/samples/TpsPlugin/TpsPlugin.csproj new file mode 100644 index 00000000..3e53b85a --- /dev/null +++ b/samples/TpsPlugin/TpsPlugin.csproj @@ -0,0 +1,17 @@ + + + net10.0 + enable + enable + TpsPlugin + TpsPlugin + true + + + + + false + runtime + + + diff --git a/tools/stress-test/stress_test.py b/tools/stress-test/stress_test.py index e746f380..414337d5 100644 --- a/tools/stress-test/stress_test.py +++ b/tools/stress-test/stress_test.py @@ -20,6 +20,7 @@ Options: import argparse import logging +import math import os import random import secrets @@ -68,7 +69,9 @@ CIPHER_ON_PATTERN = ( b"\x00\x00" ) +CIPHER_KEY_CHANNEL = "MC|CKey" CIPHER_ACK_CHANNEL = "MC|CAck" +CIPHER_ON_CHANNEL = "MC|COn" IDENTITY_TOKEN_ISSUE = "MC|CTIssue" IDENTITY_TOKEN_CHALLENGE = "MC|CTChallenge" IDENTITY_TOKEN_RESPONSE = "MC|CTResponse" @@ -130,14 +133,17 @@ class Stats: # Movement packet builder # --------------------------------------------------------------------------- -MOVE_PLAYER = 0x0D # MovePlayerPacket ID +MOVE_PLAYER = 0x0D # MovePlayerPacket::PosRot — what we send AND what server sends for teleports. def build_move_player(x: float, y: float, z: float, yaw: float, pitch: float, on_ground: bool) -> bytes: + # Wire order matches MovePlayerPacket::PosRot::write: x, y (feet), + # yView (eye), z, yaw, pitch, flags. Server kicks for IllegalStance + # if (yView - y) is outside [0.1, 1.65], so feet must come first. dos = DataOutputStream() dos.write_double(x) - dos.write_double(y + 1.62) # stance dos.write_double(y) + dos.write_double(y + 1.62) dos.write_double(z) dos.write_float(yaw) dos.write_float(pitch) @@ -174,6 +180,13 @@ class StressBot: self._identity_token = b"" self._entity_id = 0 self._running = True + # Server-tracked position. Initialized when server sends its first + # MovePlayer::PosRot teleport after login, and updated whenever the + # server teleports us (eg. plugin scatter, anti-cheat correction). + self._pos_x = 0.0 + self._pos_y = 64.0 + self._pos_z = 0.0 + self._pos_initialized = False def log(self, msg: str) -> None: if not self.quiet: @@ -250,8 +263,6 @@ class StressBot: hold_end = time.time() + hold_time last_keepalive = time.time() keepalive_counter = 0 - move_x, move_z = random.uniform(-50, 50), random.uniform(-50, 50) - move_y = 65.0 while time.time() < hold_end and self._running: # Drain incoming data @@ -276,13 +287,22 @@ class StressBot: self.stats.keepalives_sent += 1 last_keepalive = now - # Movement packets every 50ms - if self.send_moves: - move_x += random.uniform(-0.5, 0.5) - move_z += random.uniform(-0.5, 0.5) + # Movement packets every 50ms. We can't do real travel because + # the server's anti-cheat compares our claimed position against + # what its own physics computes, and we don't simulate collision + # or gravity. Instead we drift ±0.3 blocks from whatever + # position the server most recently teleported us to. To spread + # bots out, use the test plugin's /fktest scatter from in-game. + if self.send_moves and self._pos_initialized: + new_x = self._pos_x + random.uniform(-0.3, 0.3) + new_z = self._pos_z + random.uniform(-0.3, 0.3) yaw = random.uniform(0, 360) self._send_packet(MOVE_PLAYER, - build_move_player(move_x, move_y, move_z, yaw, 0.0, True)) + build_move_player(new_x, self._pos_y, new_z, yaw, 0.0, True)) + # Optimistically update; server will correct us via PosRot + # if it disagreed (eg. we drifted into a block). + self._pos_x = new_x + self._pos_z = new_z with self.stats.lock: self.stats.moves_sent += 1 time.sleep(0.05) @@ -294,11 +314,23 @@ class StressBot: return True def _do_cipher_scan(self) -> None: - """Scan for cipher handshake for up to 3 seconds.""" + """Wait for the cipher handshake to finish or up to ~4s. + + Returns early once both keys are exchanged. The upper bound has to + cover the worst case where a stack of plaintext setup packets + (level info, scoreboard, initial chunks) sits in front of MC|CKey + in the recv buffer. The server's own cipher-handshake grace is + 100 ticks (~5s). + """ scan_start = time.time() scan_buf = bytearray() - while time.time() - scan_start < 0.5 and self._running: + while time.time() - scan_start < 4.0 and self._running: + # _handle_custom_payload may have already activated cipher via + # the drain path inside _read_until_packet. + if self._cipher_key and self._recv_cipher: + return + try: chunk = self._sock.recv(65536) if not chunk: @@ -393,6 +425,14 @@ class StressBot: pass return True + # Handle cipher handshake during the login wait. With + # require-secure-client, the server holds the Login response + # behind the security gate until cipher activates, so the + # gate-bypass MC|CKey/MC|COn frames arrive before LOGIN. + # Dropping them here would deadlock both sides. + elif packet_id == CUSTOM_PAYLOAD: + self._handle_custom_payload(data) + elif packet_id == DISCONNECT: try: dis = DataInputStream(data) @@ -428,16 +468,51 @@ class StressBot: # Handle identity tokens if packet_id == CUSTOM_PAYLOAD: self._handle_custom_payload(data) + elif packet_id == MOVE_PLAYER: + self._handle_server_move(data) + + def _handle_server_move(self, data: bytes) -> None: + """Track server's view of our position. PosRot format: + double x, double y, double yView, double z, float yRot, float xRot, byte flags.""" + try: + dis = DataInputStream(data) + x = dis.read_double() + y = dis.read_double() + _yView = dis.read_double() + z = dis.read_double() + self._pos_x = x + self._pos_y = y + self._pos_z = z + self._pos_initialized = True + except Exception: + pass def _handle_custom_payload(self, data: bytes) -> None: - """Handle identity token packets.""" + """Handle cipher handshake and identity token channels.""" try: dis = DataInputStream(data) channel = dis.read_utf() length = dis.read_short() payload = dis.read_raw(length) if length > 0 else b"" - if channel == IDENTITY_TOKEN_ISSUE and len(payload) == 32: + # Cipher channels arrive in plaintext before encryption is active. + # Handle them here so the bot survives bursts where the whole + # handshake frame lands in a single recv(), bypassing the leftover + # byte-pattern scan. + if channel == CIPHER_KEY_CHANNEL and len(payload) == 32 and not self._cipher_key: + self._cipher_key = payload[:16] + self._cipher_iv = payload[16:32] + self.log(f"[{self.name}] got cipher key") + self._send_packet(CUSTOM_PAYLOAD, + build_custom_payload(CIPHER_ACK_CHANNEL)) + iv_send = bytearray(self._cipher_iv) + iv_send[0] ^= 0x80 + self._send_cipher = CipherState(self._cipher_key, bytes(iv_send)) + elif channel == CIPHER_ON_CHANNEL: + if self._cipher_key and not self._recv_cipher: + self._recv_cipher = CipherState(self._cipher_key, self._cipher_iv) + self.log(f"[{self.name}] cipher active") + elif channel == IDENTITY_TOKEN_ISSUE and len(payload) == 32: self._identity_token = payload self.log(f"[{self.name}] got identity token") elif channel == IDENTITY_TOKEN_CHALLENGE: diff --git a/tools/stress-test/test_fourkit_chunk.bat b/tools/stress-test/test_fourkit_chunk.bat new file mode 100644 index 00000000..95712630 --- /dev/null +++ b/tools/stress-test/test_fourkit_chunk.bat @@ -0,0 +1,14 @@ +@echo off +REM FourKit chunk + move event stress: 50 concurrent moving bots held for 1-2min +REM each, exercising FireChunkLoad / FireChunkUnload / FirePlayerMove. Validates +REM the HasHandlers fast-path and Server GC at the 50-player target. +REM +REM Set require-secure-client=false in server.properties before running. The +REM 100-tick cipher handshake grace cannot keep up with 50 simultaneous bot +REM joins, which is unrelated to what this test is measuring. +set /p HOST="Server IP [127.0.0.1]: " || set HOST=127.0.0.1 +set /p PORT="Server Port [25565]: " || set PORT=25565 +if "%HOST%"=="" set HOST=127.0.0.1 +if "%PORT%"=="" set PORT=25565 +python "%~dp0stress_test.py" %HOST% %PORT% --bots 50 --burst 10 --move --hold 60 120 --ramp 0.5 --duration 600 --cycles 0 --quiet +pause diff --git a/tools/stress-test/test_fourkit_steady.bat b/tools/stress-test/test_fourkit_steady.bat new file mode 100644 index 00000000..4a6250a0 --- /dev/null +++ b/tools/stress-test/test_fourkit_steady.bat @@ -0,0 +1,29 @@ +@echo off +REM FourKit steady-state TPS validation: 50 concurrent moving bots that stay +REM connected for the full duration with NO disconnect/reconnect rotation. The +REM "real workload" complement to test_fourkit_chunk.bat. Isolates steady-state +REM per-tick cost from the mass-disconnect cleanup spikes that bot rotation +REM triggers. Validates that chunk-send throttles hold 20 TPS at scale once +REM the initial scatter chunk-load wave has drained. +REM +REM Suggested in-game routine while this is running: +REM 1. Wait ~10s for bots to fully join. +REM 2. Run /fktest scatter to spread them across the world. +REM 3. Wait ~30-60s for chunk-load wave to drain. +REM 4. Run /fktest tps and read the 5s/30s/60s windows. +REM +REM Set require-secure-client=false in server.properties before running. The +REM 100-tick cipher handshake grace cannot keep up with 50 simultaneous bot +REM joins, which is unrelated to what this test is measuring. +set /p HOST="Server IP [127.0.0.1]: " || set HOST=127.0.0.1 +set /p PORT="Server Port [25565]: " || set PORT=25565 +if "%HOST%"=="" set HOST=127.0.0.1 +if "%PORT%"=="" set PORT=25565 +REM --hold 99999 99999 : bots never reach the hold timeout during the run, so +REM they stay connected for the full duration. +REM --cycles 0 : the spawner keeps launching until --bots is reached. +REM (cycles is a global spawn counter, NOT a per-bot +REM reconnect cap; cycles=1 would cap total spawns at 1.) +REM --duration 600 : 10-minute run; cap at whatever you need +python "%~dp0stress_test.py" %HOST% %PORT% --bots 50 --burst 10 --move --hold 99999 99999 --ramp 0.5 --duration 600 --cycles 0 --quiet +pause