From 2550f14564aec8835f23ade531989eec49e80e87 Mon Sep 17 00:00:00 2001 From: wed150 Date: Sun, 5 Jul 2026 14:52:35 +0800 Subject: [PATCH] feat: add entity management form --- src/Features/Cleaner.h | 4 + src/Features/MobForm.cpp | 288 ++++++++++++++++++++++++++++++ src/Features/UnloadActorClean.cpp | 4 +- src/Language.h | 48 +++++ src/Mod.cpp | 9 + src/Mod.h | 2 +- src/RegisterCommand.cpp | 14 +- 7 files changed, 366 insertions(+), 3 deletions(-) create mode 100644 src/Features/MobForm.cpp diff --git a/src/Features/Cleaner.h b/src/Features/Cleaner.h index a8f52ee..72b76d4 100644 --- a/src/Features/Cleaner.h +++ b/src/Features/Cleaner.h @@ -1,6 +1,9 @@ #pragma once #include "Global.h" +#include +extern void MainForm(Player* player); + using namespace ll::chrono_literals; namespace Cleaner { @@ -17,6 +20,7 @@ extern void CleanTask(); extern void reloadCleaner(); extern void loadCleaner(); extern void unloadCleaner(); +extern bool shouldIgnore(gmlib::GMActor* ac); extern bool isMatch(std::string& A, std::string& B); diff --git a/src/Features/MobForm.cpp b/src/Features/MobForm.cpp new file mode 100644 index 0000000..6020892 --- /dev/null +++ b/src/Features/MobForm.cpp @@ -0,0 +1,288 @@ +#include "Cleaner.h" +#include "Global.h" +#include "Mod.h" + +#include "ll/api/base/Containers.h" +#include "ll/api/service/Bedrock.h" +#include "mc/common/Globals.h" +#include "mc/world/level/Level.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using UnloadEntityMap = std::unordered_map>; + +struct { + std::chrono::system_clock::time_point lastUpdate{}; + std::atomic_bool isUpdating{false}; + UnloadEntityMap offlineEntities{}; + ll::SmallDenseSet waitedPlayers{}; + std::atomic_bool requireUpdate{false}; + + std::mutex offlineEntitiesMutex; // ✅ 新增 +} OfflineEntity; + +auto typeNameToLocString(std::string const& typeName) { + return EntityTypeToLocString( + EntityTypeFromString(typeName), + ActorTypeNamespaceRules::ReturnWithoutNamespace + ); +} + +auto getAllEntityTypes() { + return std::ranges::to>( + ll::service::getLevel()->getRuntimeActorList() + | std::views::transform(&Actor::getTypeName) + ); +} + +auto getEntityCountByType(std::string const& typeName) { + return std::ranges::count_if( + ll::service::getLevel()->getRuntimeActorList(), + [&](const Actor* a) { return a && a->getTypeName() == typeName; } + ); +} + +void ConfirmForm(Player& player, bool isOnline, const std::string& TypeName, bool needCondition) { + auto fm = ll::form::ModalForm( + tr("cleaner.form.confirm.title"), + tr(isOnline&&needCondition?"cleaner.form.confirm.important" + :"cleaner.form.confirm.warning",{ + tr(typeNameToLocString(TypeName)), + TypeName, + std::to_string( + isOnline + ? getEntityCountByType(TypeName) + : OfflineEntity.offlineEntities[TypeName].size() + ) + }), + tr("cleaner.form.confirm.confirm"), + tr("cleaner.form.confirm.cancel") + ); + + fm.sendTo(player, [isOnline, TypeName, needCondition]( + const Player& player, + ll::form::ModalFormResult rst, + ll::form::FormCancelReason res + ) { + if (res) return; + + if (!rst || rst.value() != ll::form::ModalFormSelectedButton::Upper) + return; + + if (isOnline) { + for (auto en : ll::service::getLevel()->getRuntimeActorList()) { + auto actor = (gmlib::GMActor*)en; + + if (en->getTypeName() == TypeName && + (!needCondition || !Cleaner::shouldIgnore(actor))) { + en->despawn(); + } + } + } else { + if (!OfflineEntity.isUpdating.exchange(true)) { + std::thread([TypeName]() { + { + std::lock_guard lock( + OfflineEntity.offlineEntitiesMutex + ); + + auto it = OfflineEntity.offlineEntities.find(TypeName); + if (it != OfflineEntity.offlineEntities.end()) { + for (auto& en : it->second) { + if (en.isValid()) en.remove(); + } + OfflineEntity.offlineEntities.erase(it); + } + } + + OfflineEntity.isUpdating.store(false); + }).detach(); + } else { + player.sendMessage(tr("")); + } + } + }); +} + +void SettingCleanForm(Player& player, bool isOnline, const std::string& TypeName) { + if (!isOnline) { + ConfirmForm(player, isOnline, TypeName, false); + return; + } + + auto fm = ll::form::CustomForm(tr("cleaner.form.setting.title")); + fm.appendLabel( + tr("cleaner.form.setting.content",{ + tr(typeNameToLocString(TypeName)), + TypeName, + std::to_string( + isOnline + ? getEntityCountByType(TypeName) + : OfflineEntity.offlineEntities[TypeName].size() + ) + }) + ); + fm.appendToggle( + "cleanIgnoreActor", + tr("cleaner.form.setting.cleanIgnore"), + false, + tr("cleaner.form.setting.cleanIgnore.tip") + ); + + fm.sendTo(player, [TypeName, isOnline]( + Player& player, + const ll::form::CustomFormResult& rst, + ll::form::FormCancelReason res + ) { + if (!rst) return; + + bool cleanIgnoreActor = + std::get(rst->at("cleanIgnoreActor")); + + ConfirmForm(player, isOnline, TypeName, !cleanIgnoreActor); + }); +} + +void OnlineEntityForm(Player& player) { + auto fm = ll::form::SimpleForm(tr("cleaner.form.runtime.title"), tr("cleaner.form.runtime.content")); + + for (auto& entityID : getAllEntityTypes()) { + fm.appendButton( + typeNameToLocString(entityID), + [entityID](Player& player) { + SettingCleanForm(player, true, entityID); + } + ); + } + + fm.sendTo(player); +} + +void updateUnloadEntityMap(const Player& player, bool notice = false) { + if (OfflineEntity.lastUpdate.time_since_epoch().count() == 0) + OfflineEntity.requireUpdate.store(true); + + if (OfflineEntity.lastUpdate >= + std::chrono::system_clock::now() - std::chrono::minutes(10)) { + + if (notice) player.sendMessage(tr("cleaner.unload.updated.warn")); + + return; + } + + if (!OfflineEntity.isUpdating.exchange(true) && + OfflineEntity.requireUpdate) { + + std::thread([]() { + UnloadEntityMap offlineEntityMap{}; + + + gmlib::UnloadedActor::foreachUnloadedActor( + [&](gmlib::UnloadedActor& actor) -> bool { + offlineEntityMap[actor.getTypeName()] + .emplace_back(std::move(actor)); + return true; + } + ); + + + { + std::lock_guard lock( + OfflineEntity.offlineEntitiesMutex + ); + + for (auto& playerID : OfflineEntity.waitedPlayers) { + if (auto pl = + ll::service::getLevel()->getPlayer(playerID)) + pl->sendMessage(tr("cleaner.unload.updated.msg")); + } + + OfflineEntity.offlineEntities.swap(offlineEntityMap); + + } + + OfflineEntity.lastUpdate = std::chrono::system_clock::now(); + OfflineEntity.isUpdating.store(false); + }).detach(); + + } else if (notice) { + player.sendMessage(tr("cleaner.unload.update.warn")); + } +} + +void OfflineEntityForm(Player& player) { + Cleaner::Entry::getInstance().getSelf().getLogger().info("preload"); + updateUnloadEntityMap(player); + + auto fm = ll::form::SimpleForm(tr("cleaner.form.unload.title"), tr("cleaner.form.unload.content")); + + if (OfflineEntity.lastUpdate != + std::chrono::system_clock::time_point{}) { + + fm.appendLabel( + tr("cleaner.form.unload.updated",{std::format("{:%Y-%m-%d %H:%M}", OfflineEntity.lastUpdate)}) + ); + } + + if (OfflineEntity.isUpdating.load()) { + fm.appendLabel( + tr("cleaner.form.unload.updating") + ); + + { + std::lock_guard lock( + OfflineEntity.offlineEntitiesMutex + ); + OfflineEntity.waitedPlayers.insert( + player.getOrCreateUniqueID() + ); + } + + fm.appendButton(tr("cleaner.form.unload.refresh"), [](Player& player) { + OfflineEntityForm(player); + }); + } else { + fm.appendButton(tr("cleaner.form.unload.refresh"), [](Player& player) { + OfflineEntity.requireUpdate.store(true); + updateUnloadEntityMap(player, true); + }); + } + + { + std::lock_guard lock( + OfflineEntity.offlineEntitiesMutex + ); + + for (auto& entity : OfflineEntity.offlineEntities) { + fm.appendButton( + typeNameToLocString(entity.first), + [typeName = entity.first](Player& player) { + SettingCleanForm(player, false, typeName); + } + ); + } + } + + fm.sendTo(player); +} + +void MainForm(Player* player) { + auto fm = ll::form::SimpleForm(tr("cleaner.form.main.title"), tr("cleaner.form.main.context")); + + fm.appendButton(tr("cleaner.form.main.runtime"), [](Player& player) { + OnlineEntityForm(player); + }); + + fm.appendButton(tr("cleaner.form.main.unload"), [](Player& player) { + OfflineEntityForm(player); + }); + + fm.sendTo(*player); +} diff --git a/src/Features/UnloadActorClean.cpp b/src/Features/UnloadActorClean.cpp index 895a486..c5bf442 100644 --- a/src/Features/UnloadActorClean.cpp +++ b/src/Features/UnloadActorClean.cpp @@ -5,7 +5,8 @@ namespace UnloadActorClean { void cleanUnloadActor() { auto& config = Cleaner::Entry::getInstance().getConfig(); - gmlib::UnloadedActor::foreachUnloadedActor( + std::thread([config]() { + gmlib::UnloadedActor::foreachUnloadedActor( [config](gmlib::UnloadedActor& actor) -> bool { for (auto& actorname : config.UnloadActorClean.CleanList) { if (actor.getTypeName() == actorname) { @@ -15,5 +16,6 @@ void cleanUnloadActor() { return true; } ); + }).detach(); } } // namespace UnloadActorClean \ No newline at end of file diff --git a/src/Language.h b/src/Language.h index 0dbd265..243c0e4 100644 --- a/src/Language.h +++ b/src/Language.h @@ -35,6 +35,30 @@ std::string en_US = R"( cleaner.vote.title=Vote Cleaning cleaner.vote.voted=You have been voted! cleaner.vote.voteMessage=%1$s lunched a clean vote. If you agree cleaning entities but did not received a form, please type command /voteclean to vote. + cleaner.form.main.title=§e§lCleaner§r - Main Menu + cleaner.form.main.context=Select entity category to clean: + cleaner.form.main.runtime=Loaded Entities + cleaner.form.main.unload=Unloaded Entities + cleaner.form.runtime.title=§e§lCleaner§r - Loaded Entity Manager + cleaner.form.runtime.content=Select loaded entity to clean: + cleaner.form.unload.title=§e§lCleaner§r - Unloaded Entity Manager + cleaner.form.unload.content=Select unloaded entity to clean: + cleaner.form.unload.updating=§2Updating...§r\nIf your world contains many entities, this may take §b a long time (~20k entities/min)§r. You can close this panel first; you'll be notified when the update finishes. + cleaner.form.unload.updated=Last updated: §b%1$s§r\n§eData may be out of sync. Click "Refresh" to update. Max 1 refresh per 10 minutes.\n§c§lWarning: Cleaning unloaded entities will force-remove ALL of them, including named, tamed, and ignored ones. Proceed with caution. + cleaner.form.unload.refresh=Refresh + cleaner.unload.updated.msg=§e§lUnloaded entity list§r has finished loading + cleaner.unload.update.warn=§g§lUnloaded entity list is already updating, cannot trigger again§r + cleaner.unload.updated.warn=§g§lUnloaded entity list refresh too frequent (max once per 10 min)§r + cleaner.form.setting.title=§e§lCleaner§r - Clean Options + cleaner.form.setting.content=Deleting entities\n§b%1$s (%2$s)\n§eTotal: %3$s + cleaner.form.setting.cleanIgnore=Force Clean All Entities + cleaner.form.setting.cleanIgnore.tip=§c§lDangerous: §r§gThis will clean ALL entities including named, tamed, and config-ignored ones + cleaner.form.confirm.title=Confirmation + cleaner.form.confirm.warning=§c§lConfirm dangerous operation:§r\nConfirm §c§lforce §rdeletion of\n§b%1$s (%2$s)\n§eTotal: %3$s ? + cleaner.form.confirm.important=§u§lConfirm operation:§r\nConfirm deletion of\n§b%1$s (%2$s)\n§eTotal: %3$s ? This will force-remove ALL including named, tamed, config-ignored, etc. Proceed with caution. + cleaner.form.confirm.confirm=Confirm + cleaner.form.confirm.cancel=Cancel + cleaner.confirm.unload.warn=Form is updating, deletion temporarily unavailable )"; std::string zh_CN = R"( @@ -71,4 +95,28 @@ std::string zh_CN = R"( cleaner.vote.title=投票清理 cleaner.vote.voted=你已经投过票了! cleaner.vote.voteMessage=%1$s 发起了服务器实体清理投票!如果同意清理但是未收到表单,请输入命令 /voteclean 投票。拒绝清理请忽略此信息。 + cleaner.form.main.title=§e§lCleaner§r 主菜单 + cleaner.form.main.context=选择你要清理的生物种类: + cleaner.form.main.runtime=已加载生物 + cleaner.form.main.unload=未加载生物 + cleaner.form.runtime.title=§e§lCleaner§r-已加载生物管理 + cleaner.form.runtime.content=选择你要清理的已加载生物: + cleaner.form.unload.title=§e§lCleaner-未加载生物管理 + cleaner.form.unload.content=选择你要清理的未加载生物: + cleaner.form.unload.updating=§2正在更新中...§r\n如果您存档中有较多实体,这可能花费§b较长时间(大约2w个生物/分钟)§r。你可以先关闭这个面板, 更新完成后将会通知您。 + cleaner.form.unload.updated=数据最后更新时间:§b %1$s \n§e数据有时不同步,你可以点击刷新按钮进行刷新。每10分钟最多进行一次刷新\n§c§l注意, 清理未加载生物会全部强制清除, 包过有命名, 被驯服等, 请谨慎操作 + cleaner.form.unload.refresh=刷新 + cleaner.unload.updated.msg=§e§l离线生物列表§r已完成加载 + cleaner.unload.update.warn=§g§l离线生物列表正在更新,不能重复触发§r + cleaner.unload.updated.warn=§g§l离线生物列表更新过频繁,每10分钟最多一次 + cleaner.form.setting.title=§e§lCleaner-清理选项 + cleaner.form.setting.content=正在删除生物\n§b%1$s (%2$s)\n§e共计: %3$s 个 + cleaner.form.setting.cleanIgnore=强制清理全部生物 + cleaner.form.setting.cleanIgnore.tip=§c§l危险操作: §r§g这会清理包过有命名, 被驯服, 配置忽略的所有生物 + cleaner.form.confirm.title=确认消息 + cleaner.form.confirm.warning=§c§l确认危险操作:§r\n确认§c§l强制§r删除生物\n§b%1$s (%2$s)\n§e共计: %3$s 个? + cleaner.form.confirm.important=§u§l确认操作:§r\n确认删除生物\n§b%1$s (%2$s)\n§e共计: %3$s 个?这会强制全部清除, 包过有命名, 被驯服, 配置忽略等, 请谨慎操作 + cleaner.form.confirm.confirm=确认 + cleaner.form.confirm.cancel=取消 + cleaner.confirm.unload.warn=表单正在更新, 暂时无法删除 )"; \ No newline at end of file diff --git a/src/Mod.cpp b/src/Mod.cpp index 17f2d72..cd7bb31 100644 --- a/src/Mod.cpp +++ b/src/Mod.cpp @@ -3,6 +3,8 @@ #include "Global.h" #include "Language.h" #include "ll/api/utils/ErrorUtils.h" +#include "ll/api/memory/Hook.h" +#include "mc/world/actor/Actor.h" #include "gmlib/mc/locale/I18nAPI.h" #include "gmlib/gm/data/TpsStatus.h" @@ -27,12 +29,14 @@ bool Entry::enable() { gmlib::I18nAPI::updateOrCreateLanguageFile(getSelf().getLangDir(), "en_US", en_US); gmlib::I18nAPI::updateOrCreateLanguageFile(getSelf().getLangDir(), "zh_CN", zh_CN); gmlib::I18nAPI::loadLanguagesFromDirectory(getSelf().getLangDir()); + gmlib::I18nAPI::chooseLanguage(Cleaner::Entry::getInstance().getConfig().language); Cleaner::ListenEvents(); RegisterCommands(); Cleaner::loadCleaner(); getSelf().getLogger().info("Cleaner Loaded!"); getSelf().getLogger().info("Author: GroupMountain"); getSelf().getLogger().info("Repository: https://github.com/GroupMountain/Cleaner"); + return true; } @@ -42,6 +46,11 @@ bool Entry::disable() { return true; } +bool Entry::unload() { + + return true; +} + Config& Entry::getConfig() { return mConfig.value(); } void Entry::saveConfig() { ll::config::saveConfig(*mConfig, getSelf().getConfigDir() / "config.json"); } diff --git a/src/Mod.h b/src/Mod.h index d1d0941..060e709 100644 --- a/src/Mod.h +++ b/src/Mod.h @@ -25,7 +25,7 @@ public: // TODO: Implement this method if you need to unload the mod. // /// @return True if the mod is unloaded successfully. - // bool unload(); + bool unload(); Config& getConfig(); diff --git a/src/RegisterCommand.cpp b/src/RegisterCommand.cpp index a69f4f3..f49924c 100644 --- a/src/RegisterCommand.cpp +++ b/src/RegisterCommand.cpp @@ -4,7 +4,7 @@ #include "mc/server/commands/CommandOutput.h" struct CleanerParam { enum class Despawn { despawn } despawn; - enum class Action { tps, clean, reload, mspt } action; + enum class Action { gui ,tps, clean, reload, mspt } action; enum class DespawnTime { despawntime } despawntime; int ticks; CommandSelector entity; @@ -24,6 +24,14 @@ void RegCleanerCommand() { return output.error(tr("cleaner.command.error.noTarget")); } for (auto en : ens) { + /*NO NEED + *Tip: Player don't exit through normal logic when despawn player + *That may cause other plugins crash,causing Levilamina crash + *if (en->hasCategory(ActorCategory::Player)) { + auto pl=(Player*)en; + pl->disconnect("You've been despawn by Cleaner"); + continue; + }*/ en->despawn(); } return output.success(tr("cleaner.command.despawnSuccess", {S(ens.size())})); @@ -61,6 +69,10 @@ void RegCleanerCommand() { Cleaner::reloadCleaner(); return output.success(tr("cleaner.output.reload")); } + case CleanerParam::Action::gui: + if (origin.getOriginType()==CommandOriginType::Player) + MainForm((Player*)origin.getEntity()); + break; } } );