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