├── 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 | 
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 | 
17 |
18 | 
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 | 
42 |
43 | ### 下载创意工坊信息
44 | 如果本地信息较少,可以从创意工坊获取更详细的模组信息,但是必须满足条件:
45 |
46 | * 模组是从workshop迁移的,并且没有重命名模组文件(默认从创意工坊下载的模组,模组名是"[VPKID].vpk")。
47 |
48 | ~~软件从模组文件名称获取VPKID,使用爬虫在线获取模组信息,记录到本地数据库。之后,模组列表显示的缩略图和名称改为创意工坊的缩略图和名称。~~
49 |
50 | MMT支持两种获取创意工坊信息的模式,分别为`网络爬虫模式`和`SteamWorks模式`。你可以在软件左下角的状态栏中看到当前软件使用的模式。
51 |
52 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------