游戏工具(GameTools)
游戏工具(GameTools)是 Watt Toolkit(SteamTools)桌面客户端中的一个标准插件模块(BD.WTTS.Client.Plugins.GameTools),通过 MEF(CompositionExport)被宿主程序自动发现并装配,向主界面注册"游戏相关"菜单页,并在该页面中以卡片形式聚合若干与游戏相关的实用小工具(游戏无边框窗口化、CS:GO VAC 修复、CPU 游戏调度优化等),这些工具大多仅限 Windows 平台使用。
目的与范围(Purpose and Scope)
本页覆盖 GameTools 插件作为一个整体 的完整机制:
- 插件程序集的组成与文件结构(
src/BD.WTTS.Client.Plugins.GameTools/); - 插件的注册与发现方式(
Plugin类、MEFCompositionExport、PluginBase<T>/IPlugin契约); - 主页面
GameToolsPage的卡片式导航布局、平台可用性控制与页面导航分发方式; - 插件元数据(Id、名称、描述、图标、版本)如何驱动
PageBase的标题/副标题/预览图渲染。
以下内容有意留给兄弟页面,本页不展开其内部实现:
- 无边框窗口化的窗口处理细节 → 参见
Extensions/HandleWindowExtensions.cs与UI/Views/BorderlessGamePage.axaml.cs、UI/ViewModels/BorderlessGamePageViewModel.cs(若目录中存在对应专题页,请参考该页); - CS:GO VAC 修复的具体修复逻辑 → 参见
UI/ViewModels/CsgoVacRepairPageViewModel.cs(对应专题页); - 插件系统本身的通用机制(
IPlugin、PluginBase<T>、MEF 装配管线)→ 参见宿主层插件框架相关页面。
概述(Overview)
Watt Toolkit 采用"核心 + 插件"的架构,每个功能域(账号切换、网络加速、游戏工具等)都是独立的 BD.WTTS.Client.Plugins.* 程序集。GameTools 插件的职责非常纯粹:
- 声明自身为一个插件:
Plugin类继承PluginBase<Plugin>并实现IPlugin,通过#if (WINDOWS || MACCATALYST || MACOS || LINUX) && !(IOS || ANDROID)条件编译 +[CompositionExport(typeof(IPlugin))]]暴露给 MEF 容器,仅在桌面端生效。 - 向主菜单贡献一个入口:重写
GetMenuTabItems(),返回一个MenuTabItemViewModel,其PageType = typeof(GameToolsPage),IconKey = Icon,IsResourceGet = true(菜单标题使用资源字符串Strings.GameRelated,即"游戏相关")。 - 提供工具聚合页:
GameToolsPage是一个PageBase派生页,页头元数据直接绑定到Plugin.Instance(标题 = Name、描述 = Description、副标题 = Author 格式化字符串、预览图 = Icon 经BitmapAssetValueConverter转换),页面主体是一个WrapPanel+ 卡片按钮网格。
页面当前提供三张工具卡片(均标注"仅 Windows 可用"):
| 卡片标题 | 目标页面(Button.Tag 中的 x:Type) | 状态 |
|---|---|---|
| 游戏无边框窗口化 | spp:BorderlessGamePage | 已实现,点击可导航 |
| CS:GO VAC 修复 | spp:CsgoVacRepairPage | 已实现,点击可导航 |
| CPU 游戏调度优化 | {x:Null} | 占位卡片,无目标页面 |
设计意图:把"功能入口聚合 + 统一的平台门控"收敛到一个页面,而把每个工具的具体实现拆分到独立的 Page/ViewModel,使插件保持低耦合、易扩展——新增一个工具只需新增一个页面类型并在 WrapPanel 中追加一张卡片。
架构(Architecture)
要点说明:
- 装配层:
Plugin是唯一的 MEF 导出点([CompositionExport(typeof(IPlugin))]),宿主的组合容器在启动扫描时发现它;插件本身不注册任何额外服务——ConfigureDemandServices、ConfigureRequiredServices、OnAddAutoMapper均为空实现,说明该插件当前完全由 UI 驱动,没有后台服务或持久化映射。 - UI 层:
GameToolsPage继承自spp:PageBase(项目自带的页面基类),三张卡片按钮通过Button.Tag携带目标页面x:Type,统一由 code-behind 的GameToolsPage_Click事件处理器完成导航分发。 - 平台门控:每张卡片按钮都设置
IsEnabled="{spp:OnPlatform Windows}",非 Windows 平台直接禁用点击,从 UI 层保证工具只暴露给支持的平台。 - 资源层:插件自带
Resources(toolbox图标)与UI/Assets下的卡片配图(BorderlessWindow.png、CounterStrike.png、CPU.png),通过avares://BD.WTTS.Client.Plugins.GameTools/...资源 URI 引用。
主内容:实现详解
1. 插件入口 Plugin
插件的全部注册逻辑集中在 Plugins/Plugin.cs,核心成员如下(完整源码):
1using BD.WTTS.Properties;
2using BD.WTTS.UI.Views.Pages;
3
4namespace BD.WTTS.Plugins;
5
6#if (WINDOWS || MACCATALYST || MACOS || LINUX) && !(IOS || ANDROID)
7[CompositionExport(typeof(IPlugin))]
8#endif
9public sealed class Plugin : PluginBase<Plugin>, IPlugin
10{
11 const string moduleName = AssemblyInfo.GameTools;
12
13 public override Guid Id => Guid.Parse(AssemblyInfo.GameToolsId);
14
15 public override string Name => Strings.GameRelated;
16
17 public sealed override string UniqueEnglishName => moduleName;
18
19 public sealed override string Description => "通用游戏工具";
20
21 protected sealed override string? AuthorOriginalString => null;
22
23 public sealed override object? Icon => Resources.toolbox; //"avares://BD.WTTS.Client.Plugins.GameTools/UI/Assets/toolbox.ico";
24
25 public override IEnumerable<MenuTabItemViewModel>? GetMenuTabItems()
26 {
27 yield return new MenuTabItemViewModel(this, nameof(Strings.GameRelated))
28 {
29 PageType = typeof(GameToolsPage),
30 IsResourceGet = true,
31 IconKey = Icon,
32 };
33 }
34 // ConfigureDemandServices / ConfigureRequiredServices / OnAddAutoMapper 均为空实现
35}Source: Plugin.cs
逐成员解读(WHY):
- 条件编译导出:
#if (WINDOWS || MACCATALYST || MACOS || LINUX) && !(IOS || ANDROID)限定该导出仅在桌面/桌面类平台生效。Watt Toolkit 是多平台项目,移动端不加载桌面插件,用条件编译在编译期裁剪,比运行时判断更彻底。 Id => Guid.Parse(AssemblyInfo.GameToolsId):插件身份用固定的 GUID 标识(常量由AssemblyInfo生成),宿主据此做插件去重与持久化识别。Name => Strings.GameRelated/IsResourceGet = true:菜单名不是硬编码字符串,而是资源键,配合IsResourceGet由本地化服务解析,保证多语言下菜单正确翻译。UniqueEnglishName => moduleName(AssemblyInfo.GameTools):稳定的英文标识,用于设置存储命名空间等场景。Description => "通用游戏工具"、AuthorOriginalString => null:描述直接给中文常量(当前未走资源),作者为空时GameToolsPage的Subtitle绑定会显示格式化后的空值(仅显示Plugin_Author格式占位)。Icon => Resources.toolbox:图标来自程序集强类型资源类,二进制资源避免跨程序集的avares://路径漂移(源码中保留了被注释的avares写法作为历史方案)。GetMenuTabItems()用yield return:基类契约允许多个菜单项,这里只返回一项——每个插件可向主界面贡献多个 Tab,GameTools 目前仅一个。
2. 页面元数据与 PageBase 的绑定契约
GameToolsPage.axaml 的根节点直接把页面头信息绑定到插件单例,宿主渲染页面时无需插件写任何额外胶水代码:
1<spp:PageBase
2 x:Class="BD.WTTS.UI.Views.Pages.GameToolsPage"
3 xmlns:s="https://steampp.net/services"
4 xmlns:spp="https://steampp.net/ui"
5 Title="{Binding Name, Source={x:Static s:Plugin.Instance}, Mode=OneWay}"
6 Description="{Binding Description, Source={x:Static s:Plugin.Instance}, Mode=OneWay}"
7 Subtitle="{Binding Author, Source={x:Static s:Plugin.Instance}, Mode=OneWay, Converter={StaticResource StringFormatConverter}, ConverterParameter=Plugin_Author}"
8 mc:Ignorable="d">
9 <spp:PageBase.PreviewImage>
10 <ui:ImageIconSource Source="{Binding Icon, Source={x:Static s:Plugin.Instance}, Mode=OneWay, Converter={StaticResource BitmapAssetValueConverter}}" />
11 </spp:PageBase.PreviewImage>Source: GameToolsPage.axaml
s:Plugin.Instance是PluginBase<Plugin>提供的单例(x:Static访问),XAML 直接以 OneWay 模式读取其Name/Description/Author/Icon。Subtitle使用StringFormatConverter+Plugin_Author资源格式(形如"作者:{0}"),把原始Author包装成本地化文案。Icon(二进制资源)经BitmapAssetValueConverter转成ImageIconSource,用作页面预览图——这就是插件Icon属性"object?"设计的用途:同一对象既可作为菜单图标键(IconKey)也可转为图像。
3. 卡片导航网格与平台门控
页面主体是 ScrollViewer > WrapPanel,卡片按钮统一套用 NavButtonStyle(宽 160、外边距 6、内容裁剪),Button.Tag 承载目标页面类型,GameToolsPage_Click 作为统一入口:
1<ScrollViewer>
2 <WrapPanel>
3 <Border spp:Animations.EnableAnimations="False">
4 <Button
5 Click="GameToolsPage_Click"
6 IsEnabled="{spp:OnPlatform Windows}"
7 Theme="{StaticResource NavButtonStyle}">
8 <Button.Tag>
9 <x:Type TypeName="spp:BorderlessGamePage" />
10 </Button.Tag>
11 <Grid RowDefinitions="3*,*">
12 <Border CornerRadius="4 4 0 0" RenderOptions.BitmapInterpolationMode="HighQuality">
13 <spp:Image2 Source="avares://BD.WTTS.Client.Plugins.GameTools/UI/Assets/BorderlessWindow.png" Stretch="Uniform" />
14 </Border>
15 <StackPanel Grid.Row="1" Margin="12" Spacing="5">
16 <TextBlock FontWeight="SemiBold" Text="游戏无边框窗口化" />
17 <TextBlock Foreground="{DynamicResource TextFillColorSecondaryBrush}"
18 Text="仅 Windows 可用" Theme="{StaticResource CaptionTextBlockStyle}" />
19 </StackPanel>
20 </Grid>
21 </Button>
22 </Border>
23 <!-- CsgoVacRepairPage 卡片、CPU 调度优化占位卡片结构相同 -->
24 </WrapPanel>
25</ScrollViewer>Source: GameToolsPage.axaml
设计要点:
spp:OnPlatform Windows标记扩展:声明式的平台开关,等价于 Avalonia 的OnPlatform,但来自项目自身的https://steampp.net/ui命名空间。三张卡片的可见按钮状态在非 Windows 端全部禁用,副标题文本"仅 Windows 可用"同时作为视觉提示——双重(行为 + 文案)提示避免用户误操作。Tag+x:Type携带导航目标:所有卡片共享同一个Click处理器,code-behind(GameToolsPage.axaml.cs,public partial class GameToolsPage : PageBase)从Button.Tag取出Type后发起 Frame 导航。这种"数据驱动的统一分发"让新增工具卡片无需新增事件处理器。spp:Animations.EnableAnimations="False":关闭卡片入场动画(在WrapPanel中大量元素同时动画会掉帧),性能优先的取舍。- 占位卡片:第三张"CPU 游戏调度优化"卡片
Tag="{x:Null}",仅作为预告入口存在,点击不会导航(实现细节见下文边界情况)。
4. 页面操作区(ActionContent)
PageBase.ActionContent 中放置了"刷新"按钮与"更多"下拉菜单:
1<spp:PageBase.ActionContent>
2 <StackPanel Orientation="Horizontal" Spacing="2">
3 <Button Padding="8,4" Theme="{StaticResource TransparentButton}">
4 <!-- 刷新图标 + Res.Refresh 本地化文本 -->
5 </Button>
6 <DropDownButton Content="{StaticResource More}" Theme="{StaticResource TransparentButton}">
7 <DropDownButton.Flyout>
8 <ui:FAMenuFlyout Placement="BottomEdgeAlignedRight">
9 <ui:MenuFlyoutItem IsEnabled="False">
10 <ui:MenuFlyoutItem.Text>
11 <MultiBinding StringFormat="{}{0}{1}">
12 <CompiledBinding Path="Res.Plugin_Version" Source="{x:Static s:ResourceService.Current}" />
13 <CompiledBinding Path="Version" Source="{x:Static s:Plugin.Instance}" />
14 </MultiBinding>
15 </ui:MenuFlyoutItem.Text>
16 </ui:MenuFlyoutItem>
17 <!-- 设置/在商店中查看/使用帮助/关于此插件 等菜单项当前被注释 -->
18 </ui:FAMenuFlyout>
19 </DropDownButton.Flyout>
20 </DropDownButton>
21 </StackPanel>
22</spp:PageBase.ActionContent>Source: GameToolsPage.axaml
MultiBinding 将本地化的"插件版本:"前缀与 Plugin.Instance.Version 拼接为只读菜单项(IsEnabled="False"),是插件版本信息的标准展示位。被注释掉的菜单项(插件设置、在商店中查看、使用帮助、关于此插件)表明该插件的功能尚在演进中。
核心流程(Core Flow)
流程解读:插件生命周期从 MEF 发现开始 → 贡献菜单项 → 用户进入聚合页 → 页面从插件单例读取元数据自渲染 → 卡片点击按 Tag 类型分发到具体工具页。整条链路中插件与宿主唯一的"服务级"耦合点是 IPlugin 契约,其余全部通过数据绑定与类型反射完成,符合插件化架构的低侵入要求。
使用示例(Usage Examples)
示例 1:向插件贡献一个菜单入口(插件作者视角)
以下代码展示了插件如何把自己注册进主菜单——GetMenuTabItems() 是插件与宿主 UI 之间的标准扩展点:
1public override IEnumerable<MenuTabItemViewModel>? GetMenuTabItems()
2{
3 yield return new MenuTabItemViewModel(this, nameof(Strings.GameRelated))
4 {
5 PageType = typeof(GameToolsPage),
6 IsResourceGet = true,
7 IconKey = Icon,
8 };
9}Source: Plugin.cs
说明:MenuTabItemViewModel 构造参数 (plugin, resourceKey) 指明归属插件与标题资源键;PageType 决定点击菜单时导航到哪个页面;IsResourceGet = true 表示第二个参数是资源键而非字面文本。
示例 2:声明仅在桌面平台导出的插件
1#if (WINDOWS || MACCATALYST || MACOS || LINUX) && !(IOS || ANDROID)
2[CompositionExport(typeof(IPlugin))]
3#endif
4public sealed class Plugin : PluginBase<Plugin>, IPlugin
5{
6 const string moduleName = AssemblyInfo.GameTools;
7
8 public override Guid Id => Guid.Parse(AssemblyInfo.GameToolsId);
9
10 public override string Name => Strings.GameRelated;
11
12 public sealed override string UniqueEnglishName => moduleName;
13}Source: Plugin.cs
说明:条件编译确保 iOS/Android 构建产物中不存在该 MEF 导出,宿主在移动端根本看不到此插件,比运行时禁用更安全。
示例 3:平台门控的工具卡片
1<Button
2 Click="GameToolsPage_Click"
3 IsEnabled="{spp:OnPlatform Windows}"
4 Theme="{StaticResource NavButtonStyle}">
5 <Button.Tag>
6 <x:Type TypeName="spp:CsgoVacRepairPage" />
7 </Button.Tag>
8 <Grid RowDefinitions="3*,*">
9 <Border CornerRadius="4 4 0 0" RenderOptions.BitmapInterpolationMode="HighQuality">
10 <spp:Image2 Source="avares://BD.WTTS.Client.Plugins.GameTools/UI/Assets/CounterStrike.png" Stretch="Uniform" />
11 </Border>
12 <StackPanel Grid.Row="1" Margin="12" Spacing="5">
13 <TextBlock FontWeight="SemiBold" Text="CS:GO VAC 修复" />
14 <TextBlock Foreground="{DynamicResource TextFillColorSecondaryBrush}"
15 Text="仅 Windows 可用" Theme="{StaticResource CaptionTextBlockStyle}" />
16 </StackPanel>
17 </Grid>
18</Button>Source: GameToolsPage.axaml
说明:这是新增一个工具卡片的最小模板——复制该块、替换 Button.Tag 的目标类型、配图 avares:// URI 与标题文本即可接入统一导航。
配置选项(Configuration Options)
本插件没有暴露任何配置项。Plugin.ConfigureDemandServices()、Plugin.ConfigureRequiredServices()、Plugin.OnAddAutoMapper() 均为空实现,未注册服务、未声明选项:
1public override void ConfigureDemandServices(IServiceCollection services, Startup startup)
2{
3}
4
5public override void ConfigureRequiredServices(IServiceCollection services, Startup startup)
6{
7}
8
9public override void OnAddAutoMapper(IMapperConfigurationExpression cfg)
10{
11
12}Source: Plugin.cs
这本身就是一个架构信号:GameTools 当前是纯 UI 聚合插件,无后台任务、无持久化配置、无数据库映射。各工具页的行为配置(如无边框窗口化的进程列表)由各工具页自己的 ViewModel 负责,不属于本页范围。
API 参考(API Reference)
以下为插件程序集内公开类型的签名速览;
Plugin之外的工具页内部 API 属于各兄弟页面主题,此处仅列出与插件装配/导航直接相关的成员。
Plugin : PluginBase<Plugin>, IPlugin(sealed)
程序集唯一的 MEF 导出类型,命名空间 BD.WTTS.Plugins。
| 成员 | 签名 | 说明 |
|---|---|---|
Id | override Guid | Guid.Parse(AssemblyInfo.GameToolsId),插件持久身份 |
Name | override string | Strings.GameRelated(本地化资源,"游戏相关") |
UniqueEnglishName | sealed override string | AssemblyInfo.GameTools 常量 |
Description | sealed override string | 常量 "通用游戏工具" |
AuthorOriginalString | protected sealed override string? | 返回 null(无作者) |
Icon | sealed override object? | Resources.toolbox 二进制图标资源 |
GetMenuTabItems() | override IEnumerable<MenuTabItemViewModel>? | yield return 单个菜单项,PageType = typeof(GameToolsPage) |
ConfigureDemandServices / ConfigureRequiredServices / OnAddAutoMapper | — | 空实现(无服务注册、无映射) |
参数 / 返回值 / 异常:GetMenuTabItems() 无参数,返回可枚举的 MenuTabItemViewModel(可为 null);当前源码未显示任何抛出异常的路径。
GameToolsPage : PageBase(partial)
- XAML code-behind 类,
x:Class="BD.WTTS.UI.Views.Pages.GameToolsPage",声明于GameToolsPage.axaml.cs第 5 行(public partial class GameToolsPage : PageBase)。 - 公开事件处理器
GameToolsPage_Click(被三张卡片的Click绑定引用),负责读取Button.Tag中的目标页面Type并触发导航。
MainFramePage / BorderlessGamePage / CsgoVacRepairPage
均为 PageBase 派生的工具页(每个配一个 *PageViewModel),其内部 API 与实现细节由对应专题页覆盖。
失败模式、边界情况与并发(Failure Modes, Edge Cases & Concurrency)
基于源码可确认的边界行为:
- 占位卡片不可导航:"CPU 游戏调度优化"卡片
Tag="{x:Null}"(GameToolsPage.axaml 第 167 行),点击后GameToolsPage_Click拿不到目标类型,不会发起导航——按钮仍可点击但无实际效果,属于功能未完成的显式占位。 - 非 Windows 平台禁用全部工具:三张卡片均
IsEnabled="{spp:OnPlatform Windows}",配合副标题"仅 Windows 可用"。平台判断发生在 UI 绑定层,工具页内部无需重复判断(但依赖 Win32 互操作的HandleWindowExtensions仍受调用方约束)。 - 移动端不存在此插件:MEF 导出被
#if条件编译剔除,iOS/Android 构建中宿主无法发现该插件,菜单中也不会出现"游戏相关"项——不存在运行时加载失败的路径。 - 本地化回退:
Name/菜单标题走Strings.GameRelated资源;而卡片标题("游戏无边框窗口化"等)与Description("通用游戏工具")当前是硬编码中文,未随语言切换翻译,属于已知的不一致。 - 无服务/无并发面:插件不注册后台服务、不持有共享可变状态;页面为一次性导航创建的 UI 对象,没有明显的并发或竞态场景。卡片动画被显式关闭(
Animations.EnableAnimations="False")以规避WrapPanel批量渲染的性能问题。
性能与运维(Performance & Operational Notes)
- 零后台开销:无定时器、无网络轮询、无数据库访问,插件仅在用户打开页面时产生 UI 渲染成本。
- 卡片渲染优化:
NavButtonStyle强制ClipToBounds,配图使用HighQuality位图插值、Stretch="Uniform",并且卡片容器禁用动画,保证多卡片布局下的首屏流畅。 - 资源加载:所有图片通过
avares://打包进程序集,随插件一次性加载,无外部文件依赖,插件可独立分发/卸载。
扩展点(Extension Points)
要给 GameTools 新增一个工具,标准做法是:
- 在
UI/Views/新建XxxPage.axaml(.cs)(继承PageBase),在UI/ViewModels/新建对应 ViewModel; - 在
UI/Assets/放置卡片配图; - 在
GameToolsPage.axaml的WrapPanel内按"示例 3"模板追加一张卡片,Button.Tag填<x:Type TypeName="spp:XxxPage" />; - 无需修改
Plugin.cs(除非要新增菜单 Tab 或注册服务——后者需在ConfigureDemandServices/ConfigureRequiredServices中补充)。
IPlugin 契约本身(GetMenuTabItems、服务配置钩子、AutoMapper 钩子)是宿主开放的通用扩展面;本插件目前只用了菜单贡献这一个钩子。
测试(Tests)
在本次源码探索范围内未发现 BD.WTTS.Client.Plugins.GameTools 对应的独立测试工程或测试文件;该插件的验证主要依赖客户端整体的手动/集成测试路径。若仓库后续补充测试,请以测试工程实际内容为准。
相关链接(Related Links)
- 插件入口:Plugin.cs
- 聚合页 XAML:GameToolsPage.axaml
- 聚合页 code-behind:GameToolsPage.axaml.cs
- 工具页视图模型:BorderlessGamePageViewModel.cs、CsgoVacRepairPageViewModel.cs
- 窗口处理扩展:HandleWindowExtensions.cs
- 插件工程定义:BD.WTTS.Client.Plugins.GameTools.csproj
- 专题细节(无边框窗口化、CS:GO VAC 修复、插件框架机制)请参考目录中对应的兄弟页面。