diff --git a/ConsoleCommands/BaseListCommand.cs b/ConsoleCommands/BaseListCommand.cs new file mode 100644 index 00000000..955f3c99 --- /dev/null +++ b/ConsoleCommands/BaseListCommand.cs @@ -0,0 +1,144 @@ +using EFT.InventoryLogic; +using EFT.Trainer.Extensions; +using EFT.Trainer.Features; +using JsonType; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System; +using System.Linq; +using Comfort.Common; +using EFT.Interactive; + +#nullable enable + +namespace EFT.Trainer.ConsoleCommands; + +internal abstract class BaseListCommand : LootItemsRelatedCommand +{ + public override string Pattern => OptionalArgumentPattern; + protected virtual ELootRarity? Rarity => null; + + public override void Execute(Match match) + { + ListLootItems(match, Rarity); + } + + private void ListLootItems(Match match, ELootRarity? rarityFilter = null) + { + var search = string.Empty; + var matchGroup = match.Groups[ValueGroup]; + if (matchGroup is {Success: true}) + { + search = matchGroup.Value.Trim(); + if (search == TrackedItem.MatchAll) + search = string.Empty; + } + + var world = Singleton.Instance; + if (world == null) + return; + + var itemsPerName = new Dictionary>(); + + // Step 1 - look outside containers and inside corpses (loot items) + FindLootItems(world, itemsPerName); + + // Step 2 - look inside containers (items) + if (LootItems.SearchInsideContainers) + FindItemsInContainers(world, itemsPerName); + + var names = itemsPerName.Keys.ToList(); + names.Sort(); + names.Reverse(); + + var count = 0; + foreach (var itemName in names) + { + if (itemName.IndexOf(search, StringComparison.OrdinalIgnoreCase) < 0) + continue; + + var list = itemsPerName[itemName]; + var rarity = list.First().Template.GetEstimatedRarity(); + if (rarityFilter.HasValue && rarityFilter.Value != rarity) + continue; + + var extra = rarity != ELootRarity.Not_exist ? $" ({rarity.Color()})" : string.Empty; + AddConsoleLog($"{itemName} [{list.Count.ToString().Cyan()}]{extra}"); + + count += list.Count; + } + + AddConsoleLog("------"); + AddConsoleLog($"found {count.ToString().Cyan()} item(s)"); + } + + private static void FindItemsInRootItem(Dictionary> itemsPerName, Item? rootItem) + { + var items = rootItem? + .GetAllItems()? + .ToArray(); + + if (items == null) + return; + + IndexItems(items, itemsPerName); + } + + private void FindLootItems(GameWorld world, Dictionary> itemsPerName) + { + var lootItems = world.LootItems; + var filteredItems = new List(); + for (var i = 0; i < lootItems.Count; i++) + { + var lootItem = lootItems.GetByIndex(i); + if (!lootItem.IsValid()) + continue; + + if (lootItem is Corpse corpse) + { + if (LootItems.SearchInsideCorpses) + FindItemsInRootItem(itemsPerName, corpse.ItemOwner?.RootItem); + + continue; + } + + filteredItems.Add(lootItem.Item); + } + + IndexItems(filteredItems, itemsPerName); + } + + private static void IndexItems(IEnumerable items, Dictionary> itemsPerName) + { + foreach (var item in items) + { + if (!item.IsValid() || item.IsFiltered()) + continue; + + var itemName = item.ShortName.Localized(); + if (!itemsPerName.TryGetValue(itemName, out var pnList)) + { + pnList = []; + itemsPerName[itemName] = pnList; + } + + pnList.Add(item); + } + } + + private static void FindItemsInContainers(GameWorld world, Dictionary> itemsPerName) + { + var owners = world.ItemOwners; // contains all containers: corpses, LootContainers, ... + foreach (var owner in owners) + { + var rootItem = owner.Key.RootItem; + if (rootItem is not { IsContainer: true }) + continue; + + if (!rootItem.IsValid() || rootItem.IsFiltered()) // filter default inventory container here, given we special case the corpse container + continue; + + FindItemsInRootItem(itemsPerName, rootItem); + } + } +} diff --git a/ConsoleCommands/BaseTemplateCommand.cs b/ConsoleCommands/BaseTemplateCommand.cs new file mode 100644 index 00000000..b044f62c --- /dev/null +++ b/ConsoleCommands/BaseTemplateCommand.cs @@ -0,0 +1,34 @@ +using System; +using EFT.InventoryLogic; +using System.Collections.Generic; +using System.Linq; +using Comfort.Common; + +#nullable enable + +namespace EFT.Trainer.ConsoleCommands; + +internal abstract class BaseTemplateCommand : ConsoleCommandWithArgument +{ + public override string Pattern => RequiredArgumentPattern; + + protected static IEnumerable FindTemplates(string searchShortNameOrTemplateId) + { + if (!Singleton.Instantiated) + return []; + + var templates = Singleton + .Instance + .ItemTemplates; + + // Match by TemplateId + if (templates.TryGetValue(searchShortNameOrTemplateId, out var template)) + return [template]; + + // Match by short name(s) + return templates + .Values + .Where(t => t.ShortNameLocalizationKey.Localized().IndexOf(searchShortNameOrTemplateId, StringComparison.OrdinalIgnoreCase) >= 0 + || t.NameLocalizationKey.Localized().IndexOf(searchShortNameOrTemplateId, StringComparison.OrdinalIgnoreCase) >= 0); + } +} diff --git a/ConsoleCommands/BaseTrackCommand.cs b/ConsoleCommands/BaseTrackCommand.cs new file mode 100644 index 00000000..08a33b37 --- /dev/null +++ b/ConsoleCommands/BaseTrackCommand.cs @@ -0,0 +1,35 @@ +using EFT.Trainer.Configuration; +using EFT.Trainer.Extensions; +using JsonType; +using System.Text.RegularExpressions; +using UnityEngine; + +#nullable enable + +namespace EFT.Trainer.ConsoleCommands; + +internal abstract class BaseTrackCommand : LootItemsRelatedCommand +{ + private static string ColorNames => string.Join("|", ColorConverter.ColorNames()); + public override string Pattern => $"(?<{ValueGroup}>.+?)(?<{ExtraGroup}> ({ColorNames}|\\[[\\.,\\d ]*\\]{{1}}))?"; + protected virtual ELootRarity? Rarity => null; + + public override void Execute(Match match) + { + TrackLootItem(match, Rarity); + } + + private void TrackLootItem(Match match, ELootRarity? rarity = null) + { + var matchGroup = match.Groups[ValueGroup]; + if (matchGroup is not {Success: true}) + return; + + Color? color = null; + var extraGroup = match.Groups[ExtraGroup]; + if (extraGroup is {Success: true}) + color = ColorConverter.Parse(extraGroup.Value); + + TrackList.ShowTrackList(this, LootItems, LootItems.Track(matchGroup.Value, color, rarity)); + } +} diff --git a/ConsoleCommands/BaseTrackListCommand.cs b/ConsoleCommands/BaseTrackListCommand.cs new file mode 100644 index 00000000..c9fb411c --- /dev/null +++ b/ConsoleCommands/BaseTrackListCommand.cs @@ -0,0 +1,31 @@ +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Text.RegularExpressions; + +#nullable enable + +namespace EFT.Trainer.ConsoleCommands; + +internal abstract class BaseTrackListCommand : LootItemsRelatedCommand +{ + public override string Pattern => RequiredArgumentPattern; + + protected static bool TryGetTrackListFilename(Match match, [NotNullWhen(true)] out string? filename) + { + filename = null; + + var matchGroup = match.Groups[ValueGroup]; + if (matchGroup is not {Success: true}) + return false; + + filename = matchGroup.Value; + + if (!Path.IsPathRooted(filename)) + filename = Path.Combine(Context.UserPath, filename); + + if (!Path.HasExtension(filename)) + filename += ".tl"; + + return true; + } +} diff --git a/ConsoleCommands/BuiltInCommand.cs b/ConsoleCommands/BuiltInCommand.cs new file mode 100644 index 00000000..cbc24679 --- /dev/null +++ b/ConsoleCommands/BuiltInCommand.cs @@ -0,0 +1,15 @@ +using System; + +#nullable enable + +namespace EFT.Trainer.ConsoleCommands; + +internal class BuiltInCommand(string name, Action action) : ConsoleCommandWithoutArgument +{ + public override string Name => name; + + public override void Execute() + { + action(); + } +} diff --git a/ConsoleCommands/ConsoleCommand.cs b/ConsoleCommands/ConsoleCommand.cs new file mode 100644 index 00000000..f89715bc --- /dev/null +++ b/ConsoleCommands/ConsoleCommand.cs @@ -0,0 +1,18 @@ +using EFT.UI; + +#nullable enable + +namespace EFT.Trainer.ConsoleCommands; + +internal abstract class ConsoleCommand +{ + public abstract string Name { get; } + + internal void AddConsoleLog(string log) + { + if (PreloaderUI.Instantiated) + ConsoleScreen.Log(log); + } + + public abstract void Register(); +} diff --git a/ConsoleCommands/ConsoleCommandWithArgument.cs b/ConsoleCommands/ConsoleCommandWithArgument.cs new file mode 100644 index 00000000..9278d20f --- /dev/null +++ b/ConsoleCommands/ConsoleCommandWithArgument.cs @@ -0,0 +1,38 @@ +using System.Text.RegularExpressions; +using EFT.UI; + +#nullable enable + +namespace EFT.Trainer.ConsoleCommands; + +internal abstract class ConsoleCommandWithArgument : ConsoleCommand +{ + public abstract string Pattern { get; } + + public abstract void Execute(Match match); + + protected const string ValueGroup = "value"; + protected const string ExtraGroup = "extra"; + + protected const string RequiredArgumentPattern = $"(?<{ValueGroup}>.+)"; + protected const string OptionalArgumentPattern = $"(?<{ValueGroup}>.*)"; + + public override void Register() + { +#if DEBUG + AddConsoleLog($"Registering {Name} command with arguments..."); +#endif + ConsoleScreen.Processor.RegisterCommand(Name, (string args) => + { + var regex = new Regex("^" + Pattern + "$"); + if (regex.IsMatch(args)) + { + Execute(regex.Match(args)); + } + else + { + ConsoleScreen.LogError("Invalid arguments"); + } + }); + } +} diff --git a/ConsoleCommands/ConsoleCommandWithoutArgument.cs b/ConsoleCommands/ConsoleCommandWithoutArgument.cs new file mode 100644 index 00000000..4c043726 --- /dev/null +++ b/ConsoleCommands/ConsoleCommandWithoutArgument.cs @@ -0,0 +1,18 @@ +using EFT.UI; + +#nullable enable + +namespace EFT.Trainer.ConsoleCommands; + +internal abstract class ConsoleCommandWithoutArgument : ConsoleCommand +{ + public abstract void Execute(); + + public override void Register() + { +#if DEBUG + AddConsoleLog($"Registering {Name} command..."); +#endif + ConsoleScreen.Processor.RegisterCommand(Name, Execute); + } +} diff --git a/ConsoleCommands/Dump.cs b/ConsoleCommands/Dump.cs new file mode 100644 index 00000000..b0ec9e84 --- /dev/null +++ b/ConsoleCommands/Dump.cs @@ -0,0 +1,54 @@ +using System; +using System.IO; +using EFT.Trainer.Features; +using JetBrains.Annotations; +using UnityEngine; +using UnityEngine.SceneManagement; + +#nullable enable + +namespace EFT.Trainer.ConsoleCommands; + +[UsedImplicitly] +internal class Dump : ConsoleCommandWithoutArgument +{ + public override string Name => "dump"; + + public override void Execute() + { + var dumpfolder = Path.Combine(Context.UserPath, "Dumps"); + var thisDump = Path.Combine(dumpfolder, $"{DateTime.Now:yyyyMMdd-HHmmss}"); + + Directory.CreateDirectory(thisDump); + + AddConsoleLog("Dumping scenes..."); + for (int i = 0; i < SceneManager.sceneCount; i++) + { + var scene = SceneManager.GetSceneAt(i); + if (!scene.isLoaded) + continue; + + var json = SceneDumper.DumpScene(scene).ToPrettyJson(); + File.WriteAllText(Path.Combine(thisDump, GetSafeFilename($"@scene - {scene.name}.txt")), json); + } + + AddConsoleLog("Dumping game objects..."); + foreach (var go in UnityEngine.Object.FindObjectsOfType()) + { + if (go == null || go.transform.parent != null || !go.activeSelf) + continue; + + var filename = GetSafeFilename(go.name + "-" + go.GetHashCode() + ".txt"); + var json = SceneDumper.DumpGameObject(go).ToPrettyJson(); + File.WriteAllText(Path.Combine(thisDump, filename), json); + } + + AddConsoleLog($"Dump created in {thisDump}"); + } + + private static string GetSafeFilename(string filename) + { + return string.Join("_", filename.Split(Path.GetInvalidFileNameChars())); + } + +} diff --git a/ConsoleCommands/List.cs b/ConsoleCommands/List.cs new file mode 100644 index 00000000..41d63d00 --- /dev/null +++ b/ConsoleCommands/List.cs @@ -0,0 +1,12 @@ +using JetBrains.Annotations; +using JsonType; + +#nullable enable + +namespace EFT.Trainer.ConsoleCommands; + +[UsedImplicitly] +internal class List : BaseListCommand +{ + public override string Name => "list"; +} diff --git a/ConsoleCommands/ListRare.cs b/ConsoleCommands/ListRare.cs new file mode 100644 index 00000000..5d297226 --- /dev/null +++ b/ConsoleCommands/ListRare.cs @@ -0,0 +1,13 @@ +using JetBrains.Annotations; +using JsonType; + +#nullable enable + +namespace EFT.Trainer.ConsoleCommands; + +[UsedImplicitly] +internal class ListRare : BaseListCommand +{ + public override string Name => "listr"; + protected override ELootRarity? Rarity => ELootRarity.Rare; +} diff --git a/ConsoleCommands/ListSuperRare.cs b/ConsoleCommands/ListSuperRare.cs new file mode 100644 index 00000000..2dedb987 --- /dev/null +++ b/ConsoleCommands/ListSuperRare.cs @@ -0,0 +1,13 @@ +using JetBrains.Annotations; +using JsonType; + +#nullable enable + +namespace EFT.Trainer.ConsoleCommands; + +[UsedImplicitly] +internal class ListSuperRare : BaseListCommand +{ + public override string Name => "listsr"; + protected override ELootRarity? Rarity => ELootRarity.Superrare; +} diff --git a/ConsoleCommands/LoadTrackList.cs b/ConsoleCommands/LoadTrackList.cs new file mode 100644 index 00000000..89d039f7 --- /dev/null +++ b/ConsoleCommands/LoadTrackList.cs @@ -0,0 +1,21 @@ +using System.Text.RegularExpressions; +using EFT.Trainer.Configuration; +using JetBrains.Annotations; + +#nullable enable + +namespace EFT.Trainer.ConsoleCommands; + +[UsedImplicitly] +internal class LoadTrackList : BaseTrackListCommand +{ + public override string Name => "loadtl"; + + public override void Execute(Match match) + { + if (!TryGetTrackListFilename(match, out var filename)) + return; + + ConfigurationManager.LoadPropertyValue(filename, LootItems, nameof(Features.LootItems.TrackedNames)); + } +} diff --git a/ConsoleCommands/LootItemsRelatedCommand.cs b/ConsoleCommands/LootItemsRelatedCommand.cs new file mode 100644 index 00000000..b5d2c208 --- /dev/null +++ b/ConsoleCommands/LootItemsRelatedCommand.cs @@ -0,0 +1,12 @@ +using System; +using EFT.Trainer.Features; + +#nullable enable + +namespace EFT.Trainer.ConsoleCommands; + +internal abstract class LootItemsRelatedCommand : ConsoleCommandWithArgument +{ + private readonly Lazy _lootItems = new(() => FeatureFactory.GetFeature()!); + protected LootItems LootItems => _lootItems.Value; +} diff --git a/ConsoleCommands/SaveTrackList.cs b/ConsoleCommands/SaveTrackList.cs new file mode 100644 index 00000000..e6d578e5 --- /dev/null +++ b/ConsoleCommands/SaveTrackList.cs @@ -0,0 +1,21 @@ +using System.Text.RegularExpressions; +using EFT.Trainer.Configuration; +using JetBrains.Annotations; + +#nullable enable + +namespace EFT.Trainer.ConsoleCommands; + +[UsedImplicitly] +internal class SaveTrackList : BaseTrackListCommand +{ + public override string Name => "savetl"; + + public override void Execute(Match match) + { + if (!TryGetTrackListFilename(match, out var filename)) + return; + + ConfigurationManager.SavePropertyValue(filename, LootItems, nameof(LootItems.TrackedNames)); + } +} diff --git a/ConsoleCommands/Spawn.cs b/ConsoleCommands/Spawn.cs new file mode 100644 index 00000000..8da25ba1 --- /dev/null +++ b/ConsoleCommands/Spawn.cs @@ -0,0 +1,87 @@ +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Comfort.Common; +using Diz.Utils; +using EFT.CameraControl; +using EFT.Trainer.Extensions; +using EFT.Trainer.Features; +using JetBrains.Annotations; + +#nullable enable + +namespace EFT.Trainer.ConsoleCommands; + +[UsedImplicitly] +internal class Spawn : BaseTemplateCommand +{ + public override string Name => "spawn"; + + public override void Execute(Match match) + { + var matchGroup = match.Groups[ValueGroup]; + if (matchGroup is not { Success: true }) + return; + + var player = GameState.Current?.LocalPlayer; + if (player == null) + return; + + var search = matchGroup.Value; + var templates = FindTemplates(search).ToArray(); + + switch (templates.Length) + { + case 0: + AddConsoleLog("No template found!"); + return; + case > 1: + { + foreach (var template in templates) + AddConsoleLog($"{template._id}: {template.ShortNameLocalizationKey.Localized().Green()} [{template.NameLocalizationKey.Localized()}]"); + + AddConsoleLog($"found {templates.Length.ToString().Cyan()} templates, be more specific"); + return; + } + } + + var tpl = templates[0]; + var poolManager = Singleton.Instance; + + poolManager + .LoadBundlesAndCreatePools(PoolManager.PoolsCategory.Raid, PoolManager.AssemblyType.Online, [..tpl.AllResources], JobPriority.Immediate) + .ContinueWith(task => + { + AsyncWorker.RunInMainTread(delegate + { + if (task.IsFaulted) + { + AddConsoleLog("Failed to load item bundle!"); + } + else + { + var itemFactory = Singleton.Instance; + var item = itemFactory.CreateItem(MongoID.Generate(), tpl._id, null); + if (item == null) + { + AddConsoleLog("Failed to create item!"); + } + else + { + item.SpawnedInSession = true; // found in raid + + _ = new TraderControllerClass(item, item.Id, item.ShortName); + var go = poolManager.CreateLootPrefab(item, ECameraType.Default); + + go.SetActive(value: true); + var lootItem = Singleton.Instance.CreateLootWithRigidbody(go, item, item.ShortName, Singleton.Instance, randomRotation: false, null, out _); + lootItem.transform.SetPositionAndRotation(player.Transform.position + player.Transform.forward * 2f + player.Transform.up * 0.5f, player.Transform.rotation); + lootItem.LastOwner = player; + } + } + }); + + return Task.CompletedTask; + }); + } +} diff --git a/ConsoleCommands/Status.cs b/ConsoleCommands/Status.cs new file mode 100644 index 00000000..3b3daa3a --- /dev/null +++ b/ConsoleCommands/Status.cs @@ -0,0 +1,30 @@ +using EFT.Trainer.Features; +using JetBrains.Annotations; +using UnityEngine; + +#nullable enable + +namespace EFT.Trainer.ConsoleCommands; + +[UsedImplicitly] +internal class Status : ConsoleCommandWithoutArgument +{ + public override string Name => "status"; + + private static string GetFeatureHelpText(ToggleFeature feature) + { + var toggleKey = feature.Key != KeyCode.None ? $" ({feature.Key} to toggle)" : string.Empty; + return $"{feature.Name} is {(feature.Enabled ? "on".Green() : "off".Red())}{toggleKey}"; + } + + public override void Execute() + { + foreach (var feature in Context.ToggleableFeatures.Value) + { + if (feature is Commands or GameState) + continue; + + AddConsoleLog(GetFeatureHelpText(feature)); + } + } +} diff --git a/ConsoleCommands/Template.cs b/ConsoleCommands/Template.cs new file mode 100644 index 00000000..01a4945a --- /dev/null +++ b/ConsoleCommands/Template.cs @@ -0,0 +1,35 @@ +using System.Linq; +using System.Text.RegularExpressions; +using Comfort.Common; +using EFT.Trainer.Extensions; +using JetBrains.Annotations; + +#nullable enable + +namespace EFT.Trainer.ConsoleCommands; + +[UsedImplicitly] +internal class Template : BaseTemplateCommand +{ + public override string Name => "template"; + + public override void Execute(Match match) + { + var matchGroup = match.Groups[ValueGroup]; + if (matchGroup is not {Success: true}) + return; + + if (!Singleton.Instantiated) + return; + + var search = matchGroup.Value; + + var templates = FindTemplates(search).ToArray(); + + foreach (var template in templates) + AddConsoleLog($"{template._id}: {template.ShortNameLocalizationKey.Localized().Green()} [{template.NameLocalizationKey.Localized()}]"); + + AddConsoleLog("------"); + AddConsoleLog($"found {templates.Length.ToString().Cyan()} template(s)"); + } +} diff --git a/ConsoleCommands/ToggleFeatureCommand.cs b/ConsoleCommands/ToggleFeatureCommand.cs new file mode 100644 index 00000000..e9d0405c --- /dev/null +++ b/ConsoleCommands/ToggleFeatureCommand.cs @@ -0,0 +1,26 @@ +using System.Text.RegularExpressions; +using EFT.Trainer.Features; + +#nullable enable + +namespace EFT.Trainer.ConsoleCommands; + +internal class ToggleFeatureCommand(ToggleFeature feature) : ConsoleCommandWithArgument +{ + public override string Name => feature.Name; + public override string Pattern => $"(?<{ValueGroup}>(on)|(off))"; + + public override void Execute(Match match) + { + var matchGroup = match.Groups[ValueGroup]; + if (matchGroup is not {Success: true}) + return; + + feature.Enabled = matchGroup.Value switch + { + "on" => true, + "off" => false, + _ => feature.Enabled + }; + } +} diff --git a/ConsoleCommands/Track.cs b/ConsoleCommands/Track.cs new file mode 100644 index 00000000..221a2833 --- /dev/null +++ b/ConsoleCommands/Track.cs @@ -0,0 +1,11 @@ +using JetBrains.Annotations; + +#nullable enable + +namespace EFT.Trainer.ConsoleCommands; + +[UsedImplicitly] +internal class Track : BaseTrackCommand +{ + public override string Name => "track"; +} diff --git a/ConsoleCommands/TrackList.cs b/ConsoleCommands/TrackList.cs new file mode 100644 index 00000000..e5dd72dc --- /dev/null +++ b/ConsoleCommands/TrackList.cs @@ -0,0 +1,39 @@ +using System; +using EFT.Trainer.Extensions; +using EFT.Trainer.Features; +using JetBrains.Annotations; + +#nullable enable + +namespace EFT.Trainer.ConsoleCommands; + +[UsedImplicitly] +internal class TrackList : ConsoleCommandWithoutArgument +{ + public override string Name => "tracklist"; + + // Unfortunately we cannot use BaseTrackCommand or LootItemsRelatedCommand here + private readonly Lazy _lootItems = new(() => FeatureFactory.GetFeature()!); + private LootItems LootItems => _lootItems.Value; + + + public override void Execute() + { + ShowTrackList(this, LootItems); + } + + internal static void ShowTrackList(ConsoleCommand command, LootItems lootItems, bool changed = false) + { + if (changed) + command.AddConsoleLog("Tracking list updated..."); + + foreach (var templateId in lootItems.Wishlist) + command.AddConsoleLog($"Tracking: {templateId.LocalizedShortName()} (Wishlist)"); + + foreach (var item in lootItems.TrackedNames) + { + var extra = item.Rarity.HasValue ? $" ({item.Rarity.Value.Color()})" : string.Empty; + command.AddConsoleLog(item.Color.HasValue ? $"Tracking: {item.Name.Color(item.Color.Value)}{extra}" : $"Tracking: {item.Name}{extra}"); + } + } +} diff --git a/ConsoleCommands/TrackRare.cs b/ConsoleCommands/TrackRare.cs new file mode 100644 index 00000000..58dfd6d4 --- /dev/null +++ b/ConsoleCommands/TrackRare.cs @@ -0,0 +1,13 @@ +using JetBrains.Annotations; +using JsonType; + +#nullable enable + +namespace EFT.Trainer.ConsoleCommands; + +[UsedImplicitly] +internal class TrackRare : BaseTrackCommand +{ + public override string Name => "trackr"; + protected override ELootRarity? Rarity => ELootRarity.Rare; +} diff --git a/ConsoleCommands/TrackSuperRare.cs b/ConsoleCommands/TrackSuperRare.cs new file mode 100644 index 00000000..8d76abdd --- /dev/null +++ b/ConsoleCommands/TrackSuperRare.cs @@ -0,0 +1,13 @@ +using JetBrains.Annotations; +using JsonType; + +#nullable enable + +namespace EFT.Trainer.ConsoleCommands; + +[UsedImplicitly] +internal class TrackSuperRare : BaseTrackCommand +{ + public override string Name => "tracksr"; + protected override ELootRarity? Rarity => ELootRarity.Superrare; +} diff --git a/ConsoleCommands/UnTrack.cs b/ConsoleCommands/UnTrack.cs new file mode 100644 index 00000000..eabbb700 --- /dev/null +++ b/ConsoleCommands/UnTrack.cs @@ -0,0 +1,21 @@ +using System.Text.RegularExpressions; +using JetBrains.Annotations; + +#nullable enable + +namespace EFT.Trainer.ConsoleCommands; + +[UsedImplicitly] +internal class UnTrack : BaseTrackCommand +{ + public override string Name => "untrack"; + + public override void Execute(Match match) + { + var matchGroup = match.Groups[ValueGroup]; + if (matchGroup is not {Success: true}) + return; + + TrackList.ShowTrackList(this, LootItems, LootItems.UnTrack(matchGroup.Value)); + } +} diff --git a/Context.cs b/Context.cs new file mode 100644 index 00000000..f9e8dd73 --- /dev/null +++ b/Context.cs @@ -0,0 +1,15 @@ +using System; +using System.IO; +using System.Linq; +using EFT.Trainer.Features; + +namespace EFT.Trainer; + +internal static class Context +{ + public static string UserPath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Escape from Tarkov"); + public static string ConfigFile => Path.Combine(UserPath, "trainer.ini"); + + public static Lazy Features => new(() => [.. FeatureFactory.GetAllFeatures().OrderBy(f => f.Name)]); + public static Lazy ToggleableFeatures => new(() => [.. FeatureFactory.GetAllToggleableFeatures().OrderByDescending(f => f.Name)]); +} diff --git a/Features/Commands.cs b/Features/Commands.cs index 909b8305..18f777c8 100644 --- a/Features/Commands.cs +++ b/Features/Commands.cs @@ -1,21 +1,10 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Linq; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Comfort.Common; -using Diz.Utils; -using EFT.CameraControl; -using EFT.Interactive; -using EFT.InventoryLogic; using EFT.Trainer.Configuration; -using EFT.Trainer.Extensions; +using EFT.Trainer.ConsoleCommands; using EFT.UI; -using JsonType; using UnityEngine; -using UnityEngine.SceneManagement; #nullable enable @@ -38,10 +27,6 @@ internal class Commands : FeatureRenderer public override KeyCode Key { get; set; } = KeyCode.RightAlt; private bool Registered { get; set; } = false; - private const string ValueGroup = "value"; - private const string ExtraGroup = "extra"; - private const float DefaultX = 40f; - private const float DefaultY = 20f; protected override void Update() { @@ -59,40 +44,25 @@ protected override void Update() private void RegisterCommands() { - foreach(var feature in ToggleableFeatures.Value) + foreach(var feature in Context.ToggleableFeatures.Value) { if (feature is Commands or GameState) continue; - CreateCommand($"{feature.Name}", $"(?<{ValueGroup}>(on)|(off))", m => OnToggleFeature(feature, m)); - - if (feature is not LootItems liFeature) - continue; - - CreateCommand("list", $"(?<{ValueGroup}>.*)", m => ListLootItems(m, liFeature)); - CreateCommand("listr", $"(?<{ValueGroup}>.*)", m => ListLootItems(m, liFeature, ELootRarity.Rare)); - CreateCommand("listsr", $"(?<{ValueGroup}>.*)", m => ListLootItems(m, liFeature, ELootRarity.Superrare)); - - var colorNames = string.Join("|", ColorConverter.ColorNames()); - CreateCommand("track", $"(?.+?)(? ({colorNames}|\\[[\\.,\\d ]*\\]{{1}}))?", m => TrackLootItem(m, liFeature)); - CreateCommand("trackr", $"(?.+?)(? ({colorNames}|\\[[\\.,\\d ]*\\]{{1}}))?", m => TrackLootItem(m, liFeature, ELootRarity.Rare)); - CreateCommand("tracksr", $"(?.+?)(? ({colorNames}|\\[[\\.,\\d ]*\\]{{1}}))?", m => TrackLootItem(m, liFeature, ELootRarity.Superrare)); - - CreateCommand("untrack", $"(?<{ValueGroup}>.+)", m => UnTrackLootItem(m, liFeature)); - CreateCommand("loadtl", $"(?<{ValueGroup}>.+)", m => LoadTrackList(m, liFeature)); - CreateCommand("savetl", $"(?<{ValueGroup}>.+)", m => SaveTrackList(m, liFeature)); - - CreateCommand("tracklist", () => ShowTrackList(liFeature)); + new ToggleFeatureCommand(feature) + .Register(); } - CreateCommand("dump", Dump); - CreateCommand("status", Status); + // Dynamically register commands + foreach (var command in GetCommands()) + command.Register(); - CreateCommand("load", () => LoadSettings()); - CreateCommand("save", SaveSettings); + // built-in commands + new BuiltInCommand("load", () => LoadSettings()) + .Register(); - CreateCommand("spawn", $"(?<{ValueGroup}>.+)", SpawnItem); - CreateCommand("template", $"(?<{ValueGroup}>.+)", FindTemplates); + new BuiltInCommand("save", SaveSettings) + .Register(); // Load default configuration LoadSettings(false); @@ -101,419 +71,14 @@ private void RegisterCommands() Registered = true; } - private static IEnumerable FindTemplates(string searchShortNameOrTemplateId) - { - if (!Singleton.Instantiated) - return []; - - var templates = Singleton - .Instance - .ItemTemplates; - - // Match by TemplateId - if (templates.TryGetValue(searchShortNameOrTemplateId, out var template)) - return [template]; - - // Match by short name(s) - return templates - .Values - .Where(t => t.ShortNameLocalizationKey.Localized().IndexOf(searchShortNameOrTemplateId, StringComparison.OrdinalIgnoreCase) >= 0 - || t.NameLocalizationKey.Localized().IndexOf(searchShortNameOrTemplateId, StringComparison.OrdinalIgnoreCase) >= 0); - } - - private void FindTemplates(Match match) - { - var matchGroup = match.Groups[ValueGroup]; - if (matchGroup is not {Success: true}) - return; - - if (!Singleton.Instantiated) - return; - - var search = matchGroup.Value; - - var templates = FindTemplates(search).ToArray(); - - foreach (var template in templates) - AddConsoleLog($"{template._id}: {template.ShortNameLocalizationKey.Localized().Green()} [{template.NameLocalizationKey.Localized()}]"); - - AddConsoleLog("------"); - AddConsoleLog($"found {templates.Length.ToString().Cyan()} template(s)"); - } - - private void SpawnItem(Match match) - { - var matchGroup = match.Groups[ValueGroup]; - if (matchGroup is not { Success: true }) - return; - - var player = GameState.Current?.LocalPlayer; - if (player == null) - return; - - var search = matchGroup.Value; - var templates = FindTemplates(search).ToArray(); - - switch (templates.Length) - { - case 0: - AddConsoleLog("No template found!"); - return; - case > 1: - { - foreach (var template in templates) - AddConsoleLog($"{template._id}: {template.ShortNameLocalizationKey.Localized().Green()} [{template.NameLocalizationKey.Localized()}]"); - - AddConsoleLog($"found {templates.Length.ToString().Cyan()} templates, be more specific"); - return; - } - } - - var tpl = templates[0]; - var poolManager = Singleton.Instance; - - poolManager - .LoadBundlesAndCreatePools(PoolManager.PoolsCategory.Raid, PoolManager.AssemblyType.Online, [..tpl.AllResources], JobPriority.Immediate) - .ContinueWith(task => - { - AsyncWorker.RunInMainTread(delegate - { - if (task.IsFaulted) - { - AddConsoleLog("Failed to load item bundle!"); - } - else - { - var itemFactory = Singleton.Instance; - var item = itemFactory.CreateItem(MongoID.Generate(), tpl._id, null); - if (item == null) - { - AddConsoleLog("Failed to create item!"); - } - else - { - item.SpawnedInSession = true; // found in raid - - _ = new TraderControllerClass(item, item.Id, item.ShortName); - var go = poolManager.CreateLootPrefab(item, ECameraType.Default); - - go.SetActive(value: true); - var lootItem = Singleton.Instance.CreateLootWithRigidbody(go, item, item.ShortName, Singleton.Instance, randomRotation: false, null, out _); - lootItem.transform.SetPositionAndRotation(player.Transform.position + player.Transform.forward * 2f + player.Transform.up * 0.5f, player.Transform.rotation); - lootItem.LastOwner = player; - } - } - }); - - return Task.CompletedTask; - }); - } - - private void SetupWindowCoordinates() - { - bool needfix = false; - X = FixCoordinate(X, Screen.width, DefaultX, ref needfix); - Y = FixCoordinate(Y, Screen.height, DefaultY, ref needfix); - - if (needfix) - SaveSettings(); - } - - private static float FixCoordinate(float coord, float maxValue, float defaultValue, ref bool needfix) - { - if (coord < 0 || coord >= maxValue) - { - coord = defaultValue; - needfix = true; - } - - return coord; - } - - private void CreateCommand(string cmdName, Action action) - { -#if DEBUG - AddConsoleLog($"Registering {cmdName} command..."); -#endif - ConsoleScreen.Processor.RegisterCommand(cmdName, action); - } - - private void CreateCommand(string cmdName, string pattern, Action action) - { -#if DEBUG - AddConsoleLog($"Registering {cmdName} command..."); -#endif - ConsoleScreen.Processor.RegisterCommand(cmdName, (string args) => - { - var regex = new Regex("^" + pattern + "$"); - if (regex.IsMatch(args)) - { - action(regex.Match(args)); - } - else - { - ConsoleScreen.LogError("Invalid arguments"); - } - }); - } - - private void ShowTrackList(LootItems feature, bool changed = false) - { - if (changed) - AddConsoleLog("Tracking list updated..."); - - foreach (var templateId in feature.Wishlist) - AddConsoleLog($"Tracking: {templateId.LocalizedShortName()} (Wishlist)"); - - foreach (var item in feature.TrackedNames) - { - var extra = item.Rarity.HasValue ? $" ({item.Rarity.Value.Color()})" : string.Empty; - AddConsoleLog(item.Color.HasValue ? $"Tracking: {item.Name.Color(item.Color.Value)}{extra}" : $"Tracking: {item.Name}{extra}"); - } - } - - private static bool TryGetTrackListFilename(Match match, [NotNullWhen(true)] out string? filename) - { - filename = null; - - var matchGroup = match.Groups[ValueGroup]; - if (matchGroup is not {Success: true}) - return false; - - filename = matchGroup.Value; - - if (!Path.IsPathRooted(filename)) - filename = Path.Combine(UserPath, filename); - - if (!Path.HasExtension(filename)) - filename += ".tl"; - - return true; - } - - private static void LoadTrackList(Match match, LootItems feature) - { - if (!TryGetTrackListFilename(match, out var filename)) - return; - - ConfigurationManager.LoadPropertyValue(filename, feature, nameof(LootItems.TrackedNames)); - } - - private static void SaveTrackList(Match match, LootItems feature) - { - if (!TryGetTrackListFilename(match, out var filename)) - return; - - ConfigurationManager.SavePropertyValue(filename, feature, nameof(LootItems.TrackedNames)); - } - - private void UnTrackLootItem(Match match, LootItems feature) - { - var matchGroup = match.Groups[ValueGroup]; - if (matchGroup is not {Success: true}) - return; - - ShowTrackList(feature, feature.UnTrack(matchGroup.Value)); - } - - private void TrackLootItem(Match match, LootItems feature, ELootRarity? rarity = null) - { - var matchGroup = match.Groups[ValueGroup]; - if (matchGroup is not {Success: true}) - return; - - Color? color = null; - var extraGroup = match.Groups[ExtraGroup]; - if (extraGroup is {Success: true}) - color = ColorConverter.Parse(extraGroup.Value); - - ShowTrackList(feature, feature.Track(matchGroup.Value, color, rarity)); - } - - private void ListLootItems(Match match, LootItems feature, ELootRarity? rarityFilter = null) - { - var search = string.Empty; - var matchGroup = match.Groups[ValueGroup]; - if (matchGroup is {Success: true}) - { - search = matchGroup.Value.Trim(); - if (search == TrackedItem.MatchAll) - search = string.Empty; - } - - var world = Singleton.Instance; - if (world == null) - return; - - var itemsPerName = new Dictionary>(); - - // Step 1 - look outside containers and inside corpses (loot items) - FindLootItems(world, itemsPerName, feature); - - // Step 2 - look inside containers (items) - if (feature.SearchInsideContainers) - FindItemsInContainers(world, itemsPerName); - - var names = itemsPerName.Keys.ToList(); - names.Sort(); - names.Reverse(); - - var count = 0; - foreach (var itemName in names) - { - if (itemName.IndexOf(search, StringComparison.OrdinalIgnoreCase) < 0) - continue; - - var list = itemsPerName[itemName]; - var rarity = list.First().Template.GetEstimatedRarity(); - if (rarityFilter.HasValue && rarityFilter.Value != rarity) - continue; - - var extra = rarity != ELootRarity.Not_exist ? $" ({rarity.Color()})" : string.Empty; - AddConsoleLog($"{itemName} [{list.Count.ToString().Cyan()}]{extra}"); - - count += list.Count; - } - - AddConsoleLog("------"); - AddConsoleLog($"found {count.ToString().Cyan()} item(s)"); - } - - private static void FindItemsInContainers(GameWorld world, Dictionary> itemsPerName) - { - var owners = world.ItemOwners; // contains all containers: corpses, LootContainers, ... - foreach (var owner in owners) - { - var rootItem = owner.Key.RootItem; - if (rootItem is not { IsContainer: true }) - continue; - - if (!rootItem.IsValid() || rootItem.IsFiltered()) // filter default inventory container here, given we special case the corpse container - continue; - - FindItemsInRootItem(itemsPerName, rootItem); - } - } - - private static void FindItemsInRootItem(Dictionary> itemsPerName, Item? rootItem) - { - var items = rootItem? - .GetAllItems()? - .ToArray(); - - if (items == null) - return; - - IndexItems(items, itemsPerName); - } - - private static void FindLootItems(GameWorld world, Dictionary> itemsPerName, LootItems feature) - { - var lootItems = world.LootItems; - var filteredItems = new List(); - for (var i = 0; i < lootItems.Count; i++) - { - var lootItem = lootItems.GetByIndex(i); - if (!lootItem.IsValid()) - continue; - - if (lootItem is Corpse corpse) - { - if (feature.SearchInsideCorpses) - FindItemsInRootItem(itemsPerName, corpse.ItemOwner?.RootItem); - - continue; - } - - filteredItems.Add(lootItem.Item); - } - - IndexItems(filteredItems, itemsPerName); - } - - private static void IndexItems(IEnumerable items, Dictionary> itemsPerName) - { - foreach (var item in items) - { - if (!item.IsValid() || item.IsFiltered()) - continue; - - var itemName = item.ShortName.Localized(); - if (!itemsPerName.TryGetValue(itemName, out var pnList)) - { - pnList = []; - itemsPerName[itemName] = pnList; - } - - pnList.Add(item); - } - } - - private static string GetFeatureHelpText(ToggleFeature feature) - { - var toggleKey = feature.Key != KeyCode.None ? $" ({feature.Key} to toggle)" : string.Empty; - return $"{feature.Name} is {(feature.Enabled ? "on".Green() : "off".Red())}{toggleKey}"; - } - - private void Status() - { - foreach (var feature in ToggleableFeatures.Value) - { - if (feature is Commands or GameState) - continue; - - AddConsoleLog(GetFeatureHelpText(feature)); - } - } - - private void Dump() - { - var dumpfolder = Path.Combine(UserPath, "Dumps"); - var thisDump = Path.Combine(dumpfolder, $"{DateTime.Now:yyyyMMdd-HHmmss}"); - - Directory.CreateDirectory(thisDump); - - AddConsoleLog("Dumping scenes..."); - for (int i = 0; i < SceneManager.sceneCount; i++) - { - var scene = SceneManager.GetSceneAt(i); - if (!scene.isLoaded) - continue; - - var json = SceneDumper.DumpScene(scene).ToPrettyJson(); - File.WriteAllText(Path.Combine(thisDump, GetSafeFilename($"@scene - {scene.name}.txt")), json); - } - - AddConsoleLog("Dumping game objects..."); - foreach (var go in FindObjectsOfType()) - { - if (go == null || go.transform.parent != null || !go.activeSelf) - continue; - - var filename = GetSafeFilename(go.name + "-" + go.GetHashCode() + ".txt"); - var json = SceneDumper.DumpGameObject(go).ToPrettyJson(); - File.WriteAllText(Path.Combine(thisDump, filename), json); - } - - AddConsoleLog($"Dump created in {thisDump}"); - } - - private static string GetSafeFilename(string filename) + private IEnumerable GetCommands() { - return string.Join("_", filename.Split(Path.GetInvalidFileNameChars())); - } + var types = GetType() + .Assembly + .GetTypes() + .Where(t => t.IsSubclassOf(typeof(ConsoleCommand)) && !t.IsAbstract && t.GetConstructor(Type.EmptyTypes) != null); - public void OnToggleFeature(ToggleFeature feature, Match match) - { - var matchGroup = match.Groups[ValueGroup]; - if (matchGroup is not {Success: true}) - return; - - feature.Enabled = matchGroup.Value switch - { - "on" => true, - "off" => false, - _ => feature.Enabled - }; + foreach (var type in types) + yield return (ConsoleCommand) Activator.CreateInstance(type); } } diff --git a/Features/FeatureRenderer.cs b/Features/FeatureRenderer.cs index cf8e5bd5..5cb400b6 100644 --- a/Features/FeatureRenderer.cs +++ b/Features/FeatureRenderer.cs @@ -16,16 +16,35 @@ internal abstract class FeatureRenderer : ToggleFeature public abstract float X { get; set; } public abstract float Y { get; set; } - protected static string UserPath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Escape from Tarkov"); - private static string ConfigFile => Path.Combine(UserPath, "trainer.ini"); - - private static Lazy Features => new(() => [.. FeatureFactory.GetAllFeatures().OrderBy(f => f.Name)]); - protected static Lazy ToggleableFeatures => new(() => [.. FeatureFactory.GetAllToggleableFeatures().OrderByDescending(f => f.Name)]); + protected const float DefaultX = 40f; + protected const float DefaultY = 20f; private static GUIStyle LabelStyle => new() {wordWrap = false, normal = {textColor = Color.white}, margin = new RectOffset(8,0,8,0), fixedWidth = 150f, stretchWidth = false}; private static GUIStyle DescriptionStyle => new() {wordWrap = true, normal = {textColor = Color.white}, margin = new RectOffset(8,0,8,0), stretchWidth = true}; private static GUIStyle BoxStyle => new(GUI.skin.box) {normal = {background = Texture2D.whiteTexture, textColor = Color.white}}; + protected void SetupWindowCoordinates() + { + bool needfix = false; + X = FixCoordinate(X, Screen.width, DefaultX, ref needfix); + Y = FixCoordinate(Y, Screen.height, DefaultY, ref needfix); + + if (needfix) + SaveSettings(); + } + + private static float FixCoordinate(float coord, float maxValue, float defaultValue, ref bool needfix) + { + if (coord < 0 || coord >= maxValue) + { + coord = defaultValue; + needfix = true; + } + + return coord; + } + + internal abstract class SelectionContext { protected SelectionContext(IFeature feature, OrderedProperty orderedProperty, float parentX, float parentY, Func> builder) @@ -92,7 +111,8 @@ private void RenderFeatureWindow(int id) var tabs = fixedTabs .Concat ( - Features + Context + .Features .Value .Select(RenderFeatureText) ) @@ -119,7 +139,7 @@ private void RenderFeatureWindow(int id) RenderSummary(); break; default: - var feature = Features.Value[_selectedTabIndex - fixedTabs.Length]; + var feature = Context.Features.Value[_selectedTabIndex - fixedTabs.Length]; RenderFeature(feature); break; @@ -155,7 +175,7 @@ private void RenderSummary() protected static void SaveSettings() { - ConfigurationManager.Save(ConfigFile, Features.Value); + ConfigurationManager.Save(Context.ConfigFile, Context.Features.Value); } protected void LoadSettings(bool warnIfNotExists = true) @@ -163,7 +183,7 @@ protected void LoadSettings(bool warnIfNotExists = true) var cx = X; var cy = Y; - ConfigurationManager.Load(ConfigFile, Features.Value, warnIfNotExists); + ConfigurationManager.Load(Context.ConfigFile, Context.Features.Value, warnIfNotExists); _controlValues.Clear(); if (!Enabled) diff --git a/Installer/InstallCommand.cs b/Installer/InstallCommand.cs index 352e7e26..4c52da3c 100644 --- a/Installer/InstallCommand.cs +++ b/Installer/InstallCommand.cs @@ -30,8 +30,20 @@ internal class Settings : CommandSettings public string? Branch { get; set; } [Description("Disable feature.")] - [CommandOption("-d|--disable")] + [CommandOption("-f|--feature")] public string[]? DisabledFeatures { get; set; } + + [Description("Disable command.")] + [CommandOption("-c|--command")] + public string[]? DisabledCommands { get; set; } + } + + public static string[] ToSourceFile(string[]? names, string folder) + { + names ??= []; + return names + .Select(f => $"{folder}\\{f}.cs") + .ToArray(); } [SupportedOSPlatform("windows")] @@ -49,12 +61,12 @@ public override async Task ExecuteAsync(CommandContext commandContext, Sett AnsiConsole.MarkupLine($"Target [green]EscapeFromTarkov ({installation.Version})[/] in [blue]{installation.Location.EscapeMarkup()}[/]."); const string features = "Features"; - settings.DisabledFeatures ??= []; - settings.DisabledFeatures = settings.DisabledFeatures - .Select(f => $"{features}\\{f}.cs") - .ToArray(); + const string commands = "ConsoleCommands"; + + settings.DisabledFeatures = ToSourceFile(settings.DisabledFeatures, features); + settings.DisabledCommands = ToSourceFile(settings.DisabledCommands, commands); - var (compilation, archive) = await BuildTrainerAsync(settings, installation, features); + var (compilation, archive) = await BuildTrainerAsync(settings, installation, features, commands); if (compilation == null) { @@ -127,12 +139,12 @@ public override async Task ExecuteAsync(CommandContext commandContext, Sett return (int)ExitCode.Success; } - private static async Task<(CSharpCompilation?, ZipArchive?)> BuildTrainerAsync(Settings settings, Installation installation, string features) + private static async Task<(CSharpCompilation?, ZipArchive?)> BuildTrainerAsync(Settings settings, Installation installation, params string[] folders) { // Try first to compile against master var context = new CompilationContext(installation, "trainer", "NLog.EFT.Trainer.csproj") { - Exclude = settings.DisabledFeatures!, + Exclude = [.. settings.DisabledFeatures!, .. settings.DisabledCommands!], Branch = GetInitialBranch(settings) }; @@ -154,11 +166,12 @@ public override async Task ExecuteAsync(CommandContext commandContext, Sett } } - if (compilation == null && files.Length != 0 && files.All(f => f!.StartsWith(features))) + if (compilation == null && files.Length != 0 && files.All(file => folders.Any(folder => file!.StartsWith(folder)))) { // Failure, retry by removing faulting features if possible - AnsiConsole.MarkupLine($"[yellow]Trying to disable faulting feature(s): [red]{GetFaultingFeatures(files)}[/].[/]"); - context.Exclude = files.Concat(settings.DisabledFeatures!).ToArray()!; + AnsiConsole.MarkupLine($"[yellow]Trying to disable faulting feature/command: [red]{GetFaultingNames(files!)}[/].[/]"); + + context.Exclude = [.. files!, .. settings.DisabledFeatures!, .. settings.DisabledCommands!]; context.Branch = GetFallbackBranch(settings); (compilation, archive, errors) = await GetCompilationAsync(context); @@ -170,9 +183,13 @@ public override async Task ExecuteAsync(CommandContext commandContext, Sett return (compilation, archive); } - private static string GetFaultingFeatures(string?[] files) + private static string GetFaultingNames(string[] files) { - return string.Join(", ", files.Select(Path.GetFileNameWithoutExtension)); + return string.Join(", ", files + .Select(Path.GetFileNameWithoutExtension) + .Where(f => !f!.StartsWith("Base")) + .Distinct() + .OrderBy(f => f)); } private static string GetDefaultBranch() diff --git a/Installer/Installer.csproj b/Installer/Installer.csproj index 5b10c954..725676ea 100644 --- a/Installer/Installer.csproj +++ b/Installer/Installer.csproj @@ -17,7 +17,7 @@ Sebastien Lebreton https://github.com/sailro/EscapeFromTarkov-Trainer - 2.6.0.0 + 2.7.0.0 Sebastien Lebreton diff --git a/NLog.EFT.Trainer.csproj b/NLog.EFT.Trainer.csproj index d823cd2d..1dbd96b3 100644 --- a/NLog.EFT.Trainer.csproj +++ b/NLog.EFT.Trainer.csproj @@ -117,6 +117,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + +