Repository Wiki
BeyondDimension/SteamTools

游戏工具(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 类、MEF CompositionExport、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 插件的职责非常纯粹:

  1. 声明自身为一个插件:Plugin 类继承 PluginBase<Plugin> 并实现 IPlugin,通过 #if (WINDOWS || MACCATALYST || MACOS || LINUX) && !(IOS || ANDROID) 条件编译 + [CompositionExport(typeof(IPlugin))]] 暴露给 MEF 容器,仅在桌面端生效。
  2. 向主菜单贡献一个入口:重写 GetMenuTabItems(),返回一个 MenuTabItemViewModel,其 PageType = typeof(GameToolsPage),IconKey = Icon,IsResourceGet = true(菜单标题使用资源字符串 Strings.GameRelated,即"游戏相关")。
  3. 提供工具聚合页: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)

Loading diagram...

要点说明:

  • 装配层: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,核心成员如下(完整源码):

csharp
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 的根节点直接把页面头信息绑定到插件单例,宿主渲染页面时无需插件写任何额外胶水代码:

xml
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 作为统一入口:

xml
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 中放置了"刷新"按钮与"更多"下拉菜单:

xml
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)

Loading diagram...

流程解读:插件生命周期从 MEF 发现开始 → 贡献菜单项 → 用户进入聚合页 → 页面从插件单例读取元数据自渲染 → 卡片点击按 Tag 类型分发到具体工具页。整条链路中插件与宿主唯一的"服务级"耦合点是 IPlugin 契约,其余全部通过数据绑定与类型反射完成,符合插件化架构的低侵入要求。

使用示例(Usage Examples)

示例 1:向插件贡献一个菜单入口(插件作者视角)

以下代码展示了插件如何把自己注册进主菜单——GetMenuTabItems() 是插件与宿主 UI 之间的标准扩展点:

csharp
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:声明仅在桌面平台导出的插件

csharp
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:平台门控的工具卡片

xml
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() 均为空实现,未注册服务、未声明选项:

csharp
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。

成员签名说明
Idoverride GuidGuid.Parse(AssemblyInfo.GameToolsId),插件持久身份
Nameoverride stringStrings.GameRelated(本地化资源,"游戏相关")
UniqueEnglishNamesealed override stringAssemblyInfo.GameTools 常量
Descriptionsealed override string常量 "通用游戏工具"
AuthorOriginalStringprotected sealed override string?返回 null(无作者)
Iconsealed 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 新增一个工具,标准做法是:

  1. 在 UI/Views/ 新建 XxxPage.axaml(.cs)(继承 PageBase),在 UI/ViewModels/ 新建对应 ViewModel;
  2. 在 UI/Assets/ 放置卡片配图;
  3. 在 GameToolsPage.axaml 的 WrapPanel 内按"示例 3"模板追加一张卡片,Button.Tag 填 <x:Type TypeName="spp:XxxPage" />;
  4. 无需修改 Plugin.cs(除非要新增菜单 Tab 或注册服务——后者需在 ConfigureDemandServices/ConfigureRequiredServices 中补充)。

IPlugin 契约本身(GetMenuTabItems、服务配置钩子、AutoMapper 钩子)是宿主开放的通用扩展面;本插件目前只用了菜单贡献这一个钩子。

测试(Tests)

在本次源码探索范围内未发现 BD.WTTS.Client.Plugins.GameTools 对应的独立测试工程或测试文件;该插件的验证主要依赖客户端整体的手动/集成测试路径。若仓库后续补充测试,请以测试工程实际内容为准。

Sources

(2 files)
src/BD.WTTS.Client.Plugins.GameTools/Plugins
src/BD.WTTS.Client.Plugins.GameTools/UI/Views