Browse Source

Publish PCL Communist community fork

Angel 19 hours ago
parent
commit
6453df05be
56 changed files with 1068 additions and 2732 deletions
  1. 0 3
      .github/FUNDING.yml
  2. 0 39
      .github/ISSUE_TEMPLATE/bug1.yml
  3. 0 41
      .github/ISSUE_TEMPLATE/bug2.yml
  4. 0 37
      .github/ISSUE_TEMPLATE/bug9.yml
  5. 20 0
      .github/ISSUE_TEMPLATE/bug_report.yml
  6. 0 32
      .github/ISSUE_TEMPLATE/ch.yml
  7. 1 14
      .github/ISSUE_TEMPLATE/config.yml
  8. 0 33
      .github/ISSUE_TEMPLATE/feature.yml
  9. 10 0
      .github/ISSUE_TEMPLATE/feature_request.yml
  10. 59 53
      .github/workflows/build.yml
  11. 9 0
      .gitignore
  12. 1 1
      PCLCS/PCLCS.csproj
  13. 1 1
      Plain Craft Launcher 2/App.config
  14. 5 5
      Plain Craft Launcher 2/Application.xaml.vb
  15. 19 1
      Plain Craft Launcher 2/FormMain.xaml
  16. 3 42
      Plain Craft Launcher 2/FormMain.xaml.vb
  17. BIN
      Plain Craft Launcher 2/Images/Red/Splash.png
  18. BIN
      Plain Craft Launcher 2/Images/icon.ico
  19. 1 1
      Plain Craft Launcher 2/Modules/Base/ModBase.vb
  20. 5 22
      Plain Craft Launcher 2/Modules/Base/ModNet.vb
  21. 8 286
      Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb
  22. 4 100
      Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb
  23. 1 28
      Plain Craft Launcher 2/Modules/ModEvent.vb
  24. 1 16
      Plain Craft Launcher 2/Modules/ModMain.vb
  25. 109 21
      Plain Craft Launcher 2/Modules/ModSecret.vb
  26. 52 0
      Plain Craft Launcher 2/Modules/RedPolicy.vb
  27. 3 3
      Plain Craft Launcher 2/My Project/AssemblyInfo.vb
  28. 2 2
      Plain Craft Launcher 2/My Project/Resources.resx
  29. 1 3
      Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSetup.xaml
  30. 2 1
      Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSetup.xaml.vb
  31. 1 101
      Plain Craft Launcher 2/Pages/PageLaunch/MySkin.xaml.vb
  32. 8 140
      Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchLeft.xaml.vb
  33. 11 0
      Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml
  34. 13 170
      Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.vb
  35. 1 1
      Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMsSkin.xaml
  36. 2 57
      Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMsSkin.xaml.vb
  37. 2 206
      Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml
  38. 6 1099
      Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml.vb
  39. 10 7
      Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml
  40. 5 0
      Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml.vb
  41. 7 16
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupSystem.xaml
  42. 3 31
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupSystem.xaml.vb
  43. 25 49
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml
  44. 4 42
      Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml.vb
  45. 6 7
      Plain Craft Launcher 2/Pages/PageSetup/Settings.vb
  46. 12 8
      Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj
  47. 72 13
      README.md
  48. BIN
      docs/images/launcher.png
  49. BIN
      docs/images/theme-satire.png
  50. 14 0
      tools/Build.ps1
  51. 65 0
      tools/Generate-Icon.ps1
  52. 60 0
      tools/Package.ps1
  53. 277 0
      tools/SmokeTest/Program.cs
  54. 14 0
      tools/SmokeTest/SmokeTest.csproj
  55. 133 0
      tools/Test-IsolatedProcess.ps1
  56. BIN
      最新正式版.zip

+ 0 - 3
.github/FUNDING.yml

@@ -1,3 +0,0 @@
-# These are supported funding model platforms
-
-custom: ['https://meloong.com/afd/a/LTCat']

+ 0 - 39
.github/ISSUE_TEMPLATE/bug1.yml

@@ -1,39 +0,0 @@
-name: "Minecraft 崩溃"
-description: "PCL 提示 “Minecraft 出现错误”,或游戏崩溃"
-labels: [· Bug, 新提交]
-body:
-- type: checkboxes
-  id: "yml-1"
-  attributes:
-    label: "检查项"
-    description: "请逐个检查下列项目,并勾选确认。"
-    options:
-    - label: "**我所启动的游戏不是整合包,且未安装任何 Mod。** 安装 Mod 后的崩溃基本不是 PCL 导致的,请 **不要** 提交反馈。你可以前往 [PCL 汉医堂](https://qm.qq.com/q/JJTzwEAmcw) 等民间 QQ 群求助,但这里不是求助的地方……"
-      required: false
-    - label: "**我已尝试使用 HMCL 启动,HMCL 没有出现问题。** 如果 HMCL 也无法启动就不是 PCL 导致的问题,请 **不要** 提交反馈。[下载 HMCL](https://hmcl.huangyuhui.net/download)"
-      required: true
-    - label: "我已在 [Issues 页面](https://github.com/Meloong-Git/PCL/issues?q=is%3Aissue+) 和 [常见&难检反馈及问题列表](https://github.com/Meloong-Git/PCL/discussions/1930) 中搜索,确认了这一 Bug 未被提交过。"
-      required: true
-- type: textarea
-  id: "yml-2"
-  attributes:
-    label: 描述
-    description: "详细描述具体表现。"
-  validations:
-    required: true
-- type: textarea
-  id: "yml-3"
-  attributes:
-    label: PCL 错误报告、日志与附件
-    description: "上传 PCL 提供的错误报告(在崩溃时选择导出错误报告)。如果没有,也可以上传 PCL 文件夹中的 Log1~5.txt。"
-    placeholder: "先点击这个文本框,然后再将文件直接拖拽到文本框中以上传。"
-  validations:
-    required: true
-- type: textarea
-  id: "yml-4"
-  attributes:
-    label: HMCL 启动脚本
-    description: "在 HMCL 中进入版本列表,点击版本右侧的三个点,选择 生成启动脚本。"
-    placeholder: "先点击这个文本框,然后再将文件直接拖拽到文本框中以上传。"
-  validations:
-    required: false

+ 0 - 41
.github/ISSUE_TEMPLATE/bug2.yml

@@ -1,41 +0,0 @@
-name: "下载或登录时卡住或失败"
-description: "下载游戏、Mod 或整合包,或登录正版账号时出现问题"
-labels: [· Bug, 新提交]
-body:
-- type: checkboxes
-  id: "yml-1"
-  attributes:
-    label: "检查项"
-    description: "请逐个检查下列项目,并勾选确认。"
-    options:
-    - label: "**我已尝试使用其他启动器,其他启动器没有出现问题。** 如果其他启动器也存在问题,证明是网络环境不佳[(解决方法)](https://shimo.im/docs/qKPttVvXKqPD8YDC#anchor-0hDr),并非 PCL 的问题,请 **不要** 提交反馈。[下载 HMCL](https://hmcl.huangyuhui.net/download)"
-      required: false
-    - label: "我知晓大多数此类问题都是网络环境不佳导致的,但我确实认为我的问题可能是 PCL 导致的,和网络环境无关。"
-      required: true
-    - label: "我已在 [Issues 页面](https://github.com/Meloong-Git/PCL/issues?q=is%3Aissue+) 和 [常见&难检反馈及问题列表](https://github.com/Meloong-Git/PCL/discussions/1930) 中搜索,确认了这一 Bug 未被提交过。"
-      required: true
-- type: textarea
-  id: "yml-2"
-  attributes:
-    label: 描述
-    description: "详细描述该 Bug 的具体表现。"
-  validations:
-    required: true
-- type: textarea
-  id: "yml-3"
-  attributes:
-    label: 重现步骤
-    description: "详细描述要怎么操作才能再次触发这个 Bug。"
-    value: |
-      1、点击xxxx
-      2、往下滚,然后点击xxxx
-  validations:
-    required: true
-- type: textarea
-  id: "yml-4"
-  attributes:
-    label: 日志与附件
-    description: "上传 PCL 文件夹中的 Log1~5.txt。[日志在哪?](https://shimo.im/docs/qKPttVvXKqPD8YDC#anchor-fvZW)"
-    placeholder: "先点击这个文本框,然后再将文件直接拖拽到文本框中以上传。"
-  validations:
-    required: true

+ 0 - 37
.github/ISSUE_TEMPLATE/bug9.yml

@@ -1,37 +0,0 @@
-name: "综合 Bug 反馈"
-description: "遇见了没有细分选项的其他 Bug"
-labels: [· Bug, 新提交]
-body:
-- type: checkboxes
-  id: "yml-1"
-  attributes:
-    label: "检查项"
-    description: "请逐个检查下列项目,并勾选确认。"
-    options:
-    - label: "我已在 [Issues 页面](https://github.com/Meloong-Git/PCL/issues?q=is%3Aissue+) 和 [常见&难检反馈及问题列表](https://github.com/Meloong-Git/PCL/discussions/1930) 中搜索,确认了这一 Bug 未被提交过。"
-      required: true
-- type: textarea
-  id: "yml-2"
-  attributes:
-    label: 描述
-    description: "详细描述该 Bug 的具体表现。"
-  validations:
-    required: true
-- type: textarea
-  id: "yml-3"
-  attributes:
-    label: 重现步骤
-    description: "详细描述要怎么操作才能再次触发这个 Bug。"
-    value: |
-      1、点击xxxx
-      2、往下滚,然后点击xxxx
-  validations:
-    required: true
-- type: textarea
-  id: "yml-4"
-  attributes:
-    label: 日志与附件
-    description: "上传 PCL 文件夹中的 Log1~5.txt。[日志在哪?](https://shimo.im/docs/qKPttVvXKqPD8YDC#anchor-fvZW)"
-    placeholder: "先点击这个文本框,然后再将文件直接拖拽到文本框中以上传。"
-  validations:
-    required: true

+ 20 - 0
.github/ISSUE_TEMPLATE/bug_report.yml

@@ -0,0 +1,20 @@
+name: 问题反馈
+description: 报告 PCL Communist 中的问题
+body:
+  - type: textarea
+    id: problem
+    attributes:
+      label: 问题与复现步骤
+      description: 请说明发生了什么、如何复现,以及你期望的结果。
+    validations:
+      required: true
+  - type: input
+    id: environment
+    attributes:
+      label: 版本与系统
+      placeholder: PCL Communist 2.99.0.0 / Windows 11
+  - type: textarea
+    id: logs
+    attributes:
+      label: 日志或截图
+      description: 提交前请移除账号令牌和个人信息。

+ 0 - 32
.github/ISSUE_TEMPLATE/ch.yml

@@ -1,32 +0,0 @@
-name: "优化建议"
-description: "对已有功能的小幅度优化或改进建议"
-labels: [· 优化, 新提交]
-body:
-- type: checkboxes
-  id: "yml-1"
-  attributes:
-    label: "检查项"
-    description: "请逐个检查下列项目,并勾选确认。"
-    options:
-    - label: "我已在 [Issues 页面](https://github.com/Meloong-Git/PCL/issues?q=is%3Aissue+) 和 [常见&难检反馈及问题列表](https://github.com/Meloong-Git/PCL/discussions/1930) 中搜索,确认了这一建议未被提交过。"
-      required: true
-    - label: "我已查看 [功能投票页面](https://github.com/Meloong-Git/PCL/discussions/categories/%E5%8A%9F%E8%83%BD%E6%8A%95%E7%A5%A8/),确认了这一建议未在投票列表中。"
-      required: true
-- type: textarea
-  id: "yml-2"
-  attributes:
-    label: 描述
-    description: "详细描述具体需要优化哪些地方,要改成怎样的。"
-  validations:
-    required: true
-- type: textarea
-  id: "yml-3"
-  attributes:
-    label: 原因
-    description: "详细描述你为什么需要这项优化,这有助于开发者评估它的优先度。"
-    value: |
-      (示例)
-      这项优化让我可以更方便地xxxx。
-      如果没有这项优化,我每次都必须xxxx,让操作变得很麻烦。
-  validations:
-    required: true

+ 1 - 14
.github/ISSUE_TEMPLATE/config.yml

@@ -1,14 +1 @@
-blank_issues_enabled: false
-contact_links:
-  - name: 主页预设反馈
-    url: https://github.com/Meloong-Git/PCL/discussions/categories/自定义主页
-    about: 提交与预设的主页(设置 → 个性化 → 主页预设)中的具体内容相关的反馈
-  - name: 帮助文档反馈
-    url: https://github.com/LTCatt/PCL2Help/issues
-    about: 提交与 PCL 帮助文档(更多 → 帮助)中的具体内容相关的反馈
-  - name: 提问
-    url: https://github.com/Meloong-Git/PCL/discussions/new?category=%E6%8F%90%E9%97%AE
-    about: 我想问一些 PCL 相关的问题……
-  - name: 讨论
-    url: https://github.com/Meloong-Git/PCL/discussions/new?category=%E8%AE%A8%E8%AE%BA
-    about: 我想讨论一些 PCL 相关的事情……
+blank_issues_enabled: true

+ 0 - 33
.github/ISSUE_TEMPLATE/feature.yml

@@ -1,33 +0,0 @@
-name: "新功能提案"
-description: "对已有功能的大幅度修改,或添加一个新内容或选项"
-labels: [· 新功能, 新提交]
-body:
-- type: checkboxes
-  id: "yml-1"
-  attributes:
-    label: "检查项"
-    description: "请逐个检查下列项目,并勾选确认。"
-    options:
-    - label: "我已在 [Issues 页面](https://github.com/Meloong-Git/PCL/issues?q=is%3Aissue+) 和 [常见&难检反馈及问题列表](https://github.com/Meloong-Git/PCL/discussions/1930) 中搜索,确认了这一提案未被提交过。"
-      required: true
-    - label: "我已查看 [功能投票页面](https://github.com/Meloong-Git/PCL/discussions/categories/%E5%8A%9F%E8%83%BD%E6%8A%95%E7%A5%A8/),确认了这一提案未在投票列表中。"
-      required: true
-    - label: "我知晓还没做的新功能真的太多了,忙不过来,所以新功能提案几乎不会被处理,也不建议再提交新功能提案 qwq……"
-      required: true
-- type: textarea
-  id: "yml-2"
-  attributes:
-    label: 描述
-    description: "详细描述你想添加的功能具体是怎样的。"
-  validations:
-    required: true
-- type: textarea
-  id: "yml-3"
-  attributes:
-    label: 原因
-    description: "详细描述你为什么需要这项功能,这有助于开发者评估它的优先度。"
-    value: |
-      (示例)
-      我需要这个功能来xxxx。很多人都有xxxx的需求,但如果没有这个功能,就不能xxxx了。
-  validations:
-    required: true

+ 10 - 0
.github/ISSUE_TEMPLATE/feature_request.yml

@@ -0,0 +1,10 @@
+name: 改进建议
+description: 提出功能或体验改进
+body:
+  - type: textarea
+    id: proposal
+    attributes:
+      label: 建议与使用场景
+      description: 请描述你希望改善的体验,以及建议的行为。
+    validations:
+      required: true

+ 59 - 53
.github/workflows/build.yml

@@ -1,69 +1,75 @@
 name: Build
+
 on:
   push:
-    paths:
-      - 'Plain Craft Launcher 2/**'
-      - '.github/workflows/**'
+    branches: [main]
+    paths-ignore:
+      - '**.md'
+      - 'docs/**'
   pull_request:
-    paths:
-      - 'Plain Craft Launcher 2/**'
-      - '.github/workflows/**'
+    paths-ignore:
+      - '**.md'
+      - 'docs/**'
   workflow_dispatch:
 
+permissions:
+  contents: read
+
 jobs:
-  build:
-    name: Build
+  windows:
+    name: Windows Release
     runs-on: windows-latest
-    strategy:  
-       matrix:  
-         configuration: [Debug, Release]
-
+    timeout-minutes: 20
     steps:
+      - name: Checkout source and pinned dependencies
+        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+        with:
+          submodules: recursive
 
-    - name: Checkout
-      uses: actions/checkout@v6
-      with:
-        fetch-depth: 1
-        submodules: true
+      - name: Set up .NET SDK
+        uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
+        with:
+          dotnet-version: 9.0.x
 
-    - name: Set Describe
-      shell: bash
-      run: |
-          describe=`git describe --tags --always`
-          echo "describe=$describe" >> $GITHUB_ENV
+      - name: Build public single executable
+        shell: pwsh
+        run: ./tools/Build.ps1
 
-    - name: Setup MSBuild.exe
-      uses: microsoft/setup-msbuild@v3
-      with:
-        msbuild-architecture: x64
-    
-    - name: Update Help
-      run:  |
-        Set-Location ..
-        git clone https://github.com/LTCatt/PCL2Help.git -b master --single-branch --depth 1
-        Set-Location PCL2Help
-        Remove-Item -Path *.* -Recurse -Force
-        Compress-Archive -Path .\* -DestinationPath .\Help.zip
-        Copy-Item -Path .\Help.zip -Destination "${{ GITHUB.WORKSPACE }}\Plain Craft Launcher 2\Resources\Help.zip" -Force
-        Set-Location ..
-        Remove-Item PCL2Help -Recurse -Force
-        Set-Location ${{ GITHUB.WORKSPACE }}
+      - name: Build runtime checks
+        shell: pwsh
+        run: dotnet build ./tools/SmokeTest/SmokeTest.csproj -c Release -v:minimal
 
+      - name: Check themes and public executable
+        shell: powershell
+        run: |
+          $launcher = Join-Path $env:GITHUB_WORKSPACE 'artifacts\PCL-Communist-2.99.0.0\PCL Communist.exe'
+          $hostExe = Join-Path $env:GITHUB_WORKSPACE 'tools\SmokeTest\bin\Release\net48\SmokeTest.exe'
+          $results = Join-Path $env:RUNNER_TEMP 'pcl-runtime-results'
+          $testArguments = '"' + $launcher + '" "' + $results + '"'
+          ./tools/Test-IsolatedProcess.ps1 -Executable $hostExe -ArgumentLine $testArguments -OutputDirectory $results -TimeoutSeconds 60
+          Get-Content -LiteralPath (Join-Path $results 'test-console.txt')
 
-    - name: Replace
-      run: |
-        (gc "Plain Craft Launcher 2\Modules\ModSecret.vb") -replace 'Public OAuthClientId As String = If(Environment.GetEnvironmentVariable("PCL_MS_CLIENT_ID"), "")', 'Public OAuthClientId As String = If(Environment.GetEnvironmentVariable("PCL_MS_CLIENT_ID"), "${{ secrets.CLIENT_ID }}")' | Out-File "Plain Craft Launcher 2\Modules\ModSecret.vb"
-        (gc "Plain Craft Launcher 2\Modules\ModSecret.vb") -replace 'Public CurseForgeAPIKey As String = If(Environment.GetEnvironmentVariable("PCL_CURSEFORGE_API_KEY"), "")', 'Public CurseForgeAPIKey As String = If(Environment.GetEnvironmentVariable("PCL_CURSEFORGE_API_KEY"), "${{ secrets.CURSEFORGE_API_KEY }}")' | Out-File "Plain Craft Launcher 2\Modules\ModSecret.vb"
-        (gc "Plain Craft Launcher 2\Modules\Base\ModBase.vb") -replace 'Public Const CommitHash As String = ""', 'Public Const CommitHash As String = "${{ github.sha }}"' | Out-File "Plain Craft Launcher 2\Modules\Base\ModBase.vb"
-        
-    - name: Restore NuGet Packages
-      run: dotnet restore PCLCS\PCLCS.csproj
+      - name: Check standalone startup
+        shell: powershell
+        run: |
+          $fixture = Join-Path $env:RUNNER_TEMP 'pcl-standalone'
+          New-Item -ItemType Directory -Path $fixture | Out-Null
+          Copy-Item -LiteralPath './artifacts/PCL-Communist-2.99.0.0/PCL Communist.exe' -Destination $fixture
+          ./tools/Test-IsolatedProcess.ps1 -Executable (Join-Path $fixture 'PCL Communist.exe') -OutputDirectory (Join-Path $env:RUNNER_TEMP 'pcl-startup-results') -TimeoutSeconds 10 -ExpectMainWindow
 
-    - name: Build
-      run: msbuild "Plain Craft Launcher 2\Plain Craft Launcher 2.vbproj" -p:Configuration=${{ matrix.configuration }}
+      - name: Upload executable
+        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
+        with:
+          name: PCL-Communist
+          path: artifacts/PCL-Communist-2.99.0.0/PCL Communist.exe
+          if-no-files-found: error
 
-    - name: Upload a Build Artifact
-      uses: actions/upload-artifact@v7
-      with:
-        name: ${{ matrix.configuration }} ${{ env.describe }}
-        path: Plain Craft Launcher 2\obj\${{ matrix.configuration }}\Plain Craft Launcher 2.exe
+      - name: Upload runtime diagnostics
+        if: failure()
+        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
+        with:
+          name: runtime-diagnostics
+          path: |
+            ${{ runner.temp }}/pcl-runtime-results
+            ${{ runner.temp }}/pcl-startup-results
+          if-no-files-found: ignore

+ 9 - 0
.gitignore

@@ -342,3 +342,12 @@ ASALocalRun/
 healthchecksdb
 
 MaskFiles/
+
+# Local build credentials and packaged artifacts
+.local/
+artifacts/
+.env
+.env.*
+*.pem
+*.key
+build-credentials.json

+ 1 - 1
PCLCS/PCLCS.csproj

@@ -59,7 +59,7 @@
 		<EmbeddedResource Include="Launch\JavaWrapper.jar" LogicalName="Launch\JavaWrapper.jar" />
 		<EmbeddedResource Include="Launch\LwjglUnsafeAgent.jar" LogicalName="Launch\LwjglUnsafeAgent.jar" />
 		<EmbeddedResource Include="Resource\WikiEntries.txt" LogicalName="Resource\WikiEntries.txt" />
-		<EmbeddedResource Include="Minecraft\OptiFineHashes.json" LogicalName="Minecraft\OptiFineHashes.json" />
+		<EmbeddedResource Include="Minecraft\OptifineHashes.json" LogicalName="Minecraft\OptiFineHashes.json" />
 	</ItemGroup>
 	<ItemGroup>
 	  <Reference Include="Microsoft.VisualBasic" />

+ 1 - 1
Plain Craft Launcher 2/App.config

@@ -6,7 +6,7 @@
         </settings>
     </system.net>
     <startup>
-		<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2"/>
+		<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8"/>
     </startup>
     <runtime>
         <generatePublisherEvidence enabled="false"/>

+ 5 - 5
Plain Craft Launcher 2/Application.xaml.vb

@@ -75,7 +75,8 @@ Public Class Application
                 MsgBox($"PCL 没有对当前文件夹的权限({Paths.Base}PCL\),请尝试:" & vbCrLf &
                   "1. 将 PCL 移动到其他文件夹" & If(PathUtils.GetDiskName(Paths.Base) = "C"c, ",例如 C 盘和桌面以外的其他位置。", "。") & vbCrLf &
                   "2. 删除当前目录中的 PCL 文件夹,然后再试。" & vbCrLf &
-                  "3. 右键 PCL 选择属性,打开 兼容性 中的 以管理员身份运行此程序。",
+                  "3. 右键 PCL 选择属性,打开 兼容性 中的 以管理员身份运行此程序。" & vbCrLf & vbCrLf &
+                  "错误详情:" & ex.GetDisplay(True),
                 MsgBoxStyle.Critical, "运行环境错误")
                 Environment.[Exit](ProcessReturnValues.Cancel)
             End Try
@@ -108,7 +109,7 @@ RetryCacheCheck:
             End If
             '设置初始窗口
             If Settings.Get(Of Boolean)("UiLauncherLogo") Then
-                FrmStart = New SplashScreen("Images\icon.ico")
+                FrmStart = New SplashScreen("Images\Red\Splash.png")
                 FrmStart.Show(False, True)
             End If
             '基础信息
@@ -152,7 +153,7 @@ RetryCacheCheck:
         If BuildType = BuildTypes.Debug Then Return
         Try
             Dim MutexCreatedNew As Boolean
-            PclMutex = New Mutex(True, "PCL_SingletonMutex", MutexCreatedNew)
+            PclMutex = New Mutex(True, "PCL_Communist_SingletonMutex", MutexCreatedNew)
             If MutexCreatedNew Then Return
             Logger.Warn("已有一个 PCL 实例正在运行")
             '等待已有的 PCL 退出
@@ -164,8 +165,7 @@ RetryCacheCheck:
                 End Try
             End If
             '将已有的 PCL 窗口拖出来
-            Dim WindowHwnd As IntPtr = FindWindow(Nothing, "Plain Craft Launcher ")
-            If WindowHwnd = IntPtr.Zero Then WindowHwnd = FindWindow(Nothing, "Plain Craft Launcher 2 ")
+            Dim WindowHwnd As IntPtr = FindWindow(Nothing, "PCL Communist")
             If WindowHwnd <> IntPtr.Zero Then ShowWindowToTop(WindowHwnd)
             '播放提示音并退出
             Beep()

+ 19 - 1
Plain Craft Launcher 2/FormMain.xaml

@@ -4,7 +4,7 @@
     xmlns:local="clr-namespace:PCL"
     xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
     x:Name="WindMain" x:Class="FormMain" Style="{StaticResource MyWindow}"
-    AllowDrop="True" Topmost="True" Title="Plain Craft Launcher " AllowsTransparency="True" MinHeight="470" MinWidth="810" 
+    AllowDrop="True" Topmost="True" Title="PCL Communist" AllowsTransparency="True" MinHeight="470" MinWidth="810"
     ScrollViewer.VerticalScrollBarVisibility="Disabled" Icon="/Plain Craft Launcher 2;component/Images/icon.ico" WindowStyle="None" 
     ResizeMode="CanMinimize" Background="{x:Null}" d:DesignWidth="870" d:DesignHeight="520" Width="850" Height="500" 
     RenderTransformOrigin="0.5,0.5">
@@ -97,6 +97,17 @@
                 </Grid.RowDefinitions>
                 <Canvas x:Name="ImgBack" IsHitTestVisible="False" Grid.Row="1" RenderOptions.BitmapScalingMode="LowQuality"
                         HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
+                <Grid x:Name="CommunismBackdrop" Grid.Row="1" IsHitTestVisible="False" Visibility="Collapsed" ClipToBounds="True">
+                    <Viewbox Width="420" Height="420" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="0,0,-45,-70" Opacity="0.12">
+                        <Grid Width="420" Height="420">
+                            <Ellipse Width="336" Height="336" Stroke="#FFC19547" StrokeThickness="2" />
+                            <Ellipse Width="360" Height="360" Stroke="#FFC19547" StrokeThickness="1" />
+                            <TextBlock Text="☭" FontFamily="Segoe UI Symbol" FontSize="282" Foreground="#FFAC1520" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="8,-10,0,0" />
+                            <Path Data="M 15,0 L 19,10 L 30,11 L 22,18 L 24,30 L 15,24 L 6,30 L 8,18 L 0,11 L 11,10 Z" Fill="#FFAC1520" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="0,12,0,0" />
+                        </Grid>
+                    </Viewbox>
+                    <Border Width="4" Background="#30C19547" HorizontalAlignment="Left" Margin="12,24,0,24" />
+                </Grid>
                 <Grid x:Name="PanTitle" Height="48">
                     <Grid.Background>
                         <LinearGradientBrush EndPoint="1,0" StartPoint="0,0">
@@ -104,6 +115,13 @@
                         </LinearGradientBrush>
                     </Grid.Background>
                     <local:MyImage x:Name="ImgTitle" IsHitTestVisible="False" Stretch="UniformToFill" IsEnabled="False" RenderOptions.BitmapScalingMode="LowQuality" />
+                    <Viewbox x:Name="CommunismEmblem" Width="78" Height="44" HorizontalAlignment="Right" Margin="0,0,116,0" Visibility="Collapsed" IsHitTestVisible="False" Opacity="0.85">
+                        <Grid Width="90" Height="50">
+                            <TextBlock Text="☭" FontFamily="Segoe UI Symbol" FontSize="46" Foreground="#FFFFD36A" HorizontalAlignment="Center" VerticalAlignment="Center" />
+                            <Path Data="M 10,0 L 13,7 L 21,8 L 15,13 L 17,21 L 10,17 L 3,21 L 5,13 L 0,8 L 7,7 Z" Fill="#FFFFD36A" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="0,2,0,0" />
+                        </Grid>
+                    </Viewbox>
+                    <Border x:Name="CommunismRule" Height="2" Background="#FFFFD36A" VerticalAlignment="Bottom" Visibility="Collapsed" IsHitTestVisible="False" />
                     <local:MyIconButton x:Name="BtnTitleClose" HorizontalAlignment="Right" Height="28" VerticalAlignment="Center" Width="28" Margin="0,0,12,0" Theme="White" 
                                        LogoScale="0.72" Logo="F1 M2,0 L0,2 8,10 0,18 2,20 10,12 18,20 20,18 12,10 20,2 18,0 10,8 2,0Z" />
                     <local:MyIconButton x:Name="BtnTitleMin" HorizontalAlignment="Right" Height="28" VerticalAlignment="Center" Width="28" Margin="0,0,44,0" Theme="White" 

+ 3 - 42
Plain Craft Launcher 2/FormMain.xaml.vb

@@ -12,7 +12,7 @@ Public Class FormMain
         '统计更新日志条目
         If BuildType = BuildTypes.Release Then
             If LastVersion < 413 Then 'Release 2.99.0.0
-                FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "删除:为遵守相关法律法规,PCL 暂时不再提供 MC 下载功能,非常抱歉 qwq……"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "PCL Communist:开放 Minecraft 下载与全部主题,默认苏维埃红主题"))
             End If
             If LastVersion < 411 Then 'Release 2.14.0.0
                 FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "新增:允许修改、添加、移除、更新已安装版本的各个组件,可从版本设置中进入"))
@@ -104,7 +104,7 @@ Public Class FormMain
             '2:BUG* IMP-
             '1:BUG-
             If LastVersion < 414 Then 'Snapshot 2.99.0.0
-                FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "删除:为遵守相关法律法规,PCL 暂时不再提供 MC 下载功能,非常抱歉 qwq……"))
+                FeatureList.Add(New KeyValuePair(Of Integer, String)(5, "PCL Communist:开放 Minecraft 下载与全部主题,默认苏维埃红主题"))
             End If
             If LastVersion < 412 Then 'Snapshot 2.14.0.0
                 FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "新增:允许修改、添加、移除、更新已安装版本的各个组件,可从版本设置中进入"))
@@ -413,38 +413,15 @@ Public Class FormMain
         '加载池
         RunInNewThread(
         Sub()
-            'EULA 提示
-            Const EulaVersion As Integer = 2
-            If Settings.Get(Of Integer)("SystemEulaVersion") < EulaVersion Then
-                Select Case MyMsgBox(
-                    If(Settings.Get(Of Integer)("SystemEulaVersion") = 0,
-                        "在使用 PCL 前,请先阅读用户协议与免责声明。",
-                        $"PCL 的用户协议与免责声明已更新。{vbCrLf}请阅读更新后的用户协议与免责声明。"),
-                        "协议授权", "同意", "拒绝", "查看用户协议与免责声明",
-                        Button3Action:=Sub() OpenWebsite("https://shimo.im/docs/rGrd8pY8xWkt6ryW"))
-                    Case 1
-                        Settings.Set("SystemEulaVersion", EulaVersion)
-                    Case 2
-                        EndProgram(False)
-                End Select
-            End If
             '启动加载器池
             Try
-                JavaInit() '延后到同意协议后再执行,避免在初次启动时进行进程操作
+                JavaInit()
                 Thread.Sleep(100)
-                DlClientListMojangLoader.Start(1) 'PCL 会同时根据这里的加载结果决定是否使用官方源进行下载
                 RunCountSub()
-                ServerLoader.Start()
                 RunInNewThread(AddressOf TryClearTaskTemp, "TryClearTaskTemp", ThreadPriority.BelowNormal)
             Catch ex As Exception
                 Logger.Error(ex, "初始化加载池运行失败")
             End Try
-            '清理自动更新文件
-            Try
-                FileUtils.Delete(Paths.Base & "PCL\Plain Craft Launcher 2.exe")
-            Catch ex As Exception
-                Logger.Warn(ex, "清理自动更新文件失败")
-            End Try
             '上报
             Telemetry("启动")
             '开源版本提示
@@ -494,22 +471,6 @@ Public Class FormMain
             UnlockedTheme.AddRange(New List(Of String)(Settings.Get(Of String)("UiLauncherThemeHide2").ToString.Split("|")))
             Settings.Set("UiLauncherThemeHide2", UnlockedTheme.Distinct.ToList.Join("|"c))
         End If
-        '重置欧皇彩
-        If LastVersionCode <= 115 AndAlso Settings.Get(Of String)("UiLauncherThemeHide2").ToString.Split("|").Contains("13") Then
-            Dim UnlockedTheme As New List(Of String)(Settings.Get(Of String)("UiLauncherThemeHide2").ToString.Split("|"))
-            UnlockedTheme.Remove("13")
-            Settings.Set("UiLauncherThemeHide2", UnlockedTheme.Join("|"c))
-            MyMsgBox("由于新版 PCL 修改了欧皇彩的解锁方式,你需要重新解锁欧皇彩。" & vbCrLf &
-                     "多谢各位的理解啦!", "重新解锁提醒")
-        End If
-        '重置滑稽彩
-        If LastVersionCode <= 152 AndAlso Settings.Get(Of String)("UiLauncherThemeHide2").ToString.Split("|").Contains("12") Then
-            Dim UnlockedTheme As New List(Of String)(Settings.Get(Of String)("UiLauncherThemeHide2").ToString.Split("|"))
-            UnlockedTheme.Remove("12")
-            Settings.Set("UiLauncherThemeHide2", UnlockedTheme.Join("|"c))
-            MyMsgBox("由于新版 PCL 修改了滑稽彩的解锁方式,你需要重新解锁滑稽彩。" & vbCrLf &
-                     "多谢各位的理解啦!", "重新解锁提醒")
-        End If
         '移动自定义皮肤
         If LastVersionCode <= 161 AndAlso FileUtils.Exists(Paths.Base & "PCL\CustomSkin.png") AndAlso Not FileUtils.Exists(Paths.AppDataThenName & "CustomSkin.png") Then
             FileUtils.Copy(Paths.Base & "PCL\CustomSkin.png", Paths.AppDataThenName & "CustomSkin.png")

BIN
Plain Craft Launcher 2/Images/Red/Splash.png


BIN
Plain Craft Launcher 2/Images/icon.ico


+ 1 - 1
Plain Craft Launcher 2/Modules/Base/ModBase.vb

@@ -15,7 +15,7 @@ Public Module ModBase
 #End If
 
     '版本信息
-    Public Const VersionDisplay As String = BuildTypeDisplay & " " & VersionBaseName
+    Public Const VersionDisplay As String = BuildTypeDisplay & " " & VersionBaseName & " · PCL Communist"
 #If DEBUG Then
     Public Const BuildTypeDisplay As String = "开发版"
     Public Const BuildType As BuildTypes = BuildTypes.Debug

+ 5 - 22
Plain Craft Launcher 2/Modules/Base/ModNet.vb

@@ -174,7 +174,7 @@ RequestFinished:
         Dim RetryCount As Integer = 0
 Retry:
         Try
-            FileUtils.Write(LocalPath, SendRequest(Url, HttpMethod.Get, SimulateBrowserHeaders:=SimulateBrowserHeaders))
+            FileUtils.Write(LocalPath, SendRequest(Url, HttpMethod.Get, SimulateBrowserHeaders:=SimulateBrowserHeaders, IsFileDownload:=True))
         Catch ex As Exception
             FileUtils.Delete(LocalPath)
             ex.ThrowIfCanceled()
@@ -215,7 +215,9 @@ Retry:
     '基础请求函数
     Private Function SendRequest(Url As String, Method As HttpMethod,
             Optional Content As Object = Nothing, Optional Headers As Dictionary(Of String, String) = Nothing,
-            Optional SimulateBrowserHeaders As Boolean = False, Optional Timeout As Integer = 25000, Optional Encoding As Encoding = Nothing) As Byte()
+            Optional SimulateBrowserHeaders As Boolean = False, Optional Timeout As Integer = 25000, Optional Encoding As Encoding = Nothing,
+            Optional IsFileDownload As Boolean = False) As Byte()
+        EnsureNetworkAllowed(Url, Method, IsFileDownload)
         If RunInUi() AndAlso Not Url.Contains("//127.") Then Throw New Exception("在 UI 线程执行了网络请求")
         Dim Request As HttpRequestMessage = Nothing, CancelToken As CancellationTokenSource = Nothing, Response As HttpResponseMessage = Nothing
         Try
@@ -912,6 +914,7 @@ StartThread:
                 Dim HttpDataCount As Integer = 0
                 If SourcesOnce.Contains(Th.Source) AndAlso Th <> Th.Source.SingleThread Then GoTo SourceBreak
                 HttpRequest = New HttpRequestMessage(HttpMethod.Get, Th.Source.Url)
+                EnsureNetworkAllowed(Th.Source.Url, HttpMethod.Get, IsFileDownload:=True)
                 SecretHeadersSign(Th.Source.Url, HttpRequest, SimulateBrowserHeaders)
                 CancelToken = New CancellationTokenSource(Timeout)
                 If Not Th.IsFirstThread Then HttpRequest.Headers.Range = New Headers.RangeHeaderValue(Th.DownloadStart, Nothing)
@@ -1890,26 +1893,6 @@ FinishExCatch:
 
 #End Region
 
-    ''' <summary>
-    ''' 测试 Ping。失败则返回 -1。
-    ''' </summary>
-    Public Function Ping(Ip As String, Optional Timeout As Integer = 10000, Optional MakeLog As Boolean = True) As Integer
-        Dim PingResult As NetworkInformation.PingReply
-        Try
-            PingResult = (New NetworkInformation.Ping).Send(Ip)
-        Catch ex As Exception
-            If MakeLog Then Logger.Info($"Ping {Ip} 失败:{ex.Message}")
-            Return -1
-        End Try
-        If PingResult.Status = NetworkInformation.IPStatus.Success Then
-            If MakeLog Then Logger.Info($"Ping {Ip} 结束:{PingResult.RoundtripTime}ms")
-            Return PingResult.RoundtripTime
-        Else
-            If MakeLog Then Logger.Info($"Ping {Ip} 失败")
-            Return -1
-        End If
-    End Function
-
     ''' <summary>
     ''' 设置是否应当在正版登录时校验 SSL 证书。
     ''' </summary>

+ 8 - 286
Plain Craft Launcher 2/Modules/Minecraft/ModLaunch.vb

@@ -506,6 +506,9 @@ NextInner:
         End Try
     End Function
     Private Sub McLoginStart(Data As LoaderTask(Of McLoginData, McLoginResult))
+        If Data.Input.Type = McLoginType.Auth OrElse Data.Input.Type = McLoginType.Nide Then
+            Throw New NotSupportedException("此版本仅保留微软正版登录与离线模式。")
+        End If
         McLaunchLog("登录加载已开始")
         '校验登录信息
         Dim CheckResult As String = McLoginAble(Data.Input)
@@ -609,78 +612,7 @@ SkipLogin:
         If ThemeUnlock(10, False) Then MyMsgBox("感谢你对正版游戏的支持!" & vbCrLf & "隐藏主题 跳票红 已解锁!", "提示")
     End Sub
     Private Sub McLoginServerStart(Data As LoaderTask(Of McLoginServer, McLoginResult))
-        Dim Input As McLoginServer = Data.Input
-        Dim NeedRefresh As Boolean = False, WasRefreshed As Boolean = False
-        McLaunchLog("登录方式:" & Input.Description)
-        Data.Progress = 0.05
-        '尝试登录
-        If (Not Data.Input.ForceReselectProfile) AndAlso
-            Settings.Get(Of String)("Cache" & Input.Token & "Username") = Data.Input.UserName AndAlso
-            Settings.Get(Of String)("Cache" & Input.Token & "Pass") = Data.Input.Password AndAlso
-            Settings.Get(Of String)("Cache" & Input.Token & "Access") <> "" AndAlso
-            Settings.Get(Of String)("Cache" & Input.Token & "Client") <> "" AndAlso
-            Settings.Get(Of String)("Cache" & Input.Token & "Uuid") <> "" AndAlso
-            Settings.Get(Of String)("Cache" & Input.Token & "Name") <> "" Then
-            '尝试验证登录
-            Try
-                If Data.IsCanceled Then Throw New OperationCanceledException
-                McLoginRequestValidate(Data)
-                GoTo LoginFinish
-            Catch ex As Exception
-                Logger.Warn(ex, "验证登录失败")
-                If ex.IsBadNetwork Then Throw New Exception("$登录失败:你的网络环境不佳,导致难以连接到海外服务器。" & vbCrLf & "请稍后再试,或使用加速器、VPN 改善网络环境。")
-            End Try
-            Data.Progress = 0.25
-            '尝试刷新登录
-Refresh:
-            Try
-                If Data.IsCanceled Then Throw New OperationCanceledException
-                McLoginRequestRefresh(Data, NeedRefresh)
-                GoTo LoginFinish
-            Catch ex As Exception
-                McLaunchLog("刷新登录失败:" & ex.GetDisplay(True))
-                If WasRefreshed Then Throw New Exception("二轮刷新登录失败", ex)
-            End Try
-            Data.Progress = If(NeedRefresh, 0.85, 0.45)
-        End If
-        '尝试普通登录
-        Try
-            If Data.IsCanceled Then Throw New OperationCanceledException
-            NeedRefresh = McLoginRequestLogin(Data)
-        Catch ex As Exception
-            McLaunchLog("登录失败:" & ex.GetDisplay(True))
-            Throw
-        End Try
-        If NeedRefresh Then
-            McLaunchLog("重新进行刷新登录")
-            WasRefreshed = True
-            Data.Progress = 0.65
-            GoTo Refresh
-        End If
-LoginFinish:
-        Data.Progress = 0.95
-        '保存启动记录
-        Dim Dict As New Dictionary(Of String, String)
-        Dim Emails As New List(Of String)
-        Dim Passwords As New List(Of String)
-        Try
-            If Not Settings.Get(Of String)("Login" & Input.Token & "Email") = "" Then Emails.AddRange(Settings.Get(Of String)("Login" & Input.Token & "Email").Split("¨"))
-            If Not Settings.Get(Of String)("Login" & Input.Token & "Pass") = "" Then Passwords.AddRange(Settings.Get(Of String)("Login" & Input.Token & "Pass").Split("¨"))
-            For i = 0 To Emails.Count - 1
-                Dict.Add(Emails(i), Passwords(i))
-            Next
-            Dict.Remove(Input.UserName)
-            Emails = New List(Of String)(Dict.Keys)
-            Emails.Insert(0, Input.UserName)
-            Passwords = New List(Of String)(Dict.Values)
-            Passwords.Insert(0, Input.Password)
-            Settings.Set("Login" & Input.Token & "Email", Emails.Join("¨"c))
-            Settings.Set("Login" & Input.Token & "Pass", Passwords.Join("¨"c))
-        Catch ex As Exception
-            Logger.Error(ex, "保存启动记录失败", LogBehavior.Toast)
-            Settings.Set("Login" & Input.Token & "Email", "")
-            Settings.Set("Login" & Input.Token & "Pass", "")
-        End Try
+        Throw New NotSupportedException("此版本仅保留微软正版登录与离线模式。")
     End Sub
     Private Sub McLoginLegacyStart(Data As LoaderTask(Of McLoginLegacy, McLoginResult))
         Dim Input As McLoginLegacy = Data.Input
@@ -703,180 +635,9 @@ LoginFinish:
     End Sub
 
     'Server 登录:三种验证方式的请求
-    Private Sub McLoginRequestValidate(ByRef Data As LoaderTask(Of McLoginServer, McLoginResult))
-        McLaunchLog("验证登录开始(Validate, " & Data.Input.Token & ")")
-        '提前缓存信息,否则如果在登录请求过程中退出登录,设置项目会被清空,导致输出存在空值
-        Dim AccessToken As String = Settings.Get(Of String)("Cache" & Data.Input.Token & "Access")
-        Dim ClientToken As String = Settings.Get(Of String)("Cache" & Data.Input.Token & "Client")
-        Dim Uuid As String = Settings.Get(Of String)("Cache" & Data.Input.Token & "Uuid")
-        Dim Name As String = Settings.Get(Of String)("Cache" & Data.Input.Token & "Name")
-        '发送登录请求
-        Dim RequestData As New JObject(
-            New JProperty("accessToken", AccessToken), New JProperty("clientToken", ClientToken), New JProperty("requestUser", True))
-        NetRequestByClientRetry(
-            Data.Input.BaseUrl & "/validate", HttpMethod.Post,
-            Content:=RequestData.ToString(Newtonsoft.Json.Formatting.None),
-            Headers:={{"Accept-Language", "zh-CN"}},
-            ContentType:="application/json; charset=utf-8") '没有返回值的
-        '将登录结果输出
-        Data.Output.AccessToken = AccessToken
-        Data.Output.ClientToken = ClientToken
-        Data.Output.Uuid = Uuid
-        Data.Output.Name = Name
-        Data.Output.Type = Data.Input.Token
-        '不更改缓存,直接结束
-        McLaunchLog("验证登录成功(Validate, " & Data.Input.Token & ")")
-    End Sub
-    Private Sub McLoginRequestRefresh(ByRef Data As LoaderTask(Of McLoginServer, McLoginResult), RequestUser As Boolean)
-        McLaunchLog("刷新登录开始(Refresh, " & Data.Input.Token & ")")
-        Dim LoginJson As JObject = NetRequestByClientRetry(
-               Data.Input.BaseUrl & "/refresh", HttpMethod.Post,
-               Content:=New JObject(
-                   New JProperty("selectedProfile", New JObject(
-                       New JProperty("name", Settings.Get(Of String)($"Cache{Data.Input.Token}Name")),
-                       New JProperty("id", Settings.Get(Of String)($"Cache{Data.Input.Token}Uuid"))
-                   )),
-                   New JProperty("accessToken", Settings.Get(Of String)($"Cache{Data.Input.Token}Access")),
-                   New JProperty("requestUser", True)
-               ).ToString(Newtonsoft.Json.Formatting.None),
-               Headers:={{"Accept-Language", "zh-CN"}},
-               ContentType:="application/json; charset=utf-8", RequireJson:=True).DeserializeJson()
-        '将登录结果输出
-        If LoginJson("selectedProfile") Is Nothing Then Throw New Exception("选择的角色 " & Settings.Get(Of String)("Cache" & Data.Input.Token & "Name") & " 无效!")
-        Data.Output.AccessToken = LoginJson("accessToken").ToString
-        Data.Output.ClientToken = LoginJson("clientToken").ToString
-        Data.Output.Uuid = LoginJson("selectedProfile")("id").ToString
-        Data.Output.Name = LoginJson("selectedProfile")("name").ToString
-        Data.Output.Type = Data.Input.Token
-        '保存缓存
-        Settings.Set("Cache" & Data.Input.Token & "Access", Data.Output.AccessToken)
-        Settings.Set("Cache" & Data.Input.Token & "Client", Data.Output.ClientToken)
-        Settings.Set("Cache" & Data.Input.Token & "Uuid", Data.Output.Uuid)
-        Settings.Set("Cache" & Data.Input.Token & "Name", Data.Output.Name)
-        Settings.Set("Cache" & Data.Input.Token & "Username", Data.Input.UserName)
-        Settings.Set("Cache" & Data.Input.Token & "Pass", Data.Input.Password)
-        McLaunchLog("刷新登录成功(Refresh, " & Data.Input.Token & ")")
-    End Sub
-    Private Function McLoginRequestLogin(ByRef Data As LoaderTask(Of McLoginServer, McLoginResult)) As Boolean
-        Try
-            Dim NeedRefresh As Boolean = False
-            McLaunchLog("登录开始(Login, " & Data.Input.Token & ")")
-            Dim RequestData As New JObject(
-                New JProperty("agent", New JObject(New JProperty("name", "Minecraft"), New JProperty("version", 1))),
-                New JProperty("username", Data.Input.UserName),
-                New JProperty("password", Data.Input.Password),
-                New JProperty("requestUser", True))
-            Dim LoginJson As JObject = NetRequestByClientRetry(
-                Data.Input.BaseUrl & "/authenticate", HttpMethod.Post,
-                Content:=RequestData.ToString(Newtonsoft.Json.Formatting.None),
-                Headers:={{"Accept-Language", "zh-CN"}},
-                ContentType:="application/json; charset=utf-8", RequireJson:=True).DeserializeJson()
-            '检查登录结果
-            If LoginJson("availableProfiles").Count = 0 Then
-                If Data.Input.ForceReselectProfile Then Hint("你还没有创建角色,无法更换!", HintType.Red)
-                Throw New Exception("$你还没有创建角色,请在创建角色后再试!")
-            ElseIf Data.Input.ForceReselectProfile AndAlso LoginJson("availableProfiles").IsSingle Then
-                Hint("你的账户中只有一个角色,无法更换!", HintType.Red)
-            End If
-            Dim SelectedName As String = Nothing
-            Dim SelectedId As String = Nothing
-            If (LoginJson("selectedProfile") Is Nothing OrElse Data.Input.ForceReselectProfile) AndAlso LoginJson("availableProfiles").Count > 1 Then
-                '要求选择档案;优先从缓存读取
-                NeedRefresh = True
-                Dim CacheId As String = Settings.Get(Of String)("Cache" & Data.Input.Token & "Uuid")
-                For Each Profile In LoginJson("availableProfiles")
-                    If Profile("id").ToString = CacheId Then
-                        SelectedName = Profile("name").ToString
-                        SelectedId = Profile("id").ToString
-                        McLaunchLog("根据缓存选择的角色:" & SelectedName)
-                    End If
-                Next
-                '缓存无效,要求玩家选择
-                If SelectedName Is Nothing Then
-                    McLaunchLog("要求玩家选择角色")
-                    RunInUiWait(
-                    Sub()
-                        Dim SelectionControl As New List(Of IMyRadio)
-                        Dim SelectionJson As New List(Of JToken)
-                        For Each Profile In LoginJson("availableProfiles")
-                            SelectionControl.Add(New MyRadioBox With {.Text = Profile("name").ToString})
-                            SelectionJson.Add(Profile)
-                        Next
-                        Dim SelectedIndex As Integer = MyMsgBoxSelect(SelectionControl, "选择使用的角色")
-                        SelectedName = SelectionJson(SelectedIndex)("name").ToString
-                        SelectedId = SelectionJson(SelectedIndex)("id").ToString
-                    End Sub)
-                    McLaunchLog("玩家选择的角色:" & SelectedName)
-                End If
-            Else
-                SelectedName = LoginJson("selectedProfile")("name").ToString
-                SelectedId = LoginJson("selectedProfile")("id").ToString
-            End If
-            '将登录结果输出
-            Data.Output.AccessToken = LoginJson("accessToken").ToString
-            Data.Output.ClientToken = LoginJson("clientToken").ToString
-            Data.Output.Name = SelectedName
-            Data.Output.Uuid = SelectedId
-            Data.Output.Type = Data.Input.Token
-            '保存缓存
-            Settings.Set("Cache" & Data.Input.Token & "Access", Data.Output.AccessToken)
-            Settings.Set("Cache" & Data.Input.Token & "Client", Data.Output.ClientToken)
-            Settings.Set("Cache" & Data.Input.Token & "Uuid", Data.Output.Uuid)
-            Settings.Set("Cache" & Data.Input.Token & "Name", Data.Output.Name)
-            Settings.Set("Cache" & Data.Input.Token & "Username", Data.Input.UserName)
-            Settings.Set("Cache" & Data.Input.Token & "Pass", Data.Input.Password)
-            McLaunchLog("登录成功(Login, " & Data.Input.Token & ")")
-            Return NeedRefresh
-        Catch ex As Exception
-            Dim AllMessage As String = ex.GetDisplay(False)
-            Logger.Info(ex, "登录失败原始错误信息")
-            '读取服务器返回的错误
-            If TypeOf ex Is HttpRequestCodeException Then
-                Dim ErrorMessage As String = Nothing
-                Try
-                    Dim Response = DirectCast(ex, HttpRequestCodeException).Response
-                    If Response.ContainsIgnoreCase("errorMessage") Then ErrorMessage = Response.DeserializeJson()("errorMessage").ToString()
-                Catch
-                End Try
-                If Not String.IsNullOrWhiteSpace(ErrorMessage) Then
-                    If ErrorMessage.Contains("密码错误") OrElse ErrorMessage.ContainsIgnoreCase("Incorrect username or password") Then
-                        '密码错误,退出登录 (#5090)
-                        McLaunchLog("密码错误,退出登录")
-                        Select Case Data.Input.Type
-                            Case McLoginType.Auth
-                                RunInUi(AddressOf PageLoginAuthSkin.ExitLogin)
-                            Case McLoginType.Nide
-                                RunInUi(AddressOf PageLoginNideSkin.ExitLogin)
-                        End Select
-                    End If
-                    Throw New Exception("$登录失败:" & ErrorMessage)
-                End If
-            End If
-            '通用关键字检测
-            If AllMessage.Contains("(403)") Then
-                Select Case Data.Input.Type
-                    Case McLoginType.Auth
-                        Throw New Exception("$登录失败,以下为可能的原因:" & vbCrLf &
-                                            " - 输入的账号或密码错误。" & vbCrLf &
-                                            " - 登录尝试过于频繁,导致被暂时屏蔽。请不要操作,等待 10 分钟后再试。" & vbCrLf &
-                                            " - 只注册了账号,但没有在皮肤站新建角色。")
-                    Case McLoginType.Nide
-                        Throw New Exception("$登录失败,以下为可能的原因:" & vbCrLf &
-                                            " - 输入的账号或密码错误。" & vbCrLf &
-                                            " - 密码错误次数过多,导致被暂时屏蔽。请不要操作,等待 10 分钟后再试。" & vbCrLf &
-                                            If(Data.Input.UserName.Contains("@"), "", " - 登录账号应为邮箱或统一通行证账号,而非游戏角色 ID。" & vbCrLf) &
-                                            " - 只注册了账号,但没有加入对应服务器。")
-                End Select
-            ElseIf ex.IsBadNetwork() Then
-                Throw New Exception("$登录失败:你的网络环境不佳,导致难以连接到海外服务器。" & vbCrLf & "请稍后再试,或使用加速器、VPN 改善网络环境。")
-            ElseIf ex.Message.StartsWithF("$") Then
-                Throw
-            Else
-                Throw New Exception("登录失败:" & ex.Message, ex)
-            End If
-            Return False
-        End Try
-    End Function
+
+
+
 
     '微软登录步骤 1,原始登录:获取 DeviceCode 并开启登录网页
     Private Function MsLoginStep1New(Data As LoaderTask(Of McLoginMs, McLoginResult)) As (OAuthAccessToken As String, OAuthRefreshToken As String)
@@ -1138,27 +899,7 @@ Retry:
     End Function
     '根据用户名返回对应 UUID,需要多线程
     Public Function McLoginMojangUuid(Name As String, ThrowOnNotFound As Boolean)
-        If Name.Trim.Length = 0 Then Return New String("0"c, 32)
-        '从缓存获取
-        Dim Uuid As String = ReadIni(PathTemp & "Cache\Uuid\Mojang.ini", Name, "")
-        If Len(Uuid) = 32 Then Return Uuid
-        '从官网获取
-        Try
-            Dim GotJson As JObject = NetRequestByClientRetry("https://api.mojang.com/users/profiles/minecraft/" & Name, RequireJson:=True).DeserializeJson()
-            If GotJson Is Nothing Then Throw New FileNotFoundException("正版玩家档案不存在(" & Name & ")")
-            Uuid = If(GotJson("id"), "")
-        Catch ex As Exception
-            Logger.Warn(ex, $"从官网获取正版 Uuid 失败({Name})")
-            If Not ThrowOnNotFound AndAlso ex.GetType.Name = "FileNotFoundException" Then
-                Uuid = McLoginLegacyUuid(Name) '玩家档案不存在
-            Else
-                Throw New Exception("从官网获取正版 Uuid 失败", ex)
-            End If
-        End Try
-        '写入缓存
-        If Not Len(Uuid) = 32 Then Throw New Exception("获取的正版 Uuid 长度不足(" & Uuid & ")")
-        WriteIni(PathTemp & "Cache\Uuid\Mojang.ini", Name, Uuid)
-        Return Uuid
+        Return McLoginLegacyUuid(Name)
     End Function
     Public Function McLoginLegacyUuid(Name As String)
         Dim FullUuid As String = Name.Length.ToString("X").EnsureLength("0", 16) & Name.GetStableHashCode().ToString("X").EnsureLength("0", 16)
@@ -1268,25 +1009,6 @@ NextInstance:
         '重新分割字符串并去重
         Args = DeduplicateJavaArguments(SplitJavaArguments(Args.Join(" "c).Replace("McEmu= ", "McEmu=") & " " & CustomArg).ToList, True)
 
-        '统一通行证
-        If McLoginLoader.Output.Type = "Nide" Then
-            Args.Add("-javaagent:""${pure_directory}\nide8auth.jar""=""" & Settings.Get(Of String)("VersionServerNide", Instance:=McInstanceSelected) & """")
-        End If
-        'Authlib-Injector
-        If McLoginLoader.Output.Type = "Auth" Then
-            If McLaunchJavaSelected.Version.Major >= 6 Then Args.Add("-Djavax.net.ssl.trustStoreType=WINDOWS-ROOT") '信任系统根证书(5252#)
-            Dim Server As String = Settings.Get(Of String)("VersionServerAuthServer", Instance:=McInstanceSelected)
-            Try
-                McLaunchLog($"开始 Authlib Injector Prefetch")
-                Dim Response As String = NetRequestByClientRetry(Server, Encoding:=Encoding.UTF8)
-                Args.Add("-javaagent:""${pure_directory}\authlib-injector.jar""=""" & Server & """")
-                Args.Add("-Dauthlibinjector.side=client")
-                Args.Add("-Dauthlibinjector.yggdrasil.prefetched=" & Convert.ToBase64String(Encoding.UTF8.GetBytes(Response)))
-            Catch ex As Exception
-                Throw New Exception("无法连接到第三方登录服务器(" & If(Server, Nothing) & ")", ex)
-            End Try
-        End If
-
         'JLW
         '非 GBK 编码下命令行参数乱码的问题(JDK-8272352)
         Dim UseJLW As Boolean =

+ 4 - 100
Plain Craft Launcher 2/Modules/Minecraft/ModMinecraft.vb

@@ -1403,68 +1403,15 @@ OnLoaded:
     ''' 获取 Uuid 对应的皮肤文件地址,失败将抛出异常。
     ''' </summary>
     Public Function McSkinGetAddress(UUID As String, Type As String) As String
-        If UUID = "" Then Throw New Exception("UUID 为空。")
-        If UUID.StartsWithF("00000") AndAlso Type <> "Auth" Then Throw New Exception("离线 UUID 无正版皮肤文件:" & UUID)
-        '尝试读取缓存
-        Dim CacheSkinAddress As String = ReadIni(PathTemp & "Cache\Skin\Index" & Type & ".ini", UUID)
-        If Not CacheSkinAddress = "" Then Return CacheSkinAddress
-        '获取皮肤地址
-        Dim Url As String
-        Select Case Type
-            Case "Mojang", "Ms"
-                Url = "https://sessionserver.mojang.com/session/minecraft/profile/"
-            Case "Nide"
-                Url = "https://auth.mc-user.com:233/" & If(McInstanceSelected Is Nothing, Settings.Get(Of String)("CacheNideServer"), Settings.Get(Of String)("VersionServerNide", Instance:=McInstanceSelected)) & "/sessionserver/session/minecraft/profile/"
-            Case "Auth"
-                Url = If(McInstanceSelected Is Nothing, Settings.Get(Of String)("CacheAuthServerServer"), Settings.Get(Of String)("VersionServerAuthServer", Instance:=McInstanceSelected)) & "/sessionserver/session/minecraft/profile/"
-            Case Else
-                Throw New ArgumentException("皮肤地址种类无效:" & If(Type, "null"))
-        End Select
-        Dim SkinString = NetRequestByClientRetry(Url & UUID, RequireJson:=True)
-        If SkinString = "" Then Throw New Exception("皮肤返回值为空,可能是未设置自定义皮肤的用户")
-        '处理皮肤地址
-        Dim SkinValue As String
-        Try
-            For Each SkinProperty In SkinString.DeserializeJson()("properties")
-                If SkinProperty("name") = "textures" Then
-                    SkinValue = SkinProperty("value")
-                    Exit Try
-                End If
-            Next
-            Throw New Exception("未从皮肤返回值中找到符合条件的 Property")
-        Catch ex As Exception
-            Logger.Warn(ex, $"无法完成解析的皮肤返回值,可能是未设置自定义皮肤的用户:{SkinString}")
-            Throw New Exception("皮肤返回值中不包含皮肤数据项,可能是未设置自定义皮肤的用户", ex)
-        End Try
-        SkinString = Encoding.UTF8.GetString(Convert.FromBase64String(SkinValue))
-        Dim SkinJson As JObject = SkinString.Lower.DeserializeJson()
-        If SkinJson("textures") Is Nothing OrElse SkinJson("textures")("skin") Is Nothing OrElse SkinJson("textures")("skin")("url") Is Nothing Then
-            Throw New Exception("用户未设置自定义皮肤")
-        Else
-            Dim SkinUrl As String = SkinJson("textures")("skin")("url").ToString
-            SkinValue = If(SkinUrl.Contains("minecraft.net/"), SkinUrl.Replace("http://", "https://"), SkinUrl)
-        End If
-        '保存缓存
-        WriteIni(PathTemp & "Cache\Skin\Index" & Type & ".ini", UUID, SkinValue)
-        Logger.Info($"UUID {UUID} 对应的皮肤文件为 {SkinValue}")
-        Return SkinValue
+        Return PathImage & "Skins/" & McSkinSex(UUID) & ".png"
     End Function
 
-    Private ReadOnly McSkinDownloadLock As New Object
     ''' <summary>
-    ''' 从 Url 下载皮肤,可能会使用缓存。返回本地文件路径,失败将抛出异常。
+    ''' 仅使用本地皮肤;在线地址回退到内置皮肤。
     ''' </summary>
     Public Function McSkinDownload(Address As String) As String
-        Dim SkinName As String = PathUtils.GetLastPart(Address)
-        Dim FileAddress As String = PathTemp & "Cache\Skin\" & Address.GetStableHashCode() & ".png"
-        SyncLock McSkinDownloadLock
-            If Not FileUtils.Exists(FileAddress) Then
-                NetDownloadByClient(Address, FileAddress & ".PCLDownloading")
-                FileUtils.Move(FileAddress & ".PCLDownloading", FileAddress)
-                Logger.Info($"皮肤下载成功:{FileAddress}")
-            End If
-            Return FileAddress
-        End SyncLock
+        If Address.StartsWith("pack://", StringComparison.OrdinalIgnoreCase) OrElse FileUtils.Exists(Address) Then Return Address
+        Return PathImage & "Skins/Steve.png"
     End Function
 
     ''' <summary>
@@ -1780,49 +1727,6 @@ OnLoaded:
         'Library 文件
         Result.AddRange(McLibNetFilesFromTokens(McLibListGet(Instance, False)))
 
-        '统一通行证文件
-        If Settings.Get(Of Integer)("VersionServerLogin", Instance:=Instance) = 3 Then
-            Dim TargetFile As String = PathPure.Value & "nide8auth.jar"
-            Try
-                '测试链接:https://auth.mc-user.com:233/00000000000000000000000000000000/
-                Logger.Info("开始获取统一通行证下载信息")
-                Dim DownloadInfo As JObject = NetRequestByClientRetry("https://auth.mc-user.com:233/" & Settings.Get(Of String)("VersionServerNide", Instance:=Instance), RequireJson:=True).DeserializeJson()
-                Result.Add(New NetFile(
-                    Urls:={"https://login.mc-user.com:233/index/jar"},
-                    LocalPath:=TargetFile,
-                    Checker:=New FileChecker With {.Hash = DownloadInfo("jarHash").ToString}))
-            Catch ex As Exception
-                If FileUtils.Exists(TargetFile) Then
-                    Logger.Warn(ex, "获取统一通行证下载信息失败")
-                Else
-                    Throw New Exception("获取统一通行证下载信息失败", ex)
-                End If
-            End Try
-        End If
-
-        'Authlib-Injector 文件
-        If Settings.Get(Of Integer)("VersionServerLogin", Instance:=Instance) = 4 Then
-            Dim TargetFile = PathPure.Value & "authlib-injector.jar"
-            Try
-                Logger.Info("开始获取 Authlib-Injector 下载信息")
-                Dim DownloadInfo As JObject = NetRequestByClientRetry(
-                        "https://authlib-injector.yushi.moe/artifact/latest.json",
-                        BackupUrl:="https://bmclapi2.bangbang93.com/mirrors/authlib-injector/artifact/latest.json", RequireJson:=True).DeserializeJson()
-                Dim DownloadAddress As String = DownloadInfo("download_url").ToString.
-                        Replace("bmclapi2.bangbang93.com/mirrors/authlib-injector", "authlib-injector.yushi.moe")
-                Result.Add(New NetFile({
-                    DownloadAddress,
-                    DownloadAddress.Replace("authlib-injector.yushi.moe", "bmclapi2.bangbang93.com/mirrors/authlib-injector")
-                }, TargetFile, New FileChecker With {.Hash = DownloadInfo("checksums")("sha256").ToString}))
-            Catch ex As Exception
-                If FileUtils.Exists(TargetFile) Then
-                    Logger.Warn(ex, "获取 Authlib-Injector 下载信息失败")
-                Else
-                    Throw New Exception("获取 Authlib-Injector 下载信息失败", ex)
-                End If
-            End Try
-        End If
-
         '跳过校验
         If ShouldIgnoreFileCheck(Instance) Then
             Logger.Warn("设置要求尽量忽略文件检查,这可能会保留有误的文件")

+ 1 - 28
Plain Craft Launcher 2/Modules/ModEvent.vb

@@ -363,34 +363,7 @@ Public Class CustomEvent
 
         '网页确认
         If RelativeUrl.StartsWithF("http", True) Then
-            If RunInUi() Then
-                Throw New Exception("能打开联网帮助页面的 MyListItem 必须手动设置 Title、Info 属性!")
-            End If
-            '获取文件名
-            Dim RawFileName As String
-            Try
-                RawFileName = PathUtils.GetLastPart(RelativeUrl)
-                If Not PathUtils.GetExtension(RawFileName) = "json" Then Throw New Exception("未指向 .json 后缀的文件")
-            Catch ex As Exception
-                Throw New Exception("联网帮助页面须指向一个帮助 JSON 文件,并在同路径下包含相应 XAML 文件!" & vbCrLf &
-                                    "例如:" & vbCrLf &
-                                    " - https://www.baidu.com/test.json(填写这个路径)" & vbCrLf &
-                                    " - https://www.baidu.com/test.xaml(同时也需要包含这个文件)", ex)
-            End Try
-            '下载文件
-            Dim LocalTemp As String = RequestTaskTempFolder() & RawFileName
-            Logger.Info($"转换网络资源:{RelativeUrl} -> {LocalTemp}")
-            Try
-                NetDownloadByClient(RelativeUrl, LocalTemp)
-                NetDownloadByClient(RelativeUrl.Replace(".json", ".xaml"), LocalTemp.Replace(".json", ".xaml"))
-            Catch ex As Exception
-                Throw New Exception("下载指定的文件失败!" & vbCrLf &
-                                    "注意,联网帮助页面须指向一个帮助 JSON 文件,并在同路径下包含相应 XAML 文件!" & vbCrLf &
-                                    "例如:" & vbCrLf &
-                                    " - https://www.baidu.com/test.json(填写这个路径)" & vbCrLf &
-                                    " - https://www.baidu.com/test.xaml(同时也需要包含这个文件)", ex)
-            End Try
-            RelativeUrl = LocalTemp
+            Throw New NotSupportedException("此版本仅支持本地帮助页面,已移除远程页面下发。")
         End If
         RelativeUrl = RelativeUrl.Replace("/", "\").Lower.TrimStart("\")
 

+ 1 - 16
Plain Craft Launcher 2/Modules/ModMain.vb

@@ -984,22 +984,7 @@ NextFile:
 
     '遥测
     Public Sub Telemetry([Event] As String, ParamArray Datas As String())
-        If BuildType = BuildTypes.Debug Then Return '开发版不上传遥测
-        If Not Settings.Get(Of Boolean)("SystemSystemTelemetry") Then Return '用户关闭了遥测
-        If Not ClsBaseUrl.StartsWithF("http") Then Return '开源版没有设置遥测地址
-        RunInNewThread(
-        Sub()
-            Try
-                Logger.Info($"匿名数据上报:{[Event]}")
-                Dim Url As String = $"{ClsBaseUrl}&Event={StringUtils.UrlEscape([Event])}"
-                For i = 0 To Datas.Length - 1 Step 2
-                    Url &= "&" & StringUtils.UrlEscape(Datas(i)) & "=" & StringUtils.UrlEscape(Datas(i + 1).ReplaceLineEndings(vbLf, mergeMultiple:=True))
-                Next
-                NetRequestByClient(Url, MakeLog:=False)
-            Catch ex As Exception
-                Logger.Warn(ex, "匿名数据上报失败")
-            End Try
-        End Sub, "Telemetry", ThreadPriority.Lowest)
+        '该修改版不收集或上传遥测。
     End Sub
 
 #End Region

+ 109 - 21
Plain Craft Launcher 2/Modules/ModSecret.vb

@@ -3,19 +3,30 @@
 Friend Module ModSecret
 
     '标注 PCL 的不同分支,仅用于替换标记
-    Public Const VersionBranchMain As String = "OpenSource"
+    Public Const VersionBranchMain As String = "Red"
     '在开源版的注册表与常规版的注册表隔离,以防数据冲突
-    Public Const RegFolder As String = "PCLDebug"
+    Public Const RegFolder As String = "PCLRed"
     '用于微软登录的 ClientId
-    Public OAuthClientId As String = If(Environment.GetEnvironmentVariable("PCL_MS_CLIENT_ID"), "")
+    Public OAuthClientId As String = If(Environment.GetEnvironmentVariable("PCL_MS_CLIENT_ID"), "fe72edc2-3a6f-4280-90e8-e2beb64ce7e1")
     'CurseForge API Key
-    Public CurseForgeAPIKey As String = If(Environment.GetEnvironmentVariable("PCL_CURSEFORGE_API_KEY"), "")
+    Public CurseForgeAPIKey As String = ReadBuildCredential("CurseForgeAPIKey", "PCL_CURSEFORGE_API_KEY")
+
+    Private Function ReadBuildCredential(Key As String, EnvironmentKey As String) As String
+        Dim Override = Environment.GetEnvironmentVariable(EnvironmentKey)
+        If Override IsNot Nothing Then Return Override
+        Using Resource = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("PCL.RedBuildCredentials.json")
+            If Resource Is Nothing Then Return ""
+            Using Reader As New StreamReader(Resource, Encoding.UTF8)
+                Return If(JObject.Parse(Reader.ReadToEnd())(Key)?.ToString(), "")
+            End Using
+        End Using
+    End Function
     '用于匿名数据收集的腾讯云日志服务上报 URL,形如 https://{region}.cls.tencentcs.com/track?topic_id={topic_id}
     Public Const ClsBaseUrl As String = ""
 
 #Region "网络鉴权"
 
-    Public Const RestrictMcDownload As Boolean = True
+    Public Const RestrictMcDownload As Boolean = False
     Friend Function SecretCdnSign(UrlWithMark As String) As String
         If Not UrlWithMark.EndsWithF("{CDN}") Then Return UrlWithMark
         Return UrlWithMark.Replace("{CDN}", "").Replace(" ", "%20")
@@ -64,11 +75,57 @@ Friend Module ModSecret
     Public ThemeNow As Integer = -1
     Public ColorHue As Integer = 210, ColorSat As Integer = 85, ColorLightAdjust As Integer = 0, ColorHueTopbarDelta As OneOf(Of Integer, Integer()) = 0
     Public ThemeDontClick As Integer = 0
+    Private ReadOnly LuckyThemeHue As Integer = RandomInteger(0, 359)
 
     Public Sub ThemeRefresh(Optional NewTheme As Integer = -1)
         Try
+            If NewTheme >= 0 AndAlso Not ThemeCheckOne(NewTheme) Then NewTheme = DefaultTheme
             If ThemeNow = NewTheme AndAlso NewTheme >= 0 Then Return
             If NewTheme >= 0 Then ThemeNow = NewTheme
+            If Not ThemeCheckOne(ThemeNow) Then ThemeNow = DefaultTheme
+
+            ColorHue = 210
+            ColorSat = 85
+            ColorLightAdjust = 0
+            ColorHueTopbarDelta = 0
+            Select Case ThemeNow
+                Case 1 '甜柠青
+                    ColorHue = 170 : ColorSat = 75 : ColorLightAdjust = -5
+                Case 2 '小草绿
+                    ColorHue = 125 : ColorSat = 70 : ColorLightAdjust = -6
+                Case 3 '菠萝黄
+                    ColorHue = 50 : ColorSat = 90 : ColorLightAdjust = -7
+                Case 4 '橡木棕
+                    ColorHue = 28 : ColorSat = 55 : ColorLightAdjust = -9
+                Case 5 '玄素黑
+                    ColorSat = 5 : ColorLightAdjust = -12
+                Case 6 '铁杆粉
+                    ColorHue = 335 : ColorSat = 75
+                Case 7 '神秘紫
+                    ColorHue = 270 : ColorSat = 65
+                Case 8 '秋仪金
+                    ColorHue = 42 : ColorLightAdjust = -8 : ColorHueTopbarDelta = 15
+                Case 9 '活跃橙
+                    ColorHue = 25 : ColorSat = 95
+                Case 10 '跳票红
+                    ColorHue = 355 : ColorSat = 80
+                Case 11 '极客蓝
+                    ColorHue = 220 : ColorLightAdjust = -14
+                Case 12 '滑稽彩:随时间平滑改变色相
+                    ColorHue = CInt((Date.UtcNow.Ticks \ TimeSpan.TicksPerMillisecond \ 50) Mod 360)
+                    ColorSat = 75 : ColorHueTopbarDelta = 35
+                Case 13 '欧皇彩:每次启动随机配色
+                    ColorHue = LuckyThemeHue : ColorSat = 75 : ColorHueTopbarDelta = 120
+                Case 14 '自定义
+                    ColorHue = Settings.Get(Of Integer)("UiLauncherHue")
+                    ColorSat = Settings.Get(Of Integer)("UiLauncherSat")
+                    ColorLightAdjust = Settings.Get(Of Integer)("UiLauncherLight") - 20
+                    ColorHueTopbarDelta = Settings.Get(Of Integer)("UiLauncherDelta")
+                Case 15 '苏维埃红
+                    ColorHue = 0 : ColorSat = 90 : ColorLightAdjust = -6 : ColorHueTopbarDelta = 7
+                Case 42 '纪念彩
+                    ColorHue = 285 : ColorSat = 80 : ColorHueTopbarDelta = 120
+            End Select
 
             Color1 = New MyColor().FromHSL2(ColorHue, ColorSat * 0.2, 25 + ColorLightAdjust * 0.3)
             Color2 = New MyColor().FromHSL2(ColorHue, ColorSat, 45 + ColorLightAdjust)
@@ -110,21 +167,43 @@ Friend Module ModSecret
     Public Sub ThemeRefreshMain()
         RunInUi(
         Sub()
-            If Not FrmMain.IsLoaded Then Return
+            If FrmMain Is Nothing OrElse Not FrmMain.IsLoaded Then Return
             '顶部条背景
             Dim Brush = New LinearGradientBrush With {.EndPoint = New Point(1, 0), .StartPoint = New Point(0, 0)}
             Dim Deltas = ColorHueTopbarDelta.Switch(Function(d) New Integer() {-d, 0, d}, Function(d) d)
             Brush.GradientStops.Add(New GradientStop With {.Offset = 0, .Color = New MyColor().FromHSL2(ColorHue + Deltas(0), ColorSat, 48 + ColorLightAdjust)})
             Brush.GradientStops.Add(New GradientStop With {.Offset = 0.5, .Color = New MyColor().FromHSL2(ColorHue + Deltas(1), ColorSat, 54 + ColorLightAdjust)})
             Brush.GradientStops.Add(New GradientStop With {.Offset = 1, .Color = New MyColor().FromHSL2(ColorHue + Deltas(2), ColorSat, 48 + ColorLightAdjust)})
+            If ThemeNow = 5 Then
+                Brush.GradientStops.Clear()
+                Brush.GradientStops.Add(New GradientStop(New MyColor().FromHSL2(ColorHue, ColorSat, 25), 0))
+                Brush.GradientStops.Add(New GradientStop(New MyColor().FromHSL2(ColorHue, ColorSat, 15), 0.5))
+                Brush.GradientStops.Add(New GradientStop(New MyColor().FromHSL2(ColorHue, ColorSat, 25), 1))
+            ElseIf ThemeNow = DefaultTheme Then
+                Brush.GradientStops.Clear()
+                Brush.GradientStops.Add(New GradientStop(Color.FromRgb(142, 12, 24), 0))
+                Brush.GradientStops.Add(New GradientStop(Color.FromRgb(198, 23, 38), 0.55))
+                Brush.GradientStops.Add(New GradientStop(Color.FromRgb(163, 13, 26), 1))
+            End If
             FrmMain.PanTitle.Background = Brush
             FrmMain.PanTitle.Background.Freeze()
+            FrmMain.ImgTitle.Source = If(ThemeNow >= 5 AndAlso ThemeNow <> 14 AndAlso ThemeNow <> DefaultTheme, $"{PathImage}Themes/{ThemeNow}.png", Nothing)
+            FrmMain.ImgTitle.Opacity = If(ThemeNow = 13, 0.25, 0.5)
+            FrmMain.CommunismEmblem.Visibility = If(ThemeNow = DefaultTheme, Visibility.Visible, Visibility.Collapsed)
+            FrmMain.CommunismRule.Visibility = FrmMain.CommunismEmblem.Visibility
+            FrmMain.CommunismBackdrop.Visibility = FrmMain.CommunismEmblem.Visibility
+            If FrmLaunchRight IsNot Nothing Then FrmLaunchRight.CommunismWelcome.Visibility = FrmMain.CommunismEmblem.Visibility
             '主页面背景
             If Settings.Get(Of Boolean)("UiBackgroundColorful") Then
                 Brush = New LinearGradientBrush With {.EndPoint = New Point(0.1, 1), .StartPoint = New Point(0.9, 0)}
                 Brush.GradientStops.Add(New GradientStop With {.Offset = -0.1, .Color = New MyColor().FromHSL2(ColorHue - 15, ColorSat * 0.8, 91)})
                 Brush.GradientStops.Add(New GradientStop With {.Offset = 0.4, .Color = New MyColor().FromHSL2(ColorHue, ColorSat * 0.8, 91)})
                 Brush.GradientStops.Add(New GradientStop With {.Offset = 1.1, .Color = New MyColor().FromHSL2(ColorHue + 15, ColorSat * 0.8, 91)})
+                If ThemeNow = DefaultTheme Then
+                    Brush.GradientStops.Clear()
+                    Brush.GradientStops.Add(New GradientStop(Color.FromRgb(255, 249, 239), 0))
+                    Brush.GradientStops.Add(New GradientStop(Color.FromRgb(247, 230, 219), 1))
+                End If
                 FrmMain.PanForm.Background = Brush
             Else
                 FrmMain.PanForm.Background = New MyColor(245, 245, 245)
@@ -133,9 +212,29 @@ Friend Module ModSecret
         End Sub)
     End Sub
     Friend Sub ThemeCheckAll(EffectSetup As Boolean)
+        RunInUi(
+        Sub()
+            If Not ThemeCheckOne(Settings.Get(Of Integer)("UiLauncherTheme")) Then Settings.Set("UiLauncherTheme", DefaultTheme)
+            Const AllThemes As String = "0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|42"
+            If Settings.Get(Of String)("UiLauncherThemeHide2") <> AllThemes Then Settings.Set("UiLauncherThemeHide2", AllThemes)
+            If Not EffectSetup OrElse FrmSetupUI Is Nothing Then Return
+            For Each Control As UIElement In FrmSetupUI.PanLauncherTheme.Children
+                If Not TypeOf Control Is MyRadioBox OrElse Control Is FrmSetupUI.RadioLauncherTheme5Gray Then Continue For
+                Dim Radio = DirectCast(Control, MyRadioBox)
+                Radio.IsEnabled = True
+                Radio.Opacity = 1
+                Radio.ToolTip = "所有主题均已开放"
+            Next
+            FrmSetupUI.RadioLauncherTheme5Gray.Visibility = Visibility.Collapsed
+            FrmSetupUI.LabLauncherTheme5Unlock.Visibility = Visibility.Collapsed
+            FrmSetupUI.LabLauncherTheme8Copy.Visibility = Visibility.Collapsed
+            FrmSetupUI.LabLauncherTheme9Copy.Visibility = Visibility.Collapsed
+            FrmSetupUI.LabLauncherTheme11Click.Visibility = Visibility.Collapsed
+            FrmSetupUI.PanLauncherHide.Visibility = Visibility.Collapsed
+        End Sub)
     End Sub
     Friend Function ThemeCheckOne(Id As Integer) As Boolean
-        Return True
+        Return (Id >= 0 AndAlso Id <= DefaultTheme) OrElse Id = 42
     End Function
     Friend Function ThemeUnlock(Id As Integer, Optional ShowDoubleHint As Boolean = True, Optional UnlockHint As String = Nothing) As Boolean
         Return False
@@ -146,7 +245,7 @@ Friend Module ModSecret
 #Region "更新"
 
     Friend Sub UpdateCheckByButton()
-        Hint("该版本中不包含更新功能……")
+        Hint("PCL Communist 已移除启动器远程更新和配置下发")
     End Sub
 
     Friend IsUpdateWaitingRestart As Boolean = False
@@ -160,23 +259,12 @@ Friend Module ModSecret
     ''' 如果不是,则下载一个。
     ''' </summary>
     Friend Sub DownloadLatestPCL(Optional LoaderToSyncProgress As LoaderBase = Nothing)
-        '注意:如果要自行实现这个功能,请换用另一个文件路径,以免与官方版本冲突
+        '整合包携带当前本地版本,不向远程服务器请求或替换程序。
+        FileUtils.Copy(PathExe, PathTemp & "Latest.exe")
     End Sub
 
 #End Region
 
-#Region "联网配置"
-
-    ''' <summary>
-    ''' 联网获取的配置信息。
-    ''' 若获取失败或仍在获取中,可能为 Nothing。
-    ''' </summary>
-    Public ServerConfig As JObject
-
-    Public ServerLoader As New LoaderTask(Of Integer, Integer)("PCL 配置更新", Sub() Logger.Info("该版本中不包含更新通知功能……"), Priority:=ThreadPriority.BelowNormal) With
-        {.ReloadTimeout = 1000 * 60 * 60} '超时 1 小时
-
-#End Region
 
 #Region "赞助等级"
 

+ 52 - 0
Plain Craft Launcher 2/Modules/RedPolicy.vb

@@ -0,0 +1,52 @@
+'仅保留下载(包括下载所需的版本/依赖元数据)与微软正版登录。
+Public Module RedPolicy
+    Public Const DefaultTheme As Integer = 15
+
+    Private ReadOnly MetadataHosts As New HashSet(Of String)(StringComparer.OrdinalIgnoreCase) From {
+        "launchermeta.mojang.com", "piston-meta.mojang.com", "launcher.mojang.com",
+        "bmclapi2.bangbang93.com", "optifine.net", "files.minecraftforge.net",
+        "maven.minecraftforge.net", "maven.neoforged.net", "meta.fabricmc.net",
+        "dl.liteloader.com", "jenkins.liteloader.com", "api.modrinth.com",
+        "api.curseforge.com", "mod.mcimirror.top", "files.mcimirror.top"
+    }
+
+    Public Function IsNetworkAllowed(Url As String, Method As HttpMethod, Optional IsFileDownload As Boolean = False) As Boolean
+        Dim Address As Uri = Nothing
+        If Not Uri.TryCreate(Url.Replace("{CDN}", ""), UriKind.Absolute, Address) Then Return False
+        If Address.Scheme <> Uri.UriSchemeHttps AndAlso Address.Scheme <> Uri.UriSchemeHttp Then Return False
+        Dim Host = Address.IdnHost
+        Dim RequestMethod = If(Method, HttpMethod.Get).Method.ToUpperInvariant()
+        Dim ReadOnlyRequest = RequestMethod = "GET" OrElse RequestMethod = "HEAD"
+        '即使被当作文件下载,也不允许访问旧的公告、更新、遥测服务。
+        If Host = "pcl2-server-1253424809.file.myqcloud.com" OrElse
+           Host.EndsWith(".cls.tencentcs.com", StringComparison.OrdinalIgnoreCase) OrElse
+           Host = "cls.tencentcs.com" Then Return False
+        If IsFileDownload Then Return ReadOnlyRequest
+        If Address.Scheme <> Uri.UriSchemeHttps Then Return False
+
+        Select Case Host.ToLowerInvariant()
+            Case "login.microsoftonline.com"
+                Return RequestMethod = "POST" AndAlso
+                    (Address.AbsolutePath = "/consumers/oauth2/v2.0/devicecode" OrElse Address.AbsolutePath = "/consumers/oauth2/v2.0/token")
+            Case "login.live.com"
+                Return RequestMethod = "POST" AndAlso Address.AbsolutePath = "/oauth20_token.srf"
+            Case "user.auth.xboxlive.com"
+                Return RequestMethod = "POST" AndAlso Address.AbsolutePath = "/user/authenticate"
+            Case "xsts.auth.xboxlive.com"
+                Return RequestMethod = "POST" AndAlso Address.AbsolutePath = "/xsts/authorize"
+            Case "api.minecraftservices.com"
+                Return (RequestMethod = "POST" AndAlso Address.AbsolutePath = "/authentication/login_with_xbox") OrElse
+                    (ReadOnlyRequest AndAlso (Address.AbsolutePath = "/entitlements/mcstore" OrElse Address.AbsolutePath = "/minecraft/profile"))
+        End Select
+        If Not MetadataHosts.Contains(Host) Then Return False
+        '资源平台使用 POST 批量查询文件哈希、依赖和下载版本。
+        Return ReadOnlyRequest OrElse (RequestMethod = "POST" AndAlso
+            (Host = "api.modrinth.com" OrElse Host = "api.curseforge.com" OrElse Host = "mod.mcimirror.top"))
+    End Function
+
+    Public Sub EnsureNetworkAllowed(Url As String, Method As HttpMethod, Optional IsFileDownload As Boolean = False)
+        If Not IsNetworkAllowed(Url, Method, IsFileDownload) Then
+            Throw New NotSupportedException("此修改版仅允许下载及微软正版登录,已禁用此联网功能。")
+        End If
+    End Sub
+End Module

+ 3 - 3
Plain Craft Launcher 2/My Project/AssemblyInfo.vb

@@ -8,10 +8,10 @@ Imports System.Runtime.InteropServices
 
 '查看程序集特性的值
 
-<Assembly: AssemblyTitle("Plain Craft Launcher 启动器")>
-<Assembly: AssemblyDescription("Minecraft 启动器")>
+<Assembly: AssemblyTitle("PCL Communist")>
+<Assembly: AssemblyDescription("PCL Communist · Minecraft 启动器(第三方独立修改)")>
 <Assembly: AssemblyCompany("")>
-<Assembly: AssemblyProduct("Plain Craft Launcher")>
+<Assembly: AssemblyProduct("PCL Communist")>
 <Assembly: AssemblyCopyright("Copyright © 成都瓜皮龙科技有限公司")>
 <Assembly: AssemblyConfiguration("PCL2 Config Mark")>
 <Assembly: ComVisible(False)>

+ 2 - 2
Plain Craft Launcher 2/My Project/Resources.resx

@@ -131,7 +131,7 @@
     <value>..\Resources\ThrottleDebounce.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
   </data>
   <data name="Custom" type="System.Resources.ResXFileRef, System.Windows.Forms">
-    <value>..\resources\custom.xaml;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+    <value>..\Resources\Custom.xaml;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
   </data>
   <data name="ForgeInstaller" type="System.Resources.ResXFileRef, System.Windows.Forms">
     <value>..\Resources\forge-installer.jar;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
@@ -161,7 +161,7 @@
     <value>..\Resources\Newtonsoft.Json.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
   </data>
   <data name="Ookii.Dialogs.Wpf" type="System.Resources.ResXFileRef, System.Windows.Forms">
-    <value>..\resources\ookii.dialogs.wpf.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+    <value>..\Resources\Ookii.Dialogs.Wpf.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
   </data>
   <data name="PCLCS" type="System.Resources.ResXFileRef, System.Windows.Forms">
     <value>..\..\PCLCS\bin\PCLCS.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>

+ 1 - 3
Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSetup.xaml

@@ -171,8 +171,6 @@
                             <local:MyComboBoxItem Content="正版登录或离线登录" IsSelected="True" />
                             <local:MyComboBoxItem Content="仅正版登录" />
                             <local:MyComboBoxItem Content="仅离线登录" />
-                            <local:MyComboBoxItem Content="第三方登录:统一通行证" />
-                            <local:MyComboBoxItem Content="第三方登录:Authlib Injector 或 LittleSkin" />
                         </local:MyComboBox>
                         <TextBlock Name="LabServerNide" Grid.Row="1" VerticalAlignment="Center" HorizontalAlignment="Left" Text="服务器 ID(必填)" Margin="0,8,25,0" Visibility="Collapsed" />
                         <local:MyTextBox x:Name="TextServerNide" Grid.Row="1" Grid.ColumnSpan="2" ShowValidateResult="False" local:SettingService.Key="VersionServerNide" Grid.Column="1" Margin="0,8,0,0" Visibility="Collapsed" HintText="服主后台 → 集成说明 中提供的服务器 ID">
@@ -273,4 +271,4 @@
                 LogoScale="0.9" Logo="M73 584L920 584 608 896C579 925 579 972 608 1001 637 1030 683 1030 712 1001L1149 565C1164 550 1170 531 1170 511 1170 492 1164 472 1149 457L712 21C683-7 637-7 608 21 579 50 579 97 608 126L920 438 73 438C33 438 0 471 0 511 0 551 33 584 73 584Z" />
         </StackPanel>
     </local:MyScrollViewer>
-</local:MyPageRight>
+</local:MyPageRight>

+ 2 - 1
Plain Craft Launcher 2/Pages/PageInstance/PageInstanceSetup.xaml.vb

@@ -53,7 +53,8 @@ Public Class PageInstanceSetup
             UpdateJavaList()
 
             '服务器
-            ComboServerLogin.SelectedIndex = Settings.Get(Of Integer)("VersionServerLogin", Instance:=PageInstanceLeft.Instance)
+            Dim LoginMode = Settings.Get(Of Integer)("VersionServerLogin", Instance:=PageInstanceLeft.Instance)
+            ComboServerLogin.SelectedIndex = If(LoginMode >= 0 AndAlso LoginMode <= 2, LoginMode, 0)
             ComboServerLoginLast = ComboServerLogin.SelectedIndex
             UpdateServerLoginUI()
 

+ 1 - 101
Plain Craft Launcher 2/Pages/PageLaunch/MySkin.xaml.vb

@@ -207,107 +207,7 @@ Public Class MySkin
     End Property
     Private IsChanging As Boolean = False
     Public Sub BtnSkinCape_Click() Handles BtnSkinCape.Click
-        '检查条件,获取新披风
-        If IsChanging Then
-            Hint("正在更改披风中,请稍候!")
-            Return
-        End If
-        If McLoginMsLoader.State = LoadState.Failed Then
-            Hint("登录失败,无法更改披风!", HintType.Red)
-            Return
-        End If
-        If McLoginMsLoader.State <> LoadState.Finished Then Hint("正在获取披风列表,请稍候……")
-        IsChanging = True
-        '开始实际获取
-        RunInNewThread(
-        Sub()
-Retry:
-            Try
-                '获取登录信息
-                If McLoginMsLoader.State <> LoadState.Finished Then McLoginMsLoader.WaitForExit(PageLoginMsSkin.GetLoginData())
-                If McLoginMsLoader.State <> LoadState.Finished Then
-                    Hint("登录失败,无法更改披风!", HintType.Red)
-                    Return
-                End If
-                Dim AccessToken As String = McLoginMsLoader.Output.AccessToken
-                Dim Uuid As String = McLoginMsLoader.Output.Uuid
-                Dim SkinData As JObject = McLoginMsLoader.Output.ProfileJson.DeserializeJson()
-                '获取玩家的所有披风
-                Dim SelectedIndex As Integer? = Nothing
-                RunInUiWait(
-                Sub()
-                    Try
-                        Dim CapeNames As New Dictionary(Of String, String) From {
-                            {"Migrator", "迁移者披风"}, {"MapMaker", "Realms 地图制作者披风"}, {"Moderator", "Mojira 管理员披风"},
-                            {"Translator-Chinese", "Crowdin 中文翻译者披风"}, {"Translator", "Crowdin 翻译者披风"}, {"Cobalt", "Cobalt 披风"},
-                            {"Vanilla", "原版披风"}, {"Minecon2011", "Minecon 2011 参与者披风"}, {"Minecon2012", "Minecon 2012 参与者披风"},
-                            {"Minecon2013", "Minecon 2013 参与者披风"}, {"Minecon2015", "Minecon 2015 参与者披风"}, {"Minecon2016", "Minecon 2016 参与者披风"},
-                            {"Cherry Blossom", "樱花披风"}, {"15th Anniversary", "15 周年纪念披风"}, {"Purple Heart", "紫色心形披风"},
-                            {"Follower's", "追随者披风"}, {"MCC 15th Year", "MCC 15 周年披风"}, {"Minecraft Experience", "村民救援披风"},
-                            {"Mojang Office", "Mojang 办公室披风"}, {"Home", "家园披风"}, {"Menace", "入侵披风"}, {"Yearn", "渴望披风"},
-                            {"Common", "普通披风"}, {"Pan", "薄煎饼披风"}, {"Founder's", "创始人披风"}, {"Copper", "铜披风"}, {"Zombie Horse", "僵尸马披风"}, {"Builder", "建造者披风"}, {"Crafter", "工匠披风"},
-                            {"Aurora", "极光披风"}
-                        }
-                        Dim SelectionControl As New List(Of IMyRadio) From {
-                            New MyRadioBox With {.Text = "无披风", .Checked = Not SkinData("capes").Any(Function(c) c("state")?.ToString = "ACTIVE")}}
-                        For Each Cape In SkinData("capes")
-                            Dim CapeName As String = Cape("alias").ToString
-                            If CapeNames.ContainsKey(CapeName) Then CapeName = CapeNames(CapeName)
-                            SelectionControl.Add(New MyRadioBox With {.Text = CapeName, .Checked = Cape("state")?.ToString = "ACTIVE"})
-                        Next
-                        SelectedIndex = MyMsgBoxSelect(SelectionControl, "选择披风", "确定", "取消")
-                    Catch ex As Exception
-                        Logger.Error(ex, "获取玩家皮肤列表失败")
-                    End Try
-                End Sub)
-                If SelectedIndex Is Nothing Then Return
-                '发送请求
-                Dim Result As String = NetRequestByClientRetry("https://api.minecraftservices.com/minecraft/profile/capes/active",
-                    If(SelectedIndex = 0, HttpMethod.Delete, HttpMethod.Put),
-                    Content:=If(SelectedIndex = 0, "", New JObject(New JProperty("capeId", SkinData("capes")(SelectedIndex - 1)("id"))).ToString(0)),
-                    ContentType:="application/json",
-                    Headers:={{"Authorization", "Bearer " & AccessToken}})
-                If Result.Contains("""errorMessage""") Then
-                    Hint("更改披风失败:" & Result.DeserializeJson()("errorMessage").ToString, HintType.Red)
-                    Return
-                Else
-                    Hint("更改披风成功!", HintType.Green)
-                    '更新当前选择的披风
-                    For Each Cape In SkinData("capes")
-                        Cape("state") = "INACTIVE"
-                    Next
-                    If SelectedIndex > 0 Then SkinData("capes")(SelectedIndex - 1)("state") = "ACTIVE"
-                    McLoginMsLoader.Output.ProfileJson = SkinData.ToString()
-                    Settings.Set("CacheMsV2ProfileJson", McLoginMsLoader.Output.ProfileJson)
-                End If
-            Catch ex As Exception
-                If TypeOf ex Is HttpRequestCodeException Then
-                    Dim requestException As HttpRequestCodeException = CType(ex, HttpRequestCodeException)
-                    Select Case requestException.StatusCode
-                        Case HttpStatusCode.BadRequest
-                            Logger.Warn(ex, "更改披风时遭遇 400 错误")
-                            If requestException.Response?.Contains("""error""") Then
-                                Hint("更改披风失败:" & requestException.Response.DeserializeJson()("error").ToString, HintType.Red)
-                                Return
-                            ElseIf requestException.Response?.Contains("""errorMessage""") Then
-                                Hint("更改披风失败:" & requestException.Response.DeserializeJson()("errorMessage").ToString, HintType.Red)
-                                Return
-                            End If
-                        Case HttpStatusCode.Unauthorized
-                            Logger.Warn(ex, "更改披风时遭遇 401 错误")
-                            Hint("正在重新登录,将在登录后自动更改披风……")
-                            McLoginMsLoader.Start(PageLoginMsSkin.GetLoginData(), IsForceRestart:=True)
-                            GoTo Retry
-                    End Select
-                ElseIf ex.IsBadNetwork Then
-                    Hint("更改披风失败:连接 Mojang 服务器超时,请稍后再试,或使用 VPN 改善网络环境", HintType.Red)
-                Else
-                    Logger.Error(ex, "更改披风失败", LogBehavior.Toast)
-                End If
-            Finally
-                IsChanging = False
-            End Try
-        End Sub, "Cape Change")
+        Hint("此版本已移除在线披风管理。")
     End Sub
 
 End Class

+ 8 - 140
Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchLeft.xaml.vb

@@ -299,6 +299,8 @@ Public Class PageLaunchLeft
         Else
             LoginPageType = Settings.Get(Of Integer)("LoginPageType")
         End If
+        '旧版本的第三方登录设置在此版本中回到微软/离线选择。
+        If LoginPageType < 0 OrElse LoginPageType > 2 Then LoginPageType = 0
         Select Case LoginPageType
             Case 0 '正版或离线
 UnknownType:
@@ -337,29 +339,6 @@ UnknownType:
                 PanTypeOne.Visibility = Visibility.Visible
                 PathTypeOne.Data = (New GeometryConverter).ConvertFromString(Logo.IconButtonOffline)
                 LabTypeOne.Text = "离线登录"
-            Case 3 '统一通行证
-                If Settings.Get(Of String)("CacheNideAccess") = "" Then
-                    Type = PageType.Nide
-                Else
-                    Type = PageType.NideSkin
-                End If
-                Settings.Set("LoginType", McLoginType.Nide)
-                PanType.Visibility = Visibility.Collapsed
-                PanTypeOne.Visibility = Visibility.Visible
-                PathTypeOne.Data = (New GeometryConverter).ConvertFromString(Logo.IconButtonCard)
-                LabTypeOne.Text = "统一通行证登录"
-            Case 4 'Authlib-Injector
-                If Settings.Get(Of String)("CacheAuthAccess") = "" Then
-                    Type = PageType.Auth
-                Else
-                    Type = PageType.AuthSkin
-                End If
-                Settings.Set("LoginType", McLoginType.Auth)
-                PanType.Visibility = Visibility.Collapsed
-                PanTypeOne.Visibility = Visibility.Visible
-                PathTypeOne.Data = (New GeometryConverter).ConvertFromString(Logo.IconButtonCard)
-                LabTypeOne.Text = If(McInstanceSelected Is Nothing, Settings.Get(Of String)("CacheAuthServerName"), Settings.Get(Of String)("VersionServerAuthName", Instance:=McInstanceSelected))
-                If LabTypeOne.Text = "" Then LabTypeOne.Text = "第三方登录"
             Case Else
                 Logger.Error($"未知的登录页面:{LoginPageType}", LogBehavior.Toast)
                 GoTo UnknownType
@@ -385,45 +364,8 @@ UnknownType:
         Return (Settings.Get(Of String)("CacheMsV2Name"), Settings.Get(Of String)("CacheMsV2Uuid"))
     End Function
     Private Shared Sub SkinMsLoad(Data As LoaderTask(Of (String, String), String))
-        '清空已有皮肤
-        '如果在输入时清空皮肤,若输入内容一样则不会执行 Load 方法,导致皮肤不被加载
-        RunInUi(Sub() If FrmLoginMsSkin IsNot Nothing AndAlso FrmLoginMsSkin.Skin IsNot Nothing Then FrmLoginMsSkin.Skin.Clear())
-        '获取 Url
-        Dim UserName As String = Data.Input.Item1
-        Dim Uuid As String = Data.Input.Item2
-        If UserName = "" Then
-            Data.Output = PathImage & "Skins/" & McSkinSex(McLoginLegacyUuid(UserName)) & ".png"
-            Logger.Info("获取微软正版皮肤失败,ID 为空")
-            GoTo Finish
-        End If
-        Try
-            Dim Result As String = McSkinGetAddress(Uuid, "Ms")
-            If Data.IsCanceled Then Throw New OperationCanceledException("当前任务已取消:" & UserName)
-            Result = McSkinDownload(Result)
-            If Data.IsCanceled Then Throw New OperationCanceledException("当前任务已取消:" & UserName)
-            Data.Output = Result
-        Catch ex As Exception
-            If ex.IsCanceled Then
-                Data.Output = ""
-                Return
-            ElseIf ex.GetDisplay(False).Contains("(429)") Then
-                Data.Output = PathImage & "Skins/" & McSkinSex(McLoginLegacyUuid(UserName)) & ".png"
-                Logger.Error($"获取正版皮肤失败({UserName}):获取皮肤太过频繁,请 5 分钟后再试!", LogBehavior.Toast)
-            ElseIf ex.GetDisplay(False).Contains("未设置自定义皮肤") Then
-                Data.Output = PathImage & "Skins/" & McSkinSex(McLoginLegacyUuid(UserName)) & ".png"
-                Logger.Info("用户未设置自定义皮肤,跳过皮肤加载")
-            Else
-                Data.Output = PathImage & "Skins/" & McSkinSex(McLoginLegacyUuid(UserName)) & ".png"
-                Logger.Error(ex, $"获取微软正版皮肤失败({UserName})", LogBehavior.Toast)
-            End If
-        End Try
-Finish:
-        '刷新显示
-        If FrmLoginMsSkin IsNot Nothing Then
-            RunInUi(AddressOf FrmLoginMsSkin.Skin.Load)
-        ElseIf Not Data.IsCanceled Then '如果已经中断,Input 也被清空,就不会再次刷新
-            Data.Input = Nothing '清空输入,因为皮肤实际上没有被渲染,如果不清空切换到页面的 Start 会由于输入相同而不渲染
-        End If
+        Data.Output = PathImage & "Skins/" & McSkinSex(If(Data.Input.Item2, "")) & ".png"
+        RunInUi(Sub() If FrmLoginMsSkin IsNot Nothing Then FrmLoginMsSkin.Skin.Load())
     End Sub
 
     '离线皮肤
@@ -507,45 +449,8 @@ UseDefault:
         Return (Settings.Get(Of String)("CacheNideName"), Settings.Get(Of String)("CacheNideUuid"))
     End Function
     Private Shared Sub SkinNideLoad(Data As LoaderTask(Of (String, String), String))
-        '清空已有皮肤
-        '如果在输入时清空皮肤,若输入内容一样则不会执行 Load 方法,导致皮肤不被加载
-        RunInUi(Sub() If FrmLoginNideSkin IsNot Nothing AndAlso FrmLoginNideSkin.Skin IsNot Nothing Then FrmLoginNideSkin.Skin.Clear())
-        '获取 Url
-        Dim UserName As String = Data.Input.Item1
-        Dim Uuid As String = Data.Input.Item2
-        If UserName = "" Then
-            Data.Output = PathImage & "Skins/" & McSkinSex(McLoginLegacyUuid(UserName)) & ".png"
-            Logger.Info("获取统一通行证皮肤失败,ID 为空")
-            GoTo Finish
-        End If
-        Try
-            Dim Result As String = McSkinGetAddress(Uuid, "Nide")
-            If Data.IsCanceled Then Throw New OperationCanceledException("当前任务已取消:" & UserName)
-            Result = McSkinDownload(Result)
-            If Data.IsCanceled Then Throw New OperationCanceledException("当前任务已取消:" & UserName)
-            Data.Output = Result
-        Catch ex As Exception
-            If ex.IsCanceled Then
-                Data.Output = ""
-                Return
-            ElseIf ex.GetDisplay(False).Contains("(429)") Then
-                Data.Output = PathImage & "Skins/Steve.png"
-                Logger.Error($"获取统一通行证皮肤失败({UserName}):获取皮肤太过频繁,请 5 分钟后再试!", LogBehavior.Toast)
-            ElseIf ex.GetDisplay(False).Contains("未设置自定义皮肤") Then
-                Data.Output = PathImage & "Skins/Steve.png"
-                Logger.Info("用户未设置自定义皮肤,跳过皮肤加载")
-            Else
-                Data.Output = PathImage & "Skins/Steve.png"
-                Logger.Error(ex, $"获取统一通行证皮肤失败({UserName})", LogBehavior.Toast)
-            End If
-        End Try
-Finish:
-        '刷新显示
-        If FrmLoginNideSkin IsNot Nothing Then
-            RunInUi(AddressOf FrmLoginNideSkin.Skin.Load)
-        ElseIf Not Data.IsCanceled Then '如果已经中断,Input 也被清空,就不会再次刷新
-            Data.Input = Nothing '清空输入,因为皮肤实际上没有被渲染,如果不清空切换到页面的 Start 会由于输入相同而不渲染
-        End If
+        Data.Output = PathImage & "Skins/" & McSkinSex(If(Data.Input.Item2, "")) & ".png"
+        RunInUi(Sub() If FrmLoginNideSkin IsNot Nothing Then FrmLoginNideSkin.Skin.Load())
     End Sub
 
     'Authlib-Injector 皮肤
@@ -555,45 +460,8 @@ Finish:
         Return (Settings.Get(Of String)("CacheAuthName"), Settings.Get(Of String)("CacheAuthUuid"))
     End Function
     Private Shared Sub SkinAuthLoad(Data As LoaderTask(Of (String, String), String))
-        '清空已有皮肤
-        '如果在输入时清空皮肤,若输入内容一样则不会执行 Load 方法,导致皮肤不被加载
-        RunInUi(Sub() If FrmLoginAuthSkin IsNot Nothing AndAlso FrmLoginAuthSkin.Skin IsNot Nothing Then FrmLoginAuthSkin.Skin.Clear())
-        '获取 Url
-        Dim UserName As String = Data.Input.Item1
-        Dim UUID As String = Data.Input.Item2
-        If UserName = "" Then
-            Data.Output = PathImage & "Skins/Steve.png"
-            Logger.Info("获取 Authlib-Injector 皮肤失败,ID 为空")
-            GoTo Finish
-        End If
-        Try
-            Dim Result As String = McSkinGetAddress(UUID, "Auth")
-            If Data.IsCanceled Then Throw New OperationCanceledException("当前任务已取消:" & UserName)
-            Result = McSkinDownload(Result)
-            If Data.IsCanceled Then Throw New OperationCanceledException("当前任务已取消:" & UserName)
-            Data.Output = Result
-        Catch ex As Exception
-            If ex.IsCanceled Then
-                Data.Output = ""
-                Return
-            ElseIf ex.GetDisplay(False).Contains("(429)") Then
-                Data.Output = PathImage & "Skins/Steve.png"
-                Logger.Error($"获取 Authlib-Injector 皮肤失败({UserName}):获取皮肤太过频繁,请 5 分钟后再试!", LogBehavior.Toast)
-            ElseIf ex.GetDisplay(False).Contains("未设置自定义皮肤") Then
-                Data.Output = PathImage & "Skins/Steve.png"
-                Logger.Info("用户未设置自定义皮肤,跳过皮肤加载")
-            Else
-                Data.Output = PathImage & "Skins/Steve.png"
-                Logger.Error(ex, $"获取 Authlib-Injector 皮肤失败({UserName})", LogBehavior.Toast)
-            End If
-        End Try
-Finish:
-        '刷新显示
-        If FrmLoginAuthSkin IsNot Nothing Then
-            RunInUi(AddressOf FrmLoginAuthSkin.Skin.Load)
-        ElseIf Not Data.IsCanceled Then '如果已经中断,Input 也被清空,就不会再次刷新
-            Data.Input = Nothing '清空输入,因为皮肤实际上没有被渲染,如果不清空切换到页面的 Start 会由于输入相同而不渲染
-        End If
+        Data.Output = PathImage & "Skins/" & McSkinSex(If(Data.Input.Item2, "")) & ".png"
+        RunInUi(Sub() If FrmLoginAuthSkin IsNot Nothing Then FrmLoginAuthSkin.Skin.Load())
     End Sub
 
     '全部皮肤加载器

+ 11 - 0
Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml

@@ -11,6 +11,17 @@
                           Text="快照版 PCL 包含尚未正式发布的测试功能,仅限赞助者自己使用。&#xa;请不要随意发给其他人,或是用来制作整合包哦!" />
             <StackPanel x:Name="PanCustom">
             </StackPanel>
+            <Border x:Name="CommunismWelcome" Margin="0,0,0,15" Padding="26,25" CornerRadius="10" Background="#F2FFFAF0" BorderBrush="#60C19547" BorderThickness="1" Visibility="Collapsed">
+                <StackPanel>
+                    <TextBlock Text="★  MINECRAFT / JAVA EDITION" FontSize="11" FontWeight="SemiBold" Foreground="#FFA97B2E" Margin="0,0,0,14" />
+                    <TextBlock Text="万众一心,自由启程。" FontSize="28" FontWeight="SemiBold" Foreground="#FF981422" TextWrapping="Wrap" Margin="0,0,0,10" />
+                    <TextBlock Text="选择你的世界。每一种色彩,都向所有人开放。" FontSize="13" Foreground="#FF80695D" TextWrapping="Wrap" Margin="0,0,0,25" />
+                    <WrapPanel>
+                        <local:MyButton x:Name="BtnRedDownload" Text="下载 Minecraft" Height="35" MinWidth="140" Padding="14,0" ColorType="Highlight" Margin="0,0,14,0" />
+                        <local:MyButton x:Name="BtnRedThemes" Text="选择主题" Height="35" MinWidth="120" Padding="14,0" />
+                    </WrapPanel>
+                </StackPanel>
+            </Border>
             <local:MyCard Margin="0,0,0,15" Title="启动日志" x:Name="PanLog">
                 <local:MyTextBox x:Name="LabLog" Margin="15,34,15,13" Padding="0,4" IsReadOnly="True" IsUndoEnabled="False"
                          AcceptsReturn="True" TextWrapping="Wrap" BorderThickness="0" HasBackground="False"

+ 13 - 170
Plain Craft Launcher 2/Pages/PageLaunch/PageLaunchRight.xaml.vb

@@ -5,6 +5,7 @@ Public Class PageLaunchRight
 
     Private Sub Init() Handles Me.Loaded
         PanBack.ScrollToHome()
+        CommunismWelcome.Visibility = If(ThemeNow = DefaultTheme, Visibility.Visible, Visibility.Collapsed)
         PanLog.Visibility = ModeDebug.ToVisibility
         '快照版提示
         PanHint.Visibility =
@@ -12,6 +13,13 @@ Public Class PageLaunchRight
              CurrentRank < DonationRank.Rank23 AndAlso Not Settings.Get(Of Boolean)("HintSnapshot")).ToVisibility
     End Sub
 
+    Private Sub BtnRedDownload_Click(sender As Object, e As EventArgs) Handles BtnRedDownload.Click
+        FrmMain.PageChange(FormMain.PageType.Download)
+    End Sub
+    Private Sub BtnRedThemes_Click(sender As Object, e As EventArgs) Handles BtnRedThemes.Click
+        FrmMain.PageChange(FormMain.PageType.Setup, FormMain.PageSubType.SetupUI)
+    End Sub
+
 #Region "主页"
 
     ''' <summary>
@@ -30,180 +38,16 @@ Public Class PageLaunchRight
         End Sub)
     End Sub
     Private Sub RefreshReal()
-        Dim Content As String = Nothing, Url As String = Nothing
-        Select Case Settings.Get(Of Integer)("UiCustomType")
-            Case 1
-                '加载本地文件
-                Logger.Info("主页自定义数据来源:本地文件")
-                Content = FileUtils.TryReadAsString(Paths.Base & "PCL\Custom.xaml")
-            Case 2
-                '联网下载
-                Url = Settings.Get(Of String)("UiCustomNet")
-            Case 3
-                '预设
-                Select Case Settings.Get(Of Integer)("UiCustomPreset")
-                    Case 0
-                        Logger.Info("主页预设:你知道吗")
-                        Content = "
-                            <local:MyCard Title=""你知道吗?"" Margin=""0,0,0,15"">
-                                <TextBlock Margin=""25,38,23,15"" FontSize=""13.5"" IsHitTestVisible=""False"" Text=""{hint}"" TextWrapping=""Wrap"" Foreground=""{DynamicResource ColorBrush1}"" />
-                                <local:MyIconButton Height=""22"" Width=""22"" Margin=""9"" VerticalAlignment=""Top"" HorizontalAlignment=""Right"" 
-                                    EventType=""刷新主页"" EventData=""/""
-                                    Logo=""M512.0 838.3c-80.2 0-153.4-29.3-210.2-77.4l75.5-75.5c11.5-11.5 25.8-22.0 25.8-37.0a27.2 27.2 0 0 0-27.1-27.1H104.0c-27.1 0-27.1 23.9-27.1 27.1v271.9a27.1 27.1 0 0 0 27.1 27.1c15.0 0 27.8-16.6 42.5-31.2l77.9-77.9c76.6 67.7 177.1 108.9 287.4 108.9 221.7 0 404.5-166.0 431.2-380.6h-109.8c-25.9 154.2-159.7 271.9-321.3 271.9zM919.9 76.6c-15.0 0-27.8 16.6-42.5 31.3L799.5 185.8c-76.5-67.7-177.1-108.9-287.4-108.9-221.8 0-404.5 166.1-431.3 380.6H190.6c25.9-154.2 159.7-271.9 321.4-271.9 80.2 0 153.4 29.3 210.1 77.4l-75.5 75.5c-11.6 11.5-25.8 22.0-25.8 37.1a27.2 27.2 0 0 0 27.1 27.1h271.9c27.1 0 27.1-23.9 27.1-27.1V103.8a27.1 27.1 0 0 0-27.1-27.1z"" />
-                            </local:MyCard>"
-                    Case 1
-                        Logger.Info("主页预设:回声洞")
-                        Content = "
-                            <local:MyCard Title=""回声洞"" Margin=""0,0,0,15"">
-                                <TextBlock Margin=""25,38,23,15"" FontSize=""13.5"" IsHitTestVisible=""False"" Text=""{cave}"" TextWrapping=""Wrap"" Foreground=""{DynamicResource ColorBrush1}"" />
-                                <local:MyIconButton Height=""22"" Width=""22"" Margin=""9"" VerticalAlignment=""Top"" HorizontalAlignment=""Right"" 
-                                    EventType=""刷新主页"" EventData=""/""
-                                    Logo=""M512.0 838.3c-80.2 0-153.4-29.3-210.2-77.4l75.5-75.5c11.5-11.5 25.8-22.0 25.8-37.0a27.2 27.2 0 0 0-27.1-27.1H104.0c-27.1 0-27.1 23.9-27.1 27.1v271.9a27.1 27.1 0 0 0 27.1 27.1c15.0 0 27.8-16.6 42.5-31.2l77.9-77.9c76.6 67.7 177.1 108.9 287.4 108.9 221.7 0 404.5-166.0 431.2-380.6h-109.8c-25.9 154.2-159.7 271.9-321.3 271.9zM919.9 76.6c-15.0 0-27.8 16.6-42.5 31.3L799.5 185.8c-76.5-67.7-177.1-108.9-287.4-108.9-221.8 0-404.5 166.1-431.3 380.6H190.6c25.9-154.2 159.7-271.9 321.4-271.9 80.2 0 153.4 29.3 210.1 77.4l-75.5 75.5c-11.6 11.5-25.8 22.0-25.8 37.1a27.2 27.2 0 0 0 27.1 27.1h271.9c27.1 0 27.1-23.9 27.1-27.1V103.8a27.1 27.1 0 0 0-27.1-27.1z"" />
-                            </local:MyCard>"
-                    Case 2
-                        Logger.Info("主页预设:Minecraft 新闻")
-                        Url = "https://mcnews.meloong.com"
-                    Case 3
-                        Logger.Info("主页预设:简单主页")
-                        Url = "https://pclhomeplazaoss.lingyunawa.top:26994/d/Homepages/MFn233/Custom.xaml"
-                    Case 4
-                        Logger.Info("主页预设:每日整合包推荐")
-                        Url = "https://pclsub.sodamc.com/"
-                    Case 5
-                        Logger.Info("主页预设:Minecraft 皮肤推荐")
-                        Url = "https://forgepixel.com/pcl_sub_file"
-                    Case 6
-                        Logger.Info("主页预设:OpenBMCLAPI 仪表盘 Lite")
-                        Url = "https://pcl-bmcl.milu.ink/"
-                    Case 7
-                        Logger.Info("主页预设:主页市场")
-                        Url = "https://pclhomeplazaoss.lingyunawa.top:26994/p/Homepages/Homepage.Market/Custom.xaml"
-                    Case 8
-                        Logger.Info("主页预设:更新日志")
-                        Url = "https://pclhomeplazaoss.lingyunawa.top:26994/d/Homepages/Joker2184/UpdateHomepage.xaml"
-                    Case 9
-                        Logger.Info("主页预设:PCL 新功能说明书")
-                        Url = "https://raw.gitcode.com/WForst-Breeze/WhatsNewPCL/raw/main/Custom.xaml"
-                    Case 10
-                        Logger.Info("主页预设:OpenMCIM Dashboard")
-                        Url = "https://files.mcimirror.top/PCL"
-                    Case 11
-                        Logger.Info("主页预设:杂志主页")
-                        Url = "https://gh-proxy.com/https://github.com/Neclyon/Magazine-Homepage-PCL/raw/main/output/Custom.xaml"
-                    Case 12
-                        Logger.Info("主页预设:PCL GitHub 仪表盘")
-                        Url = "https://ddf.pcl-community.top/Custom.xaml"
-                    Case 13
-                        Logger.Info("主页预设:PCL 更新摘要")
-                        Url = "https://raw.gitcode.com/ENC_Euphony/PCL-AI-Summary-HomePage/raw/master/Custom.xaml"
-                    Case 14
-                        Logger.Info("主页预设:今日新闻热点")
-                        Url = "https://pcl.wyc-w.top/index.xaml"
-                    Case 15
-                        Logger.Info("主页预设:Minecraft 芝士站")
-                        Url = "https://mkss-services.awaeric.xyz"
-                    Case 16
-                        Logger.Info("主页预设:整合包推荐引擎")
-                        Url = "https://qawsedrftgyhujiko.fun/pcl2/Custom.xaml"
-                    Case 17
-                        Logger.Info("主页预设:Bangumi 番剧主页")
-                        Url = "https://bangumi.p.kaphia.top"
-                    Case 18
-                        Logger.Info("主页预设:Bilibili 热门")
-                        Url = "https://bilibili.p.kaphia.top"
-                    Case 19
-                        Logger.Info("主页预设:Music 云音乐热门")
-                        Url = "https://cloudmusic.p.kaphia.top"
-                    Case 20
-                        Logger.Info("主页预设:模组推荐")
-                        Url = "https://g-fish.dpdns.org/download/ModPage.xaml"
-                    Case 21
-                        Logger.Info("主页预设:每日游戏推荐")
-                        Url = "https://pcl.xn--9iq209asnop94b.top/download/GamePage.xaml"
-                    Case 22
-                        Logger.Info("主页预设:Wallpaper 每日壁纸")
-                        Url = "https://wallpaper.p.kaphia.top"
-                    Case 23
-                        Logger.Info("主页预设:Weather 天气预报")
-                        Url = "https://weather-p.kaphia.top/{variable:PCLDailyWeatherPage:choose}.xaml"
-                    Case 24
-                        Logger.Info("主页预设:新一代狗屁王大杂烩主页")
-                        Url = "https://end.g-fish.dpdns.org/Custom.xaml"
-                End Select
-        End Select
-        '联网下载
-        If Not String.IsNullOrWhiteSpace(Url) Then
-            '替换自定义变量与设置
-            Url = ArgumentReplace(Url, AddressOf WebUtility.HtmlEncode)
-            If Url = Settings.Get(Of String)("CacheSavedPageUrl") AndAlso FileUtils.Exists(PathTemp & "Cache\Custom.xaml") Then
-                '缓存可用
-                Logger.Info("主页自定义数据来源:联网缓存文件")
-                Content = FileUtils.TryReadAsString(PathTemp & "Cache\Custom.xaml")
-                '后台更新缓存
-                OnlineLoader.Start((Url, False))
-            Else
-                '缓存不可用
-                Logger.Info("主页自定义数据来源:联网全新下载")
-                Hint("正在加载主页……")
-                RunInUiWait(Sub() LoadContent(Nothing)) '在加载结束前清空页面
-                Settings.Set("CacheSavedPageVersion", "")
-                OnlineLoader.Start((Url, True)) '下载完成后将会再次触发更新
-                Return
-            End If
+        Dim Content As String = Nothing
+        If Settings.Get(Of Integer)("UiCustomType") = 1 Then
+            Content = FileUtils.TryReadAsString(Paths.Base & "PCL\Custom.xaml")
+        ElseIf Settings.Get(Of Integer)("UiCustomType") = 3 Then
+            Content = "<local:MyCard Title=""你知道吗?"" Margin=""0,0,0,15""><TextBlock Margin=""25,38,23,15"" Text=""{hint}"" TextWrapping=""Wrap"" /></local:MyCard>"
         End If
-        '同步到 UI
         RunInUi(Sub() LoadContent(Content))
     End Sub
     Private RefreshLock As New Object
 
-    '联网获取主页文件
-    Private OnlineLoader As New LoaderTask(Of (Address As String, ShouldRefresh As Boolean), Integer)("下载主页", AddressOf OnlineLoaderSub) With {.ReloadTimeout = 10 * 60 * 1000}
-    Private Sub OnlineLoaderSub(Task As LoaderTask(Of (Address As String, ShouldRefresh As Boolean), Integer))
-        Dim Address As String = Task.Input.Address '#3721 中连续触发两次导致内容变化
-        Dim ShouldRefresh As Boolean = Task.Input.ShouldRefresh
-        Try
-            '获取版本校验地址
-            Dim VersionAddress As String
-            If Address.Contains(".xaml") Then
-                VersionAddress = Address.Replace(".xaml", ".xaml.ini")
-            Else
-                VersionAddress = Address.BeforeFirst("?")
-                If Not VersionAddress.EndsWithF("/") Then VersionAddress += "/"
-                VersionAddress += "version"
-                If Address.Contains("?") Then VersionAddress += "?" & Address.AfterFirst("?")
-            End If
-            '校验版本
-            Dim Version As String = ""
-            Try
-                Version = NetRequestByClientRetry(VersionAddress)
-                If Version.Length > 1000 Then Throw New Exception($"获取的主页版本过长({Version.Length} 字符)")
-                Dim CurrentVersion As String = Settings.Get(Of String)("CacheSavedPageVersion")
-                If Version <> "" AndAlso CurrentVersion <> "" AndAlso Version = CurrentVersion Then
-                    Logger.Info($"当前缓存的主页已为最新,当前版本:{Version},检查源:{VersionAddress}")
-                    Return
-                End If
-                Logger.Info($"需要下载联网主页,当前版本:{Version},检查源:{VersionAddress}")
-            Catch exx As Exception
-                Logger.Warn(exx, $"联网获取主页版本失败")
-                Logger.Info($"无法检查联网主页版本,将直接下载,检查源:{VersionAddress}")
-            End Try
-            '实际下载
-            Dim FileContent As String = NetRequestByClientRetry(Address)
-            Logger.Info($"已联网下载主页,内容长度:{FileContent.Length},来源:{Address}")
-            Settings.Set("CacheSavedPageUrl", Address)
-            Settings.Set("CacheSavedPageVersion", Version)
-            FileUtils.Write(PathTemp & "Cache\Custom.xaml", FileContent)
-            '若内容变更则要求刷新
-            If LoadedContentHash <> FileContent.GetStableHashCode() AndAlso ShouldRefresh Then Refresh()
-        Catch ex As Exception
-            Logger.Error(ex, $"下载主页失败({Address})", If(ModeDebug, LogBehavior.Alert, LogBehavior.Toast))
-        End Try
-    End Sub
-
-    ''' <summary>
-    ''' 立即强制刷新主页。
-    ''' 必须在 UI 线程调用。
-    ''' </summary>
     Public Sub ForceRefresh() Implements IRefreshable.Refresh
         Logger.Info("要求强制刷新主页")
         ClearCache()
@@ -221,7 +65,6 @@ Public Class PageLaunchRight
     ''' </summary>
     Private Sub ClearCache()
         LoadedContentHash = Nothing
-        OnlineLoader.Input = ("", True)
         Settings.Set("CacheSavedPageUrl", "")
         Settings.Set("CacheSavedPageVersion", "")
         Logger.Info("已清空主页缓存")

+ 1 - 1
Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMsSkin.xaml

@@ -17,7 +17,7 @@
         <Border Grid.Row="3" HorizontalAlignment="Center" x:Name="PanButtons" Opacity="0"
                 CornerRadius="5" Background="{DynamicResource ColorBrush8}" Padding="10,3,9,3">
             <StackPanel Orientation="Horizontal">
-                <local:MyIconButton Height="24" Margin="0,0.5,9,0"  x:Name="BtnSkin"
+                <local:MyIconButton Height="24" Margin="0,0.5,9,0"  x:Name="BtnSkin" IsEnabled="False"
                                 ToolTip="皮肤与披风" ToolTipService.InitialShowDelay="50"
                                 LogoScale="1.08" Logo="M764.0003 0.076794a215.433442 215.433442 0 0 0-76.282279 13.950954l-9.06172 3.686123c-49.148314 20.734445-88.518161 30.53851-115.498538 30.53851-27.031573 0-66.478214-9.855261-116.676049-31.050471a217.583681 217.583681 0 0 0-72.186586-16.766743l-9.138515-0.307177-3.839712-0.051196-8.626553 0.307177-4.223683 0.281579A160.755943 160.755943 0 0 0 250.477214 45.385396l-219.32435 212.438467a102.392321 102.392321 0 0 0-11.263156 134.210734l106.360023 144.552359 3.378947 4.351674a102.469115 102.469115 0 0 0 112.810739 31.690423l5.657176-2.175837V883.210559a140.789441 140.789441 0 0 0 140.789441 140.789441h372.170487l5.657175-0.102392a140.789441 140.789441 0 0 0 135.132266-140.687049l-0.025599-318.6705 1.177512 0.716747a102.392321 102.392321 0 0 0 141.941355-44.233483l70.906682-144.39877a102.392321 102.392321 0 0 0-20.657651-118.647101L875.86391 45.385396A160.730345 160.730345 0 0 0 764.0003 0.076794z m-357.349199 111.454041C468.444867 137.615279 520.562558 150.644702 563.157763 150.644702c42.492813 0 94.584906-12.978227 156.199485-38.985876A112.580356 112.580356 0 0 1 764.0003 102.469115c15.154063 0 29.693773 5.887558 40.598555 16.433967L1023.923206 331.495138l-70.906682 144.39877-93.023423-65.684674a38.39712 38.39712 0 0 0-60.53946 31.383247V883.210559a38.39712 38.39712 0 0 1-38.39712 38.39712H388.886034a38.39712 38.39712 0 0 1-38.397121-38.39712V445.176212a38.39712 38.39712 0 0 0-61.691373-30.487314l-80.070795 61.20501L102.392321 331.341549l219.32435-212.438467a58.363623 58.363623 0 0 1 35.581332-16.229183L362.110442 102.469115l6.322726 0.179186a115.191361 115.191361 0 0 1 38.217933 8.882534z">
                     <local:MyIconButton.ContextMenu>

+ 2 - 57
Plain Craft Launcher 2/Pages/PageLaunch/PageLoginMsSkin.xaml.vb

@@ -83,63 +83,8 @@ Public Class PageLoginMsSkin
         RunInNewThread(Sub() EditSkin(SkinInfo), "Ms Skin Upload")
     End Sub
     Private Sub EditSkin(SkinInfo As McSkinInfo)
-Retry:
-        Try
-            Do While McLoginMsLoader.State = LoadState.Loading '等待登录结束
-                Thread.Sleep(10)
-            Loop
-            If McLoginMsLoader.State = LoadState.Failed Then Throw New Exception("登录失败", McLoginMsLoader.Error)
-            Dim AccessToken As String = Settings.Get(Of String)("CacheMsV2Access")
-            Dim Uuid As String = Settings.Get(Of String)("CacheMsV2Uuid")
-            Dim Result As String = NetRequestByClientRetry("https://api.minecraftservices.com/minecraft/profile/skins", HttpMethod.Post,
-                Content:=New Net.Http.MultipartFormDataContent From {
-                    {New Net.Http.StringContent(If(SkinInfo.IsSlim, "slim", "classic")), "variant"},
-                    {New Net.Http.ByteArrayContent(FileUtils.ReadAsBytes(SkinInfo.LocalFile)), "file", PathUtils.GetLastPart(SkinInfo.LocalFile)}
-                },
-                Headers:={{"Authorization", "Bearer " & AccessToken}, {"Accept", "*/*"}, {"User-Agent", "MojangSharp/0.1"}})
-            If Result.Contains("request requires user authentication") Then
-                Hint("正在重新登录,将在登录后自动更改皮肤……")
-                McLoginMsLoader.Start(GetLoginData(), IsForceRestart:=True)
-                GoTo Retry
-            End If
-            '获取新皮肤地址
-            Logger.Info($"皮肤修改返回值:{vbCrLf}{Result}")
-            Dim ResultJson As JObject = Result.DeserializeJson()
-            If ResultJson.ContainsKey("errorMessage") Then Throw New Exception(ResultJson("errorMessage").ToString) '#5309
-            For Each Skin As JObject In ResultJson("skins")
-                If Skin("state").ToString = "ACTIVE" Then
-                    MySkin.ReloadCache(Skin("url"))
-                    Return
-                End If
-            Next
-            Throw New Exception("未知错误(" & Result & ")")
-        Catch ex As Exception
-            If TypeOf ex Is HttpRequestCodeException Then
-                Dim requestException As HttpRequestCodeException = CType(ex, HttpRequestCodeException)
-                Select Case requestException.StatusCode
-                    Case HttpStatusCode.BadRequest
-                        Logger.Warn(ex, "更改皮肤时遭遇 400 错误")
-                        If requestException.Response?.Contains("""error""") Then
-                            Hint("更改皮肤失败:" & requestException.Response.DeserializeJson()("error").ToString, HintType.Red)
-                            Return
-                        ElseIf requestException.Response?.Contains("""errorMessage""") Then
-                            Hint("更改皮肤失败:" & requestException.Response.DeserializeJson()("errorMessage").ToString, HintType.Red)
-                            Return
-                        End If
-                    Case HttpStatusCode.Unauthorized
-                        Logger.Warn(ex, "更改皮肤时遭遇 401 错误")
-                        Hint("正在重新登录,将在登录后自动更改皮肤……")
-                        McLoginMsLoader.Start(GetLoginData(), IsForceRestart:=True)
-                        GoTo Retry
-                End Select
-            ElseIf ex.IsBadNetwork Then
-                Hint("更改皮肤失败:连接 Mojang 服务器超时,请稍后再试,或使用 VPN 改善网络环境", HintType.Red)
-            Else
-                Logger.Error(ex, "更改皮肤失败", LogBehavior.Toast)
-            End If
-        Finally
-            IsChanging = False
-        End Try
+        IsChanging = False
+        Hint("此版本已移除在线皮肤上传。")
     End Sub
 
     '保存皮肤

+ 2 - 206
Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml

@@ -1,207 +1,3 @@
-<local:MyPageRight
-             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
-             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-             xmlns:local="clr-namespace:PCL" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
-             mc:Ignorable="d" x:Class="PageLinkMain"
-             d:DesignWidth="800" d:DesignHeight="800">
-    <Grid>
-        <Grid x:Name="PanLoad">
-            <local:MyCard HorizontalAlignment="Center" VerticalAlignment="Center" x:Name="CardLoad" MinWidth="300">
-                <Grid>
-                    <Grid.ColumnDefinitions>
-                        <ColumnDefinition Width="25" />
-                        <ColumnDefinition Width="Auto" />
-                        <ColumnDefinition Width="20" />
-                        <ColumnDefinition Width="Auto" />
-                        <ColumnDefinition Width="1*" />
-                        <ColumnDefinition Width="Auto" />
-                        <ColumnDefinition Width="20" />
-                    </Grid.ColumnDefinitions>
-                    <Grid.RowDefinitions>
-                        <RowDefinition Height="20" />
-                        <RowDefinition Height="Auto" />
-                        <RowDefinition Height="Auto" />
-                        <RowDefinition Height="1*" />
-                        <RowDefinition Height="20" />
-                    </Grid.RowDefinitions>
-                    <local:MyLoading Grid.Column="1" Grid.Row="1" x:Name="Load" Grid.RowSpan="3" Margin="0,-4,0,-29" HorizontalAlignment="Center"
-                                     Text="" TextError="" TextErrorInherit="False" AutoRun="False" IsHitTestVisible="False" RenderTransformOrigin="0.5,0.5">
-                        <local:MyLoading.RenderTransform>
-                            <ScaleTransform ScaleX="0.9" ScaleY="0.9" />
-                        </local:MyLoading.RenderTransform>
-                    </local:MyLoading>
-                    <TextBlock x:Name="LabLoadTitle" Grid.Column="3" Grid.Row="1" Grid.ColumnSpan="2" VerticalAlignment="Center" Text="正在创建联机房间" FontSize="19"
-                               Margin="0,0,25,1" Foreground="{Binding Foreground, ElementName=Load}" IsHitTestVisible="False" />
-                    <TextBlock x:Name="LabLoadDesc" Grid.Column="3" Grid.ColumnSpan="3" Grid.Row="2" VerticalAlignment="Center" Margin="0,5,10,-5" Text="下载联机模块中……" FontSize="14" 
-                               LineHeight="19" MaxWidth="450" MaxHeight="220" TextWrapping="Wrap" Foreground="{StaticResource ColorBrushGray3}" IsHitTestVisible="False" />
-                    <local:MyIconButton Grid.Column="5" Grid.Row="1" x:Name="BtnLoadCancel" Height="25" Width="25" 
-                                        VerticalAlignment="Top" Margin="0,-13,-13,0"
-                                        Theme="Black" Opacity="0.3"
-                                        ToolTip="取消" ToolTipService.InitialShowDelay="100"
-                                        LogoScale="0.72" Logo="F1 M2,0 L0,2 8,10 0,18 2,20 10,12 18,20 20,18 12,10 20,2 18,0 10,8 2,0Z" />
-                    <Grid Height="12" VerticalAlignment="Bottom" Grid.ColumnSpan="100" Grid.RowSpan="100" Margin="0,0,0,-0.6">
-                        <Grid.ColumnDefinitions>
-                            <ColumnDefinition Width="0*" x:Name="ColumnProgressA" />
-                            <ColumnDefinition Width="1*" x:Name="ColumnProgressB" />
-                        </Grid.ColumnDefinitions>
-                        <Grid.Clip>
-                            <RectangleGeometry RadiusX="6" RadiusY="6" x:Name="RectProgressClip" />
-                        </Grid.Clip>
-                        <Rectangle Fill="{Binding Foreground, ElementName=Load}" VerticalAlignment="Bottom" Height="2" />
-                    </Grid>
-                </Grid>
-            </local:MyCard>
-        </Grid>
-        <Grid x:Name="PanContent">
-            <Grid x:Name="PanSelect">
-                <Grid.ColumnDefinitions>
-                    <ColumnDefinition Width="1*" />
-                    <ColumnDefinition Width="Auto" />
-                    <ColumnDefinition Width="1*" />
-                </Grid.ColumnDefinitions>
-                <Grid.RowDefinitions>
-                    <RowDefinition Height="1*" />
-                    <RowDefinition Height="Auto" />
-                    <RowDefinition Height="35" />
-                    <RowDefinition Height="Auto" />
-                    <RowDefinition Height="10" />
-                    <RowDefinition Height="Auto" />
-                    <RowDefinition Height="1*" />
-                </Grid.RowDefinitions>
-                <local:MyCard x:Name="PanSelectCreate" Grid.Column="1" Grid.Row="1" CornerRadius="13" VerticalAlignment="Center">
-                    <Grid IsHitTestVisible="False">
-                        <Grid.ColumnDefinitions>
-                            <ColumnDefinition Width="25" />
-                            <ColumnDefinition Width="Auto" />
-                            <ColumnDefinition Width="13" />
-                            <ColumnDefinition Width="1*" />
-                            <ColumnDefinition Width="Auto" />
-                        </Grid.ColumnDefinitions>
-                        <Grid.RowDefinitions>
-                            <RowDefinition Height="22" />
-                            <RowDefinition Height="Auto" />
-                            <RowDefinition Height="Auto" />
-                            <RowDefinition Height="12" />
-                        </Grid.RowDefinitions>
-                        <Path Grid.Column="1" Grid.Row="1" Height="26" Width="26" Margin="0,-1,0,0"
-                            Stretch="Uniform" Fill="{DynamicResource ColorBrush3}"
-                            Data="M384 128v192h256a64 64 0 0 1 64 64v256h192V128H384zM320 320V128a64 64 0 0 1 64-64h512a64 64 0 0 1 64 64v512a64 64 0 0 1-64 64h-192v192a64 64 0 0 1-64 64H128a64 64 0 0 1-64-64V384a64 64 0 0 1 64-64h192z m320 384H384a64 64 0 0 1-64-64V384H128v512h512v-192z m0-64V384H384v256h256z" />
-                        <TextBlock Grid.Column="3" Grid.ColumnSpan="3" Grid.Row="1"
-                            Text="创建房间" FontSize="20" Foreground="{DynamicResource ColorBrush3}" />
-                        <TextBlock Grid.Column="3" Grid.ColumnSpan="3" Grid.Row="2" Margin="1,10,30,10" 
-                            Text="邀请朋友加入你的单人游戏。"
-                            FontSize="15" LineHeight="19" MaxWidth="400" TextWrapping="Wrap" Foreground="{StaticResource ColorBrushGray3}" />
-                    </Grid>
-                </local:MyCard>
-                <local:MyCard x:Name="PanSelectJoin" Grid.Column="1" Grid.Row="3" CornerRadius="13" VerticalAlignment="Center">
-                    <Grid IsHitTestVisible="False">
-                        <Grid.ColumnDefinitions>
-                            <ColumnDefinition Width="25" />
-                            <ColumnDefinition Width="Auto" />
-                            <ColumnDefinition Width="13" />
-                            <ColumnDefinition Width="1*" />
-                            <ColumnDefinition Width="Auto" />
-                        </Grid.ColumnDefinitions>
-                        <Grid.RowDefinitions>
-                            <RowDefinition Height="22" />
-                            <RowDefinition Height="Auto" />
-                            <RowDefinition Height="Auto" />
-                            <RowDefinition Height="12" />
-                        </Grid.RowDefinitions>
-                        <Path Grid.Column="1" Grid.Row="1" Height="26" Width="26" Margin="0,-1,0,0"
-                            Stretch="Uniform" Fill="{DynamicResource ColorBrush3}"
-                            Data="m399,355l-92.5,101.6c-13.7,13.7-10.6,36.4,4.6,47,13.7,9.1,30.3,7.6,41-4.5l127.4-138.1c16.7-18.2,18.2-47,0-66.8l-125.9-141.1c-12.1-13.7-34.9-15.2-47,0-12.1,12.1-12.1,31.9,0,44l92.5,98.6H28.8c-16.7,0-28.8,13.7-28.8,28.8s13.7,28.8,28.8,28.8h370.2v1.5ZM625.7,0H224.6c-44,0-78.9,34.9-78.9,78.9v101.6c0,15.2,12.1,27.3,27.3,27.3s27.3-12.1,27.3-27.3v-98.6c0-15.2,12.1-27.3,27.3-27.3h387.5c15.2,0,27.3,12.1,27.3,27.3v493c0,15.2-12.1,27.3-27.3,27.3H229.2c-15.2,0-27.3-12.1-27.3-27.3v-101.6c0-16.7-13.7-28.8-28.8-28.8s-28.8,13.7-28.8,28.8v98.6c0,44,34.9,78.9,78.9,78.9h407.2c44,0,67.9-34.9,67.9-78.9V78.9c-4.6-44-28.4-78.9-72.4-78.9Z" />
-                        <TextBlock Grid.Column="3" Grid.ColumnSpan="3" Grid.Row="1"
-                            Text="加入房间" FontSize="20" Foreground="{DynamicResource ColorBrush3}" />
-                        <TextBlock Grid.Column="3" Grid.ColumnSpan="3" Grid.Row="2" Margin="1,10,30,10" 
-                            Text="输入你收到的邀请码,加入朋友的房间。"
-                            FontSize="15" LineHeight="19" MaxWidth="400" TextWrapping="Wrap" Foreground="{StaticResource ColorBrushGray3}" />
-                    </Grid>
-                </local:MyCard>
-            </Grid>
-            <Grid x:Name="PanFinish" Visibility="Collapsed" HorizontalAlignment="Center" VerticalAlignment="Center" MinWidth="300">
-                <Grid.RowDefinitions>
-                    <RowDefinition Height="30" />
-                    <RowDefinition Height="35" />
-                    <RowDefinition Height="Auto" />
-                    <RowDefinition Height="25" />
-                    <RowDefinition Height="Auto" />
-                    <RowDefinition Height="60" />
-                </Grid.RowDefinitions>
-                <local:MyHint Text="你的网络环境差,正使用社区节点进行中继。" HasBorder="True"
-                              HorizontalAlignment="Center" x:Name="HintFinish" Theme="Yellow" />
-                <local:MyCard Grid.Row="2" HorizontalAlignment="Center" VerticalAlignment="Center" CornerRadius="12">
-                    <Grid>
-                        <Grid.ColumnDefinitions>
-                            <ColumnDefinition Width="25" />
-                            <ColumnDefinition Width="Auto" /> <!--Logo-->
-                            <ColumnDefinition Width="13" />
-                            <ColumnDefinition Width="1*" /> <!--文本区 ↓↓-->
-                            <ColumnDefinition Width="Auto" /> <!--按钮 1-->
-                            <ColumnDefinition Width="Auto" /> <!--按钮 2-->
-                            <ColumnDefinition Width="25" />
-                        </Grid.ColumnDefinitions>
-                        <Grid.RowDefinitions>
-                            <RowDefinition Height="21" />
-                            <RowDefinition Height="Auto" /> <!--标题栏-->
-                            <RowDefinition Height="Auto" /> <!--文本区-->
-                            <RowDefinition Height="Auto" /> <!--按钮-->
-                            <RowDefinition Height="21" />
-                        </Grid.RowDefinitions>
-                        <Path Grid.Column="1" Grid.Row="1" Height="28" Width="28" x:Name="ImgFinishLogo"
-                              Stretch="Uniform" Fill="{DynamicResource ColorBrush3}" RenderTransformOrigin="0.5,0.54"
-                              Data="M845 547.45h65.54A400.6 400.6 0 0 0 712.7 201.77l-33.22 56.49C781.6 318.32 845 429.13 845 547.45zM347.72 253.92L316 196.58a401.21 401.21 0 0 0-207 350.87h65.54a335.66 335.66 0 0 1 173.18-293.53zM509.79 882.68a334 334 0 0 1-181.3-53.2L293 884.57a401.13 401.13 0 0 0 419.1 8.91L679 836.93a334.94 334.94 0 0 1-169.21 45.75z M821.51 625.1A108.64 108.64 0 0 0 752.2 650l-67.31-38.86a185.44 185.44 0 0 0 11.22-63.7c0-91.55-66.38-167.9-153.54-183.42V289a109.14 109.14 0 1 0-65.57 0v75c-87.16 15.53-153.54 91.87-153.54 183.42a185.46 185.46 0 0 0 11.22 63.7L267.39 650a109.17 109.17 0 1 0 35.81 55l64.47-37.23a186 186 0 0 0 284.23 0L716.38 705a109.13 109.13 0 1 0 105.13-79.9zM198.07 777.85a43.6 43.6 0 1 1 43.61-43.61 43.66 43.66 0 0 1-43.61 43.61z m311.72-636.53a43.61 43.61 0 1 1-43.61 43.61 43.65 43.65 0 0 1 43.61-43.61z m0 526.91a120.78 120.78 0 1 1 120.78-120.78 120.91 120.91 0 0 1-120.78 120.77z m311.72 109.62a43.6 43.6 0 1 1 43.61-43.61 43.65 43.65 0 0 1-43.61 43.61z" />
-                        <TextBlock x:Name="LabFinishTitle" Grid.Column="3" Grid.ColumnSpan="3" Grid.Row="1" Margin="0,3,0,0"
-                                   Text="已加入房间" FontSize="20" Foreground="{DynamicResource ColorBrush3}" />
-                        <TextBlock x:Name="LabFinishDesc" Grid.Column="3" Grid.ColumnSpan="3" Grid.Row="2" Margin="1,9,10,10" 
-                                   Text="描述文本。"
-                                   FontSize="14" LineHeight="19" MaxWidth="400" TextWrapping="Wrap" Foreground="{StaticResource ColorBrushGray2}" />
-                        <local:MyButton x:Name="BtnFinishCopy" Grid.Column="4" Grid.Row="3" Margin="0,0,15,0" Text="复制邀请码" Padding="13,8" ColorType="Highlight" />
-                        <local:MyButton x:Name="BtnFinishExit" Grid.Column="5" Grid.Row="3" Text="退出" Padding="13,8" />
-                    </Grid>
-                </local:MyCard>
-                <local:MyCard Grid.Row="4" HorizontalAlignment="Center" VerticalAlignment="Center" CornerRadius="8">
-                    <StackPanel Orientation="Horizontal" Margin="3,0">
-                        <StackPanel x:Name="BtnFinishPing" Orientation="Horizontal" Background="{StaticResource ColorBrushSemiTransparent}"
-                                    ToolTip="延迟" ToolTipService.InitialShowDelay="50">
-                            <Path IsHitTestVisible="False" Height="14" Width="14" Stretch="Uniform" Fill="#9C9C9C" Margin="13,0,8,0"
-                                Data="M60.9 966.2A60.3 60.3 0 0 1 0.8 906.1v-90.1a60.3 60.3 0 0 1 60.1-60.1 60.3 60.3 0 0 1 60.1 60.1v90.1a60.2 60.2 0 0 1-60.1 60.1z m300.6 0a60.3 60.3 0 0 1-60.1-60.1V665.6a60.3 60.3 0 0 1 60.1-60.1 60.3 60.3 0 0 1 60.1 60.1v240.5a60.2 60.2 0 0 1-60.1 60.1z m300.6 0a60.3 60.3 0 0 1-60.1-60.1V425.1a60.3 60.3 0 0 1 60.1-60.1 60.3 60.3 0 0 1 60.1 60.1v481.0a60.3 60.3 0 0 1-60.1 60.1z m300.6 0a60.3 60.3 0 0 1-60.1-60.1V124.5a60.3 60.3 0 0 1 60.1-60.1 60.3 60.3 0 0 1 60.1 60.1v781.6a60.3 60.3 0 0 1-60.1 60.1z" />
-                            <TextBlock x:Name="LabFinishPing" IsHitTestVisible="False" Text="15 ms" VerticalAlignment="Center" Foreground="{StaticResource ColorBrushGray1}" Margin="0,0.5,13,0" />
-                        </StackPanel>
-                        <TextBlock Text="|" Opacity="0.2" Margin="0,8.8,0,8" RenderTransformOrigin="0.5,0.5">
-                            <TextBlock.RenderTransform>
-                                <ScaleTransform ScaleX="1.5" ScaleY="2.75" CenterX="0.5" CenterY="0.5" />
-                            </TextBlock.RenderTransform>
-                        </TextBlock>
-                        <StackPanel x:Name="BtnFinishPlayer" Orientation="Horizontal" Background="{StaticResource ColorBrushSemiTransparent}"
-                                    ToolTip="房间内人数" ToolTipService.InitialShowDelay="50">
-                            <Path IsHitTestVisible="False" Height="18" Width="18" Stretch="Uniform" Fill="{StaticResource ColorBrushGray4}" Margin="13,0,8,0"
-                                Data="m843.3,478.4c-.1-3.9-.2-7.7-.2-10.8,0-42.8-11.1-84.4-32-120.2-19.6-33.5-47.1-60.7-79.7-79,15-16.8,23.6-39.2,25-65.2,1.3-24.8-3.7-52.1-14.6-79-20.5-50.8-56-86.3-86.3-86.3s-25.1,4.1-37.5,23.5l-.4.7-.2.8c-4.1,16.6,3.9,33.4,20,41.7l.7.4h.8c34.9,5.6,50.6,38.7,53.5,65.3,3.3,30.7-7.8,67.2-40.1,80.1-7.4,1.1-9.9,7.7-11.5,12.1-.6,1.7-1.2,3.3-1.9,4.3l-.7,1v1.2c-3.4,27.8,19.6,39.8,46.3,53.6,41.4,21.4,93,48.1,93.1,144.9v.3c.3,4.3,0,9.9-.1,15.7-.6,17.9-1.4,40.2,10.3,52.4,5.5,5.7,13,8.6,22.5,8.7.7,0,1.4,0,2.1,0h0c8.1,0,14.7-2.6,19.8-7.9,12.7-13.1,11.9-39.4,11.4-58.6Z M204.7,295.9l.5-1.1v-30.1c0-3.9-2.1-6-3.3-7.1-.1-.1-.2-.2-.3-.3v-1.7l-8.9-9.2-.7-.4c-26.3-13.5-42.6-40.8-42.6-71.3s21.8-67.4,55.5-78.1c17-4.5,28.2-22.1,24.5-38.4v-.6c-.1,0-.4-.5-.4-.5-7.5-15.3-21.4-24.7-36.2-24.7s-5.1.3-7.7.8h-.2c-29.8,7.2-56.6,25.5-75.7,51.4-18.9,25.7-29.3,57.8-29.3,90.4s11.1,64.8,32.2,92.1C45.3,303.9,2.5,376.9,0,458.9v57.5s0,.2,0,.2c1.4,15.3,13.1,30.8,32.8,30.8s32.9-12.7,32.9-33.9v-43.3c0-76.2,45.9-139.2,114.3-157.1,10.5-.5,19.5-6.8,24.7-17.3Z M516,286.5c36.8-31.1,58.5-77.4,58.5-125.4C574.4,70.8,507.4,0,421.7,0s-152.7,70.8-152.7,161.2,20.4,95.1,56.4,125.6c-51.7,19.8-96.1,54.8-129,101.5-35.2,50.2-53.9,109.7-53.9,172v33.8c0,16.9,13.7,30.6,30.6,30.6h496.6c16.9,0,30.6-13.7,30.6-30.6v-33.8c0-61.4-19-120.6-54.9-171.3-32.9-46.4-78.6-82.5-129.5-102.4Zm-94.5-225.3c51.3,0,91.5,43.9,91.5,100s-41.9,100-91.5,100-91.5-43.9-91.5-100,40.2-100,91.5-100Zm218,502H203.8v-3.2c0-127,97.8-230.3,218-230.3s217.7,105.4,217.7,230.3v3.2Z" />
-                            <TextBlock x:Name="LabFinishPlayer" IsHitTestVisible="False" Text="2  人" VerticalAlignment="Center" Foreground="{StaticResource ColorBrushGray1}" Margin="0,0.5,13,0" />
-                        </StackPanel>
-                        <TextBlock Text="|" x:Name="LineFinishPort" Opacity="0.2" Margin="0,8.8,0,8" RenderTransformOrigin="0.5,0.5">
-                            <TextBlock.RenderTransform>
-                                <ScaleTransform ScaleX="1.5" ScaleY="2.75" CenterX="0.5" CenterY="0.5" />
-                            </TextBlock.RenderTransform>
-                        </TextBlock>
-                        <StackPanel x:Name="BtnFinishPort" Orientation="Horizontal" Background="{StaticResource ColorBrushSemiTransparent}"
-                                    ToolTip="端口" ToolTipService.InitialShowDelay="50">
-                            <Path IsHitTestVisible="False" Height="16" Width="16" Stretch="Uniform" Fill="{StaticResource ColorBrushGray4}" Margin="13,0,7,0"
-                                Data="m365,295.3c-15.1-3.6-36.5-1.8-39.6,17.5-1.2,12.4,0,107-.5,119.7.8,16.8-5.2,42.4,16.8,47.1,14.3,3.3,34.5,1.9,38.7-15.4,2.6-1.4.1-142.4,1-148.3-.7-10.9-6.2-17.8-16.4-20.6ZM234.1,311.6c-3.2-16.7-20-18.6-34.4-17.2-9.8.4-17.2,6.2-20,15.4-4.4,47.8-.2,97.3-1.6,144.8,0,16,9.8,26.6,24.6,26.1,14.4,1.4,29.1-2.9,31.7-19,2.2-8.8.8-63.9,1.1-74.1-1-25.3,2.1-50.6-1.5-75.9ZM521.4,300.6c-5.4-6.2-15.9-7.1-24.6-6.4-11.9-1-23,6.6-24.7,18.7-2.2,24.1-2,124-.2,148.1,2.6,19.7,24.1,22.4,40.1,18.4,11.9-2.4,16.6-15.2,16.3-27.1,0-33.6.1-69.3,0-103.1-1.2-15.1,4.2-37-6.9-48.5ZM708.8,194.2c.7-34.5-26.8-63.7-61.5-63.4-22.1-.1-52.4,0-75.6,0-13.4,1.1-8.7-12.1-9.6-20.9-1.6-30.6,7.5-67.2-17.5-90.9-11.7-11.9-27.4-18.4-44.3-18.4-64.8,0-129.7,0-194.5,0-37.7,1.4-76.5-3.1-113.8,2.2-48.1,14.7-46.7,55.3-45.4,97.3,0,8,.1,16.1,0,24.1.2,7-7.3,6.8-12.2,6.6-16,0-32,0-48,0-24.4-.7-51.3-1-68.6,18.4C5.8,161.2,0,176.1,0,194.7c0,0,0,350.9,0,350.9,0,22.9,5.5,41.7,16.8,57.5,19.2,27,45.9,40.7,79.5,40.7,37.4,0,77.4.1,129.9.1,42.5,0,85,0,127.5,0,11.2-1.3,273.6,2.4,278-1.8,46.1-9.1,77.1-46.8,77.1-93.8,0-117.8,0-237.9,0-354.1Zm-71-5.3c7.6.2,9.2,1.9,9.3,9.7,0,0,0,347.3,0,347.3,0,3.2.2,6.5-.3,9.9-2.8,19.3-18.9,32.3-38.5,31.9-4.5.4-508.9-.4-513.5,0-3.2,0-6.5.1-9.9-.4-19-2.9-31.8-17.8-31.8-37.1,0-117.9,0-233.9,0-350.9,0-9,2.3-10.3,10.9-10.3,0,0,131.2,0,131.2,0,1.6,0,3,0,4.5,0,0-1.5,0-2.7,0-4.3,0,0,0-115.7,0-115.7.1-8.3,1.9-10.1,10.3-10.1h280.3c8.3,0,10.1,1.8,10.1,10.3v114.1c0,1.9,0,3.7,0,5.6" />
-                            <TextBlock x:Name="LabFinishPort" IsHitTestVisible="False" Text="12345" VerticalAlignment="Center" Foreground="{StaticResource ColorBrushGray1}" Margin="0,0.5,13,0" />
-                        </StackPanel>
-                        <StackPanel x:Name="BtnFinishIp" Orientation="Horizontal" Background="{StaticResource ColorBrushSemiTransparent}"
-                                    ToolTip="服务器 IP(点击复制)" ToolTipService.InitialShowDelay="50">
-                            <Path IsHitTestVisible="False" Height="17" Width="17" Stretch="Uniform" Fill="{StaticResource ColorBrushGray4}" Margin="11,0,7,0"
-                                Data="m346.4,692.8c-92.5,0-179.5-36-244.9-101.5C36,525.9,0,438.9,0,346.4S36,166.9,101.5,101.5C166.9,36,253.9,0,346.4,0s179.5,36,244.9,101.5c65.4,65.4,101.5,152.4,101.5,244.9s-36,179.5-101.5,244.9c-65.4,65.4-152.4,101.5-244.9,101.5Zm0-645.1c-164.7,0-298.7,134-298.7,298.7s134,298.7,298.7,298.7,298.7-134,298.7-298.7S511.1,47.7,346.4,47.7Zm13.8,473.4c-14.5,0-28.2-9.8-28.2-24.2V207.9c0-14.5,11.8-26.2,26.2-26.2h78.9c57.9,0,105.1,47.1,105.1,105.1s-47.1,105.1-105.1,105.1h-52.6v105.1c0,14.5-9.8,24.2-24.2,24.2Zm24.2-181.7h52.6c29,0,52.6-23.6,52.6-52.6s-20-49.2-46.5-52.3h-.4s-5.3-.4-5.3-.4h-53v105.2Zm-158.8,181.7c-14.5,0-28.2-9.8-28.2-24.2V207.9c0-14.5,11.8-26.2,26.2-26.2s26.2,11.8,26.2,26.2v289c0,14.5-9.8,24.2-24.2,24.2Z" />
-                            <TextBlock x:Name="LabFinishIp" IsHitTestVisible="False" Text="6.0.0.1:12345" VerticalAlignment="Center" Foreground="{StaticResource ColorBrushGray1}" Margin="0,0.5,13,0" />
-                        </StackPanel>
-                    </StackPanel>
-                </local:MyCard>
-            </Grid>
-        </Grid>
-        <Grid x:Name="PanAlways" HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="15" Opacity="0.5">
-            <local:MyTextButton Text="示例文本 www.meloong.com" Opacity="0.4" />
-        </Grid>
-    </Grid>
+<local:MyPageRight x:Class="PageLinkMain" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:PCL">
+    <TextBlock Text="此版本已移除联机功能。" Margin="25" Foreground="{DynamicResource ColorBrush1}" />
 </local:MyPageRight>

+ 6 - 1099
Plain Craft Launcher 2/Pages/PageLink/PageLinkMain.xaml.vb

@@ -1,1116 +1,23 @@
-Imports System.Collections.ObjectModel
-Imports System.Globalization
-Imports System.Net.Sockets
-Imports System.Reflection
-Imports PCL
-
 Public Class PageLinkMain
-    Private Const INVITE_CODE_VERSION As Integer = 2
-
-    '===============================
-    '  状态机与前端页面
-    '===============================
-
-#Region "状态管理"
-
     Public Enum LinkStates
         Waiting
         Loading
         Failed
         Finished
     End Enum
-    Public Shared LinkState As LinkStates = LinkStates.Waiting
-
-    ''' <summary>
-    ''' 切换到指定的状态。
-    ''' </summary>
-    Private Sub ChangeState(NewState As LinkStates)
-        If LinkState = NewState Then Return
-        Dim OldState = LinkState
-        LinkState = NewState
-        Logger.Info($"主状态由 {OldState} 变更为 {NewState}")
-        '触发状态切换
-        RunInUi(
-        Sub()
-            Select Case NewState
-                Case LinkStates.Waiting
-                    SwitchToWaiting(OldState)
-                Case LinkStates.Loading
-                    SwitchToLoading(OldState)
-                Case LinkStates.Failed
-                    SwitchToFailed(OldState)
-                Case LinkStates.Finished
-                    SwitchToFinished(OldState)
-            End Select
-        End Sub)
+    Public Shared ReadOnly LinkState As LinkStates = LinkStates.Waiting
+    Public Shared Sub Join()
+        Hint("此版本已移除联机功能。")
     End Sub
-
-    '让加载器的 成功/失败 事件触发状态切换
-    Private Sub LoaderInit() Handles Me.Initialized
-        PageLoaderInit(Load, PanLoad, PanContent, PanAlways, LinkLoader, AutoRun:=False)
-        AddHandler LinkLoader.OnStateChangedUi,
-        Sub(Loader As LoaderBase, NewState As LoadState, OldState As LoadState)
-            Select Case NewState
-                Case LoadState.Finished
-                    ChangeState(LinkStates.Finished)
-                Case LoadState.Failed
-                    Telemetry("联机失败", "Exception", FilterUserName(Loader.Error.GetDisplay(True), "*"))
-                    ChangeState(LinkStates.Failed)
-            End Select
-        End Sub
-    End Sub
-
-    '页面动画兼容
-    Private Sub UpdatePanelVisibility() Handles Me.PageEnter
-        FrmLinkMain.PanSelect.Visibility = If(LinkState = LinkStates.Waiting, Visibility.Visible, Visibility.Collapsed)
-        FrmLinkMain.PanFinish.Visibility = If(LinkState = LinkStates.Finished, Visibility.Visible, Visibility.Collapsed)
-    End Sub
-
-#End Region
-
-#Region "初始页面 (Waiting)"
-
-    '由任意状态切换到 Waiting
-    Private Sub SwitchToWaiting(OldState As LinkStates)
-        '加载器与进程状态
-        LinkLoader.Cancel()
-        SyncLock LinkLoader.LockState
-            LinkLoader.State = LoadState.Waiting
-        End SyncLock
-        ProcessStop()
-        '页面切换
-        If OldState <> LinkStates.Finished Then
-            '从完成页面退出时,PageOnContentExit 结束会触发 PageEnter 事件,这会调用 UpdatePanelVisibility
-            '如果在现在提前调用了,会丢失完成页面的退出动画
-            UpdatePanelVisibility()
-        End If
-        PageOnContentExit()
-    End Sub
-
-    '===============================
-    '  页面 UI
-    '===============================
-
-    '创建
-    Private Sub Create_MouseLeftButtonUp() Handles PanSelectCreate.MouseLeftButtonUp
-        '输入端口号
-        Dim Port As String = MyMsgBoxInput("输入端口", $"在单人游戏的暂停菜单选择 {vbLQ}对局域网开放{vbRQ},然后输入端口数字。{vbCrLf}甚至可以输入其他游戏的端口……嗯……",
-            ValidateRules:=New Collection(Of Validate) From {New ValidateInteger(1024, 65535)},
-            HintText:="端口号")
-        If Port Is Nothing Then Return
-        '基础信息
-        IsServerSide = True
-        ServerPort = Port
-        Dim GenerateRandomCode =
-        Function() As String
-            Dim Result As String = ""
-            For i = 1 To 5
-                Result &= RandomInteger(0, 35).ConvertRadix(36)
-            Next
-            Return Result.Replace("O", "0").Replace("I", "1")
-        End Function
-        NetworkName = $"P{ServerPort.ConvertRadix(16).PadLeft(4, "0"c)}-{GenerateRandomCode()}"
-        NetworkSecret = GenerateRandomCode()
-        DiscoverNodeID = -1
-        Logger.Info($"尝试创建房间,网络名 {NetworkName},网络密码 {NetworkSecret},端口 {ServerPort}")
-        '启动
-        ChangeState(LinkStates.Loading)
-    End Sub
-
-    '加入
-    Private Shared LastCode As String = Nothing
-    ''' <summary>
-    ''' 输入邀请码,切换到联机页并立即加入房间。
-    ''' </summary>
-    Public Shared Sub Join() Handles PanSelectJoin.MouseLeftButtonUp
-        Dim Code As String = MyMsgBoxInput("输入邀请码", "输入房主发给你的邀请码。",
-            HintText:=If(String.IsNullOrEmpty(LastCode), "", "使用上一次的邀请码"))
-        If Not String.IsNullOrEmpty(LastCode) AndAlso Code IsNot Nothing AndAlso Code = "" Then Code = LastCode
-        If Code Is Nothing Then Return
-        Join(Code)
-    End Sub
-    Public Sub JoinInternal(Code As String)
-        If Code Is Nothing Then Return
-        '基础格式校验
-        Dim ValidateResult = ValidateCodeFormat(Code)
-        If ValidateResult IsNot Nothing Then
-            Hint(ValidateResult, HintType.Red)
-            Return
-        End If
-        Code = FixCodeFormat(Code)
-        Logger.Info($"实际使用的邀请码:{Code}")
-        '基础信息
-        IsServerSide = False
-        ServerPort = Code.Substring(1, 4).Upper.ConvertRadix(16, 10)
-        NetworkName = Code.Substring(0, 11)
-        NetworkSecret = Code.Substring(12, 5)
-        If Code.Substring(20, 3) = "000" Then
-            DiscoverNodeID = -2
-        Else
-            DiscoverNodeID = Code.Substring(20, 3).Upper.ConvertRadix(16, 10)
-        End If
-        Logger.Info($"尝试加入房间,网络名 {NetworkName},网络密码 {NetworkSecret},端口 {ServerPort},发现节点 {DiscoverNodeID}")
-        '启动
-        LastCode = Code
-        ChangeState(LinkStates.Loading)
+    Public Shared Sub Join(Code As String)
+        Join()
     End Sub
     Public Shared Function ValidateCodeFormat(Code As String) As String
-        If Code Is Nothing Then Return "邀请码为空!"
-        Code = FixCodeFormat(Code)
-        '判断类型
-        If Not (Code.Length >= 14 AndAlso Code(0) = "P"c AndAlso Code(5) = "-"c AndAlso Code(11) = "-"c) Then
-            If Code.StartsWithF("U/") Then 'HMCL
-                Return "请让房主使用 PCL 创建房间!"
-            ElseIf Code.Length = 10 Then 'PCL CE
-                Return "请让房主使用非社区版的 PCL 创建房间!"
-            Else
-                Return "邀请码有误,请让房主使用 PCL 创建房间!"
-            End If
-        End If
-        '校验版本
-        If Code.Length >= 23 AndAlso Code(17) = "-"c AndAlso
-            Val(Code.Substring(18, 2)) > INVITE_CODE_VERSION Then Return "你的 PCL 版本太老了,请在更新 PCL 之后再联机!"
-        Return Nothing
-    End Function
-    Private Shared Function FixCodeFormat(Code As String) As String
-        Code = Code.Between("【", "】").Between("[", "]") '从完整消息中提取
-        Code = Code.Upper.Replace("O", "0").Replace("I", "1") '输入修正
-        '版本 1 兼容
-        If Code.Length >= 17 AndAlso (Code.Length < 23 OrElse (Code.Length >= 18 AndAlso Code(17) <> "-"c)) Then
-            Code = Code.Substring(0, 17) & "-0105E"
-        End If
-        Return Code
+        Return "此版本已移除联机功能。"
     End Function
-
-    '自动加入
-
-    ''' <summary>
-    ''' 切换到联机页并立即加入指定房间。
-    ''' </summary>
-    Public Shared Sub Join(Code As String)
-        If LinkState <> LinkStates.Waiting Then
-            Hint("你已经在联机房间中了!", HintType.Red)
-        ElseIf FrmMain.PageCurrent = FormMain.PageType.Link Then
-            FrmLinkMain.JoinInternal(Code)
-        Else
-            AutoJoinCode = Code
-            FrmMain.PageChange(FormMain.PageType.Link, FormMain.PageSubType.LinkMain)
-        End If
-    End Sub
-    '在进入页面时自动尝试加入房间
-    Private Shared AutoJoinCode As String = Nothing
-    Private Sub PanSelectJoin_Loaded() Handles PanSelectJoin.Loaded
-        If AutoJoinCode Is Nothing Then Return
-        If LinkState = LinkStates.Waiting Then JoinInternal(AutoJoinCode)
-        AutoJoinCode = Nothing
-    End Sub
-
-#End Region
-
-#Region "加载页面 (Loading | Failed)"
-
-    '由 Waiting 或 Failed 状态切换到 Loading
-    Private Sub SwitchToLoading(OldState As LinkStates)
-        '清理变量
-        Peers = Nothing
-        FailCount = 0
-        '加载器与进程状态
-        LinkLoader.Start(IsForceRestart:=True)
-        '页面切换会由 Loader 调用 MyPageRight 来触发
-        'UI 更新
-        UpdateProgressBar(0)
-        LabLoadTitle.Text = If(IsServerSide, "创建房间中", "加入房间中")
-        UpdateLoadingPage("正在初始化……", "准备初始化")
-    End Sub
-
-    '由 Loading 状态切换到 Failed
-    Private Sub SwitchToFailed(OldState As LinkStates)
-        '加载器与进程状态
-        LinkLoader.Cancel()
-        ProcessStop()
-        'UI 更新
-        UpdateProgressBar(1)
-        '显示错误信息
-        LabLoadTitle.Text = FailReason
-        Dim Brief As String = LinkLoader.Error.GetDisplay(False)
-        Dim InnerEx As Exception = LinkLoader.Error
-        If InnerEx.Message.StartsWithF("$") Then Brief = InnerEx.Message
-        Do Until InnerEx.InnerException Is Nothing
-            InnerEx = InnerEx.InnerException
-            If InnerEx.Message.StartsWithF("$") Then Brief = InnerEx.Message
-        Loop
-        LabLoadDesc.Text = If(Brief.StartsWithF("$"), Brief.TrimStart("$"), LinkLoader.Error.GetDisplay(True))
-        Logger.Warn(LinkLoader.Error, LabLoadTitle.Text)
-    End Sub
-
-    '===============================
-    '  页面 UI
-    '===============================
-
-    Private Sub UpdateLoadingPage(Title As String, FailBrief As String)
-        If FailReason = FailBrief & "失败" Then Return
-        FailReason = FailBrief & "失败"
-        Logger.Info($"开始步骤:{Title}")
-        RunInUiWait(
-        Sub()
-            If FrmLinkMain Is Nothing OrElse Not FrmLinkMain.LabLoadDesc.IsLoaded Then Return
-            FrmLinkMain.LabLoadDesc.Text = Title
-        End Sub)
-    End Sub
-    Private FailReason As String = "准备初始化"
-
-    '取消加载
-    Private Sub BtnLoadCancel_Click() Handles BtnLoadCancel.Click
-        ChangeState(LinkStates.Waiting)
-    End Sub
-
-    '点击重试
-    Private Sub CardLoad_MouseLeftButtonUp() Handles CardLoad.MouseLeftButtonUp
-        ChangeState(LinkStates.Loading)
-    End Sub
-
-    '进度条
-    Private Sub UpdateProgressBar(Optional Value As Double = -1)
-        If Value = -1 Then Value = LinkLoader.Progress
-        Dim DisplayingProgress As Double = ColumnProgressA.Width.Value
-        If Math.Round(Value - DisplayingProgress, 3) = 0 Then Return
-        If DisplayingProgress <= Value Then
-            Dim NewProgress As Double = If(Value = 1, 1, DisplayingProgress +
-                (Value - DisplayingProgress) ^ 2 * 0.5)
-            AniStart({
-                AaGridLengthWidth(ColumnProgressA, NewProgress - ColumnProgressA.Width.Value, 200),
-                AaGridLengthWidth(ColumnProgressB, (1 - NewProgress) - ColumnProgressB.Width.Value, 200)
-            }, "Link Progress")
-        Else
-            ColumnProgressA.Width = New GridLength(Value, GridUnitType.Star)
-            ColumnProgressB.Width = New GridLength(1 - Value, GridUnitType.Star)
-            AniStop("Link Progress")
-        End If
-    End Sub
-    Private Sub CardLoad_SizeChanged() Handles CardLoad.SizeChanged
-        RectProgressClip.Rect = New Rect(0, 0, CardLoad.ActualWidth, 12)
-    End Sub
-
-#End Region
-
-#Region "完成页面 (Finished)"
-
-    '由 Loading 状态切换到 Finished
-    Private Sub SwitchToFinished(OldState As LinkStates)
-        '由于只可能从加载器的完成事件触发,不需要管加载器
-        UpdatePanelVisibility() '页面的实际切换会由 Loader 调用 MyPageRight 来触发
-
-        'UI 更新
-        If IsServerSide Then
-            LabFinishTitle.Text = "已创建房间"
-            LabFinishDesc.Text = $"把邀请码发给朋友,让大家加入房间吧!{vbCrLf}邀请码:{GetInviteCode()}"
-            BtnFinishExit.Text = "关闭"
-            BtnFinishPing.ToolTip = "网络延迟"
-            BtnFinishCopy.Visibility = Visibility.Visible
-            Copy() '立即复制邀请码
-            '下边栏
-            BtnFinishPort.Visibility = Visibility.Visible
-            LabFinishPort.Visibility = Visibility.Visible
-            BtnFinishIp.Visibility = Visibility.Collapsed
-            LabFinishIp.Visibility = Visibility.Collapsed
-            LabFinishPort.Text = ServerPort
-        Else
-            LabFinishTitle.Text = "已加入房间"
-            LabFinishDesc.Text = $"在多人游戏页面的最下方就能找到联机房间!{vbCrLf}注意:使用离线登录时不要手动输入 IP!"
-            BtnFinishExit.Text = "离开"
-            BtnFinishPing.ToolTip = "与房主的延迟"
-            BtnFinishCopy.Visibility = Visibility.Collapsed
-            '下边栏
-            BtnFinishPort.Visibility = Visibility.Collapsed
-            LabFinishPort.Visibility = Visibility.Collapsed
-            LabFinishIp.Text = ClientAddress
-            BtnFinishIp.Visibility = Visibility.Visible
-            LabFinishIp.Visibility = Visibility.Visible
-        End If
-        Update() '立即刷新
-    End Sub
-
-    '===============================
-    '  页面 UI
-    '===============================
-
-    '退出
-    Private Sub BtnFinishExit_Click() Handles BtnFinishExit.Click
-        TryExit(False, False)
-    End Sub
-    ''' <summary>
-    ''' 退出联机。
-    ''' 返回是否弹出了警告窗口并且玩家选择了取消。
-    ''' </summary>
     Public Function TryExit(Silent As Boolean, Closing As Boolean) As Boolean
-        If LinkState = LinkStates.Waiting OrElse LinkState = LinkStates.Failed Then Return False
-        If Not Silent Then
-            If IsServerSide AndAlso PeopleCount > 1 Then
-                If MyMsgBox("你确定要关闭联机房间吗?" & vbCrLf & "所有玩家都需要重新输入邀请码才可加入游戏!", "退出联机", "确定", "取消", IsWarn:=True) = 2 Then Return True
-            ElseIf Closing Then
-                If MyMsgBox(If(IsServerSide, "你确定要关闭联机房间吗?", "你确定要离开联机房间吗?"), "退出联机", "确定", "取消") = 2 Then Return True
-            End If
-        End If
-        ChangeState(LinkStates.Waiting)
         Return False
     End Function
-
-    '复制邀请码
-    Private Sub Copy() Handles BtnFinishCopy.Click
-        Dim CodeText As String = $"在 PCL 启动器中输入邀请码【{GetInviteCode()}】,即可加入联机房间!"
-        ClipboardSet(CodeText, False)
-        Settings.Set("LinkLastAutoJoinInviteCode", CodeText)
-        Hint("已复制邀请码!", HintType.Green)
-    End Sub
-    Private Function GetInviteCode() As String
-        Return $"{NetworkName}-{NetworkSecret}-{INVITE_CODE_VERSION.ToString.PadLeft(2, "0"c)}{ _
-            If(DiscoverNodeID = -1, 0, DiscoverNodeID).ConvertRadix(16).PadLeft(3, "0"c)}"
-    End Function
-
-    '复制 IP
-    Private Sub BtnFinishIp_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles BtnFinishIp.MouseLeftButtonUp
-        ClipboardSet(ClientAddress, False)
-        Hint("已复制服务器地址!", HintType.Green)
-    End Sub
-
-#End Region
-
-    '===============================
-    '  后端
-    '===============================
-
-    ''' <summary>
-    ''' EasyTier 所在的文件夹路径,以 \ 结尾。
-    ''' </summary>
-    Private PathEasyTier As String = Paths.AppDataThenName & "EasyTier\"
-    ''' <summary>
-    ''' 当前是服务端还是客户端。
-    ''' </summary>
-    Private IsServerSide As Boolean
-    ''' <summary>
-    ''' 服务端映射前的端口号。
-    ''' </summary>
-    Private ServerPort As Integer
-    ''' <summary>
-    ''' 客户端映射后的端口号。
-    ''' </summary>
-    Private ClientPort As Integer
-    ''' <summary>
-    ''' RPC 端口号。
-    ''' </summary>
-    Private RPCPort As Integer
-    ''' <summary>
-    ''' 在客户端映射后的地址。
-    ''' 除非已作为客户端启动 EasyTier,否则一直为 Nothing。
-    ''' </summary>
-    Private ClientAddress As String = Nothing
-    ''' <summary>
-    ''' 发现节点的 ID。
-    ''' 若必须设定自定义节点则为 -2;若等待选择则为 -1;选择回退节点则为 0;否则为对应节点的 ID。
-    ''' </summary>
-    Private DiscoverNodeID As Integer = -1
-    ''' <summary>
-    ''' 网络信息。
-    ''' </summary>
-    Private NetworkName As String, NetworkSecret As String
-
-#Region "加载"
-
-    Private WithEvents LinkLoader As New LoaderCombo(Of Integer)("联机", {
-        New LoaderTask(Of Integer, Integer)("获取配置", AddressOf InitConfig) With {.ProgressWeight = 8},
-        New LoaderTask(Of Integer, List(Of NetFile))("准备下载联机模块", AddressOf InitPrepareDownload) With {.ProgressWeight = 2},
-        New LoaderDownload("下载联机模块", New List(Of NetFile)) With {.ProgressWeight = 40},
-        New LoaderTask(Of Integer, Integer)("启动联机模块", AddressOf InitLaunch) With {.ProgressWeight = 50}
-    })
-
-    '1. 获取服务器配置
-    Private Sub InitConfig(Task As LoaderTask(Of Integer, Integer))
-        UpdateLoadingPage("正在联网获取配置……", "联网获取配置")
-        If VersionBranchMain <> "Official" Then
-            Throw New Exception($"$开源版无法联网获取配置。{vbCrLf}你可以在 PCL 官方版的缓存文件夹下查看 ServerConfig 的当前内容, 并在代码中进行相应修改。")
-        End If
-        ServerLoader.WaitForExit(LoaderToSyncProgress:=Task)
-        If ServerConfig Is Nothing Then Throw New Exception("无法从服务器获取配置")
-        '检查是否已禁用联机功能
-        Dim DisableReason = ServerConfig("Link")?("DisableReason2")?.ToString
-        If Not String.IsNullOrEmpty(DisableReason) Then Throw New Exception("$" & DisableReason) 'TODO: 可能不会显示错误原因?
-        If CType(ServerConfig("Link"), JObject).ContainsKey("MinVersionCode") AndAlso
-           VersionCode < ServerConfig("Link")("MinVersionCode").ToObject(Of Integer) Then
-            Throw New Exception("$你的 PCL 版本太老了,请在更新 PCL 之后再联机!")
-        End If
-    End Sub
-
-    '2. 获取需要下载的文件
-    Private ServerVersion As Integer
-    Private Sub InitPrepareDownload(Task As LoaderTask(Of Integer, List(Of NetFile)))
-        '获取 CPU 架构
-        UpdateLoadingPage("正在获取 CPU 架构……", "获取 CPU 架构")
-        Dim Architecture As String = GetType(String).Assembly.GetName().ProcessorArchitecture
-        Select Case Architecture
-            Case ProcessorArchitecture.X86
-                Architecture = "i686"
-            Case ProcessorArchitecture.Amd64
-                Architecture = "x86_64"
-            Case ProcessorArchitecture.Arm
-                Architecture = "arm64"
-            Case Else
-                Logger.Warn($"CPU 是不支持的 {Architecture} 架构,这可能会导致联机模块无法启动!")
-                Architecture = "arm64"
-        End Select
-        Logger.Info($"CPU 架构:{Architecture}")
-        Telemetry("联机开始")
-        '检查 EasyTier 版本
-        UpdateLoadingPage("正在检查联机模块版本……", "检查联机模块版本")
-        If Not (FileUtils.Exists(PathEasyTier & "联机模块 CLI.exe") AndAlso FileUtils.Exists(PathEasyTier & "联机模块.exe") AndAlso
-                FileUtils.Exists(PathEasyTier & "Packet.dll")) Then
-            Settings.Set("LinkEasyTierVersion", -1)
-        End If
-        Dim LocalVersion As Integer = Settings.Get(Of Integer)("LinkEasyTierVersion")
-        ServerVersion = ServerConfig("Link")("EasyTierVersion")
-        Dim RequiredFiles As New List(Of NetFile)
-        Logger.Info($"EasyTier 本地版本:{LocalVersion},需求版本:{ServerVersion}")
-        If LocalVersion < ServerVersion Then
-            RequiredFiles.Add(New NetFile(
-                ServerConfig("Link")("Downloads").Select(Function(UrlEntry) UrlEntry.ToString.Replace("{arch}", Architecture)),
-                PathEasyTier & "EasyTier.zip",
-                New FileChecker With {.MinSize = 1024 * 1024 * 2}))
-        End If
-        '开始下载
-        UpdateLoadingPage("正在下载联机模块……", "下载联机模块")
-        Task.Output = RequiredFiles
-    End Sub
-
-    '3. 启动联机模块
-    Private Sub InitLaunch(Task As LoaderTask(Of Integer, Integer))
-        '解压文件
-        UpdateLoadingPage("正在解压联机模块……", "解压联机模块")
-        If FileUtils.Exists(PathEasyTier & "EasyTier.zip") Then
-            '解压
-            Dim ExtractPath As String = RequestTaskTempFolder()
-            'FileUtils.ExtractToDirectory(PathEasyTier & "EasyTier.zip", ExtractPath,
-            '    progressHandler:=Sub(Percentage) Task.Progress = Percentage * 0.05)
-            DirectoryUtils.Copy(DirectoryUtils.EnumerateDirectories(ExtractPath, True).First, PathEasyTier)
-            '重命名
-            FileUtils.Move(PathEasyTier & "easytier-core.exe", PathEasyTier & "联机模块.exe")
-            FileUtils.Move(PathEasyTier & "easytier-cli.exe", PathEasyTier & "联机模块 CLI.exe")
-            '清理
-            FileUtils.Delete(PathEasyTier & "EasyTier.zip")
-            Settings.Set("LinkEasyTierVersion", ServerVersion)
-        End If
-        Task.Progress = 0.07
-        '获取节点列表
-        UpdateLoadingPage("正在获取节点列表……", "获取节点列表")
-        Dim RawPeers As List(Of String)
-        Dim CustomPeers As String = Settings.Get(Of String)("LinkCustomPeer")
-        If String.IsNullOrWhiteSpace(CustomPeers) Then
-            If DiscoverNodeID = -2 AndAlso Not IsServerSide Then
-                Panic("未填写自定义节点设置", $"$你必须在 {vbLQ}自定义节点{vbRQ} 设置中填写与房主相同的内容,{vbCrLf}才能进入该房间!")
-                Return
-            End If
-            RawPeers = GetTargetPeers()
-        Else
-            If DiscoverNodeID <> -2 AndAlso Not IsServerSide Then Hint("房主可能没有使用自定义节点设置,请确认你们的自定义节点设置是否一致!")
-            RawPeers = CustomPeers.Split(",,".ToCharArray).Select(Function(p) p.Trim).Where(Function(p) Not String.IsNullOrEmpty(p)).ToList()
-            Logger.Info($"使用自定义节点:{CustomPeers}")
-        End If
-        Task.Progress = 0.13
-        '获取空闲端口
-        UpdateLoadingPage("正在启动联机模块……", "启动联机模块")
-        Dim FreePorts = FindFreePorts(5, ServerPort)
-        ClientPort = FreePorts(0)
-        RPCPort = FreePorts(1)
-        Dim ListenersPort As Integer = FreePorts(2) '它会占用【当前、当前 +1、当前 +2】共三个端口
-        '获取启动参数
-        Dim Arguments As String = ServerConfig("Link")("Argument")
-        Arguments += $" --network-name={NetworkName} --network-secret={NetworkSecret} --listeners {ListenersPort} --rpc-portal {RPCPort} --private-mode true"
-        Dim HostName = If(IsServerSide, "Server-", "Client-") & Identify.GetStableHashCode().ConvertRadix(36).TrimStart("-"c).EnsureLength("0", 6)
-        If IsServerSide Then
-            Arguments += $" -i 10.114.114.114 --hostname={HostName} --tcp-whitelist={ServerPort} --udp-whitelist={ServerPort}"
-        Else
-            Arguments += $" -d --hostname={HostName} --tcp-whitelist=0 --udp-whitelist=0"
-            '端口转发
-            ClientAddress = $"localhost:{ClientPort}"
-            Arguments += $" --port-forward tcp://[{IPAddress.IPv6Loopback}]:{ClientPort}/10.114.114.114:{ServerPort}"
-            Arguments += $" --port-forward udp://[{IPAddress.IPv6Loopback}]:{ClientPort}/10.114.114.114:{ServerPort}"
-            Arguments += $" --port-forward tcp://{IPAddress.Loopback}:{ClientPort}/10.114.114.114:{ServerPort}"
-            Arguments += $" --port-forward udp://{IPAddress.Loopback}:{ClientPort}/10.114.114.114:{ServerPort}"
-        End If
-        For Each Peer As String In RawPeers
-            Arguments += $" -p=""{Peer}"""
-        Next
-        If Settings.Get(Of Integer)("LinkLatencyMode") = 1 Then Arguments += " --latency-first"
-        '启动进程
-        ProcessStart(Arguments)
-        Task.Progress = 0.15
-        '等待连接
-        Dim TimeoutCounter As Integer = 0
-        Do
-            Thread.Sleep(500) '为避免还没启动好先跑 CLI 了,先等半秒
-            '刷新信息
-            CheckCrash()
-            RefreshPeerLoader.WaitForExit(IsForceRestart:=True)
-            '查找目标节点
-            Dim Ping = GetPeerPing()
-            If Ping > 0 Then
-                Logger.Info($"已与目标建立连接,当前 Ping 为 {Ping:0.0}ms")
-                Telemetry("联机成功", "Server", IsServerSide, "NAT", NATType)
-                Exit Do '退出循环
-            End If
-            '更新进度
-            Dim LastProgress = Task.Progress
-            Dim PeerCount As Integer = If(Peers Is Nothing, -1, Peers.Where(Function(p) p.Ping > 0).Count)
-            Select Case PeerCount
-                Case -1 'CLI 无返回
-                    Task.Progress = (Task.Progress + 0.02).Clamp(0.15, 0.25)
-                Case 0 'CLI 有返回,但未连接到任何节点
-                    UpdateLoadingPage("正在连接到节点……", "连接节点")
-                    Task.Progress = (Task.Progress + 0.02).Clamp(If(IsServerSide, 0.5, 0.3), If(IsServerSide, 0.95, 0.5))
-                Case Else '已连接到节点,但未连接到房主
-                    UpdateLoadingPage("正在连接到房主……", "连接房主")
-                    Task.Progress = (Task.Progress + 0.02).Clamp(Math.Min(0.45 + PeerCount * 0.05, 0.65), 0.95)
-            End Select
-            '超时判定
-            If LastProgress <> Task.Progress Then
-                TimeoutCounter = 0
-            ElseIf TimeoutCounter < 30 * 2 Then
-                TimeoutCounter += 1
-            Else '进度停滞超过 30s
-                Select Case PeerCount
-                    Case -1 'CLI 无返回
-                        Panic("无法启动联机模块", "近期日志:" & vbCrLf & LogHistory.Join(vbCrLf))
-                    Case 0 'CLI 有返回,但未连接到任何节点
-                        Panic("无法连接到节点", $"请检查你的网络环境是否良好。")
-                    Case Else '已连接到节点,但未连接到房主
-                        Panic("无法连接到房主", $"可能的原因:{vbCrLf}- 你或者房主的网络环境不佳{vbCrLf}- 房主已关闭房间{vbCrLf}- 邀请码输错了")
-                End Select
-            End If
-        Loop Until Task.IsCanceled
-        If Task.IsCanceled Then Throw New OperationCanceledException
-        '等待连接稳定,最多 5s
-        If IsServerSide Then Return
-        UpdateLoadingPage("连接优化中……", "优化连接")
-        Task.Progress = 0.999
-        For i = 1 To 50
-            Dim Server = GetTargetPeer()
-            If Server IsNot Nothing AndAlso Not Server.Relay AndAlso Server.Ping < 100 Then Return '结束
-            If Task.IsCanceled Then Throw New OperationCanceledException
-            Thread.Sleep(100)
-        Next
-    End Sub
-    ''' <summary>
-    ''' 从在线配置和 API 获取需要连接的节点列表,并更新发现节点 ID。
-    ''' 根据发现节点 ID,会有以下行为变化:
-    '''   -1:作为房主,根据负载均衡选择一个发现节点。
-    '''   0:作为加入者,但邀请码未提供发现节点 ID,使用回退发现节点。
-    '''   >0:作为加入者,根据 ID 选择对应的发现节点;如果没有,使用回退发现节点。
-    ''' </summary>
-    Private Function GetTargetPeers() As List(Of String)
-        Dim FinalPeers As New List(Of String), FinalDiscoverID As Integer = -1
-        Dim FallbackDiscoverID = ServerConfig("Link")("DiscoverPeerId").ToObject(Of Integer)
-        Dim FallbackDiscoverAddress As String = ServerConfig("Link")("DiscoverPeer").ToString()
-        Try
-            '从 API 获取节点列表
-            Dim RawNodes As JObject
-            Dim IsFallbackRawList As Boolean = False
-            Try
-                RawNodes = NetRequestByClient("https://uptime.easytier.cn/api/nodes?page=1&per_page=1000", RequireJson:=True).DeserializeJson()
-            Catch exx As Exception
-                Logger.Warn(exx, "从源站获取节点列表失败,将使用 CDN 缓存")
-                RawNodes = NetRequestByClientRetry("https://easytier.meloong.com/?page=1&per_page=1000", RequireJson:=True).DeserializeJson()
-                IsFallbackRawList = True
-            End Try
-            '分析节点列表
-            Dim Nodes As New List(Of JObject) '负载会添加在 load 字段上
-            Dim BlackList As List(Of String) = ServerConfig("Link")("PeersBlackList").Select(Function(p) p.ToString).ToList()
-            For Each Node As JObject In RawNodes("data")("items")
-                '状态检查
-                Dim ID = Node("id").ToObject(Of Integer)
-                If Not Node("is_active").ToObject(Of Boolean) OrElse Not Node("is_approved").ToObject(Of Boolean) Then Continue For
-                If ID = DiscoverNodeID Then GoTo ForcedPass '若为指定的发现节点,忽略后续检查
-                If ID = FallbackDiscoverID Then Continue For '不主动选取回退发现节点
-                If BlackList.Contains(Node("address").ToString) Then Continue For 'ServerConfig 黑名单
-                If Node("usage_percentage").ToObject(Of Double) = 0 AndAlso RandomInteger(1, 100) <> 1 Then Continue For '或许节点有问题才导致是 0 负载,让它只有 1% 概率被选中
-                '标签检查
-                Dim Tags = Node("tags").Select(Function(t) t.ToString).ToList
-                If Not Tags.Contains("国内") OrElse Not Tags.Contains("MC中继") Then Continue For
-                '计算负载并加入列表
-ForcedPass:
-                Dim Load As Double = Node("usage_percentage").ToObject(Of Double) '负载
-                Load *= 110 - Node("health_percentage_24h").ToObject(Of Double) '可用率
-                Node("load") = Load
-                Nodes.Add(Node)
-            Next
-            '排序
-            If Not IsFallbackRawList Then
-                Nodes = Nodes.OrderBy(Function(n) n("load").ToObject(Of Double)).ToList() '按负载从低到高排序
-            Else
-                Nodes = Nodes.Shuffle().ToList() '回退到 CDN 缓存时,由于负载数据可能过期,直接随机选择
-            End If
-            '选取发现节点
-            Dim SelectedDiscoverNode As JObject = Nothing
-            If DiscoverNodeID = -1 Then '-1:作为房主,根据负载均衡选择一个发现节点
-                SelectedDiscoverNode = Nodes.FirstOrDefault(Function(n) Not n("allow_relay").ToObject(Of Boolean))
-            ElseIf DiscoverNodeID > 0 Then '>0:作为加入者,根据 ID 选择对应的发现节点;如果没有,使用回退发现节点
-                SelectedDiscoverNode = Nodes.FirstOrDefault(Function(n) n("id").ToObject(Of Integer) = DiscoverNodeID)
-                If SelectedDiscoverNode Is Nothing Then
-                    Logger.Warn($"未找到 ID {DiscoverNodeID} 的发现节点")
-                    Panic("房间已过期", "请让房主重新创建房间!")
-                    Throw New OperationCanceledException
-                End If
-            End If
-            If SelectedDiscoverNode Is Nothing Then '使用回退发现节点
-                SelectedDiscoverNode = New JObject From {{"address", FallbackDiscoverAddress}, {"id", FallbackDiscoverID}}
-                Logger.Warn("将使用回退发现节点")
-            End If
-            FinalPeers.Add(SelectedDiscoverNode("address").ToString())
-            FinalDiscoverID = SelectedDiscoverNode("id").ToObject(Of Integer)
-            Logger.Info($"发现节点:{SelectedDiscoverNode("address")} (ID: {FinalDiscoverID})")
-            '选取中继节点
-            If ModeDebug OrElse Not IsServerSide Then '房主只连接发现节点,不连接中继节点
-                Dim RelayCount As Integer = ServerConfig("Link")("RandomPeer").ToObject(Of Integer)
-                Dim RelayNodes = Nodes.Where(Function(n) n("allow_relay").ToObject(Of Boolean)).ToList()
-                If RelayNodes.Count < RelayCount Then Throw New Exception($"可用的中继节点数量不足,需要 {RelayCount} 个,实际 {RelayNodes.Count} 个")
-                FinalPeers.AddRange(RelayNodes.Take(RelayCount).Select(Function(n) n("address").ToString()))
-            End If
-        Catch ex As Exception
-            ex.ThrowIfCanceled()
-            Logger.Error(ex, "获取节点列表失败,联机质量可能受到影响", LogBehavior.Toast)
-            FinalPeers.AddRange(ServerConfig("Link")("Peers").Select(Function(p) p.ToString))
-            If FinalDiscoverID <= 0 Then
-                FinalPeers.Add(FallbackDiscoverAddress)
-                FinalDiscoverID = FallbackDiscoverID
-            End If
-        End Try
-        '版本 1 兼容
-        If CType(ServerConfig("Link"), JObject).ContainsKey("EnableForcedPeer") Then
-            FinalPeers.Add("tcp://mc1.easytier.cn:55558")
-            If FinalDiscoverID <= 0 Then FinalDiscoverID = 94
-        End If
-        '强制添加的节点
-        FinalPeers.AddRange(ServerConfig("Link")("MandatoryPeers").Select(Function(p) p.ToString))
-        '结束
-        DiscoverNodeID = FinalDiscoverID
-        Return FinalPeers.Distinct.ToList
-    End Function
-
-#End Region
-
-#Region "进程与日志"
-
-    ''' <summary>
-    ''' EasyTier Core 进程。
-    ''' </summary>
-    Private Shared ProcessCore As Process
-    Private Shared ProcessOutputHandle As AutoResetEvent, ProcessErrorHandle As AutoResetEvent
-
-    ''' <summary>
-    ''' 若程序正在运行,则结束程序进程。
-    ''' 即使失败也不会抛出异常。
-    ''' </summary>
     Public Shared Sub ProcessStop()
-        Try
-            '关闭未捕获的进程
-            For Each ProcessObject In Process.GetProcesses
-                If ProcessObject.Id = ProcessCore?.Id Then Continue For
-                If ProcessObject.ProcessName <> "联机模块" AndAlso ProcessObject.ProcessName <> "联机模块 CLI" Then Continue For
-                Try
-                    Logger.Info($"停止残留的联机模块,PID:{ProcessObject.Id},进程名:{ProcessObject.ProcessName}")
-                    ProcessObject.Kill()
-                    ProcessObject.Close()
-                Catch exx As Exception
-                    Logger.Warn(exx, $"结束进程失败({ProcessObject.ProcessName},PID {ProcessObject.Id})")
-                End Try
-            Next
-            '关闭由自身启动的进程
-            If ProcessCore IsNot Nothing AndAlso Not ProcessCore.HasExited Then
-                Logger.Info($"停止所启动的联机模块,PID:{ProcessCore.Id}")
-                ProcessCore.Kill()
-                ProcessCore.Close()
-                ProcessCore = Nothing
-            End If
-            '释放资源
-            ProcessOutputHandle?.Dispose()
-            ProcessErrorHandle?.Dispose()
-        Catch ex As Exception
-            Logger.Warn(ex, "停止联机模块失败")
-        End Try
-    End Sub
-    ''' <summary>
-    ''' 出现意外错误,给出错误信息并结束联机。
-    ''' </summary>
-    Private Sub Panic(Brief As String, Detail As String)
-        '常见原因分析
-        If Detail.Contains("failed to listen on ") Then
-            Detail = $"监听端口失败。{vbCrLf}请点击重试,如果还是出现此错误,可以重启电脑解决。{vbCrLf}{vbCrLf}{Detail}"
-        End If
-        '显示信息
-        If LinkState = LinkStates.Loading Then
-            FailReason = Brief
-            LinkLoader.Failed(New Exception(Detail))
-        Else
-            MyMsgBox(Detail, "联机出错:" & Brief, IsWarn:=True)
-            ChangeState(LinkStates.Waiting)
-            Telemetry("联机失败", "Exception", FilterUserName(Brief & ":" & Detail, "*"))
-        End If
-    End Sub
-
-    ''' <summary>
-    ''' 启动进程。
-    ''' </summary>
-    Private Sub ProcessStart(Arguments As String)
-        Dim Info = New ProcessStartInfo With {
-            .FileName = PathEasyTier & "联机模块.exe",
-            .Arguments = Arguments,
-            .UseShellExecute = False,
-            .CreateNoWindow = True,
-            .RedirectStandardError = True,
-            .RedirectStandardOutput = True,
-            .StandardOutputEncoding = Encoding.UTF8,
-            .StandardErrorEncoding = Encoding.UTF8
-        }
-        Logger.Info($"正在启动 EasyTier:{Arguments}")
-        ProcessCore = New Process With {.StartInfo = Info}
-        Dim LogLineHandler =
-        Sub(sender As Object, e As DataReceivedEventArgs, Handle As AutoResetEvent)
-            Try
-                If e.Data Is Nothing Then
-                    Handle.[Set]()
-                    Return
-                End If
-                ProcessLogLine(e.Data)
-            Catch unused As ObjectDisposedException
-            Catch ex As Exception
-                Logger.Warn(ex, "读取 EasyTier 信息失败")
-            End Try
-        End Sub
-        ProcessOutputHandle = New AutoResetEvent(False)
-        AddHandler ProcessCore.OutputDataReceived, Sub(sender, e) LogLineHandler(sender, e, ProcessOutputHandle)
-        ProcessErrorHandle = New AutoResetEvent(False)
-        AddHandler ProcessCore.ErrorDataReceived, Sub(sender, e) LogLineHandler(sender, e, ProcessErrorHandle)
-        ProcessCore.Start()
-        ProcessCore.BeginOutputReadLine()
-        ProcessCore.BeginErrorReadLine()
-    End Sub
-    Private Sub ProcessLogLine(Line As String)
-        '记录日志
-        Logger.Info(Line)
-        LogHistory.Enqueue(Line)
-        If LogHistory.Count >= 10 Then LogHistory.Dequeue()
-        '检查日志内容
-        If Line.ContainsIgnoreCase("new peer connection added") Then
-            Logger.Warn($"已建立连接:{If(Line.RegexSeek("(?<=remote_addr.+?"")[^""}]{3,}"), Line)}")
-            Update()
-        ElseIf Line.ContainsIgnoreCase("peer connection removed") Then
-            Logger.Warn($"已断开连接:{If(Line.RegexSeek("(?<=remote_addr.+?"")[^""}]{3,}"), Line)}")
-            Update()
-        End If
-    End Sub
-    Private LogHistory As New Queue(Of String)(10)
-
-#End Region
-
-#Region "CLI 节点信息获取"
-
-    Private Class Peer
-        ''' <summary>
-        ''' 该节点的类型。
-        ''' </summary>
-        Public ReadOnly Type As Types
-        Public Enum Types
-            ''' <summary>
-            ''' 客户端。
-            ''' </summary>
-            Client
-            ''' <summary>
-            ''' 服务器。
-            ''' </summary>
-            Server
-            ''' <summary>
-            ''' 公共节点。
-            ''' </summary>
-            Misc
-            ''' <summary>
-            ''' 自己,或者自己的重复(它可能返回多个自己)。
-            ''' </summary>
-            Self
-        End Enum
-        ''' <summary>
-        ''' 与该节点的延迟 (ms)。
-        ''' 若是自己则为 0。
-        ''' </summary>
-        Public ReadOnly Ping As Double
-        ''' <summary>
-        ''' 名称。
-        ''' </summary>
-        Public ReadOnly Name As String
-        ''' <summary>
-        ''' 是否通过中继连接。
-        ''' </summary>
-        Public ReadOnly Relay As Boolean
-        ''' <summary>
-        ''' NAT 类型。
-        ''' </summary>
-        Public ReadOnly NATType As NATTypes
-
-        ''' <summary>
-        ''' 从 CLI 给出的信息分析对应的数据。
-        ''' </summary>
-        Public Sub New(Info As JObject)
-            '类别
-            Dim PeerName = Info("hostname").ToString
-            If Info("cost").ToString = "Local" Then
-                Type = Types.Self
-            ElseIf PeerName.StartsWithF("Client") Then
-                Type = Types.Client
-            ElseIf Info("ipv4") = "10.114.114.114" Then '只允许这个 IP 作为房主,避免网络被多个房主搞炸的情况
-                Type = Types.Server
-            Else
-                Type = Types.Misc
-            End If
-            '基础信息
-            Double.TryParse(Info("lat_ms"), NumberStyles.Any, CultureInfo.InvariantCulture, Ping)
-            Name = PeerName
-            Relay = Info("cost").ToString.ContainsIgnoreCase("relay")
-            NATType = EnumUtils.FromString(Of NATTypes)(Info("nat_type"))
-        End Sub
-        Public Overrides Function ToString() As String
-            Return $"{Type} - {Name} - Ping {Ping:0.0}ms [中继? {Relay}] - NAT {NATType}"
-        End Function
-    End Class
-    Private Enum NATTypes
-        'https://github.com/EasyTier/EasyTier/blob/6bb2fd9a15ab2499bdeabdcc3a925e9bd9aebf50/easytier/src/proto/common.proto#L129
-        Unknown = 0
-        OpenInternet = 1
-        NoPAT = 2
-        FullCone = 3
-        Restricted = 4
-        PortRestricted = 5
-        Symmetric = 6
-        SymUdpFirewall = 7
-        SymmetricEasyInc = 8
-        SymmetricEasyDec = 9
-        ''' <summary>
-        ''' 尚未获取。
-        ''' </summary>
-        Pending = 10
-    End Enum
-
-    ''' <summary>
-    ''' 当前的节点列表,使用 RefreshPeerLoader 来刷新。
-    ''' 若尚未成功获取过则为 Nothing,但保证在加载完成后至少是一个列表。
-    ''' </summary>
-    Private Peers As List(Of Peer) = Nothing
-    ''' <summary>
-    ''' 自己的 NAT 类型。
-    ''' </summary>
-    Private NATType As NATTypes = NATTypes.Pending
-
-    ''' <summary>
-    ''' 调用 EasyTier CLI 获取已连接节点信息。
-    ''' 这会过滤掉尚未完成连接的其他节点以及自己。
-    ''' 若在加载完成后连续获取失败 3 次,则会强制断开连接。
-    ''' </summary>
-    Private RefreshPeerLoader As New LoaderTask(Of Integer, List(Of Peer))("EasyTier CLI", AddressOf RefreshPeer)
-    Private Sub RefreshPeer()
-        Try
-            Dim CliResult = "" 'StartProcessAndGetOutput(PathEasyTier & "联机模块 CLI.exe", $"-o json -p 127.0.0.1:{RPCPort} peer", 2000, Encoding:=Encoding.UTF8, PrintLog:=False)
-            '解析
-            If Not CliResult.Contains("lat_ms") Then Throw New Exception("CLI 调用失败:" & vbCrLf & CliResult)
-            If GetUuid() Mod If(ModeDebug, 23, 103) = 0 Then Logger.Info($"CLI 输出抽样:{vbCrLf}{CliResult}")
-            Dim NewPeers As New List(Of Peer)
-            For Each Line As JObject In CType(CliResult.DeserializeJson(), JArray)
-                Try
-                    Dim Peer = New Peer(Line)
-                    If Peer.Type = Peer.Types.Self Then
-                        NATType = Peer.NATType '记录自己的 NAT
-                    Else
-                        NewPeers.Add(Peer)
-                    End If
-                Catch exx As Exception
-                    Logger.Warn(exx, $"错误的信息({Line})")
-                End Try
-            Next
-            '完成
-            Peers = NewPeers
-            FailCount = 0
-        Catch ex As Exception
-            Logger.Warn(ex, "获取节点信息失败")
-            If LinkState = LinkStates.Finished Then
-                FailCount += 1
-                If FailCount >= 4 Then Panic("获取节点信息失败", ex.Message)
-            End If
-        End Try
-    End Sub
-    ''' <summary>
-    ''' CLI 调用连续失败的次数。
-    ''' </summary>
-    Private FailCount As Integer = 0
-
-    ''' <summary>
-    ''' 根据当前的节点列表获取 Ping 值。
-    ''' 服务端会返回所有节点中最低的 Ping,客户端会返回服务端的 Ping。
-    ''' 0 代表尚不可用。
-    ''' </summary>
-    Private Function GetPeerPing() As Double
-        Dim Peer = GetTargetPeer()
-        If Peer Is Nothing Then Return 0
-        Return Peer.Ping
-    End Function
-    ''' <summary>
-    ''' 服务端会返回所有节点中 Ping 大于 0 最低的那一个,客户端会返回服务端。
-    ''' 若没有则为 Nothing。
-    ''' </summary>
-    Private Function GetTargetPeer() As Peer
-        If Peers Is Nothing Then Return Nothing
-        Dim Targets As IEnumerable(Of Peer)
-        If IsServerSide Then
-            Targets = Peers.Where(Function(p) p.Ping > 0)
-        Else
-            Targets = Peers.Where(Function(p) p.Type = Peer.Types.Server AndAlso p.Ping > 0)
-        End If
-        If Not Targets.Any Then Return Nothing
-        '返回 Ping 且最低的那个
-        Dim MinPing = Targets.Min(Function(p) p.Ping)
-        Return Targets.First(Function(p) p.Ping = MinPing)
-    End Function
-
-    ''' <summary>
-    ''' 获取当前房间中的人数,包括自己。
-    ''' 0 代表不可用。
-    ''' </summary>
-    Private ReadOnly Property PeopleCount As Integer
-        Get
-            If Peers Is Nothing Then Return 0
-            Return Peers.Where(Function(p) p.Type <> Peer.Types.Misc).Count() + 1 '加上自己
-        End Get
-    End Property
-
-#End Region
-
-#Region "定时任务"
-
-    '启动
-    Private Sub StartTimerThread() Handles Me.Loaded
-        Static IsTimerStarted As Boolean = False
-        If IsTimerStarted Then Return
-        RunInNewThread(
-        Sub()
-            Dim Counter As Integer = 0
-            Do While True
-                Try
-                    Thread.Sleep(200)
-                    Counter += 200
-                    '每秒执行一次
-                    If Counter >= 1000 Then
-                        Counter = 0
-                        Update()
-                    End If
-                    '每 200ms 更新进度条
-                    If LinkState = LinkStates.Loading Then RunInUi(AddressOf UpdateProgressBar)
-                Catch ex As Exception
-                    Logger.Error(ex, "联机模块主时钟出错")
-                    Thread.Sleep(10000)
-                End Try
-            Loop
-        End Sub, "Link Timer")
-    End Sub
-
-    '每秒或进入页面时触发
-    Private Sub Update() Handles Me.Loaded
-        If LinkState <> LinkStates.Finished Then Return
-        '重新获取信息
-        SyncLock RefreshPeerLoader.LockState
-            If RefreshPeerLoader.State <> LoadState.Loading Then RefreshPeerLoader.Start(IsForceRestart:=True)
-        End SyncLock
-        '更新 UI
-        If FrmMain.PageCurrent = FormMain.PageType.Link Then
-            RunInUi(
-            Sub()
-                'Logo 旋转动画
-                AniStart(AaRotateTransform(ImgFinishLogo, 500, 5000), "Link Logo Rotation")
-                'Ping
-                Dim Ping As Double = GetPeerPing()
-                Dim Connecting As Boolean = Ping Mod 500 = 0 OrElse $"{Ping:0.0}" = "1.0" OrElse FailCount > 0
-                If Connecting Then
-                    LabFinishPing.Text = "连接优化中"
-                Else
-                    LabFinishPing.Text = If(Ping >= 10, $"{Ping:0} ms", $"{Ping:0.0} ms")
-                End If
-                '人数显示
-                LabFinishPlayer.Text = PeopleCount & "  人"
-                '------------------------
-                '  提示条
-                '------------------------
-                HintFinish.Visibility = Visibility.Collapsed
-                If IsServerSide Then Return '服务端没有需要显示的提示
-                If Connecting Then Return '连接稳定过程中不显示提示
-                '中继提示
-                Dim Server As Peer = GetTargetPeer()
-                If Server Is Nothing Then Return
-                If Server.Relay Then
-                    If NATType >= NATTypes.Symmetric AndAlso Server.NATType >= NATTypes.Symmetric Then
-                        HintFinish.Text = "你和房主的网络环境都不太好,"
-                    ElseIf NATType >= NATTypes.Symmetric Then
-                        HintFinish.Text = "你的网络环境不太好,"
-                    ElseIf Server.NATType >= NATTypes.Symmetric Then
-                        HintFinish.Text = "房主的网络环境不太好,"
-                    Else
-                        HintFinish.Text = "你或者房主的网络环境不太好,"
-                    End If
-                    If String.IsNullOrWhiteSpace(Settings.Get(Of String)("LinkCustomPeer")) Then
-                        HintFinish.Text &= "正使用社区节点进行中继。"
-                    Else
-                        HintFinish.Text &= "正通过自定义节点进行中继。"
-                    End If
-                    HintFinish.Visibility = Visibility.Visible
-                    HintFinish.Theme = MyHint.Themes.Yellow
-                    Return
-                End If
-                '环境比房主更好的提示
-                If NATType >= NATTypes.PortRestricted Then Return '自身的 NAT 为 2 或更好
-                If Server.NATType < NATTypes.PortRestricted Then Return '房主的 NAT 为 3 或更差
-                Dim OtherPlayers = Peers.Where(Function(p) p.Type = Peer.Types.Client).ToList()
-                If Not OtherPlayers.Any() Then Return '房间里还有其他玩家
-                If OtherPlayers.Any(Function(p) p.Relay) Then Return '自己可以与所有玩家打洞
-                If OtherPlayers.All(Function(p) p.NATType < NATTypes.PortRestricted) Then Return '任意其他玩家的 NAT 为 3 或更差
-                HintFinish.Visibility = Visibility.Visible
-                HintFinish.Theme = MyHint.Themes.Blue
-                HintFinish.Text = "你的网络环境比房主更好!如果你来当房主,其他玩家或许能更加流畅!"
-            End Sub)
-        End If
-        '检查核心状态
-        CheckCrash()
-        '检查节点状态
-        If Not Peers.Any Then
-            Panic("网络连接已断开", "请检查你的网络环境是否正常。")
-            Return
-        End If
-        If Not IsServerSide AndAlso Not Peers.Any(Function(p) p.Type = Peer.Types.Server) Then
-            MyMsgBox("房主已离开房间!", "联机结束")
-            ChangeState(LinkStates.Waiting)
-            Return
-        End If
-        '广播联机房间端口
-        If Not IsServerSide Then
-            Try
-                Static BroadcastSocket As New Socket(SocketType.Dgram, ProtocolType.Udp)
-                BroadcastSocket.SendTo(
-                    Encoding.UTF8.GetBytes($"[MOTD]PCL 联机房间[/MOTD][AD]{ClientPort}[/AD]"),
-                    SocketFlags.None,
-                    New IPEndPoint(IPAddress.Loopback, 4445))
-            Catch ex As Exception
-                Logger.Warn(ex, "广播联机房间端口失败")
-            End Try
-        End If
-    End Sub
-    ''' <summary>
-    ''' 检查联机模块是否崩溃。
-    ''' </summary>
-    Private Sub CheckCrash()
-        If ProcessCore IsNot Nothing AndAlso Not ProcessCore.HasExited Then Return
-        Panic("联机模块已崩溃", "近期日志:" & vbCrLf & LogHistory.Join(vbCrLf))
     End Sub
-
-#End Region
-
 End Class

+ 10 - 7
Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml

@@ -20,7 +20,7 @@
                     </Grid.ColumnDefinitions>
                     <local:MyListItem IsHitTestVisible="False" Title="龙腾猫跃" Margin="-5,0,15,0" Info="Plain Craft Launcher 的开发者!" Grid.Row="0" Grid.Column="1" />
                     <local:MyListItem IsHitTestVisible="False" Title="HerobrineXia" Margin="-5,0,15,0" Info="Plain Craft Launcher 的协力开发者!" Grid.Row="1" Grid.Column="1" />
-                    <local:MyListItem IsHitTestVisible="False" Title="Plain Craft Launcher 2" Margin="-5,0,15,0" Info="当前版本:%VERSION%(%VERSIONCODE%.%BRANCH%)" Grid.Row="2" Grid.Column="1" x:Name="ItemAboutPcl" />
+                    <local:MyListItem IsHitTestVisible="False" Title="PCL Communist" Margin="-5,0,15,0" Info="基于 Plain Craft Launcher 的第三方独立修改 · 当前版本:%VERSION%(%VERSIONCODE%.%BRANCH%)" Grid.Row="2" Grid.Column="1" x:Name="ItemAboutPcl" />
                     <Image Source="/Plain Craft Launcher 2;component/Images/Heads/LTCat.jpg" Grid.Row="0" Margin="3">
                         <Image.Clip>
                             <EllipseGeometry Center="17 17" RadiusX="17" RadiusY="17" />
@@ -38,7 +38,7 @@
                     </Image>
                     <local:MyButton Text="赞助 PCL !" Grid.Column="2" Grid.Row="0" Height="35" ColorType="Highlight"
                                     local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://meloong.com/afd/a/LTCat" />
-                    <local:MyButton Text="检查更新" Grid.Column="2" Grid.Row="2" Height="35" 
+                    <local:MyButton Text="已移除远程更新" IsEnabled="False" Grid.Column="2" Grid.Row="2" Height="35"
                                     x:Name="BtnAboutUpdate" />
                 </Grid>
             </local:MyCard>
@@ -532,10 +532,10 @@
             </local:MyCard>
             <local:MyCard Margin="0,0,0,15" Title="法律信息" IsSwapped="True" CanSwap="True">
                 <StackPanel Margin="25,39,23,20">
-                    <TextBlock Text="匿名数据收集" TextWrapping="Wrap" Margin="0,6,0,4" FontWeight="Bold" />
-                    <TextBlock Margin="0,0,0,2" LineHeight="17" Text="当下载失败、PCL 出错、游戏崩溃等事件时,PCL 会匿名上报信息,以通知龙猫 “哦吼,出错了”,让龙猫能更好地修 Bug……(PCL 不会上传或收集任何个人信息,包括上报的东西里面也没有!)&#xa;你可以在 设置 → 其他 → 启动器 中关闭上报,不过龙猫就不知道你遇到了什么 Bug 了……&#xa;详情:https://github.com/Meloong-Git/PCL/discussions/7036" TextWrapping="Wrap" />
-                    <TextBlock Text="隐私声明与个人信息保护政策" TextWrapping="Wrap" Margin="0,10,0,4" FontWeight="Bold" />
-                    <TextBlock Margin="0,0,0,2" LineHeight="17" Text="PCL 没有收集个人信息,所以这里其实啥都不用写……&#xa;留个这个其实只是为了让你知道 PCL 没有收集个人信息,诶嘿。" TextWrapping="Wrap" />
+                    <TextBlock Text="网络功能" TextWrapping="Wrap" Margin="0,6,0,4" FontWeight="Bold" />
+                    <TextBlock Margin="0,0,0,2" LineHeight="17" Text="本版本只保留资源下载和微软正版登录所需的联网功能,已移除匿名上报、启动器远程更新和配置下发。" TextWrapping="Wrap" />
+                    <TextBlock Text="隐私说明" TextWrapping="Wrap" Margin="0,10,0,4" FontWeight="Bold" />
+                    <TextBlock Margin="0,0,0,2" LineHeight="17" Text="微软正版登录与账号刷新会连接微软、Xbox 和 Minecraft 官方服务。下载操作会连接所选下载源。" TextWrapping="Wrap" />
                     <TextBlock Text="其他信息" TextWrapping="Wrap" Margin="0,10,0,4" FontWeight="Bold" />
                     <TextBlock Margin="0,0,0,2" LineHeight="17" Text="Copyright © 成都瓜皮龙科技有限公司&#13;计算机软件著作权登记号:2020SR0875133&#13;违法违规行为举报投诉邮箱:[email protected]&#13;非 MINECRAFT 官方产品。未经 MOJANG 或 MICROSOFT 批准,也不与 MOJANG 或 MICROSOFT 关联。" TextWrapping="Wrap" />
                     <StackPanel Orientation="Horizontal" Height="35" HorizontalAlignment="Left" Margin="0,12,0,0">
@@ -543,10 +543,13 @@
                                         local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://shimo.im/docs/rGrd8pY8xWkt6ryW#anchor-RnU9" />
                         <local:MyButton Text="PCL 分发有限许可" Width="170" Padding="13,0" Margin="0,0,20,0" 
                                         local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://shimo.im/docs/rGrd8pY8xWkt6ryW#anchor-X0bo" />
-                        <local:MyButton Text="开源代码" Width="140" Padding="13,0" local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://github.com/Hex-Dragon/PCL2" />
+                        <local:MyButton Text="源代码" Width="140" Padding="13,0" local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://github.com/Angelhellwolf/PCL-Communist" />
                     </StackPanel>
                 </StackPanel>
             </local:MyCard>
+            <local:MyCard Margin="0,0,0,15" Title="PCL 分发许可与存储库指南" IsSwapped="True" CanSwap="True">
+                <TextBlock x:Name="TextBundledLicence" Margin="25,39,23,20" LineHeight="19" TextWrapping="Wrap" />
+            </local:MyCard>
             <local:MyCard Margin="0,0,0,15" Title="许可与版权声明" IsSwapped="True" CanSwap="True">
                 <Grid Margin="25,42,23,4">
                     <Grid.ColumnDefinitions>

+ 5 - 0
Plain Craft Launcher 2/Pages/PageOther/PageOtherAbout.xaml.vb

@@ -11,6 +11,11 @@ Public Class PageOtherAbout
         IsLoaded = True
 
         ItemAboutPcl.Info = ItemAboutPcl.Info.Replace("%VERSION%", VersionDisplay).Replace("%VERSIONCODE%", VersionCode).Replace("%BRANCH%", CInt(BuildType))
+        Using LicenceStream = Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("PCL.Licence.txt")
+            Using Reader As New StreamReader(LicenceStream, Encoding.UTF8)
+                TextBundledLicence.Text = Reader.ReadToEnd()
+            End Using
+        End Using
         If BuildType = BuildTypes.Debug Then
             BtnDonateDonate.Visibility = Visibility.Collapsed
             BtnDonateOutput.Visibility = Visibility.Visible

+ 7 - 16
Plain Craft Launcher 2/Pages/PageSetup/PageSetupSystem.xaml

@@ -121,19 +121,10 @@
                         <RowDefinition Height="Auto" />
                         <RowDefinition Height="Auto" />
                     </Grid.RowDefinitions>
-                    <TextBlock VerticalAlignment="Center" HorizontalAlignment="Left" Text="启动器更新" Margin="0,0,25,0" />
-                    <local:MyComboBox x:Name="ComboSystemUpdate" local:SettingService.Key="SystemSystemUpdate" Grid.Column="1">
-                        <local:MyComboBoxItem Content="在有新版本时自动下载" x:Name="ItemSystemUpdateDownload" />
-                        <local:MyComboBoxItem Content="在有新版本时显示提示" />
-                        <local:MyComboBoxItem Content="仅在有重大漏洞更新时显示提示" />
-                        <local:MyComboBoxItem Content="关闭更新提示" />
-                    </local:MyComboBox>
-                    <TextBlock VerticalAlignment="Center" HorizontalAlignment="Left" Text="启动器公告" Margin="0,0,25,0" Grid.Row="2" />
-                    <local:MyComboBox x:Name="ComboSystemActivity" local:SettingService.Key="SystemSystemActivity" Grid.Column="1" Grid.Row="2">
-                        <local:MyComboBoxItem Content="显示所有公告" ToolTip="显示包括启动器功能调查、活动等在内的全部公告" />
-                        <local:MyComboBoxItem Content="仅在有重要通知时显示公告" />
-                        <local:MyComboBoxItem Content="关闭所有公告" />
-                    </local:MyComboBox>
+                    <TextBlock VerticalAlignment="Center" Text="更新与公告" Margin="0,0,25,0" />
+                    <TextBlock Grid.Column="1" VerticalAlignment="Center" Text="已移除远程更新与配置下发" Foreground="{DynamicResource ColorBrushGray3}" />
+                    <TextBlock VerticalAlignment="Center" Grid.Row="2" Text="联网功能" Margin="0,0,25,0" />
+                    <TextBlock Grid.Row="2" Grid.Column="1" VerticalAlignment="Center" Text="下载与微软正版登录" Foreground="{DynamicResource ColorBrushGray3}" />
                     <TextBlock Grid.Row="4" VerticalAlignment="Center" HorizontalAlignment="Left" Text="缓存文件夹" Margin="0,0,25,0" />
                     <local:MyTextBox x:Name="TextSystemCache" Grid.Row="4" Grid.ColumnSpan="2" local:SettingService.Key="SystemSystemCache" Grid.Column="1" ToolTip="PCL 的下载、皮肤等缓存文件的存储位置。&#xa;不推荐路径中带有空格。&#xa;留空即为默认值,重启 PCL 后生效。" HintText="默认">
                         <local:MyTextBox.ValidateRules>
@@ -142,7 +133,7 @@
                         </local:MyTextBox.ValidateRules>
                     </local:MyTextBox>
                     <local:MyCheckBox Margin="0,5,0,4" Text="匿名数据收集" Grid.Row="6" Height="22" Grid.ColumnSpan="2"
-                        x:Name="CheckSystemTelemetry" local:SettingService.Key="SystemSystemTelemetry" 
+                        x:Name="CheckSystemTelemetry" Visibility="Collapsed" local:SettingService.Key="SystemSystemTelemetry"
                         ToolTip="当下载失败、PCL 出错、游戏崩溃等事件时,PCL 会匿名上报情况,以通知龙猫 “哦吼,出错了”,让龙猫能更好地修 Bug……&#xa;你可以关闭上报,不过龙猫就不知道你遇到了什么 Bug 了……&#13;PCL 不会上传或收集任何个人信息,包括上报的东西里面也没有!&#xa;详情:https://github.com/Meloong-Git/PCL/discussions/7036" />
                     <Grid Height="35" Grid.Row="7" Grid.ColumnSpan="5" Margin="0,12,0,0">
                         <Grid.ColumnDefinitions>
@@ -150,7 +141,7 @@
                             <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
                             <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
                         </Grid.ColumnDefinitions>
-                        <local:MyButton Grid.Column="0" x:Name="BtnSystemUpdate" MinWidth="140" Text="检查更新" Padding="13,0" Margin="0,0,20,0" />
+                        <local:MyButton Grid.Column="0" x:Name="BtnSystemUpdate" Visibility="Collapsed" MinWidth="140" Text="检查更新" Padding="13,0" Margin="0,0,20,0" />
                         <local:MyButton Grid.Column="1" x:Name="BtnSystemSettingExp" MinWidth="140" Text="导出设置" Padding="13,0" Margin="0,0,20,0" />
                         <local:MyButton Grid.Column="2" x:Name="BtnSystemSettingImp" MinWidth="140" Text="导入设置" Padding="13,0" Margin="0,0,20,0" />
                     </Grid>
@@ -205,4 +196,4 @@
             </local:MyCard>
         </StackPanel>
     </local:MyScrollViewer>
-</local:MyPageRight>
+</local:MyPageRight>

+ 3 - 31
Plain Craft Launcher 2/Pages/PageSetup/PageSetupSystem.xaml.vb

@@ -10,7 +10,6 @@ Public Class PageSetupSystem
             PanDonate.Visibility = Visibility.Collapsed
         Else
             PanDonate.Visibility = Visibility.Visible
-            ItemSystemUpdateDownload.Content = "在有新版本时自动下载(更新快照版前需要先输入土豆码)"
         End If
 
         '非重复加载部分
@@ -79,26 +78,8 @@ Public Class PageSetupSystem
     End Sub
 
     '自动更新
-    Private Sub ComboSystemActivity_SelectionChanged(sender As Object, e As SelectionChangedEventArgs) Handles ComboSystemActivity.SelectionChanged
-        If AniControlEnabled <> 0 Then Return
-        If ComboSystemActivity.SelectedIndex <> 2 Then Return
-        If MyMsgBox("若选择此项,即使在将来出现严重问题时,你也无法获取相关通知。" & vbCrLf &
-                    "例如,如果发现某个版本游戏存在严重 Bug,你可能就会因为无法得到通知而导致无法预知的后果。" & vbCrLf & vbCrLf &
-                    "一般选择 仅在有重要通知时显示公告 就可以让你尽量不受打扰了。" & vbCrLf &
-                    "除非你在制作服务器整合包,或时常手动更新启动器,否则极度不推荐选择此项!", "警告", "我知道我在做什么", "取消", IsWarn:=True) = 2 Then
-            If e.RemovedItems.Count > 0 Then ComboSystemActivity.SelectedItem = e.RemovedItems(0)
-        End If
-    End Sub
-    Private Sub ComboSystemUpdate_SelectionChanged(sender As Object, e As SelectionChangedEventArgs) Handles ComboSystemUpdate.SelectionChanged
-        If AniControlEnabled <> 0 Then Return
-        If ComboSystemUpdate.SelectedIndex <> 3 Then Return
-        If MyMsgBox("若选择此项,即使在启动器将来出现严重问题时,你也无法获取更新并获得修复。" & vbCrLf &
-                    "例如,如果官方修改了登录方式,从而导致现有启动器无法登录,你可能就会因为无法更新而无法开始游戏。" & vbCrLf & vbCrLf &
-                    "一般选择 仅在有重大漏洞更新时显示提示 就可以让你尽量不受打扰了。" & vbCrLf &
-                    "除非你在制作服务器整合包,或时常手动更新启动器,否则极度不推荐选择此项!", "警告", "我知道我在做什么", "取消", IsWarn:=True) = 2 Then
-            If e.RemovedItems.Count > 0 Then ComboSystemUpdate.SelectedItem = e.RemovedItems(0)
-        End If
-    End Sub
+
+
     Private Sub BtnSystemUpdate_Click(sender As Object, e As EventArgs) Handles BtnSystemUpdate.Click
         UpdateCheckByButton()
     End Sub
@@ -107,16 +88,7 @@ Public Class PageSetupSystem
     ''' 若返回 Nothing,则代表无更新缓存文件或出错。
     ''' </summary>
     Public Shared Function IsLauncherNewest() As Boolean?
-        Try
-            '确认服务器公告是否正常
-            Dim ServerContent As String = If(FileUtils.TryReadAsString(PathTemp & "Cache\Notice.cfg"), "")
-            If ServerContent.Split("|").Count < 3 Then Return Nothing
-            '确认是否为最新
-            Return ServerContent.Split("|")(If(BuildType = BuildTypes.Release, 2, 1)) <= VersionCode
-        Catch ex As Exception
-            Logger.Error(ex, "确认启动器更新失败")
-            Return Nothing
-        End Try
+        Return Nothing
     End Function
 
 #Region "导出 / 导入设置"

+ 25 - 49
Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml

@@ -38,7 +38,7 @@
                             <local:MySlider local:SettingService.Key="UiLauncherLight" 
                                             Grid.Row="2" Grid.Column="4" x:Name="SliderLauncherLight" Visibility="Collapsed" MaxValue="40" ValueByKey="5" Margin="0,10,0,0" />
                         </Grid>
-                        <Grid x:Name="PanLauncherTheme" Height="90" Margin="0,0,0,7">
+                        <Grid x:Name="PanLauncherTheme" Height="120" Margin="0,0,0,7">
                             <Grid.ColumnDefinitions>
                                 <ColumnDefinition Width="Auto" SharedSizeGroup="Name1" />
                                 <ColumnDefinition />
@@ -51,6 +51,7 @@
                                 <RowDefinition />
                                 <RowDefinition />
                                 <RowDefinition />
+                                <RowDefinition Height="30" />
                             </Grid.RowDefinitions>
                             <TextBlock VerticalAlignment="Center" HorizontalAlignment="Left" Text="主题" Margin="0,0,20,0" />
                             <TextBlock VerticalAlignment="Center" HorizontalAlignment="Left" Text="隐藏主题" Margin="0,0,20,0" Grid.Row="1" />
@@ -65,40 +66,38 @@
                             <local:MyRadioBox local:SettingService.Key="UiLauncherTheme" local:SettingService.Value="4" 
                                               Grid.Column="5" Grid.Row="0" Text="橡木棕" x:Name="RadioLauncherTheme4" />
                             <local:MyRadioBox local:SettingService.Key="UiLauncherTheme" local:SettingService.Value="5" 
-                                              Grid.Column="1" Grid.Row="1" Text="玄素黑" x:Name="RadioLauncherTheme5" Opacity="0" ToolTip="灰色是个谎言" />
-                            <local:MyRadioBox Grid.Column="1" Grid.Row="1" Text="玄素黑" x:Name="RadioLauncherTheme5Gray" IsEnabled="False" Opacity="1" />
-                            <TextBlock Grid.Column="1" Grid.Row="1" x:Name="LabLauncherTheme5Unlock" Background="{StaticResource ColorBrushSemiTransparent}" ToolTip="灰色是个谎言" />
+                                              Grid.Column="1" Grid.Row="1" Text="玄素黑" x:Name="RadioLauncherTheme5" Opacity="1" ToolTip="所有主题均已开放" />
+                            <local:MyRadioBox Grid.Column="1" Grid.Row="1" Text="玄素黑" x:Name="RadioLauncherTheme5Gray" Visibility="Collapsed" IsEnabled="False" Opacity="1" />
+                            <TextBlock Grid.Column="1" Grid.Row="1" x:Name="LabLauncherTheme5Unlock" Visibility="Collapsed" Background="{StaticResource ColorBrushSemiTransparent}" ToolTip="所有主题均已开放" />
                             <local:MyRadioBox local:SettingService.Key="UiLauncherTheme" local:SettingService.Value="12" 
-                                              Grid.Column="2" Grid.Row="1" Text="滑稽彩" x:Name="RadioLauncherTheme12" IsEnabled="False" ToolTip="滑稽树上滑稽果,滑稽树下你和我,滑稽之日搞事情,欢乐多又多" ToolTipService.ShowOnDisabled="True" />
+                                              Grid.Column="2" Grid.Row="1" Text="滑稽彩" x:Name="RadioLauncherTheme12" ToolTip="所有主题均已开放" ToolTipService.ShowOnDisabled="True" />
                             <local:MyRadioBox local:SettingService.Key="UiLauncherTheme" local:SettingService.Value="6" 
-                                              Grid.Column="3" Grid.Row="1" Text="铁杆粉" x:Name="RadioLauncherTheme6" IsEnabled="False" ToolTip="99 次重逢的喜悦" ToolTipService.ShowOnDisabled="True" />
+                                              Grid.Column="3" Grid.Row="1" Text="铁杆粉" x:Name="RadioLauncherTheme6" ToolTip="所有主题均已开放" ToolTipService.ShowOnDisabled="True" />
                             <local:MyRadioBox local:SettingService.Key="UiLauncherTheme" local:SettingService.Value="7" 
-                                              Grid.Column="4" Grid.Row="1" Text="神秘紫" x:Name="RadioLauncherTheme7" IsEnabled="False" ToolTip="072 101 108 112 032 068 101 118 032 048 046 052 046 052" ToolTipService.ShowOnDisabled="True" />
+                                              Grid.Column="4" Grid.Row="1" Text="神秘紫" x:Name="RadioLauncherTheme7" ToolTip="所有主题均已开放" ToolTipService.ShowOnDisabled="True" />
                             <local:MyRadioBox local:SettingService.Key="UiLauncherTheme" local:SettingService.Value="13" 
-                                              Grid.Column="5" Grid.Row="1" Text="欧皇彩" x:Name="RadioLauncherTheme13" IsEnabled="False" ToolTip="这就是传说中的欧皇了吧" ToolTipService.ShowOnDisabled="True" />
+                                              Grid.Column="5" Grid.Row="1" Text="欧皇彩" x:Name="RadioLauncherTheme13" ToolTip="所有主题均已开放" ToolTipService.ShowOnDisabled="True" />
                             <local:MyRadioBox local:SettingService.Key="UiLauncherTheme" local:SettingService.Value="8" 
-                                              Grid.Column="1" Grid.Row="2" Text="秋仪金" x:Name="RadioLauncherTheme8" IsEnabled="False" />
-                            <TextBlock Grid.Column="1" Grid.Row="2" x:Name="LabLauncherTheme8Copy" Background="{StaticResource ColorBrushSemiTransparent}" />
+                                              Grid.Column="1" Grid.Row="2" Text="秋仪金" x:Name="RadioLauncherTheme8" />
+                            <TextBlock Grid.Column="1" Grid.Row="2" x:Name="LabLauncherTheme8Copy" Visibility="Collapsed" Background="{StaticResource ColorBrushSemiTransparent}" />
                             <local:MyRadioBox local:SettingService.Key="UiLauncherTheme" local:SettingService.Value="9" 
-                                              Grid.Column="2" Grid.Row="2" Text="活跃橙" x:Name="RadioLauncherTheme9" IsEnabled="False" />
-                            <TextBlock Grid.Column="2" Grid.Row="2" x:Name="LabLauncherTheme9Copy" Background="{StaticResource ColorBrushSemiTransparent}" />
+                                              Grid.Column="2" Grid.Row="2" Text="活跃橙" x:Name="RadioLauncherTheme9" />
+                            <TextBlock Grid.Column="2" Grid.Row="2" x:Name="LabLauncherTheme9Copy" Visibility="Collapsed" Background="{StaticResource ColorBrushSemiTransparent}" />
                             <local:MyRadioBox local:SettingService.Key="UiLauncherTheme" local:SettingService.Value="10" 
-                                              Grid.Column="3" Grid.Row="2" Text="跳票红" x:Name="RadioLauncherTheme10" IsEnabled="False" ToolTip="进行一次正版登录,支持正版游戏!" ToolTipService.ShowOnDisabled="True" />
+                                              Grid.Column="3" Grid.Row="2" Text="跳票红" x:Name="RadioLauncherTheme10" ToolTip="所有主题均已开放" ToolTipService.ShowOnDisabled="True" />
                             <local:MyRadioBox local:SettingService.Key="UiLauncherTheme" local:SettingService.Value="11" 
-                                              Grid.Column="4" Grid.Row="2" Text="极客蓝" x:Name="RadioLauncherTheme11" IsEnabled="False" ToolTip="右键打开解密游戏入口" />
-                            <TextBlock Grid.Column="4" Grid.Row="2" x:Name="LabLauncherTheme11Click" Background="{StaticResource ColorBrushSemiTransparent}" />
+                                              Grid.Column="4" Grid.Row="2" Text="极客蓝" x:Name="RadioLauncherTheme11" ToolTip="所有主题均已开放" />
+                            <TextBlock Grid.Column="4" Grid.Row="2" x:Name="LabLauncherTheme11Click" Visibility="Collapsed" Background="{StaticResource ColorBrushSemiTransparent}" />
                             <local:MyRadioBox local:SettingService.Key="UiLauncherTheme" local:SettingService.Value="14" 
-                                              Grid.Column="5" Grid.Row="2" Text="自定义" x:Name="RadioLauncherTheme14" IsEnabled="False" ToolTip="需要解锁五个隐藏主题" ToolTipService.ShowOnDisabled="True" />
+                                              Grid.Column="5" Grid.Row="2" Text="自定义" x:Name="RadioLauncherTheme14" ToolTip="所有主题均已开放" ToolTipService.ShowOnDisabled="True" />
+                            <TextBlock VerticalAlignment="Center" Text="成就主题" Margin="0,0,20,0" Grid.Row="2" />
+                            <TextBlock VerticalAlignment="Center" Text="特别主题" Margin="0,0,20,0" Grid.Row="3" />
+                            <local:MyRadioBox local:SettingService.Key="UiLauncherTheme" local:SettingService.Value="15" Grid.Column="1" Grid.ColumnSpan="2" Grid.Row="3" Text="苏维埃红" x:Name="RadioLauncherTheme15" ToolTip="默认主题 · 赤红与鎏金" />
+                            <local:MyRadioBox local:SettingService.Key="UiLauncherTheme" local:SettingService.Value="42" Grid.Column="3" Grid.Row="3" Text="纪念彩" x:Name="RadioLauncherTheme42" ToolTip="特别主题已开放" />
                         </Grid>
-                        <local:MyCheckBox local:SettingService.Key="UiLauncherLogo" Text="打开启动器时显示 PCL 图标" Margin="-1,0,0,1" Height="22" />
+                        <local:MyCheckBox local:SettingService.Key="UiLauncherLogo" Text="打开启动器时显示镰刀锤子图标" Margin="-1,0,0,1" Height="22" />
                     </StackPanel>
-                    <Border Background="#DDDDDDDD" Margin="-8,23,-5,28" CornerRadius="4" Name="PanLauncherHide" Visibility="Collapsed">
-                        <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
-                            <TextBlock Text="如果可以小小赞助一下 PCL,就能解锁主题颜色更换功能啦……&#xa;多亏了大家的支持,PCL 才能在免费、无任何广告的情况下维持数年的更新(磕头)!" FontSize="13" Margin="0,0,0,0" />
-                            <local:MyButton Height="32" MinWidth="140" Text="支持 PCL 开发!" Padding="13,0" HorizontalAlignment="Center" Margin="0,9,0,0" ColorType="Highlight"
-                                            local:CustomEventService.EventType="打开网页" local:CustomEventService.EventData="https://meloong.com/afd/a/LTCat" />
-                        </StackPanel>
-                    </Border>
+                    <Border Background="#DDDDDDDD" Margin="-8,23,-5,28" CornerRadius="4" Name="PanLauncherHide" Visibility="Collapsed"></Border>
                 </Grid>
             </local:MyCard>
             <local:MyCard x:Name="CardBackground" Margin="0,0,0,15" Title="背景图片">
@@ -228,9 +227,9 @@
                             <ColumnDefinition Width="0.2*" />
                         </Grid.ColumnDefinitions>
                         <local:MyRadioBox Text="空白" x:Name="RadioCustomType0" Height="22" local:SettingService.Key="UiCustomType" local:SettingService.Value="0" Grid.Column="0" />
-                        <local:MyRadioBox Text="预设" x:Name="RadioCustomType3" Height="22" local:SettingService.Key="UiCustomType" local:SettingService.Value="3" Grid.Column="1" />
+                        <local:MyRadioBox Text="本地预设" x:Name="RadioCustomType3" Height="22" local:SettingService.Key="UiCustomType" local:SettingService.Value="3" Grid.Column="1" />
                         <local:MyRadioBox Text="读取本地文件" x:Name="RadioCustomType1" Height="22" local:SettingService.Key="UiCustomType" local:SettingService.Value="1" Grid.Column="2" />
-                        <local:MyRadioBox Text="联网更新" x:Name="RadioCustomType2" Height="22" local:SettingService.Key="UiCustomType" local:SettingService.Value="2" Grid.Column="3" />
+                        <local:MyRadioBox Text="联网主页已移除" IsEnabled="False" x:Name="RadioCustomType2" Height="22" local:SettingService.Key="UiCustomType" local:SettingService.Value="2" Grid.Column="3" />
                     </Grid>
                     <local:MyHint Margin="0,15,0,-4" Text="内容见 ModSetup" Theme="Blue" x:Name="HintCustom" />
                     <local:MyHint Margin="0,15,0,6" Text="请谨慎使用陌生人提供的主页,恶意代码可能会造成损害!" Theme="Yellow" x:Name="HintCustomWarn" 
@@ -268,29 +267,6 @@
                         <local:MyComboBox local:SettingService.Key="UiCustomPreset" Grid.ColumnSpan="2" Grid.Column="1">
                             <local:MyComboBoxItem local:SettingService.Value="0" Content="你知道吗?" />
                             <local:MyComboBoxItem local:SettingService.Value="1" Content="回声洞" />
-                            <local:MyComboBoxItem local:SettingService.Value="2" Content="Minecraft 新闻(作者:最亮的信标)" />
-                            <!--<local:MyComboBoxItem local:SettingService.Value="3" Content="简单主页(作者:MFn233)" />-->
-                            <local:MyComboBoxItem local:SettingService.Value="4" Content="每日整合包推荐(作者:wkea)" />
-                            <local:MyComboBoxItem local:SettingService.Value="5" Content="Minecraft 皮肤推荐(作者:wkea)" />
-                            <local:MyComboBoxItem local:SettingService.Value="6" Content="OpenBMCLAPI 仪表盘 Lite(作者:Silverteal、Mxmilu666)" />
-                            <!--<local:MyComboBoxItem local:SettingService.Value="7" Content="PCL 主页市场(作者:凌云)" />-->
-                            <!--<local:MyComboBoxItem local:SettingService.Value="8" Content="PCL 新闻速报(作者:Joker2184)" />-->
-                            <local:MyComboBoxItem local:SettingService.Value="9" Content="PCL 新功能说明书(作者:WForst-Breeze)" />
-                            <!--<local:MyComboBoxItem local:SettingService.Value="10" Content="OpenMCIM 仪表盘(作者:SALTWOOD)" />-->
-                            <local:MyComboBoxItem local:SettingService.Value="11" Content="杂志主页(作者:CreeperIsASpy)" />
-                            <local:MyComboBoxItem local:SettingService.Value="12" Content="PCL GitHub 仪表盘(作者:Deep-Dark-Forest)" />
-                            <local:MyComboBoxItem local:SettingService.Value="13" Content="Minecraft 更新摘要(作者:pynickle)" />
-                            <local:MyComboBoxItem local:SettingService.Value="14" Content="今日新闻热点(作者:EYicheng)" />
-                            <local:MyComboBoxItem local:SettingService.Value="15" Content="Minecraft 芝士站(作者:awaEric233)" />
-                            <local:MyComboBoxItem local:SettingService.Value="16" Content="整合包推荐引擎(作者:CHMYGZ)" />
-                            <local:MyComboBoxItem local:SettingService.Value="17" Content="Bangumi 番剧主页(作者:wzyaeu)" />
-                            <local:MyComboBoxItem local:SettingService.Value="18" Content="Bilibili 热门(作者:wzyaeu)" />
-                            <local:MyComboBoxItem local:SettingService.Value="19" Content="Music 云音乐热门(作者:wzyaeu)" />
-                            <local:MyComboBoxItem local:SettingService.Value="20" Content="模组推荐(作者:llsgllsg)" />
-                            <local:MyComboBoxItem local:SettingService.Value="21" Content="每日游戏推荐(作者:llsgllsg)" />
-                            <local:MyComboBoxItem local:SettingService.Value="22" Content="Wallpaper 每日壁纸(作者:wzyaeu)" />
-                            <local:MyComboBoxItem local:SettingService.Value="23" Content="Weather 天气预报(作者:wzyaeu)" />
-                            <local:MyComboBoxItem local:SettingService.Value="24" Content="新一代狗屁王大杂烩主页(作者:llsgllsg)" />
                         </local:MyComboBox>
                     </Grid>
                 </StackPanel>

+ 4 - 42
Plain Craft Launcher 2/Pages/PageSetup/PageSetupUI.xaml.vb

@@ -28,14 +28,7 @@ Public Class PageSetupUI
 
         SliderLoad()
 
-        If BuildType = BuildTypes.Release Then PanLauncherHide.Visibility = Visibility.Visible
-
-        '设置解锁
-        If Not RadioLauncherTheme8.IsEnabled Then LabLauncherTheme8Copy.ToolTip = $"累积赞助达到 ¥23.33 后,在爱发电私信发送【土豆 {Identify}】以解锁。" & vbCrLf & "右键打开赞助页面,如果觉得 PCL 做得还不错就支持一下吧 =w=!"
-        RadioLauncherTheme8.ToolTip = $"累积赞助达到 ¥23.33 后,在爱发电私信发送【土豆 {Identify}】以解锁"
-        If Not RadioLauncherTheme9.IsEnabled Then LabLauncherTheme9Copy.ToolTip = "· 反馈一个 Bug,在标记为 [完成] 后回复识别码要求解锁(右键打开反馈页面)" & vbCrLf & "· 提交一个 Pull Request 或主页预设,在标记为 [完成] 后回复识别码要求解锁"
-        RadioLauncherTheme9.ToolTip = "· 反馈一个 Bug,在标记为 [完成] 后回复识别码要求解锁" & vbCrLf & "· 提交一个 Pull Request 或主页预设,在标记为 [完成] 后回复识别码要求解锁"
-        '极客蓝的处理在 ThemeCheck 中
+        PanLauncherHide.Visibility = Visibility.Collapsed
 
     End Sub
     Public Sub Refresh()
@@ -307,6 +300,9 @@ Refresh:
                  "直接将主页文件拖进 PCL 窗口也可以快捷加载。", "主页自定义教程")
     End Sub
     Public Shared Sub OnMainPageTypeChanged()
+        If Settings.Get(Of Integer)("UiCustomType") = 2 Then Settings.Set("UiCustomType", 0)
+        If Settings.Get(Of Integer)("UiCustomPreset") > 1 Then Settings.Set("UiCustomPreset", 0)
+
         If FrmSetupUI Is Nothing Then Return
         Select Case CInt(Settings.Get(Of Integer)("UiCustomType"))
             Case 0 '无
@@ -350,40 +346,6 @@ Refresh:
         End If
     End Sub
 
-    '主题
-    Private Sub LabLauncherTheme5Unlock_MouseLeftButtonUp(sender As Object, e As MouseButtonEventArgs) Handles LabLauncherTheme5Unlock.MouseLeftButtonUp
-        RadioLauncherTheme5Gray.Opacity -= 0.7
-        RadioLauncherTheme5.Opacity += 0.7
-        AniStart({
-            AaOpacity(RadioLauncherTheme5Gray, 1, 1000 * AniSpeed, 500 * AniSpeed, New AniEaseInFluent),
-            AaOpacity(RadioLauncherTheme5, -1, 1000 * AniSpeed, 500 * AniSpeed, New AniEaseInFluent)
-        }, "ThemeUnlock")
-        If RadioLauncherTheme5Gray.Opacity < 0.02 Then
-            ThemeUnlock(5, UnlockHint:="隐藏主题 玄素黑 已解锁!")
-            AniStop("ThemeUnlock")
-            RadioLauncherTheme5.Checked = True
-        End If
-    End Sub
-    Private Sub LabLauncherTheme11Click_MouseLeftButtonUp() Handles LabLauncherTheme11Click.MouseLeftButtonUp, RadioLauncherTheme11.MouseRightButtonUp
-        If LabLauncherTheme11Click.Visibility = Visibility.Collapsed OrElse If(LabLauncherTheme11Click.ToolTip, "").ToString.Contains("点击") Then
-            If MyMsgBox(
-                "1. 不爬取或攻击相关服务或网站,不盗取相关账号,没有谜题可以或需要以此来解决。" & vbCrLf &
-                "2. 不得篡改或损毁相关公开信息,请尽量让它们保持原状。" & vbCrLf &
-                "3. 在你感到迷茫的时候,看看回声洞可能会给你带来惊喜。" & vbCrLf & vbCrLf &
-                "若违规,可能会被从任意相关群中踢出!",
-                "解密游戏的基本规则", "我知道了", "恕我拒绝") = 1 Then
-                MyMsgBox("你需要用自己的智慧来找到下一步的线索……" & vbCrLf &
-                         "初始线索:gnp.dorC61\60\20\0202\moc.x1xa.2s\\:sp" & "T".Lower & "th", "解密游戏") '防止触发病毒检测规则
-            End If
-        End If
-    End Sub
-    Private Sub LabLauncherTheme8Copy_MouseRightButtonUp() Handles LabLauncherTheme8Copy.MouseRightButtonUp, RadioLauncherTheme8.MouseRightButtonUp
-        OpenWebsite("https://meloong.com/afd/a/LTCat")
-    End Sub
-    Private Sub LabLauncherTheme9Copy_MouseRightButtonUp() Handles LabLauncherTheme9Copy.MouseRightButtonUp, RadioLauncherTheme9.MouseRightButtonUp
-        PageOtherLeft.TryFeedback()
-    End Sub
-
     '主题自定义
     Private Sub RadioLauncherTheme14_Change(sender As Object, e As RouteEventArgs) Handles RadioLauncherTheme14.Changed
         If RadioLauncherTheme14.Checked Then

+ 6 - 7
Plain Craft Launcher 2/Pages/PageSetup/Settings.vb

@@ -22,7 +22,6 @@ Public Class Settings
         New Setting("HintIndieSetup", False, Source:=Sources.Registry),
         New Setting("HintExportConfig", False, Source:=Sources.Registry),
         New Setting("HintSnapshot", False, Source:=Sources.Registry),
-        New Setting("SystemEulaVersion", 0, Source:=Sources.Registry),
         New Setting("SystemCount", 0, Source:=Sources.Registry, Encrypted:=True),
         New Setting("SystemLaunchCount", 0, Source:=Sources.Registry, Encrypted:=True),
         New Setting("SystemLastVersionReg", 0, Source:=Sources.Registry, Encrypted:=True),
@@ -35,9 +34,9 @@ Public Class Settings
         New Setting("SystemDebugDelay", False, Source:=Sources.Registry),
         New Setting("SystemDebugSkipCopy", False, Source:=Sources.Registry),
         New Setting("SystemSystemCache", "", Source:=Sources.Registry),
-        New Setting("SystemSystemUpdate", 0),
-        New Setting("SystemSystemActivity", 0),
-        New Setting("SystemSystemTelemetry", True, Source:=Sources.Registry),
+        New Setting("SystemSystemUpdate", 3),
+        New Setting("SystemSystemActivity", 2),
+        New Setting("SystemSystemTelemetry", False, Source:=Sources.Registry),
         New Setting("CacheDrops", "", Source:=Sources.Registry),
         New Setting("CacheConfig", 0, Source:=Sources.Registry),
         New Setting("CacheExportConfig", "", Source:=Sources.Registry),
@@ -116,8 +115,8 @@ Public Class Settings
         New Setting("ToolHelpChinese", True, Source:=Sources.Registry),
         New Setting("ToolDownloadThread", 63, Source:=Sources.Registry),
         New Setting("ToolDownloadSpeed", 42, Source:=Sources.Registry, OnChanged:=AddressOf ModNet.UpdateNetTaskSpeedLimitHigh),
-        New Setting("ToolDownloadSource", 1, Source:=Sources.Registry),
-        New Setting("ToolDownloadVersion", 1, Source:=Sources.Registry),
+        New Setting("ToolDownloadSource", 0, Source:=Sources.Registry),
+        New Setting("ToolDownloadVersion", 0, Source:=Sources.Registry),
         New Setting("ToolDownloadTranslate", 0, Source:=Sources.Registry),
         New Setting("ToolDownloadTranslateV2", 1, Source:=Sources.Registry),
         New Setting("ToolDownloadCert", False, Source:=Sources.Registry, OnChanged:=AddressOf ModNet.ShouldValidateSslCertificateOnLogin),
@@ -132,7 +131,7 @@ Public Class Settings
         New Setting("UiLauncherSat", 80),
         New Setting("UiLauncherDelta", 90),
         New Setting("UiLauncherLight", 20),
-        New Setting("UiLauncherTheme", 0, OnChanged:=AddressOf ThemeRefresh),
+        New Setting("UiLauncherTheme", DefaultTheme, OnChanged:=AddressOf ThemeRefresh),
         New Setting("UiLauncherThemeHide", "0|1|2|3|4", Source:=Sources.Registry, Encrypted:=True),
         New Setting("UiLauncherThemeHide2", "0|1|2|3|4", Source:=Sources.Registry, Encrypted:=True),
         New Setting("UiLauncherLogo", True),

+ 12 - 8
Plain Craft Launcher 2/Plain Craft Launcher 2.vbproj

@@ -1,5 +1,4 @@
-<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
-  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+<Project Sdk="Microsoft.NET.Sdk">
   <PropertyGroup>
     <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
     <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
@@ -8,7 +7,12 @@
     <OutputType>WinExe</OutputType>
     <RootNamespace>PCL</RootNamespace>
     <AssemblyName>Plain Craft Launcher 2</AssemblyName>
-    <TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
+    <TargetFramework>net48</TargetFramework>
+    <UseWPF>true</UseWPF>
+    <UseWindowsForms>true</UseWindowsForms>
+    <EnableDefaultItems>false</EnableDefaultItems>
+    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
     <MyType>Custom</MyType>
     <IsWebBootstrapper>false</IsWebBootstrapper>
     <TargetFrameworkProfile>
@@ -43,7 +47,6 @@
     <DocumentationFile>Plain Craft Launcher 2.xml</DocumentationFile>
     <NoWarn>41999,42016,42017,42018,42019,42020,42021,42022,42032,42036,42314</NoWarn>
     <Prefer32Bit>false</Prefer32Bit>
-    <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
     <Optimize>false</Optimize>
     <WarningLevel>1</WarningLevel>
     <RunCodeAnalysis>false</RunCodeAnalysis>
@@ -61,7 +64,6 @@
     <DocumentationFile>Plain Craft Launcher 2.xml</DocumentationFile>
     <NoWarn>41999,42016,42017,42018,42019,42020,42021,42022,42032,42036,42314</NoWarn>
     <Prefer32Bit>false</Prefer32Bit>
-    <CodeAnalysisRuleSet>BasicDesignGuidelineRules.ruleset</CodeAnalysisRuleSet>
     <WarningLevel>1</WarningLevel>
     <DefineConstants>SNAPSHOT</DefineConstants>
   </PropertyGroup>
@@ -108,7 +110,6 @@
     <WarningLevel>1</WarningLevel>
     <NoWarn>41999,42016,42017,42018,42019,42020,42021,42022,42032,42036,42314</NoWarn>
     <PlatformTarget>AnyCPU</PlatformTarget>
-    <CodeAnalysisRuleSet>BasicDesignGuidelineRules.ruleset</CodeAnalysisRuleSet>
     <DefineConstants>RELEASE</DefineConstants>
     <Prefer32Bit>false</Prefer32Bit>
   </PropertyGroup>
@@ -120,6 +121,9 @@
     </AssemblyOriginatorKeyFile>
   </PropertyGroup>
   <ItemGroup>
+    <Compile Include="Modules\RedPolicy.vb" />
+    <EmbeddedResource Include="..\LICENCE" LogicalName="PCL.Licence.txt" />
+    <EmbeddedResource Include="..\.local\build-credentials.json" Condition="'$(IncludePrivateBuildCredentials)' == 'true' And Exists('..\.local\build-credentials.json')" LogicalName="PCL.RedBuildCredentials.json" />
     <Reference Include="CacheCow.Client, Version=2.13.1.0, Culture=neutral, processorArchitecture=MSIL">
       <SpecificVersion>False</SpecificVersion>
       <HintPath>Resources\CacheCow.Client.dll</HintPath>
@@ -751,6 +755,7 @@
   </ItemGroup>
   <ItemGroup>
     <Resource Include="Images\icon.ico" />
+    <Resource Include="Images\Red\Splash.png" />
   </ItemGroup>
   <ItemGroup>
     <Resource Include="Resources\Font.ttf" />
@@ -950,5 +955,4 @@
     <Resource Include="Images\Heads\HerobrineXia.png" />
 
   </ItemGroup>
-  <Import Project="$(MSBuildToolsPath)\Microsoft.VisualBasic.targets" />
-</Project>
+</Project>

+ 72 - 13
README.md

@@ -1,17 +1,76 @@
-# Plain Craft Launcher
+# PCL Communist
 
-[![Stars](https://img.shields.io/github/stars/Meloong-Git/PCL?style=flat&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEiIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZD0iTTggLjI1YS43NS43NSAwIDAgMSAuNjczLjQxOGwxLjg4MiAzLjgxNSA0LjIxLjYxMmEuNzUuNzUgMCAwIDEgLjQxNiAxLjI3OWwtMy4wNDYgMi45Ny43MTkgNC4xOTJhLjc1MS43NTEgMCAwIDEtMS4wODguNzkxTDggMTIuMzQ3bC0zLjc2NiAxLjk4YS43NS43NSAwIDAgMS0xLjA4OC0uNzlsLjcyLTQuMTk0TC44MTggNi4zNzRhLjc1Ljc1IDAgMCAxIC40MTYtMS4yOGw0LjIxLS42MTFMNy4zMjcuNjY4QS43NS43NSAwIDAgMSA4IC4yNVoiIGZpbGw9IiNlYWM1NGYiLz48L3N2Zz4=&logoSize=auto&label=Stars&labelColor=444444&color=eac54f)](https://github.com/Meloong-Git/PCL/)
-[![Issues](https://img.shields.io/github/issues/Meloong-Git/PCL?style=flat&label=Issues&labelColor=444444&color=1F883D)](https://github.com/Meloong-Git/PCL/issues)
-[![哔哩哔哩](https://img.shields.io/badge/动态-BiliBili-00A4DB?style=flat&labelColor=444444&logoSize=auto)](https://space.bilibili.com/11343203/dynamic)
-[![爱发电](https://img.shields.io/badge/赞助-爱发电-946ce6?style=flat&labelColor=444444&logoSize=auto)](https://meloong.com/afd/a/LTCat)
+<p align="center"><img src="Plain%20Craft%20Launcher%202/Images/Red/Splash.png" width="112" alt="红底金色镰刀锤子图标"></p>
 
-包含 PCL 的大部分源代码,包括 UI 库、动画模块、下载模块、Minecraft 启动模块等!<br/>
-代码并不是实时更新的,它会在每次 PCL 发布更新时同步一次。<br/>
-非常感谢社区的大力支持!
+**让所有颜色属于所有人。**
 
-<br/>
+基于 Plain Craft Launcher 的第三方独立修改版。原作者为龙腾猫跃及 PCL 贡献者;本项目由社区独立维护。
 
-### 相关资源
-- [PCL 下载](https://meloong.com/pcl):下载可免费使用的正式版 PCL!
-- [PCL 功能投票](https://github.com/Meloong-Git/PCL/discussions/2):来参加投票吧,开发者会优先处理票数较高的项目!
-- [帮助文档库](https://github.com/LTCatt/PCL2Help):PCL 内置帮助文档的存储库
+[下载单文件 EXE](https://github.com/Angelhellwolf/PCL-Communist/releases/latest) · [查看源代码](https://github.com/Angelhellwolf/PCL-Communist) · [反馈问题](https://github.com/Angelhellwolf/PCL-Communist/issues)
+
+![启动器主页](docs/images/launcher.png)
+
+## 使用
+
+在 Releases 下载启动器 EXE,直接运行。运行环境为 Windows 与 .NET Framework 4.8,无需旁置 DLL、配置文件或安装 .NET SDK。程序运行后会按需创建设置、日志和游戏文件。
+
+## 功能
+
+- 默认苏维埃红主题:红金配色、镰刀锤子背景,以及统一的程序、窗口和启动图标。
+- 全部 17 个主题直接可选,包括隐藏、成就、自定义和动态主题;移除赞助遮罩及解锁条件。
+- 开放 Minecraft 客户端下载,不设置中国大陆地区的下载限制;默认优先 BMCLAPI 镜像,保留官方回退和文件哈希校验。实际可达性取决于下载源与当地网络。
+- 保留微软正版登录、账号刷新、正版购买验证、离线模式,以及游戏、Java、加载器、模组和整合包下载所需的元数据。
+- 移除启动器远程更新、配置下发、遥测、联网主页、远程帮助、EasyTier、第三方登录和在线皮肤管理。
+- 首次启动直接进入主页,移除“协议授权”弹窗和启动阻断。
+
+## 为什么我们把它叫作“共产主义”
+
+在这个启动器里,我们把“真正的共产主义”落实为几件可以检查的事:所有用户获得相同的主题选择,下载入口不按地区划分身份,修改源码公开,改进可以由社区共同参与。红色属于每个人,金色也属于每个人;自愿赞助可以支持开发,不能成为颜色的户籍。
+
+社会主义的优越性,在本项目的设计立场中体现为共同享有和公平可及:降低使用门槛,让每个人都能受益于共同维护的工具,把时间花在修复问题和改善体验上。我们的口号有一点戏谑,功能选择是认真的。
+
+## 关于“资本主义”和“配色反革命”的讽刺
+
+龙猫的开发劳动值得署名,龙猫制定的规则也应当接受批评。[上游分发指南](https://github.com/lZiMUl/PCL2/blob/6b9a50df911c9d68b69b24c979efba477fb14b67/LICENCE)要求 Windows 修改版避免与赞助解锁功能相似的表现,并以主要色相必须为蓝色为例。在我们看来,把颜色与赞助特权绑定、再限制其他修改版提供相同选择,体现的是资本主义的排他和差别待遇逻辑。
+
+我们把这套管理方式戏称为“朝鲜式的 RGB 王国”:连换个颜色,都仿佛要等“将军”批准;连别人的修改版,也想统一制服。嘴上讲共享,手里给颜色划阶层,这就是本项目所讽刺的“配色反革命”。我们对龙猫的批评针对其公开的软件规则与功能设计;这里的“朝鲜”“将军”“反革命”是政治比喻和讽刺。
+
+我们的回答写在程序里:主题全部开放,客户端下载开放,源码公开。让选择权回到用户手中。
+
+<p align="center"><img src="docs/images/theme-satire.png" width="560" alt="以双雕像和 PCL 图标戏仿软件项目中的个人崇拜"></p>
+
+<p align="center"><em>用户提供的戏仿图,用于讽刺软件治理中的个人崇拜与集中控制。</em></p>
+
+## 从源码构建
+
+需要 Windows 与 .NET 9 SDK。
+
+```powershell
+git clone --recurse-submodules https://github.com/Angelhellwolf/PCL-Communist.git
+cd PCL-Communist
+powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\Build.ps1
+```
+
+构建结果为 `artifacts/PCL-Communist-2.99.0.0/PCL Communist.exe`。可用 `-DotnetPath` 指定独立 SDK 路径。完整源码 ZIP 已包含固定版本的 MeloongCore,解压后可直接运行构建脚本。
+
+公开构建不内嵌私有凭据。微软登录使用公开 OAuth 客户端标识,可通过 `PCL_MS_CLIENT_ID` 环境变量覆盖;CurseForge 官方 API 可通过运行环境中的 `PCL_CURSEFORGE_API_KEY` 配置,下载镜像回退仍保留。个人构建配置、访问令牌和账号信息不属于仓库内容。
+
+只打包完整源码:
+
+```powershell
+powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\Package.ps1 -OutputDirectory .\artifacts\source -SourceOnly
+```
+
+## 验证与贡献
+
+GitHub Actions 自动构建 Release 并运行 WPF 检查。`tools/SmokeTest` 覆盖主题、背景、图标、产品名称、无协议弹窗、联网规则和公开构建凭据检查;`tools/Test-IsolatedProcess.ps1` 在非活动桌面验证独立启动。可给测试宿主追加 `--download`,实际验证 BMCLAPI、官方回退和 Minecraft 1.21.1 客户端 SHA1。
+
+已有本机检查覆盖单 EXE 首次启动、全部主题和实际客户端下载。微软完整账号授权需要用户自行完成,未代替用户实登。问题和改进建议请提交到本仓库 Issues 或 Pull Requests。
+
+## 来源与许可
+
+- PCL 源码基线:`lZiMUl/PCL2`,提交 `6b9a50df911c9d68b69b24c979efba477fb14b67`,2.99.0.0 正式版。
+- MeloongCore 固定提交:`98157d65c7627721c816c02e8be96ac4fe61d615`。
+- 原作者:龙腾猫跃及 PCL 贡献者。[原作者赞助页面](https://meloong.com/afd/a/LTCat)。
+- 本仓库公开修改源码;上游 PCL 沿用其有限分发许可,详见 [LICENCE](LICENCE)。依赖保留各自许可和版权声明。
+- 主题配色为本修改版补全实现。此版本由第三方独立修改,非 Minecraft 官方产品。

BIN
docs/images/launcher.png


BIN
docs/images/theme-satire.png


+ 14 - 0
tools/Build.ps1

@@ -0,0 +1,14 @@
+param(
+    [string]$DotnetPath = 'dotnet',
+    [string]$OutputDirectory = ''
+)
+$ErrorActionPreference = 'Stop'
+$repo = Split-Path -Parent $PSScriptRoot
+if (-not $OutputDirectory) { $OutputDirectory = Join-Path $repo 'artifacts\PCL-Communist-2.99.0.0' }
+$project = Join-Path $repo 'Plain Craft Launcher 2\Plain Craft Launcher 2.vbproj'
+& $DotnetPath build $project -c Release -v:minimal -p:IncludePrivateBuildCredentials=false
+if ($LASTEXITCODE -ne 0) { throw 'Launcher build failed.' }
+New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null
+$binary = Join-Path $repo 'Plain Craft Launcher 2\bin\Plain Craft Launcher 2.exe'
+Copy-Item -LiteralPath $binary -Destination (Join-Path $OutputDirectory 'PCL Communist.exe') -Force
+Get-FileHash -LiteralPath (Join-Path $OutputDirectory 'PCL Communist.exe') -Algorithm SHA256

+ 65 - 0
tools/Generate-Icon.ps1

@@ -0,0 +1,65 @@
+#Requires -PSEdition Desktop
+param()
+$ErrorActionPreference = 'Stop'
+Add-Type -AssemblyName WindowsBase, PresentationCore, PresentationFramework
+$imageRoot = Join-Path (Split-Path -Parent $PSScriptRoot) 'Plain Craft Launcher 2\Images'
+$redRoot = Join-Path $imageRoot 'Red'
+New-Item -ItemType Directory -Path $redRoot -Force | Out-Null
+
+# Render native WPF vector geometry at every icon resolution.
+$symbol = [Windows.Media.FormattedText]::new([string][char]0x262D, [Globalization.CultureInfo]::InvariantCulture,
+    [Windows.FlowDirection]::LeftToRight, [Windows.Media.Typeface]::new('Segoe UI Symbol'), 200,
+    [Windows.Media.Brushes]::White, 1.0)
+$emblem = $symbol.BuildGeometry([Windows.Point]::new(0, 0))
+$bounds = $emblem.Bounds
+$emblemScale = [Math]::Min(168 / $bounds.Width, 168 / $bounds.Height)
+$emblem.Transform = [Windows.Media.MatrixTransform]::new($emblemScale, 0, 0, $emblemScale,
+    128 - ($bounds.X + $bounds.Width / 2) * $emblemScale,
+    147 - ($bounds.Y + $bounds.Height / 2) * $emblemScale)
+$emblem.Freeze()
+$star = [Windows.Media.Geometry]::Parse('M128,28 L132.3,39.2 L144.3,39.8 L134.9,47.4 L138,59 L128,52.5 L118,59 L121.1,47.4 L111.7,39.8 L123.7,39.2 Z')
+$red = [Windows.Media.LinearGradientBrush]::new([Windows.Media.ColorConverter]::ConvertFromString('#CE1D2E'), [Windows.Media.ColorConverter]::ConvertFromString('#860A17'), 90)
+$gold = [Windows.Media.LinearGradientBrush]::new([Windows.Media.ColorConverter]::ConvertFromString('#FFE69D'), [Windows.Media.ColorConverter]::ConvertFromString('#E6AF43'), 90)
+$rim = [Windows.Media.Pen]::new([Windows.Media.SolidColorBrush]::new([Windows.Media.ColorConverter]::ConvertFromString('#C9E0B566')), 2)
+
+function Get-IconPng([int]$Size) {
+    $visual = [Windows.Media.DrawingVisual]::new()
+    $context = $visual.RenderOpen()
+    $context.PushTransform([Windows.Media.ScaleTransform]::new($Size / 256.0, $Size / 256.0))
+    $context.DrawRoundedRectangle($red, $null, [Windows.Rect]::new(0, 0, 256, 256), 48, 48)
+    if ($Size -ge 48) {
+        $context.DrawRoundedRectangle($null, $rim, [Windows.Rect]::new(13, 13, 230, 230), 37, 37)
+    }
+    $context.DrawGeometry($gold, $null, $star)
+    $context.DrawGeometry($gold, $null, $emblem)
+    $context.Pop()
+    $context.Close()
+    $bitmap = [Windows.Media.Imaging.RenderTargetBitmap]::new($Size, $Size, 96, 96, [Windows.Media.PixelFormats]::Pbgra32)
+    $bitmap.Render($visual)
+    $encoder = [Windows.Media.Imaging.PngBitmapEncoder]::new()
+    $encoder.Frames.Add([Windows.Media.Imaging.BitmapFrame]::Create($bitmap))
+    $stream = [IO.MemoryStream]::new()
+    try { $encoder.Save($stream); return ,$stream.ToArray() }
+    finally { $stream.Dispose() }
+}
+
+$sizes = @(16, 24, 32, 48, 64, 128, 256)
+$frames = @($sizes | ForEach-Object { ,(Get-IconPng $_) })
+$iconStream = [IO.MemoryStream]::new()
+$writer = [IO.BinaryWriter]::new($iconStream)
+try {
+    $writer.Write([uint16]0); $writer.Write([uint16]1); $writer.Write([uint16]$sizes.Count)
+    $offset = 6 + 16 * $sizes.Count
+    for ($i = 0; $i -lt $sizes.Count; $i++) {
+        $dimension = if ($sizes[$i] -eq 256) { 0 } else { $sizes[$i] }
+        $writer.Write([byte]$dimension); $writer.Write([byte]$dimension)
+        $writer.Write([byte]0); $writer.Write([byte]0)
+        $writer.Write([uint16]1); $writer.Write([uint16]32)
+        $writer.Write([uint32]$frames[$i].Length); $writer.Write([uint32]$offset)
+        $offset += $frames[$i].Length
+    }
+    foreach ($frame in $frames) { $writer.Write([byte[]]$frame) }
+    [IO.File]::WriteAllBytes((Join-Path $imageRoot 'icon.ico'), $iconStream.ToArray())
+} finally { $writer.Dispose(); $iconStream.Dispose() }
+[IO.File]::WriteAllBytes((Join-Path $redRoot 'Splash.png'), $frames[-1])
+Write-Output 'Created red-and-gold hammer-and-sickle splash and 7-resolution application icon.'

+ 60 - 0
tools/Package.ps1

@@ -0,0 +1,60 @@
+param(
+    [Parameter(Mandatory = $true)][string]$OutputDirectory,
+    [string]$BinaryDirectory = '',
+    [switch]$SourceOnly
+)
+$ErrorActionPreference = 'Stop'
+Add-Type -AssemblyName System.IO.Compression, System.IO.Compression.FileSystem
+$repo = Split-Path -Parent $PSScriptRoot
+if (-not $BinaryDirectory) { $BinaryDirectory = Join-Path $repo 'artifacts\PCL-Communist-2.99.0.0' }
+New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null
+$excludedDirectories = @('.git', '.local', 'bin', 'obj', 'artifacts', '.vs', '.idea', '__pycache__')
+
+function Add-PackageDirectory([IO.Compression.ZipArchive]$Archive, [string]$Directory, [string]$Prefix, [bool]$Source) {
+    foreach ($item in Get-ChildItem -LiteralPath $Directory -Force) {
+        if ($item.PSIsContainer) {
+            if ($excludedDirectories -contains $item.Name) { continue }
+            Add-PackageDirectory $Archive $item.FullName ($Prefix + $item.Name + '/') $Source
+        } else {
+            if ($item.Name -in @('.git', 'build-credentials.json', 'decompiled.cs') -or
+                $item.Name -like '.env*' -or $item.Extension -in @('.pfx', '.pem', '.key', '.snk', '.user', '.suo')) { continue }
+            if ($Source -and $Directory -eq $repo -and $item.Extension -eq '.zip') { continue }
+            [void][IO.Compression.ZipFileExtensions]::CreateEntryFromFile($Archive, $item.FullName, $Prefix + $item.Name, [IO.Compression.CompressionLevel]::Optimal)
+        }
+    }
+}
+
+function New-ProjectZip([string]$Path, [string]$Directory, [string]$Prefix, [bool]$Source) {
+    $stream = [IO.File]::Open($Path, [IO.FileMode]::Create, [IO.FileAccess]::Write)
+    try {
+        $archive = New-Object IO.Compression.ZipArchive($stream, [IO.Compression.ZipArchiveMode]::Create, $true)
+        try { Add-PackageDirectory $archive $Directory $Prefix $Source }
+        finally { $archive.Dispose() }
+    } finally { $stream.Dispose() }
+}
+
+$binaryZip = Join-Path $OutputDirectory 'PCL-Communist-2.99.0.0.zip'
+$sourceZip = Join-Path $OutputDirectory 'PCL-Communist-2.99.0.0-source.zip'
+if (-not $SourceOnly) { New-ProjectZip $binaryZip $BinaryDirectory 'PCL-Communist-2.99.0.0/' $false }
+New-ProjectZip $sourceZip $repo 'PCL-Communist-source/' $true
+
+$sourceArchive = [IO.Compression.ZipFile]::OpenRead($sourceZip)
+try {
+    if ($sourceArchive.Entries.FullName -match '(^|/)(\.git|\.local|bin|obj|artifacts)(/|$)' -or
+        $sourceArchive.Entries.FullName -match '(build-credentials\.json|decompiled\.cs)$') {
+        throw 'Unexpected private or generated file in source package.'
+    }
+    if (-not ($sourceArchive.Entries.FullName -contains 'PCL-Communist-source/MeloongCore/Shared/MeloongCore.csproj')) {
+        throw 'Source package is missing the pinned MeloongCore source.'
+    }
+    Write-Output "Source package: $($sourceArchive.Entries.Count) files; private and generated directories excluded."
+} finally { $sourceArchive.Dispose() }
+
+$packagePaths = @($sourceZip)
+if (-not $SourceOnly) { $packagePaths = @($binaryZip, $sourceZip) }
+$hashes = $packagePaths | ForEach-Object { Get-FileHash -LiteralPath $_ -Algorithm SHA256 }
+if (-not $SourceOnly) {
+    $hashLines = $hashes | ForEach-Object { "$($_.Hash.ToLowerInvariant())  $([IO.Path]::GetFileName($_.Path))" }
+    [IO.File]::WriteAllLines((Join-Path $OutputDirectory 'PCL-Communist-2.99.0.0.sha256'), [string[]]$hashLines, (New-Object Text.UTF8Encoding($false)))
+}
+$hashes

+ 277 - 0
tools/SmokeTest/Program.cs

@@ -0,0 +1,277 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Reflection;
+using System.Security.Cryptography;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Web.Script.Serialization;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Threading;
+
+internal static class Program {
+    private const BindingFlags Static = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
+    private const BindingFlags Instance = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
+    private static Assembly launcher = null!;
+    private static int checks;
+    private static readonly List<string> report = new();
+    private static Type T(string name) => launcher.GetType("PCL." + name, true)!;
+    private static object? Field(string type, string name) => T(type).GetField(name, Static)!.GetValue(null);
+    private static object? Call(string type, string name, params object?[] args) =>
+        T(type).GetMethods(Static).Single(m => m.Name == name && m.GetParameters().Length == args.Length).Invoke(null, args);
+    private static object? CallOptional(string type, string name, params object?[] args) {
+        var method = T(type).GetMethods(Static).Single(m => m.Name == name &&
+            m.GetParameters()[0].ParameterType.IsInstanceOfType(args[0]));
+        return method.Invoke(null, args.Concat(Enumerable.Repeat<object?>(Type.Missing, method.GetParameters().Length - args.Length)).ToArray());
+    }
+    private static void Set(string key, object value) => Call("Settings", "Set", key, value, null);
+    private static object Control(object page, string name) =>
+        page.GetType().GetProperty(name, Instance)?.GetValue(page) ??
+        page.GetType().GetField(name, Instance)?.GetValue(page) ??
+        throw new MissingMemberException(page.GetType().FullName, name);
+    private static void Check(bool ok, string name) {
+        if (!ok) throw new Exception("FAIL: " + name);
+        checks++;
+        report.Add("PASS: " + name);
+    }
+    private static void Pump(int milliseconds) {
+        var until = DateTime.UtcNow.AddMilliseconds(milliseconds);
+        while (DateTime.UtcNow < until) {
+            var frame = new DispatcherFrame();
+            Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() => frame.Continue = false));
+            Dispatcher.PushFrame(frame);
+            Thread.Sleep(10);
+        }
+    }
+    private static void Capture(FrameworkElement element, string path) {
+        element.UpdateLayout();
+        var bitmap = new RenderTargetBitmap((int)element.ActualWidth, (int)element.ActualHeight, 96, 96, PixelFormats.Pbgra32);
+        bitmap.Render(element);
+        var png = new PngBitmapEncoder();
+        png.Frames.Add(BitmapFrame.Create(bitmap));
+        using (var stream = File.Create(path)) png.Save(stream);
+    }
+    [STAThread]
+    private static void Main(string[] args) {
+        string output = Path.GetFullPath(args[1]);
+        Directory.CreateDirectory(output);
+        string errorPath = Path.Combine(output, "smoke-error.txt");
+        if (File.Exists(errorPath)) File.Delete(errorPath);
+        var transcript = new StreamWriter(Path.Combine(output, "test-console.txt")) { AutoFlush = true };
+        Console.SetOut(transcript);
+        Console.SetError(transcript);
+        try {
+            launcher = Assembly.LoadFrom(Path.GetFullPath(args[0]));
+            Check(!launcher.GetManifestResourceNames().Contains("PCL.RedBuildCredentials.json"), "Public executable excludes private build credentials");
+            Check(launcher.GetCustomAttribute<System.Runtime.Versioning.TargetFrameworkAttribute>()?.FrameworkName == ".NETFramework,Version=v4.8",
+                "Executable declares .NET Framework 4.8, including modern path handling");
+            var app = (Application)Activator.CreateInstance(T("Application"), true)!;
+            // Pumping WPF queues Application.Startup even in a host. Keep the host
+            // isolated from the launcher's normal splash screen and startup path.
+            var startup = (StartupEventHandler)Delegate.CreateDelegate(typeof(StartupEventHandler), app, T("Application").GetMethod("Application_Startup", Instance)!);
+            app.Startup -= startup;
+            T("Application").GetMethod("InitializeComponent")!.Invoke(app, null);
+            // WPF rejects null through the setter. Only the test host clears the
+            // backing field to prevent automatic navigation to FormMain.xaml.
+            typeof(Application).GetField("_startupUri", Instance)!.SetValue(app, null);
+            var unhandled = (DispatcherUnhandledExceptionEventHandler)Delegate.CreateDelegate(
+                typeof(DispatcherUnhandledExceptionEventHandler), app,
+                T("Application").GetMethod("Application_DispatcherUnhandledException", Instance)!);
+            app.DispatcherUnhandledException -= unhandled;
+            app.DispatcherUnhandledException += (_, error) => {
+                File.WriteAllText(Path.Combine(output, "smoke-error.txt"), error.Exception.ToString());
+                Console.Error.WriteLine(error.Exception);
+                Environment.Exit(1);
+            };
+            var core = Assembly.Load("MeloongCore");
+            core.GetType("MeloongCore.Main")!.GetMethod("Init")!.Invoke(null, new object[] { "PCL" });
+            Assembly.Load("MeloongCore.Wpf").GetType("MeloongCore.Wpf.Main")!.GetMethod("Init")!.Invoke(null, null);
+            Assembly.Load("PCLCS");
+            using (var resource = Application.GetResourceStream(new Uri("pack://application:,,,/Plain Craft Launcher 2;component/Images/Red/Splash.png")).Stream) {
+                var splash = BitmapFrame.Create(resource, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
+                Check(splash.PixelWidth == 256 && splash.PixelHeight == 256, "Hammer-and-sickle startup splash is embedded at 256 pixels");
+            }
+            using (var icon = System.Drawing.Icon.ExtractAssociatedIcon(launcher.Location))
+            using (var bitmap = icon!.ToBitmap()) {
+                bitmap.Save(Path.Combine(output, "Executable-icon.png"), System.Drawing.Imaging.ImageFormat.Png);
+                int gold = 0, red = 0;
+                for (int x = 0; x < bitmap.Width; x++) for (int y = 0; y < bitmap.Height; y++) {
+                    var pixel = bitmap.GetPixel(x, y);
+                    if (pixel.R > 200 && pixel.G > 130 && pixel.B < 180) gold++;
+                    if (pixel.R > 80 && pixel.G < 70 && pixel.B < 100) red++;
+                }
+                Check(gold > 5 && red > 5, "Executable icon contains the new red-and-gold emblem");
+            }
+            Check(((string)T("ModBase").GetField("VersionDisplay", Static)!.GetRawConstantValue()!).EndsWith("PCL Communist"), "Version text uses the requested product name");
+            Check(launcher.GetCustomAttribute<AssemblyTitleAttribute>()?.Title == "PCL Communist" &&
+                launcher.GetCustomAttribute<AssemblyProductAttribute>()?.Product == "PCL Communist", "Executable metadata uses PCL Communist");
+            using (var licence = new StreamReader(launcher.GetManifestResourceStream("PCL.Licence.txt")!))
+                Check(licence.ReadToEnd().Contains("PCL 分发有限许可"), "Distribution licence is embedded in the single executable");
+
+            // Route every test setting to the isolated host directory, including registry settings.
+            var entries = (IDictionary)T("Settings").GetField("Entries", Static)!.GetValue(null)!;
+            foreach (DictionaryEntry item in entries) {
+                object setting = item.Value;
+                var source = setting.GetType().GetField("Source")!;
+                if (source.GetValue(setting)!.ToString() == "Registry") source.SetValue(setting, Enum.Parse(source.FieldType, "Normal"));
+            }
+            Check((int)entries["UiLauncherTheme"]!.GetType().GetField("DefaultValue")!.GetValue(entries["UiLauncherTheme"])! == 15, "Soviet red is the fresh-install default");
+            Check(!(bool)T("ModSecret").GetField("RestrictMcDownload", Static)!.GetRawConstantValue()!, "Minecraft downloads are unrestricted");
+            Check(!string.IsNullOrWhiteSpace((string)Field("ModSecret", "OAuthClientId")!), "Microsoft public client ID is configured");
+            Check((int)entries["ToolDownloadSource"]!.GetType().GetField("DefaultValue")!.GetValue(entries["ToolDownloadSource"])! == 0, "Mainland mirrors are preferred for files");
+            Check((int)entries["ToolDownloadVersion"]!.GetType().GetField("DefaultValue")!.GetValue(entries["ToolDownloadVersion"])! == 0, "Mainland mirrors are preferred for manifests");
+
+            bool Allowed(string url, string method = "GET", bool file = false) =>
+                (bool)Call("RedPolicy", "IsNetworkAllowed", url, new HttpMethod(method), file)!;
+            Check(Allowed("https://bmclapi2.bangbang93.com/mc/game/version_manifest.json"), "BMCLAPI manifest is allowed");
+            Check(Allowed("https://piston-meta.mojang.com/v1/test.json"), "Mojang metadata fallback is allowed");
+            Check(Allowed("https://cdn.example.org/pack.mrpack", file: true), "Custom pack file downloads are allowed");
+            Check(Allowed("https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode", "POST"), "Microsoft device login is allowed");
+            Check(Allowed("https://login.live.com/oauth20_token.srf", "POST"), "Microsoft token refresh is allowed");
+            Check(Allowed("https://user.auth.xboxlive.com/user/authenticate", "POST"), "Xbox authentication is allowed");
+            Check(Allowed("https://xsts.auth.xboxlive.com/xsts/authorize", "POST"), "XSTS authentication is allowed");
+            Check(Allowed("https://api.minecraftservices.com/entitlements/mcstore"), "Minecraft ownership validation is allowed");
+            Check(Allowed("https://api.minecraftservices.com/minecraft/profile"), "Authenticated Minecraft identity is allowed");
+            Check(!Allowed("https://api.minecraftservices.com/minecraft/profile/skins", "POST"), "Skin uploads are blocked");
+            Check(!Allowed("https://api.minecraftservices.com/minecraft/profile/capes/active", "PUT"), "Cape mutations are blocked");
+            Check(!Allowed("https://sessionserver.mojang.com/session/minecraft/profile/id"), "Unrelated online skin lookups are blocked");
+            Check(!Allowed("https://login.microsoftonline.com.evil.example/consumers/oauth2/v2.0/token", "POST"), "Lookalike authentication hosts are blocked");
+            Check(!Allowed("https://pcl2-server-1253424809.file.myqcloud.com/ServerConfig.json", file: true), "Launcher config server is blocked even as a download");
+            Check(!Allowed("https://ap-guangzhou.cls.tencentcs.com/track", file: true), "Telemetry is blocked even as a download");
+            Check(!Allowed("https://example.org/Custom.xaml"), "Remote homepage requests are blocked");
+            Check(!Allowed("https://auth.example.org/refresh", "POST"), "Third-party authentication is blocked");
+            Check(!Allowed("ftp://example.org/pack.zip", file: true), "Unsupported URL schemes are rejected");
+            Check(!Allowed("https://cdn.example.org/upload", "POST", true), "The file download channel cannot upload");
+            Console.WriteLine("PASS: defaults and network policy");
+
+            if (args.Skip(2).Contains("--download")) {
+                var download = Task.Run(() => TestDownload(output));
+                while (!download.IsCompleted) Pump(100);
+                download.GetAwaiter().GetResult();
+                File.WriteAllLines(Path.Combine(output, "download-results.txt"), report);
+                Console.WriteLine("PASS: " + checks + " download and policy checks.");
+                Environment.Exit(0);
+            }
+
+            Set("SystemLastVersionReg", 413);
+            Set("UiLauncherTheme", 15);
+            var window = (Window)Activator.CreateInstance(T("FormMain"))!;
+            app.MainWindow = window;
+            window.WindowStartupLocation = WindowStartupLocation.Manual;
+            window.Left = -10000; window.Top = -10000;
+            window.Width = 1000; window.Height = 650;
+            window.Show();
+            Pump(1000);
+            Check(window.Title == "PCL Communist", "Main window uses the requested product name");
+            Check(((Panel)Control(window, "PanMsg")).Children.Count == 0, "Startup reaches the main page without an agreement dialog");
+            Call("ModSecret", "ThemeRefresh", 15);
+            Check((int)Field("ModSecret", "ThemeNow")! == 15, "Default theme survives startup");
+            Check(((FrameworkElement)Control(window, "CommunismBackdrop")).Visibility == Visibility.Visible, "Hammer-and-sickle background is visible");
+            Check(((string)Call("ModMinecraft", "McSkinDownload", "https://textures.minecraft.net/texture/test")!).StartsWith("pack://"), "Online skins use bundled local fallback");
+            Capture(window, Path.Combine(output, "Soviet-red-home.png"));
+
+            var page = Activator.CreateInstance(T("PageSetupUI"))!;
+            T("ModMain").GetField("FrmSetupUI", Static)!.SetValue(null, page);
+            Call("ModSecret", "ThemeCheckAll", true);
+            var ids = Enumerable.Range(0, 16).Concat(new[] { 42 }).ToArray();
+            var colors = new HashSet<int>();
+            foreach (int id in ids) {
+                Check((bool)Call("ModSecret", "ThemeCheckOne", id)!, "Theme " + id + " is unlocked");
+                Check(((UIElement)Control(page, "RadioLauncherTheme" + id)).IsEnabled, "Theme " + id + " is selectable");
+                Call("ModSecret", "ThemeRefresh", id);
+                Check((int)Field("ModSecret", "ThemeNow")! == id, "Theme " + id + " applies");
+                colors.Add((int)Field("ModSecret", "ColorHue")!);
+            }
+            Check(colors.Count >= 13, "Theme selection changes actual colors");
+            Check(!(bool)Call("ModSecret", "ThemeCheckOne", -1)!, "Invalid theme IDs are rejected");
+            Set("UiLauncherHue", 123); Set("UiLauncherSat", 45);
+            Set("UiLauncherLight", 30); Set("UiLauncherDelta", 10);
+            Call("ModSecret", "ThemeRefresh", 14);
+            Call("ModSecret", "ThemeRefresh", -1);
+            Check((int)Field("ModSecret", "ColorHue")! == 123 && (int)Field("ModSecret", "ColorSat")! == 45 && (int)Field("ModSecret", "ColorLightAdjust")! == 10, "Custom HSL sliders affect the rendered palette");
+            Call("ModSecret", "ThemeRefresh", 12);
+            int hue = (int)Field("ModSecret", "ColorHue")!;
+            Pump(100);
+            Call("ModSecret", "ThemeRefresh", -1);
+            Check((int)Field("ModSecret", "ColorHue")! != hue, "Rainbow theme changes over time");
+            Call("ModSecret", "ThemeRefresh", 15);
+            Call("ModSecret", "ThemeRefresh", 999);
+            Check((int)Field("ModSecret", "ThemeNow")! == 15, "Invalid saved theme falls back to Soviet red");
+
+            Set("UiLauncherTheme", 15);
+            ((Border)Control(window, "PanMainRight")).Child = (UIElement)page;
+            Pump(500);
+            Capture(window, Path.Combine(output, "All-themes-unlocked.png"));
+            var about = (UIElement)Activator.CreateInstance(T("PageOtherAbout"))!;
+            ((Border)Control(window, "PanMainRight")).Child = about;
+            Pump(200);
+            Check(((TextBlock)Control(about, "TextBundledLicence")).Text.Contains("PCL 分发有限许可"), "About page shows bundled distribution licence without a sidecar file");
+            File.WriteAllLines(Path.Combine(output, "smoke-results.txt"), report);
+            Console.WriteLine("PASS: " + checks + " runtime checks; home and theme screenshots saved.");
+            Environment.Exit(0);
+        } catch (Exception error) {
+            File.WriteAllText(Path.Combine(output, "smoke-error.txt"), error.ToString());
+            Console.Error.WriteLine(error);
+            Environment.Exit(1);
+        }
+    }
+
+    private static void TestDownload(string output) {
+        var serializer = new JavaScriptSerializer { MaxJsonLength = 8 * 1024 * 1024 };
+        Dictionary<string, object> Json(string url) => serializer.Deserialize<Dictionary<string, object>>(
+            (string)CallOptional("ModNet", "NetRequestByClientRetry", url)!);
+        Dictionary<string, object> Version(Dictionary<string, object> manifest) =>
+            ((IEnumerable)manifest["versions"]).Cast<Dictionary<string, object>>().Single(v => (string)v["id"] == "1.21.1");
+        var mirror = Json("https://bmclapi2.bangbang93.com/mc/game/version_manifest.json");
+        Check(Version(mirror).ContainsKey("url"), "BMCLAPI manifest loads through compiled request engine");
+        Console.WriteLine(report.Last());
+        var official = Json("https://launchermeta.mojang.com/mc/game/version_manifest.json");
+        var version = Version(official);
+        Check(version.ContainsKey("url"), "Official manifest fallback is reachable");
+        var sources = ((IEnumerable<string>)Call("ModDownload", "DlSourceLauncherOrMetaGet", version["url"])!).ToArray();
+        Check(new Uri(sources.First()).Host == "bmclapi2.bangbang93.com" && sources.Last() == (string)version["url"], "Version metadata uses mirror first with official fallback");
+        var metadata = Json(sources.First());
+        var client = (Dictionary<string, object>)((Dictionary<string, object>)metadata["downloads"])["client"];
+        var officialMetadata = Json((string)version["url"]);
+        var officialClient = (Dictionary<string, object>)((Dictionary<string, object>)officialMetadata["downloads"])["client"];
+        Check((string)client["sha1"] == (string)officialClient["sha1"], "Mirror client hash matches official metadata");
+        string expectedHash = (string)officialClient["sha1"];
+        long expectedSize = Convert.ToInt64(officialClient["size"]);
+        var checkerType = Assembly.Load("MeloongCore").GetTypes().Single(t => t.Name == "FileChecker");
+        object Checker() {
+            var checker = Activator.CreateInstance(checkerType)!;
+            checkerType.GetField("Hash")!.SetValue(checker, expectedHash);
+            checkerType.GetField("ActualSize")!.SetValue(checker, expectedSize);
+            return checker;
+        }
+        var urls = ((IEnumerable<string>)Call("ModDownload", "DlSourceLauncherOrMetaGet", officialClient["url"])!).ToArray();
+        Check(new Uri(urls.First()).Host == "bmclapi2.bangbang93.com" && urls.Last() == (string)officialClient["url"], "Client files use mirror first with official fallback");
+        string target = Path.Combine(output, "minecraft-1.21.1-client.jar");
+        if (File.Exists(target)) File.Delete(target);
+        Console.WriteLine("Downloading Minecraft 1.21.1 client through compiled multi-thread downloader...");
+        CallOptional("ModNet", "NetDownloadByLoader", new[] { urls.First() }, target, null, Checker());
+        using (var sha = SHA1.Create()) using (var stream = File.OpenRead(target)) {
+            Check(BitConverter.ToString(sha.ComputeHash(stream)).Replace("-", "").ToLowerInvariant() == expectedHash, "Actual BMCLAPI client download passes SHA1");
+        }
+        Check(new FileInfo(target).Length == expectedSize, "Downloaded client size matches official metadata");
+        Console.WriteLine(report.Last());
+        string fallback = Path.Combine(output, "minecraft-1.21.1-official-fallback.jar");
+        if (File.Exists(fallback)) File.Delete(fallback);
+        Console.WriteLine("Testing unavailable-source fallback to official client...");
+        // A deliberately blocked first source fails locally, so the same engine
+        // must advance to the real official client URL.
+        CallOptional("ModNet", "NetDownloadByLoader", new[] {
+            "https://pcl2-server-1253424809.file.myqcloud.com/disabled-client.jar", urls.Last()
+        }, fallback, null, Checker());
+        using (var sha = SHA1.Create()) using (var stream = File.OpenRead(fallback)) {
+            Check(BitConverter.ToString(sha.ComputeHash(stream)).Replace("-", "").ToLowerInvariant() == expectedHash, "Unavailable first source falls back to official client and passes SHA1");
+        }
+        report.Add("Minecraft 1.21.1 client: " + expectedSize + " bytes; SHA1 " + expectedHash);
+    }
+}

+ 14 - 0
tools/SmokeTest/SmokeTest.csproj

@@ -0,0 +1,14 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <TargetFramework>net48</TargetFramework>
+    <OutputType>Exe</OutputType>
+    <UseWPF>true</UseWPF>
+    <LangVersion>13.0</LangVersion>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+  <ItemGroup>
+    <Reference Include="System.Net.Http" />
+    <Reference Include="System.Web.Extensions" />
+    <Reference Include="System.Drawing" />
+  </ItemGroup>
+</Project>

+ 133 - 0
tools/Test-IsolatedProcess.ps1

@@ -0,0 +1,133 @@
+#Requires -PSEdition Desktop
+param(
+    [Parameter(Mandatory = $true)][string]$Executable,
+    [string]$ArgumentLine = '',
+    [Parameter(Mandatory = $true)][string]$OutputDirectory,
+    [int]$TimeoutSeconds = 30,
+    [switch]$ExpectMainWindow
+)
+$ErrorActionPreference = 'Stop'
+$Executable = [IO.Path]::GetFullPath($Executable)
+$OutputDirectory = [IO.Path]::GetFullPath($OutputDirectory)
+New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null
+
+# Run our own test process on an inactive desktop. FormMain moves itself to the
+# foreground during startup, so merely setting WindowStyle Hidden is insufficient.
+if (-not ('RedIsolatedTest' -as [type])) {
+    Add-Type -ReferencedAssemblies System.Drawing -TypeDefinition @'
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Drawing;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+
+public static class RedIsolatedTest {
+    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+    private struct StartupInfo {
+        public int cb; public string reserved; public string desktop; public string title;
+        public int x, y, width, height, charsX, charsY, fill, flags;
+        public short show, reservedSize;
+        public IntPtr reservedPointer, input, output, error;
+    }
+    [StructLayout(LayoutKind.Sequential)]
+    private struct ProcessInfo { public IntPtr process, thread; public uint id, threadId; }
+    [StructLayout(LayoutKind.Sequential)]
+    private struct Rect { public int left, top, right, bottom; }
+    private delegate bool EnumWindow(IntPtr window, IntPtr parameter);
+    [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+    private static extern IntPtr CreateDesktop(string name, IntPtr device, IntPtr mode, int flags, uint access, IntPtr security);
+    [DllImport("user32.dll")] private static extern bool CloseDesktop(IntPtr desktop);
+    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
+    private static extern bool CreateProcess(string file, StringBuilder command, IntPtr processSecurity, IntPtr threadSecurity,
+        bool inherit, uint flags, IntPtr environment, string directory, ref StartupInfo startup, out ProcessInfo process);
+    [DllImport("kernel32.dll")] private static extern uint WaitForSingleObject(IntPtr handle, uint timeout);
+    [DllImport("kernel32.dll")] private static extern bool GetExitCodeProcess(IntPtr process, out uint code);
+    [DllImport("kernel32.dll")] private static extern bool CloseHandle(IntPtr handle);
+    [DllImport("user32.dll")] private static extern bool EnumDesktopWindows(IntPtr desktop, EnumWindow callback, IntPtr parameter);
+    [DllImport("user32.dll")] private static extern bool EnumChildWindows(IntPtr parent, EnumWindow callback, IntPtr parameter);
+    [DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr window, out uint process);
+    [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern int GetWindowText(IntPtr window, StringBuilder text, int max);
+    [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern int GetClassName(IntPtr window, StringBuilder text, int max);
+    [DllImport("user32.dll")] private static extern bool GetWindowRect(IntPtr window, out Rect rectangle);
+    [DllImport("user32.dll")] private static extern bool PrintWindow(IntPtr window, IntPtr context, uint flags);
+
+    public static int Run(string executable, string arguments, string output, int seconds, bool expectWindow) {
+        string name = "RedTest-" + Guid.NewGuid().ToString("N");
+        IntPtr desktop = CreateDesktop(name, IntPtr.Zero, IntPtr.Zero, 0, 0x01FF, IntPtr.Zero);
+        if (desktop == IntPtr.Zero) throw new Win32Exception(Marshal.GetLastWin32Error());
+        ProcessInfo process = new ProcessInfo();
+        try {
+            var startup = new StartupInfo { cb = Marshal.SizeOf(typeof(StartupInfo)), desktop = name };
+            var command = new StringBuilder("\"" + executable + "\" " + arguments);
+            if (!CreateProcess(executable, command, IntPtr.Zero, IntPtr.Zero, false, 0x08000000,
+                IntPtr.Zero, Path.GetDirectoryName(executable), ref startup, out process))
+                throw new Win32Exception(Marshal.GetLastWin32Error());
+            bool exited = WaitForSingleObject(process.process, (uint)(seconds * 1000)) == 0;
+            uint code;
+            if (exited) {
+                GetExitCodeProcess(process.process, out code);
+                File.WriteAllText(Path.Combine(output, "isolated-process.txt"), "Process exited: " + code);
+                if (expectWindow) throw new Exception("Launcher exited before main-window inspection: " + code);
+                return (int)code;
+            }
+            var windows = new List<string>();
+            IntPtr main = IntPtr.Zero;
+            bool errorDialog = false;
+            EnumDesktopWindows(desktop, delegate(IntPtr window, IntPtr parameter) {
+                uint owner;
+                GetWindowThreadProcessId(window, out owner);
+                if (owner != process.id) return true;
+                var title = new StringBuilder(512);
+                var kind = new StringBuilder(256);
+                GetWindowText(window, title, title.Capacity);
+                GetClassName(window, kind, kind.Capacity);
+                windows.Add(kind + " | " + title);
+                if (title.ToString() == "PCL Communist" && kind.ToString().StartsWith("HwndWrapper")) main = window;
+                if (kind.ToString() == "#32770") {
+                    errorDialog = true;
+                    EnumChildWindows(window, delegate(IntPtr child, IntPtr unused) {
+                        var content = new StringBuilder(8192);
+                        GetWindowText(child, content, content.Capacity);
+                        if (content.Length > 0) windows.Add("  " + content);
+                        return true;
+                    }, IntPtr.Zero);
+                }
+                return true;
+            }, IntPtr.Zero);
+            File.WriteAllLines(Path.Combine(output, "isolated-process.txt"), windows);
+            if (!expectWindow) throw new TimeoutException("Test process exceeded " + seconds + " seconds.");
+            if (main == IntPtr.Zero || errorDialog) throw new Exception("Launcher main window missing or native error dialog detected. See isolated-process.txt.");
+            Rect bounds;
+            GetWindowRect(main, out bounds);
+            using (var bitmap = new Bitmap(bounds.right - bounds.left, bounds.bottom - bounds.top))
+            using (var graphics = Graphics.FromImage(bitmap)) {
+                IntPtr dc = graphics.GetHdc();
+                bool captured;
+                try { captured = PrintWindow(main, dc, 2); }
+                finally { graphics.ReleaseHdc(dc); }
+                if (captured) bitmap.Save(Path.Combine(output, "standalone-startup.png"), System.Drawing.Imaging.ImageFormat.Png);
+            }
+            File.AppendAllText(Path.Combine(output, "isolated-process.txt"), Environment.NewLine + "PASS: standalone launcher main window is alive; no native initialization error dialog.");
+            return 0;
+        } finally {
+            if (process.process != IntPtr.Zero) {
+                if (WaitForSingleObject(process.process, 0) != 0) {
+                    try { Process.GetProcessById((int)process.id).Kill(); } catch (ArgumentException) { }
+                    WaitForSingleObject(process.process, 5000);
+                }
+                CloseHandle(process.thread);
+                CloseHandle(process.process);
+            }
+            CloseDesktop(desktop);
+        }
+    }
+}
+'@
+}
+$testExitCode = [RedIsolatedTest]::Run($Executable, $ArgumentLine, $OutputDirectory, $TimeoutSeconds, $ExpectMainWindow.IsPresent)
+if ($testExitCode -ne 0) { throw "Isolated test failed with exit code $testExitCode. See $OutputDirectory." }
+Write-Output "PASS: isolated process check. Results: $OutputDirectory"

BIN
最新正式版.zip