diff --git a/src/AutoClear/AutoClear.cs b/src/AutoClear/AutoClear.cs
deleted file mode 100644
index 061e0cf67..000000000
--- a/src/AutoClear/AutoClear.cs
+++ /dev/null
@@ -1,168 +0,0 @@
-using LazyAPI;
-using Terraria;
-using Terraria.GameContent.Events;
-using TerrariaApi.Server;
-using TShockAPI;
-
-namespace AutoClear;
-
-[ApiVersion(2, 1)]
-public class AutoClear(Main game) : LazyPlugin(game)
-{
- public override string Author => "大豆子[Mute适配1447],肝帝熙恩更新";
- public override string Description => GetString("智能扫地机");
- public override string Name => System.Reflection.Assembly.GetExecutingAssembly().GetName().Name!;
- public override Version Version => new Version(1, 1, 0);
-
- private bool _sweepScheduled;
- private DateTime _sweepScheduledAt;
- private long _updateCounter;
-
- public override void Initialize()
- {
- ServerApi.Hooks.GameUpdate.Register(this, this.OnUpdate);
- }
-
- protected override void Dispose(bool disposing)
- {
- if (disposing)
- {
- ServerApi.Hooks.GameUpdate.Deregister(this, this.OnUpdate);
- }
- base.Dispose(disposing);
- }
-
- private void OnUpdate(EventArgs args)
- {
- this._updateCounter++;
-
- if (this._updateCounter % (60 * Configuration.Instance.DetectionIntervalSeconds) != 0)
- {
- return;
- }
-
- if (Main.item.Count(i => i is { active: true } && !Configuration.Instance.NonSweepableItemIDs.Contains(i.type)) < Configuration.Instance.SmartSweepThreshold)
- {
- return;
- }
-
- if (!this._sweepScheduled)
- {
- this._sweepScheduled = true;
- this._sweepScheduledAt = DateTime.UtcNow.AddSeconds(Configuration.Instance.DelayedSweepTimeoutSeconds);
- TSPlayer.All.SendSuccessMessage($"{Configuration.Instance.DelayedSweepCustomMessage}");
- }
-
- if (this._sweepScheduled && DateTime.UtcNow >= this._sweepScheduledAt)
- {
- // 到达清扫时间,执行清扫任务
- this._sweepScheduled = false;
- if (CanSweep())
- {
- PerformSmartSweep();
- }
- else
- {
- TSPlayer.All.SendSuccessMessage(GetString($"智能扫地机: 由于存在事件或BOSS,已跳过自动清理"));
- }
-
- }
- }
-
- private static bool CanSweep()
- {
- // 入侵
- if (Main.invasionType != 0 && Main.invasionSize != 0)
- {
- return false;
- }
-
- // 血月 | 旧日军团 | 日食
- if (Main.bloodMoon || DD2Event.Ongoing || Main.eclipse)
- {
- return false;
- }
-
- // BOSS
- if (Main.npc.Any(npc => npc is { active: true, boss: true }))
- {
- return false;
- }
-
- return true;
-
- }
-
- private static void PerformSmartSweep()
- {
-
- var totalItems = 0;
- var totalThrowable = 0;
- var totalSwinging = 0;
- var totalRegular = 0;
- var totalEquipment = 0;
- var totalVanity = 0;
-
- for (var i = 0; i < Main.item.Length; i++)
- {
- if (Main.item[i].active && !Configuration.Instance.NonSweepableItemIDs.Contains(Main.item[i].type))
- {
- var isThrowable = Main.item[i].damage > 0 && Main.item[i].maxStack > 1;
- var isSwinging = Main.item[i].damage > 0 && Main.item[i].maxStack == 1;
- var isRegular = Main.item[i].damage < 0 && Main.item[i].maxStack > 1;
- var isEquipment = Main.item[i].damage == 0 && Main.item[i].maxStack == 1;
- var isVanity = Main.item[i].damage < 0 && Main.item[i].maxStack == 1;
-
- if ((Configuration.Instance.SweepThrowable && isThrowable) ||
- (Configuration.Instance.SweepSwinging && isSwinging) ||
- (Configuration.Instance.SweepRegular && isRegular) ||
- (Configuration.Instance.SweepEquipment && isEquipment) ||
- (Configuration.Instance.SweepVanity && isVanity))
- {
- Main.item[i].TurnToAir();
- TSPlayer.All.SendData(PacketTypes.ItemDrop, null, i);
- totalItems++;
-
- if (isThrowable)
- {
- totalThrowable++;
- }
-
- if (isSwinging)
- {
- totalSwinging++;
- }
-
- if (isRegular)
- {
- totalRegular++;
- }
-
- if (isEquipment)
- {
- totalEquipment++;
- }
-
- if (isVanity)
- {
- totalVanity++;
- }
- }
- }
- }
-
- if (totalItems > 0)
- {
- if (!string.IsNullOrEmpty(Configuration.Instance.CustomMessage))
- {
- TSPlayer.All.SendSuccessMessage($"{Configuration.Instance.CustomMessage}");
- }
-
- if (Configuration.Instance.SpecificMessage)
- {
- TSPlayer.All.SendSuccessMessage(GetString($"智能扫地机已清扫:[c/FFFFFF:{totalItems}]种物品"));
- TSPlayer.All.SendSuccessMessage(GetString($"包含:【投掷武器[c/FFFFFF:{totalThrowable}]】-【挥动武器[c/FFFFFF:{totalSwinging}]】-【普通物品[c/FFFFFF:{totalRegular}]】-【装备[c/FFFFFF:{totalEquipment}]】-【时装[c/FFFFFF:{totalVanity}]】"));
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/AutoClear/AutoClear.csproj b/src/AutoClear/AutoClear.csproj
index db42cf41b..048b98a97 100644
--- a/src/AutoClear/AutoClear.csproj
+++ b/src/AutoClear/AutoClear.csproj
@@ -1,9 +1,16 @@
+
+ net9.0
+ Library
+ AutoClear
+ false
+ disable
+ 1.1.0.0
+ 1.1.0.0
+
-
-
-
-
-
-
-
\ No newline at end of file
+
+
+
+
+
diff --git a/src/AutoClear/AutoClearCommandRules.cs b/src/AutoClear/AutoClearCommandRules.cs
new file mode 100644
index 000000000..20b78965d
--- /dev/null
+++ b/src/AutoClear/AutoClearCommandRules.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+
+namespace AutoClear
+{
+ internal static class AutoClearCommandRules
+ {
+ internal const int DefaultRadiusTiles = 50;
+ internal const int ItemsPerTick = 8;
+ internal const int WorldItemSlotCount = 400;
+
+ internal static bool IsItemSubcommand(string value)
+ {
+ return string.Equals(value, "item", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(value, "items", StringComparison.OrdinalIgnoreCase)
+ || string.Equals(value, "i", StringComparison.OrdinalIgnoreCase);
+ }
+
+ internal static bool TryParseItemClearParameters(IReadOnlyList parameters, out int radiusTiles)
+ {
+ radiusTiles = DefaultRadiusTiles;
+ if (parameters == null
+ || (parameters.Count != 1 && parameters.Count != 2)
+ || !IsItemSubcommand(parameters[0]))
+ {
+ return false;
+ }
+
+ return parameters.Count == 1
+ || (int.TryParse(parameters[1], out radiusTiles) && radiusTiles > 0);
+ }
+
+ internal static bool TryParseSafeCommandParameters(IReadOnlyList parameters, out int radiusTiles)
+ {
+ radiusTiles = DefaultRadiusTiles;
+ return parameters != null
+ && (parameters.Count == 0
+ || (parameters.Count == 1
+ && int.TryParse(parameters[0], out radiusTiles)
+ && radiusTiles > 0));
+ }
+
+ internal static bool IsWithinRadius(
+ float itemX,
+ float itemY,
+ float centerX,
+ float centerY,
+ int radiusTiles)
+ {
+ if (radiusTiles <= 0)
+ {
+ return false;
+ }
+
+ double deltaX = itemX - centerX;
+ double deltaY = itemY - centerY;
+ double radiusPixels = radiusTiles * 16d;
+ return deltaX * deltaX + deltaY * deltaY <= radiusPixels * radiusPixels;
+ }
+
+ internal static int GetBatchSize(int remainingItems)
+ {
+ return Math.Clamp(remainingItems, 0, ItemsPerTick);
+ }
+ }
+}
diff --git a/src/AutoClear/AutoClearConfiguration.cs b/src/AutoClear/AutoClearConfiguration.cs
new file mode 100644
index 000000000..1944bcd25
--- /dev/null
+++ b/src/AutoClear/AutoClearConfiguration.cs
@@ -0,0 +1,194 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using TShockAPI;
+
+namespace AutoClear
+{
+ public sealed class AutoClearConfiguration
+ {
+ private static readonly string[] LegacyConfigNames =
+ {
+ "WorldItemCleanupGuard.json",
+ "Autoclear.zh-CN.json",
+ "AutoClear.zh-CN.json",
+ "Autoclear.en-US.json",
+ "AutoClear.en-US.json",
+ };
+
+ [JsonProperty("启用自动清扫")]
+ public bool EnableAutomaticSweep { get; set; } = true;
+
+ [JsonProperty("检测间隔秒")]
+ public int DetectionIntervalSeconds { get; set; } = 10;
+
+ [JsonProperty("排除物品ID")]
+ public List NonSweepableItemIds { get; set; } = new List();
+
+ [JsonProperty("清扫阈值")]
+ public int SmartSweepThreshold { get; set; } = 100;
+
+ [JsonProperty("延迟清扫秒")]
+ public int DelayedSweepTimeoutSeconds { get; set; } = 10;
+
+ [JsonProperty("延迟清扫消息")]
+ public string DelayedSweepCustomMessage { get; set; } = "世界物品已达到清扫阈值,将在 {0} 秒后进行安全清理。";
+
+ [JsonProperty("清扫挥动武器")]
+ public bool SweepSwinging { get; set; } = true;
+
+ [JsonProperty("清扫投掷武器")]
+ public bool SweepThrowable { get; set; } = true;
+
+ [JsonProperty("清扫普通物品")]
+ public bool SweepRegular { get; set; } = true;
+
+ [JsonProperty("清扫装备")]
+ public bool SweepEquipment { get; set; } = true;
+
+ [JsonProperty("清扫时装")]
+ public bool SweepVanity { get; set; } = true;
+
+ [JsonProperty("完成清扫消息")]
+ public string CustomMessage { get; set; } = string.Empty;
+
+ [JsonProperty("显示分类统计")]
+ public bool SpecificMessage { get; set; } = true;
+
+ [JsonIgnore]
+ internal HashSet ExcludedItemIdSet { get; private set; } = new HashSet();
+
+ public static string ConfigPath => Path.Combine(TShock.SavePath, "AutoClear.json");
+
+ internal static AutoClearConfiguration Load()
+ {
+ try
+ {
+ AutoClearConfiguration configuration;
+ if (File.Exists(ConfigPath))
+ {
+ JObject root = JObject.Parse(File.ReadAllText(ConfigPath, Encoding.UTF8));
+ configuration = FromLegacyObject(root);
+ }
+ else if (!TryImportLegacy(out configuration))
+ {
+ configuration = new AutoClearConfiguration();
+ }
+
+ configuration.Normalize();
+ configuration.Write();
+ return configuration;
+ }
+ catch (Exception ex)
+ {
+ TShock.Log.ConsoleError($"[AutoClear] Failed to load configuration: {ex}");
+ AutoClearConfiguration fallback = new AutoClearConfiguration();
+ fallback.Normalize();
+ return fallback;
+ }
+ }
+
+ internal void Normalize()
+ {
+ DetectionIntervalSeconds = Math.Max(1, DetectionIntervalSeconds);
+ SmartSweepThreshold = Math.Max(1, SmartSweepThreshold);
+ DelayedSweepTimeoutSeconds = Math.Max(0, DelayedSweepTimeoutSeconds);
+ DelayedSweepCustomMessage ??= string.Empty;
+ CustomMessage ??= string.Empty;
+ NonSweepableItemIds = (NonSweepableItemIds ?? new List())
+ .Where(itemId => itemId > 0)
+ .Distinct()
+ .OrderBy(itemId => itemId)
+ .ToList();
+ ExcludedItemIdSet = new HashSet(NonSweepableItemIds);
+ }
+
+ internal void Write()
+ {
+ string directory = Path.GetDirectoryName(ConfigPath);
+ if (!string.IsNullOrEmpty(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+
+ File.WriteAllText(
+ ConfigPath,
+ JsonConvert.SerializeObject(this, Formatting.Indented),
+ new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
+ }
+
+ private static bool TryImportLegacy(out AutoClearConfiguration configuration)
+ {
+ configuration = null;
+ foreach (string fileName in LegacyConfigNames)
+ {
+ string path = Path.Combine(TShock.SavePath, fileName);
+ if (!File.Exists(path))
+ {
+ continue;
+ }
+
+ JObject root = JObject.Parse(File.ReadAllText(path, Encoding.UTF8));
+ configuration = FromLegacyObject(root);
+ TShock.Log.ConsoleInfo(
+ $"[AutoClear] Imported legacy configuration from {path}");
+ return true;
+ }
+
+ return false;
+ }
+
+ internal static AutoClearConfiguration FromLegacyObject(JObject root)
+ {
+ AutoClearConfiguration configuration = new AutoClearConfiguration();
+ configuration.EnableAutomaticSweep = ReadValue(root, configuration.EnableAutomaticSweep,
+ "启用自动清扫", "EnableAutomaticSweep");
+ configuration.DetectionIntervalSeconds = ReadValue(root, configuration.DetectionIntervalSeconds,
+ "检测间隔秒", "清理间隔", "多久检测一次(s)", "Interval");
+ configuration.NonSweepableItemIds = ReadValue(root, configuration.NonSweepableItemIds,
+ "排除物品ID", "排除列表", "不清扫的物品ID列表", "Exclude");
+ configuration.SmartSweepThreshold = ReadValue(root, configuration.SmartSweepThreshold,
+ "清扫阈值", "清理阈值", "智能清扫数量临界值", "Threshold");
+ configuration.DelayedSweepTimeoutSeconds = ReadValue(root, configuration.DelayedSweepTimeoutSeconds,
+ "延迟清扫秒", "延迟清扫", "延迟清扫(s)", "Dealy", "Delay");
+ configuration.DelayedSweepCustomMessage = ReadValue(root, configuration.DelayedSweepCustomMessage,
+ "延迟清扫消息", "延迟清扫自定义消息", "DealyMsg", "DelayMsg");
+ configuration.SweepSwinging = ReadValue(root, configuration.SweepSwinging,
+ "清扫挥动武器", "是否清扫挥动武器", "SweepSwinging");
+ configuration.SweepThrowable = ReadValue(root, configuration.SweepThrowable,
+ "清扫投掷武器", "是否清扫投掷武器", "SweepThrowable");
+ configuration.SweepRegular = ReadValue(root, configuration.SweepRegular,
+ "清扫普通物品", "是否清扫普通物品", "SweepRegular", "SweepRegaular");
+ configuration.SweepEquipment = ReadValue(root, configuration.SweepEquipment,
+ "清扫装备", "是否清扫装备", "SweepEquipment");
+ configuration.SweepVanity = ReadValue(root, configuration.SweepVanity,
+ "清扫时装", "是否清扫时装", "SweepVanity");
+ configuration.CustomMessage = ReadValue(root, configuration.CustomMessage,
+ "完成清扫消息", "完成清扫自定义消息", "SweepMsg");
+ configuration.SpecificMessage = ReadValue(root, configuration.SpecificMessage,
+ "显示分类统计", "清理提示", "具体消息", "SweepTip");
+ configuration.Normalize();
+ return configuration;
+ }
+
+ private static T ReadValue(JObject root, T fallback, params string[] aliases)
+ {
+ foreach (string alias in aliases)
+ {
+ if (!root.TryGetValue(alias, StringComparison.OrdinalIgnoreCase, out JToken token))
+ {
+ continue;
+ }
+
+ T value = token.ToObject();
+ return value != null ? value : fallback;
+ }
+
+ return fallback;
+ }
+ }
+}
diff --git a/src/AutoClear/AutoClearItemRules.cs b/src/AutoClear/AutoClearItemRules.cs
new file mode 100644
index 000000000..1664d6759
--- /dev/null
+++ b/src/AutoClear/AutoClearItemRules.cs
@@ -0,0 +1,51 @@
+namespace AutoClear
+{
+ internal enum AutoClearItemCategory
+ {
+ None,
+ Throwable,
+ Swinging,
+ Regular,
+ Equipment,
+ Vanity,
+ }
+
+ internal static class AutoClearItemRules
+ {
+ internal static AutoClearItemCategory Classify(int damage, int maxStack)
+ {
+ if (damage > 0)
+ {
+ return maxStack > 1
+ ? AutoClearItemCategory.Throwable
+ : AutoClearItemCategory.Swinging;
+ }
+
+ if (damage < 0)
+ {
+ return maxStack > 1
+ ? AutoClearItemCategory.Regular
+ : AutoClearItemCategory.Vanity;
+ }
+
+ return maxStack == 1
+ ? AutoClearItemCategory.Equipment
+ : AutoClearItemCategory.None;
+ }
+
+ internal static bool IsEnabled(
+ AutoClearItemCategory category,
+ AutoClearConfiguration configuration)
+ {
+ return category switch
+ {
+ AutoClearItemCategory.Throwable => configuration.SweepThrowable,
+ AutoClearItemCategory.Swinging => configuration.SweepSwinging,
+ AutoClearItemCategory.Regular => configuration.SweepRegular,
+ AutoClearItemCategory.Equipment => configuration.SweepEquipment,
+ AutoClearItemCategory.Vanity => configuration.SweepVanity,
+ _ => false,
+ };
+ }
+ }
+}
diff --git a/src/AutoClear/AutoClearPlugin.cs b/src/AutoClear/AutoClearPlugin.cs
new file mode 100644
index 000000000..0b5ec18b0
--- /dev/null
+++ b/src/AutoClear/AutoClearPlugin.cs
@@ -0,0 +1,250 @@
+using System;
+using System.Linq;
+using System.Reflection;
+using Terraria;
+using TerrariaApi.Server;
+using TShockAPI;
+using TShockAPI.Hooks;
+
+namespace AutoClear
+{
+ [ApiVersion(2, 1)]
+ public sealed class AutoClearPlugin : TerrariaPlugin
+ {
+ private Command tshockClearCommand;
+ private Command safeClearCommand;
+ private Command reloadCommand;
+ private AutoClearConfiguration configuration;
+ private AutoClearScheduler scheduler;
+ private bool starverProtectionActive;
+
+ public AutoClearPlugin(Main game)
+ : base(game)
+ {
+ }
+
+ public override string Name => "AutoClear";
+ public override string Author => "大豆子, Mute, 肝帝熙恩";
+ public override string Description => "Safely paces manual and automatic world-item cleanup.";
+ public override Version Version => Assembly.GetExecutingAssembly().GetName().Version;
+
+ public override void Initialize()
+ {
+ configuration = AutoClearConfiguration.Load();
+ scheduler = new AutoClearScheduler(
+ configuration,
+ IsStarverCleanupRunning);
+ starverProtectionActive = HasStarverCleanupProtection();
+ tshockClearCommand = FindTShockClearCommand();
+ if (tshockClearCommand == null)
+ {
+ TShock.Log.ConsoleError(
+ "[AutoClear] Unable to locate TShock's built-in /clear command; use /safeclearitems instead.");
+ }
+
+ safeClearCommand = new Command(
+ Permissions.clear,
+ OnSafeClearItems,
+ "safeclearitems",
+ "sclearitems")
+ {
+ HelpText = "Safely clears world items in paced batches. Usage: /safeclearitems [radius]",
+ };
+ reloadCommand = new Command(
+ Permissions.cfgreload,
+ OnReloadConfiguration,
+ "autoclearreload",
+ "acreload")
+ {
+ HelpText = "Reloads AutoClear.json.",
+ };
+
+ Commands.ChatCommands.Add(safeClearCommand);
+ Commands.ChatCommands.Add(reloadCommand);
+ PlayerHooks.PlayerCommand += OnPlayerCommand;
+ PlayerHooks.PrePlayerCommand += OnPrePlayerCommand;
+ ServerApi.Hooks.GameUpdate.Register(this, OnGameUpdate);
+
+ if (starverProtectionActive)
+ {
+ TShock.Log.ConsoleInfo(
+ "[AutoClear] Starver cleanup protection detected; /clear item remains delegated to Starver while automatic sweep stays available.");
+ }
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ ServerApi.Hooks.GameUpdate.Deregister(this, OnGameUpdate);
+ PlayerHooks.PrePlayerCommand -= OnPrePlayerCommand;
+ PlayerHooks.PlayerCommand -= OnPlayerCommand;
+ if (safeClearCommand != null)
+ {
+ Commands.ChatCommands.Remove(safeClearCommand);
+ }
+ if (reloadCommand != null)
+ {
+ Commands.ChatCommands.Remove(reloadCommand);
+ }
+
+ AutoClearService.Reset();
+ scheduler = null;
+ configuration = null;
+ reloadCommand = null;
+ safeClearCommand = null;
+ tshockClearCommand = null;
+ base.Dispose(disposing);
+ }
+
+ private static Command FindTShockClearCommand()
+ {
+ return Commands.ChatCommands.FirstOrDefault(command =>
+ string.Equals(command.Name, "clear", StringComparison.OrdinalIgnoreCase)
+ && command.Permissions.Contains(Permissions.clear)
+ && command.CommandDelegate?.Method.DeclaringType == typeof(Commands)
+ && string.Equals(command.CommandDelegate.Method.Name, "Clear", StringComparison.Ordinal));
+ }
+
+ private void OnPrePlayerCommand(PrePlayerCommandEventArgs args)
+ {
+ if (HasStarverCleanupProtection()
+ || !ReferenceEquals(args.Command, tshockClearCommand)
+ || !AutoClearCommandRules.TryParseItemClearParameters(
+ args.Arguments.Parameters,
+ out int radiusTiles))
+ {
+ return;
+ }
+
+ args.Handled = true;
+ StartCleanup(args.Arguments.Player, radiusTiles, args.Arguments.Silent);
+ }
+
+ private static void OnPlayerCommand(PlayerCommandEventArgs args)
+ {
+ if (!AutoClearService.IsRunning
+ || !args.Player.HasPermission(Permissions.clear)
+ || !string.Equals(args.CommandName, "clear", StringComparison.OrdinalIgnoreCase)
+ || !AutoClearCommandRules.TryParseItemClearParameters(
+ args.Parameters,
+ out _))
+ {
+ return;
+ }
+
+ args.Handled = true;
+ args.Player.SendWarningMessage(
+ $"自动或安全物品清理正在进行,剩余 {AutoClearService.RemainingItems} 个物品,请稍后再试。");
+ }
+
+ private static void OnSafeClearItems(CommandArgs args)
+ {
+ if (!AutoClearCommandRules.TryParseSafeCommandParameters(
+ args.Parameters,
+ out int radiusTiles))
+ {
+ args.Player.SendErrorMessage("正确用法: /safeclearitems [半径]");
+ return;
+ }
+
+ if (HasStarverCleanupProtection())
+ {
+ args.Player.SendWarningMessage("检测到 Starver 已内置相同保护,请使用 /clear item [半径]。");
+ return;
+ }
+
+ StartCleanup(args.Player, radiusTiles, args.Silent);
+ }
+
+ private void OnReloadConfiguration(CommandArgs args)
+ {
+ if (args.Parameters.Count != 0)
+ {
+ args.Player.SendErrorMessage("正确用法: /autoclearreload");
+ return;
+ }
+
+ configuration = AutoClearConfiguration.Load();
+ scheduler.Reload(configuration);
+ starverProtectionActive = HasStarverCleanupProtection();
+ args.Player.SendSuccessMessage(
+ $"AutoClear 配置已重载:自动清扫={(configuration.EnableAutomaticSweep ? "开启" : "关闭")}," +
+ $"阈值={configuration.SmartSweepThreshold},检测间隔={configuration.DetectionIntervalSeconds}秒," +
+ $"延迟={configuration.DelayedSweepTimeoutSeconds}秒。");
+
+ if (starverProtectionActive)
+ {
+ args.Player.SendInfoMessage("检测到 Starver 内置清理保护:/clear item 由 Starver 接管,自动清扫仍由本插件安全执行。");
+ }
+ }
+
+ private static void StartCleanup(TSPlayer player, int radiusTiles, bool silent)
+ {
+ AutoClearStartResult result = AutoClearService.TryStart(
+ player,
+ radiusTiles,
+ silent,
+ out _);
+
+ switch (result)
+ {
+ case AutoClearStartResult.Started:
+ case AutoClearStartResult.NoMatchingItems:
+ return;
+
+ case AutoClearStartResult.Busy:
+ player.SendWarningMessage(
+ $"已有世界物品清理任务正在进行,剩余 {AutoClearService.RemainingItems} 个物品,请稍后再试。");
+ return;
+
+ case AutoClearStartResult.MainThreadRequired:
+ player.SendErrorMessage("世界物品安全清理只能在服务器主线程启动,本次请求已取消。");
+ return;
+
+ default:
+ player.SendErrorMessage("无法启动世界物品安全清理任务。");
+ return;
+ }
+ }
+
+ private static bool HasStarverCleanupProtection()
+ {
+ return ServerApi.Plugins.Any(container =>
+ container.Initialized
+ && container.Plugin?.GetType().Assembly.GetType(
+ "Starvers.WorldItemCleanupCoordinator",
+ throwOnError: false) != null);
+ }
+
+ private void OnGameUpdate(EventArgs args)
+ {
+ AutoClearService.Update();
+ scheduler?.Update();
+ }
+
+ private static bool IsStarverCleanupRunning()
+ {
+ foreach (var container in ServerApi.Plugins)
+ {
+ if (!container.Initialized || container.Plugin == null)
+ {
+ continue;
+ }
+
+ Type coordinatorType = container.Plugin.GetType().Assembly.GetType(
+ "Starvers.WorldItemCleanupCoordinator",
+ throwOnError: false);
+ if (coordinatorType == null)
+ {
+ continue;
+ }
+
+ object value = coordinatorType.GetProperty(
+ "IsRunning",
+ BindingFlags.Public | BindingFlags.Static)?.GetValue(null);
+ return value is bool isRunning && isRunning;
+ }
+
+ return false;
+ }
+ }
+}
diff --git a/src/AutoClear/AutoClearScheduler.cs b/src/AutoClear/AutoClearScheduler.cs
new file mode 100644
index 000000000..1c9c49cc1
--- /dev/null
+++ b/src/AutoClear/AutoClearScheduler.cs
@@ -0,0 +1,244 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Terraria;
+using Terraria.GameContent.Events;
+using TShockAPI;
+
+namespace AutoClear
+{
+ internal sealed class AutoClearScheduler
+ {
+ private AutoClearConfiguration configuration;
+ private readonly Func externalCleanupIsRunning;
+ private bool sweepScheduled;
+ private DateTime sweepScheduledAtUtc;
+ private DateTime nextDetectionAtUtc;
+
+ internal AutoClearScheduler(
+ AutoClearConfiguration configuration,
+ Func externalCleanupIsRunning)
+ {
+ this.externalCleanupIsRunning = externalCleanupIsRunning;
+ Reload(configuration);
+ }
+
+ internal void Reload(AutoClearConfiguration newConfiguration)
+ {
+ configuration = newConfiguration ?? new AutoClearConfiguration();
+ configuration.Normalize();
+ sweepScheduled = false;
+ nextDetectionAtUtc = DateTime.MinValue;
+ }
+
+ internal void Update()
+ {
+ DateTime now = DateTime.UtcNow;
+ if (!configuration.EnableAutomaticSweep)
+ {
+ sweepScheduled = false;
+ nextDetectionAtUtc = now.AddSeconds(configuration.DetectionIntervalSeconds);
+ return;
+ }
+
+ if (sweepScheduled)
+ {
+ if (now >= sweepScheduledAtUtc)
+ {
+ TryExecuteScheduledSweep(now);
+ }
+ return;
+ }
+
+ if (now < nextDetectionAtUtc)
+ {
+ return;
+ }
+
+ nextDetectionAtUtc = now.AddSeconds(configuration.DetectionIntervalSeconds);
+ List candidates = CaptureCandidates(configuration);
+ if (candidates.Count < configuration.SmartSweepThreshold)
+ {
+ return;
+ }
+
+ sweepScheduled = true;
+ sweepScheduledAtUtc = now.AddSeconds(configuration.DelayedSweepTimeoutSeconds);
+ if (!string.IsNullOrWhiteSpace(configuration.DelayedSweepCustomMessage))
+ {
+ string message = configuration.DelayedSweepCustomMessage.Replace(
+ "{0}",
+ configuration.DelayedSweepTimeoutSeconds.ToString());
+ TSPlayer.All.SendSuccessMessage(message);
+ }
+
+ if (configuration.DelayedSweepTimeoutSeconds == 0)
+ {
+ TryExecuteScheduledSweep(now);
+ }
+ }
+
+ private void TryExecuteScheduledSweep(DateTime now)
+ {
+ if (AutoClearService.IsRunning || externalCleanupIsRunning?.Invoke() == true)
+ {
+ sweepScheduledAtUtc = now.AddSeconds(1);
+ return;
+ }
+
+ List candidates = CaptureCandidates(configuration);
+ if (candidates.Count < configuration.SmartSweepThreshold)
+ {
+ ResetSchedule(now);
+ return;
+ }
+
+ if (!CanSweep(out string blockedReason))
+ {
+ TSPlayer.All.SendWarningMessage(
+ $"智能扫地机:由于{blockedReason},已跳过本次自动清理。");
+ ResetSchedule(now);
+ return;
+ }
+
+ AutoClearConfiguration completionConfiguration = configuration;
+ AutoClearStartResult result = AutoClearService.TryStartAutomatic(
+ candidates,
+ summary => SendCompletionMessages(completionConfiguration, summary),
+ out int queuedItems);
+
+ switch (result)
+ {
+ case AutoClearStartResult.Started:
+#if DEBUG
+ TShock.Log.ConsoleDebug(
+ $"[AutoClear] Automatic cleanup queued items={queuedItems}");
+#endif
+ ResetSchedule(now);
+ return;
+
+ case AutoClearStartResult.NoMatchingItems:
+ ResetSchedule(now);
+ return;
+
+ case AutoClearStartResult.Busy:
+ sweepScheduledAtUtc = now.AddSeconds(1);
+ return;
+
+ default:
+ TShock.Log.ConsoleError(
+ $"[AutoClear] Unable to start automatic cleanup: {result}");
+ ResetSchedule(now);
+ return;
+ }
+ }
+
+ private void ResetSchedule(DateTime now)
+ {
+ sweepScheduled = false;
+ nextDetectionAtUtc = now.AddSeconds(configuration.DetectionIntervalSeconds);
+ }
+
+ private static List CaptureCandidates(
+ AutoClearConfiguration configuration)
+ {
+ int slotCount = Math.Min(AutoClearCommandRules.WorldItemSlotCount, Main.item.Length);
+ List candidates = new List();
+ for (int slot = 0; slot < slotCount; slot++)
+ {
+ WorldItem item = Main.item[slot];
+ if (item == null
+ || !item.active
+ || configuration.ExcludedItemIdSet.Contains(item.type))
+ {
+ continue;
+ }
+
+ AutoClearItemCategory category = AutoClearItemRules.Classify(item.damage, item.maxStack);
+ if (AutoClearItemRules.IsEnabled(category, configuration))
+ {
+ candidates.Add(new AutoClearCandidate(slot, category));
+ }
+ }
+
+ return candidates;
+ }
+
+ private static bool CanSweep(out string blockedReason)
+ {
+ if (Main.invasionType != 0 && Main.invasionSize != 0)
+ {
+ blockedReason = "入侵事件正在进行";
+ return false;
+ }
+
+ if (Main.bloodMoon)
+ {
+ blockedReason = "血月正在进行";
+ return false;
+ }
+
+ if (DD2Event.Ongoing)
+ {
+ blockedReason = "旧日军团正在进行";
+ return false;
+ }
+
+ if (Main.eclipse)
+ {
+ blockedReason = "日食正在进行";
+ return false;
+ }
+
+ if (Main.pumpkinMoon || Main.snowMoon)
+ {
+ blockedReason = "月亮事件正在进行";
+ return false;
+ }
+
+ if (Main.npc.Any(npc => npc != null && npc.active && npc.boss))
+ {
+ blockedReason = "Boss 战正在进行";
+ return false;
+ }
+
+ blockedReason = string.Empty;
+ return true;
+ }
+
+ private static void SendCompletionMessages(
+ AutoClearConfiguration configuration,
+ AutoClearSummary summary)
+ {
+ if (summary.DeletedItems <= 0)
+ {
+ return;
+ }
+
+ if (!string.IsNullOrWhiteSpace(configuration.CustomMessage))
+ {
+ TSPlayer.All.SendSuccessMessage(configuration.CustomMessage);
+ }
+
+ if (!configuration.SpecificMessage)
+ {
+ return;
+ }
+
+ TSPlayer.All.SendSuccessMessage(
+ $"智能扫地机已安全清扫:[c/FFFFFF:{summary.DeletedItems}] 个物品");
+ TSPlayer.All.SendSuccessMessage(
+ $"包含:【投掷武器[c/FFFFFF:{summary.ThrowableItems}]】-" +
+ $"【挥动武器[c/FFFFFF:{summary.SwingingItems}]】-" +
+ $"【普通物品[c/FFFFFF:{summary.RegularItems}]】-" +
+ $"【装备[c/FFFFFF:{summary.EquipmentItems}]】-" +
+ $"【时装[c/FFFFFF:{summary.VanityItems}]】");
+
+ if (summary.SkippedItems > 0)
+ {
+ TSPlayer.All.SendInfoMessage(
+ $"另有 {summary.SkippedItems} 个物品因被拾取、合并或槽位变化而跳过。");
+ }
+ }
+ }
+}
diff --git a/src/AutoClear/AutoClearService.cs b/src/AutoClear/AutoClearService.cs
new file mode 100644
index 000000000..bb1d204bd
--- /dev/null
+++ b/src/AutoClear/AutoClearService.cs
@@ -0,0 +1,455 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using Terraria;
+using TShockAPI;
+
+namespace AutoClear
+{
+ public enum AutoClearStartResult
+ {
+ Started,
+ NoMatchingItems,
+ Busy,
+ InvalidRequest,
+ MainThreadRequired,
+ }
+
+ internal readonly struct AutoClearCandidate
+ {
+ internal AutoClearCandidate(int slot, AutoClearItemCategory category)
+ {
+ Slot = slot;
+ Category = category;
+ }
+
+ internal int Slot { get; }
+ internal AutoClearItemCategory Category { get; }
+ }
+
+ internal sealed class AutoClearSummary
+ {
+ internal AutoClearSummary(
+ int queuedItems,
+ int deletedItems,
+ int skippedItems,
+ int throwableItems,
+ int swingingItems,
+ int regularItems,
+ int equipmentItems,
+ int vanityItems)
+ {
+ QueuedItems = queuedItems;
+ DeletedItems = deletedItems;
+ SkippedItems = skippedItems;
+ ThrowableItems = throwableItems;
+ SwingingItems = swingingItems;
+ RegularItems = regularItems;
+ EquipmentItems = equipmentItems;
+ VanityItems = vanityItems;
+ }
+
+ internal int QueuedItems { get; }
+ internal int DeletedItems { get; }
+ internal int SkippedItems { get; }
+ internal int ThrowableItems { get; }
+ internal int SwingingItems { get; }
+ internal int RegularItems { get; }
+ internal int EquipmentItems { get; }
+ internal int VanityItems { get; }
+ }
+
+ public static class AutoClearService
+ {
+ private static CleanupJob activeJob;
+ private static int mainThreadId = -1;
+
+ public static bool IsRunning => activeJob != null;
+ public static int RemainingItems => activeJob?.Pending.Count ?? 0;
+
+ ///
+ /// Starts a paced world-item cleanup. Call this method from Terraria's main thread.
+ ///
+ public static AutoClearStartResult TryStart(
+ TSPlayer initiator,
+ int radiusTiles,
+ bool silent,
+ out int queuedItems)
+ {
+ queuedItems = 0;
+ if (initiator == null || radiusTiles <= 0 || Main.item == null)
+ {
+ return AutoClearStartResult.InvalidRequest;
+ }
+
+ AutoClearStartResult stateResult = ValidateStartState();
+ if (stateResult != AutoClearStartResult.Started)
+ {
+ return stateResult;
+ }
+
+ Queue pending = CaptureMatchingItems(initiator.X, initiator.Y, radiusTiles);
+ queuedItems = pending.Count;
+ if (pending.Count == 0)
+ {
+ SendCompletionMessage(initiator, initiator.Name, silent, radiusTiles, 0, 0);
+ return AutoClearStartResult.NoMatchingItems;
+ }
+
+ activeJob = new CleanupJob(
+ initiator,
+ initiator.Name,
+ silent,
+ radiusTiles,
+ pending,
+ sendDefaultCompletion: true,
+ completion: null);
+
+ if (CanReceiveMessage(initiator))
+ {
+ initiator.SendInfoMessage(
+ $"已将 {queuedItems} 个世界物品加入安全清理队列,每 tick 最多处理 {AutoClearCommandRules.ItemsPerTick} 个。");
+ }
+
+ return AutoClearStartResult.Started;
+ }
+
+ internal static AutoClearStartResult TryStartAutomatic(
+ IReadOnlyList candidates,
+ Action completion,
+ out int queuedItems)
+ {
+ queuedItems = 0;
+ if (candidates == null || Main.item == null)
+ {
+ return AutoClearStartResult.InvalidRequest;
+ }
+
+ AutoClearStartResult stateResult = ValidateStartState();
+ if (stateResult != AutoClearStartResult.Started)
+ {
+ return stateResult;
+ }
+
+ Queue pending = CaptureCandidates(candidates);
+ queuedItems = pending.Count;
+ if (pending.Count == 0)
+ {
+ InvokeCompletion(completion, CreateEmptySummary());
+ return AutoClearStartResult.NoMatchingItems;
+ }
+
+ activeJob = new CleanupJob(
+ initiator: null,
+ initiatorName: "AutoClear",
+ silent: true,
+ radiusTiles: 0,
+ pending,
+ sendDefaultCompletion: false,
+ completion);
+ return AutoClearStartResult.Started;
+ }
+
+ internal static void Update()
+ {
+ Interlocked.CompareExchange(
+ ref mainThreadId,
+ Thread.CurrentThread.ManagedThreadId,
+ comparand: -1);
+
+ CleanupJob job = activeJob;
+ if (job == null)
+ {
+ return;
+ }
+
+ int batchSize = AutoClearCommandRules.GetBatchSize(job.Pending.Count);
+ for (int i = 0; i < batchSize; i++)
+ {
+ WorldItemSnapshot snapshot = job.Pending.Dequeue();
+ if (!snapshot.TryResolve(out WorldItem item))
+ {
+ job.SkippedItems++;
+ continue;
+ }
+
+ item.TurnToAir(false);
+ TSPlayer.All.SendData(PacketTypes.SyncItemDespawn, "", snapshot.Slot);
+ job.RecordDeleted(snapshot.Category);
+ }
+
+ if (job.Pending.Count != 0)
+ {
+ return;
+ }
+
+ activeJob = null;
+ AutoClearSummary summary = job.CreateSummary();
+ if (job.SendDefaultCompletion)
+ {
+ SendCompletionMessage(
+ job.Initiator,
+ job.InitiatorName,
+ job.Silent,
+ job.RadiusTiles,
+ summary.DeletedItems,
+ summary.SkippedItems);
+ }
+ else
+ {
+#if DEBUG
+ TShock.Log.ConsoleDebug(
+ $"[AutoClear] Automatic cleanup completed deleted={summary.DeletedItems} skipped={summary.SkippedItems}");
+#endif
+ }
+
+ InvokeCompletion(job.Completion, summary);
+ }
+
+ internal static void Reset()
+ {
+ if (activeJob != null)
+ {
+ TShock.Log.ConsoleInfo(
+ $"[AutoClear] Cancelled pending cleanup during plugin unload. remaining={activeJob.Pending.Count}");
+ }
+
+ activeJob = null;
+ Volatile.Write(ref mainThreadId, -1);
+ }
+
+ private static AutoClearStartResult ValidateStartState()
+ {
+ if (Thread.CurrentThread.ManagedThreadId != Volatile.Read(ref mainThreadId))
+ {
+ return AutoClearStartResult.MainThreadRequired;
+ }
+
+ return activeJob == null
+ ? AutoClearStartResult.Started
+ : AutoClearStartResult.Busy;
+ }
+
+ private static Queue CaptureMatchingItems(
+ float centerX,
+ float centerY,
+ int radiusTiles)
+ {
+ int slotCount = Math.Min(AutoClearCommandRules.WorldItemSlotCount, Main.item.Length);
+ Queue pending = new Queue(slotCount);
+ for (int slot = 0; slot < slotCount; slot++)
+ {
+ WorldItem item = Main.item[slot];
+ if (item == null
+ || !item.active
+ || !AutoClearCommandRules.IsWithinRadius(
+ item.position.X,
+ item.position.Y,
+ centerX,
+ centerY,
+ radiusTiles))
+ {
+ continue;
+ }
+
+ pending.Enqueue(new WorldItemSnapshot(slot, item, AutoClearItemCategory.None));
+ }
+
+ return pending;
+ }
+
+ private static Queue CaptureCandidates(
+ IReadOnlyList candidates)
+ {
+ Queue pending = new Queue(candidates.Count);
+ HashSet capturedSlots = new HashSet();
+ foreach (AutoClearCandidate candidate in candidates)
+ {
+ if ((uint)candidate.Slot >= (uint)Main.item.Length
+ || candidate.Category == AutoClearItemCategory.None
+ || !capturedSlots.Add(candidate.Slot))
+ {
+ continue;
+ }
+
+ WorldItem item = Main.item[candidate.Slot];
+ if (item != null && item.active)
+ {
+ pending.Enqueue(new WorldItemSnapshot(candidate.Slot, item, candidate.Category));
+ }
+ }
+
+ return pending;
+ }
+
+ private static void SendCompletionMessage(
+ TSPlayer initiator,
+ string initiatorName,
+ bool silent,
+ int radiusTiles,
+ int deletedItems,
+ int skippedItems)
+ {
+ string skippedSuffix = skippedItems > 0
+ ? $",另有 {skippedItems} 个因槽位状态变化而跳过"
+ : string.Empty;
+
+ if (silent)
+ {
+ if (CanReceiveMessage(initiator))
+ {
+ initiator.SendSuccessMessage(
+ $"已在半径 {radiusTiles} 格内安全清理 {deletedItems} 个物品{skippedSuffix}。");
+ }
+ }
+ else
+ {
+ TSPlayer.All.SendInfoMessage(
+ $"{initiatorName} 在半径 {radiusTiles} 格内安全清理了 {deletedItems} 个物品{skippedSuffix}。");
+ }
+
+ TShock.Log.ConsoleInfo(
+ $"[AutoClear] Completed requester={initiatorName} radius={radiusTiles} deleted={deletedItems} skipped={skippedItems}");
+ }
+
+ private static void InvokeCompletion(
+ Action completion,
+ AutoClearSummary summary)
+ {
+ if (completion == null)
+ {
+ return;
+ }
+
+ try
+ {
+ completion(summary);
+ }
+ catch (Exception ex)
+ {
+ TShock.Log.ConsoleError($"[AutoClear] Cleanup completion callback failed: {ex}");
+ }
+ }
+
+ private static AutoClearSummary CreateEmptySummary()
+ {
+ return new AutoClearSummary(0, 0, 0, 0, 0, 0, 0, 0);
+ }
+
+ private static bool CanReceiveMessage(TSPlayer player)
+ {
+ return player != null && (!player.RealPlayer || player.Active);
+ }
+
+ private sealed class CleanupJob
+ {
+ private readonly int[] deletedCategoryCounts = new int[6];
+
+ internal CleanupJob(
+ TSPlayer initiator,
+ string initiatorName,
+ bool silent,
+ int radiusTiles,
+ Queue pending,
+ bool sendDefaultCompletion,
+ Action completion)
+ {
+ Initiator = initiator;
+ InitiatorName = initiatorName;
+ Silent = silent;
+ RadiusTiles = radiusTiles;
+ Pending = pending;
+ InitialItems = pending.Count;
+ SendDefaultCompletion = sendDefaultCompletion;
+ Completion = completion;
+ }
+
+ internal TSPlayer Initiator { get; }
+ internal string InitiatorName { get; }
+ internal bool Silent { get; }
+ internal int RadiusTiles { get; }
+ internal Queue Pending { get; }
+ internal int InitialItems { get; }
+ internal int DeletedItems { get; private set; }
+ internal int SkippedItems { get; set; }
+ internal bool SendDefaultCompletion { get; }
+ internal Action Completion { get; }
+
+ internal void RecordDeleted(AutoClearItemCategory category)
+ {
+ DeletedItems++;
+ int categoryIndex = (int)category;
+ if ((uint)categoryIndex < (uint)deletedCategoryCounts.Length)
+ {
+ deletedCategoryCounts[categoryIndex]++;
+ }
+ }
+
+ internal AutoClearSummary CreateSummary()
+ {
+ return new AutoClearSummary(
+ InitialItems,
+ DeletedItems,
+ SkippedItems,
+ deletedCategoryCounts[(int)AutoClearItemCategory.Throwable],
+ deletedCategoryCounts[(int)AutoClearItemCategory.Swinging],
+ deletedCategoryCounts[(int)AutoClearItemCategory.Regular],
+ deletedCategoryCounts[(int)AutoClearItemCategory.Equipment],
+ deletedCategoryCounts[(int)AutoClearItemCategory.Vanity]);
+ }
+ }
+
+ private readonly struct WorldItemSnapshot
+ {
+ private readonly WorldItem itemReference;
+ private readonly int itemType;
+ private readonly int stack;
+ private readonly byte prefix;
+ private readonly int reservedForPlayer;
+ private readonly int timeSinceItemSpawned;
+
+ internal WorldItemSnapshot(
+ int slot,
+ WorldItem item,
+ AutoClearItemCategory category)
+ {
+ Slot = slot;
+ Category = category;
+ itemReference = item;
+ itemType = item.type;
+ stack = item.stack;
+ prefix = item.inner.prefix;
+ reservedForPlayer = item.playerIndexTheItemIsReservedFor;
+ timeSinceItemSpawned = item.timeSinceItemSpawned;
+ }
+
+ internal int Slot { get; }
+ internal AutoClearItemCategory Category { get; }
+
+ internal bool TryResolve(out WorldItem item)
+ {
+ item = null;
+ if ((uint)Slot >= (uint)Main.item.Length)
+ {
+ return false;
+ }
+
+ WorldItem current = Main.item[Slot];
+ if (current == null
+ || !current.active
+ || !ReferenceEquals(current, itemReference)
+ || current.type != itemType
+ || current.stack != stack
+ || current.inner.prefix != prefix
+ || current.playerIndexTheItemIsReservedFor != reservedForPlayer
+ || current.timeSinceItemSpawned < timeSinceItemSpawned)
+ {
+ return false;
+ }
+
+ item = current;
+ return true;
+ }
+ }
+ }
+}
diff --git a/src/AutoClear/Configuration.cs b/src/AutoClear/Configuration.cs
deleted file mode 100644
index c80bde867..000000000
--- a/src/AutoClear/Configuration.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-using LazyAPI.Attributes;
-using LazyAPI.ConfigFiles;
-
-namespace AutoClear;
-
-[Config]
-public class Configuration : JsonConfigBase
-{
- protected override string Filename => "AutoClear";
-
- [LocalizedPropertyName(CultureType.Chinese, "清理间隔")]
- [LocalizedPropertyName(CultureType.English, "Interval")]
- public int DetectionIntervalSeconds { get; set; } = 100;
-
- [LocalizedPropertyName(CultureType.Chinese, "排除列表")]
- [LocalizedPropertyName(CultureType.English, "Exclude")]
- // ReSharper disable once CollectionNeverUpdated.Global
- public List NonSweepableItemIDs { get; set; } = [];
-
- [LocalizedPropertyName(CultureType.Chinese, "清理阈值")]
- [LocalizedPropertyName(CultureType.English, "Threshold")]
- public int SmartSweepThreshold { get; set; } = 10;
-
- [LocalizedPropertyName(CultureType.Chinese, "延迟清扫")]
- [LocalizedPropertyName(CultureType.English, "Dealy")]
- public int DelayedSweepTimeoutSeconds { get; set; } = 10;
-
- [LocalizedPropertyName(CultureType.Chinese, "延迟清扫消息")]
- [LocalizedPropertyName(CultureType.English, "DealyMsg")]
- public string DelayedSweepCustomMessage { get; set; } = "";
-
- [LocalizedPropertyName(CultureType.Chinese, "清扫挥动武器")]
- [LocalizedPropertyName(CultureType.English, "SweepSwinging")]
- public bool SweepSwinging { get; set; } = true;
-
- [LocalizedPropertyName(CultureType.Chinese, "清扫投掷武器")]
- [LocalizedPropertyName(CultureType.English, "SweepThrowable")]
- public bool SweepThrowable { get; set; } = true;
-
- [LocalizedPropertyName(CultureType.Chinese, "清扫普通物品")]
- [LocalizedPropertyName(CultureType.English, "SweepRegular")]
- public bool SweepRegular { get; set; } = true;
-
- [LocalizedPropertyName(CultureType.Chinese, "清扫装备")]
- [LocalizedPropertyName(CultureType.English, "SweepEquipment")]
- public bool SweepEquipment { get; set; } = true;
-
- [LocalizedPropertyName(CultureType.Chinese, "清扫时装")]
- [LocalizedPropertyName(CultureType.English, "SweepVanity")]
- public bool SweepVanity { get; set; } = true;
-
- [LocalizedPropertyName(CultureType.Chinese, "完成清扫消息")]
- [LocalizedPropertyName(CultureType.English, "SweepMsg")]
- public string CustomMessage { get; set; } = "";
-
- [LocalizedPropertyName(CultureType.Chinese, "清理提示")]
- [LocalizedPropertyName(CultureType.English, "SweepTip")]
- public bool SpecificMessage { get; set; } = true;
-}
\ No newline at end of file
diff --git a/src/AutoClear/Properties/AssemblyInfo.cs b/src/AutoClear/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..28a1c0ca6
--- /dev/null
+++ b/src/AutoClear/Properties/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("AutoClear.Tests")]
diff --git a/src/AutoClear/i18n/en-US.po b/src/AutoClear/i18n/en-US.po
deleted file mode 100644
index 24146a271..000000000
--- a/src/AutoClear/i18n/en-US.po
+++ /dev/null
@@ -1,37 +0,0 @@
-msgid ""
-msgstr ""
-"Project-Id-Version: tshock-chinese-plugin\n"
-"POT-Creation-Date: 2025-06-29 04:14:02+0000\n"
-"PO-Revision-Date: 2025-06-30 01:26\n"
-"Last-Translator: \n"
-"Language-Team: English\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Generator: GetText.NET Extractor\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-"X-Crowdin-Project: tshock-chinese-plugin\n"
-"X-Crowdin-Project-ID: 751499\n"
-"X-Crowdin-Language: en\n"
-"X-Crowdin-File: /master/src/AutoClear/i18n/template.pot\n"
-"X-Crowdin-File-ID: 1090\n"
-"Language: en_US\n"
-
-#: ../../AutoClear.cs:164
-#, csharp-format
-msgid "包含:【投掷武器[c/FFFFFF:{0}]】-【挥动武器[c/FFFFFF:{1}]】-【普通物品[c/FFFFFF:{2}]】-【装备[c/FFFFFF:{3}]】-【时装[c/FFFFFF:{4}]】"
-msgstr "Including:【Consumable[c/FFFFFF:{0}]】-【Melee Weapon[c/FFFFFF:{1}]】-【Common Items[c/FFFFFF:{2}]】-【Armor[c/FFFFFF:{3}]】-【Vanity[c/FFFFFF:{4}]】"
-
-#: ../../AutoClear.cs:13
-msgid "智能扫地机"
-msgstr "Smart sweeper"
-
-#: ../../AutoClear.cs:66
-msgid "智能扫地机: 由于存在事件或BOSS,已跳过自动清理"
-msgstr ""
-
-#: ../../AutoClear.cs:163
-#, csharp-format
-msgid "智能扫地机已清扫:[c/FFFFFF:{0}]种物品"
-msgstr "Autoclear has cleaned: [c/FFFFFF:{0}] types of items."
-
diff --git a/src/AutoClear/i18n/es-ES.po b/src/AutoClear/i18n/es-ES.po
deleted file mode 100644
index 178d13e9f..000000000
--- a/src/AutoClear/i18n/es-ES.po
+++ /dev/null
@@ -1,37 +0,0 @@
-msgid ""
-msgstr ""
-"Project-Id-Version: tshock-chinese-plugin\n"
-"POT-Creation-Date: 2025-06-29 04:14:02+0000\n"
-"PO-Revision-Date: 2025-06-30 01:26\n"
-"Last-Translator: \n"
-"Language-Team: Spanish\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Generator: GetText.NET Extractor\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-"X-Crowdin-Project: tshock-chinese-plugin\n"
-"X-Crowdin-Project-ID: 751499\n"
-"X-Crowdin-Language: es-ES\n"
-"X-Crowdin-File: /master/src/AutoClear/i18n/template.pot\n"
-"X-Crowdin-File-ID: 1090\n"
-"Language: es_ES\n"
-
-#: ../../AutoClear.cs:164
-#, csharp-format
-msgid "包含:【投掷武器[c/FFFFFF:{0}]】-【挥动武器[c/FFFFFF:{1}]】-【普通物品[c/FFFFFF:{2}]】-【装备[c/FFFFFF:{3}]】-【时装[c/FFFFFF:{4}]】"
-msgstr ""
-
-#: ../../AutoClear.cs:13
-msgid "智能扫地机"
-msgstr ""
-
-#: ../../AutoClear.cs:66
-msgid "智能扫地机: 由于存在事件或BOSS,已跳过自动清理"
-msgstr ""
-
-#: ../../AutoClear.cs:163
-#, csharp-format
-msgid "智能扫地机已清扫:[c/FFFFFF:{0}]种物品"
-msgstr ""
-
diff --git a/src/AutoClear/i18n/ru-RU.po b/src/AutoClear/i18n/ru-RU.po
deleted file mode 100644
index c00fbd2dd..000000000
--- a/src/AutoClear/i18n/ru-RU.po
+++ /dev/null
@@ -1,37 +0,0 @@
-msgid ""
-msgstr ""
-"Project-Id-Version: tshock-chinese-plugin\n"
-"POT-Creation-Date: 2025-06-29 04:14:02+0000\n"
-"PO-Revision-Date: 2025-06-30 01:26\n"
-"Last-Translator: \n"
-"Language-Team: Russian\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Generator: GetText.NET Extractor\n"
-"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
-"X-Crowdin-Project: tshock-chinese-plugin\n"
-"X-Crowdin-Project-ID: 751499\n"
-"X-Crowdin-Language: ru\n"
-"X-Crowdin-File: /master/src/AutoClear/i18n/template.pot\n"
-"X-Crowdin-File-ID: 1090\n"
-"Language: ru_RU\n"
-
-#: ../../AutoClear.cs:164
-#, csharp-format
-msgid "包含:【投掷武器[c/FFFFFF:{0}]】-【挥动武器[c/FFFFFF:{1}]】-【普通物品[c/FFFFFF:{2}]】-【装备[c/FFFFFF:{3}]】-【时装[c/FFFFFF:{4}]】"
-msgstr ""
-
-#: ../../AutoClear.cs:13
-msgid "智能扫地机"
-msgstr ""
-
-#: ../../AutoClear.cs:66
-msgid "智能扫地机: 由于存在事件或BOSS,已跳过自动清理"
-msgstr ""
-
-#: ../../AutoClear.cs:163
-#, csharp-format
-msgid "智能扫地机已清扫:[c/FFFFFF:{0}]种物品"
-msgstr ""
-
diff --git a/src/AutoClear/i18n/template.pot b/src/AutoClear/i18n/template.pot
deleted file mode 100644
index e058fc0d7..000000000
--- a/src/AutoClear/i18n/template.pot
+++ /dev/null
@@ -1,31 +0,0 @@
-msgid ""
-msgstr ""
-"Project-Id-Version: AutoClear\n"
-"POT-Creation-Date: 2025-06-29 04:14:02+0000\n"
-"PO-Revision-Date: 2025-06-29 04:14:03+0000\n"
-"Last-Translator: \n"
-"Language-Team: \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=utf-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"X-Generator: GetText.NET Extractor\n"
-
-#: ../../AutoClear.cs:164
-#, csharp-format
-msgid ""
-"包含:【投掷武器[c/FFFFFF:{0}]】-【挥动武器[c/FFFFFF:{1}]】-【普通物品[c/FFFFFF:{2}]】-【装备[c/FFFFFF:{3}]】-【时装[c/FFFFFF:{4}]】"
-msgstr ""
-
-#: ../../AutoClear.cs:13
-msgid "智能扫地机"
-msgstr ""
-
-#: ../../AutoClear.cs:66
-msgid "智能扫地机: 由于存在事件或BOSS,已跳过自动清理"
-msgstr ""
-
-#: ../../AutoClear.cs:163
-#, csharp-format
-msgid "智能扫地机已清扫:[c/FFFFFF:{0}]种物品"
-msgstr ""
-
diff --git a/src/AutoClear/manifest.json b/src/AutoClear/manifest.json
deleted file mode 100644
index 58e7c0098..000000000
--- a/src/AutoClear/manifest.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "README.en-US": {
- "Description": "Intelligent automatic cleaning"
- },
- "README.es-ES": {
- "Description": "Limpieza automática inteligente"
- },
- "README": {
- "Description": "智能自动扫地"
- }
-}
\ No newline at end of file