├── Domain ├── .gitignore ├── ModFile │ ├── ModFileChanged.cs │ ├── PO_ModFile.cs │ ├── ModFile.cs │ ├── ModScanner.cs │ └── ModFileRepository.cs ├── Core │ ├── ImageFile.cs │ ├── WorkshopInfoModule │ │ ├── Tag.cs │ │ ├── PO_WorkshopInfo.cs │ │ ├── WorkshopInfo.cs │ │ └── WorkshopInfoRepository.cs │ ├── VpkId.cs │ ├── L4d2Folder.cs │ ├── ModBrief.cs │ ├── ModBriefModule │ │ ├── ModBriefSpecification.cs │ │ ├── ModBriefSpecificationBuilder.cs │ │ └── ModBriefList.cs │ └── ModStatusModule │ │ └── AddonListRepository.cs ├── ModSorter │ ├── ModSorter_Default.cs │ ├── IModSorter.cs │ ├── ModSorter_ByEnabled.cs │ ├── ModSorter_ByFile.cs │ ├── ModSorter_ByName.cs │ └── ModSorter_ByAuthor.cs ├── Settings │ ├── Setting.cs │ └── SettingFP.cs ├── Domain.csproj └── ModLocalInfo │ ├── Category.cs │ ├── PO_LocalInfo.cs │ ├── CategoryRuleGroup.cs │ ├── LocalInfo.cs │ ├── ModAnalysisServer.cs │ ├── CategoryRuleGroupFactory.cs │ ├── LocalInfoRepository.cs │ ├── BatchAnalysis.cs │ └── VpkFile.cs ├── SharpVPK ├── .gitignore ├── SharpVPK.csproj ├── IVpkArchiveHeader.cs ├── ArchivePart.cs ├── Exceptions │ └── ArchiveParsingException.cs ├── VpkDirectory.cs ├── V1 │ ├── VpkArchiveHeaderV1.cs │ └── VpkReaderV1.cs ├── V2 │ ├── VpkArchiveHeaderV2.cs │ └── VpkReaderV2.cs ├── LICENSE ├── VpkArchive.cs ├── VpkEntry.cs └── VpkReaderBase.cs ├── Infrastructure ├── .gitignore ├── Infrastructure.csproj ├── Entity.cs └── Utility │ ├── FileExplorerUtils.cs │ └── FPExtension.cs ├── L4d2_Mod_Manager_Tool ├── .gitignore ├── icon.ico ├── Program.cs ├── icon_large.ico ├── Resources │ ├── off.png │ ├── on.png │ ├── bgtask.png │ ├── no-image.png │ ├── ascending.png │ ├── descending.png │ ├── icon_large.jpg │ ├── steam_api.dll │ ├── steam_api64.dll │ ├── steamworkshop.cer │ └── categories.json ├── Service │ ├── X509CertificateStore.cs │ ├── AddonInfoDownload │ │ ├── IAddonInfoDownloadStrategy.cs │ │ ├── AddonInfoDownloadService.cs │ │ └── SteamworksAddonInfoDownloadStrategy.cs │ ├── L4d2Folder.cs │ ├── ModCrossServer.cs │ ├── ModCategoryService.cs │ └── ModFileService.cs ├── Domain │ ├── AddonList.cs │ ├── ModFilter │ │ ├── EmptyFilter.cs │ │ ├── PredicateModFilter.cs │ │ ├── OrFilter.cs │ │ ├── AndFilter.cs │ │ ├── ModBlurFilter.cs │ │ ├── IModFilter.cs │ │ └── ModFilterBuilder.cs │ ├── ModSorter │ │ ├── ModSorter_Default.cs │ │ ├── IModSorter.cs │ │ ├── ModSorter_ByEnabled.cs │ │ ├── ModSorter_ByFile.cs │ │ ├── ModSorter_ByName.cs │ │ └── ModSorter_ByAuthor.cs │ ├── WorkshopItem.cs │ ├── VpkSnippet.cs │ ├── ModDetail.cs │ ├── ModFile.cs │ ├── ModInfo.cs │ ├── ModWorkshopInfo.cs │ ├── ModPreviewInfo.cs │ ├── Specifications │ │ ├── ModDetailSpecificationSqliteExtend.cs │ │ └── ModDetailSpecification.cs │ ├── Repository │ │ ├── SQLiteDatabaseRepositoryBase.cs │ │ ├── WorkshopItemRepository.cs │ │ ├── AddonListRepository.cs │ │ └── ModDetailQuery.cs │ ├── ModTag.cs │ └── Mod.cs ├── TaskFramework │ ├── MessageTask.cs │ ├── BackgroundTask.cs │ └── BackgroundTaskList.cs ├── App │ ├── BackgroundTaskApplication.cs │ └── WorkshopInfoApplication.cs ├── DTO │ └── BackgroundTaskProgress.cs ├── Utility │ ├── WinformUtility.cs │ └── FPExtension.cs ├── Arthitecture │ ├── NoVtfConverter.cs │ └── RunProcess.cs ├── Module │ └── FileExplorer │ │ └── FileExplorerUtils.cs ├── L4d2_Mod_Manager_Tool.csproj.user ├── Widget │ ├── Widget_BackgroundTask.cs │ ├── MenuItemsZipper.cs │ ├── VOBackgroundTaskModel.cs │ ├── Widget_TagSelector.cs │ ├── ListViewColumnSorter.cs │ ├── Widget_TagSelector.Designer.cs │ ├── Widget_FilterMod.resx │ ├── Widget_ModOverview.resx │ ├── Widget_TagSelector.resx │ ├── Widget_BackgroundTask.resx │ ├── Widget_BackgroundTaskList.resx │ ├── Widget_ModOverview.cs │ ├── Widget_BackgroundTaskList.cs │ ├── Widget_BackgroundTask.Designer.cs │ └── Widget_BackgroundTaskList.Designer.cs ├── Form_Settings.cs ├── Properties │ └── Resources.Designer.cs ├── Dialog_AboutSoftware.cs ├── Form_Settings.resx ├── Form_RunningTask.resx ├── L4d2_Mod_Manager_Tool.csproj └── Form_RunningTask.cs ├── .gitignore ├── img ├── banner.png ├── yishi1_1.jpg ├── menu_option.jpg ├── modenabler.jpg ├── sort_header.jpg ├── steamtools.jpg ├── option_dialog.jpg ├── find_authoringtools.jpg └── modinfo_strategyStatus.jpg ├── CHANGELOG.md ├── L4d2_Mod_Manager.sln └── README.md /Domain/.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | /obj/ -------------------------------------------------------------------------------- /SharpVPK/.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | /obj/ -------------------------------------------------------------------------------- /Infrastructure/.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | /obj/ -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/.gitignore: -------------------------------------------------------------------------------- 1 | /bin/ 2 | /obj/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /\.vs/ 2 | /L4d2_Mod_Manager/bin/ 3 | /L4d2_Mod_Manager/obj/ 4 | /assets/ -------------------------------------------------------------------------------- /img/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-created-name/L4d2_Mod_Manager_Tool/HEAD/img/banner.png -------------------------------------------------------------------------------- /img/yishi1_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-created-name/L4d2_Mod_Manager_Tool/HEAD/img/yishi1_1.jpg -------------------------------------------------------------------------------- /img/menu_option.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-created-name/L4d2_Mod_Manager_Tool/HEAD/img/menu_option.jpg -------------------------------------------------------------------------------- /img/modenabler.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-created-name/L4d2_Mod_Manager_Tool/HEAD/img/modenabler.jpg -------------------------------------------------------------------------------- /img/sort_header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-created-name/L4d2_Mod_Manager_Tool/HEAD/img/sort_header.jpg -------------------------------------------------------------------------------- /img/steamtools.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-created-name/L4d2_Mod_Manager_Tool/HEAD/img/steamtools.jpg -------------------------------------------------------------------------------- /img/option_dialog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-created-name/L4d2_Mod_Manager_Tool/HEAD/img/option_dialog.jpg -------------------------------------------------------------------------------- /img/find_authoringtools.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-created-name/L4d2_Mod_Manager_Tool/HEAD/img/find_authoringtools.jpg -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-created-name/L4d2_Mod_Manager_Tool/HEAD/L4d2_Mod_Manager_Tool/icon.ico -------------------------------------------------------------------------------- /img/modinfo_strategyStatus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-created-name/L4d2_Mod_Manager_Tool/HEAD/img/modinfo_strategyStatus.jpg -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Program.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-created-name/L4d2_Mod_Manager_Tool/HEAD/L4d2_Mod_Manager_Tool/Program.cs -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/icon_large.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-created-name/L4d2_Mod_Manager_Tool/HEAD/L4d2_Mod_Manager_Tool/icon_large.ico -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Resources/off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-created-name/L4d2_Mod_Manager_Tool/HEAD/L4d2_Mod_Manager_Tool/Resources/off.png -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Resources/on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-created-name/L4d2_Mod_Manager_Tool/HEAD/L4d2_Mod_Manager_Tool/Resources/on.png -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Resources/bgtask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-created-name/L4d2_Mod_Manager_Tool/HEAD/L4d2_Mod_Manager_Tool/Resources/bgtask.png -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Resources/no-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-created-name/L4d2_Mod_Manager_Tool/HEAD/L4d2_Mod_Manager_Tool/Resources/no-image.png -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Resources/ascending.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-created-name/L4d2_Mod_Manager_Tool/HEAD/L4d2_Mod_Manager_Tool/Resources/ascending.png -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Resources/descending.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-created-name/L4d2_Mod_Manager_Tool/HEAD/L4d2_Mod_Manager_Tool/Resources/descending.png -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Resources/icon_large.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-created-name/L4d2_Mod_Manager_Tool/HEAD/L4d2_Mod_Manager_Tool/Resources/icon_large.jpg -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Resources/steam_api.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-created-name/L4d2_Mod_Manager_Tool/HEAD/L4d2_Mod_Manager_Tool/Resources/steam_api.dll -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Service/X509CertificateStore.cs: -------------------------------------------------------------------------------- 1 | namespace L4d2_Mod_Manager_Tool.Service 2 | { 3 | internal class X509CertificateStore 4 | { 5 | } 6 | } -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Resources/steam_api64.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-created-name/L4d2_Mod_Manager_Tool/HEAD/L4d2_Mod_Manager_Tool/Resources/steam_api64.dll -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Resources/steamworkshop.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/auto-created-name/L4d2_Mod_Manager_Tool/HEAD/L4d2_Mod_Manager_Tool/Resources/steamworkshop.cer -------------------------------------------------------------------------------- /SharpVPK/SharpVPK.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Domain/ModFile/ModFileChanged.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Domain.ModFile 8 | { 9 | public record ModFileChanged(ModFile[] Lost, ModFile[] New); 10 | } 11 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/AddonList.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace L4d2_Mod_Manager_Tool.Domain 8 | { 9 | record AddonList(string ModFile, bool Enable); 10 | } 11 | -------------------------------------------------------------------------------- /Domain/Core/ImageFile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Domain.Core 8 | { 9 | public record ImageFile(string File) 10 | { 11 | public static ImageFile MissingImage 12 | => new(""); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/TaskFramework/MessageTask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace L4d2_Mod_Manager_Tool.TaskFramework 8 | { 9 | public interface IMessageTask 10 | { 11 | string TaskName { get; } 12 | void DoTask(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/App/BackgroundTaskApplication.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace L4d2_Mod_Manager_Tool.App 8 | { 9 | /// 10 | /// 进行繁重的后台任务的应用程序 11 | /// 12 | internal class BackgroundTaskApplication 13 | { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/ModFilter/EmptyFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace L4d2_Mod_Manager_Tool.Domain.ModFilter 8 | { 9 | class EmptyFilter : IModFilter 10 | { 11 | public IEnumerable FilterMod(IEnumerable mods) => mods; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Domain/ModSorter/ModSorter_Default.cs: -------------------------------------------------------------------------------- 1 | using Domain.Core; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Domain.ModSorter 9 | { 10 | public class ModSorter_Default : IModSorter 11 | { 12 | public IEnumerable Sort(IEnumerable mods) => mods; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/ModSorter/ModSorter_Default.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace L4d2_Mod_Manager_Tool.Domain.ModSorter 8 | { 9 | class ModSorter_Default : IModSorter 10 | { 11 | public IEnumerable Sort(IEnumerable mods) => mods; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/WorkshopItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace L4d2_Mod_Manager_Tool.Domain 8 | { 9 | /// 10 | /// 创意工坊项,在steam创意工坊中下载的在线信息 11 | /// 12 | public record WorkshopItem(string Id, string Title, string Descript, string Preview); 13 | } 14 | -------------------------------------------------------------------------------- /Infrastructure/Infrastructure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Domain/Settings/Setting.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Domain.Settings 4 | { 5 | /// 6 | /// 软件设置信息 7 | /// 8 | public class Setting 9 | { 10 | /// 11 | /// 模组文件夹 12 | /// 13 | public string GamePath; 14 | 15 | /// 16 | /// no_vtf可执行程序 17 | /// 18 | public string NoVtfExecutablePath; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Domain/Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/DTO/BackgroundTaskProgress.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace L4d2_Mod_Manager_Tool.DTO 8 | { 9 | public class BackgroundTaskProgress 10 | { 11 | public int Id { get; set; } 12 | public string Name { get; set; } 13 | public int Progress { get; set; } 14 | public string Status {get;set;} 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SharpVPK/IVpkArchiveHeader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.InteropServices; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace SharpVPK 9 | { 10 | interface IVpkArchiveHeader 11 | { 12 | uint Signature { get; set; } 13 | uint Version { get; set; } 14 | uint TreeLength { get; set; } 15 | 16 | bool Verify(); 17 | uint CalculateDataOffset(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Infrastructure/Entity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Infrastructure 8 | { 9 | /// 10 | /// 实体基类 11 | /// 12 | /// 标识类型 13 | public abstract class Entity 14 | { 15 | public T Id { get; private set; } 16 | public Entity(T id) 17 | { 18 | Id = id; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Domain/ModFile/PO_ModFile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Dapper.Contrib.Extensions; 7 | 8 | namespace Domain.ModFile 9 | { 10 | [Table("mod_file")] 11 | class PO_ModFile 12 | { 13 | [Key] 14 | public int id { get; set; } 15 | public long vpk_id { get; set; } 16 | public string file_name { get; set; } 17 | public int localinfo_id { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Domain/Core/WorkshopInfoModule/Tag.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Domain.Core.WorkshopInfoModule 8 | { 9 | public record Tag(string Name) 10 | { 11 | public static Tag[] ParseGroup(string str) 12 | => str.Split(',').Select(s => new Tag(s)).ToArray(); 13 | 14 | public static string Concat(Tag[] tags) 15 | => string.Join(',', tags.Select(t => t.Name)); 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/VpkSnippet.cs: -------------------------------------------------------------------------------- 1 | using L4d2_Mod_Manager_Tool.Utility; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using System.Collections.Immutable; 6 | 7 | namespace L4d2_Mod_Manager_Tool.Domain 8 | { 9 | /// 10 | /// vpk文件简略信息,可能包含图片、说明 11 | /// 12 | public record VpkSnippet( 13 | string VpkName, 14 | Maybe AddonImage, 15 | Maybe AddonInfo, 16 | ImmutableArray Categories 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /Domain/ModLocalInfo/Category.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Domain.ModLocalInfo { 8 | public record Category(string Name) 9 | { 10 | public static Category[] ParseGroup(string str) 11 | => str.Split(',').Select(str => new Category(str)).ToArray(); 12 | 13 | public static string Concat(Category[] cats) 14 | => string.Join(',', cats.Select(cat => cat.Name)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /SharpVPK/ArchivePart.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SharpVPK 8 | { 9 | internal class ArchivePart 10 | { 11 | public uint Size { get; set; } 12 | public int Index { get; set; } 13 | public string Filename { get; set; } 14 | 15 | public ArchivePart(uint size, int index, string filename) 16 | { 17 | Size = size; 18 | Index = index; 19 | Filename = filename; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Service/AddonInfoDownload/IAddonInfoDownloadStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Infrastructure.Utility; 7 | using L4d2_Mod_Manager_Tool.Domain; 8 | 9 | namespace L4d2_Mod_Manager_Tool.Service.AddonInfoDownload 10 | { 11 | /// 12 | /// 模组信息下载策略 13 | /// 14 | interface IAddonInfoDownloadStrategy 15 | { 16 | Task> DownloadAddonInfoAsync(ulong vpkid); 17 | string StrategyName { get; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/ModDetail.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace L4d2_Mod_Manager_Tool.Domain 9 | { 10 | public record ModDetail( 11 | int Id, 12 | string Name, 13 | string FileName, 14 | bool Enabled, 15 | string Author, 16 | string Tagline 17 | ) 18 | { 19 | public static ModDetail Default 20 | => new(0, string.Empty, string.Empty, false, string.Empty, string.Empty); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/ModFile.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace L4d2_Mod_Manager_Tool.Domain 4 | { 5 | public record ModFile(int Id, string FilePath, bool Actived); 6 | 7 | public static class ModFileFP 8 | { 9 | /// 10 | /// 创建一个新的模组文件 11 | /// 12 | public static ModFile CreateModFile(string file) 13 | { 14 | return new ModFile(-1, file, true); 15 | } 16 | 17 | public static string ModFileName(ModFile mf) 18 | { 19 | return Path.GetFileNameWithoutExtension(mf.FilePath); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/ModInfo.cs: -------------------------------------------------------------------------------- 1 | using L4d2_Mod_Manager_Tool.Utility; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Collections.Immutable; 5 | using System.Drawing; 6 | using System.Linq; 7 | using System.Text; 8 | 9 | namespace L4d2_Mod_Manager_Tool.Domain 10 | { 11 | /// 12 | /// Entity - 模组信息 13 | /// 14 | public record ModInfo( 15 | Maybe Title, 16 | Maybe Version, 17 | Maybe Tagline, 18 | Maybe Author, 19 | Maybe Description, 20 | ImmutableArray Categories 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Utility/WinformUtility.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Windows.Forms; 3 | 4 | namespace L4d2_Mod_Manager_Tool.Utility 5 | { 6 | public static class WinformUtility 7 | { 8 | 9 | public static void ErrorMessageBox(string content, string caption) 10 | { 11 | MessageBox.Show(content, caption, MessageBoxButtons.OK, MessageBoxIcon.Error); 12 | } 13 | 14 | public static string SoftwareVersion => 15 | $"{CurrentAssembly.GetName().Version.ToString(3)}.a1"; 16 | 17 | private static Assembly CurrentAssembly => Assembly.GetExecutingAssembly(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Domain/ModSorter/IModSorter.cs: -------------------------------------------------------------------------------- 1 | using Domain.Core; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Domain.ModSorter 9 | { 10 | /// 11 | /// 模组排序顺序 12 | /// 13 | public enum ModSortOrder { 14 | /// 15 | /// 升序 16 | /// 17 | Ascending, 18 | /// 19 | /// 降序 20 | /// 21 | Descending 22 | } 23 | public interface IModSorter 24 | { 25 | IEnumerable Sort(IEnumerable mods); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Domain/Core/WorkshopInfoModule/PO_WorkshopInfo.cs: -------------------------------------------------------------------------------- 1 | using Dapper.Contrib.Extensions; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Domain.Core.WorkshopInfoModule 9 | { 10 | [Table("workshopinfo")] 11 | internal class PO_WorkshopInfo 12 | { 13 | [Key] 14 | public long vpk_id { get; set; } 15 | public string preview { get; set; } 16 | public string author { get; set; } 17 | public string title { get; set; } 18 | public string description { get; set; } 19 | public string tags { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /SharpVPK/Exceptions/ArchiveParsingException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace SharpVPK.Exceptions 8 | { 9 | public class ArchiveParsingException : Exception 10 | { 11 | public ArchiveParsingException() 12 | { 13 | } 14 | 15 | public ArchiveParsingException(string message) 16 | : base(message) 17 | { 18 | } 19 | 20 | public ArchiveParsingException(string message, Exception innerException) 21 | : base(message, innerException) 22 | { 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/ModSorter/IModSorter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace L4d2_Mod_Manager_Tool.Domain.ModSorter 8 | { 9 | /// 10 | /// 模组排序顺序 11 | /// 12 | public enum ModSortOrder { 13 | /// 14 | /// 升序 15 | /// 16 | Ascending, 17 | /// 18 | /// 降序 19 | /// 20 | Descending 21 | } 22 | interface IModSorter 23 | { 24 | IEnumerable Sort(IEnumerable mods); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SharpVPK/VpkDirectory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace SharpVPK 9 | { 10 | public class VpkDirectory 11 | { 12 | public List Entries { get; set; } 13 | public string Path { get; set; } 14 | internal VpkArchive ParentArchive { get; set; } 15 | 16 | internal VpkDirectory(VpkArchive parentArchive, string path, List entries) 17 | { 18 | ParentArchive = parentArchive; 19 | Path = path; 20 | Entries = entries; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/ModFilter/PredicateModFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace L4d2_Mod_Manager_Tool.Domain.ModFilter 8 | { 9 | class PredicateModFilter : IModFilter 10 | { 11 | private Func predicate; 12 | public PredicateModFilter(Func predicate) 13 | { 14 | this.predicate = predicate ?? throw new Exception("谓词不能为空"); 15 | } 16 | public IEnumerable FilterMod(IEnumerable mods) 17 | { 18 | return mods.Where(predicate); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/ModFilter/OrFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace L4d2_Mod_Manager_Tool.Domain.ModFilter 8 | { 9 | class OrFilter : IModFilter 10 | { 11 | private IModFilter filter1, filter2; 12 | public OrFilter(IModFilter filter1, IModFilter filter2) 13 | { 14 | this.filter1 = filter1; 15 | this.filter2 = filter2; 16 | } 17 | 18 | public IEnumerable FilterMod(IEnumerable mods) 19 | { 20 | return filter1.FilterMod(mods).Union(filter2.FilterMod(mods)); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Domain/ModLocalInfo/PO_LocalInfo.cs: -------------------------------------------------------------------------------- 1 | using Dapper.Contrib.Extensions; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Domain.ModLocalInfo 9 | { 10 | [Table("mod")] 11 | internal class PO_LocalInfo 12 | { 13 | [Key] 14 | public int id { get;set; } 15 | public string thumbnail { get; set; } 16 | public string title { get; set; } 17 | public string version { get; set; } 18 | public string tagline { get; set; } 19 | public string author { get; set; } 20 | public string description { get; set; } 21 | public string categories { get; set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/ModFilter/AndFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace L4d2_Mod_Manager_Tool.Domain.ModFilter 8 | { 9 | class AndFilter : IModFilter 10 | { 11 | private IModFilter filter1, filter2; 12 | public AndFilter(IModFilter filter1, IModFilter filter2) 13 | { 14 | this.filter1 = filter1; 15 | this.filter2 = filter2; 16 | } 17 | public IEnumerable FilterMod(IEnumerable mods) 18 | { 19 | return filter1.FilterMod(mods).Intersect(filter2.FilterMod(mods), new ModEqualityComparer()); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Arthitecture/NoVtfConverter.cs: -------------------------------------------------------------------------------- 1 | using Domain.Settings; 2 | using System.IO; 3 | 4 | namespace L4d2_Mod_Manager_Tool.Arthitecture 5 | { 6 | /// 7 | /// 基础设施 - Vtf转换器,将Vtf转为jpg 8 | /// 9 | public class NoVtfConverter 10 | { 11 | private static string NoVtfExecutablePath => SettingFP.GetSetting().NoVtfExecutablePath; 12 | 13 | /// 14 | /// 将Vtf文件转换为普通图片文件 15 | /// 16 | public static string ConverVtf(string file) 17 | { 18 | string res = RunProcess.Run(NoVtfExecutablePath, $"-l png --compress \"{Path.GetDirectoryName(file)}\""); 19 | return Path.ChangeExtension(file, ".png"); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Domain/Core/VpkId.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Text.RegularExpressions; 7 | using System.Threading.Tasks; 8 | 9 | namespace Domain.Core 10 | { 11 | public record VpkId(long Id) 12 | { 13 | public static VpkId Undefined => new(0); 14 | 15 | /// 16 | /// 尝试从文件名中解析vpkid 17 | /// 18 | public static VpkId TryParse(string file) 19 | { 20 | var fn = Path.GetFileNameWithoutExtension(file); 21 | return IsVpkId(fn) ? new VpkId(long.Parse(fn)) : Undefined; 22 | } 23 | 24 | public static bool IsVpkId(string str) 25 | => Regex.IsMatch(str, @"^[1-9]?[0-9]{9}$"); 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/ModWorkshopInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace L4d2_Mod_Manager_Tool.Domain 8 | { 9 | /// 10 | /// 模组的创意工坊信息 11 | /// 12 | public record ModWorkshopInfo( 13 | string Author, 14 | string Title, 15 | string Descript, 16 | string PreviewImage, 17 | ImmutableArray Tags 18 | ) 19 | { 20 | public bool IsEmpty 21 | => string.IsNullOrEmpty(Author) 22 | && string.IsNullOrEmpty(Title) 23 | && string.IsNullOrEmpty(Descript) 24 | && string.IsNullOrEmpty(PreviewImage) 25 | && Tags.IsEmpty; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Domain/ModSorter/ModSorter_ByEnabled.cs: -------------------------------------------------------------------------------- 1 | using Domain.Core; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Domain.ModSorter 9 | { 10 | public class ModSorter_ByEnabled : IModSorter 11 | { 12 | private ModSortOrder order; 13 | public ModSorter_ByEnabled(ModSortOrder order) 14 | { 15 | this.order = order; 16 | } 17 | public IEnumerable Sort(IEnumerable mods) 18 | { 19 | return order switch 20 | { 21 | ModSortOrder.Ascending => mods.OrderBy(m => m.Enabled), 22 | ModSortOrder.Descending => mods.OrderByDescending(m => m.Enabled) 23 | }; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Domain/ModSorter/ModSorter_ByFile.cs: -------------------------------------------------------------------------------- 1 | using Domain.Core; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Domain.ModSorter 9 | { 10 | public class ModSorter_ByFile : IModSorter 11 | { 12 | private ModSortOrder order; 13 | public ModSorter_ByFile(ModSortOrder order) 14 | { 15 | this.order = order; 16 | } 17 | 18 | public IEnumerable Sort(IEnumerable mods) 19 | { 20 | return order switch 21 | { 22 | ModSortOrder.Ascending => mods.OrderBy(m => m.FileName), 23 | ModSortOrder.Descending => mods.OrderByDescending(m => m.FileName) 24 | }; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/ModSorter/ModSorter_ByEnabled.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace L4d2_Mod_Manager_Tool.Domain.ModSorter 8 | { 9 | class ModSorter_ByEnabled : IModSorter 10 | { 11 | private ModSortOrder order; 12 | public ModSorter_ByEnabled(ModSortOrder order) 13 | { 14 | this.order = order; 15 | } 16 | public IEnumerable Sort(IEnumerable mods) 17 | { 18 | return order switch 19 | { 20 | ModSortOrder.Ascending => mods.OrderBy(m => m.Enabled), 21 | ModSortOrder.Descending => mods.OrderByDescending(m => m.Enabled) 22 | }; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/ModSorter/ModSorter_ByFile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace L4d2_Mod_Manager_Tool.Domain.ModSorter 8 | { 9 | class ModSorter_ByFile : IModSorter 10 | { 11 | private ModSortOrder order; 12 | public ModSorter_ByFile(ModSortOrder order) 13 | { 14 | this.order = order; 15 | } 16 | 17 | public IEnumerable Sort(IEnumerable mods) 18 | { 19 | return order switch 20 | { 21 | ModSortOrder.Ascending => mods.OrderBy(m => m.FileName), 22 | ModSortOrder.Descending => mods.OrderByDescending(m => m.FileName) 23 | }; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/ModFilter/ModBlurFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace L4d2_Mod_Manager_Tool.Domain.ModFilter 8 | { 9 | class ModBlurFilter : IModFilter 10 | { 11 | private string name; 12 | public ModBlurFilter(string name) 13 | { 14 | this.name = name; 15 | } 16 | public IEnumerable FilterMod(IEnumerable mods) 17 | { 18 | var filter = 19 | new PredicateModFilter(ModFP.NameContains(name)) 20 | .Or(new PredicateModFilter(ModFP.AuthorContains(name))) 21 | .Or(new PredicateModFilter(ModFP.VPKIdContains(name))); 22 | return filter.FilterMod(mods); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SharpVPK/V1/VpkArchiveHeaderV1.cs: -------------------------------------------------------------------------------- 1 | namespace SharpVPK.V1 2 | { 3 | public struct VpkArchiveHeaderV1 : IVpkArchiveHeader 4 | { 5 | private const int StaticSignature = 0x55aa1234; 6 | 7 | public uint Signature { get; set; } 8 | public uint Version { get; set; } 9 | public uint TreeLength { get; set; } 10 | 11 | public VpkArchiveHeaderV1(uint signature, uint version, uint treeLength) 12 | : this() 13 | { 14 | Signature = signature; 15 | Version = version; 16 | TreeLength = treeLength; 17 | } 18 | 19 | public bool Verify() 20 | { 21 | return Signature == StaticSignature && Version == 1; 22 | } 23 | 24 | public uint CalculateDataOffset() 25 | { 26 | return 12 + TreeLength; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /SharpVPK/V1/VpkReaderV1.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace SharpVPK.V1 5 | { 6 | internal class VpkReaderV1 : VpkReaderBase 7 | { 8 | public VpkReaderV1(string filename) 9 | : base(filename) 10 | { 11 | } 12 | 13 | public override IVpkArchiveHeader ReadArchiveHeader() 14 | { 15 | var hdrStructSize = Marshal.SizeOf(typeof(VpkArchiveHeaderV1)); 16 | if (hdrStructSize != 12) 17 | throw new InvalidDataException(); 18 | var hdrBuff = Reader.ReadBytes(hdrStructSize); 19 | return BytesToStructure(hdrBuff); 20 | } 21 | 22 | public override uint CalculateEntryOffset(uint offset) 23 | { 24 | throw new System.NotImplementedException(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Domain/ModFile/ModFile.cs: -------------------------------------------------------------------------------- 1 | using Domain.Core; 2 | using Infrastructure.Utility; 3 | 4 | namespace Domain.ModFile 5 | { 6 | /// 7 | /// 模组文件实体 8 | /// 9 | public record ModFile( 10 | int Id, 11 | VpkId VpkId, 12 | string FileLoc, 13 | int LocalinfoId 14 | ){ 15 | public static ModFile CreateFromFile(string file) 16 | => new(0, VpkId.TryParse(file), file, 0); 17 | 18 | public void ShowFileInExplorer() 19 | { 20 | var file = L4d2Folder.GetAddonFileFullPath(FileLoc); 21 | FileExplorerUtils.OpenFileExplorerAndSelectItem(file); 22 | } 23 | 24 | public void OpenModFile() 25 | { 26 | var file = L4d2Folder.GetAddonFileFullPath(FileLoc); 27 | FileExplorerUtils.OpenFileInExplorer(file); 28 | } 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /Domain/ModSorter/ModSorter_ByName.cs: -------------------------------------------------------------------------------- 1 | using Domain.Core; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Domain.ModSorter 9 | { 10 | public class ModSorter_ByName : IModSorter 11 | { 12 | private ModSortOrder order; 13 | public ModSorter_ByName(ModSortOrder order) 14 | { 15 | this.order = order; 16 | } 17 | public IEnumerable Sort(IEnumerable mods) 18 | { 19 | var ord = mods.OrderBy(m => m.Name, StringComparer.CurrentCultureIgnoreCase); 20 | return order switch 21 | { 22 | ModSortOrder.Ascending => ord, 23 | ModSortOrder.Descending => ord.Reverse(), 24 | _ => ord 25 | }; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Domain/ModSorter/ModSorter_ByAuthor.cs: -------------------------------------------------------------------------------- 1 | using Domain.Core; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Domain.ModSorter 9 | { 10 | public class ModSorter_ByAuthor : IModSorter 11 | { 12 | private ModSortOrder order; 13 | public ModSorter_ByAuthor(ModSortOrder order) 14 | { 15 | this.order = order; 16 | } 17 | public IEnumerable Sort(IEnumerable mods) 18 | { 19 | var ord = mods.OrderBy(m => m.Author, StringComparer.CurrentCultureIgnoreCase); 20 | return order switch 21 | { 22 | ModSortOrder.Ascending => ord, 23 | ModSortOrder.Descending => ord.Reverse(), 24 | _ => ord 25 | }; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/ModSorter/ModSorter_ByName.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace L4d2_Mod_Manager_Tool.Domain.ModSorter 8 | { 9 | class ModSorter_ByName : IModSorter 10 | { 11 | private ModSortOrder order; 12 | public ModSorter_ByName(ModSortOrder order) 13 | { 14 | this.order = order; 15 | } 16 | public IEnumerable Sort(IEnumerable mods) 17 | { 18 | var ord = mods.OrderBy(m => m.Name, StringComparer.CurrentCultureIgnoreCase); 19 | return order switch 20 | { 21 | ModSortOrder.Ascending => ord, 22 | ModSortOrder.Descending => ord.Reverse(), 23 | _ => ord 24 | }; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/ModSorter/ModSorter_ByAuthor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace L4d2_Mod_Manager_Tool.Domain.ModSorter 8 | { 9 | class ModSorter_ByAuthor : IModSorter 10 | { 11 | private ModSortOrder order; 12 | public ModSorter_ByAuthor(ModSortOrder order) 13 | { 14 | this.order = order; 15 | } 16 | public IEnumerable Sort(IEnumerable mods) 17 | { 18 | var ord = mods.OrderBy(m => m.Author, StringComparer.CurrentCultureIgnoreCase); 19 | return order switch 20 | { 21 | ModSortOrder.Ascending => ord, 22 | ModSortOrder.Descending => ord.Reverse(), 23 | _ => ord 24 | }; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Domain/ModLocalInfo/CategoryRuleGroup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Text.RegularExpressions; 6 | using System.Threading.Tasks; 7 | 8 | namespace Domain.ModLocalInfo 9 | { 10 | record CategoryRule(string Category, string Regular, string Path); 11 | internal class CategoryRuleGroup 12 | { 13 | private List categoryRules = new(); 14 | public CategoryRuleGroup(List rules) 15 | { 16 | categoryRules = rules; 17 | } 18 | 19 | public string[] Pathes => categoryRules.Select(x => x.Path).ToArray(); 20 | 21 | public string[] MatchCategories(string entry) 22 | { 23 | return categoryRules 24 | .Where(r => Regex.IsMatch(entry, r.Regular)) 25 | .Select(r => r.Category).ToArray(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/ModFilter/IModFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace L4d2_Mod_Manager_Tool.Domain.ModFilter 8 | { 9 | /// 10 | /// 模组过滤器接口 11 | /// 12 | public interface IModFilter 13 | { 14 | /// 15 | /// 过滤模组 16 | /// 17 | IEnumerable FilterMod(IEnumerable mods); 18 | } 19 | 20 | public static class ModFilterFP 21 | { 22 | public static IModFilter And(this IModFilter filter, IModFilter otherFilter) 23 | { 24 | return new AndFilter(filter, otherFilter); 25 | } 26 | 27 | public static IModFilter Or(this IModFilter filter, IModFilter otherFilter) 28 | { 29 | return new OrFilter(filter, otherFilter); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/ModPreviewInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace L4d2_Mod_Manager_Tool.Domain 8 | { 9 | /// 10 | /// 模组预览信息 11 | /// 12 | public struct ModPreviewInfo 13 | { 14 | public string PreviewImg; 15 | public string Name; 16 | public string Author; 17 | public string Descript; 18 | public string Categories; 19 | public string Tags; 20 | 21 | public static ModPreviewInfo Default 22 | => new() 23 | { 24 | PreviewImg = string.Empty, 25 | Name = string.Empty, 26 | Author = string.Empty, 27 | Descript = string.Empty, 28 | Categories = string.Empty, 29 | Tags = string.Empty, 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /SharpVPK/V2/VpkArchiveHeaderV2.cs: -------------------------------------------------------------------------------- 1 | namespace SharpVPK.V2 2 | { 3 | public struct VpkArchiveHeaderV2 : IVpkArchiveHeader 4 | { 5 | private const int StaticSignature = 0x55aa1234; 6 | 7 | public uint Signature { get; set; } 8 | public uint Version { get; set; } 9 | public uint TreeLength { get; set; } 10 | public uint FooterLength { get; set; } 11 | 12 | public VpkArchiveHeaderV2(uint signature, uint version, uint treeLength, uint footerLength) 13 | : this() 14 | { 15 | Signature = signature; 16 | Version = version; 17 | TreeLength = treeLength; 18 | FooterLength = footerLength; 19 | } 20 | 21 | public bool Verify() 22 | { 23 | return Signature == StaticSignature && Version == 1; 24 | } 25 | 26 | public uint CalculateDataOffset() 27 | { 28 | return 12 + TreeLength; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /SharpVPK/V2/VpkReaderV2.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | using SharpVPK.V1; 3 | 4 | namespace SharpVPK.V2 5 | { 6 | internal class VpkReaderV2 : VpkReaderBase 7 | { 8 | public VpkReaderV2(string filename) 9 | : base(filename) 10 | { 11 | } 12 | 13 | public override IVpkArchiveHeader ReadArchiveHeader() 14 | { 15 | var hdrStructSize = Marshal.SizeOf(typeof(VpkArchiveHeaderV2)); 16 | var hdrBuff = Reader.ReadBytes(hdrStructSize); 17 | // skip unknown values 18 | Reader.ReadInt32(); 19 | var hdr = BytesToStructure(hdrBuff); 20 | hdr.FooterLength = Reader.ReadUInt32(); 21 | Reader.ReadInt32(); 22 | Reader.ReadInt32(); 23 | return hdr; 24 | } 25 | 26 | public override uint CalculateEntryOffset(uint offset) 27 | { 28 | throw new System.NotImplementedException(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Infrastructure/Utility/FileExplorerUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Diagnostics; 7 | 8 | namespace Infrastructure.Utility 9 | { 10 | public static class FileExplorerUtils 11 | { 12 | /// 13 | /// 打开资源管理器,选中制定项 14 | /// 15 | /// 16 | public static void OpenFileExplorerAndSelectItem(string file) 17 | { 18 | ProcessStartInfo psi = new ProcessStartInfo("Explorer.exe"); 19 | psi.Arguments = $"/select,{file}"; 20 | Process.Start(psi); 21 | } 22 | 23 | /// 24 | /// 使用资源管理器打开文件 25 | /// 26 | public static void OpenFileInExplorer(string file) 27 | { 28 | ProcessStartInfo psi = new ProcessStartInfo("Explorer.exe"); 29 | psi.Arguments = file; 30 | Process.Start(psi); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Domain/Core/WorkshopInfoModule/WorkshopInfo.cs: -------------------------------------------------------------------------------- 1 | using Infrastructure; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Domain.Core.WorkshopInfoModule 9 | { 10 | /// 11 | /// 创意工坊信息 12 | /// 13 | 14 | public class WorkshopInfo : Entity 15 | { 16 | public WorkshopInfo(VpkId id) : base(id) { } 17 | 18 | public ImageFile Preview { get; set; } 19 | public string Title { get; set; } 20 | public string Autor { get; set; } 21 | public string Description { get; set; } 22 | public Tag[] Tags { get; set; } 23 | 24 | public static WorkshopInfo CreateEmpty(VpkId id) 25 | { 26 | return new WorkshopInfo(id) 27 | { 28 | Preview = ImageFile.MissingImage, 29 | Title = string.Empty, 30 | Description = string.Empty, 31 | Tags = Array.Empty() 32 | }; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Module/FileExplorer/FileExplorerUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Diagnostics; 7 | 8 | namespace L4d2_Mod_Manager_Tool.Module.FileExplorer 9 | { 10 | public static class FileExplorerUtils 11 | { 12 | /// 13 | /// 打开资源管理器,选中制定项 14 | /// 15 | /// 16 | public static void OpenFileExplorerAndSelectItem(string file) 17 | { 18 | ProcessStartInfo psi = new ProcessStartInfo("Explorer.exe"); 19 | psi.Arguments = $"/select,{file}"; 20 | Process.Start(psi); 21 | } 22 | 23 | /// 24 | /// 使用资源管理器打开文件 25 | /// 26 | public static void OpenFileInExplorer(string file) 27 | { 28 | ProcessStartInfo psi = new ProcessStartInfo("Explorer.exe"); 29 | psi.Arguments = file; 30 | Process.Start(psi); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Domain/Settings/SettingFP.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.IO; 3 | 4 | namespace Domain.Settings 5 | { 6 | public static class SettingFP 7 | { 8 | public const string SettingFile = "setting.json"; 9 | private static Setting setting = null; 10 | public static Setting GetSetting() 11 | { 12 | if (setting == null) 13 | { 14 | if (File.Exists(SettingFile)) 15 | setting = JsonConvert.DeserializeObject(File.ReadAllText(SettingFile)); 16 | else 17 | setting = new() 18 | { 19 | GamePath = "", 20 | NoVtfExecutablePath = "no_vtf-windows_x64\\no_vtf.exe" 21 | }; 22 | } 23 | return setting; 24 | } 25 | 26 | public static void SaveSetting(Setting setting) 27 | { 28 | var content = JsonConvert.SerializeObject(setting); 29 | File.WriteAllText(SettingFile, content); 30 | SettingFP.setting = setting; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /SharpVPK/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 UbbeLoL 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/Specifications/ModDetailSpecificationSqliteExtend.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace L4d2_Mod_Manager_Tool.Domain.Specifications 8 | { 9 | public static class ModDetailSpecificationSqliteExtend 10 | { 11 | public static string ToSqlite(this ModDetailSpecification spec) 12 | { 13 | return spec switch 14 | { 15 | MS_Empty => "", 16 | MS_Category => $"categories like '%{(spec as MS_Category).Category}%'", 17 | MS_Tag => $"workshop_tags like '%{(spec as MS_Tag).Tag}%'", 18 | MS_Blur blur => string.Format("(title like '%{0}%' OR workshop_title like '%{0}%' OR author like '%{0}%' OR vpk_id like '{0}%')", blur.Blur), 19 | MS_And and => and.Left.ToSqlite() + " AND " + and.Right.ToSqlite(), 20 | MS_Or or => $"({or.Left.ToSqlite()} OR {or.Right.ToSqlite()})", 21 | _ => throw new InvalidOperationException() 22 | }; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/TaskFramework/BackgroundTask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using L4d2_Mod_Manager_Tool.Widget; 7 | 8 | namespace L4d2_Mod_Manager_Tool.TaskFramework 9 | { 10 | /// 11 | /// 描述一个正在进行的后台任务 12 | /// 13 | public class BackgroundTask : IDisposable 14 | { 15 | public event EventHandler OnProgressChanged; 16 | public event EventHandler OnFinished; 17 | public event EventHandler OnCanceling; 18 | 19 | public int Id { get; private set; } 20 | public string Name { get; set; } 21 | public string Status { get; set; } 22 | public int Progress { get; set; } 23 | 24 | public BackgroundTask(int id) 25 | => Id = id; 26 | 27 | public void Cancel() 28 | => OnCanceling?.Invoke(this, null); 29 | 30 | public void UpdateProgress() 31 | => OnProgressChanged?.Invoke(this, null); 32 | 33 | public void Dispose() 34 | { 35 | OnFinished?.Invoke(this, null); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Arthitecture/RunProcess.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace L4d2_Mod_Manager_Tool.Arthitecture 9 | { 10 | public static class RunProcess 11 | { 12 | public static string Run(string exePath, string workDir, string args) 13 | { 14 | Process p = new Process(); 15 | ProcessStartInfo si = new ProcessStartInfo(); 16 | si.CreateNoWindow = true; 17 | si.FileName = exePath; 18 | si.RedirectStandardError = true; 19 | si.RedirectStandardInput = true; 20 | si.RedirectStandardOutput = true; 21 | si.WorkingDirectory = workDir; 22 | string arg = args; 23 | si.Arguments = arg; 24 | 25 | p.StartInfo = si; 26 | p.Start(); 27 | 28 | return p.StandardOutput.ReadToEnd(); 29 | } 30 | 31 | public static string Run(string exePath, string args) 32 | { 33 | return Run(exePath, Environment.CurrentDirectory, args); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/L4d2_Mod_Manager_Tool.csproj.user: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Form 6 | 7 | 8 | Form 9 | 10 | 11 | Form 12 | 13 | 14 | Form 15 | 16 | 17 | UserControl 18 | 19 | 20 | UserControl 21 | 22 | 23 | UserControl 24 | 25 | 26 | UserControl 27 | 28 | 29 | UserControl 30 | 31 | 32 | -------------------------------------------------------------------------------- /Domain/ModLocalInfo/LocalInfo.cs: -------------------------------------------------------------------------------- 1 | using Domain.Core; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Domain.ModLocalInfo 9 | { 10 | public class LocalInfo 11 | { 12 | public int Id { get; private set; } 13 | public ImageFile AddonImage { get; set; } 14 | public string Title { get; set; } 15 | public string Version { get; set; } 16 | public string Tagline { get; set; } 17 | public string Author { get; set; } 18 | public string Description { get; set; } 19 | public Category[] Categories { get; set; } 20 | 21 | public LocalInfo(int id) 22 | { 23 | Id = id; 24 | } 25 | 26 | public LocalInfo WithId(int id) 27 | { 28 | var clone = (LocalInfo)MemberwiseClone(); 29 | clone.Id = id; 30 | return clone; 31 | } 32 | 33 | public LocalInfo() 34 | { 35 | Title = string.Empty; 36 | Version = string.Empty; 37 | Tagline = string.Empty; 38 | Author = string.Empty; 39 | Description = string.Empty; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Service/L4d2Folder.cs: -------------------------------------------------------------------------------- 1 | using Domain.Settings; 2 | using System.IO; 3 | 4 | namespace L4d2_Mod_Manager_Tool.Service 5 | { 6 | /// 7 | /// 求生之路2常用文件夹 8 | /// 9 | static class L4d2Folder 10 | { 11 | private const string CoreFolderName = "left4dead2"; 12 | private const string AddonFolderName = "addons"; 13 | private const string WorkshopAddonFolderName = "workshop"; 14 | /// 15 | /// 游戏核心文件夹 16 | /// 17 | public static string CoreFolder => 18 | Path.Combine(SettingFP.GetSetting().GamePath, CoreFolderName); 19 | /// 20 | /// 离线模组文件夹 21 | /// 22 | public static string AddonsFolder => 23 | Path.Combine(CoreFolder, AddonFolderName); 24 | /// 25 | /// 创意工坊模组文件夹 26 | /// 27 | public static string WorkshopAddonsFolder => 28 | Path.Combine(AddonsFolder, WorkshopAddonFolderName); 29 | 30 | /// 31 | /// 获取模组文件完整路径 32 | /// 33 | public static string GetAddonFileFullPath(string filePath) 34 | => Path.Combine(AddonsFolder, filePath); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/Repository/SQLiteDatabaseRepositoryBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.SQLite; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace L4d2_Mod_Manager_Tool.Domain.Repository 9 | { 10 | public abstract class SQLiteDatabaseRepositoryBase : IDisposable 11 | { 12 | private SQLiteConnection connection; 13 | public SQLiteDatabaseRepositoryBase(string file) 14 | { 15 | connection = new SQLiteConnection($"data source={file}"); 16 | connection.Open(); 17 | CreateDatabase(); 18 | } 19 | 20 | protected abstract void CreateDatabase(); 21 | 22 | protected int ExecuteNonQuery(string cmd) 23 | => CreateCommand(cmd).ExecuteNonQuery(); 24 | 25 | protected SQLiteDataReader ExecuteQueryReader(string cmd) 26 | => CreateCommand(cmd).ExecuteReader(); 27 | 28 | protected SQLiteCommand CreateCommand(string cmd) 29 | { 30 | var command = connection.CreateCommand(); 31 | command.CommandText = cmd; 32 | return command; 33 | } 34 | 35 | public void Dispose() 36 | { 37 | connection.Close(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/Repository/WorkshopItemRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.SQLite; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace L4d2_Mod_Manager_Tool.Domain.Repository 9 | { 10 | /// 11 | /// 创意工坊项存储库 12 | /// 13 | public class WorkshopItemRepository 14 | { 15 | 16 | private SQLiteConnection connection; 17 | 18 | public WorkshopItemRepository() 19 | { 20 | connection = new SQLiteConnection("data source=mod.db"); 21 | connection.Open(); 22 | CreateTableIfNotExists(); 23 | } 24 | 25 | /// 26 | /// [副作用]创建表格 27 | /// 28 | private void CreateTableIfNotExists() 29 | { 30 | var command = connection.CreateCommand(); 31 | command.CommandText = 32 | "CREATE TABLE IF NOT EXISTS workshop_infomation(" + 33 | "id INTEGER PRIMARY KEY" + 34 | ",mod_id INTEGER NOT NULL" + 35 | ",title TEXT" + 36 | ",descript TEXT" + 37 | ",preview TEXT" + 38 | ",FOREIGN KEY(mod_id) REFERENCES mod(id)" + 39 | "); "; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Widget/Widget_BackgroundTask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Data; 5 | using System.Drawing; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using System.Windows.Forms; 10 | 11 | namespace L4d2_Mod_Manager_Tool.Widget 12 | { 13 | public partial class Widget_BackgroundTask : UserControl 14 | { 15 | public event EventHandler OnTaskCancelRequested; 16 | private int _id = 0; 17 | public Widget_BackgroundTask() 18 | { 19 | InitializeComponent(); 20 | } 21 | 22 | public int Id { get;set; } 23 | public string TaskName 24 | { 25 | get => label_taskName.Text; 26 | set => label_taskName.Text = value; 27 | } 28 | 29 | public string TaskStatus 30 | { 31 | get => label_status.Text; 32 | set => label_status.Text = value; 33 | } 34 | 35 | public int Progress 36 | { 37 | get => progressBar_taskProgress.Value; 38 | set => progressBar_taskProgress.Value = value; 39 | } 40 | 41 | private void button_cancel_Click(object sender, EventArgs e) 42 | { 43 | OnTaskCancelRequested?.Invoke(this, null); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Domain/Core/L4d2Folder.cs: -------------------------------------------------------------------------------- 1 | using Domain.Settings; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Domain.Core 10 | { 11 | /// 12 | /// 求生之路2常用文件夹 13 | /// 14 | public static class L4d2Folder 15 | { 16 | private const string CoreFolderName = "left4dead2"; 17 | private const string AddonFolderName = "addons"; 18 | private const string WorkshopAddonFolderName = "workshop"; 19 | /// 20 | /// 游戏核心文件夹 21 | /// 22 | public static string CoreFolder => 23 | Path.Combine(SettingFP.GetSetting().GamePath, CoreFolderName); 24 | /// 25 | /// 离线模组文件夹 26 | /// 27 | public static string AddonsFolder => 28 | Path.Combine(CoreFolder, AddonFolderName); 29 | /// 30 | /// 创意工坊模组文件夹 31 | /// 32 | public static string WorkshopAddonsFolder => 33 | Path.Combine(AddonsFolder, WorkshopAddonFolderName); 34 | 35 | /// 36 | /// 获取模组文件完整路径 37 | /// 38 | public static string GetAddonFileFullPath(string filePath) 39 | => Path.Combine(AddonsFolder, filePath); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Domain/ModLocalInfo/ModAnalysisServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Domain.Core; 8 | 9 | namespace Domain.ModLocalInfo 10 | { 11 | public class ModAnalysisServer 12 | { 13 | private CategoryRuleGroup ruleGroup; 14 | 15 | public ModAnalysisServer() 16 | { 17 | // 初始化内容分类规则 18 | ruleGroup = new CategoryRuleGroupFactory().Create(); 19 | } 20 | 21 | public LocalInfo AnalysisMod(ModFile.ModFile mf) 22 | { 23 | var fn = Path.Combine(L4d2Folder.AddonsFolder, mf.FileLoc); 24 | if (!File.Exists(fn)) return null; 25 | 26 | var vpkFile = new VpkFile(fn); 27 | 28 | LocalInfo li = new(); 29 | li.Categories = vpkFile.Categories; 30 | li.AddonImage = vpkFile.Image.ValueOr(ImageFile.MissingImage); 31 | vpkFile.Info.Match(ai => 32 | { 33 | li.Title = ai.Title; 34 | li.Author = ai.Author; 35 | li.Version = ai.Version; 36 | li.Tagline = ai.Tagline; 37 | li.Description = ai.Description; 38 | li.Categories = li.Categories.Concat(ai.Categories).Distinct().ToArray(); 39 | }, () => { }); 40 | return li; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Service/ModCrossServer.cs: -------------------------------------------------------------------------------- 1 | using L4d2_Mod_Manager_Tool.Module.FileExplorer; 2 | using L4d2_Mod_Manager_Tool.Utility; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace L4d2_Mod_Manager_Tool.Service 10 | { 11 | static class ModCrossServer 12 | { 13 | /// 14 | /// 通过Mod ID获取模组路径(相对路径) 15 | /// 16 | /// 17 | /// 18 | public static Maybe GetModFileByModId(int modId) 19 | { 20 | return ModOperation.FindModById(modId) 21 | .Bind(mod => ModFileService.FindFileById(mod.FileId)) 22 | .Map(mf => mf.FilePath); 23 | } 24 | 25 | 26 | 27 | /// 28 | /// 打开资源管理器,选中模组文件 29 | /// 30 | public static void ShowModInFileExplorer(int modId) 31 | { 32 | GetModFileByModId(modId) 33 | .Map(f => FileExplorerUtils.OpenFileExplorerAndSelectItem( 34 | L4d2Folder.GetAddonFileFullPath(f))); 35 | } 36 | 37 | /// 38 | /// 使用资源管理器打开模组文件 39 | /// 40 | public static void OpenModFileInExplorer(int modId) 41 | { 42 | GetModFileByModId(modId) 43 | .Map(f => FileExplorerUtils.OpenFileInExplorer( 44 | L4d2Folder.GetAddonFileFullPath(f))); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Domain/ModLocalInfo/CategoryRuleGroupFactory.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Domain.ModLocalInfo 10 | { 11 | struct Json_CategoryRule 12 | { 13 | [JsonProperty("regular")] 14 | public string Regular; 15 | [JsonProperty("category")] 16 | public string Category; 17 | } 18 | 19 | internal class CategoryRuleGroupFactory 20 | { 21 | private const string CategoriesConfigureFile = "Resources/categories.json"; 22 | 23 | public CategoryRuleGroup Create() 24 | { 25 | try 26 | { 27 | var jsonModel = JsonConvert.DeserializeObject>( 28 | File.ReadAllText(CategoriesConfigureFile)); 29 | var categoryRules = jsonModel.Select(m => 30 | { 31 | int lastIndex = m.Category.LastIndexOf('/') + 1; 32 | if (lastIndex > 0) 33 | return new CategoryRule(m.Category.Substring(lastIndex), m.Regular, m.Category); 34 | else 35 | return new CategoryRule(m.Category, m.Regular, m.Category); 36 | }).ToList(); 37 | return new CategoryRuleGroup(categoryRules); 38 | } 39 | catch (Exception e) 40 | { 41 | throw new Exception("分类规则加载失败:" + e.Message); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/Specifications/ModDetailSpecification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace L4d2_Mod_Manager_Tool.Domain.Specifications 8 | { 9 | // 规格描述 10 | public record ModDetailSpecification; 11 | public record MS_Empty : ModDetailSpecification; 12 | public record MS_Category(string Category) : ModDetailSpecification; 13 | public record MS_Tag(string Tag) : ModDetailSpecification; 14 | public record MS_Blur(string Blur) : ModDetailSpecification; 15 | public record MS_And(ModDetailSpecification Left, ModDetailSpecification Right) : ModDetailSpecification; 16 | public record MS_Or(ModDetailSpecification Left, ModDetailSpecification Right) : ModDetailSpecification; 17 | 18 | public static class ModDetailSpecificationPackage 19 | { 20 | public static ModDetailSpecification And(this ModDetailSpecification left, ModDetailSpecification right) 21 | { 22 | return (left, right) switch 23 | { 24 | (MS_Empty, _) => right, 25 | (_, MS_Empty) => left, 26 | (_, _) => new MS_And(left, right) 27 | }; 28 | } 29 | 30 | public static ModDetailSpecification Or(this ModDetailSpecification left, ModDetailSpecification right) 31 | { 32 | return (left, right) switch 33 | { 34 | (MS_Empty, _) => left, 35 | (_, MS_Empty) => right, 36 | (_, _) => new MS_Or(left, right) 37 | }; 38 | } 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 更新日志 2 | 3 | ## [Unrelease] - XXXX/XX/XX 4 | ### Added - 新增 5 | ### Changed - 变更 6 | ### Deprecated - 丢弃 7 | ### Removed - 移除 8 | ### Fixed - 修复 9 | ### Others - 其他 10 | 11 | ## [0.4.1] - 2023/2/28 12 | ### Fixed - 修复 13 | * 修复开启/关闭模组会操作到错误模组的问题 14 | * 修复打开模组/打开模组文件所在文件夹打开了错误模组的问题 15 | 16 | ## [0.4.0.a1] - 2022/10/23 17 | ### Added - 新增 18 | * 增加后台任务列表,可以查看、取消正在进行的后台任务 19 | * 现在支持对模组同时进行扫描和创意工坊信息的下载 20 | ### Changed - 变更 21 | * 模组文件扫描弹窗改为软件状态栏右下角的进度提示 22 | * 模组信息下载弹窗改为软件状态栏右下角的进度提示 23 | ### Deprecated - 丢弃 24 | * 使用no_vtf进行本地模组预览图转换的功能(暂时) 25 | ### Fixed - 修复 26 | * 修复模组启用状态图标不实时更新 27 | * 修复模组列表不显示创意工坊的模组信息 28 | ### Others - 其他 29 | * 优化了扫描本地模组的速度 30 | * 优化了下载模组信息的速度 31 | 32 | ## [0.3.0.a2] - 2022/8/2 33 | ### Fixed - 修复 34 | * 修复进行模糊查询时报 Invalid Operation 的错误 35 | 36 | ## [0.3.0.a1] - 2022/8/1 37 | ### Added - 新增 38 | * 可配置的模组内容分类过滤功能 39 | * 模组启用状态管理,通过模组列表对一个/多个模组开启/关闭操作 40 | * 支持通过Steam API获取模组信息,自动选择模组信息获取方式 41 | * 对模组列表进行排序,支持通过名称、文件、状态、作者排序模组 42 | ### Changed - 变更 43 | * 模组位置设置改为直接对游戏路径设置 44 | ### Others - 其他 45 | * 优化了模组列表显示,增强模组状态图标辨识度 46 | 47 | ## [0.2.0] - 2022/7/9 48 | ### Added - 新增 49 | * 增加对模组类型的解析功能 50 | * 创意工坊下载的信息增加标签信息 51 | * 增加模组预览(Mod Overview),在模组列表中选择的模组会在左侧显示模组的预览图、名称、分类、标签和描述 52 | * 模组标签分类过滤功能 53 | 54 | ### Removed - 移除 55 | * 不再依赖vpk.exe进行模组解包,现在可以独立解包vpk了(使用SharpVPK代替对vpk.exe的依赖) 56 | 57 | ### Others - 其他 58 | * 优化了模组解析速度 59 | * 优化模组列表刷新速度 60 | 61 | ## [0.1.0.a2] - 2022-7-2 62 | ### Added - 新增 63 | * 自定义模组文件夹管理,同时跟踪多个模组文件夹 64 | * 解析模组文件,提取缩略图、名称、描述等参数 65 | * 通过VPKID,从创意工坊下载在线信息,包括标题、缩略图和描述 66 | * 本地数据库,缓存扫描的模组文件 67 | * 模组列表查看器,以列表方式浏览模组缩略图(本地和工坊缩略图)、名称(本地和工坊名称)、作者、简介、VPKID 68 | * 从模组列表打开资源管理器并定位模组文件 69 | * 双击快速打开模组文件(需要GCFScape) -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Utility/FPExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | namespace L4d2_Mod_Manager_Tool.Utility 8 | { 9 | public static class FPExtension 10 | { 11 | public static Func Not(Func f) => x => !f(x); 12 | 13 | public static T Identity(T t) => t; 14 | 15 | /// 16 | /// 在序列间插入制定元素 17 | /// 18 | public static IEnumerable Intersperse(this IEnumerable source, T element) { 19 | bool first = true; 20 | foreach (T value in source) 21 | { 22 | if (!first) 23 | yield return element; 24 | yield return value; 25 | first = false; 26 | } 27 | } 28 | 29 | public static void Iter(this IEnumerable xs, Action a) 30 | { 31 | foreach (var x in xs) 32 | a(x); 33 | } 34 | 35 | 36 | public static Maybe FindElementSafe(this ILookup lk, T key) 37 | { 38 | if (lk.Contains(key)) 39 | { 40 | return Maybe.Some(lk[key].First()); 41 | } 42 | else 43 | { 44 | return Maybe.None; 45 | } 46 | } 47 | 48 | /// 49 | /// 安全地获取序列中的第一个元素 50 | /// 51 | public static Maybe FirstElementSafe(this IEnumerable xs) 52 | { 53 | return xs.Any() ? xs.First() : Maybe.None; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Widget/MenuItemsZipper.cs: -------------------------------------------------------------------------------- 1 | using Infrastructure.Utility; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using System.Windows.Forms; 8 | 9 | namespace L4d2_Mod_Manager_Tool.Widget 10 | { 11 | class MenuItemsZipper 12 | { 13 | private ToolStripMenuItem item; 14 | public MenuItemsZipper() 15 | { 16 | item = new ToolStripMenuItem(); 17 | } 18 | 19 | private MenuItemsZipper(ToolStripMenuItem item) 20 | { 21 | this.item = item; 22 | } 23 | 24 | public ToolStripMenuItem CurrentItem => item; 25 | 26 | public bool HaveChildItem(string text) 27 | { 28 | return item.DropDownItems.Cast() 29 | .Where(x => x.Text.Equals(text)).Any(); 30 | } 31 | 32 | public Maybe GotoItem(string text) 33 | { 34 | var it = 35 | item.DropDownItems.Cast() 36 | .Where(x => x.Text.Equals(text)).FirstElementSafe(); 37 | 38 | return it.Select(item => new MenuItemsZipper(item)); 39 | } 40 | 41 | public MenuItemsZipper InsertItem(string text) 42 | { 43 | ToolStripMenuItem i = new(text); 44 | item.DropDownItems.Add(i); 45 | return this; 46 | } 47 | 48 | public MenuItemsZipper Top() 49 | { 50 | var i = item; 51 | while (i.OwnerItem != null) 52 | i = i.OwnerItem as ToolStripMenuItem; 53 | return new MenuItemsZipper(i); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Widget/VOBackgroundTaskModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace L4d2_Mod_Manager_Tool.Widget 9 | { 10 | public class VOBackgroundTaskModel : INotifyPropertyChanged 11 | { 12 | public event PropertyChangedEventHandler PropertyChanged; 13 | 14 | private int _id; 15 | private string _name; 16 | private float _progress; 17 | private string _status; 18 | 19 | public int Id 20 | { 21 | get => _id; 22 | set 23 | { 24 | _id = value; 25 | OnPropertyChanged(nameof(Id)); 26 | } 27 | } 28 | 29 | public string Name 30 | { 31 | 32 | get => _name; 33 | set 34 | { 35 | _name = value; 36 | OnPropertyChanged(nameof(Name)); 37 | } 38 | } 39 | 40 | public float Progress 41 | { 42 | get => _progress; 43 | set 44 | { 45 | _progress = value; 46 | OnPropertyChanged(nameof(Progress)); 47 | } 48 | } 49 | 50 | public string Status 51 | { 52 | get => _status; 53 | set 54 | { 55 | _status = value; 56 | OnPropertyChanged(nameof(Status)); 57 | } 58 | } 59 | 60 | private void OnPropertyChanged(string propertyName) 61 | => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Widget/Widget_TagSelector.cs: -------------------------------------------------------------------------------- 1 | using L4d2_Mod_Manager_Tool.Domain; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel; 5 | using System.Data; 6 | using System.Drawing; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using System.Windows.Forms; 11 | 12 | namespace L4d2_Mod_Manager_Tool.Widget 13 | { 14 | public partial class Widget_TagSelector : UserControl 15 | { 16 | public Widget_TagSelector() 17 | { 18 | InitializeComponent(); 19 | GenerateTags("Survivors", ModTag.SurvivorsTags); 20 | GenerateTags("Infected", ModTag.InfectedTags); 21 | GenerateTags("Game Content", ModTag.GameContentTags); 22 | GenerateTags("Game Modes", ModTag.GameModesTags); 23 | GenerateTags("Weapons", ModTag.WeaponsTags); 24 | GenerateTags("Items", ModTag.ItemsTags); 25 | } 26 | 27 | private void GenerateTags(string name, string[] tags) 28 | { 29 | flowLayoutPanel1.Controls.Add(new Label() 30 | { 31 | Text = name 32 | , 33 | Margin = new Padding(0, 5, 0, 0) 34 | , 35 | Font = new Font("黑体", 10, FontStyle.Bold) 36 | }); 37 | foreach (var tag in tags) 38 | { 39 | flowLayoutPanel1.Controls.Add(new CheckBox() 40 | { 41 | Text = tag 42 | , 43 | Margin = new Padding(0) 44 | , 45 | Padding = new Padding(0) 46 | }); 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/Repository/AddonListRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using L4d2_Mod_Manager_Tool.Utility; 7 | 8 | namespace L4d2_Mod_Manager_Tool.Domain.Repository 9 | { 10 | public class AddonListRepository 11 | { 12 | private Dictionary enabledDic = new(); 13 | private Dictionary missingEnabledDic = new(); 14 | 15 | /// 16 | /// 模组启用列表 17 | /// 18 | public IEnumerable<(int, bool)> AddonList 19 | => enabledDic.Select(p => (p.Key, p.Value)); 20 | 21 | /// 22 | /// 未识别的模组启用列表 23 | /// 24 | public IEnumerable<(string, bool)> MissingAddonList 25 | => missingEnabledDic.Select(p => (p.Key, p.Value)); 26 | public void AddRange(IEnumerable<(int, bool)> datas) 27 | { 28 | datas.Iter(t => enabledDic.Add(t.Item1, t.Item2)); 29 | } 30 | 31 | public void AddMissingAddonInfos(IEnumerable<(string, bool)> datas) 32 | { 33 | datas.Iter(t => missingEnabledDic.Add(t.Item1, t.Item2)); 34 | } 35 | 36 | public void Clear() 37 | { 38 | enabledDic.Clear(); 39 | missingEnabledDic.Clear(); 40 | } 41 | 42 | public void SetModEnabled(int modId, bool enabled) 43 | { 44 | enabledDic[modId] = enabled; 45 | } 46 | 47 | public Maybe ModEnabled(int modId) 48 | { 49 | if (enabledDic.TryGetValue(modId, out bool b)) 50 | return Maybe.Some(b); 51 | else 52 | return Maybe.None; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Domain/ModFile/ModScanner.cs: -------------------------------------------------------------------------------- 1 | using Domain.Core; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Domain.ModFile 10 | { 11 | public class ModScanner 12 | { 13 | private readonly ModFileRepository modFileRepository; 14 | 15 | public ModScanner(ModFileRepository modFileRepository) 16 | { 17 | this.modFileRepository = modFileRepository; 18 | } 19 | 20 | /// 21 | /// 扫描所有的模组文件,找到与记录的变化 22 | /// 23 | public ModFileChanged ScanModFileChanged() 24 | { 25 | var savedMods = modFileRepository.GetAll(); 26 | 27 | var localAddons = ListVpk(L4d2Folder.AddonsFolder); 28 | var workshopAddons = ListVpk(L4d2Folder.WorkshopAddonsFolder).Select(x => $"workshop\\{x}"); 29 | var addons = localAddons.Concat(workshopAddons).ToArray(); 30 | 31 | var newAddons = addons.Except(savedMods.Select(m => m.FileLoc)); 32 | var lostMods = savedMods.Where(m => !addons.Contains(m.FileLoc)); 33 | 34 | return new(lostMods.ToArray(), newAddons.Select(ModFile.CreateFromFile).ToArray()); 35 | } 36 | 37 | /// 38 | /// 列出指定文件夹中所有vpk文件 39 | /// 40 | private static IEnumerable ListVpk(string folder) 41 | { 42 | DirectoryInfo di = new(folder); 43 | if (di.Exists) 44 | { 45 | var res = di.GetFiles("*.vpk").Select(f => f.Name).ToArray(); 46 | return res; 47 | } 48 | else 49 | { 50 | return Array.Empty(); 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Resources/categories.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"regular":"^scripts","category":"Scripts"}, 3 | {"regular":"^maps/(?:.+)","category":"Maps"}, 4 | {"regular":"^models/survivors","category":"Survivors/Survivors"}, 5 | {"regular":"^models/survivors/survivor_namvet\\.mdl","category":"Survivors/Bill"}, 6 | {"regular":"^models/survivors/survivor_biker\\.mdl","category":"Survivors/Francis"}, 7 | {"regular":"^models/survivors/survivor_manager\\.mdl","category":"Survivors/Louis"}, 8 | {"regular":"^models/survivors/survivor_teenangst\\.mdl","category":"Survivors/Zoey"}, 9 | {"regular":"^models/survivors/survivor_mechanic\\.mdl","category":"Survivors/Ellis"}, 10 | {"regular":"^models/survivors/survivor_coach\\.mdl","category":"Survivors/Coach"}, 11 | {"regular":"^models/survivors/survivor_gambler\\.mdl","category":"Survivors/Nick"}, 12 | {"regular":"^models/survivors/survivor_producer\\.mdl","category":"Survivors/Rochelle"}, 13 | {"regular":"^models/infected","category":"Infected/Infected"}, 14 | {"regular":"^models/infected/common.+\\.mdl","category":"Infected/Common Infected"}, 15 | {"regular":"^models/infected/[bchjsw].+\\.mdl","category":"Infected/Special Infected"}, 16 | {"regular":"^models/infected/witch\\.mdl","category":"Infected/Witch"}, 17 | {"regular":"^models/infected/hulk\\.mdl","category":"Infected/Tank"}, 18 | {"regular":"^models/infected/hunter\\.mdl","category":"Infected/Hunter"}, 19 | {"regular":"^models/infected/boome(r|tte)\\.mdl","category":"Infected/Boomer"}, 20 | {"regular":"^models/infected/smoker\\.mdl","category":"Infected/Smoker"}, 21 | {"regular":"^models/infected/charger\\.mdl","category":"Infected/Charger"}, 22 | {"regular":"^models/infected/jockey\\.mdl","category":"Infected/Jockey"}, 23 | {"regular":"^models/infected/spitter\\.mdl","category":"Infected/Spitter"}, 24 | {"regular":"^models/w_models/weapons/(?:.+)\\.mdl","category":"Weapons"} 25 | ] -------------------------------------------------------------------------------- /Domain/Core/ModBrief.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Domain.Core 9 | { 10 | public class ModBrief 11 | { 12 | private const string NotName = "<无名称>"; 13 | private const string NotAuthor = "<未知作者>"; 14 | private const string NotTagline = "<无简介>"; 15 | 16 | public int Id { get;private set; } 17 | public VpkId VpkId { get; set; } 18 | public string Name { get; set; } 19 | public string ReadableName => string.IsNullOrEmpty(Name) ? NotName : Name; 20 | public string FileName { get; set; } 21 | public bool Enabled { get; set; } 22 | public string ReadableEnabled => Enabled ? "启用" : "禁用"; 23 | public string Author { get; set; } 24 | public string ReadableAuthor => string.IsNullOrEmpty(Author) ? NotAuthor : Author; 25 | public string Tagline { get; set; } 26 | public string ReadableTagline => string.IsNullOrEmpty(Tagline) ? NotTagline : Tagline; 27 | public string Tags { get; set; } 28 | public string Categories { get; set; } 29 | 30 | public ModBrief(int id) 31 | { 32 | Id = id; 33 | Author = string.Empty; 34 | Categories = string.Empty; 35 | Enabled = false; 36 | FileName = string.Empty; 37 | Name = string.Empty; 38 | Tagline = string.Empty; 39 | Tags = string.Empty; 40 | } 41 | 42 | public static ModBrief Default 43 | => new(0) 44 | { 45 | VpkId = VpkId.Undefined, 46 | Author = string.Empty, 47 | Categories = string.Empty, 48 | Enabled = false, 49 | FileName = string.Empty, 50 | Name = string.Empty, 51 | Tagline = string.Empty, 52 | Tags = string.Empty 53 | }; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Form_Settings.cs: -------------------------------------------------------------------------------- 1 | using Domain.Settings; 2 | using System; 3 | using System.IO; 4 | using System.Windows.Forms; 5 | 6 | namespace L4d2_Mod_Manager_Tool 7 | { 8 | public partial class Form_Settings : Form 9 | { 10 | private Setting setting; 11 | public Form_Settings() 12 | { 13 | InitializeComponent(); 14 | setting = SettingFP.GetSetting(); 15 | textBox_gamePath.Text = setting.GamePath; 16 | textBox_novtfExecutable.Text = setting.NoVtfExecutablePath; 17 | } 18 | 19 | private void SetGamePath(string gp) 20 | { 21 | textBox_gamePath.Text = gp; 22 | } 23 | 24 | private void button_importLocationFromGameFolder_Click(object sender, EventArgs e) 25 | { 26 | OpenFileDialog dialog = new(); 27 | dialog.Filter = "left4dead2.exe | left4dead2.exe"; 28 | dialog.Title = "选择left4dead2.exe"; 29 | if(dialog.ShowDialog() == DialogResult.OK) 30 | { 31 | var folder = Path.GetDirectoryName(dialog.FileName); 32 | SetGamePath(folder); 33 | } 34 | } 35 | 36 | private void button_cancel_Click(object sender, EventArgs e) 37 | { 38 | Close(); 39 | } 40 | 41 | private void button_confirm_Click(object sender, EventArgs e) 42 | { 43 | setting.GamePath = textBox_gamePath.Text; 44 | setting.NoVtfExecutablePath = textBox_novtfExecutable.Text; 45 | SettingFP.SaveSetting(setting); 46 | Close(); 47 | } 48 | 49 | private void button_novtfSelectExecutable_Click(object sender, EventArgs e) 50 | { 51 | OpenFileDialog dialog = new(); 52 | dialog.Filter = "no_vtf(*.exe) | *.exe"; 53 | if(dialog.ShowDialog() == DialogResult.OK) 54 | { 55 | textBox_novtfExecutable.Text = dialog.FileName; 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Infrastructure/Utility/FPExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | namespace Infrastructure.Utility 8 | { 9 | public static class FPExtension 10 | { 11 | public static Func Not(Func f) => x => !f(x); 12 | 13 | public static T Identity(T t) => t; 14 | 15 | public static IEnumerable Pure(T element) 16 | { 17 | yield return element; 18 | } 19 | 20 | /// 21 | /// 在序列间插入制定元素 22 | /// 23 | public static IEnumerable Intersperse(this IEnumerable source, T element) { 24 | bool first = true; 25 | foreach (T value in source) 26 | { 27 | if (!first) 28 | yield return element; 29 | yield return value; 30 | first = false; 31 | } 32 | } 33 | 34 | public static void Iter(this IEnumerable xs, Action a) 35 | { 36 | foreach (var x in xs) 37 | a(x); 38 | } 39 | 40 | 41 | public static Maybe FindElementSafe(this ILookup lk, T key) 42 | { 43 | if (lk.Contains(key)) 44 | { 45 | return Maybe.Some(lk[key].First()); 46 | } 47 | else 48 | { 49 | return Maybe.None; 50 | } 51 | } 52 | 53 | /// 54 | /// 安全地获取序列中的第一个元素 55 | /// 56 | public static Maybe FirstElementSafe(this IEnumerable xs) 57 | { 58 | return xs.Any() ? xs.First() : Maybe.None; 59 | } 60 | 61 | public static IEnumerable DiscardMaybe(this IEnumerable> xs) 62 | { 63 | foreach(var x in xs) 64 | { 65 | if (x.HasValue) 66 | yield return x.ValueOrThrow(""); 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Service/ModCategoryService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Text.RegularExpressions; 7 | using System.Threading.Tasks; 8 | using Newtonsoft.Json; 9 | 10 | namespace L4d2_Mod_Manager_Tool.Service 11 | { 12 | struct Json_CategoryRule 13 | { 14 | [JsonProperty("regular")] 15 | public string Regular; 16 | [JsonProperty("category")] 17 | public string Category; 18 | } 19 | 20 | record CategoryRule(string Category, string Regular, string Path); 21 | 22 | static class ModCategoryService 23 | { 24 | private const string CategoriesConfigureFile = "Resources/categories.json"; 25 | 26 | private static List categoryRules = new(); 27 | 28 | /// 29 | /// 从文件载入数据 30 | /// 31 | public static void Load() 32 | { 33 | try 34 | { 35 | var jsonModel = JsonConvert.DeserializeObject>( 36 | File.ReadAllText(CategoriesConfigureFile)); 37 | categoryRules = jsonModel.Select(m => 38 | { 39 | int lastIndex = m.Category.LastIndexOf('/') + 1; 40 | if(lastIndex > 0) 41 | return new CategoryRule(m.Category.Substring(lastIndex), m.Regular, m.Category); 42 | else 43 | return new CategoryRule(m.Category, m.Regular, m.Category); 44 | }).ToList(); 45 | } 46 | catch (Exception e){ 47 | Utility.WinformUtility.ErrorMessageBox("分类规则加载失败:" + e.Message, "服务初始化错误"); 48 | } 49 | } 50 | 51 | public static IEnumerable Pathes => categoryRules.Select(x => x.Path); 52 | 53 | public static IEnumerable MatchCategories(string entry) 54 | { 55 | return categoryRules 56 | .Where(r => Regex.IsMatch(entry, r.Regular)) 57 | .Select(r => r.Category); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Domain/ModLocalInfo/LocalInfoRepository.cs: -------------------------------------------------------------------------------- 1 | using Infrastructure; 2 | using Infrastructure.Utility; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace Domain.ModLocalInfo 10 | { 11 | public delegate void LocalInfosDel(IEnumerable localInfos); 12 | public class LocalInfoRepository 13 | { 14 | private DapperHelper dapperHelper = DapperHelper.Instance; 15 | public event LocalInfosDel OnLocalInfosAdded; 16 | 17 | public LocalInfo Save(LocalInfo li) 18 | { 19 | var po = LocalInfo_DoToPo(li); 20 | int id = (int)dapperHelper.Insert(po); 21 | var resLi = li.WithId(id); 22 | OnLocalInfosAdded?.Invoke(FPExtension.Pure(resLi)); 23 | return resLi; 24 | } 25 | 26 | public LocalInfo FindById(int id) 27 | { 28 | var po = dapperHelper.Get(id); 29 | return LocalInfo_PoToDo(po); 30 | } 31 | 32 | private PO_LocalInfo LocalInfo_DoToPo(LocalInfo li) 33 | { 34 | if (li == null) return null; 35 | 36 | return new PO_LocalInfo() 37 | { 38 | id = li.Id, 39 | author = li.Author, 40 | categories = Category.Concat(li.Categories), 41 | description = li.Description, 42 | tagline = li.Tagline, 43 | thumbnail = li.AddonImage.File, 44 | title = li.Title, 45 | version = li.Version 46 | }; 47 | } 48 | 49 | private LocalInfo LocalInfo_PoToDo(PO_LocalInfo po) 50 | { 51 | if(po == null) return null; 52 | 53 | return new LocalInfo(po.id) 54 | { 55 | AddonImage = new(po.thumbnail), 56 | Author = po.author, 57 | Categories = Category.ParseGroup(po.categories), 58 | Description = po.description, 59 | Tagline = po.tagline, 60 | Title = po.title, 61 | Version = po.version 62 | }; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Domain/ModLocalInfo/BatchAnalysis.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Threading; 7 | 8 | namespace Domain.ModLocalInfo 9 | { 10 | public class BatchAnalysis 11 | { 12 | private ModFile.ModFile[] _mfs; 13 | private LocalInfo[] _localInfosResult; 14 | 15 | public bool Completed { get; private set; } = false; 16 | 17 | public BatchAnalysis(ModFile.ModFile[] mfs) 18 | { 19 | _mfs = mfs; 20 | } 21 | 22 | public void DoBatchAnalysis(ModAnalysisServer analysisServer, CancellationToken cancellationToken) 23 | { 24 | if (Completed) 25 | throw new InvalidOperationException("无法重复执行一个已经完成的合批分析"); 26 | 27 | var lis = _mfs 28 | // 带上取消标记并转换为LocalInfo 29 | .Select(analysisServer.AnalysisMod) 30 | // 并行后保证执行顺序,附带取消符号 31 | .AsParallel().AsOrdered().WithCancellation(cancellationToken) 32 | // 执行 33 | .ToArray(); 34 | 35 | _localInfosResult = lis; 36 | // 完成了分析 37 | Completed = true; 38 | } 39 | 40 | public void UpdateEntities(LocalInfoRepository liRepo, ModFile.ModFileRepository mfRepo) 41 | { 42 | if (!Completed) 43 | throw new InvalidOperationException("在分析完成前无法更新相关实体"); 44 | if (_mfs == null || _localInfosResult == null) 45 | return; 46 | _mfs.Zip(_localInfosResult).ToList().ForEach(pair => 47 | { 48 | (var mf, var li) = pair; 49 | var savedLi = liRepo.Save(li); 50 | var newMf = mf with { LocalinfoId = savedLi.Id }; 51 | mfRepo.Update(newMf); 52 | }); 53 | } 54 | 55 | public static IEnumerable InBatches(ModFile.ModFile[] mfs, int numPerBatch) 56 | { 57 | if (numPerBatch < 1) 58 | throw new ArgumentException("合批数量不能小于1"); 59 | 60 | for (int i = 0; i < mfs.Length; i += numPerBatch) 61 | { 62 | yield return new BatchAnalysis(mfs.Skip(i).Take(numPerBatch).ToArray()); 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Domain/Core/ModBriefModule/ModBriefSpecification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Domain.Core.ModBriefModule 8 | { 9 | // 规格描述 10 | public record ModBriefSpecification; 11 | public record MS_Empty : ModBriefSpecification; 12 | public record MS_Category(string Category) : ModBriefSpecification; 13 | public record MS_Tag(string Tag) : ModBriefSpecification; 14 | public record MS_Blur(string Blur) : ModBriefSpecification; 15 | public record MS_And(ModBriefSpecification Left, ModBriefSpecification Right) : ModBriefSpecification; 16 | public record MS_Or(ModBriefSpecification Left, ModBriefSpecification Right) : ModBriefSpecification; 17 | 18 | public static class ModDetailSpecificationPackage 19 | { 20 | public static ModBriefSpecification And(this ModBriefSpecification left, ModBriefSpecification right) 21 | { 22 | return (left, right) switch 23 | { 24 | (MS_Empty, _) => right, 25 | (_, MS_Empty) => left, 26 | (_, _) => new MS_And(left, right) 27 | }; 28 | } 29 | 30 | public static ModBriefSpecification Or(this ModBriefSpecification left, ModBriefSpecification right) 31 | { 32 | return (left, right) switch 33 | { 34 | (MS_Empty, _) => left, 35 | (_, MS_Empty) => right, 36 | (_, _) => new MS_Or(left, right) 37 | }; 38 | } 39 | 40 | public static bool IsSpecified(this ModBriefSpecification spec, ModBrief md) 41 | { 42 | return spec switch 43 | { 44 | MS_Empty => true, 45 | MS_Category c => md.Categories.Contains(c.Category, StringComparison.OrdinalIgnoreCase), 46 | MS_Tag t => md.Tags.Contains(t.Tag, StringComparison.OrdinalIgnoreCase), 47 | MS_Blur b => md.VpkId.Id.ToString().Contains(b.Blur, StringComparison.OrdinalIgnoreCase) || md.Author.Contains(b.Blur, StringComparison.OrdinalIgnoreCase) || md.Name.Contains(b.Blur, StringComparison.OrdinalIgnoreCase), 48 | MS_And and => IsSpecified(and.Left, md) && IsSpecified(and.Right, md), 49 | MS_Or or => IsSpecified(or.Left, md) || IsSpecified(or.Right, md), 50 | _ => throw new NotImplementedException() 51 | }; 52 | } 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Widget/ListViewColumnSorter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using System.Windows.Forms; 8 | 9 | namespace L4d2_Mod_Manager_Tool.Widget 10 | { 11 | /// 12 | /// 对ListView的列进行比较排序 13 | /// 14 | public class ListViewColumnSorter : IComparer 15 | { 16 | private int ColumnToSort; //指定按照哪列排序 17 | private SortOrder OrderOfSort; //指定排序的方式 18 | private CaseInsensitiveComparer ObjectCompare; //声明CaaseInsensitiveComparer类对象 19 | public ListViewColumnSorter() //构造函数 20 | { 21 | ColumnToSort = 0; //默认按第一列排序 22 | OrderOfSort = SortOrder.None; //排序 23 | ObjectCompare = new CaseInsensitiveComparer(); //初始化CaseInsensitiveComparer类对象 24 | } 25 | //重写IComparer接口 26 | //返回比较的结果:如果x=y返回0;如果x>y返回1;如果x 7 | /// 必需的设计器变量。 8 | /// 9 | private System.ComponentModel.IContainer components = null; 10 | 11 | /// 12 | /// 清理所有正在使用的资源。 13 | /// 14 | /// 如果应释放托管资源,为 true;否则为 false。 15 | protected override void Dispose(bool disposing) 16 | { 17 | if (disposing && (components != null)) 18 | { 19 | components.Dispose(); 20 | } 21 | base.Dispose(disposing); 22 | } 23 | 24 | #region 组件设计器生成的代码 25 | 26 | /// 27 | /// 设计器支持所需的方法 - 不要修改 28 | /// 使用代码编辑器修改此方法的内容。 29 | /// 30 | private void InitializeComponent() 31 | { 32 | this.flowLayoutPanel1 = new System.Windows.Forms.FlowLayoutPanel(); 33 | this.SuspendLayout(); 34 | // 35 | // flowLayoutPanel1 36 | // 37 | this.flowLayoutPanel1.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 38 | | System.Windows.Forms.AnchorStyles.Left) 39 | | System.Windows.Forms.AnchorStyles.Right))); 40 | this.flowLayoutPanel1.AutoScroll = true; 41 | this.flowLayoutPanel1.FlowDirection = System.Windows.Forms.FlowDirection.TopDown; 42 | this.flowLayoutPanel1.Location = new System.Drawing.Point(3, 3); 43 | this.flowLayoutPanel1.Name = "flowLayoutPanel1"; 44 | this.flowLayoutPanel1.Size = new System.Drawing.Size(211, 579); 45 | this.flowLayoutPanel1.TabIndex = 0; 46 | this.flowLayoutPanel1.WrapContents = false; 47 | // 48 | // Widget_TagSelector 49 | // 50 | this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 17F); 51 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 52 | this.Controls.Add(this.flowLayoutPanel1); 53 | this.Name = "Widget_TagSelector"; 54 | this.Size = new System.Drawing.Size(217, 585); 55 | this.ResumeLayout(false); 56 | 57 | } 58 | 59 | #endregion 60 | 61 | private System.Windows.Forms.FlowLayoutPanel flowLayoutPanel1; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Domain/Core/ModBriefModule/ModBriefSpecificationBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Domain.Core.ModBriefModule 8 | { 9 | /// 10 | /// 模组概要规格构建器 11 | /// 12 | public class ModBriefSpecificationBuilder 13 | { 14 | private string filterName = ""; 15 | public List Tags { get; private set; } = new(); 16 | public List Categories { get; private set; } = new(); 17 | 18 | public void SetName(string name) 19 | { 20 | filterName = name ?? ""; 21 | } 22 | 23 | public void SetTags(IEnumerable tags) 24 | => Tags = tags.ToList(); 25 | 26 | public void AddTag(string tag) 27 | { 28 | if (!Tags.Contains(tag)) 29 | Tags.Add(tag); 30 | } 31 | 32 | public void RemoveTag(string tag) 33 | { 34 | Tags.Remove(tag); 35 | } 36 | 37 | public void SetCategories(IEnumerable cats) 38 | => Categories = cats.ToList(); 39 | 40 | public void AddCategory(string cat) 41 | => Categories.Add(cat); 42 | 43 | public void RemoveCategory(string cat) 44 | => Categories.Remove(cat); 45 | 46 | public ModBriefSpecification FinalSpec 47 | { 48 | get => CreateBlurSpec() 49 | .And(CreateTagsSpec()) 50 | .And(CreateCategoriesSpec()); 51 | } 52 | 53 | private ModBriefSpecification CreateBlurSpec() 54 | { 55 | return string.IsNullOrEmpty(filterName) 56 | ? new MS_Empty() 57 | : new MS_Blur(filterName); 58 | } 59 | 60 | private ModBriefSpecification CreateTagsSpec() 61 | { 62 | return Tags.Count switch 63 | { 64 | 0 => new MS_Empty(), 65 | _ => Tags.Select(x => (ModBriefSpecification)new MS_Tag(x)) 66 | .Aggregate((x, y) => x.And(y)) 67 | }; 68 | } 69 | 70 | private ModBriefSpecification CreateCategoriesSpec() 71 | { 72 | return Categories.Count switch 73 | { 74 | 0 => new MS_Empty(), 75 | _ => Categories.Select(x => (ModBriefSpecification)new MS_Category(x)) 76 | .Aggregate((x, y) => x.And(y)) 77 | }; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Domain/Core/WorkshopInfoModule/WorkshopInfoRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Infrastructure; 7 | using Infrastructure.Utility; 8 | 9 | namespace Domain.Core.WorkshopInfoModule 10 | { 11 | public delegate void WorkshopInfoDel(IEnumerable workshopInfos); 12 | public class WorkshopInfoRepository 13 | { 14 | private DapperHelper dapperHelper = DapperHelper.Instance; 15 | 16 | public event WorkshopInfoDel OnWorkshopInfoAdded; 17 | 18 | public WorkshopInfo[] GetAll() 19 | { 20 | var pos = dapperHelper.GetAll(); 21 | return pos.Select(WorkshopInfo_PoToDo).ToArray(); 22 | } 23 | 24 | public Maybe GetByVpkId(VpkId id) 25 | { 26 | var po = dapperHelper.Get(id.Id); 27 | return po switch 28 | { 29 | null => Maybe.None, 30 | _ => WorkshopInfo_PoToDo(po) 31 | }; 32 | } 33 | 34 | public void SaveRange(IEnumerable xs) 35 | { 36 | { 37 | using var conn = DapperHelper.OpenConnection(); 38 | using var trans = conn.BeginTransaction(); 39 | 40 | var pos = xs.Select(WorkshopInfo_DoToPo).ToList();//.ForEach(x => trans.Execute("INSERT INTO workshopinfo VALUES(@vpk_id, @preview, @title, @description", x)); 41 | trans.Execute("INSERT INTO workshopinfo VALUES(@vpk_id, @author, @preview, @title, @description, @tags)", pos); 42 | } 43 | OnWorkshopInfoAdded?.Invoke(xs); 44 | } 45 | 46 | private static WorkshopInfo WorkshopInfo_PoToDo(PO_WorkshopInfo po) 47 | => new(new(po.vpk_id)) 48 | { 49 | Description = po.description, 50 | Preview = new ImageFile(po.preview), 51 | Tags = Tag.ParseGroup(po.tags), 52 | Title = po.title, 53 | Autor = po.author 54 | }; 55 | 56 | private static PO_WorkshopInfo WorkshopInfo_DoToPo(WorkshopInfo d) 57 | => new() 58 | { 59 | description = d.Description, 60 | preview = d.Preview.File, 61 | tags = Tag.Concat(d.Tags), 62 | title = d.Title, 63 | vpk_id = d.Id.Id, 64 | author = d.Autor 65 | }; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Service/AddonInfoDownload/AddonInfoDownloadService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Domain.Core; 7 | using Domain.Core.WorkshopInfoModule; 8 | using L4d2_Mod_Manager_Tool.Domain.Repository; 9 | using Infrastructure.Utility; 10 | using System.Collections.Immutable; 11 | 12 | namespace L4d2_Mod_Manager_Tool.Service.AddonInfoDownload 13 | { 14 | /// 15 | /// 模组信息下载服务 16 | /// 17 | class AddonInfoDownloadService 18 | { 19 | private IAddonInfoDownloadStrategy downloadStrategy; 20 | 21 | public string CurrentDownloadStretegyName => downloadStrategy.StrategyName; 22 | 23 | public AddonInfoDownloadService() 24 | { 25 | //首先尝试载入steamworks策略,如果失败再载入爬虫策略 26 | downloadStrategy = SteamworksAddonInfoDownloadStrategy.CreateStrategy() 27 | .Match( 28 | FPExtension.Identity, 29 | () => new SpiderAddonInfoDownloadStrategy() 30 | ); 31 | } 32 | public async Task DownloadAddonInfosAsync(IEnumerable vpks) 33 | { 34 | List res = new(); 35 | foreach (var vpk in vpks) 36 | { 37 | var wi = await DownloadAddonInfo(vpk); 38 | wi.Match(some => res.Add(some), ()=>{ }); 39 | } 40 | return res.ToArray(); 41 | //var newMods = vpks.AsParallel().Select(x => DownloadAddonInfo(x)).ToArray(); 42 | //return newMods.DiscardMaybe().ToArray(); 43 | } 44 | /// 45 | /// 下载模组信息 46 | /// 47 | /// 48 | public async Task> DownloadAddonInfo(VpkId id) 49 | { 50 | var info = await downloadStrategy.DownloadAddonInfoAsync((ulong)id.Id).ConfigureAwait(false); 51 | return info.Bind(x => ConvertIfNotEmpty(id, x)); 52 | } 53 | 54 | private Maybe ConvertIfNotEmpty(VpkId id, Domain.ModWorkshopInfo i) 55 | => i.IsEmpty ? Maybe.None 56 | : new WorkshopInfo(id) 57 | { 58 | Description = i.Descript, 59 | Preview = new ImageFile(i.PreviewImage), 60 | Tags = i.Tags.Select(x => new Tag(x)).ToArray(), 61 | Title = i.Title, 62 | Autor = i.Author 63 | }; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/ModTag.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace L4d2_Mod_Manager_Tool.Domain 9 | { 10 | public static class ModTag 11 | { 12 | public static string[] SurvivorsTags => 13 | new string[] 14 | { 15 | "Survivors" 16 | ,"Bill" 17 | ,"Francis" 18 | ,"Louis" 19 | ,"Zoey" 20 | ,"Coach" 21 | ,"Ellis" 22 | ,"Nick" 23 | ,"Rochelle" 24 | }; 25 | 26 | public static string[] InfectedTags => 27 | new string[] 28 | { 29 | "Common Infected" 30 | ,"Special Infected" 31 | ,"Boomer" 32 | ,"Charger" 33 | ,"Hunter" 34 | ,"Jockey" 35 | ,"Smoker" 36 | ,"Spitter" 37 | ,"Tank" 38 | ,"Witch" 39 | }; 40 | 41 | public static string[] GameContentTags => 42 | new string[] 43 | { 44 | "Campaigns" 45 | ,"Weapons" 46 | ,"Items" 47 | ,"Sounds" 48 | ,"Scripts" 49 | ,"UI" 50 | ,"Miscellaneous" 51 | ,"Models" 52 | ,"Textures" 53 | }; 54 | 55 | public static string[] GameModesTags => 56 | new string[] 57 | { 58 | "Single Player" 59 | ,"Co-op" 60 | ,"Versus" 61 | ,"Scavenge" 62 | ,"Survival" 63 | ,"Realism" 64 | ,"Realism Versus" 65 | ,"Mutations" 66 | }; 67 | 68 | public static string[] WeaponsTags => 69 | new string[] 70 | { 71 | "Grenade Launcher" 72 | ,"M60" 73 | ,"Melee" 74 | ,"Pistol" 75 | ,"Rifle" 76 | ,"Shotgun" 77 | ,"SMG" 78 | ,"Sniper" 79 | ,"Throwable" 80 | }; 81 | 82 | public static string[] ItemsTags => 83 | new string[] 84 | { 85 | "Adrenaline" 86 | ,"Defibrillator" 87 | ,"Medkit" 88 | ,"Pills" 89 | ,"Other" 90 | }; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.1.32407.343 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "L4d2_Mod_Manager_Tool", "L4d2_Mod_Manager_Tool\L4d2_Mod_Manager_Tool.csproj", "{220C411B-4CE1-4370-8890-3C1580C2E2D5}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpVPK", "SharpVPK\SharpVPK.csproj", "{BAA47121-EC01-406C-9591-96B22E54D551}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Infrastructure", "Infrastructure\Infrastructure.csproj", "{7D5323CB-685E-477F-96D4-26727378DF40}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Domain", "Domain\Domain.csproj", "{E51AC64C-DD28-4BE4-A566-2BFC42B97E48}" 13 | EndProject 14 | Global 15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 16 | Debug|Any CPU = Debug|Any CPU 17 | Release|Any CPU = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 20 | {220C411B-4CE1-4370-8890-3C1580C2E2D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {220C411B-4CE1-4370-8890-3C1580C2E2D5}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {220C411B-4CE1-4370-8890-3C1580C2E2D5}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {220C411B-4CE1-4370-8890-3C1580C2E2D5}.Release|Any CPU.Build.0 = Release|Any CPU 24 | {BAA47121-EC01-406C-9591-96B22E54D551}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {BAA47121-EC01-406C-9591-96B22E54D551}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {BAA47121-EC01-406C-9591-96B22E54D551}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {BAA47121-EC01-406C-9591-96B22E54D551}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {7D5323CB-685E-477F-96D4-26727378DF40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {7D5323CB-685E-477F-96D4-26727378DF40}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {7D5323CB-685E-477F-96D4-26727378DF40}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {7D5323CB-685E-477F-96D4-26727378DF40}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {E51AC64C-DD28-4BE4-A566-2BFC42B97E48}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {E51AC64C-DD28-4BE4-A566-2BFC42B97E48}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {E51AC64C-DD28-4BE4-A566-2BFC42B97E48}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {E51AC64C-DD28-4BE4-A566-2BFC42B97E48}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(ExtensibilityGlobals) = postSolution 41 | SolutionGuid = {6830C481-1360-4D36-9E43-A6CCF6FE11C5} 42 | EndGlobalSection 43 | EndGlobal 44 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/Repository/ModDetailQuery.cs: -------------------------------------------------------------------------------- 1 | using L4d2_Mod_Manager_Tool.Utility; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using System.Data.SQLite; 8 | using L4d2_Mod_Manager_Tool.Domain.Specifications; 9 | using System.Web; 10 | 11 | namespace L4d2_Mod_Manager_Tool.Domain.Repository 12 | { 13 | public class ModDetailQuery : SQLiteDatabaseRepositoryBase 14 | { 15 | private AddonListRepository addonListRepo; 16 | public ModDetailQuery(AddonListRepository addonListRepo) : base("mod.db") 17 | { 18 | this.addonListRepo = addonListRepo; 19 | } 20 | protected override void CreateDatabase() { 21 | var mfr = new ModFileRepository(); 22 | ModRepository.Instance.FindModByFileId(1); 23 | } 24 | 25 | public Maybe FindOne(ModDetailSpecification spec) 26 | { 27 | var sql = "SELECT * FROM mod INNER JOIN mod_file ON mod.file_id = mod_file.id where " + spec.ToSqlite(); 28 | using var reader = ExecuteQueryReader(sql); 29 | if (reader.Read()) 30 | return FromReader(reader); 31 | else 32 | return Maybe.None; 33 | } 34 | 35 | public IEnumerable FindAll(ModDetailSpecification spec) 36 | { 37 | var sql = "SELECT * FROM mod INNER JOIN mod_file ON mod.file_id = mod_file.id"; 38 | if (spec is not MS_Empty) 39 | sql += " where " + spec.ToSqlite(); 40 | using var reader = ExecuteQueryReader(sql); 41 | while (reader.Read()) 42 | yield return FromReader(reader); 43 | } 44 | 45 | private ModDetail FromReader(SQLiteDataReader reader) 46 | { 47 | int id = reader.GetInt32(0); 48 | bool enabled = addonListRepo.ModEnabled(id).ValueOr(true); 49 | var ws_title = HttpUtility.UrlDecode(reader["workshop_title"].ToString()); 50 | var title = HttpUtility.UrlDecode(reader["title"].ToString()); 51 | return new ModDetail( 52 | id 53 | , IfEmptyReturn(string.IsNullOrEmpty(ws_title) ? title : ws_title, "<无名称>") 54 | , reader["filename"].ToString() 55 | , enabled 56 | , IfEmptyReturn(reader["author"].ToString(), "<未知作者>") 57 | , IfEmptyReturn(reader["tagline"].ToString(), "<无简介>") 58 | ); 59 | } 60 | 61 | private string IfEmptyReturn(string ori, string def) 62 | { 63 | return string.IsNullOrEmpty(ori) ? def : ori; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // 此代码由工具生成。 4 | // 运行时版本:4.0.30319.42000 5 | // 6 | // 对此文件的更改可能会导致不正确的行为,并且如果 7 | // 重新生成代码,这些更改将会丢失。 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace L4d2_Mod_Manager_Tool.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// 一个强类型的资源类,用于查找本地化的字符串等。 17 | /// 18 | // 此类是由 StronglyTypedResourceBuilder 19 | // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。 20 | // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen 21 | // (以 /str 作为命令选项),或重新生成 VS 项目。 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// 返回此类使用的缓存的 ResourceManager 实例。 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("L4d2_Mod_Manager_Tool.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// 重写当前线程的 CurrentUICulture 属性,对 51 | /// 使用此强类型资源类的所有资源查找执行重写。 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Service/ModFileService.cs: -------------------------------------------------------------------------------- 1 | using L4d2_Mod_Manager_Tool.Domain; 2 | using L4d2_Mod_Manager_Tool.Domain.Repository; 3 | using L4d2_Mod_Manager_Tool.Utility; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace L4d2_Mod_Manager_Tool.Service 13 | { 14 | public static class ModFileService 15 | { 16 | private static ModFileRepository modFileRepo = new(); 17 | 18 | public static Maybe FindFileById(int fid) 19 | { 20 | return modFileRepo.FindModFileById(fid); 21 | } 22 | 23 | public static Maybe FindFileByFileName(string fn) 24 | { 25 | return modFileRepo.FindModFileByFileName(fn); 26 | } 27 | 28 | public static ModFile SaveModFile(ModFile mf) 29 | { 30 | return modFileRepo.SaveModFile(mf); 31 | } 32 | 33 | public static bool ModFileExists(string file) 34 | { 35 | return modFileRepo.ModFileExists(file); 36 | } 37 | 38 | /// 39 | /// 开始扫描模组文件 40 | /// 41 | public static void BeginScanModFile(IProgress reporter) 42 | { 43 | // 扫描所有本地模组,如果模组还未入系统,则解压并读取信息,存入数据库 44 | // 构建解压任务集 45 | // 推入后台任务系统 46 | var mfs = VPKServices.ScanAllModFile().Where(x => !modFileRepo.ModFileExists(x.FilePath)).ToArray(); 47 | CancellationTokenSource source = new(); 48 | //TaskFramework.BackgroundWorks.Instance.AppendTasks("扫描模组信息", mfs.Select(ExtraMod).ToArray()); 49 | ExtraMods(mfs, reporter, source.Token); 50 | } 51 | 52 | static async Task ExtraMods(ModFile[] modFiles, IProgress reporter, CancellationToken token) 53 | { 54 | await Task.Run(() => 55 | { 56 | int total = modFiles.Length; 57 | int finished = 0; 58 | var tmpMods = modFiles.AsParallel().Select(mf => 59 | { 60 | if (token.IsCancellationRequested) 61 | return null; 62 | var tmpMod = ExtraMod(mf); 63 | reporter.Report(++finished / (float)total); 64 | return tmpMod; 65 | }).ToArray(); 66 | //批量储存到数据库 67 | ModRepository.Instance.SaveRange(tmpMods); 68 | reporter.Report(1); 69 | }, token); 70 | } 71 | 72 | static Mod ExtraMod(ModFile modFile) 73 | { 74 | var savedMf = ModFileService.SaveModFile(modFile); 75 | return VPKServices.ExtraMod(savedMf); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /SharpVPK/VpkArchive.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using SharpVPK.Exceptions; 9 | using SharpVPK.V1; 10 | 11 | namespace SharpVPK 12 | { 13 | public class VpkArchive 14 | { 15 | public List Directories { get; set; } 16 | public bool IsMultiPart { get; set; } 17 | private VpkReaderBase reader; 18 | internal List Parts { get; set; } 19 | internal string ArchivePath { get; set; } 20 | public uint ArchiveOffset { get; private set; } 21 | 22 | public VpkArchive() 23 | { 24 | Directories = new List(); 25 | } 26 | 27 | public void Load(string filename) 28 | { 29 | ArchivePath = filename; 30 | IsMultiPart = filename.EndsWith("dir.vpk"); 31 | if (IsMultiPart) 32 | LoadParts(filename); 33 | reader = new VpkReaderV1(filename); 34 | var hdr = reader.ReadArchiveHeader(); 35 | if (!hdr.Verify()) 36 | throw new ArchiveParsingException("Invalid archive header"); 37 | ArchiveOffset = hdr.CalculateDataOffset(); 38 | Directories.AddRange(reader.ReadDirectories(this)); 39 | } 40 | 41 | private void LoadParts(string filename) 42 | { 43 | Parts = new List(); 44 | var fileBaseName = filename.Split('_')[0]; 45 | foreach (var file in Directory.GetFiles(Path.GetDirectoryName(filename))) 46 | { 47 | if (file.Split('_')[0] != fileBaseName || file == filename) 48 | continue; 49 | var fi = new FileInfo(file); 50 | var partIdx = Int32.Parse(file.Split('_')[1].Split('.')[0]); 51 | Parts.Add(new ArchivePart((uint)fi.Length, partIdx, file)); 52 | } 53 | Parts.Add(new ArchivePart((uint) new FileInfo(filename).Length, -1, filename)); 54 | Parts = Parts.OrderBy(p => p.Index).ToList(); 55 | } 56 | 57 | public void MergeDirectories() 58 | { 59 | Dictionary cache = new Dictionary(); 60 | for (int i = 0; i < Directories.Count; ) 61 | { 62 | if (cache.ContainsKey(Directories[i].Path)) 63 | { 64 | cache[Directories[i].Path].Entries.AddRange(Directories[i].Entries); 65 | Directories.RemoveAt(i); 66 | } 67 | else 68 | { 69 | cache.Add(Directories[i].Path, Directories[i]); 70 | i++; 71 | } 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Dialog_AboutSoftware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Data; 5 | using System.Drawing; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Reflection; 9 | using System.Text; 10 | using System.Threading.Tasks; 11 | using System.Windows.Forms; 12 | 13 | namespace L4d2_Mod_Manager_Tool 14 | { 15 | public partial class Dialog_AboutSoftware : Form 16 | { 17 | public Dialog_AboutSoftware() 18 | { 19 | InitializeComponent(); 20 | 21 | textBox_version.Text = Version; 22 | textBox_computerName.Text = ComputerName; 23 | textBox_operatorVersion.Text = OSVersion; 24 | textBox_clrVersion.Text = ClrVersion; 25 | } 26 | 27 | private string Version => 28 | $"V{Utility.WinformUtility.SoftwareVersion} - build {File.GetLastWriteTime(ExecuteFile):yyMMddHHmm}"; 29 | 30 | private string ComputerName 31 | { 32 | get 33 | { 34 | var name = Environment.MachineName; 35 | if (Environment.UserDomainName != name) name = Environment.UserDomainName + "\\" + name; 36 | return name; 37 | } 38 | } 39 | 40 | private string ExecuteFile => CurrentAssembly.Location; 41 | 42 | private Assembly CurrentAssembly => Assembly.GetExecutingAssembly(); 43 | 44 | private string OSVersion => Environment.OSVersion.ToString(); 45 | 46 | private string ClrVersion => Environment.Version.ToString(); 47 | 48 | private static DateTime GetPe32Time(string fileName) 49 | { 50 | int seconds; 51 | using (var br = new BinaryReader(new FileStream(fileName, FileMode.Open, FileAccess.Read))) 52 | { 53 | var bs = br.ReadBytes(2); 54 | var msg = "非法的PE32文件"; 55 | if (bs.Length != 2) throw new Exception(msg); 56 | if (bs[0] != 'M' || bs[1] != 'Z') throw new Exception(msg); 57 | br.BaseStream.Seek(0x3c, SeekOrigin.Begin); 58 | var offset = br.ReadByte(); 59 | br.BaseStream.Seek(offset, SeekOrigin.Begin); 60 | bs = br.ReadBytes(4); 61 | if (bs.Length != 4) throw new Exception(msg); 62 | if (bs[0] != 'P' || bs[1] != 'E' || bs[2] != 0 || bs[3] != 0) throw new Exception(msg); 63 | bs = br.ReadBytes(4); 64 | if (bs.Length != 4) throw new Exception(msg); 65 | seconds = br.ReadInt32(); 66 | } 67 | return DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc). 68 | AddSeconds(seconds).ToLocalTime(); 69 | } 70 | 71 | private void button_confirm_Click(object sender, EventArgs e) 72 | { 73 | Close(); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Form_Settings.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | text/microsoft-resx 50 | 51 | 52 | 2.0 53 | 54 | 55 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 56 | 57 | 58 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 59 | 60 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Widget/Widget_FilterMod.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | text/microsoft-resx 50 | 51 | 52 | 2.0 53 | 54 | 55 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 56 | 57 | 58 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 59 | 60 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Widget/Widget_ModOverview.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | text/microsoft-resx 50 | 51 | 52 | 2.0 53 | 54 | 55 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 56 | 57 | 58 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 59 | 60 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Widget/Widget_TagSelector.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | text/microsoft-resx 50 | 51 | 52 | 2.0 53 | 54 | 55 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 56 | 57 | 58 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 59 | 60 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Widget/Widget_BackgroundTask.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | text/microsoft-resx 50 | 51 | 52 | 2.0 53 | 54 | 55 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 56 | 57 | 58 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 59 | 60 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Widget/Widget_BackgroundTaskList.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | text/microsoft-resx 50 | 51 | 52 | 2.0 53 | 54 | 55 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 56 | 57 | 58 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 59 | 60 | -------------------------------------------------------------------------------- /Domain/ModLocalInfo/VpkFile.cs: -------------------------------------------------------------------------------- 1 | using Domain.Core; 2 | using Infrastructure.Utility; 3 | using SharpVPK; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace Domain.ModLocalInfo 12 | { 13 | internal class VpkFile 14 | { 15 | private VpkArchive archive; 16 | private VpkEntry[] entries; 17 | private string fileName; 18 | 19 | public VpkFile(string file) 20 | { 21 | if (!File.Exists(file)) 22 | throw new FileNotFoundException(file); 23 | 24 | fileName = file; 25 | archive = new(); 26 | archive.Load(file); 27 | entries = archive.Directories.SelectMany(dir => dir.Entries).ToArray(); 28 | } 29 | 30 | public Maybe Image 31 | { 32 | get 33 | { 34 | // %USERNAME\AppData\Local\Temp\[VPK File]\ 35 | var path = Path.Combine(Path.GetTempPath(), Path.GetFileNameWithoutExtension(fileName)); 36 | return entries.Where(IsAddonImage).FirstElementSafe().Map(entry => SaveEntry(entry, path)).Map(fn => new ImageFile(fn)); 37 | } 38 | } 39 | 40 | public Maybe Info 41 | => entries.Where(IsAddonInfo).FirstElementSafe().Map(entry => AddonInfo.FromData(entry.Data)); 42 | 43 | public Category[] Categories 44 | { 45 | get 46 | { 47 | var rulegroup = new CategoryRuleGroupFactory().Create(); 48 | return entries.Select(FullName) 49 | .SelectMany(rulegroup.MatchCategories) 50 | .Distinct().Select(s => new Category(s)).ToArray(); 51 | } 52 | } 53 | 54 | /// 55 | /// 将VpkEntry保存为文件 56 | /// 57 | private static string SaveEntry(VpkEntry entry, string path = "") 58 | { 59 | if (!string.IsNullOrEmpty(path) && !Directory.Exists(path)) 60 | Directory.CreateDirectory(path); 61 | string fn = Path.Combine(path, $"{entry.Filename}.{entry.Extension}"); 62 | //string fn = Path.Combine(Path.GetTempPath(), fileName); 63 | File.WriteAllBytes(fn, entry.Data); 64 | return fn; 65 | } 66 | 67 | /// 68 | /// 该文件是addonimage 69 | /// 70 | private static bool IsAddonImage(VpkEntry entry) 71 | { 72 | return entry.Path.Equals(" ") && entry.Filename.ToLower().Equals("addonimage"); 73 | } 74 | 75 | /// 76 | /// 该文件是addoninfo 77 | /// 78 | private static bool IsAddonInfo(VpkEntry entry) 79 | { 80 | return entry.Path.Equals(" ") && entry.Filename.ToLower().Equals("addoninfo"); 81 | } 82 | 83 | private static string FullName(VpkEntry entry) 84 | { 85 | return $"{entry.Path}/{entry.Filename}.{entry.Extension}"; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/TaskFramework/BackgroundTaskList.cs: -------------------------------------------------------------------------------- 1 | using L4d2_Mod_Manager_Tool.DTO; 2 | using System; 3 | using System.Collections.Concurrent; 4 | using System.Threading; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace L4d2_Mod_Manager_Tool.TaskFramework 11 | { 12 | public class BackgroundTaskProgressChangedArgs : EventArgs 13 | { 14 | public BackgroundTaskProgress Progress { get; private set; } 15 | public BackgroundTaskProgressChangedArgs(BackgroundTaskProgress progress) 16 | => Progress = progress; 17 | } 18 | 19 | public class BackgroundTaskList 20 | { 21 | public event EventHandler OnBackgroundTaskAdded; 22 | public event EventHandler OnBackgroundTaskFinished; 23 | public event EventHandler OnBackgroundTaskChanged; 24 | private ConcurrentDictionary taskList = new (); 25 | private int id = 0; 26 | 27 | public void CancelTask(int id) 28 | { 29 | taskList[id].Cancel(); 30 | } 31 | 32 | public BackgroundTask NewTask(string name) 33 | { 34 | int newId = Interlocked.Increment (ref id); 35 | var bt = new BackgroundTask(newId) { Name = name, Progress = 0, Status = string.Empty }; 36 | bt.OnProgressChanged += BackgroundTask_OnProgressChanged; 37 | bt.OnFinished += BackgroundTask_OnFinished; 38 | 39 | taskList.TryAdd (newId, bt); 40 | 41 | BackgroundTaskProgress btp = DTO(bt); 42 | OnBackgroundTaskAdded?.Invoke(this, new BackgroundTaskProgressChangedArgs(btp)); 43 | return bt; 44 | } 45 | 46 | private void BackgroundTask_OnProgressChanged(object sender, EventArgs e) 47 | { 48 | var bt = sender as BackgroundTask; 49 | BackgroundTaskProgress btp = DTO(bt); 50 | OnBackgroundTaskChanged?.Invoke(this, new BackgroundTaskProgressChangedArgs(btp)); 51 | //Utility.WinformUtility.ErrorMessageBox(bt.Status + bt.Progress.ToString(), bt.Name); 52 | } 53 | 54 | private void BackgroundTask_OnFinished(object sender, EventArgs e) 55 | { 56 | // 结束的后台断开所有消息 57 | var bt = sender as BackgroundTask; 58 | bt.OnProgressChanged -= BackgroundTask_OnProgressChanged; 59 | bt.OnFinished -= BackgroundTask_OnFinished; 60 | // 从列表中移除 61 | taskList.TryRemove(bt.Id, out _); 62 | 63 | 64 | BackgroundTaskProgress btp = DTO(bt); 65 | OnBackgroundTaskFinished?.Invoke(this, new BackgroundTaskProgressChangedArgs(btp)); 66 | } 67 | 68 | private static BackgroundTaskProgress DTO(BackgroundTask task) 69 | => new() 70 | { 71 | Id = task.Id, 72 | Name = task.Name, 73 | Progress = task.Progress, 74 | Status = task.Status 75 | }; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/App/WorkshopInfoApplication.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Concurrent; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Domain.Core; 8 | using Domain.Core.WorkshopInfoModule; 9 | using Domain.ModFile; 10 | using L4d2_Mod_Manager_Tool.Service.AddonInfoDownload; 11 | using L4d2_Mod_Manager_Tool.TaskFramework; 12 | 13 | namespace L4d2_Mod_Manager_Tool.App 14 | { 15 | internal class WorkshopInfoApplication 16 | { 17 | private ModFileRepository modFileRepository; 18 | private WorkshopInfoRepository workshopInfoRepository; 19 | private AddonInfoDownloadService addonInfoDownloadService = new(); 20 | private BackgroundTaskList backgroundTaskList; 21 | public WorkshopInfoApplication(ModFileRepository mfRepo, WorkshopInfoRepository wiRepo, BackgroundTaskList backgroundTaskList) 22 | { 23 | modFileRepository = mfRepo; 24 | workshopInfoRepository = wiRepo; 25 | this.backgroundTaskList = backgroundTaskList; 26 | } 27 | public Task DownloadWorkshopInfoIfDontHaveAsync() 28 | { 29 | return Task.Run(() => 30 | { 31 | var allVpkIds = modFileRepository.GetAll().Where(x => x.VpkId != VpkId.Undefined).Select(x => x.VpkId); 32 | var savedVpkIds = workshopInfoRepository.GetAll().Select(x => x.Id); 33 | // 找到未记录的 34 | var dontHave = allVpkIds.Except(savedVpkIds).ToArray(); 35 | 36 | var cts = new CancellationTokenSource(); 37 | using var btask = backgroundTaskList.NewTask("下载创意工坊信息"); 38 | btask.OnCanceling += (sender, args) => cts.Cancel(); 39 | btask.Status = "正在初始化..."; 40 | btask.Progress = 0; 41 | btask.UpdateProgress(); 42 | 43 | 44 | ConcurrentBag workshopInfoList = new(); 45 | int finishedCount = 0, totalCount = dontHave.Length; 46 | foreach (var id in dontHave) 47 | { 48 | // 终止 49 | if (cts.Token.IsCancellationRequested) 50 | break; 51 | 52 | btask.Status = $"正在下载 {id.Id} ({finishedCount + 1} / {totalCount})"; 53 | btask.Progress = (int)(finishedCount * 100f / totalCount); 54 | btask.UpdateProgress(); 55 | 56 | var wi = addonInfoDownloadService.DownloadAddonInfo(id).ConfigureAwait(false).GetAwaiter().GetResult(); 57 | workshopInfoList.Add(wi.ValueOr(null)); 58 | Interlocked.Increment(ref finishedCount); 59 | } 60 | 61 | //var res = await addonInfoDownloadService.DownloadAddonInfosAsync(dontHave); 62 | 63 | workshopInfoRepository.SaveRange(workshopInfoList.Where(x => x != null)); 64 | }); 65 | } 66 | 67 | public string AddonInfoDownloadStretegyName 68 | => addonInfoDownloadService.CurrentDownloadStretegyName; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Domain/ModFile/ModFileRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Domain.Core; 7 | using Infrastructure; 8 | using Infrastructure.Utility; 9 | 10 | namespace Domain.ModFile 11 | { 12 | public delegate void OnModFilesChangedDel(IEnumerable modFile); 13 | public class ModFileRepository 14 | { 15 | private DapperHelper dapperHelper = DapperHelper.Instance; 16 | 17 | public event OnModFilesChangedDel OnModFilesAdded; 18 | 19 | public List SaveRange(IEnumerable mfs) 20 | { 21 | var conn = DapperHelper.OpenConnection(); 22 | var tran = conn.BeginTransaction(); 23 | 24 | var savedList = mfs.ToList().Select(mf => 25 | { 26 | var po = ModFile_DoToPo(mf); 27 | var id = tran.Insert(po); 28 | return mf with { Id = (int)id }; 29 | }).ToList(); 30 | 31 | tran.Dispose(); 32 | conn.Dispose(); 33 | 34 | OnModFilesAdded?.Invoke(savedList); 35 | return savedList; 36 | } 37 | 38 | public void Update(ModFile mf) 39 | { 40 | var po = ModFile_DoToPo(mf); 41 | dapperHelper.Update(po); 42 | OnModFilesAdded?.Invoke(FPExtension.Pure(mf)); 43 | } 44 | 45 | public ModFile FindById(int id) 46 | { 47 | var po = dapperHelper.Get(id); 48 | return ModFile_PoToDo(po); 49 | } 50 | 51 | public Maybe FindByVpkId(VpkId vpkId) 52 | { 53 | var po = dapperHelper.QueryFirst($"SELECT * FROM mod_file WHERE vpk_id={vpkId.Id}"); 54 | return po switch 55 | { 56 | null => Maybe.None, 57 | PO_ModFile something => ModFile_PoToDo(po) 58 | }; 59 | } 60 | 61 | public List GetAllNotLocalInfo() 62 | { 63 | var pos = dapperHelper.Query("SELECT * FROM mod_file WHERE localinfo_id=0"); 64 | return pos.Select(ModFile_PoToDo).ToList(); 65 | } 66 | 67 | public List GetAll() 68 | { 69 | var po = dapperHelper.GetAll(); 70 | return po.Select(ModFile_PoToDo).ToList(); 71 | } 72 | #region Persistent Object Stuff 73 | private static PO_ModFile ModFile_DoToPo(ModFile mf) 74 | { 75 | if(mf == null) 76 | return null; 77 | 78 | return new() 79 | { 80 | id = mf.Id, 81 | vpk_id = mf.VpkId.Id, 82 | file_name = mf.FileLoc, 83 | localinfo_id = mf.LocalinfoId 84 | }; 85 | } 86 | 87 | private static ModFile ModFile_PoToDo(PO_ModFile po) 88 | { 89 | if (po == null) 90 | return null; 91 | return new(po.id, new(po.vpk_id), po.file_name, po.localinfo_id); 92 | } 93 | #endregion 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Widget/Widget_ModOverview.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Data; 5 | using System.Drawing; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using System.Windows.Forms; 10 | 11 | namespace L4d2_Mod_Manager_Tool.Widget 12 | { 13 | public partial class Widget_ModOverview : UserControl 14 | { 15 | public Widget_ModOverview() 16 | { 17 | InitializeComponent(); 18 | SetupControl(); 19 | } 20 | 21 | private void SetupControl() 22 | { 23 | flowLayoutPanel1.HorizontalScroll.Maximum = 0; 24 | flowLayoutPanel1.AutoScroll = true; 25 | ShowModOverview = false; 26 | } 27 | 28 | public bool ShowModOverview 29 | { 30 | set 31 | { 32 | pictureBox1.Visible = value; 33 | flowLayoutPanel1.Visible = value; 34 | } 35 | } 36 | 37 | public string ModPreview 38 | { 39 | set 40 | { 41 | pictureBox1.Image = SelectImage(value); 42 | } 43 | } 44 | public string ModName 45 | { 46 | set 47 | { 48 | label_modName.Text = value; 49 | } 50 | } 51 | 52 | public string ModAuthor 53 | { 54 | set 55 | { 56 | label_author.Text = "作者: " + StringOr(value, "<未知>"); 57 | } 58 | } 59 | 60 | public string ModCategories 61 | { 62 | set 63 | { 64 | label_categories.Text = "分类: " + StringOr(value, "<无>"); 65 | } 66 | } 67 | 68 | public string ModTags 69 | { 70 | set 71 | { 72 | label_tags.Text = "标签: " + StringOr(value, "<无>"); 73 | } 74 | } 75 | 76 | public string ModDescript 77 | { 78 | set 79 | { 80 | label_descript.Text = value; 81 | } 82 | } 83 | 84 | private static string StringOr(string str, string defaultVal) 85 | { 86 | return string.IsNullOrEmpty(str) ? defaultVal : str; 87 | } 88 | 89 | /// 90 | /// 选择正确的图片,如果图片不存在或空使用空图片 91 | /// 92 | private static Image SelectImage(string img) 93 | { 94 | return LoadImageSafe(img).ValueOr( 95 | Image.FromFile(@"Resources/no-image.png")); 96 | } 97 | 98 | /// 99 | /// 安全地载入图片,如果图片不存在或有错返回maybe.none 100 | /// 101 | private static Utility.Maybe LoadImageSafe(string file) 102 | { 103 | try 104 | { 105 | return Image.FromFile(file); 106 | } 107 | catch 108 | { 109 | return Utility.Maybe.None; 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Form_RunningTask.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | text/microsoft-resx 50 | 51 | 52 | 2.0 53 | 54 | 55 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 56 | 57 | 58 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 59 | 60 | 61 | 17, 17 62 | 63 | -------------------------------------------------------------------------------- /SharpVPK/VpkEntry.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace SharpVPK 9 | { 10 | public class VpkEntry 11 | { 12 | public string Extension { get; set; } 13 | public string Path { get; set; } 14 | public string Filename { get; set; } 15 | public byte[] PreloadData { get { return ReadPreloadData(); }} 16 | public byte[] Data { get { return ReadData(); } } 17 | public bool HasPreloadData { get; set; } 18 | 19 | internal uint CRC; 20 | internal ushort PreloadBytes; 21 | internal uint PreloadDataOffset; 22 | internal ushort ArchiveIndex; 23 | internal uint EntryOffset; 24 | internal uint EntryLength; 25 | internal VpkArchive ParentArchive; 26 | 27 | internal VpkEntry(VpkArchive parentArchive, uint crc, ushort preloadBytes, uint preloadDataOffset, ushort archiveIndex, uint entryOffset, 28 | uint entryLength, string extension, string path, string filename) 29 | { 30 | ParentArchive = parentArchive; 31 | CRC = crc; 32 | PreloadBytes = preloadBytes; 33 | PreloadDataOffset = preloadDataOffset; 34 | ArchiveIndex = archiveIndex; 35 | EntryOffset = entryOffset; 36 | EntryLength = entryLength; 37 | Extension = extension; 38 | Path = path; 39 | Filename = filename; 40 | HasPreloadData = preloadBytes > 0; 41 | } 42 | 43 | public override string ToString() 44 | { 45 | return string.Concat(Path, "/", Filename, ".", Extension); 46 | } 47 | 48 | private byte[] ReadPreloadData() 49 | { 50 | if (PreloadBytes > 0) 51 | { 52 | var buff = new byte[PreloadBytes]; 53 | using (var fs = new FileStream(ParentArchive.ArchivePath, FileMode.Open, FileAccess.Read)) 54 | { 55 | buff = new byte[PreloadBytes]; 56 | fs.Seek(PreloadDataOffset, SeekOrigin.Begin); 57 | fs.Read(buff, 0, buff.Length); 58 | } 59 | return buff; 60 | } 61 | return null; 62 | } 63 | 64 | private byte[] ReadData() 65 | { 66 | string partFile = ParentArchive.ArchivePath; 67 | if (ArchiveIndex != 0x7fff) 68 | { 69 | var file = ParentArchive.Parts.FirstOrDefault(part => part.Index == ArchiveIndex); 70 | if (file == null) 71 | return null; 72 | partFile = file.Filename; 73 | } 74 | 75 | if (HasPreloadData) 76 | return ReadPreloadData(); 77 | var buff = new byte[EntryLength]; 78 | using (var fs = new FileStream(partFile, FileMode.Open, FileAccess.Read)) 79 | { 80 | fs.Seek((ArchiveIndex == 0x7fff ? ParentArchive.ArchiveOffset : 0) + EntryOffset, SeekOrigin.Begin); 81 | fs.Read(buff, 0, buff.Length); 82 | } 83 | return buff; 84 | } 85 | 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /SharpVPK/VpkReaderBase.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Runtime.InteropServices; 5 | using System.Text; 6 | 7 | namespace SharpVPK 8 | { 9 | internal abstract class VpkReaderBase 10 | { 11 | public BinaryReader Reader; 12 | private readonly StringBuilder strBuilder; 13 | 14 | protected VpkReaderBase(string filename) 15 | { 16 | Reader = new BinaryReader(new FileStream(filename, FileMode.Open, FileAccess.Read)); 17 | strBuilder = new StringBuilder(256); 18 | } 19 | 20 | public abstract IVpkArchiveHeader ReadArchiveHeader(); 21 | 22 | public string ReadNullTerminatedString() 23 | { 24 | strBuilder.Clear(); 25 | char chr; 26 | while ((chr = (char)Reader.ReadByte()) != 0x0) 27 | strBuilder.Append(chr); 28 | return strBuilder.ToString(); 29 | } 30 | 31 | protected T BytesToStructure(byte[] bytearray) 32 | { 33 | var structSize = Marshal.SizeOf(typeof (T)); 34 | var pStruct = Marshal.AllocHGlobal(structSize); 35 | Marshal.Copy(bytearray, 0, pStruct, structSize); 36 | var @struct = (T)Marshal.PtrToStructure(pStruct, typeof(T)); 37 | Marshal.FreeHGlobal(pStruct); 38 | 39 | return @struct; 40 | } 41 | 42 | public IEnumerable ReadDirectories(VpkArchive parentArchive) 43 | { 44 | while (true) 45 | { 46 | var ext = ReadNullTerminatedString(); 47 | if (string.IsNullOrEmpty(ext)) 48 | break; 49 | while (true) 50 | { 51 | var path = ReadNullTerminatedString(); 52 | if (string.IsNullOrEmpty(path)) 53 | break; 54 | 55 | var entries = ReadEntries(parentArchive, ext, path).ToList(); 56 | yield return new VpkDirectory(parentArchive, path, entries); 57 | } 58 | } 59 | } 60 | 61 | public IEnumerable ReadEntries(VpkArchive parentArchive, string ext, string path) 62 | { 63 | while (true) 64 | { 65 | var fileName = ReadNullTerminatedString(); 66 | if (string.IsNullOrEmpty(fileName)) 67 | break; 68 | 69 | var crc = Reader.ReadUInt32(); 70 | var preloadBytes = Reader.ReadUInt16(); 71 | var archiveIdx = Reader.ReadUInt16(); 72 | var entryOffset = Reader.ReadUInt32(); 73 | var entryLen = Reader.ReadUInt32(); 74 | // skip terminator 75 | if (Reader.ReadUInt16() != 0xFFFF) 76 | throw new System.Exception(); 77 | 78 | var preloadDataOffset = (uint)Reader.BaseStream.Position; 79 | if (preloadBytes > 0) 80 | Reader.BaseStream.Position += preloadBytes; 81 | 82 | yield return new VpkEntry(parentArchive, crc, preloadBytes, preloadDataOffset, archiveIdx, entryOffset, entryLen, ext, path, fileName); 83 | } 84 | } 85 | 86 | public abstract uint CalculateEntryOffset(uint offset); 87 | } 88 | } 89 | 90 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/L4d2_Mod_Manager_Tool.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WinExe 5 | net5.0-windows 6 | true 7 | 0.4.1.0 8 | 0.4.1.0 9 | saint_louis 10 | 11 | 求生之路2模组管理工具 12 | 13 | icon.ico 14 | 0.4.1 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | True 36 | True 37 | Resources.resx 38 | 39 | 40 | 41 | 42 | 43 | ResXFileCodeGenerator 44 | Resources.Designer.cs 45 | 46 | 47 | 48 | 49 | 50 | Always 51 | 52 | 53 | Always 54 | 55 | 56 | PreserveNewest 57 | 58 | 59 | PreserveNewest 60 | 61 | 62 | Always 63 | 64 | 65 | PreserveNewest 66 | 67 | 68 | Always 69 | 70 | 71 | PreserveNewest 72 | 73 | 74 | PreserveNewest 75 | 76 | 77 | PreserveNewest 78 | 79 | 80 | Always 81 | 82 | 83 | PreserveNewest 84 | 85 | 86 | PreserveNewest 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /Domain/Core/ModBriefModule/ModBriefList.cs: -------------------------------------------------------------------------------- 1 | using Domain.Core.ModStatusModule; 2 | using Domain.ModFile; 3 | using Domain.ModLocalInfo; 4 | using Domain.Core.WorkshopInfoModule; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System; 8 | 9 | namespace Domain.Core.ModBriefModule 10 | { 11 | /// 12 | /// 模组摘要信息列表 13 | /// 14 | public class ModBriefList 15 | { 16 | /// 17 | /// 模组摘要更新 18 | /// 19 | public event EventHandler OnBriefUpdate; 20 | 21 | private Dictionary modDetails = new(); 22 | private ModFileRepository mfRepo; 23 | private LocalInfoRepository liRepo; 24 | private WorkshopInfoRepository wiRepo; 25 | private AddonListRepository alRepo; 26 | 27 | public ModBriefList(ModFileRepository mfRepo, LocalInfoRepository liRepo, WorkshopInfoRepository wiRepo, AddonListRepository alRepo) 28 | { 29 | this.mfRepo = mfRepo; 30 | this.liRepo = liRepo; 31 | this.wiRepo = wiRepo; 32 | this.alRepo = alRepo; 33 | 34 | mfRepo.OnModFilesAdded += ModFileRepository_OnModFilesAdded; 35 | // localinfo更新会触发mf更新,不需要单独观察更改(除非考虑li单独更新,但是可以用li删除+重置) 36 | //liRepo.OnLocalInfosAdded += LocalInfoRepository_OnLocalInfosAdded; 37 | wiRepo.OnWorkshopInfoAdded += WorkshopInfoRepository_OnWorkshopInfoAdded; 38 | 39 | mfRepo.GetAll().ForEach(UpdateModFileInfo); 40 | } 41 | 42 | // 当【创意工坊信息】更新时,更新相关视图对象 43 | private void WorkshopInfoRepository_OnWorkshopInfoAdded(IEnumerable workshopInfos) 44 | { 45 | workshopInfos.Select(wi => mfRepo.FindByVpkId(wi.Id)).ToList() 46 | .ForEach(mf => mf.Map(UpdateModFileInfo)); 47 | OnBriefUpdate?.Invoke(this, null); 48 | } 49 | 50 | // 当【有新模组文件】时,更新相关视图对象 51 | private void ModFileRepository_OnModFilesAdded(IEnumerable mfs) 52 | { 53 | mfs.ToList().ForEach(UpdateModFileInfo); 54 | OnBriefUpdate?.Invoke(this, null); 55 | } 56 | 57 | /// 58 | /// 更新指定ModFile视图 59 | /// 60 | private void UpdateModFileInfo(ModFile.ModFile mf) 61 | { 62 | ModBrief md = new(mf.Id) 63 | { 64 | FileName = mf.FileLoc, 65 | VpkId = mf.VpkId, 66 | Enabled = alRepo[mf.FileLoc].ValueOr(false) 67 | }; 68 | 69 | if (mf.LocalinfoId > 0) 70 | { 71 | var li = liRepo.FindById(mf.LocalinfoId); 72 | if (li != null) 73 | { 74 | md.Author = li.Author; 75 | md.Name = li.Title; 76 | md.Tagline = li.Tagline; 77 | md.Categories = Category.Concat(li.Categories); 78 | } 79 | } 80 | 81 | wiRepo.GetByVpkId(mf.VpkId).Match(wi => 82 | { 83 | md.Name = string.IsNullOrEmpty(wi.Title) ? md.Name : wi.Title; 84 | md.Author = string.IsNullOrEmpty(wi.Autor) ? md.Author : wi.Autor; 85 | md.Tags = Tag.Concat(wi.Tags); 86 | }, () => { }); 87 | modDetails[md.Id] = md; 88 | } 89 | 90 | public ModBrief[] GetSpecified(ModBriefSpecification spec) 91 | { 92 | return modDetails.Values.Where(x => spec.IsSpecified(x)).ToArray(); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Widget/Widget_BackgroundTaskList.cs: -------------------------------------------------------------------------------- 1 | using L4d2_Mod_Manager_Tool.TaskFramework; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel; 5 | using System.Windows.Forms; 6 | 7 | namespace L4d2_Mod_Manager_Tool.Widget 8 | { 9 | public partial class Widget_BackgroundTaskList : UserControl 10 | { 11 | private BackgroundTaskList backgroundTaskList; 12 | BindingList _backgroundTaskListBinding = new(); 13 | 14 | public BackgroundTaskList BackgroundTaskList 15 | { 16 | set 17 | { 18 | OnBackgroundTaskListChanged(backgroundTaskList, value); 19 | backgroundTaskList = value; 20 | } 21 | } 22 | private Dictionary taskWidgets = new(); 23 | 24 | public Widget_BackgroundTaskList() 25 | { 26 | InitializeComponent(); 27 | } 28 | 29 | private void OnBackgroundTaskListChanged(BackgroundTaskList oldVal, BackgroundTaskList newVal) 30 | { 31 | if(oldVal != null) 32 | { 33 | oldVal.OnBackgroundTaskAdded -= BackgroundTaskList_OnBackgroundTaskAdded; 34 | oldVal.OnBackgroundTaskChanged -= BackgroundTaskList_OnBackgroundTaskChanged; 35 | oldVal.OnBackgroundTaskFinished -= BackgroundTaskList_OnBackgroundTaskChanged; 36 | } 37 | if(newVal != null) 38 | { 39 | newVal.OnBackgroundTaskAdded += BackgroundTaskList_OnBackgroundTaskAdded; 40 | newVal.OnBackgroundTaskChanged += BackgroundTaskList_OnBackgroundTaskChanged; 41 | newVal.OnBackgroundTaskFinished += BackgroundTaskList_OnBackgroundTaskFinished; 42 | } 43 | } 44 | 45 | private void BackgroundTaskList_OnBackgroundTaskAdded(object sender, BackgroundTaskProgressChangedArgs args) 46 | { 47 | int id = args.Progress.Id; 48 | Widget_BackgroundTask widget = new(); 49 | widget.OnTaskCancelRequested += (sender, args) => 50 | backgroundTaskList.CancelTask(id); 51 | 52 | taskWidgets.Add(id, widget); 53 | if (flowLayoutPanel1.InvokeRequired) 54 | flowLayoutPanel1.Invoke(new Action(() => flowLayoutPanel1.Controls.Add(widget))); 55 | } 56 | 57 | private void BackgroundTaskList_OnBackgroundTaskChanged(object sender, BackgroundTaskProgressChangedArgs args) 58 | { 59 | if (InvokeRequired) 60 | { 61 | BeginInvoke(new Action(() => 62 | { 63 | var widget = taskWidgets[args.Progress.Id]; 64 | widget.TaskName = args.Progress.Name; 65 | widget.TaskStatus = args.Progress.Status; 66 | widget.Progress = args.Progress.Progress; 67 | })); 68 | } 69 | } 70 | 71 | private void BackgroundTaskList_OnBackgroundTaskFinished(object sender, BackgroundTaskProgressChangedArgs args) 72 | { 73 | var widget = taskWidgets[args.Progress.Id]; 74 | 75 | if (flowLayoutPanel1.InvokeRequired) 76 | flowLayoutPanel1.Invoke(new Action(() => { 77 | flowLayoutPanel1.Controls.Remove(widget); 78 | widget.Dispose(); 79 | })); 80 | } 81 | 82 | private void button_hide_Click(object sender, EventArgs e) 83 | { 84 | Visible = false; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/ModFilter/ModFilterBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using L4d2_Mod_Manager_Tool.Domain.Specifications; 7 | 8 | namespace L4d2_Mod_Manager_Tool.Domain.ModFilter 9 | { 10 | /// 11 | /// 模组过滤器构建器 12 | /// 13 | class ModFilterBuilder 14 | { 15 | private string filterName = ""; 16 | public List Tags { get; private set; } = new(); 17 | public List Categories { get; private set; } = new(); 18 | public IModFilter FinalFilter 19 | { 20 | get => CreateNameFilter() 21 | .And(CreateTagsFilter()) 22 | .And(CreateCategoriesFilter()); 23 | } 24 | 25 | public ModDetailSpecification FinalSpec 26 | { 27 | get => CreateBlurSpec() 28 | .And(CreateTagsSpec()) 29 | .And(CreateCategoriesSpec()); 30 | } 31 | 32 | public void SetName(string name) 33 | { 34 | filterName = name ?? ""; 35 | } 36 | 37 | public void AddTag(string tag) 38 | { 39 | if (!Tags.Contains(tag)) 40 | Tags.Add(tag); 41 | } 42 | 43 | public void RemoveTag(string tag) 44 | { 45 | Tags.Remove(tag); 46 | } 47 | 48 | public void AddCategory(string cat) 49 | => Categories.Add(cat); 50 | 51 | public void RemoveCategory(string cat) 52 | => Categories.Remove(cat); 53 | 54 | private ModDetailSpecification CreateBlurSpec() 55 | { 56 | return string.IsNullOrEmpty(filterName) 57 | ? new MS_Empty() 58 | : new MS_Blur(filterName); 59 | } 60 | 61 | private IModFilter CreateNameFilter() 62 | { 63 | if (string.IsNullOrEmpty(filterName)) 64 | return new EmptyFilter(); 65 | else 66 | return new ModBlurFilter(filterName); 67 | } 68 | 69 | private ModDetailSpecification CreateTagsSpec() 70 | { 71 | if (Tags.Count == 0) 72 | { 73 | return new MS_Empty(); 74 | } 75 | else 76 | { 77 | return Tags.Select(x => (ModDetailSpecification)new MS_Tag(x)) 78 | .Aggregate((x, y) => x.And(y)); 79 | } 80 | } 81 | 82 | private IModFilter CreateTagsFilter() 83 | { 84 | if (Tags.Count == 0) 85 | { 86 | return new EmptyFilter(); 87 | } 88 | else 89 | { 90 | return 91 | Tags.Select(ModFP.HaveTag) 92 | .Select(x => (IModFilter)new PredicateModFilter(x)) 93 | .Aggregate((x1, x2) => new AndFilter(x1, x2)); 94 | } 95 | } 96 | 97 | private ModDetailSpecification CreateCategoriesSpec() 98 | { 99 | if (Categories.Count == 0) 100 | { 101 | return new MS_Empty(); 102 | } 103 | else 104 | { 105 | return Categories.Select(x => (ModDetailSpecification)new MS_Category(x)) 106 | .Aggregate((x, y) => x.And(y)); 107 | } 108 | } 109 | 110 | private IModFilter CreateCategoriesFilter() 111 | { 112 | if(Categories.Count == 0) 113 | { 114 | return new EmptyFilter(); 115 | } 116 | else 117 | { 118 | return 119 | Categories.Select(ModFP.HaveCategory) 120 | .Select(x => (IModFilter)new PredicateModFilter(x)) 121 | .Aggregate((x1, x2) => new AndFilter(x1, x2)); 122 | } 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Form_RunningTask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Data; 5 | using System.Drawing; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using System.Windows.Forms; 10 | using L4d2_Mod_Manager_Tool.TaskFramework; 11 | 12 | namespace L4d2_Mod_Manager_Tool 13 | { 14 | public partial class Form_RunningTask : Form 15 | { 16 | private IMessageTask[] tasks; 17 | public Form_RunningTask(string taskName, IMessageTask[] tasks) 18 | { 19 | InitializeComponent(); 20 | // 设置标题 21 | Text = taskName; 22 | this.tasks = tasks; 23 | 24 | // 开始任务 25 | backgroundWorker1.RunWorkerAsync(); 26 | } 27 | private void AppendMessageLine(string msg) 28 | { 29 | //stringBuilder.AppendLine(msg); 30 | //textBox_msg.Text = stringBuilder.ToString(); 31 | textBox_msg.AppendText(msg + Environment.NewLine); 32 | } 33 | 34 | private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) 35 | { 36 | var bgWorker = (BackgroundWorker)sender; 37 | 38 | if (tasks == null || tasks.Length == 0) 39 | { 40 | bgWorker.ReportProgress(100); 41 | return; 42 | } 43 | 44 | for (int i = 0; i < tasks.Length; ++i) 45 | { 46 | if (bgWorker.CancellationPending) 47 | { 48 | e.Cancel = true; 49 | break; 50 | } 51 | else 52 | { 53 | var curTask = tasks[i]; 54 | 55 | try 56 | { 57 | // 在多线程中调用其他UI会引起错误 58 | //AppendMessageLine(curTask.TaskName); 59 | 60 | curTask.DoTask(); 61 | }catch(Exception ex) 62 | { 63 | MessageBox.Show(ex.Message); 64 | } 65 | 66 | // 将消息通过ReportProgress转发出去 67 | int prog = Math.Clamp((int)((i + 1.0f) / tasks.Length * 100), 0, 100); 68 | string msg = $"({i + 1}/{tasks.Length}){curTask.TaskName}"; 69 | bgWorker.ReportProgress(prog, msg); 70 | } 71 | } 72 | } 73 | 74 | 75 | private void button1_Click(object sender, EventArgs e) 76 | { 77 | DialogResult = DialogResult.OK; 78 | } 79 | 80 | private void button2_Click(object sender, EventArgs e) 81 | { 82 | backgroundWorker1.CancelAsync(); 83 | } 84 | 85 | private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) 86 | { 87 | progressBar1.Value = e.ProgressPercentage; 88 | AppendMessageLine((string)e.UserState); 89 | } 90 | 91 | private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) 92 | { 93 | if (e.Cancelled) 94 | { 95 | AppendMessageLine("任务被取消"); 96 | progressBar1.Value = 100; 97 | } 98 | else 99 | { 100 | AppendMessageLine("任务已完成"); 101 | } 102 | 103 | button_cancel.Enabled = false; 104 | button_close.Enabled = true; 105 | } 106 | 107 | private void Form_RunningTask_FormClosing(object sender, FormClosingEventArgs e) 108 | { 109 | if (backgroundWorker1.IsBusy) 110 | { 111 | var res = MessageBox.Show("有任务正在进行,是否取消任务?", "中断任务", 112 | MessageBoxButtons.YesNo, MessageBoxIcon.Question); 113 | if(res == DialogResult.Yes) 114 | { 115 | backgroundWorker1.CancelAsync(); 116 | } 117 | e.Cancel = true; 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Widget/Widget_BackgroundTask.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace L4d2_Mod_Manager_Tool.Widget 2 | { 3 | partial class Widget_BackgroundTask 4 | { 5 | /// 6 | /// 必需的设计器变量。 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// 清理所有正在使用的资源。 12 | /// 13 | /// 如果应释放托管资源,为 true;否则为 false。 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region 组件设计器生成的代码 24 | 25 | /// 26 | /// 设计器支持所需的方法 - 不要修改 27 | /// 使用代码编辑器修改此方法的内容。 28 | /// 29 | private void InitializeComponent() 30 | { 31 | this.label_taskName = new System.Windows.Forms.Label(); 32 | this.progressBar_taskProgress = new System.Windows.Forms.ProgressBar(); 33 | this.label_status = new System.Windows.Forms.Label(); 34 | this.button_cancel = new System.Windows.Forms.Button(); 35 | this.SuspendLayout(); 36 | // 37 | // label_taskName 38 | // 39 | this.label_taskName.AutoEllipsis = true; 40 | this.label_taskName.Location = new System.Drawing.Point(7, 6); 41 | this.label_taskName.Name = "label_taskName"; 42 | this.label_taskName.Size = new System.Drawing.Size(152, 17); 43 | this.label_taskName.TabIndex = 0; 44 | this.label_taskName.Text = "正在进行的任务名称"; 45 | // 46 | // progressBar_taskProgress 47 | // 48 | this.progressBar_taskProgress.Location = new System.Drawing.Point(163, 9); 49 | this.progressBar_taskProgress.Name = "progressBar_taskProgress"; 50 | this.progressBar_taskProgress.Size = new System.Drawing.Size(152, 10); 51 | this.progressBar_taskProgress.TabIndex = 1; 52 | // 53 | // label_status 54 | // 55 | this.label_status.AutoEllipsis = true; 56 | this.label_status.Location = new System.Drawing.Point(7, 23); 57 | this.label_status.Name = "label_status"; 58 | this.label_status.Size = new System.Drawing.Size(308, 17); 59 | this.label_status.TabIndex = 2; 60 | this.label_status.Text = "具体正在进行的任务名称XXXXXXXXXXXXXXXXXXXXXX"; 61 | // 62 | // button_cancel 63 | // 64 | this.button_cancel.FlatAppearance.BorderSize = 0; 65 | this.button_cancel.FlatStyle = System.Windows.Forms.FlatStyle.Flat; 66 | this.button_cancel.Location = new System.Drawing.Point(334, 10); 67 | this.button_cancel.Name = "button_cancel"; 68 | this.button_cancel.Size = new System.Drawing.Size(24, 23); 69 | this.button_cancel.TabIndex = 3; 70 | this.button_cancel.Text = "X"; 71 | this.button_cancel.UseVisualStyleBackColor = false; 72 | this.button_cancel.Click += new System.EventHandler(this.button_cancel_Click); 73 | // 74 | // Widget_BackgroundTask 75 | // 76 | this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 17F); 77 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 78 | this.Controls.Add(this.button_cancel); 79 | this.Controls.Add(this.label_status); 80 | this.Controls.Add(this.progressBar_taskProgress); 81 | this.Controls.Add(this.label_taskName); 82 | this.Name = "Widget_BackgroundTask"; 83 | this.Size = new System.Drawing.Size(369, 47); 84 | this.ResumeLayout(false); 85 | 86 | } 87 | 88 | #endregion 89 | 90 | private System.Windows.Forms.Label label_taskName; 91 | private System.Windows.Forms.ProgressBar progressBar_taskProgress; 92 | private System.Windows.Forms.Label label_status; 93 | private System.Windows.Forms.Button button_cancel; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![img](img/banner.png) 2 | # Left 4 Dead 2 Mod Manage Tool -- 求生之路2模组管理工具 3 | 求生之路2模组管理工具(以下简称MMT)是用于管理求生之路2模组的工具集。MMT主要解决模组管理困难问题,最主要的,在模组多的情况下查找和开启/关闭模组困难的问题。此外,许多玩家会刻意将模组移动至本地目录,MMT也解决移动模组后,模组信息不全的问题。 4 | 5 | ## Installation - 安装 6 | ### Requirement - 必要条件 7 | * Windows 7/10 64位 8 | * .NET 5.0.7 或更高 9 | * [no_vtf](https://sr.ht/~b5327157/no_vtf/) 10 | 11 | ## Usage - 用法 12 | ### 设置 13 | #### 游戏位置 14 | 打开菜单"工具->选项",在弹出的选项菜单中,找到"模组存储位置管理",单击"选择游戏位置"按钮来选择求生之路2游戏文件夹位置,软件会自动扫描模组文件夹。 15 | 16 | ![img](img/menu_option.jpg) 17 | 18 | ![img](img/option_dialog.jpg) 19 | 20 | 设置好文件夹后,单击选项面板的"确定"按钮保存设置。 21 | 22 | #### no_vtf可执行程序 23 | 软件默认自带一个,如有需要可以修改no_vtf可执行程序。 24 | 25 | ### 扫描模组文件 26 | 选择菜单"工具->扫描模组文件",软件从所有模组文件夹中找到vpk文件,解包读取信息,保存到软件数据库中。 27 | 28 | ### 搜索模组文件 29 | 可以使用搜索栏对模组进行模糊查询,支持通过模组名称、作者、VPKID进行查找。 30 | 31 | 使用搜索栏边的过滤按钮,按照标签进行过滤,标签需要下载创意工坊信息获取。可以点击已经添加的标签来去除过滤标签。 32 | 33 | ### 打开模组文件 34 | 在主界面的模型列表中对任意模组记录右键,在弹出的菜单选择"在文件管理器中显示",软件将会打开资源管理器并选择该模组文件。 35 | 36 | 你也可以直接双击模组记录,软件将调取合适的应用程序打开模组文件(一般为GCFScape)。 37 | 38 | ### 启用/关闭模组 39 | MMT通过读取`addonlist.txt`来获取模组启用状态。你可以在模组列表中选择一个或者多个模组,在右键菜单中选择“开启模组”或“关闭模组”来启用/关闭指定的模组。 40 | 41 | ![img](img/modenabler.jpg) 42 | 43 | ### 下载创意工坊信息 44 | 如果本地信息较少,可以从创意工坊获取更详细的模组信息,但是必须满足条件: 45 | 46 | * 模组是从workshop迁移的,并且没有重命名模组文件(默认从创意工坊下载的模组,模组名是"[VPKID].vpk")。 47 | 48 | ~~软件从模组文件名称获取VPKID,使用爬虫在线获取模组信息,记录到本地数据库。之后,模组列表显示的缩略图和名称改为创意工坊的缩略图和名称。~~ 49 | 50 | MMT支持两种获取创意工坊信息的模式,分别为`网络爬虫模式`和`SteamWorks模式`。你可以在软件左下角的状态栏中看到当前软件使用的模式。 51 | 52 | ![img](img/modinfo_strategyStatus.jpg) 53 | 54 | **网络爬虫模式** 55 | 该模式使用网页下载模组的创意工坊信息。该模式速度较慢,不需要登录steam客户端,在无法连接到Steam创意工坊的地区需要梯子才能下载信息。 56 | 57 | **SteamWorks模式** 58 | 该模式使用 Steam API 下载模组的创意工坊信息。该模式速度较快,但是需要先登录steam客户端,任何能正常登录steam客户端的地区都可以直接下载模组信息。 59 | 60 | **关于软件选择模式的策略** 61 | MMT在启动时优先选择`SteamWorks模式`,如果steam客户端未启动并登录,自动降级到`网络爬虫模式`。 62 | 63 | ### 模组排序 64 | MMT支持对模组列表进行排序,通过单击模组列表的表头,可以通过名称、文件名、状态和作者进行排序。表头名后面的图标示意当前排序策略,排序支持从小到大排序和从大到小排序,分别使用符号 ▲和▼表示。 65 | 66 | ![img](img/sort_header.jpg) 67 | 68 | ## Develop -- 开发 69 | ### Dependency - 依赖 70 | 71 | * Visual Studio 2017 / 2019 72 | * .NET 5.0 Desktop 73 | * Newtonsoft.Json 13.0.1 74 | * System.Data.SQLite 1.0.116 75 | * HtmlAgilitypack 1.11.43 76 | * Facepunch.Steamworks 2.3.3 77 | * [no_vtf](https://sr.ht/~b5327157/no_vtf/) 78 | 79 | ### 设置no_vtf 80 | 首先从 https://sr.ht/~b5327157/no_vtf/ 上下载no_vtf,将文件夹解压至编译好的可执行程序目录,更名为"no_vtf-windows_x64"即可。 81 | 82 | ## Further Feature - 计划特性 83 | ### [已完成] 模组详细信息 84 | 查看模组的本地信息和创意工坊信息 85 | 本地信息包括: 86 | * 缩略图(addonimage) 87 | * 标题(addontitle) 88 | * 版本(addonversion) 89 | * 标语(addontagline) 90 | * 作者(addonauthor) 91 | * 描述(addonDescription) 92 | 93 | 创意工坊信息包括: 94 | * 预览图(previewImageMain) 95 | * 标题(workshopItemTitle) 96 | * 描述(workshopItemDescriptionTitle) 97 | * 标签(workshopTags) 98 | 99 | ### [已完成] 分类过滤-依照标签 100 | 增加对模组的分类过滤功能,通过创意工坊的多标签分类快速筛选模组。 101 | 102 | ### [已完成]分类过滤-依照内容 103 | 通过模组内容来对模组进行过滤。 104 | 105 | ### [已完成]模组启动/关闭 106 | 在软件中批量对模组开关,加快游戏载入速度,降低模组管理难度。 107 | 108 | ### [已完成]SteamAPI支持 109 | 通过SteamAPI来管理模组,可以免除梯子以及更快地获取模组信息,但是需要steam启动支持。 110 | 111 | ### 模组别名和自定义标签 112 | 为每个模组增加别名和自定义标签,为后续的分类和搜索功能做好准备。 113 | 114 | ### 在线模组自动备份 115 | 自动备份在线模组,模组丢失检测和还原丢失模组。 116 | ==> 解决关键时刻(指联机时)起不来(指模组莫名其妙重下)的尴尬和烦恼 117 | 118 | ### [需要可行性分析] 本地化订阅的模组 119 | 区分在线模组与本地模组,提供一键移动在线模组到本地,并取消订阅在线模组功能。 120 | ==> 方便珍藏~~老婆~~模组 121 | 122 | ### [需要可行性分析] 更新本地化的模组 123 | 上接[本地化订阅的模组](#本地化订阅的模组),即使本地化了,也能及时更新模组,并且不会删除下架模组。 124 | ==> 老婆获得了永生 125 | 126 | ### [需要可行性分析] VPKID批量复制、订阅 127 | 向基友分享模组时,选择模组,复制VPKID发送给基友,基友通过ID批量订阅。 128 | ==> 自个儿下去,我QQ离线流量用完了 129 | 130 | ## [开发轶事] 131 | 记录一些开发过程中的奇妙事宜 132 | ### 本地的“幽灵”模组 133 | 我在做模组启动/关闭支持特性的测试时发现一个奇怪的现象,我先用MMT关闭了所有模组,接着查看addonlist.txt,对着一排的"0"感到非常满意后,照例启动求生开始测试。 134 | 在附加内容中,我浏览了模组列表,查看有没有“漏网之鱼”,最后点击完成,退出游戏,全程一气呵成,纵享丝滑。随后我再次打开MMT查看模组,然后Emmmmmmm? 135 | 136 | ![img](img/yishi1_1.jpg) 137 | 138 | 有些模组怎么自己启动了?难道有BUG?带着疑问我又去试了几把,总结了一些规律: 139 | 140 | 1. 确实有模组被自动开启了,而且和MMT无关 141 | 1. 被开启的模组都是addons里的,workshop中的模组没有这个现象 142 | 1. 求生在启动软件时会修改一次addonlist.txt,在打开附加内容中点击完成后会再修改一次 143 | 1. 启动软件时对addonlist.txt的修改仅限于在末尾增加列表中不存在的新模组 144 | 1. 在附加内容中点击完成后,一些模组就被开启了 145 | 146 | 然后我就对着列表一个个模组查看了过去,问题很快就查出来了,只要是本地模组,模组不包含addoninfo.txt的,都会被自动开启!而且在求生的模组列表里是看不到变化的,难怪之前打图,煤气罐被半条命的替换成不能爆炸的模型,气得把所有模组都关了都解决不了,最后还是靠移除vpk才搞定... 147 | 148 | 总之,只要模组缺失addoninfo.txt,又移动到本地文件夹,就会变成找不到又关不掉的“幽灵”模组。解决办法呢?你可以直接把vpk移除,或者去改addonlist.txt(记得不要去点附加内容),当然,也可以用MMT关闭(记得不要去点附加内容)。 -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Service/AddonInfoDownload/SteamworksAddonInfoDownloadStrategy.cs: -------------------------------------------------------------------------------- 1 | using Infrastructure.Utility; 2 | using L4d2_Mod_Manager_Tool.Domain; 3 | using Steamworks; 4 | using Steamworks.Ugc; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Collections.Immutable; 8 | using System.IO; 9 | using System.Linq; 10 | using System.Net; 11 | using System.Text; 12 | using System.Threading.Tasks; 13 | 14 | namespace L4d2_Mod_Manager_Tool.Service.AddonInfoDownload 15 | { 16 | class SteamworksAddonInfoDownloadStrategy : IAddonInfoDownloadStrategy 17 | { 18 | 19 | public static Maybe CreateStrategy() 20 | { 21 | try 22 | { 23 | return Maybe.Some(new SteamworksAddonInfoDownloadStrategy()); 24 | } 25 | catch 26 | { 27 | return Maybe.None; 28 | } 29 | } 30 | 31 | public const int AppID = 550; 32 | 33 | public string StrategyName => "SteamWorks模式"; 34 | 35 | private SteamworksAddonInfoDownloadStrategy() 36 | { 37 | SteamClient.Init(550); 38 | } 39 | 40 | ~SteamworksAddonInfoDownloadStrategy() 41 | { 42 | SteamClient.Shutdown(); 43 | } 44 | 45 | 46 | public async Task> DownloadAddonInfoAsync(ulong vpkid) 47 | { 48 | try 49 | { 50 | ResultPage? result = await Query.All.WithFileId(vpkid).GetPageAsync(1).ConfigureAwait(false); 51 | if (!result.HasValue || result.Value.ResultCount != 1) 52 | { 53 | return Maybe.None; 54 | } 55 | 56 | Item item = result.Value.Entries.First(); 57 | result.Value.Dispose(); 58 | 59 | //var res = await SteamUGC.QueryFileAsync(new() { Value = vpkid }); 60 | var f = DownloadImageFromURL(item.PreviewImageUrl); 61 | 62 | var owner = item.Owner; 63 | // 下载用户名称 64 | await owner.RequestInfoAsync(); 65 | var author = item.Owner.Name; 66 | return Maybe.Some(new ModWorkshopInfo(author, item.Title, item.Description, f, item.Tags.ToImmutableArray())); 67 | } 68 | catch (Exception e) 69 | { 70 | System.Windows.Forms.MessageBox.Show(e.Message); 71 | return Maybe.None; 72 | } 73 | } 74 | 75 | /// 76 | /// 从URL中下载图片,返回图片的本地路径 77 | /// 78 | /// 下载图片的本地路径 79 | private static string DownloadImageFromURL(string url) 80 | { 81 | string fileName = Path.GetTempFileName(); 82 | try 83 | { 84 | HttpWebRequest previewImgReq = (HttpWebRequest)WebRequest.Create(url); 85 | var previewImgRes = previewImgReq.GetResponse(); 86 | 87 | var imageType = GetImageTypeFromContentType(previewImgRes.ContentType); 88 | fileName = Path.ChangeExtension(fileName, "." + imageType); 89 | 90 | using var previewImgStream = previewImgRes.GetResponseStream(); 91 | using FileStream fs = new FileStream(fileName, FileMode.OpenOrCreate); 92 | byte[] buf = new byte[1024]; 93 | while (true) 94 | { 95 | var len = previewImgStream.Read(buf, 0, buf.Length); 96 | if (len > 0) 97 | { 98 | fs.Write(buf, 0, len); 99 | } 100 | else 101 | { 102 | break; 103 | } 104 | } 105 | return fileName; 106 | } 107 | catch 108 | { 109 | return ""; 110 | } 111 | } 112 | 113 | 114 | private static string GetImageTypeFromContentType(string str) 115 | { 116 | if (str.StartsWith("image/")) 117 | { 118 | //截断 "image/" 字符串 119 | return str.Substring(6); 120 | } 121 | else 122 | { 123 | throw new Exception("Unsupported Content Type:" + str); 124 | } 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Widget/Widget_BackgroundTaskList.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace L4d2_Mod_Manager_Tool.Widget 2 | { 3 | partial class Widget_BackgroundTaskList 4 | { 5 | /// 6 | /// 必需的设计器变量。 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// 清理所有正在使用的资源。 12 | /// 13 | /// 如果应释放托管资源,为 true;否则为 false。 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region 组件设计器生成的代码 24 | 25 | /// 26 | /// 设计器支持所需的方法 - 不要修改 27 | /// 使用代码编辑器修改此方法的内容。 28 | /// 29 | private void InitializeComponent() 30 | { 31 | this.flowLayoutPanel1 = new System.Windows.Forms.FlowLayoutPanel(); 32 | this.label1 = new System.Windows.Forms.Label(); 33 | this.button_hide = new System.Windows.Forms.Button(); 34 | this.panel1 = new System.Windows.Forms.Panel(); 35 | this.panel1.SuspendLayout(); 36 | this.SuspendLayout(); 37 | // 38 | // flowLayoutPanel1 39 | // 40 | this.flowLayoutPanel1.AutoScroll = true; 41 | this.flowLayoutPanel1.Dock = System.Windows.Forms.DockStyle.Fill; 42 | this.flowLayoutPanel1.Location = new System.Drawing.Point(0, 31); 43 | this.flowLayoutPanel1.Name = "flowLayoutPanel1"; 44 | this.flowLayoutPanel1.Size = new System.Drawing.Size(392, 106); 45 | this.flowLayoutPanel1.TabIndex = 0; 46 | // 47 | // label1 48 | // 49 | this.label1.Location = new System.Drawing.Point(3, 0); 50 | this.label1.Name = "label1"; 51 | this.label1.Size = new System.Drawing.Size(56, 30); 52 | this.label1.TabIndex = 0; 53 | this.label1.Text = "后台任务"; 54 | this.label1.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; 55 | // 56 | // button_hide 57 | // 58 | this.button_hide.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); 59 | this.button_hide.FlatAppearance.BorderSize = 0; 60 | this.button_hide.FlatStyle = System.Windows.Forms.FlatStyle.Flat; 61 | this.button_hide.Location = new System.Drawing.Point(346, 3); 62 | this.button_hide.Name = "button_hide"; 63 | this.button_hide.Size = new System.Drawing.Size(43, 25); 64 | this.button_hide.TabIndex = 1; 65 | this.button_hide.Text = "隐藏"; 66 | this.button_hide.UseVisualStyleBackColor = true; 67 | this.button_hide.Click += new System.EventHandler(this.button_hide_Click); 68 | // 69 | // panel1 70 | // 71 | this.panel1.Controls.Add(this.button_hide); 72 | this.panel1.Controls.Add(this.label1); 73 | this.panel1.Dock = System.Windows.Forms.DockStyle.Top; 74 | this.panel1.Location = new System.Drawing.Point(0, 0); 75 | this.panel1.Name = "panel1"; 76 | this.panel1.Size = new System.Drawing.Size(392, 31); 77 | this.panel1.TabIndex = 2; 78 | // 79 | // Widget_BackgroundTaskList 80 | // 81 | this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 17F); 82 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 83 | this.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; 84 | this.Controls.Add(this.flowLayoutPanel1); 85 | this.Controls.Add(this.panel1); 86 | this.Name = "Widget_BackgroundTaskList"; 87 | this.Size = new System.Drawing.Size(392, 137); 88 | this.panel1.ResumeLayout(false); 89 | this.ResumeLayout(false); 90 | 91 | } 92 | 93 | #endregion 94 | 95 | private System.Windows.Forms.FlowLayoutPanel flowLayoutPanel1; 96 | private System.Windows.Forms.Label label1; 97 | private System.Windows.Forms.Button button_hide; 98 | private System.Windows.Forms.Panel panel1; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Domain/Core/ModStatusModule/AddonListRepository.cs: -------------------------------------------------------------------------------- 1 | using Infrastructure.Utility; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Text.RegularExpressions; 8 | using System.Threading.Tasks; 9 | 10 | namespace Domain.Core.ModStatusModule 11 | { 12 | public class AddonListRepository 13 | { 14 | private Dictionary status = new(); 15 | 16 | public AddonListRepository() 17 | { 18 | Load(); 19 | } 20 | 21 | public Maybe this [string mod] 22 | => GetModStatus(mod); 23 | 24 | public Maybe GetModStatus(string mod) 25 | => status.ContainsKey(mod) ? status[mod] : Maybe.None; 26 | 27 | public void SetModStatus(string mod, bool b) 28 | => status[mod] = b; 29 | 30 | #region 读取/写入数据 31 | 32 | /// 33 | /// 读取addonlist文件 34 | /// 35 | public void Load() 36 | { 37 | status.Clear(); 38 | var als = ParseAddonList(ReadAddonListContent()); 39 | als.ForEach(x => status.Add(x.Item1, x.Item2)); 40 | } 41 | 42 | private static string AddonListFilePath 43 | => Path.Combine(L4d2Folder.CoreFolder, "addonlist.txt"); 44 | 45 | /// 46 | /// 读取模组文件列表内容 47 | /// 48 | private static string ReadAddonListContent() 49 | { 50 | string content = ""; 51 | var mf = AddonListFilePath; 52 | if (!File.Exists(AddonListFilePath)) 53 | return content; 54 | 55 | Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); 56 | using (var sr = new StreamReader(mf, Encoding.GetEncoding("GB2312"), true)) 57 | content = sr.ReadToEnd(); 58 | return content; 59 | } 60 | 61 | /// 62 | /// 从addonlist中解析模组信息 63 | /// 64 | private static List<(string, bool)> ParseAddonList(string content) 65 | { 66 | List<(string, bool)> addonLists = new(); 67 | string contentLex = @"\{([.\S\s]+)\}"; 68 | var contentMatch = Regex.Match(content, contentLex); 69 | var lex = @"""(.+?)"""; 70 | var matches = Regex.Matches(content, lex); 71 | 72 | var words = matches.Select(match => match.Groups[1].Value).ToArray(); 73 | if (words.Length == 0) 74 | return addonLists; 75 | 76 | if (!words[0].Equals("AddonList")) 77 | return addonLists; 78 | 79 | for (int i = 2; i < words.Length; i += 2) 80 | { 81 | addonLists.Add(new(words[i - 1], int.Parse(words[i]) == 1)); 82 | } 83 | return addonLists; 84 | } 85 | 86 | 87 | /// 88 | /// 保存修改到addonlist文件 89 | /// 90 | public void Save() 91 | { 92 | ApplyAddonList(); 93 | } 94 | 95 | private void ApplyAddonList() 96 | { 97 | StringBuilder stringBuilder = new(); 98 | stringBuilder.AppendLine("\"AddonList\"").AppendLine("{"); 99 | foreach(var pair in status) 100 | { 101 | var enableStr = pair.Value ? "1" : "0"; 102 | stringBuilder.AppendLine($"\t\"{pair.Key}\"\t\t\"{enableStr}\""); 103 | } 104 | stringBuilder.AppendLine("}"); 105 | 106 | WriteAddonListContent(stringBuilder.ToString()); 107 | } 108 | 109 | private static void WriteAddonListContent(string content) 110 | { 111 | var mf = AddonListFilePath; 112 | string tmpfile = Path.GetTempFileName(); 113 | Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); 114 | try 115 | { 116 | var writer = new StreamWriter(tmpfile, false, Encoding.GetEncoding("GB2312")); 117 | writer.Write(content); 118 | writer.Dispose(); 119 | // 备份,然后覆盖 120 | BackupAddonList(); 121 | File.Move(tmpfile, mf, true); 122 | } 123 | catch 124 | { 125 | throw; 126 | } 127 | } 128 | 129 | /// 130 | /// 备份目前的清单 131 | /// 132 | private static void BackupAddonList() 133 | { 134 | var mf = AddonListFilePath; 135 | File.Copy(mf, mf + ".bak", true); 136 | } 137 | #endregion 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /L4d2_Mod_Manager_Tool/Domain/Mod.cs: -------------------------------------------------------------------------------- 1 | using L4d2_Mod_Manager_Tool.Utility; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Collections.Immutable; 5 | using System.Diagnostics.CodeAnalysis; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace L4d2_Mod_Manager_Tool.Domain 11 | { 12 | public record Mod( 13 | int Id, 14 | int FileId, 15 | string vpkId, 16 | string Thumbnail, 17 | string Title, 18 | string Version, 19 | string Tagline, 20 | string Author, 21 | string Description, 22 | ImmutableArray Categories, 23 | string WorkshopTitle, 24 | string WorkshopDescript, 25 | string WorkshopPreviewImage, 26 | ImmutableArray Tags 27 | ); 28 | 29 | public static class ModFP 30 | { 31 | public static Mod CreateMod(ModFile f) 32 | { 33 | var vpkNum = GetVpkNumberFromFileName(f.FilePath); 34 | return new Mod(-1, f.Id, vpkNum.ValueOr(null), 35 | null, null, null, null, null, null, ImmutableArray.Empty, 36 | null, null, null, ImmutableArray.Empty); 37 | } 38 | 39 | public static bool HaveVpkNumber(Mod mod) => !string.IsNullOrEmpty(mod.vpkId); 40 | 41 | public static string CategoriesSingleLine(this Mod mod) => 42 | string.Join(',', mod.Categories); 43 | 44 | public static string TagsSingleLine(this Mod mod) => 45 | string.Join(',', mod.Tags); 46 | 47 | /// 48 | /// 从VPK名称上获取VPK号 49 | /// 50 | public static Maybe GetVpkNumberFromFileName(string file) 51 | { 52 | var name = System.IO.Path.GetFileNameWithoutExtension(file); 53 | return Service.Spider.IsVpkNumber(name) ? 54 | Maybe.Some(name) : Maybe.None; 55 | } 56 | 57 | /// 58 | /// 模组信息没有创意工坊信息 59 | /// 60 | public static bool HaveWorkshopInfo(Mod mod) 61 | { 62 | return 63 | !string.IsNullOrEmpty(mod.WorkshopTitle) 64 | || !string.IsNullOrEmpty(mod.WorkshopDescript) 65 | || !string.IsNullOrEmpty(mod.WorkshopPreviewImage); 66 | } 67 | 68 | public static string SelectName(Mod mod) 69 | { 70 | if (!string.IsNullOrEmpty(mod.WorkshopTitle)) 71 | return mod.WorkshopTitle; 72 | else if (!string.IsNullOrEmpty(mod.Title)) 73 | return mod.Title; 74 | else 75 | return "<无名称>"; 76 | } 77 | 78 | public static string SelectDescription(Mod mod) 79 | { 80 | if (!string.IsNullOrEmpty(mod.WorkshopDescript)) 81 | return mod.WorkshopDescript; 82 | else 83 | return mod.Description; 84 | } 85 | 86 | public static string SelectPreview(Mod mod) 87 | { 88 | if (!string.IsNullOrEmpty(mod.WorkshopPreviewImage)) 89 | return mod.WorkshopPreviewImage; 90 | else 91 | return mod.Thumbnail; 92 | } 93 | 94 | /// 95 | /// 谓词构造 - 包含指定标签 96 | /// 97 | public static Func HaveTag(string tagName) 98 | { 99 | return m => m.Tags.Contains(tagName.ToLower()); 100 | } 101 | 102 | /// 103 | /// 谓词构造 - 包含分类 104 | /// 105 | public static Func HaveCategory(string catName) 106 | { 107 | return m => m.Categories.Contains(catName); 108 | } 109 | 110 | public static Func NameContains(string name) 111 | { 112 | return m => 113 | m.Title.Contains(name, StringComparison.CurrentCultureIgnoreCase) 114 | || m.WorkshopTitle.Contains(name, StringComparison.CurrentCultureIgnoreCase); 115 | } 116 | 117 | public static Func AuthorContains(string name) 118 | { 119 | return m => m.Author.Contains(name, StringComparison.CurrentCultureIgnoreCase); 120 | } 121 | 122 | public static Func VPKIdContains(string name) 123 | { 124 | return m => m.vpkId.Contains(name, StringComparison.CurrentCultureIgnoreCase); 125 | } 126 | } 127 | 128 | public class ModEqualityComparer : IEqualityComparer 129 | { 130 | public bool Equals(Mod x, Mod y) 131 | { 132 | return x.Id == y.Id; 133 | } 134 | 135 | public int GetHashCode([DisallowNull] Mod obj) 136 | { 137 | return obj.Id; 138 | } 139 | } 140 | } 141 | --------------------------------------------------------------------------------