Explorar o código

Publish PCL Communist community fork

Angel hai 20 horas
pai
achega
6453df05be
Modificáronse 56 ficheiros con 1068 adicións e 2732 borrados
  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=BIN
      Plain Craft Launcher 2/Images/Red/Splash.png
  18. BIN=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=BIN
      docs/images/launcher.png
  49. BIN=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=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
 name: Build
+
 on:
 on:
   push:
   push:
-    paths:
-      - 'Plain Craft Launcher 2/**'
-      - '.github/workflows/**'
+    branches: [main]
+    paths-ignore:
+      - '**.md'
+      - 'docs/**'
   pull_request:
   pull_request:
-    paths:
-      - 'Plain Craft Launcher 2/**'
-      - '.github/workflows/**'
+    paths-ignore:
+      - '**.md'
+      - 'docs/**'
   workflow_dispatch:
   workflow_dispatch:
 
 
+permissions:
+  contents: read
+
 jobs:
 jobs:
-  build:
-    name: Build
+  windows:
+    name: Windows Release
     runs-on: windows-latest
     runs-on: windows-latest
-    strategy:  
-       matrix:  
-         configuration: [Debug, Release]
-
+    timeout-minutes: 20
     steps:
     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
 healthchecksdb
 
 
 MaskFiles/
 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\JavaWrapper.jar" LogicalName="Launch\JavaWrapper.jar" />
 		<EmbeddedResource Include="Launch\LwjglUnsafeAgent.jar" LogicalName="Launch\LwjglUnsafeAgent.jar" />
 		<EmbeddedResource Include="Launch\LwjglUnsafeAgent.jar" LogicalName="Launch\LwjglUnsafeAgent.jar" />
 		<EmbeddedResource Include="Resource\WikiEntries.txt" LogicalName="Resource\WikiEntries.txt" />
 		<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>
 	<ItemGroup>
 	<ItemGroup>
 	  <Reference Include="Microsoft.VisualBasic" />
 	  <Reference Include="Microsoft.VisualBasic" />

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

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

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

@@ -4,7 +4,7 @@
     xmlns:local="clr-namespace:PCL"
     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"
     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}"
     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" 
     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" 
     ResizeMode="CanMinimize" Background="{x:Null}" d:DesignWidth="870" d:DesignHeight="520" Width="850" Height="500" 
     RenderTransformOrigin="0.5,0.5">
     RenderTransformOrigin="0.5,0.5">
@@ -97,6 +97,17 @@
                 </Grid.RowDefinitions>
                 </Grid.RowDefinitions>
                 <Canvas x:Name="ImgBack" IsHitTestVisible="False" Grid.Row="1" RenderOptions.BitmapScalingMode="LowQuality"
                 <Canvas x:Name="ImgBack" IsHitTestVisible="False" Grid.Row="1" RenderOptions.BitmapScalingMode="LowQuality"
                         HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
                         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 x:Name="PanTitle" Height="48">
                     <Grid.Background>
                     <Grid.Background>
                         <LinearGradientBrush EndPoint="1,0" StartPoint="0,0">
                         <LinearGradientBrush EndPoint="1,0" StartPoint="0,0">
@@ -104,6 +115,13 @@
                         </LinearGradientBrush>
                         </LinearGradientBrush>
                     </Grid.Background>
                     </Grid.Background>
                     <local:MyImage x:Name="ImgTitle" IsHitTestVisible="False" Stretch="UniformToFill" IsEnabled="False" RenderOptions.BitmapScalingMode="LowQuality" />
                     <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" 
                     <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" />
                                        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" 
                     <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 BuildType = BuildTypes.Release Then
             If LastVersion < 413 Then 'Release 2.99.0.0
             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
             End If
             If LastVersion < 411 Then 'Release 2.14.0.0
             If LastVersion < 411 Then 'Release 2.14.0.0
                 FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "新增:允许修改、添加、移除、更新已安装版本的各个组件,可从版本设置中进入"))
                 FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "新增:允许修改、添加、移除、更新已安装版本的各个组件,可从版本设置中进入"))
@@ -104,7 +104,7 @@ Public Class FormMain
             '2:BUG* IMP-
             '2:BUG* IMP-
             '1:BUG-
             '1:BUG-
             If LastVersion < 414 Then 'Snapshot 2.99.0.0
             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
             End If
             If LastVersion < 412 Then 'Snapshot 2.14.0.0
             If LastVersion < 412 Then 'Snapshot 2.14.0.0
                 FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "新增:允许修改、添加、移除、更新已安装版本的各个组件,可从版本设置中进入"))
                 FeatureList.Add(New KeyValuePair(Of Integer, String)(4, "新增:允许修改、添加、移除、更新已安装版本的各个组件,可从版本设置中进入"))
@@ -413,38 +413,15 @@ Public Class FormMain
         '加载池
         '加载池
         RunInNewThread(
         RunInNewThread(
         Sub()
         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
             Try
-                JavaInit() '延后到同意协议后再执行,避免在初次启动时进行进程操作
+                JavaInit()
                 Thread.Sleep(100)
                 Thread.Sleep(100)
-                DlClientListMojangLoader.Start(1) 'PCL 会同时根据这里的加载结果决定是否使用官方源进行下载
                 RunCountSub()
                 RunCountSub()
-                ServerLoader.Start()
                 RunInNewThread(AddressOf TryClearTaskTemp, "TryClearTaskTemp", ThreadPriority.BelowNormal)
                 RunInNewThread(AddressOf TryClearTaskTemp, "TryClearTaskTemp", ThreadPriority.BelowNormal)
             Catch ex As Exception
             Catch ex As Exception
                 Logger.Error(ex, "初始化加载池运行失败")
                 Logger.Error(ex, "初始化加载池运行失败")
             End Try
             End Try
-            '清理自动更新文件
-            Try
-                FileUtils.Delete(Paths.Base & "PCL\Plain Craft Launcher 2.exe")
-            Catch ex As Exception
-                Logger.Warn(ex, "清理自动更新文件失败")
-            End Try
             '上报
             '上报
             Telemetry("启动")
             Telemetry("启动")
             '开源版本提示
             '开源版本提示
@@ -494,22 +471,6 @@ Public Class FormMain
             UnlockedTheme.AddRange(New List(Of String)(Settings.Get(Of String)("UiLauncherThemeHide2").ToString.Split("|")))
             UnlockedTheme.AddRange(New List(Of String)(Settings.Get(Of String)("UiLauncherThemeHide2").ToString.Split("|")))
             Settings.Set("UiLauncherThemeHide2", UnlockedTheme.Distinct.ToList.Join("|"c))
             Settings.Set("UiLauncherThemeHide2", UnlockedTheme.Distinct.ToList.Join("|"c))
         End If
         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
         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")
             FileUtils.Copy(Paths.Base & "PCL\CustomSkin.png", Paths.AppDataThenName & "CustomSkin.png")

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


BIN=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
 #End If
 
 
     '版本信息
     '版本信息
-    Public Const VersionDisplay As String = BuildTypeDisplay & " " & VersionBaseName
+    Public Const VersionDisplay As String = BuildTypeDisplay & " " & VersionBaseName & " · PCL Communist"
 #If DEBUG Then
 #If DEBUG Then
     Public Const BuildTypeDisplay As String = "开发版"
     Public Const BuildTypeDisplay As String = "开发版"
     Public Const BuildType As BuildTypes = BuildTypes.Debug
     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
         Dim RetryCount As Integer = 0
 Retry:
 Retry:
         Try
         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
         Catch ex As Exception
             FileUtils.Delete(LocalPath)
             FileUtils.Delete(LocalPath)
             ex.ThrowIfCanceled()
             ex.ThrowIfCanceled()
@@ -215,7 +215,9 @@ Retry:
     '基础请求函数
     '基础请求函数
     Private Function SendRequest(Url As String, Method As HttpMethod,
     Private Function SendRequest(Url As String, Method As HttpMethod,
             Optional Content As Object = Nothing, Optional Headers As Dictionary(Of String, String) = Nothing,
             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 线程执行了网络请求")
         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
         Dim Request As HttpRequestMessage = Nothing, CancelToken As CancellationTokenSource = Nothing, Response As HttpResponseMessage = Nothing
         Try
         Try
@@ -912,6 +914,7 @@ StartThread:
                 Dim HttpDataCount As Integer = 0
                 Dim HttpDataCount As Integer = 0
                 If SourcesOnce.Contains(Th.Source) AndAlso Th <> Th.Source.SingleThread Then GoTo SourceBreak
                 If SourcesOnce.Contains(Th.Source) AndAlso Th <> Th.Source.SingleThread Then GoTo SourceBreak
                 HttpRequest = New HttpRequestMessage(HttpMethod.Get, Th.Source.Url)
                 HttpRequest = New HttpRequestMessage(HttpMethod.Get, Th.Source.Url)
+                EnsureNetworkAllowed(Th.Source.Url, HttpMethod.Get, IsFileDownload:=True)
                 SecretHeadersSign(Th.Source.Url, HttpRequest, SimulateBrowserHeaders)
                 SecretHeadersSign(Th.Source.Url, HttpRequest, SimulateBrowserHeaders)
                 CancelToken = New CancellationTokenSource(Timeout)
                 CancelToken = New CancellationTokenSource(Timeout)
                 If Not Th.IsFirstThread Then HttpRequest.Headers.Range = New Headers.RangeHeaderValue(Th.DownloadStart, Nothing)
                 If Not Th.IsFirstThread Then HttpRequest.Headers.Range = New Headers.RangeHeaderValue(Th.DownloadStart, Nothing)
@@ -1890,26 +1893,6 @@ FinishExCatch:
 
 
 #End Region
 #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>
     ''' <summary>
     ''' 设置是否应当在正版登录时校验 SSL 证书。
     ''' 设置是否应当在正版登录时校验 SSL 证书。
     ''' </summary>
     ''' </summary>

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

@@ -506,6 +506,9 @@ NextInner:
         End Try
         End Try
     End Function
     End Function
     Private Sub McLoginStart(Data As LoaderTask(Of McLoginData, McLoginResult))
     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("登录加载已开始")
         McLaunchLog("登录加载已开始")
         '校验登录信息
         '校验登录信息
         Dim CheckResult As String = McLoginAble(Data.Input)
         Dim CheckResult As String = McLoginAble(Data.Input)
@@ -609,78 +612,7 @@ SkipLogin:
         If ThemeUnlock(10, False) Then MyMsgBox("感谢你对正版游戏的支持!" & vbCrLf & "隐藏主题 跳票红 已解锁!", "提示")
         If ThemeUnlock(10, False) Then MyMsgBox("感谢你对正版游戏的支持!" & vbCrLf & "隐藏主题 跳票红 已解锁!", "提示")
     End Sub
     End Sub
     Private Sub McLoginServerStart(Data As LoaderTask(Of McLoginServer, McLoginResult))
     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
     End Sub
     Private Sub McLoginLegacyStart(Data As LoaderTask(Of McLoginLegacy, McLoginResult))
     Private Sub McLoginLegacyStart(Data As LoaderTask(Of McLoginLegacy, McLoginResult))
         Dim Input As McLoginLegacy = Data.Input
         Dim Input As McLoginLegacy = Data.Input
@@ -703,180 +635,9 @@ LoginFinish:
     End Sub
     End Sub
 
 
     'Server 登录:三种验证方式的请求
     '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 并开启登录网页
     '微软登录步骤 1,原始登录:获取 DeviceCode 并开启登录网页
     Private Function MsLoginStep1New(Data As LoaderTask(Of McLoginMs, McLoginResult)) As (OAuthAccessToken As String, OAuthRefreshToken As String)
     Private Function MsLoginStep1New(Data As LoaderTask(Of McLoginMs, McLoginResult)) As (OAuthAccessToken As String, OAuthRefreshToken As String)
@@ -1138,27 +899,7 @@ Retry:
     End Function
     End Function
     '根据用户名返回对应 UUID,需要多线程
     '根据用户名返回对应 UUID,需要多线程
     Public Function McLoginMojangUuid(Name As String, ThrowOnNotFound As Boolean)
     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
     End Function
     Public Function McLoginLegacyUuid(Name As String)
     Public Function McLoginLegacyUuid(Name As String)
         Dim FullUuid As String = Name.Length.ToString("X").EnsureLength("0", 16) & Name.GetStableHashCode().ToString("X").EnsureLength("0", 16)
         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)
         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
         'JLW
         '非 GBK 编码下命令行参数乱码的问题(JDK-8272352)
         '非 GBK 编码下命令行参数乱码的问题(JDK-8272352)
         Dim UseJLW As Boolean =
         Dim UseJLW As Boolean =

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

@@ -1403,68 +1403,15 @@ OnLoaded:
     ''' 获取 Uuid 对应的皮肤文件地址,失败将抛出异常。
     ''' 获取 Uuid 对应的皮肤文件地址,失败将抛出异常。
     ''' </summary>
     ''' </summary>
     Public Function McSkinGetAddress(UUID As String, Type As String) As String
     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
     End Function
 
 
-    Private ReadOnly McSkinDownloadLock As New Object
     ''' <summary>
     ''' <summary>
-    ''' 从 Url 下载皮肤,可能会使用缓存。返回本地文件路径,失败将抛出异常。
+    ''' 仅使用本地皮肤;在线地址回退到内置皮肤。
     ''' </summary>
     ''' </summary>
     Public Function McSkinDownload(Address As String) As String
     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
     End Function
 
 
     ''' <summary>
     ''' <summary>
@@ -1780,49 +1727,6 @@ OnLoaded:
         'Library 文件
         'Library 文件
         Result.AddRange(McLibNetFilesFromTokens(McLibListGet(Instance, False)))
         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
         If ShouldIgnoreFileCheck(Instance) Then
             Logger.Warn("设置要求尽量忽略文件检查,这可能会保留有误的文件")
             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 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
         End If
         RelativeUrl = RelativeUrl.Replace("/", "\").Lower.TrimStart("\")
         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())
     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 Sub
 
 
 #End Region
 #End Region

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

@@ -3,19 +3,30 @@
 Friend Module ModSecret
 Friend Module ModSecret
 
 
     '标注 PCL 的不同分支,仅用于替换标记
     '标注 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
     '用于微软登录的 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
     '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}
     '用于匿名数据收集的腾讯云日志服务上报 URL,形如 https://{region}.cls.tencentcs.com/track?topic_id={topic_id}
     Public Const ClsBaseUrl As String = ""
     Public Const ClsBaseUrl As String = ""
 
 
 #Region "网络鉴权"
 #Region "网络鉴权"
 
 
-    Public Const RestrictMcDownload As Boolean = True
+    Public Const RestrictMcDownload As Boolean = False
     Friend Function SecretCdnSign(UrlWithMark As String) As String
     Friend Function SecretCdnSign(UrlWithMark As String) As String
         If Not UrlWithMark.EndsWithF("{CDN}") Then Return UrlWithMark
         If Not UrlWithMark.EndsWithF("{CDN}") Then Return UrlWithMark
         Return UrlWithMark.Replace("{CDN}", "").Replace(" ", "%20")
         Return UrlWithMark.Replace("{CDN}", "").Replace(" ", "%20")
@@ -64,11 +75,57 @@ Friend Module ModSecret
     Public ThemeNow As Integer = -1
     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 ColorHue As Integer = 210, ColorSat As Integer = 85, ColorLightAdjust As Integer = 0, ColorHueTopbarDelta As OneOf(Of Integer, Integer()) = 0
     Public ThemeDontClick As Integer = 0
     Public ThemeDontClick As Integer = 0
+    Private ReadOnly LuckyThemeHue As Integer = RandomInteger(0, 359)
 
 
     Public Sub ThemeRefresh(Optional NewTheme As Integer = -1)
     Public Sub ThemeRefresh(Optional NewTheme As Integer = -1)
         Try
         Try
+            If NewTheme >= 0 AndAlso Not ThemeCheckOne(NewTheme) Then NewTheme = DefaultTheme
             If ThemeNow = NewTheme AndAlso NewTheme >= 0 Then Return
             If ThemeNow = NewTheme AndAlso NewTheme >= 0 Then Return
             If NewTheme >= 0 Then ThemeNow = NewTheme
             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)
             Color1 = New MyColor().FromHSL2(ColorHue, ColorSat * 0.2, 25 + ColorLightAdjust * 0.3)
             Color2 = New MyColor().FromHSL2(ColorHue, ColorSat, 45 + ColorLightAdjust)
             Color2 = New MyColor().FromHSL2(ColorHue, ColorSat, 45 + ColorLightAdjust)
@@ -110,21 +167,43 @@ Friend Module ModSecret
     Public Sub ThemeRefreshMain()
     Public Sub ThemeRefreshMain()
         RunInUi(
         RunInUi(
         Sub()
         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 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)
             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, .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 = 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)})
             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 = Brush
             FrmMain.PanTitle.Background.Freeze()
             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
             If Settings.Get(Of Boolean)("UiBackgroundColorful") Then
                 Brush = New LinearGradientBrush With {.EndPoint = New Point(0.1, 1), .StartPoint = New Point(0.9, 0)}
                 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.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 = 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)})
                 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
                 FrmMain.PanForm.Background = Brush
             Else
             Else
                 FrmMain.PanForm.Background = New MyColor(245, 245, 245)
                 FrmMain.PanForm.Background = New MyColor(245, 245, 245)
@@ -133,9 +212,29 @@ Friend Module ModSecret
         End Sub)
         End Sub)
     End Sub
     End Sub
     Friend Sub ThemeCheckAll(EffectSetup As Boolean)
     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
     End Sub
     Friend Function ThemeCheckOne(Id As Integer) As Boolean
     Friend Function ThemeCheckOne(Id As Integer) As Boolean
-        Return True
+        Return (Id >= 0 AndAlso Id <= DefaultTheme) OrElse Id = 42
     End Function
     End Function
     Friend Function ThemeUnlock(Id As Integer, Optional ShowDoubleHint As Boolean = True, Optional UnlockHint As String = Nothing) As Boolean
     Friend Function ThemeUnlock(Id As Integer, Optional ShowDoubleHint As Boolean = True, Optional UnlockHint As String = Nothing) As Boolean
         Return False
         Return False
@@ -146,7 +245,7 @@ Friend Module ModSecret
 #Region "更新"
 #Region "更新"
 
 
     Friend Sub UpdateCheckByButton()
     Friend Sub UpdateCheckByButton()
-        Hint("该版本中不包含更新功能……")
+        Hint("PCL Communist 已移除启动器远程更新和配置下发")
     End Sub
     End Sub
 
 
     Friend IsUpdateWaitingRestart As Boolean = False
     Friend IsUpdateWaitingRestart As Boolean = False
@@ -160,23 +259,12 @@ Friend Module ModSecret
     ''' 如果不是,则下载一个。
     ''' 如果不是,则下载一个。
     ''' </summary>
     ''' </summary>
     Friend Sub DownloadLatestPCL(Optional LoaderToSyncProgress As LoaderBase = Nothing)
     Friend Sub DownloadLatestPCL(Optional LoaderToSyncProgress As LoaderBase = Nothing)
-        '注意:如果要自行实现这个功能,请换用另一个文件路径,以免与官方版本冲突
+        '整合包携带当前本地版本,不向远程服务器请求或替换程序。
+        FileUtils.Copy(PathExe, PathTemp & "Latest.exe")
     End Sub
     End Sub
 
 
 #End Region
 #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 "赞助等级"
 #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: AssemblyCompany("")>
-<Assembly: AssemblyProduct("Plain Craft Launcher")>
+<Assembly: AssemblyProduct("PCL Communist")>
 <Assembly: AssemblyCopyright("Copyright © 成都瓜皮龙科技有限公司")>
 <Assembly: AssemblyCopyright("Copyright © 成都瓜皮龙科技有限公司")>
 <Assembly: AssemblyConfiguration("PCL2 Config Mark")>
 <Assembly: AssemblyConfiguration("PCL2 Config Mark")>
 <Assembly: ComVisible(False)>
 <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>
     <value>..\Resources\ThrottleDebounce.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
   </data>
   </data>
   <data name="Custom" type="System.Resources.ResXFileRef, System.Windows.Forms">
   <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>
   <data name="ForgeInstaller" type="System.Resources.ResXFileRef, System.Windows.Forms">
   <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>
     <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>
     <value>..\Resources\Newtonsoft.Json.dll;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
   </data>
   </data>
   <data name="Ookii.Dialogs.Wpf" type="System.Resources.ResXFileRef, System.Windows.Forms">
   <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>
   <data name="PCLCS" type="System.Resources.ResXFileRef, System.Windows.Forms">
   <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>
     <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="正版登录或离线登录" IsSelected="True" />
                             <local:MyComboBoxItem Content="仅正版登录" />
                             <local:MyComboBoxItem Content="仅正版登录" />
                             <local:MyComboBoxItem Content="仅离线登录" />
                             <local:MyComboBoxItem Content="仅离线登录" />
-                            <local:MyComboBoxItem Content="第三方登录:统一通行证" />
-                            <local:MyComboBoxItem Content="第三方登录:Authlib Injector 或 LittleSkin" />
                         </local:MyComboBox>
                         </local:MyComboBox>
                         <TextBlock Name="LabServerNide" Grid.Row="1" VerticalAlignment="Center" HorizontalAlignment="Left" Text="服务器 ID(必填)" Margin="0,8,25,0" Visibility="Collapsed" />
                         <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">
                         <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" />
                 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>
         </StackPanel>
     </local:MyScrollViewer>
     </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()
             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
             ComboServerLoginLast = ComboServerLogin.SelectedIndex
             UpdateServerLoginUI()
             UpdateServerLoginUI()
 
 

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

@@ -207,107 +207,7 @@ Public Class MySkin
     End Property
     End Property
     Private IsChanging As Boolean = False
     Private IsChanging As Boolean = False
     Public Sub BtnSkinCape_Click() Handles BtnSkinCape.Click
     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 Sub
 
 
 End Class
 End Class

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

@@ -299,6 +299,8 @@ Public Class PageLaunchLeft
         Else
         Else
             LoginPageType = Settings.Get(Of Integer)("LoginPageType")
             LoginPageType = Settings.Get(Of Integer)("LoginPageType")
         End If
         End If
+        '旧版本的第三方登录设置在此版本中回到微软/离线选择。
+        If LoginPageType < 0 OrElse LoginPageType > 2 Then LoginPageType = 0
         Select Case LoginPageType
         Select Case LoginPageType
             Case 0 '正版或离线
             Case 0 '正版或离线
 UnknownType:
 UnknownType:
@@ -337,29 +339,6 @@ UnknownType:
                 PanTypeOne.Visibility = Visibility.Visible
                 PanTypeOne.Visibility = Visibility.Visible
                 PathTypeOne.Data = (New GeometryConverter).ConvertFromString(Logo.IconButtonOffline)
                 PathTypeOne.Data = (New GeometryConverter).ConvertFromString(Logo.IconButtonOffline)
                 LabTypeOne.Text = "离线登录"
                 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
             Case Else
                 Logger.Error($"未知的登录页面:{LoginPageType}", LogBehavior.Toast)
                 Logger.Error($"未知的登录页面:{LoginPageType}", LogBehavior.Toast)
                 GoTo UnknownType
                 GoTo UnknownType
@@ -385,45 +364,8 @@ UnknownType:
         Return (Settings.Get(Of String)("CacheMsV2Name"), Settings.Get(Of String)("CacheMsV2Uuid"))
         Return (Settings.Get(Of String)("CacheMsV2Name"), Settings.Get(Of String)("CacheMsV2Uuid"))
     End Function
     End Function
     Private Shared Sub SkinMsLoad(Data As LoaderTask(Of (String, String), String))
     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
     End Sub
 
 
     '离线皮肤
     '离线皮肤
@@ -507,45 +449,8 @@ UseDefault:
         Return (Settings.Get(Of String)("CacheNideName"), Settings.Get(Of String)("CacheNideUuid"))
         Return (Settings.Get(Of String)("CacheNideName"), Settings.Get(Of String)("CacheNideUuid"))
     End Function
     End Function
     Private Shared Sub SkinNideLoad(Data As LoaderTask(Of (String, String), String))
     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
     End Sub
 
 
     'Authlib-Injector 皮肤
     'Authlib-Injector 皮肤
@@ -555,45 +460,8 @@ Finish:
         Return (Settings.Get(Of String)("CacheAuthName"), Settings.Get(Of String)("CacheAuthUuid"))
         Return (Settings.Get(Of String)("CacheAuthName"), Settings.Get(Of String)("CacheAuthUuid"))
     End Function
     End Function
     Private Shared Sub SkinAuthLoad(Data As LoaderTask(Of (String, String), String))
     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
     End Sub
 
 
     '全部皮肤加载器
     '全部皮肤加载器

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

@@ -11,6 +11,17 @@
                           Text="快照版 PCL 包含尚未正式发布的测试功能,仅限赞助者自己使用。&#xa;请不要随意发给其他人,或是用来制作整合包哦!" />
                           Text="快照版 PCL 包含尚未正式发布的测试功能,仅限赞助者自己使用。&#xa;请不要随意发给其他人,或是用来制作整合包哦!" />
             <StackPanel x:Name="PanCustom">
             <StackPanel x:Name="PanCustom">
             </StackPanel>
             </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: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"
                 <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"
                          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
     Private Sub Init() Handles Me.Loaded
         PanBack.ScrollToHome()
         PanBack.ScrollToHome()
+        CommunismWelcome.Visibility = If(ThemeNow = DefaultTheme, Visibility.Visible, Visibility.Collapsed)
         PanLog.Visibility = ModeDebug.ToVisibility
         PanLog.Visibility = ModeDebug.ToVisibility
         '快照版提示
         '快照版提示
         PanHint.Visibility =
         PanHint.Visibility =
@@ -12,6 +13,13 @@ Public Class PageLaunchRight
              CurrentRank < DonationRank.Rank23 AndAlso Not Settings.Get(Of Boolean)("HintSnapshot")).ToVisibility
              CurrentRank < DonationRank.Rank23 AndAlso Not Settings.Get(Of Boolean)("HintSnapshot")).ToVisibility
     End Sub
     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 "主页"
 #Region "主页"
 
 
     ''' <summary>
     ''' <summary>
@@ -30,180 +38,16 @@ Public Class PageLaunchRight
         End Sub)
         End Sub)
     End Sub
     End Sub
     Private Sub RefreshReal()
     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
         End If
-        '同步到 UI
         RunInUi(Sub() LoadContent(Content))
         RunInUi(Sub() LoadContent(Content))
     End Sub
     End Sub
     Private RefreshLock As New Object
     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
     Public Sub ForceRefresh() Implements IRefreshable.Refresh
         Logger.Info("要求强制刷新主页")
         Logger.Info("要求强制刷新主页")
         ClearCache()
         ClearCache()
@@ -221,7 +65,6 @@ Public Class PageLaunchRight
     ''' </summary>
     ''' </summary>
     Private Sub ClearCache()
     Private Sub ClearCache()
         LoadedContentHash = Nothing
         LoadedContentHash = Nothing
-        OnlineLoader.Input = ("", True)
         Settings.Set("CacheSavedPageUrl", "")
         Settings.Set("CacheSavedPageUrl", "")
         Settings.Set("CacheSavedPageVersion", "")
         Settings.Set("CacheSavedPageVersion", "")
         Logger.Info("已清空主页缓存")
         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"
         <Border Grid.Row="3" HorizontalAlignment="Center" x:Name="PanButtons" Opacity="0"
                 CornerRadius="5" Background="{DynamicResource ColorBrush8}" Padding="10,3,9,3">
                 CornerRadius="5" Background="{DynamicResource ColorBrush8}" Padding="10,3,9,3">
             <StackPanel Orientation="Horizontal">
             <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"
                                 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">
                                 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>
                     <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")
         RunInNewThread(Sub() EditSkin(SkinInfo), "Ms Skin Upload")
     End Sub
     End Sub
     Private Sub EditSkin(SkinInfo As McSkinInfo)
     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
     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>
 </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
 Public Class PageLinkMain
-    Private Const INVITE_CODE_VERSION As Integer = 2
-
-    '===============================
-    '  状态机与前端页面
-    '===============================
-
-#Region "状态管理"
-
     Public Enum LinkStates
     Public Enum LinkStates
         Waiting
         Waiting
         Loading
         Loading
         Failed
         Failed
         Finished
         Finished
     End Enum
     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
     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
     End Sub
     Public Shared Function ValidateCodeFormat(Code As String) As String
     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
     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
     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
         Return False
     End Function
     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()
     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 Sub
-
-#End Region
-
 End Class
 End Class

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

@@ -20,7 +20,7 @@
                     </Grid.ColumnDefinitions>
                     </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="龙腾猫跃" 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="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 Source="/Plain Craft Launcher 2;component/Images/Heads/LTCat.jpg" Grid.Row="0" Margin="3">
                         <Image.Clip>
                         <Image.Clip>
                             <EllipseGeometry Center="17 17" RadiusX="17" RadiusY="17" />
                             <EllipseGeometry Center="17 17" RadiusX="17" RadiusY="17" />
@@ -38,7 +38,7 @@
                     </Image>
                     </Image>
                     <local:MyButton Text="赞助 PCL !" Grid.Column="2" Grid.Row="0" Height="35" ColorType="Highlight"
                     <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: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" />
                                     x:Name="BtnAboutUpdate" />
                 </Grid>
                 </Grid>
             </local:MyCard>
             </local:MyCard>
@@ -532,10 +532,10 @@
             </local:MyCard>
             </local:MyCard>
             <local:MyCard Margin="0,0,0,15" Title="法律信息" IsSwapped="True" CanSwap="True">
             <local:MyCard Margin="0,0,0,15" Title="法律信息" IsSwapped="True" CanSwap="True">
                 <StackPanel Margin="25,39,23,20">
                 <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 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" />
                     <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">
                     <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: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: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: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>
                 </StackPanel>
                 </StackPanel>
             </local:MyCard>
             </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">
             <local:MyCard Margin="0,0,0,15" Title="许可与版权声明" IsSwapped="True" CanSwap="True">
                 <Grid Margin="25,42,23,4">
                 <Grid Margin="25,42,23,4">
                     <Grid.ColumnDefinitions>
                     <Grid.ColumnDefinitions>

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

@@ -11,6 +11,11 @@ Public Class PageOtherAbout
         IsLoaded = True
         IsLoaded = True
 
 
         ItemAboutPcl.Info = ItemAboutPcl.Info.Replace("%VERSION%", VersionDisplay).Replace("%VERSIONCODE%", VersionCode).Replace("%BRANCH%", CInt(BuildType))
         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
         If BuildType = BuildTypes.Debug Then
             BtnDonateDonate.Visibility = Visibility.Collapsed
             BtnDonateDonate.Visibility = Visibility.Collapsed
             BtnDonateOutput.Visibility = Visibility.Visible
             BtnDonateOutput.Visibility = Visibility.Visible

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

@@ -121,19 +121,10 @@
                         <RowDefinition Height="Auto" />
                         <RowDefinition Height="Auto" />
                         <RowDefinition Height="Auto" />
                         <RowDefinition Height="Auto" />
                     </Grid.RowDefinitions>
                     </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" />
                     <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 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>
                         <local:MyTextBox.ValidateRules>
@@ -142,7 +133,7 @@
                         </local:MyTextBox.ValidateRules>
                         </local:MyTextBox.ValidateRules>
                     </local:MyTextBox>
                     </local:MyTextBox>
                     <local:MyCheckBox Margin="0,5,0,4" Text="匿名数据收集" Grid.Row="6" Height="22" Grid.ColumnSpan="2"
                     <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" />
                         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 Height="35" Grid.Row="7" Grid.ColumnSpan="5" Margin="0,12,0,0">
                         <Grid.ColumnDefinitions>
                         <Grid.ColumnDefinitions>
@@ -150,7 +141,7 @@
                             <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
                             <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
                             <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
                             <ColumnDefinition Width="Auto" SharedSizeGroup="Button" />
                         </Grid.ColumnDefinitions>
                         </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="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" />
                         <local:MyButton Grid.Column="2" x:Name="BtnSystemSettingImp" MinWidth="140" Text="导入设置" Padding="13,0" Margin="0,0,20,0" />
                     </Grid>
                     </Grid>
@@ -205,4 +196,4 @@
             </local:MyCard>
             </local:MyCard>
         </StackPanel>
         </StackPanel>
     </local:MyScrollViewer>
     </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
             PanDonate.Visibility = Visibility.Collapsed
         Else
         Else
             PanDonate.Visibility = Visibility.Visible
             PanDonate.Visibility = Visibility.Visible
-            ItemSystemUpdateDownload.Content = "在有新版本时自动下载(更新快照版前需要先输入土豆码)"
         End If
         End If
 
 
         '非重复加载部分
         '非重复加载部分
@@ -79,26 +78,8 @@ Public Class PageSetupSystem
     End Sub
     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
     Private Sub BtnSystemUpdate_Click(sender As Object, e As EventArgs) Handles BtnSystemUpdate.Click
         UpdateCheckByButton()
         UpdateCheckByButton()
     End Sub
     End Sub
@@ -107,16 +88,7 @@ Public Class PageSetupSystem
     ''' 若返回 Nothing,则代表无更新缓存文件或出错。
     ''' 若返回 Nothing,则代表无更新缓存文件或出错。
     ''' </summary>
     ''' </summary>
     Public Shared Function IsLauncherNewest() As Boolean?
     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
     End Function
 
 
 #Region "导出 / 导入设置"
 #Region "导出 / 导入设置"

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

@@ -38,7 +38,7 @@
                             <local:MySlider local:SettingService.Key="UiLauncherLight" 
                             <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.Row="2" Grid.Column="4" x:Name="SliderLauncherLight" Visibility="Collapsed" MaxValue="40" ValueByKey="5" Margin="0,10,0,0" />
                         </Grid>
                         </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>
                             <Grid.ColumnDefinitions>
                                 <ColumnDefinition Width="Auto" SharedSizeGroup="Name1" />
                                 <ColumnDefinition Width="Auto" SharedSizeGroup="Name1" />
                                 <ColumnDefinition />
                                 <ColumnDefinition />
@@ -51,6 +51,7 @@
                                 <RowDefinition />
                                 <RowDefinition />
                                 <RowDefinition />
                                 <RowDefinition />
                                 <RowDefinition />
                                 <RowDefinition />
+                                <RowDefinition Height="30" />
                             </Grid.RowDefinitions>
                             </Grid.RowDefinitions>
                             <TextBlock VerticalAlignment="Center" HorizontalAlignment="Left" Text="主题" Margin="0,0,20,0" />
                             <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" />
                             <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" 
                             <local:MyRadioBox local:SettingService.Key="UiLauncherTheme" local:SettingService.Value="4" 
                                               Grid.Column="5" Grid.Row="0" Text="橡木棕" x:Name="RadioLauncherTheme4" />
                                               Grid.Column="5" Grid.Row="0" Text="橡木棕" x:Name="RadioLauncherTheme4" />
                             <local:MyRadioBox local:SettingService.Key="UiLauncherTheme" local:SettingService.Value="5" 
                             <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" 
                             <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" 
                             <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" 
                             <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" 
                             <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" 
                             <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" 
                             <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" 
                             <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" 
                             <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" 
                             <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>
                         </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>
                     </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>
                 </Grid>
             </local:MyCard>
             </local:MyCard>
             <local:MyCard x:Name="CardBackground" Margin="0,0,0,15" Title="背景图片">
             <local:MyCard x:Name="CardBackground" Margin="0,0,0,15" Title="背景图片">
@@ -228,9 +227,9 @@
                             <ColumnDefinition Width="0.2*" />
                             <ColumnDefinition Width="0.2*" />
                         </Grid.ColumnDefinitions>
                         </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="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="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>
                     </Grid>
                     <local:MyHint Margin="0,15,0,-4" Text="内容见 ModSetup" Theme="Blue" x:Name="HintCustom" />
                     <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" 
                     <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:MyComboBox local:SettingService.Key="UiCustomPreset" Grid.ColumnSpan="2" Grid.Column="1">
                             <local:MyComboBoxItem local:SettingService.Value="0" Content="你知道吗?" />
                             <local:MyComboBoxItem local:SettingService.Value="0" Content="你知道吗?" />
                             <local:MyComboBoxItem local:SettingService.Value="1" 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>
                         </local:MyComboBox>
                     </Grid>
                     </Grid>
                 </StackPanel>
                 </StackPanel>

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

@@ -28,14 +28,7 @@ Public Class PageSetupUI
 
 
         SliderLoad()
         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
     End Sub
     Public Sub Refresh()
     Public Sub Refresh()
@@ -307,6 +300,9 @@ Refresh:
                  "直接将主页文件拖进 PCL 窗口也可以快捷加载。", "主页自定义教程")
                  "直接将主页文件拖进 PCL 窗口也可以快捷加载。", "主页自定义教程")
     End Sub
     End Sub
     Public Shared Sub OnMainPageTypeChanged()
     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
         If FrmSetupUI Is Nothing Then Return
         Select Case CInt(Settings.Get(Of Integer)("UiCustomType"))
         Select Case CInt(Settings.Get(Of Integer)("UiCustomType"))
             Case 0 '无
             Case 0 '无
@@ -350,40 +346,6 @@ Refresh:
         End If
         End If
     End Sub
     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
     Private Sub RadioLauncherTheme14_Change(sender As Object, e As RouteEventArgs) Handles RadioLauncherTheme14.Changed
         If RadioLauncherTheme14.Checked Then
         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("HintIndieSetup", False, Source:=Sources.Registry),
         New Setting("HintExportConfig", False, Source:=Sources.Registry),
         New Setting("HintExportConfig", False, Source:=Sources.Registry),
         New Setting("HintSnapshot", 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("SystemCount", 0, Source:=Sources.Registry, Encrypted:=True),
         New Setting("SystemLaunchCount", 0, Source:=Sources.Registry, Encrypted:=True),
         New Setting("SystemLaunchCount", 0, Source:=Sources.Registry, Encrypted:=True),
         New Setting("SystemLastVersionReg", 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("SystemDebugDelay", False, Source:=Sources.Registry),
         New Setting("SystemDebugSkipCopy", False, Source:=Sources.Registry),
         New Setting("SystemDebugSkipCopy", False, Source:=Sources.Registry),
         New Setting("SystemSystemCache", "", 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("CacheDrops", "", Source:=Sources.Registry),
         New Setting("CacheConfig", 0, Source:=Sources.Registry),
         New Setting("CacheConfig", 0, Source:=Sources.Registry),
         New Setting("CacheExportConfig", "", 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("ToolHelpChinese", True, Source:=Sources.Registry),
         New Setting("ToolDownloadThread", 63, Source:=Sources.Registry),
         New Setting("ToolDownloadThread", 63, Source:=Sources.Registry),
         New Setting("ToolDownloadSpeed", 42, Source:=Sources.Registry, OnChanged:=AddressOf ModNet.UpdateNetTaskSpeedLimitHigh),
         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("ToolDownloadTranslate", 0, Source:=Sources.Registry),
         New Setting("ToolDownloadTranslateV2", 1, Source:=Sources.Registry),
         New Setting("ToolDownloadTranslateV2", 1, Source:=Sources.Registry),
         New Setting("ToolDownloadCert", False, Source:=Sources.Registry, OnChanged:=AddressOf ModNet.ShouldValidateSslCertificateOnLogin),
         New Setting("ToolDownloadCert", False, Source:=Sources.Registry, OnChanged:=AddressOf ModNet.ShouldValidateSslCertificateOnLogin),
@@ -132,7 +131,7 @@ Public Class Settings
         New Setting("UiLauncherSat", 80),
         New Setting("UiLauncherSat", 80),
         New Setting("UiLauncherDelta", 90),
         New Setting("UiLauncherDelta", 90),
         New Setting("UiLauncherLight", 20),
         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("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("UiLauncherThemeHide2", "0|1|2|3|4", Source:=Sources.Registry, Encrypted:=True),
         New Setting("UiLauncherLogo", 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>
   <PropertyGroup>
     <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
     <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
     <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
     <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
@@ -8,7 +7,12 @@
     <OutputType>WinExe</OutputType>
     <OutputType>WinExe</OutputType>
     <RootNamespace>PCL</RootNamespace>
     <RootNamespace>PCL</RootNamespace>
     <AssemblyName>Plain Craft Launcher 2</AssemblyName>
     <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>
     <MyType>Custom</MyType>
     <IsWebBootstrapper>false</IsWebBootstrapper>
     <IsWebBootstrapper>false</IsWebBootstrapper>
     <TargetFrameworkProfile>
     <TargetFrameworkProfile>
@@ -43,7 +47,6 @@
     <DocumentationFile>Plain Craft Launcher 2.xml</DocumentationFile>
     <DocumentationFile>Plain Craft Launcher 2.xml</DocumentationFile>
     <NoWarn>41999,42016,42017,42018,42019,42020,42021,42022,42032,42036,42314</NoWarn>
     <NoWarn>41999,42016,42017,42018,42019,42020,42021,42022,42032,42036,42314</NoWarn>
     <Prefer32Bit>false</Prefer32Bit>
     <Prefer32Bit>false</Prefer32Bit>
-    <CodeAnalysisRuleSet>MinimumRecommendedRules.ruleset</CodeAnalysisRuleSet>
     <Optimize>false</Optimize>
     <Optimize>false</Optimize>
     <WarningLevel>1</WarningLevel>
     <WarningLevel>1</WarningLevel>
     <RunCodeAnalysis>false</RunCodeAnalysis>
     <RunCodeAnalysis>false</RunCodeAnalysis>
@@ -61,7 +64,6 @@
     <DocumentationFile>Plain Craft Launcher 2.xml</DocumentationFile>
     <DocumentationFile>Plain Craft Launcher 2.xml</DocumentationFile>
     <NoWarn>41999,42016,42017,42018,42019,42020,42021,42022,42032,42036,42314</NoWarn>
     <NoWarn>41999,42016,42017,42018,42019,42020,42021,42022,42032,42036,42314</NoWarn>
     <Prefer32Bit>false</Prefer32Bit>
     <Prefer32Bit>false</Prefer32Bit>
-    <CodeAnalysisRuleSet>BasicDesignGuidelineRules.ruleset</CodeAnalysisRuleSet>
     <WarningLevel>1</WarningLevel>
     <WarningLevel>1</WarningLevel>
     <DefineConstants>SNAPSHOT</DefineConstants>
     <DefineConstants>SNAPSHOT</DefineConstants>
   </PropertyGroup>
   </PropertyGroup>
@@ -108,7 +110,6 @@
     <WarningLevel>1</WarningLevel>
     <WarningLevel>1</WarningLevel>
     <NoWarn>41999,42016,42017,42018,42019,42020,42021,42022,42032,42036,42314</NoWarn>
     <NoWarn>41999,42016,42017,42018,42019,42020,42021,42022,42032,42036,42314</NoWarn>
     <PlatformTarget>AnyCPU</PlatformTarget>
     <PlatformTarget>AnyCPU</PlatformTarget>
-    <CodeAnalysisRuleSet>BasicDesignGuidelineRules.ruleset</CodeAnalysisRuleSet>
     <DefineConstants>RELEASE</DefineConstants>
     <DefineConstants>RELEASE</DefineConstants>
     <Prefer32Bit>false</Prefer32Bit>
     <Prefer32Bit>false</Prefer32Bit>
   </PropertyGroup>
   </PropertyGroup>
@@ -120,6 +121,9 @@
     </AssemblyOriginatorKeyFile>
     </AssemblyOriginatorKeyFile>
   </PropertyGroup>
   </PropertyGroup>
   <ItemGroup>
   <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">
     <Reference Include="CacheCow.Client, Version=2.13.1.0, Culture=neutral, processorArchitecture=MSIL">
       <SpecificVersion>False</SpecificVersion>
       <SpecificVersion>False</SpecificVersion>
       <HintPath>Resources\CacheCow.Client.dll</HintPath>
       <HintPath>Resources\CacheCow.Client.dll</HintPath>
@@ -751,6 +755,7 @@
   </ItemGroup>
   </ItemGroup>
   <ItemGroup>
   <ItemGroup>
     <Resource Include="Images\icon.ico" />
     <Resource Include="Images\icon.ico" />
+    <Resource Include="Images\Red\Splash.png" />
   </ItemGroup>
   </ItemGroup>
   <ItemGroup>
   <ItemGroup>
     <Resource Include="Resources\Font.ttf" />
     <Resource Include="Resources\Font.ttf" />
@@ -950,5 +955,4 @@
     <Resource Include="Images\Heads\HerobrineXia.png" />
     <Resource Include="Images\Heads\HerobrineXia.png" />
 
 
   </ItemGroup>
   </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=BIN
docs/images/launcher.png


BIN=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=BIN
最新正式版.zip