diff --git a/.gitignore b/.gitignore
index fdcc3b0..949a4ec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,10 +35,10 @@ client/Assets/Plugins/Editor/JetBrains*
.vs/
.idea/
-# Autogenerated solution & project files
-*.csproj
-*.unityproj
-*.sln
+# Autogenerated solution & project files (Unity only)
+client/*.csproj
+client/*.unityproj
+client/*.sln
*.suo
*.tmp
*.user
@@ -51,6 +51,12 @@ client/Assets/Plugins/Editor/JetBrains*
*.opendb
*.VC.db
+# .NET build artifacts
+**/bin/
+**/obj/
+launcher/PCL-CE/**/bin/
+launcher/PCL-CE/**/obj/
+
# Unity meta for generated
*.pidb.meta
*.pdb.meta
@@ -69,5 +75,10 @@ sysinfo.txt
Thumbs.db
desktop.ini
+# ===================== .NET SDK (local install) =====================
+.dotnet/
+.nuget/
+.userdata/
+
# ===================== Docker =====================
docker-compose.override.yml
diff --git a/Makefile b/Makefile
index 14e70b2..e3ad050 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: dev build-all run-auth run-economy run-blueprint run-match run-social run-relay tidy lint test
+.PHONY: dev build-all run-auth run-economy run-blueprint run-match run-social run-relay run-ai tidy lint test
# 启动开发环境基础设施
dev:
@@ -30,6 +30,9 @@ run-social:
run-relay:
cd server && go run ./cmd/relay-server
+run-ai:
+ cd server && go run ./cmd/ai-service
+
# 后端:工具
tidy:
cd server && go mod tidy
diff --git a/client/Assets/Scripts/RedCircuit.AIAssistant/AIActionExecutor.cs b/client/Assets/Scripts/RedCircuit.AIAssistant/AIActionExecutor.cs
new file mode 100644
index 0000000..a874b26
--- /dev/null
+++ b/client/Assets/Scripts/RedCircuit.AIAssistant/AIActionExecutor.cs
@@ -0,0 +1,140 @@
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace RedCircuit.AIAssistant
+{
+ ///
+ /// 操作执行器,将 AI 操作指令转换为游戏内动作。
+ /// 所有操作必须经用户确认后才执行,支持撤销。
+ ///
+ public class AIActionExecutor : MonoBehaviour
+ {
+ /// 待确认的操作队列
+ private readonly Queue _pendingQueue = new();
+
+ /// 已执行的操作批次 (用于撤销)
+ private readonly Stack _executedBatches = new();
+
+ /// 单次操作元件上限
+ private const int MaxActionsPerBatch = 20;
+
+ /// 将 AI 操作加入待确认队列
+ public void QueueActions(List actions)
+ {
+ if (actions.Count > MaxActionsPerBatch)
+ {
+ Debug.LogWarning($"[AIAction] 操作数 {actions.Count} 超过单批上限 {MaxActionsPerBatch},将截断");
+ actions = actions.GetRange(0, MaxActionsPerBatch);
+ }
+
+ foreach (var action in actions)
+ {
+ _pendingQueue.Enqueue(action);
+ }
+
+ // TODO: 在画布上预览待执行操作 (半透明高亮)
+ PreviewActions(actions);
+ }
+
+ /// 执行队列中所有待确认的操作
+ public void ExecuteQueuedActions()
+ {
+ if (_pendingQueue.Count == 0) return;
+
+ // 记录操作前快照 (用于撤销)
+ var batch = new ActionBatch();
+ batch.SnapshotBefore = CaptureCanvasSnapshot();
+
+ while (_pendingQueue.TryDequeue(out var action))
+ {
+ ExecuteAction(action);
+ batch.Actions.Add(action);
+ }
+
+ batch.SnapshotAfter = CaptureCanvasSnapshot();
+ _executedBatches.Push(batch);
+
+ Debug.Log($"[AIAction] 已执行 {batch.Actions.Count} 个操作");
+ ClearPreviews();
+ }
+
+ /// 取消所有待确认的操作
+ public void CancelQueuedActions()
+ {
+ var count = _pendingQueue.Count;
+ _pendingQueue.Clear();
+ ClearPreviews();
+ Debug.Log($"[AIAction] 已取消 {count} 个待确认操作");
+ }
+
+ /// 撤销上一次 AI 操作批次
+ public void UndoLastBatch()
+ {
+ if (!_executedBatches.TryPop(out var batch))
+ {
+ Debug.LogWarning("[AIAction] 没有可撤销的 AI 操作");
+ return;
+ }
+
+ // 恢复操作前快照
+ RestoreCanvasSnapshot(batch.SnapshotBefore);
+ Debug.Log($"[AIAction] 已撤销 {batch.Actions.Count} 个操作");
+ }
+
+ private void ExecuteAction(AIAction action)
+ {
+ switch (action.Type)
+ {
+ case ActionType.Place:
+ Debug.Log($"[AIAction] 放置 {action.Component} 于 ({action.X},{action.Y}) 旋转={action.Rotation}");
+ // TODO: 调用 PlacementController.PlaceComponent()
+ break;
+
+ case ActionType.Delete:
+ Debug.Log($"[AIAction] 删除 ({action.X},{action.Y})");
+ // TODO: 调用 PlacementController.DeleteComponent()
+ break;
+
+ case ActionType.Move:
+ Debug.Log($"[AIAction] 移动 ({action.FromX},{action.FromY}) -> ({action.X},{action.Y})");
+ // TODO: 调用 PlacementController.MoveComponent()
+ break;
+
+ case ActionType.Rotate:
+ Debug.Log($"[AIAction] 旋转 ({action.X},{action.Y}) -> {action.Rotation}");
+ // TODO: 调用 PlacementController.RotateComponent()
+ break;
+
+ case ActionType.Wire:
+ Debug.Log($"[AIAction] 连线 ({action.FromX},{action.FromY}) -> ({action.X},{action.Y})");
+ // TODO: 调用 PlacementController.PlaceWire()
+ break;
+ }
+ }
+
+ private void PreviewActions(List actions)
+ {
+ // TODO: 在画布上以半透明预览所有待执行操作
+ foreach (var action in actions)
+ {
+ // 创建预览 Ghost 对象
+ }
+ }
+
+ private void ClearPreviews()
+ {
+ // TODO: 清除所有预览 Ghost 对象
+ }
+
+ private string CaptureCanvasSnapshot()
+ {
+ // TODO: 序列化当前画布状态为 JSON
+ return "{}";
+ }
+
+ private void RestoreCanvasSnapshot(string snapshot)
+ {
+ // TODO: 从 JSON 快照恢复画布状态
+ }
+ }
+}
diff --git a/client/Assets/Scripts/RedCircuit.AIAssistant/AIAssistantManager.cs b/client/Assets/Scripts/RedCircuit.AIAssistant/AIAssistantManager.cs
new file mode 100644
index 0000000..4c016a7
--- /dev/null
+++ b/client/Assets/Scripts/RedCircuit.AIAssistant/AIAssistantManager.cs
@@ -0,0 +1,134 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using UnityEngine;
+
+namespace RedCircuit.AIAssistant
+{
+ ///
+ /// AI 助搭核心协调器,管理 AI 会话生命周期,协调各子系统。
+ ///
+ public class AIAssistantManager : MonoBehaviour
+ {
+ public static AIAssistantManager Instance { get; private set; }
+
+ /// 当前会话 ID
+ public string SessionId { get; private set; } = Guid.NewGuid().ToString("N")[..16];
+
+ /// AI 是否就绪
+ public bool IsReady { get; private set; }
+
+ /// 聊天历史记录
+ public List History { get; } = new();
+
+ private AIChatController _chatController;
+ private AICircuitAdvisor _advisor;
+ private AIActionExecutor _executor;
+ private AIContextProvider _contextProvider;
+
+ private void Awake()
+ {
+ if (Instance != null) { Destroy(gameObject); return; }
+ Instance = this;
+ }
+
+ private void Start()
+ {
+ _chatController = GetComponent() ?? gameObject.AddComponent();
+ _advisor = GetComponent() ?? gameObject.AddComponent();
+ _executor = GetComponent() ?? gameObject.AddComponent();
+ _contextProvider = GetComponent() ?? gameObject.AddComponent();
+
+ IsReady = true;
+ Debug.Log("[AIAssistant] Manager initialized");
+ }
+
+ ///
+ /// 处理用户输入的消息。
+ ///
+ public async void HandleUserMessage(string message)
+ {
+ if (!IsReady)
+ {
+ _chatController.AppendSystemMessage("AI 助手尚未就绪,请稍候...");
+ return;
+ }
+
+ // 记录用户消息
+ History.Add(new ChatMessage { Role = MessageRole.User, Content = message, Timestamp = DateTime.Now });
+ _chatController.AppendUserMessage(message);
+
+ // 采集上下文
+ var context = _contextProvider.CollectContext();
+
+ // 发送到服务端 (TODO: 实现网络请求)
+ var response = await RequestAIChat(message, context);
+
+ // 处理响应
+ ProcessAIResponse(response);
+ }
+
+ private async Task RequestAIChat(string message, GameContext context)
+ {
+ // TODO: 调用 ai-service POST /api/ai/chat
+ // 使用 SSE 流式接收响应
+ await Task.Delay(100); // 模拟网络延迟
+ return new AIResponse
+ {
+ Type = ResponseType.Text,
+ Content = "AI 助搭功能正在开发中,敬请期待!",
+ Actions = new List(),
+ RequiresConfirmation = false
+ };
+ }
+
+ private void ProcessAIResponse(AIResponse response)
+ {
+ // 记录 AI 消息
+ History.Add(new ChatMessage { Role = MessageRole.Assistant, Content = response.Content, Timestamp = DateTime.Now });
+
+ switch (response.Type)
+ {
+ case ResponseType.Text:
+ _chatController.AppendAIMessage(response.Content);
+ break;
+
+ case ResponseType.Analysis:
+ _chatController.AppendAIMessage(response.Content);
+ _advisor.ShowReport(response.Analysis);
+ break;
+
+ case ResponseType.Action:
+ _chatController.AppendAIMessage(response.Content);
+ if (response.RequiresConfirmation && response.Actions.Count > 0)
+ {
+ _executor.QueueActions(response.Actions);
+ _chatController.ShowConfirmationPrompt(response.Actions.Count);
+ }
+ break;
+
+ case ResponseType.Error:
+ _chatController.AppendErrorMessage(response.Content);
+ break;
+ }
+ }
+
+ /// 用户确认执行 AI 操作
+ public void ConfirmActions()
+ {
+ _executor.ExecuteQueuedActions();
+ }
+
+ /// 用户拒绝执行 AI 操作
+ public void RejectActions()
+ {
+ _executor.CancelQueuedActions();
+ }
+
+ /// 撤销上一次 AI 操作
+ public void UndoLastAIAction()
+ {
+ _executor.UndoLastBatch();
+ }
+ }
+}
diff --git a/client/Assets/Scripts/RedCircuit.AIAssistant/AIChatController.cs b/client/Assets/Scripts/RedCircuit.AIAssistant/AIChatController.cs
new file mode 100644
index 0000000..d1c1d9e
--- /dev/null
+++ b/client/Assets/Scripts/RedCircuit.AIAssistant/AIChatController.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace RedCircuit.AIAssistant
+{
+ ///
+ /// 聊天 UI 控制器,处理消息收发与流式显示。
+ ///
+ public class AIChatController : MonoBehaviour
+ {
+ /// 聊天面板是否可见
+ public bool IsVisible { get; private set; }
+
+ /// 消息列表 (用于 UI 渲染)
+ public List Messages { get; } = new();
+
+ /// 显示聊天面板
+ public void Show()
+ {
+ IsVisible = true;
+ // TODO: 播放面板展开动画
+ }
+
+ /// 隐藏聊天面板
+ public void Hide()
+ {
+ IsVisible = false;
+ // TODO: 播放面板收起动画
+ }
+
+ /// 追加用户消息到 UI
+ public void AppendUserMessage(string content)
+ {
+ var msg = new ChatMessage
+ {
+ Role = MessageRole.User,
+ Content = content,
+ Timestamp = DateTime.Now
+ };
+ Messages.Add(msg);
+ // TODO: 实例化用户消息气泡 UI
+ }
+
+ /// 追加 AI 消息到 UI (支持流式追加)
+ public void AppendAIMessage(string content)
+ {
+ var msg = new ChatMessage
+ {
+ Role = MessageRole.Assistant,
+ Content = content,
+ Timestamp = DateTime.Now
+ };
+ Messages.Add(msg);
+ // TODO: 实例化 AI 消息气泡 UI,支持打字机效果
+ }
+
+ /// 追加系统消息
+ public void AppendSystemMessage(string content)
+ {
+ var msg = new ChatMessage
+ {
+ Role = MessageRole.System,
+ Content = content,
+ Timestamp = DateTime.Now
+ };
+ Messages.Add(msg);
+ // TODO: 实例化系统消息 (居中灰色样式)
+ }
+
+ /// 追加错误消息
+ public void AppendErrorMessage(string content)
+ {
+ AppendSystemMessage($"[错误] {content}");
+ }
+
+ /// 显示操作确认提示
+ public void ShowConfirmationPrompt(int actionCount)
+ {
+ // TODO: 显示 "AI 建议执行 N 个操作,是否确认?" 确认栏
+ Debug.Log($"[AIChat] 等待用户确认 {actionCount} 个操作");
+ }
+
+ /// 清空聊天记录
+ public void ClearHistory()
+ {
+ Messages.Clear();
+ // TODO: 清空 UI 消息列表
+ }
+ }
+}
diff --git a/client/Assets/Scripts/RedCircuit.AIAssistant/AICircuitAdvisor.cs b/client/Assets/Scripts/RedCircuit.AIAssistant/AICircuitAdvisor.cs
new file mode 100644
index 0000000..3a411ad
--- /dev/null
+++ b/client/Assets/Scripts/RedCircuit.AIAssistant/AICircuitAdvisor.cs
@@ -0,0 +1,59 @@
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace RedCircuit.AIAssistant
+{
+ ///
+ /// 电路分析与建议,解析 AI 返回的电路方案并展示诊断报告。
+ ///
+ public class AICircuitAdvisor : MonoBehaviour
+ {
+ /// 显示电路分析报告
+ public void ShowReport(CircuitAnalysisReport report)
+ {
+ if (report == null) return;
+
+ Debug.Log($"[AIAdvisor] 电路分析: {report.Summary}");
+ Debug.Log($"[AIAdvisor] 元件数: {report.Metrics.ComponentCount}, 成本: {report.Metrics.TotalCost}, 最大延迟: {report.Metrics.MaxDelay} tick");
+
+ foreach (var issue in report.Issues)
+ {
+ var icon = issue.Severity == IssueSeverity.Error ? "[ERROR]" : "[WARN]";
+ Debug.Log($"[AIAdvisor] {icon} ({issue.Location.X},{issue.Location.Y}) {issue.Description}");
+ if (!string.IsNullOrEmpty(issue.Suggestion))
+ Debug.Log($"[AIAdvisor] -> 建议: {issue.Suggestion}");
+ }
+
+ foreach (var opt in report.Optimizations)
+ {
+ Debug.Log($"[AIAdvisor] [OPT] {opt.Description} ({opt.EstimatedImprovement})");
+ }
+
+ // TODO: 在 UI 上展示分析报告面板
+ }
+
+ /// 显示电路搭建方案供用户选择
+ public void ShowCircuitPlan(List plans)
+ {
+ foreach (var plan in plans)
+ {
+ Debug.Log($"[AIAdvisor] 方案: {plan.Name} | 成本: {plan.Cost} | 延迟: {plan.Delay} tick | 元件: {plan.ComponentCount}");
+ Debug.Log($"[AIAdvisor] {plan.Description}");
+ }
+ // TODO: 在 UI 上展示方案对比卡片
+ }
+
+ /// 高亮显示问题位置
+ public void HighlightIssue(GridPosition location, IssueSeverity severity)
+ {
+ // TODO: 在画布上高亮显示问题格子
+ // Error -> 红色边框, Warning -> 黄色边框
+ }
+
+ /// 清除所有高亮
+ public void ClearHighlights()
+ {
+ // TODO: 移除画布上的所有问题高亮
+ }
+ }
+}
diff --git a/client/Assets/Scripts/RedCircuit.AIAssistant/AIContextProvider.cs b/client/Assets/Scripts/RedCircuit.AIAssistant/AIContextProvider.cs
new file mode 100644
index 0000000..a8c0eeb
--- /dev/null
+++ b/client/Assets/Scripts/RedCircuit.AIAssistant/AIContextProvider.cs
@@ -0,0 +1,83 @@
+using UnityEngine;
+
+namespace RedCircuit.AIAssistant
+{
+ ///
+ /// 上下文采集器,收集当前游戏状态供 AI 参考。
+ ///
+ public class AIContextProvider : MonoBehaviour
+ {
+ /// 采集当前游戏上下文
+ public GameContext CollectContext()
+ {
+ var context = new GameContext
+ {
+ Mode = GetCurrentMode(),
+ CanvasWidth = GetCanvasWidth(),
+ CanvasHeight = GetCanvasHeight(),
+ ComponentCount = GetComponentCount(),
+ SelectedComponentId = GetSelectedComponentId(),
+ SelectedComponentType = GetSelectedComponentType(),
+ LevelId = GetCurrentLevelId(),
+ LevelTitle = GetCurrentLevelTitle(),
+ RedstoneCoins = GetRedstoneCoins()
+ };
+
+ return context;
+ }
+
+ private string GetCurrentMode()
+ {
+ // TODO: 从 GameManager 获取当前游戏模式
+ return "creative"; // creative / puzzle / automation / multiplayer
+ }
+
+ private int GetCanvasWidth()
+ {
+ // TODO: 从 GridCanvas 获取画布宽度
+ return 128;
+ }
+
+ private int GetCanvasHeight()
+ {
+ // TODO: 从 GridCanvas 获取画布高度
+ return 128;
+ }
+
+ private int GetComponentCount()
+ {
+ // TODO: 从 CircuitGraph 获取当前元件数
+ return 0;
+ }
+
+ private string GetSelectedComponentId()
+ {
+ // TODO: 从 PlacementController 获取选中元件 ID
+ return null;
+ }
+
+ private string GetSelectedComponentType()
+ {
+ // TODO: 从 PlacementController 获取选中元件类型
+ return null;
+ }
+
+ private int? GetCurrentLevelId()
+ {
+ // TODO: 解谜模式下获取当前关卡 ID
+ return null;
+ }
+
+ private string GetCurrentLevelTitle()
+ {
+ // TODO: 解谜模式下获取当前关卡标题
+ return null;
+ }
+
+ private long GetRedstoneCoins()
+ {
+ // TODO: 从 EconomyManager 获取红石币余额
+ return 0;
+ }
+ }
+}
diff --git a/client/Assets/Scripts/RedCircuit.AIAssistant/AIModels.cs b/client/Assets/Scripts/RedCircuit.AIAssistant/AIModels.cs
new file mode 100644
index 0000000..0f82211
--- /dev/null
+++ b/client/Assets/Scripts/RedCircuit.AIAssistant/AIModels.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Collections.Generic;
+
+namespace RedCircuit.AIAssistant
+{
+ // ==================== 消息模型 ====================
+
+ public enum MessageRole { User, Assistant, System }
+
+ [Serializable]
+ public class ChatMessage
+ {
+ public MessageRole Role;
+ public string Content;
+ public DateTime Timestamp;
+ }
+
+ // ==================== 响应模型 ====================
+
+ public enum ResponseType { Text, Analysis, Action, Error }
+
+ [Serializable]
+ public class AIResponse
+ {
+ public ResponseType Type;
+ public string Content;
+ public List Actions = new();
+ public bool RequiresConfirmation;
+ public CircuitAnalysisReport Analysis;
+ }
+
+ // ==================== 操作指令模型 ====================
+
+ public enum ActionType { Place, Delete, Move, Rotate, Wire }
+
+ [Serializable]
+ public class AIAction
+ {
+ public ActionType Type;
+ public string Component; // 元件类型 (Place 时使用)
+ public int X; // 目标 X 坐标
+ public int Y; // 目标 Y 坐标
+ public int FromX; // 源 X 坐标 (Move/Wire 时使用)
+ public int FromY; // 源 Y 坐标 (Move/Wire 时使用)
+ public int Rotation; // 旋转角度 (0/90/180/270)
+ public string Description; // 操作描述 (供用户确认时显示)
+ }
+
+ // ==================== 上下文模型 ====================
+
+ [Serializable]
+ public class GameContext
+ {
+ public string Mode; // creative / puzzle / automation / multiplayer
+ public int CanvasWidth;
+ public int CanvasHeight;
+ public int ComponentCount;
+ public string SelectedComponentId;
+ public string SelectedComponentType;
+ public int? LevelId;
+ public string LevelTitle;
+ public long RedstoneCoins;
+ }
+
+ // ==================== 电路分析模型 ====================
+
+ [Serializable]
+ public class CircuitAnalysisReport
+ {
+ public string Summary;
+ public CircuitMetrics Metrics;
+ public List Issues = new();
+ public List Optimizations = new();
+ }
+
+ [Serializable]
+ public class CircuitMetrics
+ {
+ public int ComponentCount;
+ public int TotalCost;
+ public int MaxDelay;
+ public int SignalPaths;
+ }
+
+ public enum IssueSeverity { Error, Warning, Info }
+
+ [Serializable]
+ public class CircuitIssue
+ {
+ public IssueSeverity Severity;
+ public string Type; // signal_loss / short_circuit / redundant / etc.
+ public GridPosition Location;
+ public string Description;
+ public string Suggestion;
+ }
+
+ [Serializable]
+ public class CircuitOptimization
+ {
+ public string Type; // delay_reduction / cost_reduction / etc.
+ public string Description;
+ public string EstimatedImprovement;
+ }
+
+ // ==================== 电路方案模型 ====================
+
+ [Serializable]
+ public class CircuitPlan
+ {
+ public string Name;
+ public string Description;
+ public int Cost;
+ public int Delay;
+ public int ComponentCount;
+ public List Steps = new();
+ }
+
+ // ==================== 辅助类型 ====================
+
+ [Serializable]
+ public struct GridPosition
+ {
+ public int X;
+ public int Y;
+
+ public GridPosition(int x, int y) { X = x; Y = y; }
+ }
+
+ /// 操作批次 (用于撤销)
+ public class ActionBatch
+ {
+ public List Actions = new();
+ public string SnapshotBefore;
+ public string SnapshotAfter;
+ }
+}
diff --git a/client/Assets/Scripts/RedCircuit.AIAssistant/RedCircuit.AIAssistant.asmdef b/client/Assets/Scripts/RedCircuit.AIAssistant/RedCircuit.AIAssistant.asmdef
new file mode 100644
index 0000000..5b10ce9
--- /dev/null
+++ b/client/Assets/Scripts/RedCircuit.AIAssistant/RedCircuit.AIAssistant.asmdef
@@ -0,0 +1,10 @@
+{
+ "name": "RedCircuit.AIAssistant",
+ "rootNamespace": "RedCircuit.AIAssistant",
+ "references": [],
+ "includePlatforms": [],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "autoReferenced": true,
+ "defineConstraints": []
+}
diff --git a/docs/AI助搭功能策划文档.md b/docs/AI助搭功能策划文档.md
new file mode 100644
index 0000000..15767ba
--- /dev/null
+++ b/docs/AI助搭功能策划文档.md
@@ -0,0 +1,292 @@
+# MRCC 游戏内 AI 助搭功能策划文档
+
+> 版本: v1.0 | 日期: 2026-08-07 | 状态: 设计阶段
+
+## 1. 功能概述
+
+### 1.1 定义
+
+**AI 助搭**是 MRCC 游戏内置的智能助手系统,具备三大核心能力:
+
+| 能力 | 描述 | 示例 |
+|------|------|------|
+| **问答解惑** | 解答电路原理、元件属性、游戏机制相关问题 | "红石中继器怎么用?" "AND 门真值表是什么?" |
+| **助搭电路** | 根据需求描述,建议元件选型与放置方案 | "帮我搭一个二进制计数器" "这个电路怎么优化延迟?" |
+| **操作游戏** | 经用户确认后,AI 直接执行放置/连线/删除等游戏操作 | "把红石灯放到 (10,5)" "连接拉杆和第一个中继器" |
+
+### 1.2 设计目标
+
+- **零门槛上手**:新手无需查阅文档,直接用自然语言提问
+- **渐进式辅助**:从问答 -> 建议 -> 自动搭建,用户始终拥有最终决策权
+- **上下文感知**:AI 能感知当前画布状态、选中元件、游戏模式等上下文
+- **安全可控**:AI 的游戏操作必须经用户确认,支持撤销
+
+### 1.3 用户场景
+
+```
+场景 A:新手学习
+ 用户:"红石信号会衰减吗?"
+ AI:"会。红石粉每传播 1 格衰减 1 点强度,满强度 15 最多传 15 格。
+ 中继器可以恢复至满强度。需要我在画布上演示吗?"
+ 用户:"好的"
+ AI:[放置 电源 -> 15格红石粉 -> 红石灯,并标注信号强度]
+
+场景 B:电路设计辅助
+ 用户:"帮我搭一个 XOR 门"
+ AI:"XOR 门可以用以下方案实现:
+ 方案1: 2x NOT + 1x AND + 1x OR (成本 550 红石币, 延迟 3 tick)
+ 方案2: 4x NAND (成本 880 红石币, 延迟 2 tick)
+ 推荐方案1,性价比更高。需要我自动放置吗?"
+ 用户:"用方案1"
+ AI:[在画布上放置元件并连线,用户可预览后确认]
+
+场景 C:电路分析与优化
+ 用户:"分析一下我当前的电路"
+ AI:"当前电路分析结果:
+ - 元件数: 23, 总成本: 1,840 红石币
+ - 信号路径: 电源 -> 中继器 x2 -> AND 门 -> 红石灯
+ - 最大延迟: 4 tick (可优化)
+ - 问题: 第 7 格红石粉信号已衰减至 0,导致断路
+ - 建议: 在第 5 格处添加中继器恢复信号"
+```
+
+## 2. 技术架构
+
+### 2.1 整体架构
+
+```
+┌─────────────────────────────────────────────────────┐
+│ Unity 客户端 │
+│ ┌──────────────┐ ┌──────────────┐ ┌───────────┐ │
+│ │ AIChatPanel │ │ AIAdvisor │ │ AIAction │ │
+│ │ (聊天 UI) │ │ (电路分析) │ │ Executor │ │
+│ │ │ │ │ │ (操作执行) │ │
+│ └──────┬───────┘ └──────┬───────┘ └─────┬─────┘ │
+│ │ │ │ │
+│ ┌──────┴─────────────────┴────────────────┴─────┐ │
+│ │ AIAssistantManager (核心协调器) │ │
+│ └──────────────────────┬─────────────────────────┘ │
+│ │ │
+│ ┌──────────────────────┴─────────────────────────┐ │
+│ │ AIContextProvider (上下文采集) │ │
+│ │ - 当前画布状态 - 选中元件 - 游戏模式 - 关卡信息│ │
+│ └──────────────────────────────────────────────────┘ │
+└─────────────────────────┬───────────────────────────┘
+ │ HTTPS (REST + SSE)
+┌─────────────────────────┴───────────────────────────┐
+│ ai-service (:8087) │
+│ ┌──────────┐ ┌───────────┐ ┌──────────────────┐ │
+│ │ Chat │ │ Circuit │ │ Action Planner │ │
+│ │ Handler │ │ Analyzer │ │ (操作规划器) │ │
+│ └────┬─────┘ └─────┬─────┘ └────────┬─────────┘ │
+│ │ │ │ │
+│ ┌────┴──────────────┴─────────────────┴──────────┐ │
+│ │ LLM Gateway (大模型网关) │ │
+│ │ - 意图识别 - 电路知识检索 - 操作序列生成 │ │
+│ └───────────────────────┬────────────────────────┘ │
+│ │ │
+│ ┌───────────────────────┴────────────────────────┐ │
+│ │ Knowledge Base (知识库) │ │
+│ │ - 80 种元件规格 - 电路设计模式 - 关卡攻略 │ │
+│ └────────────────────────────────────────────────┘ │
+└──────────────────────────────────────────────────────┘
+```
+
+### 2.2 技术选型
+
+| 领域 | 方案 | 说明 |
+|------|------|------|
+| LLM 引擎 | OpenAI GPT-4o / Claude 3.5 / 国产模型 | 支持多模型切换,按成本和延迟选择 |
+| 检索增强 | RAG (元件手册 + 电路模式库) | 减少幻觉,确保元件数据准确 |
+| 流式输出 | Server-Sent Events (SSE) | 打字机效果,降低用户等待感 |
+| 操作规划 | Function Calling / Tool Use | LLM 输出结构化操作指令 |
+| 上下文管理 | 滑动窗口 + 电路状态摘要 | 控制 Token 消耗 |
+| 本地缓存 | 常见问题本地缓存 | 减少 API 调用,支持离线问答 |
+
+## 3. API 设计
+
+### 3.1 REST API
+
+| 方法 | 路径 | 说明 | 鉴权 |
+|------|------|------|------|
+| POST | `/api/ai/chat` | 发送聊天消息,返回 AI 回复 | 是 |
+| POST | `/api/ai/analyze` | 分析当前电路,返回诊断报告 | 是 |
+| POST | `/api/ai/suggest` | 根据需求生成电路搭建方案 | 是 |
+| POST | `/api/ai/execute` | 执行 AI 规划的操作序列 | 是 |
+| GET | `/api/ai/history` | 获取聊天历史 | 是 |
+| POST | `/api/ai/feedback` | 用户对 AI 回复反馈 (赞/踩) | 是 |
+
+### 3.2 核心数据结构
+
+```json
+// 聊天请求
+{
+ "sessionId": "sess_abc123",
+ "message": "帮我搭一个 XOR 门",
+ "context": {
+ "mode": "creative",
+ "canvasSize": "128x128",
+ "componentCount": 0,
+ "selectedComponent": null,
+ "levelId": null
+ }
+}
+
+// 聊天响应 (SSE 流式)
+{
+ "sessionId": "sess_abc123",
+ "type": "text|action|analysis|error",
+ "content": "XOR 门可以用以下方案实现...",
+ "actions": [
+ {
+ "type": "place",
+ "component": "NOT_GATE",
+ "x": 10, "y": 20,
+ "rotation": 0,
+ "description": "放置 NOT 门 (输入反相器 1)"
+ },
+ {
+ "type": "wire",
+ "from": {"x": 10, "y": 20},
+ "to": {"x": 12, "y": 20},
+ "description": "连接 NOT 门到 AND 门"
+ }
+ ],
+ "requiresConfirmation": true
+}
+
+// 电路分析报告
+{
+ "summary": "当前电路共 23 个元件,存在 1 处断路",
+ "metrics": {
+ "componentCount": 23,
+ "totalCost": 1840,
+ "maxDelay": 4,
+ "signalPaths": 2
+ },
+ "issues": [
+ {
+ "severity": "error",
+ "type": "signal_loss",
+ "location": {"x": 7, "y": 5},
+ "description": "信号在第 7 格衰减至 0,导致断路",
+ "suggestion": "在第 5 格处添加中继器恢复信号"
+ }
+ ],
+ "optimizations": [
+ {
+ "type": "delay_reduction",
+ "description": "移除冗余中继器可减少 1 tick 延迟",
+ "estimatedImprovement": "-1 tick"
+ }
+ ]
+}
+```
+
+## 4. 客户端模块设计
+
+### 4.1 模块结构
+
+| 脚本 | 职责 |
+|------|------|
+| `AIAssistantManager` | 核心协调器,管理 AI 会话生命周期,协调各子系统 |
+| `AIChatController` | 聊天 UI 控制器,处理消息收发与流式显示 |
+| `AICircuitAdvisor` | 电路分析与建议,解析 AI 返回的电路方案 |
+| `AIActionExecutor` | 操作执行器,将 AI 操作指令转换为游戏内动作 |
+| `AIContextProvider` | 上下文采集器,收集当前游戏状态供 AI 参考 |
+| `AIModels` | 数据模型定义 (请求/响应/操作指令) |
+
+### 4.2 操作执行流程
+
+```
+用户发送消息
+ │
+ ▼
+AIAssistantManager.HandleUserMessage()
+ │
+ ├─► AIContextProvider.CollectContext() // 采集画布状态
+ │
+ ├─► ai-service POST /api/ai/chat // 发送到服务端
+ │ │
+ │ ▼ (SSE 流式响应)
+ │ 解析响应类型:
+ │ ├─ text → AIChatController.AppendText()
+ │ ├─ action → AIActionExecutor.QueueActions()
+ │ └─ analysis → AICircuitAdvisor.ShowReport()
+ │
+ ├─► AIActionExecutor (如果有操作指令)
+ │ │
+ │ ├─ 显示操作预览 (高亮待放置位置)
+ │ ├─ 用户确认 → 执行操作 (调用 PlacementController)
+ │ ├─ 用户拒绝 → 取消,记录反馈
+ │ └─ 用户编辑 → 修改后执行
+ │
+ └─► AIChatController.UpdateChatHistory()
+```
+
+### 4.3 安全约束
+
+- AI 操作**必须经用户确认**后才执行,不可自动执行
+- 每次操作最多放置 **20 个元件**,超出需分批确认
+- AI 不可操作**命令方块**和**结构方块**(仅创意模式手动放置)
+- 解谜模式下,AI 仅提供文字提示,**不可直接放置元件**
+- 所有 AI 操作支持**一键撤销** (记录操作前快照)
+
+## 5. 服务端设计
+
+### 5.1 ai-service 微服务
+
+| 属性 | 值 |
+|------|-----|
+| 端口 | 8087 |
+| 语言 | Go 1.22 / Gin |
+| 依赖 | LLM API、Redis (会话缓存)、PostgreSQL (历史记录) |
+
+### 5.2 LLM Gateway 设计
+
+```
+用户消息 + 上下文
+ │
+ ▼
+┌──────────────┐
+│ 意图识别 │ → 问答 / 助搭 / 分析 / 操作
+└──────┬───────┘
+ │
+ ├─ 问答 → 检索知识库 → LLM 生成回复
+ ├─ 助搭 → 生成电路方案 → LLM 验证可行性
+ ├─ 分析 → 本地电路引擎分析 → LLM 生成建议
+ └─ 操作 → LLM Function Calling → 生成操作序列
+ │
+ ▼
+┌──────────────┐
+│ 响应组装 │ → 统一格式输出
+└──────────────┘
+```
+
+### 5.3 知识库构建
+
+知识库包含以下结构化数据,供 RAG 检索:
+
+- **元件百科**:80 种元件的编号、属性、成本、使用说明
+- **电路模式库**:常见电路设计模式 (XOR 门、计数器、时钟发生器等)
+- **关卡攻略库**:240 关的通关提示 (仅提示,不给完整答案)
+- **红石原理**:信号衰减、延迟、BUD 更新等技术原理
+
+## 6. 开发计划
+
+| 阶段 | 内容 | 依赖 |
+|------|------|------|
+| Phase 1 | 基础聊天 + 元件问答 (接入 LLM API) | ai-service 骨架 |
+| Phase 2 | 电路上下文采集 + 电路分析报告 | 客户端仿真引擎 |
+| Phase 3 | AI 操作预览 + 确认执行 + 撤销 | 客户端编辑器 |
+| Phase 4 | RAG 知识库 + 电路方案生成 | 全部元件数据 |
+| Phase 5 | 解谜模式提示 (不操作) + 关卡攻略 | 关卡数据 |
+
+## 7. 成本估算
+
+| 项目 | 预估 |
+|------|------|
+| LLM API 调用 | $0.01-0.05 / 次对话 (GPT-4o) |
+| 月度 API 成本 (1000 DAU, 日均 5 次) | ~$1,500-7,500 |
+| 缓存命中率目标 | >40% (常见问题本地缓存) |
+| 本地模型备选 | Qwen2.5-7B (降低成本,需 GPU 服务器) |
diff --git a/launcher/MRCC.Launcher.sln b/launcher/MRCC.Launcher.sln
new file mode 100644
index 0000000..6bc6baf
--- /dev/null
+++ b/launcher/MRCC.Launcher.sln
@@ -0,0 +1,33 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MRCC.Launcher", "MRCC.Launcher\MRCC.Launcher.csproj", "{72F4C810-435B-463C-B65E-56AE39577261}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PCL.Core", "PCL-CE\PCL.Core\PCL.Core.csproj", "{A1294BFE-A9D2-44C0-87B8-7774AF7DB1D4}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PCL.Core.SourceGenerators", "PCL-CE\PCL.Core.SourceGenerators\PCL.Core.SourceGenerators.csproj", "{75040580-B94B-404E-BDA3-88242DE8D8EC}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {72F4C810-435B-463C-B65E-56AE39577261}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {72F4C810-435B-463C-B65E-56AE39577261}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {72F4C810-435B-463C-B65E-56AE39577261}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {72F4C810-435B-463C-B65E-56AE39577261}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A1294BFE-A9D2-44C0-87B8-7774AF7DB1D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A1294BFE-A9D2-44C0-87B8-7774AF7DB1D4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A1294BFE-A9D2-44C0-87B8-7774AF7DB1D4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A1294BFE-A9D2-44C0-87B8-7774AF7DB1D4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {75040580-B94B-404E-BDA3-88242DE8D8EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {75040580-B94B-404E-BDA3-88242DE8D8EC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {75040580-B94B-404E-BDA3-88242DE8D8EC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {75040580-B94B-404E-BDA3-88242DE8D8EC}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/launcher/MRCC.Launcher/App.xaml b/launcher/MRCC.Launcher/App.xaml
new file mode 100644
index 0000000..4e40b88
--- /dev/null
+++ b/launcher/MRCC.Launcher/App.xaml
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/launcher/MRCC.Launcher/App.xaml.cs b/launcher/MRCC.Launcher/App.xaml.cs
new file mode 100644
index 0000000..b9a5d57
--- /dev/null
+++ b/launcher/MRCC.Launcher/App.xaml.cs
@@ -0,0 +1,7 @@
+using System.Windows;
+
+namespace MRCC.Launcher;
+
+public partial class App : Application
+{
+}
diff --git a/launcher/MRCC.Launcher/MRCC.Launcher.csproj b/launcher/MRCC.Launcher/MRCC.Launcher.csproj
new file mode 100644
index 0000000..f7c8643
--- /dev/null
+++ b/launcher/MRCC.Launcher/MRCC.Launcher.csproj
@@ -0,0 +1,31 @@
+
+
+
+ WinExe
+ net10.0-windows
+ true
+ true
+ MRCC.Launcher
+ MRCC.Launcher
+ enable
+ 14.0
+ enable
+ app.manifest
+ MRCC.Launcher.Program
+
+
+
+
+
+
+
+
+
+
+
+
+ PCL.metadata.json
+
+
+
+
diff --git a/launcher/MRCC.Launcher/MainWindow.xaml b/launcher/MRCC.Launcher/MainWindow.xaml
new file mode 100644
index 0000000..f0fc1a7
--- /dev/null
+++ b/launcher/MRCC.Launcher/MainWindow.xaml
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/launcher/MRCC.Launcher/MainWindow.xaml.cs b/launcher/MRCC.Launcher/MainWindow.xaml.cs
new file mode 100644
index 0000000..82d13c4
--- /dev/null
+++ b/launcher/MRCC.Launcher/MainWindow.xaml.cs
@@ -0,0 +1,29 @@
+using System.Windows;
+using System.Windows.Controls;
+
+namespace MRCC.Launcher;
+
+public partial class MainWindow : Window
+{
+ public MainWindow()
+ {
+ InitializeComponent();
+ }
+
+ private void OnNavChecked(object sender, RoutedEventArgs e)
+ {
+ if (sender is not RadioButton rb) return;
+ var tag = rb.Tag?.ToString() ?? "home";
+
+ PageHome.Visibility = tag == "home" ? Visibility.Visible : Visibility.Collapsed;
+ PageDownload.Visibility = tag == "download" ? Visibility.Visible : Visibility.Collapsed;
+ PageSettings.Visibility = tag == "settings" ? Visibility.Visible : Visibility.Collapsed;
+ PageAbout.Visibility = tag == "about" ? Visibility.Visible : Visibility.Collapsed;
+ }
+
+ private void OnLaunchGame(object sender, RoutedEventArgs e)
+ {
+ // TODO: 启动 MRCC Unity 客户端
+ MessageBox.Show("游戏启动功能开发中", "MRCC Launcher", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+}
diff --git a/launcher/MRCC.Launcher/Program.cs b/launcher/MRCC.Launcher/Program.cs
new file mode 100644
index 0000000..d4c0f80
--- /dev/null
+++ b/launcher/MRCC.Launcher/Program.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Windows;
+using PCL.Core.App.Essentials;
+using PCL.Core.App.IoC;
+
+namespace MRCC.Launcher;
+
+public static class Program
+{
+ [STAThread]
+ public static void Main(string[] args)
+ {
+ // 设置 WPF Application 加载委托
+ ApplicationService.Loading = () =>
+ {
+ var app = new App();
+ return app;
+ };
+
+ // 设置主窗口加载委托
+ MainWindowService.Loading = () => new MainWindow();
+
+ // 启动生命周期
+ Lifecycle.OnInitialize();
+ }
+}
diff --git a/launcher/MRCC.Launcher/ViewModels/MainViewModel.cs b/launcher/MRCC.Launcher/ViewModels/MainViewModel.cs
new file mode 100644
index 0000000..ed09556
--- /dev/null
+++ b/launcher/MRCC.Launcher/ViewModels/MainViewModel.cs
@@ -0,0 +1,29 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace MRCC.Launcher.ViewModels;
+
+///
+/// 主窗口 ViewModel,管理导航状态与页面数据。
+///
+public partial class MainViewModel : ObservableObject
+{
+ [ObservableProperty]
+ private string _versionText = "v1.0.0-dev";
+
+ [ObservableProperty]
+ private string _statusText = "就绪";
+
+ [ObservableProperty]
+ private bool _isGameInstalled;
+
+ [ObservableProperty]
+ private int _downloadProgress;
+
+ ///
+ /// 启动游戏命令(后续实现具体逻辑)
+ ///
+ public void LaunchGame()
+ {
+ // TODO: 检查游戏安装状态 -> 启动 Unity 客户端
+ }
+}
diff --git a/launcher/MRCC.Launcher/app.manifest b/launcher/MRCC.Launcher/app.manifest
new file mode 100644
index 0000000..0251d78
--- /dev/null
+++ b/launcher/MRCC.Launcher/app.manifest
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/launcher/MRCC.Launcher/metadata.json b/launcher/MRCC.Launcher/metadata.json
new file mode 100644
index 0000000..3653787
--- /dev/null
+++ b/launcher/MRCC.Launcher/metadata.json
@@ -0,0 +1,17 @@
+{
+ "name": "MRCC Launcher",
+ "version": {
+ "base": "1.0.0",
+ "upstream": "",
+ "suffix": "dev",
+ "code": 1
+ },
+ "licenses": [
+ {
+ "name": "PCL.Core",
+ "info": "PCL Community 启动器核心库",
+ "website": "https://github.com/PCL-Community/PCL-CE",
+ "license": "https://www.apache.org/licenses/LICENSE-2.0"
+ }
+ ]
+}
diff --git a/launcher/PCL-CE/.editorconfig b/launcher/PCL-CE/.editorconfig
new file mode 100644
index 0000000..c62d9e1
--- /dev/null
+++ b/launcher/PCL-CE/.editorconfig
@@ -0,0 +1,4 @@
+[*.cs]
+
+# IDE0028: 简化集合初始化
+dotnet_style_collection_initializer = true:silent
diff --git a/launcher/PCL-CE/.gitattributes b/launcher/PCL-CE/.gitattributes
new file mode 100644
index 0000000..1ff0c42
--- /dev/null
+++ b/launcher/PCL-CE/.gitattributes
@@ -0,0 +1,63 @@
+###############################################################################
+# Set default behavior to automatically normalize line endings.
+###############################################################################
+* text=auto
+
+###############################################################################
+# Set default behavior for command prompt diff.
+#
+# This is need for earlier builds of msysgit that does not have it on by
+# default for csharp files.
+# Note: This is only used by command line
+###############################################################################
+#*.cs diff=csharp
+
+###############################################################################
+# Set the merge driver for project and solution files
+#
+# Merging from the command prompt will add diff markers to the files if there
+# are conflicts (Merging from VS is not affected by the settings below, in VS
+# the diff markers are never inserted). Diff markers may cause the following
+# file extensions to fail to load in VS. An alternative would be to treat
+# these files as binary and thus will always conflict and require user
+# intervention with every merge. To do so, just uncomment the entries below
+###############################################################################
+#*.sln merge=binary
+#*.csproj merge=binary
+#*.vbproj merge=binary
+#*.vcxproj merge=binary
+#*.vcproj merge=binary
+#*.dbproj merge=binary
+#*.fsproj merge=binary
+#*.lsproj merge=binary
+#*.wixproj merge=binary
+#*.modelproj merge=binary
+#*.sqlproj merge=binary
+#*.wwaproj merge=binary
+
+###############################################################################
+# behavior for image files
+#
+# image files are treated as binary by default.
+###############################################################################
+#*.jpg binary
+#*.png binary
+#*.gif binary
+
+###############################################################################
+# diff behavior for common document formats
+#
+# Convert binary document formats to text before diffing them. This feature
+# is only available from the command line. Turn it on by uncommenting the
+# entries below.
+###############################################################################
+#*.doc diff=astextplain
+#*.DOC diff=astextplain
+#*.docx diff=astextplain
+#*.DOCX diff=astextplain
+#*.dot diff=astextplain
+#*.DOT diff=astextplain
+#*.pdf diff=astextplain
+#*.PDF diff=astextplain
+#*.rtf diff=astextplain
+#*.RTF diff=astextplain
diff --git a/launcher/PCL-CE/.github/FUNDING.yml b/launcher/PCL-CE/.github/FUNDING.yml
new file mode 100644
index 0000000..f4820c8
--- /dev/null
+++ b/launcher/PCL-CE/.github/FUNDING.yml
@@ -0,0 +1,3 @@
+# These are supported funding model platforms
+
+custom: ['https://afdian.com/a/LTCat']
diff --git a/launcher/PCL-CE/.github/ISSUE_TEMPLATE/1-bug.yml b/launcher/PCL-CE/.github/ISSUE_TEMPLATE/1-bug.yml
new file mode 100644
index 0000000..f9c0e5c
--- /dev/null
+++ b/launcher/PCL-CE/.github/ISSUE_TEMPLATE/1-bug.yml
@@ -0,0 +1,52 @@
+name: "[旧版] 综合 Bug 反馈 / [Legacy] General Bug Report"
+description: "遇见了与启动器功能相关的 Bug / Report a bug related to launcher features"
+type: "Bug"
+body:
+ - type: checkboxes
+ id: "yml-1"
+ attributes:
+ label: "检查项 / Checklist"
+ description: "请逐个检查下列项目,并勾选确认 / Please review each item below and check the boxes to confirm."
+ options:
+ - label: "我已在 [Issues 页面](https://github.com/PCL-Community/PCL-CE/issues?q=is%3Aissue+) 中搜索,确认了这一 Bug 未被提交过 / I have searched the [Issues page](https://github.com/PCL-Community/PCL-CE/issues?q=is%3Aissue+) and confirmed that this bug has not been reported yet."
+ required: true
+ - label: "我确认只是全部选中而没有[仔细确认](https://github.com/PCL-Community/PCL-CE/issues?q=is%3Aissue+)就直接提交了这个 Issue,并且同意这个 Issue 可以直接被关闭 / I confirm that I checked all boxes without carefully reading them, and agree that this issue may be closed directly."
+ required: false
+ - label: "我确认正在使用当前更新分支的最新版 PCL CE,若不是,此 Issue 可能会被直接关闭(更新方式:启动器 `设置-关于-软件更新` 检查更新,或在 [Releases](https://github.com/PCL-Community/PCL-CE/releases) 页面下载) / I confirm that I am using the latest PCL CE version on the current update channel. If not, this issue may be closed directly. To update, check for updates in `Settings → About → Updates`, or download the latest version from the [Releases page](https://github.com/PCL-Community/PCL-CE/releases)."
+ required: true
+ - label: "我确认会详细描述我所遇到的问题,并附上日志和截图,而不是随便填点东西敷衍了事 / I will describe the problem in detail, attach logs and screenshots, and avoid submitting vague or incomplete information."
+ required: true
+ - type: textarea
+ id: "yml-2"
+ attributes:
+ label: "描述 / Description"
+ description: "详细描述该 Bug 的具体表现 / Describe the specific behavior of the bug in detail."
+ validations:
+ required: true
+ - type: textarea
+ id: "yml-3"
+ attributes:
+ label: "重现步骤 / Steps to Reproduce"
+ description: "详细描述要怎么操作才能再次触发这个 Bug / Describe the exact steps needed to reproduce this bug."
+ placeholder: |
+ 示例 / Example:
+ 1、点击 xxxx / 1. Click xxxx
+ 2、往下滚,然后点击 xxxx / 2. Scroll down, then click xxxx
+ validations:
+ required: true
+ - type: textarea
+ id: "yml-4"
+ attributes:
+ label: "日志与附件 / Logs & Attachments"
+ description: "在问题发生后立即使用 `设置-查看日志-导出日志` 并上传导出的压缩包。**若手动关掉了启动器或启动器直接崩溃,请重启之后使用 `导出全部日志` 而不是 `导出日志`,否则导出的日志可能不包含故障关键信息。** / After the issue occurs, immediately use `Settings → Logs → Export log` and upload the exported zip file. **If you manually closed the launcher, or if the launcher crashed, restart PCL CE and use `Export all logs` instead of `Export log`; otherwise, the exported logs may not contain key information about the issue.**"
+ placeholder: |
+ 先点击这个文本框,然后再将文件直接拖拽到文本框中以上传。
+ 如果有相关截图或文件,也请一并上传。
+ 请勿自行判断是否需要日志,日志文件并不仅仅包含发生错误的信息,还可能包含其它关键信息。
+ 如果缺少相关日志用于分析问题,此 Issue 可能会被直接关闭。
+ Click this text box first, then drag and drop files here to upload.
+ Please also upload any relevant screenshots or files.
+ Do not decide on your own whether logs are needed. Logs may contain not only error messages, but other key information as well.
+ If the required logs are missing, this issue may be closed directly.
+ validations:
+ required: true
diff --git a/launcher/PCL-CE/.github/ISSUE_TEMPLATE/2-game_crash.yml b/launcher/PCL-CE/.github/ISSUE_TEMPLATE/2-game_crash.yml
new file mode 100644
index 0000000..0a99dba
--- /dev/null
+++ b/launcher/PCL-CE/.github/ISSUE_TEMPLATE/2-game_crash.yml
@@ -0,0 +1,47 @@
+name: "[旧版] Minecraft 崩溃 / [Legacy] Minecraft Crash"
+description: "PCL CE 提示 “Minecraft 出现错误”,或游戏崩溃 / PCL CE shows 'Minecraft encountered an error', or Minecraft crashes"
+type: "崩溃"
+body:
+ - type: checkboxes
+ id: "yml-1"
+ attributes:
+ label: "检查项 / Checklist"
+ description: "请逐个检查下列项目,并勾选确认。/ Please review each item below and check the boxes to confirm."
+ options:
+ - label: "**我已尝试使用 [HMCL](https://hmcl.huangyuhui.net/download) 启动,HMCL 没有出现问题**(如果 HMCL 也无法启动就一般不是 PCL CE 导致的问题,请 **不要** 提交反馈) / **I have tried launching with [HMCL](https://hmcl.huangyuhui.net/download), and HMCL works without issues**. If HMCL also fails to launch, the problem is usually not caused by PCL CE, so please **do not** submit feedback."
+ required: true
+ - label: "我已在 [Issues 页面](https://github.com/PCL-Community/PCL-CE/issues?q=is%3Aissue+) 中搜索,确认了这一 Bug 未被提交过 / I have searched the [Issues page](https://github.com/PCL-Community/PCL-CE/issues?q=is%3Aissue+) and confirmed that this bug has not been reported yet."
+ required: true
+ - label: "我确认只是全部选中而没有[仔细确认](https://github.com/PCL-Community/PCL-CE/issues?q=is%3Aissue+)就直接提交了这个 Issue,并且同意这个 Issue 可以直接被关闭 / I confirm that I checked all boxes without carefully reading them, and agree that this issue may be closed directly."
+ required: false
+ - label: "我确认正在使用当前更新分支的最新版 PCL CE,若不是,此 Issue 可能会被直接关闭(更新方式:启动器 `设置-关于-软件更新` 检查更新,或在 [Releases](https://github.com/PCL-Community/PCL-CE/releases) 页面下载) / I confirm that I am using the latest PCL CE version on the current update channel. If not, this issue may be closed directly. To update, check for updates in `Settings → About → Updates`, or download the latest version from [Releases](https://github.com/PCL-Community/PCL-CE/releases)."
+ required: true
+ - label: "我确认会详细描述我所遇到的问题,并附上日志和截图,而不是随便填点东西敷衍了事。/ I will describe the issue in detail, attach logs and screenshots, and avoid submitting vague or incomplete information."
+ required: true
+ - type: textarea
+ id: "yml-2"
+ attributes:
+ label: "描述 / Description"
+ description: "详细描述具体表现。/ Describe the specific behavior in detail."
+ validations:
+ required: true
+ - type: textarea
+ id: "yml-3"
+ attributes:
+ label: "PCL CE 错误报告、日志与附件 / PCL CE Error Report, Logs & Attachments"
+ description: "上传 PCL CE 提供的错误报告(在崩溃时选择导出错误报告)。如果没有,请在游戏崩溃后不要关掉启动器,使用 `设置-查看日志-导出日志` 得到日志压缩包并上传。/ Upload the error report provided by PCL CE by choosing to export the error report when the crash occurs. If it is unavailable, do not close the launcher after Minecraft crashes; use `Settings → Logs → Export log` and upload the exported log archive."
+ placeholder: |
+ 先点击这个文本框,然后再将文件直接拖拽到文本框中以上传。
+ 如果缺少相关错误报告或日志用于分析问题,此 Issue 可能会被直接关闭。
+ Click this text box first, then drag and drop files here to upload.
+ If the required error report or logs are missing, this issue may be closed directly.
+ validations:
+ required: true
+ - type: textarea
+ id: "yml-4"
+ attributes:
+ label: "HMCL 启动脚本 / HMCL Launch Script"
+ description: "在 HMCL 中进入实例列表,点击版本右侧的三个点,选择“生成启动脚本”。/ In HMCL, open the instance list, click the three dots next to the version, and select `Generate Launch Script`."
+ placeholder: "先点击这个文本框,然后再将文件直接拖拽到文本框中以上传。/ Click this text box first, then drag and drop the file here to upload."
+ validations:
+ required: false
diff --git a/launcher/PCL-CE/.github/ISSUE_TEMPLATE/21-bug.yml b/launcher/PCL-CE/.github/ISSUE_TEMPLATE/21-bug.yml
new file mode 100644
index 0000000..ba604d6
--- /dev/null
+++ b/launcher/PCL-CE/.github/ISSUE_TEMPLATE/21-bug.yml
@@ -0,0 +1,67 @@
+name: "[新版] 综合 Bug 反馈 / [New] General Bug Report"
+description: "遇见了与新版 PCL CE 启动器功能相关的 Bug / Report a bug related to launcher features in the new PCL CE version"
+title: "[C#]: "
+type: "Bug"
+body:
+ - type: markdown
+ id: "desc"
+ attributes:
+ value: |
+ ## 感谢参与 PCL CE 新版本测试!
+ 请注意您目前正在提交和新版本有关的综合 Bug 反馈。
+ 如果您使用的不是新版本 (2.15.x),请到 [旧版综合 Bug 反馈区](https://github.com/PCL-Community/PCL-CE/issues/new?template=1-bug.yml) 提交反馈。
+
+ ## Thank you for testing the new PCL CE version!
+ Please note that you are submitting a general bug report for the new version.
+ If you are not using the new version (2.15.x), please use the [legacy general bug report template](https://github.com/PCL-Community/PCL-CE/issues/new?template=1-bug.yml).
+ - type: checkboxes
+ id: "yml-1"
+ attributes:
+ label: "检查项 / Checklist"
+ description: "请逐个检查下列项目,并勾选确认 / Please review each item below and check the boxes to confirm."
+ options:
+ - label: "我已在 [Issues 页面](https://github.com/PCL-Community/PCL-CE/issues?q=is%3Aissue+) 中搜索,确认了这一 Bug 未被提交过 / I have searched the [Issues page](https://github.com/PCL-Community/PCL-CE/issues?q=is%3Aissue+) and confirmed that this bug has not been reported yet."
+ required: true
+ - label: "我确认只是全部选中而没有[仔细确认](https://github.com/PCL-Community/PCL-CE/issues?q=is%3Aissue+)就直接提交了这个 Issue,并且同意这个 Issue 可以直接被关闭 / I confirm that I checked all boxes without carefully reading them, and agree that this issue may be closed directly."
+ required: false
+ - label: "我确认正在使用当前更新分支的最新版 PCL CE,若不是,此 Issue 可能会被直接关闭(更新方式:启动器 `设置-关于-软件更新` 检查更新,或在 [Releases](https://github.com/PCL-Community/PCL-CE/releases) 页面下载) / I confirm that I am using the latest PCL CE version on the current update channel. If not, this issue may be closed directly. To update, check for updates in `Settings → About → Updates`, or download the latest version from the [Releases page](https://github.com/PCL-Community/PCL-CE/releases)."
+ required: true
+ - label: "我确认会详细描述我所遇到的问题,并附上日志和截图,而不是随便填点东西敷衍了事 / I will describe the problem in detail, attach logs and screenshots, and avoid submitting vague or incomplete information."
+ required: true
+ - type: textarea
+ id: "yml-2"
+ attributes:
+ label: "描述 / Description"
+ description: "详细描述该 Bug 的具体表现 / Describe the specific behavior of the bug in detail."
+ validations:
+ required: true
+ - type: textarea
+ id: "yml-3"
+ attributes:
+ label: "重现步骤 / Steps to Reproduce"
+ description: "详细描述要怎么操作才能再次触发这个 Bug / Describe the exact steps needed to reproduce this bug."
+ placeholder: |
+ 示例:
+ 1、点击 xxxx
+ 2、往下滚,然后点击 xxxx
+ Example:
+ 1. Click xxxx
+ 2. Scroll down, then click xxxx
+ validations:
+ required: true
+ - type: textarea
+ id: "yml-4"
+ attributes:
+ label: "日志与附件 / Logs & Attachments"
+ description: "在问题发生后立即使用 `设置-查看日志-导出日志` 并上传导出的压缩包。**若手动关掉了启动器或启动器直接崩溃,请重启之后使用 `导出全部日志` 而不是 `导出日志`,否则导出的日志可能不包含故障关键信息。** / After the issue occurs, immediately use `Settings → Logs → Export log` and upload the exported zip file. **If you manually closed the launcher, or if the launcher crashed, restart PCL CE and use `Export all logs` instead of `Export log`; otherwise, the exported logs may not contain key information about the issue.**"
+ placeholder: |
+ 先点击这个文本框,然后再将文件直接拖拽到文本框中以上传。
+ 如果有相关截图或文件,也请一并上传。
+ 请勿自行判断是否需要日志,日志文件并不仅仅包含发生错误的信息,还可能包含其它关键信息。
+ 如果缺少相关日志用于分析问题,此 Issue 可能会被直接关闭。
+ Click this text box first, then drag and drop files here to upload.
+ Please also upload any relevant screenshots or files.
+ Do not decide on your own whether logs are needed. Logs may contain not only error messages, but other key information as well.
+ If the required logs are missing, this issue may be closed directly.
+ validations:
+ required: true
diff --git a/launcher/PCL-CE/.github/ISSUE_TEMPLATE/22-game_crash.yml b/launcher/PCL-CE/.github/ISSUE_TEMPLATE/22-game_crash.yml
new file mode 100644
index 0000000..05945d8
--- /dev/null
+++ b/launcher/PCL-CE/.github/ISSUE_TEMPLATE/22-game_crash.yml
@@ -0,0 +1,59 @@
+name: "[新版] Minecraft 崩溃 / [New] Minecraft Crash"
+description: "新版 PCL CE 提示 “Minecraft 出现错误”,或游戏崩溃 / The new PCL CE version shows 'Minecraft encountered an error', or Minecraft crashes"
+title: "[C#]: "
+type: "崩溃"
+body:
+ - type: markdown
+ id: "desc"
+ attributes:
+ value: |
+ ## 感谢参与 PCL CE 新版本测试!
+ 请注意您目前正在提交和新版本有关的综合 Bug 反馈。
+ 如果您使用的不是新版本 (2.15.x),请到 [旧版综合 Bug 反馈区](https://github.com/PCL-Community/PCL-CE/issues/new?template=1-bug.yml) 提交反馈。
+
+ ## Thank you for testing the new PCL CE version!
+ Please note that you are submitting a general bug report for the new version.
+ If you are not using the new version (2.15.x), please use the [legacy general bug report template](https://github.com/PCL-Community/PCL-CE/issues/new?template=1-bug.yml).
+ - type: checkboxes
+ id: "yml-1"
+ attributes:
+ label: "检查项 / Checklist"
+ description: "请逐个检查下列项目,并勾选确认 / Please review each item below and check the boxes to confirm."
+ options:
+ - label: "**我已尝试使用 [HMCL](https://hmcl.huangyuhui.net/download) 启动,HMCL 没有出现问题**(如果 HMCL 也无法启动就一般不是 PCL CE 导致的问题,请 **不要** 提交反馈) / **I have tried launching with [HMCL](https://hmcl.huangyuhui.net/download), and HMCL works without issues**. If HMCL also fails to launch, the problem is usually not caused by PCL CE, so please **do not** submit feedback."
+ required: true
+ - label: "我已在 [Issues 页面](https://github.com/PCL-Community/PCL-CE/issues?q=is%3Aissue+) 中搜索,确认了这一 Bug 未被提交过 / I have searched the [Issues page](https://github.com/PCL-Community/PCL-CE/issues?q=is%3Aissue+) and confirmed that this bug has not been reported yet."
+ required: true
+ - label: "我确认只是全部选中而没有[仔细确认](https://github.com/PCL-Community/PCL-CE/issues?q=is%3Aissue+)就直接提交了这个 Issue,并且同意这个 Issue 可以直接被关闭 / I confirm that I checked all boxes without carefully reading them, and agree that this issue may be closed directly."
+ required: false
+ - label: "我确认正在使用当前更新分支的最新版 PCL CE,若不是,此 Issue 可能会被直接关闭(更新方式:启动器 `设置-关于-软件更新` 检查更新,或在 [Releases](https://github.com/PCL-Community/PCL-CE/releases) 页面下载) / I confirm that I am using the latest PCL CE version on the current update channel. If not, this issue may be closed directly. To update, check for updates in `Settings → About → Updates`, or download the latest version from the [Releases page](https://github.com/PCL-Community/PCL-CE/releases)."
+ required: true
+ - label: "我确认会详细描述我所遇到的问题,并附上日志和截图,而不是随便填点东西敷衍了事。/ I will describe the problem in detail, attach logs and screenshots, and avoid submitting vague or incomplete information."
+ required: true
+ - type: textarea
+ id: "yml-2"
+ attributes:
+ label: "描述 / Description"
+ description: "详细描述具体表现 / Describe the specific behavior in detail."
+ validations:
+ required: true
+ - type: textarea
+ id: "yml-3"
+ attributes:
+ label: "PCL CE 错误报告、日志与附件 / PCL CE Error Report, Logs & Attachments"
+ description: "上传 PCL CE 提供的错误报告 (在崩溃时选择导出错误报告)。如果没有,请在游戏崩溃后不要关掉启动器,使用 `设置-查看日志-导出日志` 得到日志压缩包并上传 / Upload the error report provided by PCL CE by choosing to export the error report when the crash occurs. If it is unavailable, do not close the launcher after Minecraft crashes; use `Settings → Logs → Export log` and upload the exported log archive."
+ placeholder: |
+ 先点击这个文本框,然后再将文件直接拖拽到文本框中以上传。
+ 如果缺少相关错误报告或日志用于分析问题,此 Issue 可能会被直接关闭。
+ Click this text box first, then drag and drop files here to upload.
+ If the required error report or logs are missing, this issue may be closed directly.
+ validations:
+ required: true
+ - type: textarea
+ id: "yml-4"
+ attributes:
+ label: "HMCL 启动脚本 / HMCL Launch Script"
+ description: "在 HMCL 中进入实例列表,点击版本右侧的三个点,选择“生成启动脚本” / In HMCL, open the instance list, click the three dots next to the version, and select `Generate Launch Script`."
+ placeholder: "先点击这个文本框,然后再将文件直接拖拽到文本框中以上传 / Click this text box first, then drag and drop the file here to upload."
+ validations:
+ required: false
diff --git a/launcher/PCL-CE/.github/ISSUE_TEMPLATE/23-feature.yml.disabled b/launcher/PCL-CE/.github/ISSUE_TEMPLATE/23-feature.yml.disabled
new file mode 100644
index 0000000..f199189
--- /dev/null
+++ b/launcher/PCL-CE/.github/ISSUE_TEMPLATE/23-feature.yml.disabled
@@ -0,0 +1,44 @@
+name: "[新版] 新功能提案 / [New Version] Feature Proposal"
+description: "对新版本 PCL CE 已有功能的大幅度修改,或添加一个新内容或选项。/ Propose a major change to an existing feature in the new PCL CE version, or suggest a new feature or option."
+title: "[C#]: "
+type: "新功能"
+body:
+ - type: checkboxes
+ id: "yml-1"
+ attributes:
+ label: "检查项 / Checklist"
+ description: "请逐个检查下列项目,并勾选确认。/ Please review each item below and check the boxes to confirm."
+ options:
+ - label: "我已在 [Issues 页面](https://github.com/PCL-Community/PCL-CE/issues?q=is%3Aissue+) 中搜索,确认了这一提案未在社区版被提交过 / I have searched the [Issues page](https://github.com/PCL-Community/PCL-CE/issues?q=is%3Aissue+) and confirmed that this proposal has not been submitted to the Community Edition yet."
+ required: true
+ - label: "我确认只是全部选中而没有[仔细确认](https://github.com/PCL-Community/PCL-CE/issues?q=is%3Aissue+)就直接提交了这个 Issue,并且同意这个 Issue 可以直接被关闭 / I confirm that I checked all boxes without carefully reading them, and agree that this issue may be closed without further notice."
+ required: false
+ - label: "我确认我的提案是对某一个具体功能,而不是一堆功能要求挤在一个 Issue 里 / I confirm that this proposal focuses on a single specific feature, rather than multiple unrelated feature requests in one issue."
+ required: true
+ - label: "我已知悉本项目的开发者均为不带薪志愿者,社区不对这类 Issue 的完成时间作出任何保证,并有足够的耐心等待社区开发者的处理,同时已知悉如果我作出催促、质问或类似的举动,这个 Issue 可能会被直接关闭 / I understand that all developers of this project are unpaid volunteers. The community does not guarantee any completion time for this type of issue, and I am willing to wait patiently for review and implementation. I also understand that excessive urging, demanding, or similar behavior may result in this issue being closed."
+ required: true
+ - type: input
+ id: "yml-2"
+ attributes:
+ label: "官方版原 Issue 编号 / Original Official Issue Number"
+ description: "如果这个 Issue 是从官方版转交过来的,请将原 Issue 标题旁的**编号**输入到下方预设文本的 `#` 后,不要输入任何其他非数字字符。**若不是,请清空下方输入框。** / If this issue was transferred from the official version, enter only the **number** shown next to the original Issue title after the `#` below. Do not enter any non-numeric characters. **If not applicable, leave this field blank.**"
+ value: "- Meloong-Git/PCL#"
+ - type: textarea
+ id: "yml-3"
+ attributes:
+ label: "描述 / Description"
+ description: "详细描述你想添加的功能具体是怎样的。/ Describe in detail the feature you would like to add."
+ validations:
+ required: true
+ - type: textarea
+ id: "yml-4"
+ attributes:
+ label: "原因 / Reason"
+ description: "详细描述你为什么需要这项功能,这有助于开发者评估它的优先度。/ Explain why this feature is needed. This helps developers evaluate its priority."
+ placeholder: |
+ 示例:
+ 我需要这个功能来 xxxx,如果没有这个功能,就不能 xxxx 了。
+ Example:
+ I need this feature to xxxx. Without it, I would not be able to xxxx.
+ validations:
+ required: false
diff --git a/launcher/PCL-CE/.github/ISSUE_TEMPLATE/24-improve.yml b/launcher/PCL-CE/.github/ISSUE_TEMPLATE/24-improve.yml
new file mode 100644
index 0000000..d436147
--- /dev/null
+++ b/launcher/PCL-CE/.github/ISSUE_TEMPLATE/24-improve.yml
@@ -0,0 +1,49 @@
+name: "[新版] 优化建议 / [New Version] Improvement Suggestion"
+description: "对新版 PCL CE 已有功能的小幅度优化或改进建议 / Suggest a minor improvement to an existing feature in the new PCL CE version."
+title: "[C#]: "
+type: "优化"
+body:
+ - type: markdown
+ id: "desc"
+ attributes:
+ value: |
+ ## 感谢参与 PCL CE 新版本测试!
+ 请注意您目前正在提交和新版本有关的优化建议。
+ 由于我们目前不受理针对旧版本的优化建议,如果您使用的不是新版本 (2.15.x),
+ 您可以尝试更新到 2.15.x 版本 (Beta 渠道),或是在晚一些 2.15.0 推送到正式版之后再来提交反馈
+
+ ## Thank you for testing the new PCL CE version!
+ Please note that you are submitting an improvement suggestion for the new version.
+ Since we currently do not accept improvement suggestions for the legacy version, if you are not using the new version (2.15.x), you can update to 2.15.x on the Beta channel, or submit feedback after 2.15.0 is released to the stable channel.
+ - type: checkboxes
+ id: "yml-1"
+ attributes:
+ label: "检查项 / Checklist"
+ description: "请逐个检查下列项目,并勾选确认 / Please review each item below and check the boxes to confirm."
+ options:
+ - label: "我已在 [Issues 页面](https://github.com/PCL-Community/PCL-CE/issues?q=is%3Aissue+) 中搜索,确认了这一建议未在社区版被提交过 / I have searched the [Issues page](https://github.com/PCL-Community/PCL-CE/issues?q=is%3Aissue+) and confirmed that this suggestion has not been submitted to the Community Edition yet."
+ required: true
+ - label: "我确认只是全部选中而没有[仔细确认](https://github.com/PCL-Community/PCL-CE/issues?q=is%3Aissue+)就直接提交了这个 Issue,并且同意这个 Issue 可以直接被关闭 / I confirm that I checked all boxes without carefully reading them, and agree that this issue may be closed directly."
+ required: false
+ - label: "我确认我所描述的改进是现实的、有理有据的,而不是我临时起意的 / I confirm that the improvement I describe is realistic, reasonable, and well-justified, rather than something I came up with on a whim."
+ required: true
+ - label: "我已知悉社区不对这类 Issue 的完成时间作出任何保证,并有足够的耐心等待社区开发者的处理,同时已知悉如果我作出催促、质问或类似的举动,这个 Issue 可能会被直接关闭 / I understand that the community does not guarantee any completion time for this type of issue, and I am willing to wait patiently for the community developers' review and handling. I also understand that excessive urging, demanding, or similar behavior may result in this issue being closed."
+ required: true
+ - type: textarea
+ id: "yml-2"
+ attributes:
+ label: 描述 / Description
+ description: "详细描述具体需要优化哪些地方,要改成怎样的 / Describe in detail what needs to be improved and how it should be changed."
+ validations:
+ required: true
+ - type: textarea
+ id: "yml-3"
+ attributes:
+ label: 原因 / Reason
+ description: "详细描述你为什么需要这项优化,这有助于开发者评估它的优先度 / Explain why this improvement is needed. This helps developers evaluate its priority."
+ placeholder: |
+ 示例 / Example:
+ 这项优化让我可以更方便地 xxxx / This improvement would make it easier for me to xxxx.
+ 如果没有这项优化,我每次都必须 xxxx,让操作变得很麻烦 / Without this improvement, I have to xxxx every time, making the process cumbersome.
+ validations:
+ required: false
diff --git a/launcher/PCL-CE/.github/ISSUE_TEMPLATE/3-feature.yml.disabled b/launcher/PCL-CE/.github/ISSUE_TEMPLATE/3-feature.yml.disabled
new file mode 100644
index 0000000..479969e
--- /dev/null
+++ b/launcher/PCL-CE/.github/ISSUE_TEMPLATE/3-feature.yml.disabled
@@ -0,0 +1,37 @@
+name: "[旧版] 新功能提案 / [Legacy] Feature Proposal"
+description: "对已有功能的大幅度修改,或添加一个新内容或选项。/ Propose a major change to an existing feature, or suggest a new feature or option."
+type: "新功能"
+body:
+ - type: checkboxes
+ id: "yml-1"
+ attributes:
+ label: "检查项 / Checklist"
+ description: "请逐个检查下列项目,并勾选确认 / Please review each item below and check the boxes to confirm."
+ options:
+ - label: "我已在 [Issues 页面](https://github.com/PCL-Community/PCL-CE/issues?q=is%3Aissue+) 中搜索,确认了这一提案未在社区版被提交过 / I have searched the [Issues page](https://github.com/PCL-Community/PCL-CE/issues?q=is%3Aissue+) and confirmed that this proposal has not been submitted to the Community Edition yet."
+ required: true
+ - label: "我确认只是全部选中而没有[仔细确认](https://github.com/PCL-Community/PCL-CE/issues?q=is%3Aissue+)就直接提交了这个 Issue,并且同意这个 Issue 可以直接被关闭 / I confirm that I checked all boxes without carefully reading them, and agree that this issue may be closed directly."
+ required: false
+ - label: "我确认我的提案是对某一个具体功能,而不是一堆功能要求挤在一个 Issue 里 / I confirm that this proposal focuses on a single specific feature, rather than multiple unrelated feature requests in one issue."
+ required: true
+ - label: "我已知悉本项目的开发者均为不带薪志愿者,社区不对这类 Issue 的完成时间作出任何保证,并有足够的耐心等待社区开发者的处理,同时已知悉如果我作出催促、质问或类似的举动,这个 Issue 可能会被直接关闭 / I understand that all developers of this project are unpaid volunteers. The community does not guarantee any completion time for this type of issue, and I am willing to wait patiently for the developers' review and handling. I also understand that excessive urging, demanding, or similar behavior may result in this issue being closed."
+ required: true
+ - type: textarea
+ id: "yml-2"
+ attributes:
+ label: "描述 / Description"
+ description: "详细描述你想添加的功能具体是怎样的 / Describe in detail the feature you would like to add."
+ validations:
+ required: true
+ - type: textarea
+ id: "yml-3"
+ attributes:
+ label: "原因 / Reason"
+ description: "详细描述你为什么需要这项功能,这有助于开发者评估它的优先度 / Explain why this feature is needed to help developers evaluate its priority."
+ placeholder: |
+ 示例:
+ 我需要这个功能来 xxxx,如果没有这个功能,就不能 xxxx 了。
+ Example:
+ I need this feature to xxxx. Without it, I would not be able to xxxx.
+ validations:
+ required: false
diff --git a/launcher/PCL-CE/.github/ISSUE_TEMPLATE/4-improve.yml.disabled b/launcher/PCL-CE/.github/ISSUE_TEMPLATE/4-improve.yml.disabled
new file mode 100644
index 0000000..02d68c7
--- /dev/null
+++ b/launcher/PCL-CE/.github/ISSUE_TEMPLATE/4-improve.yml.disabled
@@ -0,0 +1,45 @@
+name: "[旧版] 优化建议 / [Legacy] Improvement Suggestion"
+description: "对已有功能的小幅度优化或改进建议。/ Suggest a minor improvement to an existing feature."
+type: "优化"
+body:
+ - type: checkboxes
+ id: "yml-1"
+ attributes:
+ label: "检查项 / Checklist"
+ description: "请逐个检查下列项目,并勾选确认。/ Please review each item below and check the boxes to confirm."
+ options:
+ - label: "我已在 [Issues 页面](https://github.com/PCL-Community/PCL-CE/issues?q=is%3Aissue+) 中搜索,确认了这一建议未在社区版被提交过 / I have searched the [Issues page](https://github.com/PCL-Community/PCL-CE/issues?q=is%3Aissue+) and confirmed that this suggestion has not been submitted to the Community Edition yet."
+ required: true
+ - label: "我确认只是全部选中而没有[仔细确认](https://github.com/PCL-Community/PCL-CE/issues?q=is%3Aissue+)就直接提交了这个 Issue,并且同意这个 Issue 可以直接被关闭 / I confirm that I checked all boxes without carefully reading them, and agree that this issue may be closed directly."
+ required: false
+ - label: "我确认我所描述的改进是现实的、有理有据的,而不是我临时起意的 / I confirm that the improvement I describe is realistic, reasonable, and well-justified, rather than something I came up with on a whim."
+ required: true
+ - label: "我已知悉社区不对这类 Issue 的完成时间作出任何保证,并有足够的耐心等待社区开发者的处理,同时已知悉如果我作出催促、质问或类似的举动,这个 Issue 可能会被直接关闭 / I understand that the community does not guarantee any completion time for this type of issue, and I am willing to wait patiently for the developers' review and handling. I also understand that excessive urging, demanding, or similar behavior may result in this issue being closed."
+ required: true
+ - type: input
+ id: "yml-2"
+ attributes:
+ label: "官方版原 Issue 编号 / Original Official Issue Number"
+ description: "如果这个 Issue 是从官方版转交过来的,请将原 Issue 标题旁的**编号**输入到下方预设文本的 `#` 后,不要输入任何其他非数字字符。**若不是,请清空下方输入框。** / If this issue was transferred from the official version, enter only the **number** shown next to the original Issue title after the `#` below. Do not enter any non-numeric characters. **If not applicable, leave this field blank.**"
+ value: "- Meloong-Git/PCL#"
+ - type: textarea
+ id: "yml-3"
+ attributes:
+ label: "描述 / Description"
+ description: "详细描述具体需要优化哪些地方,要改成怎样的。/ Describe in detail what needs to be improved and how it should be changed."
+ validations:
+ required: true
+ - type: textarea
+ id: "yml-4"
+ attributes:
+ label: "原因 / Reason"
+ description: "详细描述你为什么需要这项优化,这有助于开发者评估它的优先度。/ Explain why this improvement is needed. This helps developers evaluate its priority."
+ placeholder: |
+ 示例:
+ 这项优化让我可以更方便地 xxxx。
+ 如果没有这项优化,我每次都必须 xxxx,让操作变得很麻烦。
+ Example:
+ This improvement would make it easier for me to xxxx.
+ Without this improvement, I have to xxxx every time, making the process cumbersome.
+ validations:
+ required: false
diff --git a/launcher/PCL-CE/.github/ISSUE_TEMPLATE/config.yml b/launcher/PCL-CE/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..f00810f
--- /dev/null
+++ b/launcher/PCL-CE/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,17 @@
+blank_issues_enabled: false
+contact_links:
+ - name: "旧版本优化反馈、新功能请求暂不受理 / Legacy Version Improvement Feedback and Feature Requests Are Not Accepted"
+ url: "https://github.com/PCL-Community/PCL-CE/issues/new/choose"
+ about: "由于开发重心已经转移到 C# 新版本,所以目前不计划再对旧版做优化和新功能了。/ Because development focus has shifted to the new C# version, we currently do not plan to optimize the legacy version or add new features to it. You can submit feature requests for the new version later."
+ - name: "新版本新功能请求暂不受理 / New Version Feature Requests Are Temporarily Not Accepted"
+ url: "https://github.com/PCL-Community/PCL-CE/issues/new/choose"
+ about: "由于 C# 新版本还不稳定,我们暂时不受理对新版本提出的新功能请求,请在晚一些时候再来。/ Because the new C# version is still unstable, we are temporarily not accepting feature requests for it. Please check back later."
+ - name: "主页预设反馈 / Homepage Preset Feedback"
+ url: https://github.com/Meloong-Git/PCL/discussions/categories/自定义主页
+ about: "提交与预设的主页(设置 → 个性化 → 主页预设)中的具体内容相关的反馈 / Submit feedback about specific content in homepage presets (Settings → Personalization → Homepage Presets)."
+ - name: "帮助文档反馈 / Help Documentation Feedback"
+ url: https://github.com/PCL-Community/docs.pclc.cc/issues
+ about: "提交与 PCL CE 帮助文档(独立文档站)中的具体内容相关的反馈 / Submit feedback about specific content in the PCL CE help documentation on the standalone documentation site."
+ - name: "提问 / 讨论 / Questions / Discussions"
+ url: https://github.com/PCL-Community/PCL-CE/discussions/new
+ about: "想问问或谈谈关于社区版的事情 / Ask questions or discuss topics about the Community Edition."
\ No newline at end of file
diff --git a/launcher/PCL-CE/.github/workflows/build-test.yml b/launcher/PCL-CE/.github/workflows/build-test.yml
new file mode 100644
index 0000000..7bbce95
--- /dev/null
+++ b/launcher/PCL-CE/.github/workflows/build-test.yml
@@ -0,0 +1,38 @@
+name: Build (CI)
+
+permissions:
+ contents: read
+
+on:
+ push:
+ branches: [dev]
+ paths-ignore: &ignored
+ - '**/.editorconfig'
+ - '**/.gitignore'
+ - '**/.gitattributes'
+ - '**/*.md'
+ - '**/LICENSE'
+ - '**/LICENCE'
+ - 'PCL.Core.Test/**'
+ - '.github/ISSUE_TEMPLATE/**'
+ - '.github/FUNDING.yml'
+ pull_request:
+ paths-ignore: *ignored
+ workflow_dispatch:
+
+jobs:
+ build:
+ strategy:
+ matrix:
+ include:
+ - configuration: CI
+ architecture: x64
+ - configuration: CI
+ architecture: ARM64
+ fail-fast: false
+
+ uses: ./.github/workflows/reusable-build.yml
+ with:
+ configuration: ${{ matrix.configuration }}
+ architecture: ${{ matrix.architecture }}
+ secrets: inherit
diff --git a/launcher/PCL-CE/.github/workflows/mirrorchyan_release_note.yml b/launcher/PCL-CE/.github/workflows/mirrorchyan_release_note.yml
new file mode 100644
index 0000000..4591484
--- /dev/null
+++ b/launcher/PCL-CE/.github/workflows/mirrorchyan_release_note.yml
@@ -0,0 +1,22 @@
+name: Mirror Chyan (Release Note)
+
+permissions:
+ contents: read
+
+on:
+ workflow_dispatch:
+ release:
+ types: [edited]
+
+jobs:
+ mirrorchyan:
+ runs-on: macos-latest
+
+ steps:
+ - id: uploading
+ uses: MirrorChyan/release-note-action@v1
+ with:
+ mirrorchyan_rid: PCL2-CE
+
+ upload_token: ${{ secrets.MirrorChyanUploadToken }}
+ github_token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/launcher/PCL-CE/.github/workflows/mirrorchyan_uploading.yml b/launcher/PCL-CE/.github/workflows/mirrorchyan_uploading.yml
new file mode 100644
index 0000000..3c7bcd2
--- /dev/null
+++ b/launcher/PCL-CE/.github/workflows/mirrorchyan_uploading.yml
@@ -0,0 +1,41 @@
+name: Mirror Chyan (Upload)
+
+permissions:
+ contents: read
+
+on:
+ workflow_dispatch:
+ inputs:
+ channel:
+ required: true
+ default: 'stable'
+ type: choice
+ options:
+ - stable
+ - beta
+ arch:
+ required: true
+ default: 'x64'
+ type: choice
+ options:
+ - x64
+ - arm64
+
+jobs:
+ mirrorchyan_uploading:
+ runs-on: macos-latest
+
+ steps:
+ - uses: MirrorChyan/uploading-action@v1
+ with:
+ filetype: latest-release
+ filename: "PCL2_CE_${{ inputs.channel == 'stable' && 'Release' || 'Beta' }}_${{ inputs.arch == 'x64' && 'x64' || 'ARM64' }}.exe"
+ mirrorchyan_rid: PCL2-CE
+
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ owner: PCL-Community
+ repo: PCL2-CE
+ upload_token: ${{ secrets.MirrorChyanUploadToken }}
+ os: win
+ arch: ${{ inputs.arch }}
+ channel: ${{ inputs.channel }}
diff --git a/launcher/PCL-CE/.github/workflows/release-beta_publish.yml b/launcher/PCL-CE/.github/workflows/release-beta_publish.yml
new file mode 100644
index 0000000..6d24ecc
--- /dev/null
+++ b/launcher/PCL-CE/.github/workflows/release-beta_publish.yml
@@ -0,0 +1,102 @@
+name: Publish (Beta)
+
+permissions:
+ contents: read
+
+on:
+ release:
+ types: [published]
+
+jobs:
+ build:
+ if: ${{ github.event.release.prerelease }}
+ strategy:
+ matrix:
+ include:
+ - configuration: Beta
+ architecture: x64
+ - configuration: Beta
+ architecture: ARM64
+ fail-fast: false
+
+ uses: ./.github/workflows/reusable-build.yml
+ with:
+ configuration: ${{ matrix.configuration }}
+ architecture: ${{ matrix.architecture }}
+ secrets: inherit
+
+ changelog:
+ permissions:
+ contents: write
+ runs-on: ubuntu-latest
+ continue-on-error: true
+ if: ${{ github.event.release.prerelease }}
+ steps:
+ - name: Checkout all
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+
+ - name: Install git-cliff
+ uses: taiki-e/install-action@git-cliff
+
+ - name: Generate changelog
+ run: git-cliff --latest -o CHANGELOG.md
+
+ - name: Add changelog to release
+ uses: softprops/action-gh-release@v2.5.0
+ with:
+ tag_name: ${{ github.event.release.tag_name }}
+ body_path: CHANGELOG.md
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ rename_and_release:
+ permissions:
+ contents: write
+ actions: write
+ needs: [build, changelog]
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ include:
+ - configuration: Beta
+ architecture: x64
+ - configuration: Beta
+ architecture: ARM64
+ steps:
+ - name: Download Build Artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: PCL2_CE_${{ matrix.configuration }}_${{ matrix.architecture }}
+ path: ./artifact
+
+ - name: Rename binaries
+ run: |
+ mv "./artifact/Plain Craft Launcher 2.exe" "PCL2_CE_${{ matrix.configuration }}_${{ matrix.architecture }}.exe"
+
+ - name: Import GPG key
+ uses: crazy-max/ghaction-import-gpg@v6
+ with:
+ gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
+ passphrase: ${{ secrets.GPG_PASSPHRASE }}
+
+ - name: Sign the binary
+ run: |
+ gpg --detach-sign --armor "PCL2_CE_${{ matrix.configuration }}_${{ matrix.architecture }}.exe"
+
+ - name: Upload binary and signature to Release
+ uses: softprops/action-gh-release@v2.2.2
+ with:
+ files: |
+ PCL2_CE_${{ matrix.configuration }}_${{ matrix.architecture }}.exe
+ PCL2_CE_${{ matrix.configuration }}_${{ matrix.architecture }}.exe.asc
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Trigger MirrorChyanUploading
+ run: |
+ gh workflow run --repo $GITHUB_REPOSITORY mirrorchyan_uploading.yml -f channel=beta -f arch=${{ matrix.architecture == 'x64' && 'x64' || 'arm64' }}
+ gh workflow run --repo $GITHUB_REPOSITORY mirrorchyan_release_note.yml
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/launcher/PCL-CE/.github/workflows/release-stable_publish.yml b/launcher/PCL-CE/.github/workflows/release-stable_publish.yml
new file mode 100644
index 0000000..af2cd43
--- /dev/null
+++ b/launcher/PCL-CE/.github/workflows/release-stable_publish.yml
@@ -0,0 +1,102 @@
+name: Publish (Release)
+
+permissions:
+ contents: read
+
+on:
+ release:
+ types: [published]
+
+jobs:
+ build:
+ if: ${{ !github.event.release.prerelease }}
+ strategy:
+ matrix:
+ include:
+ - configuration: Release
+ architecture: x64
+ - configuration: Release
+ architecture: ARM64
+ fail-fast: false
+
+ uses: ./.github/workflows/reusable-build.yml
+ with:
+ configuration: ${{ matrix.configuration }}
+ architecture: ${{ matrix.architecture }}
+ secrets: inherit
+
+ changelog:
+ permissions:
+ contents: write
+ runs-on: ubuntu-latest
+ continue-on-error: true
+ if: ${{ !github.event.release.prerelease }}
+ steps:
+ - name: Checkout all
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+
+ - name: Install git-cliff
+ uses: taiki-e/install-action@git-cliff
+
+ - name: Generate changelog
+ run: git-cliff --latest -o CHANGELOG.md
+
+ - name: Add changelog to release
+ uses: softprops/action-gh-release@v2.2.2
+ with:
+ tag_name: ${{ github.event.release.tag_name }}
+ body_path: CHANGELOG.md
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ rename_and_release:
+ permissions:
+ contents: write
+ actions: write
+ needs: [build, changelog]
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ include:
+ - configuration: Release
+ architecture: x64
+ - configuration: Release
+ architecture: ARM64
+ steps:
+ - name: Download Build Artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: PCL2_CE_${{ matrix.configuration }}_${{ matrix.architecture }}
+ path: ./artifact
+
+ - name: Rename binaries
+ run: |
+ mv "./artifact/Plain Craft Launcher 2.exe" "PCL2_CE_${{ matrix.configuration }}_${{ matrix.architecture }}.exe"
+
+ - name: Import GPG key
+ uses: crazy-max/ghaction-import-gpg@v6
+ with:
+ gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
+ passphrase: ${{ secrets.GPG_PASSPHRASE }}
+
+ - name: Sign the binary
+ run: |
+ gpg --detach-sign --armor "PCL2_CE_${{ matrix.configuration }}_${{ matrix.architecture }}.exe"
+
+ - name: Upload binary and signature to Release
+ uses: softprops/action-gh-release@v2.5.0
+ with:
+ files: |
+ PCL2_CE_${{ matrix.configuration }}_${{ matrix.architecture }}.exe
+ PCL2_CE_${{ matrix.configuration }}_${{ matrix.architecture }}.exe.asc
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Trigger MirrorChyanUploading
+ run: |
+ gh workflow run --repo $GITHUB_REPOSITORY mirrorchyan_uploading.yml -f channel=stable -f arch=${{ matrix.architecture == 'x64' && 'x64' || 'arm64' }}
+ gh workflow run --repo $GITHUB_REPOSITORY mirrorchyan_release_note.yml
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/launcher/PCL-CE/.github/workflows/reusable-build.yml b/launcher/PCL-CE/.github/workflows/reusable-build.yml
new file mode 100644
index 0000000..f1eb9c8
--- /dev/null
+++ b/launcher/PCL-CE/.github/workflows/reusable-build.yml
@@ -0,0 +1,57 @@
+name: Build (Reusable)
+
+permissions:
+ contents: read
+
+on:
+ workflow_call:
+ inputs:
+ configuration:
+ required: true
+ type: string
+ architecture:
+ required: true
+ type: string
+
+jobs:
+ build:
+ name: Build
+ runs-on: ubuntu-latest
+ env:
+ LANG: en_US.UTF-8
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Set Describe
+ run: |
+ describe=`git describe --tags --always`
+ echo "describe=$describe" >> $GITHUB_ENV
+
+ - name: Setup .NET SDK
+ uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: 10.0.x
+
+ - name: Build Project
+ env:
+ PCL_WRITE_SECRET: '1'
+ PCL_MS_CLIENT_ID: ${{ secrets.CLIENT_ID }}
+ PCL_CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
+ PCL_SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
+ PCL_NAID_CLIENT_ID: ${{ secrets.NAID_CLIENT_ID }}
+ PCL_NAID_CLIENT_SECRET: ${{ secrets.NAID_CLIENT_SECRET }}
+ PCL_LINK_SERVER_ROOT: ${{ secrets.LINK_SERVER_ROOTS }}
+ PCL_LOBBY_DEFAULT_SECRET: ${{ secrets.LOBBY_DEFAULT_SECRET }}
+ PCL_GITHUB_SHA: ${{ github.sha }}
+ run: |
+ dotnet publish "Plain Craft Launcher 2/Plain Craft Launcher 2.csproj" \
+ -p:Configuration=${{ inputs.configuration }} -p:Platform=${{ inputs.architecture }} \
+ -p:DeleteExistingFiles=true -o ./artifact --no-self-contained
+
+ - name: Upload the Build Artifact
+ uses: actions/upload-artifact@v7
+ with:
+ name: PCL2_CE_${{ inputs.configuration }}_${{ inputs.architecture }}
+ path: artifact/**
diff --git a/launcher/PCL-CE/.gitignore b/launcher/PCL-CE/.gitignore
new file mode 100644
index 0000000..787b37d
--- /dev/null
+++ b/launcher/PCL-CE/.gitignore
@@ -0,0 +1,348 @@
+# --------------------------------------------------------------------------------------------
+
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- Backup*.rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+MaskFiles/
+
+# macOS
+
+**/.DS_Store
\ No newline at end of file
diff --git a/launcher/PCL-CE/CONTRIBUTING.md b/launcher/PCL-CE/CONTRIBUTING.md
new file mode 100644
index 0000000..e974c22
--- /dev/null
+++ b/launcher/PCL-CE/CONTRIBUTING.md
@@ -0,0 +1,161 @@
+# 如何为项目做贡献
+
+Wiki 页面:
+[开发指南](https://github.com/PCL-Community/PCL2-CE/wiki/开发指南)
+[技术规范](https://github.com/PCL-Community/PCL2-CE/wiki/技术规范)
+
+
+
+原 CONTRIBUTING.md 内容
+
+## 开始之前
+
+查看 [Issues](https://github.com/PCL-Community/PCL2-CE/issues) 寻找可以参与的任务,或创建新 Issue 讨论您的想法。
+
+## 贡献流程
+
+### 报告问题
+
+1. 在提交 Issue 前,请先搜索是否已有相关 Issue
+2. 使用提供的 Issue 模板
+3. 包含以下信息:
+ - 清晰的问题描述
+ - 复现步骤(包括环境信息)
+ - 预期与实际行为对比
+ - 相关日志/截图(如有)
+
+### 提交代码
+
+1. Fork 仓库并克隆到本地
+
+ ```bash
+ git clone https://github.com/你的用户名/项目名称.git
+ ```
+
+2. 创建你的分支
+
+ ```bash
+ git checkout -b feat/your-feat-name
+ # 或
+ git checkout -b fix/issue-number-desc
+ ```
+
+3. 遵循项目代码风格进行编写
+4. 提交更改,使用 Angular 规范提交信息
+
+ ```bash
+ git commit -m "(scope): "
+ ```
+
+5. 推送分支到你的 Fork
+
+ ```bash
+ git push origin your-branch
+ ```
+
+6. 创建 Pull Request
+ - 指向上游仓库的 `dev` 分支
+ - 详细填写 PR 信息
+ - 关联 Issue(如有)
+
+## 开发规范
+
+### 测试要求
+
+- 提交前请在本地编译通过确保无误后提交
+
+### Angular 规范
+
+基本格式如下
+
+```commit message
+(scope?):
+
+
+
+
\ No newline at end of file
diff --git a/launcher/PCL-CE/GPG-PUBLIC-KEY.asc b/launcher/PCL-CE/GPG-PUBLIC-KEY.asc
new file mode 100644
index 0000000..875db1f
Binary files /dev/null and b/launcher/PCL-CE/GPG-PUBLIC-KEY.asc differ
diff --git a/launcher/PCL-CE/LICENSE b/launcher/PCL-CE/LICENSE
new file mode 100644
index 0000000..33f9e2f
--- /dev/null
+++ b/launcher/PCL-CE/LICENSE
@@ -0,0 +1,207 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2025 PCL Community
+
+ SPECIAL DIRECTORY: Plain Craft Launcher 2/
+
+ Licensed under some custom terms discribed in the 'Plain Craft Launcher 2/LICENCE' file.
+
+ ALL THE OTHER DIRECTORIES
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/launcher/PCL-CE/PCL.Core.SourceGenerators/ConfigGenerator.cs b/launcher/PCL-CE/PCL.Core.SourceGenerators/ConfigGenerator.cs
new file mode 100644
index 0000000..c3d83cd
--- /dev/null
+++ b/launcher/PCL-CE/PCL.Core.SourceGenerators/ConfigGenerator.cs
@@ -0,0 +1,746 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace PCL.Core.SourceGenerators;
+
+[Generator(LanguageNames.CSharp)]
+public sealed class ConfigGenerator : IIncrementalGenerator
+{
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ // 收集所有可能的属性与类
+ var propertyCandidates = context.SyntaxProvider.CreateSyntaxProvider(
+ static (s, _) => s is PropertyDeclarationSyntax { AttributeLists.Count: > 0 },
+ static (ctx, _) => _GetItemCandidate(ctx)
+ ).Where(static m => m is not null);
+
+ var groupCandidates = context.SyntaxProvider.CreateSyntaxProvider(
+ static (s, _) => s is ClassDeclarationSyntax { AttributeLists.Count: > 0 },
+ static (ctx, _) => _GetGroupCandidate(ctx)
+ ).Where(static m => m is not null);
+
+ var configClassCandidates = context.SyntaxProvider.CreateSyntaxProvider(
+ static (s, _) => s is ClassDeclarationSyntax,
+ static (ctx, _) => _GetConfigClass(ctx)
+ ).Where(static m => m is not null);
+
+ // 新增:收集 [RegisterConfigEvent] 的 public static 属性
+ var eventCandidates = context.SyntaxProvider.CreateSyntaxProvider(
+ static (s, _) => s is PropertyDeclarationSyntax { AttributeLists.Count: > 0 },
+ static (ctx, _) => _GetRegisterConfigEventCandidate(ctx)
+ ).Where(static m => m is not null);
+
+ var collected = propertyCandidates.Collect()
+ .Combine(groupCandidates.Collect())
+ .Combine(configClassCandidates.Collect());
+
+ context.RegisterSourceOutput(collected, static (spc, triple) =>
+ {
+ var items = triple.Left.Left;
+ var groups = triple.Left.Right;
+ var configs = triple.Right;
+
+ if (configs.Length == 0) return;
+
+ // 建立快速查找
+ var itemList = items.Cast().ToImmutableArray();
+ var groupList = groups.Cast().ToImmutableArray();
+ var configList = configs.Cast().ToImmutableArray();
+
+ foreach (var config in configList)
+ {
+ try
+ {
+ var tree = _BuildConfigTree(config, itemList, groupList);
+
+ // 跳过无用生成(无顶层项和顶层组声明的类型)
+ if (tree.TopItems.Count == 0 && tree.TopGroups.Count == 0) continue;
+
+ var source = _GenerateAdditionalSource(tree);
+ var hint = _MakeHintName(config);
+ spc.AddSource(hint, source);
+ }
+ catch
+ {
+ // 可添加诊断,此处直接忽略以免打断编译
+ }
+ }
+ });
+
+ var serviceInputs = propertyCandidates.Collect()
+ .Combine(groupCandidates.Collect())
+ .Combine(eventCandidates.Collect());
+ context.RegisterSourceOutput(serviceInputs, static (spc, tuple) =>
+ {
+ var items = tuple.Left.Left.Cast().OrderBy(i => i.DeclOrder).ToList();
+ var groups = tuple.Left.Right.Cast().ToList();
+ var events = tuple.Right.Cast().OrderBy(e => e.DeclOrder).ToList();
+
+ // 两者都为空则不生成
+ if (items.Count == 0 && events.Count == 0) return;
+
+ var groupLookup = new Dictionary(SymbolEqualityComparer.Default);
+ foreach (var group in groups)
+ {
+ groupLookup[group.GroupType] = group;
+ }
+
+ var src = _GenerateServiceInitSource(items, events, groupLookup);
+ spc.AddSource("ConfigService.g.cs", src);
+ });
+ }
+
+ private static object? _GetItemCandidate(GeneratorSyntaxContext ctx)
+ {
+ var propSyntax = (PropertyDeclarationSyntax)ctx.Node;
+ var symbol = ctx.SemanticModel.GetDeclaredSymbol(propSyntax);
+ if (symbol is null) return null;
+
+ var compilation = ctx.SemanticModel.Compilation;
+ var attrDefItem = compilation.GetTypeByMetadataName("PCL.Core.App.Configuration.ConfigItemAttribute`1");
+ var attrDefAny = compilation.GetTypeByMetadataName("PCL.Core.App.Configuration.AnyConfigItemAttribute`1");
+ if (attrDefItem is null && attrDefAny is null) return null;
+
+ AttributeData? picked = null;
+ var isAny = false;
+
+ foreach (var a in symbol.GetAttributes())
+ {
+ var ac = a.AttributeClass;
+ if (ac is null) continue;
+ if (attrDefItem is not null && SymbolEqualityComparer.Default.Equals(ac.ConstructedFrom, attrDefItem))
+ {
+ picked = a; isAny = false; break;
+ }
+ if (attrDefAny is not null && SymbolEqualityComparer.Default.Equals(ac.ConstructedFrom, attrDefAny))
+ {
+ picked = a; isAny = true; break;
+ }
+ }
+ if (picked is null) return null;
+
+ if (picked.ConstructorArguments.Length < 1) return null;
+ var key = picked.ConstructorArguments[0].Value as string;
+ if (string.IsNullOrEmpty(key)) return null;
+
+ // 默认值与来源解析
+ var defaultCode = "default";
+ string? sourceCode = null;
+
+ var attrSyntax = (AttributeSyntax?)picked.ApplicationSyntaxReference?.GetSyntax();
+ if (attrSyntax is not null)
+ {
+ var args = attrSyntax.ArgumentList?.Arguments;
+
+ if (isAny)
+ {
+ // AnyConfigItem:没有“默认值”参数: 替换为无参构造函数
+ var tQualified = symbol.Type.GetFullyQualifiedName();
+ defaultCode = "() => new " + tQualified + "()";
+
+ // 来源参数若存在,是第 2 个实参
+ if (args is { Count: >= 2 })
+ {
+ sourceCode = _RenderSourceCode(ctx.SemanticModel, args.Value[1].Expression);
+ }
+ }
+ else
+ {
+ // ConfigItem:参数2为默认值,参数3为来源(可省略)
+ if (args is { Count: >= 2 })
+ {
+ defaultCode = ctx.SemanticModel.RenderDefaultValueCode(args.Value[1].Expression);
+ }
+ if (args is { Count: >= 3 })
+ {
+ sourceCode = _RenderSourceCode(ctx.SemanticModel, args.Value[2].Expression);
+ }
+ }
+ }
+
+ return new ItemModel
+ {
+ Property = symbol,
+ Key = key!,
+ Type = symbol.Type,
+ IsStatic = symbol.IsStatic,
+ DeclOrder = symbol.GetDeclarationOrder(),
+ DefaultValueCode = defaultCode,
+ SourceCode = sourceCode
+ };
+ }
+
+ private static object? _GetGroupCandidate(GeneratorSyntaxContext ctx)
+ {
+ var classSyntax = (ClassDeclarationSyntax)ctx.Node;
+ var symbol = ModelExtensions.GetDeclaredSymbol(ctx.SemanticModel, classSyntax) as INamedTypeSymbol;
+ if (symbol is null) return null;
+
+ var compilation = ctx.SemanticModel.Compilation;
+ var attrDef = compilation.GetTypeByMetadataName("PCL.Core.App.Configuration.ConfigGroupAttribute");
+ if (attrDef is null) return null;
+
+ var attr = symbol.GetAttributes().FirstOrDefault(a =>
+ a.AttributeClass is not null &&
+ SymbolEqualityComparer.Default.Equals(a.AttributeClass, attrDef));
+
+ if (attr is null) return null;
+
+ if (attr.ConstructorArguments.Length < 1) return null;
+ var name = attr.ConstructorArguments[0].Value as string;
+ if (string.IsNullOrEmpty(name)) return null;
+
+ var hasDeclaredSource = false;
+ string? declaredSourceCode = null;
+ var attrSyntax = (AttributeSyntax?)attr.ApplicationSyntaxReference?.GetSyntax();
+ if (attrSyntax?.ArgumentList?.Arguments is { Count: >= 2 } arguments)
+ {
+ hasDeclaredSource = true;
+ declaredSourceCode = _RenderSourceCode(ctx.SemanticModel, arguments[1].Expression);
+ }
+
+ return new GroupModel
+ {
+ GroupType = symbol,
+ GroupName = name!,
+ DeclOrder = symbol.GetDeclarationOrder(),
+ HasDeclaredSource = hasDeclaredSource,
+ DeclaredSourceCode = declaredSourceCode
+ };
+ }
+
+ private static object? _GetRegisterConfigEventCandidate(GeneratorSyntaxContext ctx)
+ {
+ var propSyntax = (PropertyDeclarationSyntax)ctx.Node;
+ if (ctx.SemanticModel.GetDeclaredSymbol(propSyntax) is not { } symbol) return null;
+
+ // 仅 public static
+ if (symbol.DeclaredAccessibility != Accessibility.Public || !symbol.IsStatic) return null;
+
+ // 精确匹配 [RegisterConfigEvent]
+ var compilation = ctx.SemanticModel.Compilation;
+ var attrDef = compilation.GetTypeByMetadataName("PCL.Core.App.Configuration.RegisterConfigEventAttribute");
+ if (attrDef is null) return null;
+ var hasAttr = symbol.GetAttributes().Any(a =>
+ a.AttributeClass is not null &&
+ SymbolEqualityComparer.Default.Equals(a.AttributeClass, attrDef));
+ if (!hasAttr) return null;
+
+ return new EventRegisterModel
+ {
+ Property = symbol,
+ DeclOrder = symbol.GetDeclarationOrder()
+ };
+ }
+
+ private static object? _GetConfigClass(GeneratorSyntaxContext ctx)
+ {
+ var classSyntax = (ClassDeclarationSyntax)ctx.Node;
+ if (ModelExtensions.GetDeclaredSymbol(ctx.SemanticModel, classSyntax) is not INamedTypeSymbol symbol) return null;
+
+ // 限定为 partial
+ if (!symbol.IsPartial()) return null;
+
+ // 绕过 [ConfigGroup]
+ var compilation = ctx.SemanticModel.Compilation;
+ var attrDef = compilation.GetTypeByMetadataName("PCL.Core.App.Configuration.ConfigGroupAttribute");
+ if (attrDef is not null)
+ {
+ var hasAttr = symbol.GetAttributes().Any(a =>
+ a.AttributeClass is not null &&
+ SymbolEqualityComparer.Default.Equals(a.AttributeClass, attrDef));
+ if (hasAttr) return null;
+ }
+
+ return new ConfigModel
+ {
+ ConfigType = symbol,
+ DeclOrder = symbol.GetDeclarationOrder()
+ };
+ }
+
+ private static ConfigTree _BuildConfigTree(ConfigModel config,
+ ImmutableArray items,
+ ImmutableArray groups)
+ {
+ var configType = config.ConfigType;
+
+ // 过滤归属于该 Config 的顶层项与组
+ var topItems = items.Where(i => SymbolEqualityComparer.Default.Equals(i.Property.ContainingType, configType))
+ .OrderBy(i => i.DeclOrder)
+ .ToList();
+
+ var allGroupsForConfig = groups
+ .Where(g => g.GroupType.IsNestedWithin(configType))
+ .OrderBy(g => g.DeclOrder)
+ .ToList();
+
+ // 构建组索引
+ var groupMap = allGroupsForConfig.ToDictionary(g => g.GroupType, g => new GroupNode(g), SymbolEqualityComparer.Default);
+
+ GroupModel? GroupLookup(INamedTypeSymbol type) =>
+ groupMap.TryGetValue(type, out var node) ? node.Model : null;
+
+ string ResolveItemSource(ItemModel item) =>
+ _ResolveItemSourceCode(item, GroupLookup);
+
+ // 组装层级
+ foreach (var node in groupMap.Values)
+ {
+ var parentType = node.Model.GroupType.ContainingType;
+ if (parentType is not null && !SymbolEqualityComparer.Default.Equals(parentType, configType))
+ {
+ if (groupMap.TryGetValue(parentType, out var parentNode))
+ {
+ parentNode.Children.Add(node);
+ }
+ }
+ }
+
+ // 顶层组
+ var topGroups = groupMap.Values
+ .Where(n => SymbolEqualityComparer.Default.Equals(n.Model.GroupType.ContainingType, configType))
+ .OrderBy(n => n.Model.DeclOrder)
+ .ToList();
+
+ // 将 Item 分配到各自的组
+ foreach (var item in items.Except(topItems))
+ {
+ var container = item.Property.ContainingType;
+ if (container is null) continue;
+ if (groupMap.TryGetValue(container, out var groupNode))
+ {
+ groupNode.Items.Add(item);
+ }
+ }
+
+ return new ConfigTree
+ {
+ Namespace = configType.ContainingNamespace?.ToDisplayString() ?? "",
+ ConfigType = configType,
+ TopItems = topItems,
+ TopGroups = topGroups,
+ ResolveSourceCode = ResolveItemSource
+ };
+ }
+
+ private static string _MakeHintName(ConfigModel config)
+ {
+ var ns = config.ConfigType.ContainingNamespace?.ToDisplayString() ?? "Global";
+ return $"{ns}.{config.ConfigType.Name}.g.cs";
+ }
+
+ private static string _GenerateAdditionalSource(ConfigTree tree)
+ {
+ var sb = new StringBuilder(4096);
+
+ var ns = string.IsNullOrEmpty(tree.Namespace) ? null : tree.Namespace;
+ var configType = tree.ConfigType;
+ var configName = configType.Name;
+ var resolveSource = tree.ResolveSourceCode;
+
+ sb.AppendLine("// ");
+ sb.AppendLine("// 此文件由 Source Generator 自动生成,请勿手动修改");
+ sb.AppendLine();
+ sb.AppendLine("using System.Collections.Generic;");
+ sb.AppendLine("using System.Linq;");
+ sb.AppendLine("using PCL.Core.App.Configuration;");
+ sb.AppendLine();
+ sb.AppendLine("#nullable enable");
+ sb.AppendLine();
+
+ if (!string.IsNullOrEmpty(ns))
+ {
+ sb.Append("namespace ").Append(ns).AppendLine(";");
+ sb.AppendLine();
+ }
+
+ sb.Append("partial class ").Append(configName).AppendLine();
+ sb.AppendLine("{");
+
+ // === Config Items ===
+ sb.AppendLine(" // === Config Items ===");
+ sb.AppendLine();
+ if (tree.TopItems.Count == 0)
+ {
+ sb.AppendLine();
+ }
+ else
+ {
+ foreach (var item in tree.TopItems)
+ {
+ _EmitItem(sb, item, indent: 1, isTopLevel: true, resolveSource);
+ sb.AppendLine();
+ }
+ }
+
+ // === Config Groups ===
+ sb.AppendLine(" // === Config Groups ===");
+ if (tree.TopGroups.Count > 0)
+ {
+ foreach (var grp in tree.TopGroups)
+ {
+ _EmitGroupInto(sb, grp, indent: 1, isTopLevel: true, resolveSource);
+ }
+ }
+
+ sb.AppendLine("}");
+ return sb.ToString();
+ }
+
+ private static string _GenerateServiceInitSource(
+ IReadOnlyList items,
+ IReadOnlyList events,
+ IReadOnlyDictionary groupLookup)
+ {
+ GroupModel? Lookup(INamedTypeSymbol type) =>
+ groupLookup.TryGetValue(type, out var model) ? model : null;
+
+ string ResolveSource(ItemModel item) =>
+ _ResolveItemSourceCode(item, Lookup);
+
+ var sb = new StringBuilder(1024);
+ sb.AppendLine("// ");
+ sb.AppendLine("// 此文件由 Source Generator 自动生成,请勿手动修改");
+ sb.AppendLine();
+ sb.AppendLine("namespace PCL.Core.App.Configuration;");
+ sb.AppendLine();
+ sb.AppendLine("public sealed partial class ConfigService");
+ sb.AppendLine("{");
+
+ // 配置项初始化
+ sb.AppendLine(" private static void _InitializeConfigItems()");
+ sb.AppendLine(" {");
+ sb.AppendLine(" (string, ConfigItem)[] items = [");
+
+ HashSet keysAdded = [];
+ for (var i = 0; i < items.Count; i++)
+ {
+ var it = items[i];
+ if (!keysAdded.Add(it.Key)) continue;
+ var keyLiteral = it.Key.ToLiteral();
+ var typeName = it.Type.GetFullyQualifiedName().CorrectConfigTypeName(out _);
+ var sourceCode = ResolveSource(it);
+ sb.Append(" (")
+ .Append(keyLiteral)
+ .Append(", new ConfigItem<").Append(typeName).Append(">(")
+ .Append(keyLiteral).Append(", ")
+ .Append(it.DefaultValueCode).Append(", ").Append(sourceCode)
+ .Append("))");
+ if (i != items.Count - 1) sb.Append(',');
+ sb.AppendLine();
+ }
+
+ sb.AppendLine(" ];");
+ sb.AppendLine(" foreach (var (key, value) in items) {");
+ sb.AppendLine(" _KeySet.Add(key);");
+ sb.AppendLine(" _Items[key] = value;");
+ sb.AppendLine(" }");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+
+ // 事件观察器初始化
+ sb.AppendLine(" private static void _InitializeObservers()");
+ sb.AppendLine(" {");
+ sb.AppendLine(" ConfigEventRegistry[] registers = [");
+
+ for (var i = 0; i < events.Count; i++)
+ {
+ var ev = events[i];
+ sb.Append(" ")
+ .Append(ev.Property.GetQualifiedPropertyAccess());
+ if (i != events.Count - 1) sb.Append(',');
+ sb.AppendLine();
+ }
+
+ sb.AppendLine(" ];");
+ sb.AppendLine(" foreach (var r in registers) foreach (var scope in r.Scopes) {");
+ sb.AppendLine(" RegisterObserver(scope, r.ToObserver());");
+ sb.AppendLine(" }");
+ sb.AppendLine(" }");
+
+ sb.AppendLine("}");
+ return sb.ToString();
+ }
+
+ private static string _ResolveItemSourceCode(ItemModel item, Func groupLookup)
+ {
+ if (item.SourceCode is { } explicitSource)
+ {
+ return explicitSource;
+ }
+
+ var container = item.Property.ContainingType;
+ while (container is not null)
+ {
+ var group = groupLookup(container);
+ if (group is { HasDeclaredSource: true, DeclaredSourceCode: { } declared })
+ {
+ return declared;
+ }
+ container = container.ContainingType;
+ }
+
+ return "ConfigSource.Shared";
+ }
+
+ private static Action? _EmitItem(
+ StringBuilder sb,
+ ItemModel item,
+ int indent,
+ bool isTopLevel,
+ Func resolveSource)
+ {
+ Action? accessorInitializer = null;
+ var typeName = item.Type.GetFullyQualifiedName().CorrectConfigTypeName(out var fullTypeName);
+ var propName = item.Property.Name;
+ var configItemName = propName + "Config";
+ var staticKeyword = item.IsStatic || isTopLevel ? "static " : string.Empty;
+ var indentStr = new string(' ', indent * 4);
+ var sourceCode = resolveSource(item);
+
+ // 注释
+ sb.Append(indentStr).Append("// Item: ").Append(propName).Append(" [").Append(item.Key).AppendLine("]");
+
+ // 访问器
+ sb.Append(indentStr)
+ .Append("public ").Append(staticKeyword).Append("partial ")
+ .Append(fullTypeName ?? typeName).Append(' ').Append(propName);
+
+ if (fullTypeName is not null)
+ {
+ var accessorName = "ACCESSOR_" + propName;
+ sb.Append(" => ").Append(accessorName).AppendLine(";");
+ // 初始化带参数访问器
+ sb.Append(indentStr)
+ .Append("private ").Append(staticKeyword).Append("readonly ")
+ .Append(fullTypeName).Append(' ');
+ // ReSharper disable once VariableHidesOuterVariable
+ void AccessorInitializer(StringBuilder sb)
+ {
+ sb.Append(accessorName)
+ .Append(" = new((arg) => ")
+ .Append(configItemName)
+ .Append(".GetValue(arg), (arg, value) => ")
+ .Append(configItemName)
+ .AppendLine(".SetValue(value, arg));");
+ }
+ if (item.IsStatic) AccessorInitializer(sb);
+ else {
+ accessorInitializer = AccessorInitializer;
+ sb.Append(accessorName).AppendLine(";");
+ }
+ }
+ else
+ {
+ sb.Append(" { get => ")
+ .Append(configItemName)
+ .Append(".GetValue(); set => ")
+ .Append(configItemName)
+ .AppendLine(".SetValue(value); }");
+ }
+
+ // 配置项
+ sb.Append(indentStr)
+ .Append("public ").Append(staticKeyword)
+ .Append("ConfigItem<").Append(typeName).Append("> ")
+ .Append(configItemName).Append(" { get => field ??= ConfigService.GetConfigItem<")
+ .Append(typeName)
+ .Append(">(")
+ .Append(item.Key.ToLiteral())
+ .AppendLine("); } = null!;");
+
+ return accessorInitializer;
+ }
+
+ private static void _EmitGroupInto(
+ StringBuilder sb,
+ GroupNode node,
+ int indent,
+ bool isTopLevel,
+ Func resolveSource)
+ {
+ var indentStr = new string(' ', indent * 4);
+ var type = node.Model.GroupType;
+ var typeName = type.Name;
+ var staticKeyword = isTopLevel ? "static " : string.Empty;
+
+ // 组实例字段(在其父作用域中)
+ sb.AppendLine();
+ sb.Append(indentStr).Append("// Group: ").AppendLine(node.Model.GroupName);
+ sb.Append(indentStr).Append("/// ");
+ sb.Append(indentStr)
+ .Append("public ")
+ .Append(staticKeyword)
+ .Append("readonly ")
+ .Append(typeName)
+ .Append(' ')
+ .Append(node.Model.GroupName)
+ .Append(" = ")
+ .Append(typeName)
+ .AppendLine(".SINGLE_INSTANCE;");
+
+ // 嵌套类型定义
+ sb.Append(indentStr)
+ .Append("public sealed partial class ")
+ .Append(typeName)
+ .AppendLine(" : IConfigScope");
+ sb.Append(indentStr).AppendLine("{");
+
+ // === Config Items ===
+ sb.Append(indentStr).AppendLine(" // === Config Items ===");
+ sb.Append(indentStr).AppendLine();
+ List> accessorInitializers = [];
+ foreach (var item in node.Items.OrderBy(i => i.DeclOrder))
+ {
+ var result = _EmitItem(sb, item, indent + 1, isTopLevel: false, resolveSource);
+ if (result is not null) accessorInitializers.Add(result);
+ sb.AppendLine();
+ }
+
+ // === Config Groups ===
+ sb.Append(indentStr).AppendLine(" // === Config Groups ===");
+ foreach (var child in node.Children.OrderBy(c => c.Model.DeclOrder))
+ {
+ _EmitGroupInto(sb, child, indent + 1, isTopLevel: false, resolveSource);
+ }
+
+ // === Group Scope Implementation ===
+ sb.AppendLine();
+ sb.Append(indentStr).AppendLine(" // === Group Scope Implementation ===");
+ sb.Append(indentStr).AppendLine();
+ sb.Append(indentStr).AppendLine(" public static readonly " + typeName + " SINGLE_INSTANCE = new();");
+ sb.Append(indentStr).AppendLine(" private readonly IConfigScope[] _InnerScopes;");
+ sb.Append(indentStr).AppendLine(" private " + typeName + "()");
+ sb.Append(indentStr).AppendLine(" {");
+ sb.Append(indentStr).AppendLine(" _InnerScopes = [");
+
+ // InnerScopes: 先项再子组
+ var first = true;
+ foreach (var item in node.Items.SkipWhile(i => i.IsStatic).OrderBy(i => i.DeclOrder))
+ {
+ if (!first) sb.AppendLine(",");
+ sb.Append(indentStr).Append(" ").Append(item.Property.Name).Append("Config");
+ first = false;
+ }
+ foreach (var child in node.Children.OrderBy(c => c.Model.DeclOrder))
+ {
+ if (!first) sb.AppendLine(",");
+ sb.Append(indentStr).Append(" ").Append(child.Model.GroupName);
+ first = false;
+ }
+ if (!first) sb.AppendLine();
+ sb.Append(indentStr).AppendLine(" ];");
+ foreach (var initializer in accessorInitializers)
+ {
+ sb.Append(indentStr).Append(" ");
+ initializer.Invoke(sb);
+ }
+ sb.Append(indentStr).AppendLine(" }");
+
+ // CheckScope
+ sb.Append(indentStr).AppendLine(" public IEnumerable CheckScope(IReadOnlySet keys)");
+ sb.Append(indentStr).AppendLine(" {");
+ sb.Append(indentStr).AppendLine(" IEnumerable result = [];");
+ sb.Append(indentStr).AppendLine(" foreach (var scope in _InnerScopes)");
+ sb.Append(indentStr).AppendLine(" {");
+ sb.Append(indentStr).AppendLine(" var next = scope.CheckScope(keys);");
+ sb.Append(indentStr).AppendLine(" if (next.Any()) result = result.Concat(next);");
+ sb.Append(indentStr).AppendLine(" }");
+ sb.Append(indentStr).AppendLine(" return result;");
+ sb.Append(indentStr).AppendLine(" }");
+
+ // Reset
+ sb.Append(indentStr).AppendLine(" public bool Reset(object? argument = null)");
+ sb.Append(indentStr).AppendLine(" {");
+ sb.Append(indentStr).AppendLine(" var result = true;");
+ sb.Append(indentStr).AppendLine(" foreach (var scope in _InnerScopes)");
+ sb.Append(indentStr).AppendLine(" {");
+ sb.Append(indentStr).AppendLine(" var next = scope.Reset(argument);");
+ sb.Append(indentStr).AppendLine(" if (!next) result = false;");
+ sb.Append(indentStr).AppendLine(" }");
+ sb.Append(indentStr).AppendLine(" return result;");
+ sb.Append(indentStr).AppendLine(" }");
+
+ // IsDefault
+ sb.Append(indentStr).AppendLine(" public bool IsDefault(object? argument = null)");
+ sb.Append(indentStr).AppendLine(" {");
+ sb.Append(indentStr).AppendLine(" var result = true;");
+ sb.Append(indentStr).AppendLine(" foreach (var scope in _InnerScopes)");
+ sb.Append(indentStr).AppendLine(" {");
+ sb.Append(indentStr).AppendLine(" var next = scope.IsDefault(argument);");
+ sb.Append(indentStr).AppendLine(" if (!next) result = false;");
+ sb.Append(indentStr).AppendLine(" }");
+ sb.Append(indentStr).AppendLine(" return result;");
+ sb.Append(indentStr).AppendLine(" }");
+
+ sb.Append(indentStr).AppendLine("}");
+ }
+
+ public static string _RenderSourceCode(SemanticModel sm, ExpressionSyntax expr)
+ {
+ var sym = sm.GetSymbolInfo(expr).Symbol;
+ if (sym is IFieldSymbol fs && fs.ContainingType?.ToDisplayString() == "PCL.Core.App.Configuration.ConfigSource")
+ {
+ return "ConfigSource." + fs.Name;
+ }
+ return expr.ToString();
+ }
+
+ // ===== Models/Trees =====
+
+ private sealed class ItemModel
+ {
+ public IPropertySymbol Property { get; set; } = null!;
+ public string Key { get; set; } = "";
+ public ITypeSymbol Type { get; set; } = null!;
+ public bool IsStatic { get; set; }
+ public int DeclOrder { get; set; }
+ public string DefaultValueCode { get; set; } = "";
+ public string? SourceCode { get; set; }
+ }
+
+ private sealed class GroupModel
+ {
+ public INamedTypeSymbol GroupType { get; set; } = null!;
+ public string GroupName { get; set; } = "";
+ public int DeclOrder { get; set; }
+ public bool HasDeclaredSource { get; set; }
+ public string? DeclaredSourceCode { get; set; }
+ }
+
+ private sealed class GroupNode(GroupModel model)
+ {
+ public GroupModel Model { get; } = model;
+ public List Children { get; } = [];
+ public List Items { get; } = [];
+ }
+
+ private sealed class ConfigModel
+ {
+ public INamedTypeSymbol ConfigType { get; set; } = null!;
+ // ReSharper disable once UnusedAutoPropertyAccessor.Local
+ public int DeclOrder { get; set; }
+ }
+
+ private sealed class ConfigTree
+ {
+ public string Namespace { get; set; } = "";
+ public INamedTypeSymbol ConfigType { get; set; } = null!;
+ public List TopItems { get; set; } = [];
+ public List TopGroups { get; set; } = [];
+ public Func ResolveSourceCode { get; set; } = static _ => "ConfigSource.Shared";
+ }
+
+ private sealed class EventRegisterModel
+ {
+ public IPropertySymbol Property { get; set; } = null!;
+ public int DeclOrder { get; set; }
+ }
+}
diff --git a/launcher/PCL-CE/PCL.Core.SourceGenerators/DependencyCollectorGenerator.cs b/launcher/PCL-CE/PCL.Core.SourceGenerators/DependencyCollectorGenerator.cs
new file mode 100644
index 0000000..1ae9989
--- /dev/null
+++ b/launcher/PCL-CE/PCL.Core.SourceGenerators/DependencyCollectorGenerator.cs
@@ -0,0 +1,350 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace PCL.Core.SourceGenerators;
+
+public readonly record struct CollectorInfo(
+ INamedTypeSymbol CollectorAttrSymbol,
+ ITypeSymbol DependencyType,
+ string Identifier,
+ AttributeTargets Targets
+);
+
+public readonly record struct DependencyMatchResult(
+ ISymbol Target,
+ AttributeTargets TargetType,
+ AttributeData CollectorAttr,
+ CollectorInfo Info
+);
+
+public readonly record struct InjectionPointInfo(
+ IMethodSymbol Target,
+ string Identifier
+);
+
+public readonly record struct InjectionPointMatchResult(
+ InjectionPointInfo Info,
+ ImmutableArray Dependencies
+);
+
+[Generator(LanguageNames.CSharp)]
+public sealed class DependencyCollectorGenerator : IIncrementalGenerator
+{
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ const string collectorMarkupAttr = SharedConstants.DependencyCollectorAttribute;
+ const string collectorMarkupAttrFull = $"{collectorMarkupAttr}`1";
+ const string injectionPointAttr = SharedConstants.DependencyInjectionPointAttribute;
+
+ // 收集被标记为 collector 的注解
+ var collectorAttrs = context.SyntaxProvider
+ .ForAttributeWithMetadataName(collectorMarkupAttrFull,
+ predicate: static (node, _) => node is ClassDeclarationSyntax,
+ transform: static (ctx, _) =>
+ {
+ if (ctx.TargetSymbol is not INamedTypeSymbol attr || !attr.IsAttribute()) return default;
+ var infos = new List();
+ foreach (var attrData in ctx.Attributes)
+ {
+ var attrClass = attrData.AttributeClass;
+ if (attrClass is null || attrClass.GetSimplifiedTypeName() != collectorMarkupAttr) continue;
+ // 收集注解信息
+ var dependencyType = attrClass.TypeArguments.FirstOrDefault();
+ if (dependencyType is null) continue;
+ var ctorArgs = attrData.ConstructorArguments;
+ if (ctorArgs.Length < 2
+ || ctorArgs[0].Value is not string identifier
+ || ctorArgs[1].Value is not int targets)
+ continue;
+ infos.Add(new CollectorInfo(attr, dependencyType, identifier, (AttributeTargets)targets));
+ }
+ return new KeyValuePair>(attr, infos);
+ })
+ .Where(x => x.Key is not null)
+ .Collect()
+ // 此处合并到 dictionary 以优化后续查找性能
+ .Select(static (pairs, _) =>
+ {
+ var dict = new Dictionary>(SymbolEqualityComparer.Default);
+ foreach (var pair in pairs)
+ {
+ if (dict.TryGetValue(pair.Key, out var list)) list.AddRange(pair.Value);
+ else dict[pair.Key] = pair.Value;
+ }
+ return dict.ToImmutableDictionary(SymbolEqualityComparer.Default);
+ });
+
+ // 收集所有带注解的 static member
+ var potentialTargets = context.SyntaxProvider.CreateSyntaxProvider(
+ predicate: static (node, _) =>
+ {
+ // 仅支持 class, property, method
+ if (node is not MemberDeclarationSyntax { AttributeLists.Count: > 0 } member) return false;
+ if (node is ClassDeclarationSyntax) return true;
+ if (node is PropertyDeclarationSyntax or MethodDeclarationSyntax
+ && member.Modifiers.Any(x => x.IsKind(SyntaxKind.StaticKeyword))) return true;
+ return false;
+ },
+ transform: static (ctx, _) => ctx);
+
+ // 筛选出被 collector 标记的 member
+ var matches = potentialTargets.Combine(collectorAttrs)
+ .SelectMany(static (pair, cancelToken) =>
+ {
+ var (ctx, validAttrs) = pair;
+ // 从 syntax node 获取对应语义 symbol
+ var symbol = ctx.SemanticModel.GetDeclaredSymbol(ctx.Node, cancelToken);
+ if (symbol is null) return [];
+ // 确定目标类型
+ AttributeTargets targetType = default;
+ if (symbol is INamedTypeSymbol) targetType = AttributeTargets.Class;
+ else if (symbol is IPropertySymbol) targetType = AttributeTargets.Property;
+ else if (symbol is IMethodSymbol) targetType = AttributeTargets.Method;
+ // 筛选目标所有符合条件的注解
+ var results = new List();
+ foreach (var attrData in symbol.GetAttributes())
+ {
+ var attr = attrData.AttributeClass;
+ if (attr is null) continue;
+ if (!validAttrs.TryGetValue(attr, out var infos)) continue;
+ results.AddRange(
+ from info in infos
+ where info.Targets.HasFlag(targetType)
+ select new DependencyMatchResult(symbol, targetType, attrData, info)
+ );
+ }
+ return results;
+ })
+ .Collect();
+
+ // 收集被标记为注入点的方法
+ var injectionPoints = context.SyntaxProvider
+ .ForAttributeWithMetadataName(injectionPointAttr,
+ predicate: static (node, _) => node is MethodDeclarationSyntax,
+ transform: static (ctx, _) =>
+ {
+ var method = (IMethodSymbol)ctx.TargetSymbol;
+ var attr = ctx.Attributes.First(x => x.AttributeClass?.GetSimplifiedTypeName() == injectionPointAttr);
+ var attrArgs = attr.ConstructorArguments;
+ var identifier = attrArgs[0].Value?.ToString();
+ return identifier is null ? default : new InjectionPointInfo(method, identifier);
+ })
+ .Where(x => x != default);
+
+ // 将注入点与对应标记的依赖项关联
+ var injectionPointMatches = injectionPoints.Combine(matches)
+ .Select((item, _) =>
+ {
+ var point = item.Left;
+ var deps = item.Right
+ .Where(x => x.Info.Identifier == point.Identifier)
+ .ToImmutableArray();
+ return new InjectionPointMatchResult(point, deps);
+ })
+ .Collect();
+
+ // 生成注入实现
+ context.RegisterSourceOutput(injectionPointMatches, _GenerateDependencyInjectionMethods);
+
+ // 保留旧生成模式以供旧组件兼容
+ context.RegisterSourceOutput(matches, _GenerateDependencyGroup);
+ }
+
+ private static void _GenerateDependencyGroup(SourceProductionContext spc, ImmutableArray matches)
+ {
+ var dependencyMap = new Dictionary>>();
+ foreach (var dep in matches)
+ {
+ var info = dep.Info;
+ if (!dependencyMap.TryGetValue(info, out var map))
+ {
+ map = new Dictionary>
+ {
+ [AttributeTargets.Class] = [],
+ [AttributeTargets.Method] = [],
+ [AttributeTargets.Property] = []
+ };
+ dependencyMap[info] = map;
+ }
+ map[dep.TargetType].Add(dep);
+ }
+
+ var sb = new StringBuilder(1024);
+
+ sb.AppendLine("// ");
+ sb.AppendLine("// 此文件由 Source Generator 自动生成,请勿手动修改");
+ sb.AppendLine();
+ sb.AppendLine("using System;");
+ sb.AppendLine("using System.Collections.Generic;");
+ sb.AppendLine("using System.Collections.Immutable;");
+ sb.AppendLine();
+ sb.AppendLine("namespace PCL.Core.App.IoC;");
+ sb.AppendLine();
+ sb.AppendLine("#nullable enable");
+ sb.AppendLine();
+ sb.AppendLine("public static partial class DependencyGroups");
+ sb.AppendLine("{");
+
+ sb.AppendLine(" private static readonly Dictionary> _GroupMap = new()");
+ sb.AppendLine(" {");
+
+ foreach (var (info, map) in dependencyMap
+ .Select(x => (x.Key, x.Value)))
+ {
+ sb.Append(" [").Append(info.Identifier.ToLiteral()).AppendLine("] = new()");
+ sb.AppendLine(" {");
+ string? typeStr = null;
+ string? argTypeList = null;
+ foreach (var (target, deps) in map
+ .Where(x => x.Value.Count > 0)
+ .Select(x => (x.Key, x.Value)))
+ {
+ sb.Append(" [AttributeTargets.").Append(target).Append("] = new DependencyGroup<");
+ typeStr ??= info.DependencyType.GetFullyQualifiedName();
+ switch (target)
+ {
+ case AttributeTargets.Class:
+ sb.Append("Action<").Append(typeStr).Append(">");
+ break;
+ case AttributeTargets.Method:
+ sb.Append(typeStr);
+ break;
+ case AttributeTargets.Property:
+ sb.Append("PropertyAccessor<").Append(typeStr).Append(">");
+ break;
+ }
+ argTypeList ??= ((Func)(() =>
+ {
+ var ctor = info.CollectorAttrSymbol.InstanceConstructors.FirstOrDefault();
+ if (ctor is null) return string.Empty;
+ var args = ctor.Parameters.Select(para => para.Type.GetFullyQualifiedName()).ToList();
+ var cnt = args.Count;
+ if (cnt == 0) return string.Empty;
+ if (cnt == 1) return args[0];
+ return "(" + string.Join(", ", args) + ")";
+ }))();
+ if (argTypeList != string.Empty) sb.Append(", ").Append(argTypeList);
+ sb.AppendLine("> { Items = [");
+ foreach (var dep in deps)
+ {
+ sb.Append(" (");
+ var depRef = dep.Target.GetQualifiedSymbolName();
+ switch (target)
+ {
+ case AttributeTargets.Class:
+ sb.Append("static () => new ")
+ .Append(depRef).Append("()");
+ break;
+ case AttributeTargets.Method:
+ sb.Append(depRef);
+ break;
+ case AttributeTargets.Property:
+ sb.Append("new(getter: ");
+ var prop = (IPropertySymbol)dep.Target;
+ if (prop.IsWriteOnly) sb.Append("null");
+ else sb.Append("static () => ").Append(depRef);
+ sb.Append(", setter: ");
+ if (prop.IsReadOnly) sb.Append("null");
+ else sb.Append("static value => ").Append(depRef).Append(" = value");
+ sb.Append(")");
+ break;
+ }
+ if (argTypeList != string.Empty)
+ {
+ sb.Append(", ");
+ var args = dep.CollectorAttr.ConstructorArguments.Select(arg => arg.ToCSharpString()).ToList();
+ if (args.Count == 1) sb.Append(args[0]);
+ else sb.Append("(").Append(string.Join(", ", args)).Append(")");
+ }
+ sb.AppendLine("),");
+ }
+ sb.AppendLine(" ] },");
+ }
+ sb.AppendLine(" },");
+ }
+
+ sb.AppendLine(" };");
+ sb.AppendLine("}");
+
+ spc.AddSource("DependencyGroups.g.cs", sb.ToString());
+ }
+
+ private static void _GenerateDependencyInjectionMethods(SourceProductionContext spc, ImmutableArray matches)
+ {
+ foreach (var match in matches)
+ {
+ var sb = new StringBuilder(1024);
+
+ // file header
+ sb.AppendLine("// 此文件由 Source Generator 自动生成,请勿手动修改");
+ sb.AppendLine("// ");
+ sb.AppendLine();
+ sb.AppendLine("using System;");
+ sb.AppendLine("using System.Threading.Tasks;");
+ sb.AppendLine($"using {SharedConstants.IocNamespace};");
+ sb.AppendLine();
+ sb.AppendLine("#nullable enable");
+ sb.AppendLine();
+
+ // type header
+ var targetMethod = match.Info.Target;
+ var indent = targetMethod.ContainingType.GenerateTypeHeader(sb);
+
+ // method
+ var indentStr = new string(' ', indent * 4);
+ var targetMethodName = targetMethod.Name;
+ var isStatic = targetMethod.IsStatic;
+ var isAwaitable = targetMethod.IsAwaitable();
+ sb.Append(indentStr).AppendLine("[global::System.CodeDom.Compiler.GeneratedCode(\"PCL.Core.SourceGenerators.DependencyCollectorGenerator\", \"1.0.0.0\")]");
+ sb.Append(indentStr).AppendLine("[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]");
+ var idCode = match.Info.Identifier.SnakeIdToPascal();
+ sb.Append(indentStr).Append("private ");
+ if (isStatic) sb.Append("static ");
+ sb.Append(isAwaitable ? "async Task " : "void ");
+ sb.Append(targetMethodName).Append("_InvokeInjection_").Append(idCode).AppendLine("()");
+ sb.Append(indentStr).AppendLine("{");
+ foreach (var dep in match.Dependencies)
+ {
+ sb.Append(indentStr).Append(" ");
+ if (isAwaitable) sb.Append("await ");
+ sb.Append(targetMethodName).Append("(");
+ var depRef = dep.Target.GetQualifiedSymbolName();
+ switch (dep.TargetType)
+ {
+ case AttributeTargets.Class:
+ sb.Append("static () => new ").Append(depRef).Append("()");
+ break;
+ case AttributeTargets.Method:
+ sb.Append(depRef);
+ break;
+ case AttributeTargets.Property:
+ sb.Append("new PropertyAccessor(getter: ");
+ var prop = (IPropertySymbol)dep.Target;
+ if (prop.IsWriteOnly) sb.Append("null");
+ else sb.Append("static () => ").Append(depRef);
+ sb.Append(", setter: ");
+ if (prop.IsReadOnly) sb.Append("null");
+ else sb.Append("static value => ").Append(depRef).Append(" = value");
+ sb.Append(")");
+ break;
+ }
+ foreach (var arg in dep.CollectorAttr.ConstructorArguments)
+ sb.Append(", ").Append(arg.ToCSharpString());
+ sb.AppendLine(");");
+ }
+ sb.Append(indentStr).AppendLine("}");
+
+ // type footer
+ while (indent-- > 0) sb.Append(' ', indent * 4).AppendLine("}");
+
+ // register source code
+ spc.AddSource($"{targetMethod.GetQualifiedSymbolName()}.g.cs", sb.ToString());
+ }
+ }
+}
diff --git a/launcher/PCL-CE/PCL.Core.SourceGenerators/EnvironmentInteropGenerator.cs b/launcher/PCL-CE/PCL.Core.SourceGenerators/EnvironmentInteropGenerator.cs
new file mode 100644
index 0000000..d5e04ca
--- /dev/null
+++ b/launcher/PCL-CE/PCL.Core.SourceGenerators/EnvironmentInteropGenerator.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Text;
+
+namespace PCL.Core.SourceGenerators;
+
+[Generator(LanguageNames.CSharp)]
+public class EnvironmentInteropGenerator : IIncrementalGenerator
+{
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var secretProvider = context.CompilationProvider.Select(static (_, _) =>
+ {
+ // 判断 PCL_WRITE_SECRET 是否存在并遍历转换环境变量
+ // 打死也不会用 MSBuild Properties 这种非人类设计出来的垃圾
+#pragma warning disable RS1035
+ var envs = Environment.GetEnvironmentVariables();
+#pragma warning restore RS1035
+ var secretPairs = envs.Contains("PCL_WRITE_SECRET") ? (
+ from key in (
+ from key in envs.Keys.Cast()
+ where !string.IsNullOrWhiteSpace(key) && key.StartsWith("PCL_") && key != "PCL_WRITE_SECRET"
+ select key
+ )
+ let value = envs[key]?.ToString()
+ where !string.IsNullOrWhiteSpace(value)
+ select (key.Substring(4), value)
+ ) : [];
+ return secretPairs;
+ });
+
+ // 注册源代码输出
+ context.RegisterSourceOutput(secretProvider, static (spc, secretPairs) => _Execute(spc, secretPairs));
+ }
+
+ private static void _Execute(SourceProductionContext context, IEnumerable<(string, string)> secretPairs)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("// ");
+ sb.AppendLine("// 此文件由 Source Generator 自动生成,请勿手动修改");
+ sb.AppendLine();
+ sb.AppendLine("#nullable enable");
+ sb.AppendLine();
+ sb.AppendLine("namespace PCL.Core.Utils.OS;");
+ sb.AppendLine();
+ sb.AppendLine("partial class EnvironmentInterop");
+ sb.AppendLine("{");
+ sb.AppendLine(" private static readonly System.Collections.Generic.Dictionary SecretDictionary = new()");
+ sb.AppendLine(" {");
+
+ foreach (var (key, value) in secretPairs)
+ sb.AppendLine($" [\"{key}\"] = {_ToVerbatimString(value)},");
+
+ sb.AppendLine(" };");
+ sb.AppendLine("}");
+
+ context.AddSource("EnvironmentInterop.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8));
+ }
+
+ private static string _ToVerbatimString(string text)
+ {
+ return "@\"" + text.Replace("\"", "\"\"") + "\"";
+ }
+}
diff --git a/launcher/PCL-CE/PCL.Core.SourceGenerators/LifecycleScopeGenerator.cs b/launcher/PCL-CE/PCL.Core.SourceGenerators/LifecycleScopeGenerator.cs
new file mode 100644
index 0000000..6ecb608
--- /dev/null
+++ b/launcher/PCL-CE/PCL.Core.SourceGenerators/LifecycleScopeGenerator.cs
@@ -0,0 +1,306 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace PCL.Core.SourceGenerators;
+
+[Generator(LanguageNames.CSharp)]
+public class LifecycleScopeGenerator : IIncrementalGenerator
+{
+ private const string ScopeAttributeType = SharedConstants.LifecycleScopeAttribute;
+
+ private const string StartMethodAttributeType = SharedConstants.LifecycleStartAttribute;
+ private const string StopMethodAttributeType = SharedConstants.LifecycleStopAttribute;
+ private const string CommandHandlerMethodAttributeType = SharedConstants.LifecycleCommandHandlerAttribute;
+ private const string DependencyInjectionMethodAttributeType = SharedConstants.LifecycleDependencyInjectionAttribute;
+ private const string NewDependencyInjectionPointAttributeType = SharedConstants.DependencyInjectionPointAttribute;
+
+ private static readonly HashSet _MethodAttributeTypes = [
+ StartMethodAttributeType, StopMethodAttributeType,
+ CommandHandlerMethodAttributeType, DependencyInjectionMethodAttributeType,
+ NewDependencyInjectionPointAttributeType
+ ];
+
+ private record ScopeMethodModel
+ {
+ public string MethodName { get; init; } = null!;
+ public bool Awaitable { get; init; }
+ }
+
+ private record StartMethodModel : ScopeMethodModel;
+
+ private record StopMethodModel : ScopeMethodModel;
+
+ private record CommandHandlerMethodModel(
+ string Command,
+ bool HasCommandModelArg,
+ bool HasIsCallbackArg,
+ (string Name, string TypeName, bool hasDefaultValue, object? DefaultValue)[] SplitArgs
+ ) : ScopeMethodModel;
+
+ private record DependencyInjectionMethodModel(
+ string Identifier,
+ int Targets,
+ string ParameterType
+ ) : ScopeMethodModel;
+
+ private record NewDependencyInjectionPointModel(
+ string Identifier
+ ) : ScopeMethodModel;
+
+ private class ScopeModel
+ {
+ public string Namespace { get; init; } = null!;
+ public string TypeName { get; init; } = null!;
+ public string QualifiedTypeName => $"{Namespace}.{TypeName}";
+ public string Identifier { get; init; } = null!;
+ public string Name { get; init; } = null!;
+ public bool SupportAsync { get; init; }
+ public List Methods { get; } = [];
+ }
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ // Debugger.Launch();
+ var candidates = context.SyntaxProvider.ForAttributeWithMetadataName(
+ ScopeAttributeType,
+ static (node, _) => node is ClassDeclarationSyntax syntax && syntax.Modifiers.Any(m => m.ValueText == "partial"),
+ static (INamedTypeSymbol, ScopeModel)? (ctx, _) =>
+ {
+ if (ctx.TargetSymbol is not INamedTypeSymbol typeSymbol) return null;
+ var attr = ctx.Attributes[0];
+ var args = attr.ConstructorArguments;
+ var scopeIdentifier = args[0].Value!.ToString();
+ var scopeName = args[1].Value!.ToString();
+ var scopeAsyncStart = true;
+ if (args.Length > 2 && args[2].Value is bool v) scopeAsyncStart = v;
+ var ns = typeSymbol.ContainingNamespace.ToDisplayString();
+ var typeName = typeSymbol.Name;
+ return (typeSymbol, new ScopeModel
+ {
+ Namespace = ns,
+ TypeName = typeName,
+ Identifier = scopeIdentifier,
+ Name = scopeName,
+ SupportAsync = scopeAsyncStart
+ });
+ }
+ ).Where(static i => i is not null).Select(static (i, _) => i.GetValueOrDefault());
+ var collected = candidates.Collect();
+ context.RegisterSourceOutput(collected, _CollectSources);
+ }
+
+ private static void _CollectSources(SourceProductionContext spc, ImmutableArray<(INamedTypeSymbol TypeSymbol, ScopeModel Model)> models)
+ {
+ foreach (var (symbol, model) in models)
+ {
+ model.Methods.Clear();
+ foreach (var member in symbol.GetMembers())
+ {
+ if (member is not IMethodSymbol method) continue;
+ var attrTypeName = string.Empty;
+ var attr = method.GetAttributes().FirstOrDefault(data =>
+ {
+ attrTypeName = data.AttributeClass?.GetSimplifiedTypeName();
+ return attrTypeName is not null && _MethodAttributeTypes.Contains(attrTypeName);
+ });
+ if (attr is null) continue;
+ var methodName = method.Name;
+ var awaitable = method.IsAwaitable();
+ ScopeMethodModel? methodModel = attrTypeName switch
+ {
+ StartMethodAttributeType => new StartMethodModel { MethodName = methodName, Awaitable = awaitable },
+ StopMethodAttributeType => new StopMethodModel { MethodName = methodName, Awaitable = awaitable },
+ CommandHandlerMethodAttributeType => GetCommandHandlerMethodModel(),
+ DependencyInjectionMethodAttributeType => GetDependencyInjectionMethodModel(),
+ NewDependencyInjectionPointAttributeType => GetNewDependencyInjectionPointModel(),
+ _ => null
+ };
+ if (methodModel is not null) model.Methods.Add(methodModel);
+ continue;
+ CommandHandlerMethodModel? GetCommandHandlerMethodModel()
+ {
+ if (awaitable) return null;
+ var command = attr.ConstructorArguments[0].Value!.ToString();
+ var paraArray = method.Parameters;
+ var skip = 0;
+ var hasCommandModelArg = paraArray.Length > 0
+ && paraArray[0].Type.GetSimplifiedTypeName() == "PCL.Core.App.Cli.CommandLine";
+ if (hasCommandModelArg) skip++;
+ var hasIsCallbackArgIndex = hasCommandModelArg ? 1 : 0;
+ var hasIsCallbackArg = paraArray.Length > hasIsCallbackArgIndex
+ && paraArray[hasIsCallbackArgIndex].Type.SpecialType == SpecialType.System_Boolean
+ && paraArray[hasIsCallbackArgIndex].Name == "isCallback";
+ if (hasIsCallbackArg) skip++;
+ var splitArgs = (
+ from para in paraArray.Skip(skip)
+ let name = para.Name
+ let typeName = para.Type.GetFullyQualifiedName()
+ let hasDefaultValue = para.HasExplicitDefaultValue
+ select (name, typeName, hasDefaultValue, hasDefaultValue ? para.ExplicitDefaultValue : null)
+ ).ToArray();
+ return new CommandHandlerMethodModel(command, hasCommandModelArg, hasIsCallbackArg, splitArgs)
+ {
+ MethodName = methodName,
+ Awaitable = false
+ };
+ }
+ DependencyInjectionMethodModel? GetDependencyInjectionMethodModel()
+ {
+ var args = attr.ConstructorArguments;
+ var identifier = args[0].Value!.ToString();
+ var targets = (int)args[1].Value!;
+ if (method.Parameters.FirstOrDefault() is not { } param) return null;
+ var paramType = param.Type.GetFullyQualifiedName();
+ return new DependencyInjectionMethodModel(identifier, targets, paramType)
+ {
+ MethodName = methodName,
+ Awaitable = awaitable
+ };
+ }
+ NewDependencyInjectionPointModel? GetNewDependencyInjectionPointModel()
+ {
+ var args = attr.ConstructorArguments;
+ if (args.Length > 1 && args[1].Value is false) return null; // lifecycleAutoInvoke is set to false
+ var identifier = args[0].Value!.ToString();
+ return new NewDependencyInjectionPointModel(identifier)
+ {
+ MethodName = methodName,
+ Awaitable = awaitable
+ };
+ }
+ }
+ spc.AddSource($"{model.QualifiedTypeName}.g.cs", _GenerateScopeSource(model));
+ }
+ }
+
+ private static readonly HashSet _TypesIncludingInStartMethod = [
+ typeof(StartMethodModel),
+ typeof(CommandHandlerMethodModel),
+ typeof(DependencyInjectionMethodModel),
+ typeof(CommandHandlerMethodModel),
+ typeof(NewDependencyInjectionPointModel),
+ ];
+
+ private static string _GenerateScopeSource(ScopeModel model)
+ {
+ var sb = new StringBuilder();
+
+ // head
+ sb.AppendLine("// 此文件由 Source Generator 自动生成,请勿手动修改");
+ sb.AppendLine("// ");
+ sb.AppendLine();
+ sb.AppendLine("using System;");
+ sb.AppendLine("using System.Threading.Tasks;");
+ sb.AppendLine($"using {SharedConstants.AppNamespace};");
+ sb.AppendLine($"using {SharedConstants.IocNamespace};");
+ sb.AppendLine();
+ sb.AppendLine("#nullable enable");
+ sb.AppendLine();
+ sb.AppendLine($"namespace {model.Namespace};");
+ sb.AppendLine();
+
+ // basic structure
+ sb.AppendLine($"partial class {model.TypeName} : ILifecycleService");
+ sb.AppendLine("{");
+ sb.AppendLine($" public string Identifier => {model.Identifier.ToLiteral()};");
+ sb.AppendLine($" public string Name => {model.Name.ToLiteral()};");
+ sb.AppendLine($" public bool SupportAsync => {(model.SupportAsync ? "true" : "false")};");
+ sb.AppendLine();
+ sb.AppendLine(" private static LifecycleContext Context { get => field ?? throw new InvalidOperationException(\"Not initialized\"); set; } = null!;");
+ sb.AppendLine(" private static ILifecycleService Service => Context.ServiceInstance;");
+ sb.AppendLine($" public {model.TypeName}() {{ Context = Lifecycle.GetContext(this); }}");
+ sb.AppendLine();
+
+ // StopAsync() implementation
+ sb.AppendLine(" public async Task StopAsync()");
+ sb.AppendLine(" {");
+ var stopCount = AppendMethodInvokes(2, model.Methods.Where(x => x is StopMethodModel));
+ sb.AppendLine(" }");
+ sb.AppendLine();
+
+ // StartAsync() implementation
+ sb.AppendLine(" public async Task StartAsync()");
+ sb.AppendLine(" {");
+ AppendMethodInvokes(2, model.Methods.Where(x => _TypesIncludingInStartMethod.Contains(x.GetType())));
+ if (stopCount == 0) sb.AppendLine(" Context.DeclareStopped();");
+ sb.AppendLine(" }");
+
+ // structure tail
+ sb.AppendLine("}");
+
+ return sb.ToString();
+
+ // method invokes implementation
+ int AppendMethodInvokes(int indent, IEnumerable models)
+ {
+ var count = 0;
+ var indentStr = new string(' ', indent * 4);
+ foreach (var methodModel in models)
+ {
+ count++;
+ sb.Append(indentStr).AppendLine("{");
+ foreach (var line in _EmitMethod(methodModel)) sb.Append(indentStr).Append(" ").AppendLine(line);
+ sb.Append(indentStr).AppendLine("}");
+ }
+ return count;
+ }
+ }
+
+ private static IEnumerable _EmitMethod(ScopeMethodModel model)
+ {
+ if (model is StartMethodModel or StopMethodModel)
+ {
+ yield return MethodInvoke();
+ }
+ else if (model is CommandHandlerMethodModel argModel)
+ {
+ var actionParamModel = argModel.HasCommandModelArg ? "model" : "_";
+ var actionParamIsCallback = argModel.HasIsCallbackArg ? "isCallback" : "_";
+ yield return $"Essentials.StartupService.TryHandleCommand(" +
+ $"{argModel.Command.ToLiteral()}, ({actionParamModel}, {actionParamIsCallback}) => {{";
+ var argTexts = new List();
+ if (argModel.HasCommandModelArg) argTexts.Add(actionParamModel);
+ if (argModel.HasIsCallbackArg) argTexts.Add(actionParamIsCallback);
+ foreach (var (name, typeName, hasDefaultValue, defaultValue) in argModel.SplitArgs)
+ {
+ var existsText = "exists_" + name;
+ var isTypeMatchText = "isTypeMatch_" + name;
+ var valueText = "value_" + name;
+ yield return $" var ({(hasDefaultValue ? existsText : "_")}, {isTypeMatchText}) = model.TryGetArgumentValue<{typeName}>(\"{name}\", out var {valueText});";
+ yield return $" if (!{isTypeMatchText}) throw new InvalidCastException(\"Argument type mismatch\");";
+ argTexts.Add(hasDefaultValue ? $"{existsText} ? {valueText} : {defaultValue.ToPrimitive() ?? "default"}" : valueText);
+ }
+ yield return MethodInvoke(" ", argTexts);
+ yield return "}, true);";
+ }
+ else if (model is DependencyInjectionMethodModel diModel)
+ {
+ var awaitable = diModel.Awaitable;
+ if (awaitable) yield return "await Task.Run(() => {";
+ var indentStr = awaitable ? " " : "";
+ if (awaitable) yield return $"{indentStr}Func<{diModel.ParameterType}, Task>";
+ else yield return $"{indentStr}Action<{diModel.ParameterType}>";
+ yield return $"{indentStr} action = {diModel.MethodName};";
+ yield return $"{indentStr}var result = DependencyGroups.InvokeInjection(action, " +
+ $"{diModel.Identifier.ToLiteral()}, " +
+ $"(AttributeTargets){diModel.Targets});";
+ var logStr = diModel.Identifier + "@" + diModel.Targets;
+ yield return $"{indentStr}if (result) Context.Trace(\"Dependency injection success: {logStr}\");";
+ yield return $"{indentStr}else Context.Warn(\"Dependency injection failed: {logStr}\");";
+ if (awaitable) yield return "});";
+ }
+ else if (model is NewDependencyInjectionPointModel newDiModel)
+ {
+ yield return $"{(model.Awaitable ? "await " : "")}" +
+ $"{model.MethodName}_InvokeInjection_{newDiModel.Identifier.SnakeIdToPascal()}();";
+ }
+ yield break;
+ string MethodInvoke(string prefix = "", params IEnumerable args)
+ => $"{prefix}{(model.Awaitable ? "await " : "")}{model.MethodName}({string.Join(", ", args)});";
+ }
+}
diff --git a/launcher/PCL-CE/PCL.Core.SourceGenerators/LifecycleServiceTypesGenerator.cs b/launcher/PCL-CE/PCL.Core.SourceGenerators/LifecycleServiceTypesGenerator.cs
new file mode 100644
index 0000000..8b4ff7e
--- /dev/null
+++ b/launcher/PCL-CE/PCL.Core.SourceGenerators/LifecycleServiceTypesGenerator.cs
@@ -0,0 +1,292 @@
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace PCL.Core.SourceGenerators;
+
+[Generator(LanguageNames.CSharp)]
+public class LifecycleServiceTypesGenerator : IIncrementalGenerator
+{
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ // 查找 LifecycleState.cs 文件以获取有效的枚举值
+ var lifecycleStateProvider = context.AdditionalTextsProvider
+ .Where(static file => file.Path.EndsWith("LifecycleState.cs"))
+ .Select(static (text, cancellationToken) =>
+ {
+ var content = text.GetText(cancellationToken)?.ToString();
+ return _GetValidLifecycleStates(content);
+ })
+ .Where(static states => states?.Count > 0)
+ .Collect();
+
+ // 查找带有 LifecycleService 属性的类
+ var serviceClassProvider = context.SyntaxProvider.CreateSyntaxProvider(
+ predicate: static (s, _) => s is ClassDeclarationSyntax { AttributeLists.Count: > 0 },
+ transform: static (ctx, _) => _GetLifecycleServiceInfo(ctx))
+ .Where(static x => x is not null);
+
+ // 收集所有服务信息
+ var servicesProvider = serviceClassProvider.Collect();
+
+ // 合并枚举状态和服务信息
+ var combinedProvider = lifecycleStateProvider.Combine(servicesProvider);
+
+ // 生成代码
+ context.RegisterSourceOutput(combinedProvider,
+ static (spc, data) => _Execute(spc, data.Left.FirstOrDefault() ?? new List(), [..data.Right.Where(x => x is not null).Select(x => x!)]));
+ }
+
+ private static List? _GetValidLifecycleStates(string? content)
+ {
+ if (string.IsNullOrEmpty(content))
+ return null;
+
+ var validStates = new List();
+
+ // 提取枚举定义块
+ var enumPattern = @"(?s)public\s+enum\s+LifecycleState\s*\{(.*?)\}";
+ var enumMatch = Regex.Match(content, enumPattern);
+
+ if (!enumMatch.Success)
+ return null;
+
+ var enumContent = enumMatch.Groups[1].Value;
+
+ // 按行分割并处理每一行
+ var lines = enumContent.Split('\n');
+ foreach (var line in lines)
+ {
+ var trimmedLine = line.Trim();
+
+ // 跳过空行、注释行和花括号
+ if (string.IsNullOrEmpty(trimmedLine) ||
+ trimmedLine.StartsWith("///") ||
+ trimmedLine.StartsWith("//") ||
+ trimmedLine.StartsWith("/*") ||
+ trimmedLine.StartsWith("*") ||
+ trimmedLine == "{" ||
+ trimmedLine == "}")
+ {
+ continue;
+ }
+
+ // 匹配枚举成员(可能包含逗号)
+ var memberMatch = Regex.Match(trimmedLine, @"^(\w+)\s*,?\s*$");
+ if (memberMatch.Success)
+ {
+ var enumValue = memberMatch.Groups[1].Value;
+ if (!string.IsNullOrEmpty(enumValue) && enumValue != "LifecycleState")
+ {
+ validStates.Add(enumValue);
+ }
+ }
+ }
+
+ return validStates.Count > 0 ? validStates : null;
+ }
+
+ private static LifecycleServiceInfo? _GetLifecycleServiceInfo(GeneratorSyntaxContext context)
+ {
+ var classDeclaration = (ClassDeclarationSyntax)context.Node;
+
+ // 查找 LifecycleService 属性
+ var lifecycleAttribute = classDeclaration.AttributeLists
+ .SelectMany(al => al.Attributes)
+ .FirstOrDefault(a => a.Name.ToString().Contains("LifecycleService"));
+
+ if (lifecycleAttribute is null)
+ return null;
+
+ // 获取语义模型信息
+ var semanticModel = context.SemanticModel;
+ var classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration);
+ if (classSymbol is null)
+ return null;
+
+ // 解析属性参数
+ var state = "Unknown";
+ var priority = 0;
+
+ if (lifecycleAttribute.ArgumentList?.Arguments.Count > 0)
+ {
+ // 解析第一个参数(状态)
+ var firstArg = lifecycleAttribute.ArgumentList.Arguments[0];
+ if (firstArg.Expression is MemberAccessExpressionSyntax memberAccess)
+ {
+ state = memberAccess.Name.Identifier.ValueText;
+ }
+
+ // 查找 Priority 参数(支持命名参数和位置参数)
+ var priorityArg = lifecycleAttribute.ArgumentList.Arguments
+ .FirstOrDefault(arg => arg.NameEquals?.Name.Identifier.ValueText == "Priority");
+
+ // 如果没有找到命名的Priority参数,检查第二个位置参数
+ if (priorityArg is null && lifecycleAttribute.ArgumentList.Arguments.Count > 1)
+ {
+ priorityArg = lifecycleAttribute.ArgumentList.Arguments[1];
+ }
+
+ if (priorityArg is not null)
+ {
+ priority = _ParsePriorityExpression(priorityArg.Expression, semanticModel);
+ }
+ }
+
+ return new LifecycleServiceInfo(
+ classSymbol.ToDisplayString(),
+ classSymbol.Name,
+ state,
+ priority);
+ }
+
+ private static int _ParsePriorityExpression(ExpressionSyntax expression, SemanticModel semanticModel)
+ {
+ switch (expression)
+ {
+ case LiteralExpressionSyntax literal:
+ if (int.TryParse(literal.Token.ValueText, out var literalValue))
+ return literalValue;
+ break;
+
+ case MemberAccessExpressionSyntax memberAccess:
+ var memberName = memberAccess.ToString();
+ if (memberName == "int.MaxValue")
+ return int.MaxValue;
+ if (memberName == "int.MinValue")
+ return int.MinValue;
+ break;
+
+ case PrefixUnaryExpressionSyntax unary when unary.IsKind(SyntaxKind.UnaryMinusExpression):
+ // 处理负数
+ if (unary.Operand is LiteralExpressionSyntax negLiteral &&
+ int.TryParse(negLiteral.Token.ValueText, out var negValue))
+ {
+ return -negValue;
+ }
+ break;
+
+ case BinaryExpressionSyntax binary:
+ // 简单的数学表达式支持
+ var left = _ParsePriorityExpression(binary.Left, semanticModel);
+ var right = _ParsePriorityExpression(binary.Right, semanticModel);
+
+ return binary.OperatorToken.Kind() switch
+ {
+ SyntaxKind.PlusToken => left + right,
+ SyntaxKind.MinusToken => left - right,
+ SyntaxKind.AsteriskToken => left * right,
+ SyntaxKind.SlashToken => right != 0 ? left / right : 0,
+ _ => 0
+ };
+ }
+
+ // 尝试获取常量值
+ var constantValue = semanticModel.GetConstantValue(expression);
+ return constantValue is { HasValue: true, Value: int intValue } ? intValue : 0;
+ }
+
+ private static void _Execute(SourceProductionContext context, List validStates, ImmutableArray services)
+ {
+ // 过滤服务,只保留有效状态的服务
+ var filteredServices = services.Where(s => validStates.Count == 0 || validStates.Contains(s.State)).ToList();
+
+ // 按状态分组并排序
+ var groupedServices = filteredServices
+ .GroupBy(s => s.State)
+ .OrderBy(g => g.Key)
+ .ToList();
+
+ var sb = new StringBuilder();
+ sb.AppendLine("// ");
+ sb.AppendLine("// 此文件由 Source Generator 自动生成,请勿手动修改");
+ sb.AppendLine();
+ sb.AppendLine("using System;");
+ sb.AppendLine();
+ sb.AppendLine("namespace PCL.Core.App.IoC;");
+ sb.AppendLine();
+ sb.AppendLine("/// ");
+ sb.AppendLine("/// 包含所有使用 LifecycleService 注解的类型,按 StartState 分类并按 Priority 降序排序");
+ sb.AppendLine("/// ");
+ sb.AppendLine("public static class LifecycleServiceTypes");
+ sb.AppendLine("{");
+
+ // 为每个状态生成数组
+ foreach (var group in groupedServices)
+ {
+ var sortedServices = group.OrderByDescending(s => s.Priority).ToList();
+
+ sb.AppendLine($" /// ");
+ sb.AppendLine($" /// {group.Key} 状态的生命周期服务类型");
+ sb.AppendLine($" /// ");
+ sb.AppendLine($" public static readonly Type[] {group.Key} = [");
+
+ foreach (var service in sortedServices)
+ {
+ sb.AppendLine($" typeof({service.FullName}), // Priority: {service.Priority}");
+ }
+
+ sb.AppendLine(" ];");
+ sb.AppendLine();
+ }
+
+ // 生成 GetServiceTypes 方法
+ sb.AppendLine(" /// ");
+ sb.AppendLine(" /// 获取指定生命周期状态的所有服务类型");
+ sb.AppendLine(" /// ");
+ sb.AppendLine(" /// 生命周期状态");
+ sb.AppendLine(" /// 该状态下的所有服务类型数组");
+ sb.AppendLine(" public static Type[] GetServiceTypes(LifecycleState state) => state switch");
+ sb.AppendLine(" {");
+
+ foreach (var group in groupedServices)
+ {
+ sb.AppendLine($" LifecycleState.{group.Key} => {group.Key},");
+ }
+
+ sb.AppendLine(" _ => new Type[0]");
+ sb.AppendLine(" };");
+ sb.AppendLine();
+
+ // 生成 GetAllServiceTypes 方法
+ sb.AppendLine(" /// ");
+ sb.AppendLine(" /// 获取所有生命周期服务类型的状态映射");
+ sb.AppendLine(" /// ");
+ sb.AppendLine(" /// 状态到类型数组的字典");
+ sb.AppendLine(" public static System.Collections.Generic.Dictionary GetAllServiceTypes() => new()");
+ sb.AppendLine(" {");
+
+ foreach (var group in groupedServices)
+ {
+ sb.AppendLine($" [LifecycleState.{group.Key}] = {group.Key},");
+ }
+
+ sb.AppendLine(" };");
+ sb.AppendLine();
+
+ // 生成统计信息方法
+ sb.AppendLine(" /// ");
+ sb.AppendLine(" /// 获取生命周期服务的统计信息");
+ sb.AppendLine(" /// ");
+ sb.AppendLine(" /// 包含状态数量和总服务数量的统计信息");
+ sb.AppendLine($" public static (int StateCount, int TotalServices) GetStatistics() => ({groupedServices.Count}, {filteredServices.Count});");
+
+ sb.AppendLine("}");
+
+ context.AddSource("LifecycleServiceTypes.g.cs", sb.ToString());
+ }
+}
+
+// 破烂 .NET Standard 用不了 init 修饰没法 record,先这样吧
+public class LifecycleServiceInfo(string fullName, string className, string state, int priority)
+{
+ public string FullName { get; } = fullName;
+ public string ClassName { get; } = className;
+ public string State { get; } = state;
+ public int Priority { get; } = priority;
+}
diff --git a/launcher/PCL-CE/PCL.Core.SourceGenerators/PCL.Core.SourceGenerators.csproj b/launcher/PCL-CE/PCL.Core.SourceGenerators/PCL.Core.SourceGenerators.csproj
new file mode 100644
index 0000000..96d3baa
--- /dev/null
+++ b/launcher/PCL-CE/PCL.Core.SourceGenerators/PCL.Core.SourceGenerators.csproj
@@ -0,0 +1,22 @@
+
+
+
+ netstandard2.0
+ latest
+ enable
+ false
+ false
+ true
+ AnyCPU;x64;ARM64
+ Debug;CI;Release;Beta
+
+ AnyCPU
+ false
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/launcher/PCL-CE/PCL.Core.SourceGenerators/SharedConstants.cs b/launcher/PCL-CE/PCL.Core.SourceGenerators/SharedConstants.cs
new file mode 100644
index 0000000..b9b4400
--- /dev/null
+++ b/launcher/PCL-CE/PCL.Core.SourceGenerators/SharedConstants.cs
@@ -0,0 +1,14 @@
+namespace PCL.Core.SourceGenerators;
+
+public static class SharedConstants
+{
+ public const string AppNamespace = "PCL.Core.App";
+ public const string IocNamespace = $"{AppNamespace}.IoC";
+ public const string DependencyCollectorAttribute = $"{IocNamespace}.DependencyCollectorAttribute";
+ public const string DependencyInjectionPointAttribute = $"{IocNamespace}.DependencyInjectionPointAttribute";
+ public const string LifecycleScopeAttribute = $"{IocNamespace}.LifecycleScopeAttribute";
+ public const string LifecycleStartAttribute = $"{IocNamespace}.LifecycleStartAttribute";
+ public const string LifecycleStopAttribute = $"{IocNamespace}.LifecycleStopAttribute";
+ public const string LifecycleCommandHandlerAttribute = $"{IocNamespace}.LifecycleCommandHandlerAttribute";
+ public const string LifecycleDependencyInjectionAttribute = $"{IocNamespace}.LifecycleDependencyInjectionAttribute";
+}
diff --git a/launcher/PCL-CE/PCL.Core.SourceGenerators/SharedExtensions.cs b/launcher/PCL-CE/PCL.Core.SourceGenerators/SharedExtensions.cs
new file mode 100644
index 0000000..5842804
--- /dev/null
+++ b/launcher/PCL-CE/PCL.Core.SourceGenerators/SharedExtensions.cs
@@ -0,0 +1,252 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace PCL.Core.SourceGenerators;
+
+public static class SharedExtensions
+{
+ public static string ToLiteral(this string str) => SymbolDisplay.FormatLiteral(str, true);
+
+ public static string? ToPrimitive(this object? obj) => SymbolDisplay.FormatPrimitive(obj, true, false);
+
+ public static int GetDeclarationOrder(this ISymbol symbol)
+ {
+ var loc = symbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax().GetLocation();
+ return loc?.SourceSpan.Start ?? int.MaxValue;
+ }
+
+ extension(INamedTypeSymbol type)
+ {
+ public bool IsPartial()
+ {
+ foreach (var decl in type.DeclaringSyntaxReferences)
+ {
+ if (decl.GetSyntax() is ClassDeclarationSyntax { Modifiers: { } modifiers } &&
+ modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)))
+ return true;
+ }
+ return false;
+ }
+
+ public bool IsNestedWithin(INamedTypeSymbol potentialContainer)
+ {
+ var t = type.ContainingType;
+ while (t is not null)
+ {
+ if (SymbolEqualityComparer.Default.Equals(t, potentialContainer))
+ return true;
+ t = t.ContainingType;
+ }
+ return false;
+ }
+
+ public bool IsAttribute()
+ {
+ var baseType = type.BaseType;
+ while (baseType is not null)
+ {
+ if (baseType.ToDisplayString() == "System.Attribute") return true;
+ baseType = baseType.BaseType;
+ }
+ return false;
+ }
+
+ public int GenerateTypeHeader(StringBuilder sb)
+ {
+ var ctnTypes = new Stack();
+ for (var ctnType = type.ContainingType; ctnType is not null; ctnType = ctnType.ContainingType) ctnTypes.Push(ctnType);
+ // namespace
+ var ns = type.ContainingNamespace?.ToDisplayString();
+ var indent = 0;
+ if (!string.IsNullOrEmpty(ns))
+ {
+ sb.Append("namespace ").Append(ns).AppendLine();
+ sb.AppendLine("{");
+ indent++;
+ }
+ // outer classes
+ foreach (var containingType in ctnTypes)
+ {
+ sb.Append(' ', indent * 4).Append("partial class ").Append(containingType.Name).AppendLine();
+ sb.Append(' ', indent * 4).AppendLine("{");
+ indent++;
+ }
+ // class
+ sb.Append(' ', indent * 4).Append("partial class ").Append(type.Name).AppendLine();
+ sb.Append(' ', indent * 4).AppendLine("{");
+ return indent + 1;
+ }
+ }
+
+ public static string RenderDefaultValueCode(this SemanticModel sm, ExpressionSyntax expr)
+ {
+ if (expr is LiteralExpressionSyntax || expr.IsNegativeNumeric())
+ return expr.ToString();
+
+ if (expr is TypeOfExpressionSyntax toe)
+ {
+ var type = sm.GetTypeInfo(toe.Type).Type;
+ if (type is not null)
+ return "typeof(" + type.GetFullyQualifiedName() + ")";
+ return expr.ToString();
+ }
+
+ if (expr is InvocationExpressionSyntax
+ {
+ Expression: IdentifierNameSyntax { Identifier.ValueText: "nameof" },
+ ArgumentList.Arguments.Count: 1
+ } inv)
+ {
+ var targetExpr = inv.ArgumentList.Arguments[0].Expression;
+ var sym = sm.GetSymbolInfo(targetExpr).Symbol;
+ if (sym is not null)
+ {
+ return "nameof(" + sym.GetQualifiedSymbolName() + ")";
+ }
+ return expr.ToString();
+ }
+
+ var s = sm.GetSymbolInfo(expr).Symbol;
+ if (s is IFieldSymbol fs)
+ {
+ return fs.GetQualifiedSymbolName();
+ }
+
+ return expr.ToString();
+ }
+
+ public static bool IsNegativeNumeric(this ExpressionSyntax expr)
+ {
+ return expr is PrefixUnaryExpressionSyntax p
+ && p.IsKind(SyntaxKind.UnaryMinusExpression)
+ && p.Operand is LiteralExpressionSyntax l
+ && l.IsKind(SyntaxKind.NumericLiteralExpression);
+ }
+
+ extension(ISymbol symbol)
+ {
+ public string GetQualifiedSymbolName()
+ {
+ if (symbol is ITypeSymbol ts) return ts.GetFullyQualifiedName();
+
+ var parts = new Stack();
+ parts.Push(symbol.Name);
+ var t = symbol.ContainingType;
+ while (t is not null)
+ {
+ parts.Push(t.Name);
+ t = t.ContainingType;
+ }
+ var ns = symbol.ContainingNamespace?.ToDisplayString();
+ if (!string.IsNullOrEmpty(ns)) parts.Push(ns!);
+ return string.Join(".", parts);
+ }
+ }
+
+ private static readonly SymbolDisplayFormat _SimplifiedTypeNameFormat = new(
+ globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.OmittedAsContaining,
+ typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
+ miscellaneousOptions:
+ SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers |
+ SymbolDisplayMiscellaneousOptions.CollapseTupleTypes |
+ SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier |
+ SymbolDisplayMiscellaneousOptions.UseSpecialTypes,
+ genericsOptions: SymbolDisplayGenericsOptions.None
+ );
+
+ private static readonly SymbolDisplayFormat _FullQualifiedNameFormat = new(
+ typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
+ genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
+ miscellaneousOptions:
+ SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers |
+ SymbolDisplayMiscellaneousOptions.UseSpecialTypes
+ );
+
+ extension(ITypeSymbol type)
+ {
+ public string GetSimplifiedTypeName()
+ {
+ return type.ToDisplayString(_SimplifiedTypeNameFormat);
+ }
+
+ public string GetFullyQualifiedName()
+ {
+ if (type is INamedTypeSymbol {
+ OriginalDefinition.SpecialType: SpecialType.System_Nullable_T,
+ TypeArguments.Length: 1 } nt)
+ {
+ var inner = nt.TypeArguments[0];
+ return inner.GetFullyQualifiedName() + "?";
+ }
+ if (type.TryGetSpecialTypeKeyword(out var keyword)) return keyword;
+ return type.ToDisplayString(_FullQualifiedNameFormat);
+ }
+
+ public bool TryGetSpecialTypeKeyword(out string keyword)
+ {
+ switch (type.SpecialType)
+ {
+ case SpecialType.System_Boolean: keyword = "bool"; return true;
+ case SpecialType.System_Byte: keyword = "byte"; return true;
+ case SpecialType.System_SByte: keyword = "sbyte"; return true;
+ case SpecialType.System_Int16: keyword = "short"; return true;
+ case SpecialType.System_UInt16: keyword = "ushort"; return true;
+ case SpecialType.System_Int32: keyword = "int"; return true;
+ case SpecialType.System_UInt32: keyword = "uint"; return true;
+ case SpecialType.System_Int64: keyword = "long"; return true;
+ case SpecialType.System_UInt64: keyword = "ulong"; return true;
+ case SpecialType.System_IntPtr: keyword = "nint"; return true;
+ case SpecialType.System_UIntPtr: keyword = "nuint"; return true;
+ case SpecialType.System_Char: keyword = "char"; return true;
+ case SpecialType.System_String: keyword = "string"; return true;
+ case SpecialType.System_Object: keyword = "object"; return true;
+ case SpecialType.System_Single: keyword = "float"; return true;
+ case SpecialType.System_Double: keyword = "double"; return true;
+ case SpecialType.System_Decimal: keyword = "decimal"; return true;
+ default: keyword = ""; return false;
+ }
+ }
+ }
+
+ public static string GetQualifiedPropertyAccess(this IPropertySymbol prop)
+ {
+ var owner = prop.ContainingType.GetFullyQualifiedName();
+ return owner + "." + prop.Name;
+ }
+
+ public static bool IsAwaitable(this IMethodSymbol method)
+ {
+ // TODO this is a very naive implementation.
+ return method.ReturnType.GetSimplifiedTypeName() == "System.Threading.Tasks.Task";
+ }
+
+ public static string CorrectConfigTypeName(this string typeName, out string? fullTypeName)
+ {
+ var isArgConfig = typeName.StartsWith("PCL.Core.App.Configuration.ArgConfig<");
+ if (isArgConfig)
+ {
+ fullTypeName = typeName;
+ typeName = typeName.Substring(37, typeName.Length - 38);
+ }
+ else fullTypeName = null;
+ return typeName;
+ }
+
+ extension(string str)
+ {
+ public string SnakeIdToPascal()
+ {
+ var sb = new StringBuilder();
+ foreach (var part in str.Split('-'))
+ {
+ if (part.Length == 0) continue;
+ sb.Append(char.ToUpper(part[0])).Append(part.Substring(1));
+ }
+ return sb.ToString();
+ }
+ }
+}
diff --git a/launcher/PCL-CE/PCL.Core.SourceGenerators/System.Runtime.CompilerServices.cs b/launcher/PCL-CE/PCL.Core.SourceGenerators/System.Runtime.CompilerServices.cs
new file mode 100644
index 0000000..cdc6bdf
--- /dev/null
+++ b/launcher/PCL-CE/PCL.Core.SourceGenerators/System.Runtime.CompilerServices.cs
@@ -0,0 +1,10 @@
+namespace System.Runtime.CompilerServices;
+
+using ComponentModel;
+
+///
+/// Reserved to be used by the compiler for tracking metadata.
+/// This class should not be used by developers in source code.
+///
+[EditorBrowsable(EditorBrowsableState.Never)]
+internal static class IsExternalInit;
diff --git a/launcher/PCL-CE/PCL.Core/.editorconfig b/launcher/PCL-CE/PCL.Core/.editorconfig
new file mode 100644
index 0000000..c64acf1
--- /dev/null
+++ b/launcher/PCL-CE/PCL.Core/.editorconfig
@@ -0,0 +1,177 @@
+# PCL.Core 项目级别的编辑器规则
+
+[*.cs]
+
+# 定义符号
+
+# 常量
+dotnet_naming_symbols.constants.applicable_kinds = field
+dotnet_naming_symbols.constants.applicable_accessibilities = *
+dotnet_naming_symbols.constants.required_modifiers = const
+
+# 局部变量 & 方法/构造函数参数
+dotnet_naming_symbols.local_and_parameter.applicable_kinds = local, parameter
+dotnet_naming_symbols.local_and_parameter.applicable_accessibilities = *
+dotnet_naming_symbols.local_and_parameter.required_modifiers =
+
+# 私有实例只读字段
+dotnet_naming_symbols.private_instance_readonly_fields.applicable_kinds = field
+dotnet_naming_symbols.private_instance_readonly_fields.applicable_accessibilities = private
+dotnet_naming_symbols.private_instance_readonly_fields.required_modifiers = readonly
+
+# 私有静态只读字段
+dotnet_naming_symbols.private_static_readonly_fields.applicable_kinds = field
+dotnet_naming_symbols.private_static_readonly_fields.applicable_accessibilities = private
+dotnet_naming_symbols.private_static_readonly_fields.required_modifiers = static, readonly
+
+# 私有实例字段(非只读)
+dotnet_naming_symbols.private_instance_fields.applicable_kinds = field
+dotnet_naming_symbols.private_instance_fields.applicable_accessibilities = private
+dotnet_naming_symbols.private_instance_fields.required_modifiers =
+
+# 私有静态字段(非只读)
+dotnet_naming_symbols.private_static_fields.applicable_kinds = field
+dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private
+dotnet_naming_symbols.private_static_fields.required_modifiers = static
+
+# 私有实例属性
+dotnet_naming_symbols.private_instance_properties.applicable_kinds = property
+dotnet_naming_symbols.private_instance_properties.applicable_accessibilities = private
+dotnet_naming_symbols.private_instance_properties.required_modifiers =
+
+# 私有静态属性
+dotnet_naming_symbols.private_static_properties.applicable_kinds = property
+dotnet_naming_symbols.private_static_properties.applicable_accessibilities = private
+dotnet_naming_symbols.private_static_properties.required_modifiers = static
+
+# 其它实例变量(public/internal/protected 的字段、属性、事件)
+dotnet_naming_symbols.variables_instance_others.applicable_kinds = field, property, event
+dotnet_naming_symbols.variables_instance_others.applicable_accessibilities = public, internal, protected, protected_internal, private_protected
+dotnet_naming_symbols.variables_instance_others.required_modifiers =
+
+# 其它静态变量(public/internal/protected 的字段、属性、事件)
+dotnet_naming_symbols.variables_static_others.applicable_kinds = field, property, event
+dotnet_naming_symbols.variables_static_others.applicable_accessibilities = public, internal, protected, protected_internal, private_protected
+dotnet_naming_symbols.variables_static_others.required_modifiers = static
+
+# 私有方法(静态+实例)
+dotnet_naming_symbols.private_methods.applicable_kinds = method
+dotnet_naming_symbols.private_methods.applicable_accessibilities = private
+dotnet_naming_symbols.private_methods.required_modifiers =
+
+# 非私有方法(静态+实例)
+dotnet_naming_symbols.non_private_methods.applicable_kinds = method
+dotnet_naming_symbols.non_private_methods.applicable_accessibilities = public, internal, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_private_methods.required_modifiers =
+
+# 局部方法(local function)
+dotnet_naming_symbols.local_functions.applicable_kinds = local_function
+dotnet_naming_symbols.local_functions.applicable_accessibilities = *
+dotnet_naming_symbols.local_functions.required_modifiers =
+
+# 接口
+dotnet_naming_symbols.interfaces.applicable_kinds = interface
+dotnet_naming_symbols.interfaces.applicable_accessibilities = *
+dotnet_naming_symbols.interfaces.required_modifiers =
+
+# 其它类型(class, struct, enum, delegate)
+dotnet_naming_symbols.other_types.applicable_kinds = class, struct, enum, delegate
+dotnet_naming_symbols.other_types.applicable_accessibilities = *
+dotnet_naming_symbols.other_types.required_modifiers =
+
+# 定义命名风格
+
+# camelCase
+dotnet_naming_style.camel_case_style.capitalization = camel_case
+
+# PascalCase
+dotnet_naming_style.pascal_case_style.capitalization = pascal_case
+
+# _PascalCase
+dotnet_naming_style.underscore_pascal_case_style.capitalization = pascal_case
+dotnet_naming_style.underscore_pascal_case_style.required_prefix = _
+
+# _camelCase
+dotnet_naming_style.underscore_camel_case_style.capitalization = camel_case
+dotnet_naming_style.underscore_camel_case_style.required_prefix = _
+
+# IPascalCase
+dotnet_naming_style.prefix_I_pascal_style.capitalization = pascal_case
+dotnet_naming_style.prefix_I_pascal_style.required_prefix = I
+
+# 绑定命名规则
+
+# 常量 应 PascalCase
+dotnet_naming_rule.const.symbols = constants
+dotnet_naming_rule.const.style = pascal_case_style
+dotnet_naming_rule.const.severity = warning
+
+# 局部变量 & 参数 应 camelCase
+dotnet_naming_rule.local_var.symbols = local_and_parameter
+dotnet_naming_rule.local_var.style = camel_case_style
+dotnet_naming_rule.local_var.severity = warning
+
+# 私有实例只读字段 应 _camelCase
+dotnet_naming_rule.private_readonly_field.symbols = private_instance_readonly_fields
+dotnet_naming_rule.private_readonly_field.style = underscore_camel_case_style
+dotnet_naming_rule.private_readonly_field.severity = warning
+
+# 私有静态只读字段 应 _PascalCase
+dotnet_naming_rule.private_static_readonly_field.symbols = private_static_readonly_fields
+dotnet_naming_rule.private_static_readonly_field.style = underscore_pascal_case_style
+dotnet_naming_rule.private_static_readonly_field.severity = warning
+
+# 私有实例字段 应 _camelCase
+dotnet_naming_rule.private_field.symbols = private_instance_fields
+dotnet_naming_rule.private_field.style = underscore_camel_case_style
+dotnet_naming_rule.private_field.severity = warning
+
+# 私有静态字段 应 _camelCase
+dotnet_naming_rule.private_static_field.symbols = private_static_fields
+dotnet_naming_rule.private_static_field.style = underscore_camel_case_style
+dotnet_naming_rule.private_static_field.severity = warning
+
+# 私有实例属性 应 _PascalCase
+dotnet_naming_rule.private_property.symbols = private_instance_properties
+dotnet_naming_rule.private_property.style = underscore_pascal_case_style
+dotnet_naming_rule.private_property.severity = warning
+
+# 私有静态属性 应 PascalCase
+dotnet_naming_rule.private_static_property.symbols = private_static_properties
+dotnet_naming_rule.private_static_property.style = pascal_case_style
+dotnet_naming_rule.private_static_property.severity = warning
+
+# 其它实例变量 应 PascalCase
+dotnet_naming_rule.field.symbols = variables_instance_others
+dotnet_naming_rule.field.style = pascal_case_style
+dotnet_naming_rule.field.severity = warning
+
+# 其它静态变量 应 PascalCase
+dotnet_naming_rule.static_field.symbols = variables_static_others
+dotnet_naming_rule.static_field.style = pascal_case_style
+dotnet_naming_rule.static_field.severity = warning
+
+# 私有方法 应 _PascalCase
+dotnet_naming_rule.private_method.symbols = private_methods
+dotnet_naming_rule.private_method.style = underscore_pascal_case_style
+dotnet_naming_rule.private_method.severity = warning
+
+# 非私有方法 应 PascalCase
+dotnet_naming_rule.method.symbols = non_private_methods
+dotnet_naming_rule.method.style = pascal_case_style
+dotnet_naming_rule.method.severity = warning
+
+# 局部方法 应 PascalCase
+dotnet_naming_rule.local_func.symbols = local_functions
+dotnet_naming_rule.local_func.style = pascal_case_style
+dotnet_naming_rule.local_func.severity = warning
+
+# 接口 应 IPascalCase
+dotnet_naming_rule.interface.symbols = interfaces
+dotnet_naming_rule.interface.style = prefix_I_pascal_style
+dotnet_naming_rule.interface.severity = warning
+
+# 其它类型 应 PascalCase
+dotnet_naming_rule.type.symbols = other_types
+dotnet_naming_rule.type.style = pascal_case_style
+dotnet_naming_rule.type.severity = warning
diff --git a/launcher/PCL-CE/PCL.Core/.gitignore b/launcher/PCL-CE/PCL.Core/.gitignore
new file mode 100644
index 0000000..6a35cb3
--- /dev/null
+++ b/launcher/PCL-CE/PCL.Core/.gitignore
@@ -0,0 +1,410 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+[Aa][Rr][Mm]64[Ee][Cc]/
+bld/
+[Bb]in/
+[Oo]bj/
+[Oo]ut/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+# but not Directory.Build.rsp, as it configures directory-level build defaults
+!Directory.Build.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# AWS SAM Build and Temporary Artifacts folder
+.aws-sam
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# Generated Code
+**/*.g.cs
diff --git a/launcher/PCL-CE/PCL.Core/App/Basics.cs b/launcher/PCL-CE/PCL.Core/App/Basics.cs
new file mode 100644
index 0000000..7a257ca
--- /dev/null
+++ b/launcher/PCL-CE/PCL.Core/App/Basics.cs
@@ -0,0 +1,195 @@
+using System;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.IO;
+using System.Reflection;
+using System.Text.Json;
+using System.Threading;
+using System.Windows;
+using PCL.Core.Logging;
+using PCL.Core.Utils;
+
+namespace PCL.Core.App;
+
+///
+/// 基础工具集。
+///
+public static class Basics
+{
+ #region 基本信息
+
+ ///
+ /// 启动器元数据。
+ ///
+ public static MetadataModel Metadata { get; } = JsonSerializer.Deserialize(
+ Assembly.GetEntryAssembly()!.GetManifestResourceStream("PCL.metadata.json")!, JsonCompat.SerializerOptions)!;
+
+ ///
+ /// 版本名称。
+ ///
+ public static string VersionName => Metadata.Version.BaseName;
+
+ ///
+ /// 版本内部代号。
+ ///
+ public static int VersionCode => Metadata.Version.Code;
+
+ ///
+ /// 版本分支名。
+ ///
+ public static string VersionBranch => Metadata.Version.BranchName;
+
+ ///
+ /// 当前日期是否为愚人节。
+ ///
+ public static bool IsAprilFool => DateTime.Now is { Month: 4, Day: 1 };
+
+ #endregion
+
+ #region 程序路径信息
+
+ ///
+ /// 当前进程实例。
+ ///
+ public static Process CurrentProcess { get; } = Process.GetCurrentProcess();
+
+ ///
+ /// 当前进程 ID。
+ ///
+ public static int CurrentProcessId { get; } = CurrentProcess.Id;
+
+ ///
+ /// 当前进程可执行文件的绝对路径。
+ ///
+ public static string ExecutablePath { get; } = Environment.ProcessPath!;
+
+ ///
+ /// 当前进程可执行文件所在的目录。若有需求,请使用 而不是自行拼接路径。
+ ///
+ public static string ExecutableDirectory { get; } = GetParentPath(ExecutablePath) ?? CurrentDirectory;
+
+ ///
+ /// 当前进程可执行文件的名称,含扩展名。
+ ///
+ public static string ExecutableName { get; } = Path.GetFileName(ExecutablePath);
+
+ ///
+ /// 当前进程可执行文件的名称,不含扩展名。
+ ///
+ public static string ExecutableNameWithoutExtension { get; } = Path.GetFileNameWithoutExtension(ExecutablePath);
+
+ ///
+ /// 当前进程包括第一个参数(文件名)的完整命令行参数。
+ ///
+ public static string[] FullCommandLineArguments { get; } = Environment.GetCommandLineArgs();
+
+ ///
+ /// 当前进程不包括第一个参数(文件名)的命令行参数。
+ ///
+ public static string[] CommandLineArguments { get; } = FullCommandLineArguments[1..];
+
+ ///
+ /// 实时获取的当前目录。若要在可执行文件目录中存放文件等内容,请使用更准确的 而不是这个目录。
+ ///
+ public static string CurrentDirectory => Environment.CurrentDirectory;
+
+ #endregion
+
+ #region 线程操作
+
+ ///
+ /// 在新的工作线程运行指定委托。
+ ///
+ /// 要运行的委托
+ /// 线程名,默认为 WorkerThread@[ThreadId]
+ /// 线程优先级
+ /// 新创建的线程实例
+ public static Thread RunInNewThread(Action action, string? name = null, ThreadPriority priority = ThreadPriority.Normal)
+ {
+ var threadName = new AtomicVariable(name);
+ var thread = new Thread(() =>
+ {
+ try { action(); }
+ catch (ThreadInterruptedException) { LogWrapper.Trace("Thread", $"{threadName.Value}: 已中止"); }
+ catch (Exception ex) { LogWrapper.Error(ex, "Thread", $"{threadName.Value}: 抛出异常"); }
+ })
+ { Priority = priority };
+ threadName.Value ??= $"Worker#{thread.ManagedThreadId}";
+ thread.Name = threadName.Value;
+ thread.Start();
+ return thread;
+ }
+
+ #endregion
+
+ #region 路径操作
+
+ ///
+ /// 获取某个路径的父路径/目录。
+ ///
+ /// 路径文本
+ /// 父路径文本,可能为 null
+ public static string? GetParentPath(string path) => Path.GetDirectoryName(path) ?? Path.GetPathRoot(path);
+
+ ///
+ /// 获取某个路径的父路径/目录。
+ ///
+ /// 路径文本
+ /// 父路径文本,或空白
+ public static string GetParentPathOrEmpty(string path) => GetParentPath(path) ?? string.Empty;
+
+ ///
+ /// 获取某个路径的父路径/目录。
+ ///
+ /// 路径文本
+ /// 父路径文本,或默认 ()
+ public static string GetParentPathOrDefault(string path) => GetParentPath(path) ?? CurrentDirectory;
+
+ ///
+ /// 以默认方式打开一个路径 (文件或目录)
+ ///
+ /// 路径文本
+ /// 执行工作目录
+ public static void OpenPath(string path, string? workingDirectory = null)
+ {
+ var psi = new ProcessStartInfo(path)
+ {
+ WorkingDirectory = workingDirectory ?? CurrentDirectory,
+ UseShellExecute = true,
+ CreateNoWindow = true
+ };
+ try
+ {
+ Process.Start(psi);
+ }
+ catch (Win32Exception ex) when (ex.NativeErrorCode == 1155 && File.Exists(path))
+ {
+ Process.Start(new ProcessStartInfo
+ {
+ FileName = "notepad.exe",
+ Arguments = $"\"{path}\"",
+ UseShellExecute = false
+ });
+ }
+ }
+ #endregion
+
+ #region 应用程序操作
+
+ ///
+ /// 获取程序打包资源的输入流。该资源必须声明为 Resource 类型,否则将会报错,Images
+ /// 和 Resources 目录已默认声明该类型。
+ ///
+ /// 资源路径,例如 "Resources/java-wrapper.jar"
+ /// 资源输入流,若资源不存在则为 null
+ public static Stream? GetResourceStream(string path)
+ {
+ var resourceInfo = Application.GetResourceStream(new Uri($"pack://application:,,,/{path}", UriKind.Absolute));
+ return resourceInfo?.Stream;
+ }
+
+ private const string AssemblyImagePath = "pack://application:,,,/Plain Craft Launcher 2;component/Images/";
+ public static string GetAppImagePath(string imageName) => AssemblyImagePath + imageName;
+
+ #endregion
+}
diff --git a/launcher/PCL-CE/PCL.Core/App/Cli/ArgumentValueKind.cs b/launcher/PCL-CE/PCL.Core/App/Cli/ArgumentValueKind.cs
new file mode 100644
index 0000000..d9aa035
--- /dev/null
+++ b/launcher/PCL-CE/PCL.Core/App/Cli/ArgumentValueKind.cs
@@ -0,0 +1,8 @@
+namespace PCL.Core.App.Cli;
+
+public enum ArgumentValueKind
+{
+ Bool,
+ Decimal,
+ Text,
+}
diff --git a/launcher/PCL-CE/PCL.Core/App/Cli/BoolArgument.cs b/launcher/PCL-CE/PCL.Core/App/Cli/BoolArgument.cs
new file mode 100644
index 0000000..d54318b
--- /dev/null
+++ b/launcher/PCL-CE/PCL.Core/App/Cli/BoolArgument.cs
@@ -0,0 +1,37 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+
+namespace PCL.Core.App.Cli;
+
+public class BoolArgument : CommandArgument
+{
+ public override ArgumentValueKind ValueKind => ArgumentValueKind.Bool;
+
+ protected override bool ParseValueText()
+ {
+ var text = ValueText.ToLowerInvariant().Trim();
+ return text is not ("0" or "false");
+ }
+
+ public override bool TryCastValue([NotNullWhen(true)] out T value)
+ {
+ if (base.TryCastValue(out value)) return true;
+ var type = typeof(T);
+ if (type != typeof(sbyte) &&
+ type != typeof(byte) &&
+ type != typeof(short) &&
+ type != typeof(ushort) &&
+ type != typeof(int) &&
+ type != typeof(uint) &&
+ type != typeof(long) &&
+ type != typeof(ulong) &&
+ type != typeof(nint) &&
+ type != typeof(nuint)) return false;
+ // magic code
+ var v = Value;
+ Unsafe.As(ref value) = Unsafe.As(ref v);
+#pragma warning disable CS8762 // The analyzer sucks.
+ return true;
+#pragma warning restore CS8762
+ }
+}
diff --git a/launcher/PCL-CE/PCL.Core/App/Cli/CommandArgument.cs b/launcher/PCL-CE/PCL.Core/App/Cli/CommandArgument.cs
new file mode 100644
index 0000000..9b4063f
--- /dev/null
+++ b/launcher/PCL-CE/PCL.Core/App/Cli/CommandArgument.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+
+namespace PCL.Core.App.Cli;
+
+///
+/// 无泛型的命令行参数模型
+///
+///
+public abstract class CommandArgument
+{
+ ///
+ /// 参数键
+ ///
+ public required string Key { get; init; }
+
+ ///
+ /// 参数值文本
+ ///
+ public required string ValueText { get; init; }
+
+ ///
+ /// 参数值类型
+ ///
+ public abstract ArgumentValueKind ValueKind { get; }
+
+ ///
+ /// 尝试以指定类型获取参数值
+ ///
+ /// 参数值,若尝试失败则为该类型默认值
+ /// 参数值的类型
+ /// 是否成功,若类型不匹配则失败
+ public abstract bool TryCastValue([NotNullWhen(true)] out T? value);
+
+ public T? CastValue()
+ {
+ var result = TryCastValue(out T? value);
+ return result ? value : throw new InvalidCastException("Value type mismatch or cannot cast");
+ }
+}
+
+///
+/// 命令行参数模型
+///
+/// 参数值的类型
+public abstract class CommandArgument : CommandArgument
+{
+ ///
+ /// 从参数值文本中解析参数类型
+ ///
+ /// 对应类型的参数值
+ protected abstract TValue ParseValueText();
+
+ private bool _isValueParsed = false;
+
+ ///
+ /// 参数值
+ ///
+ public TValue Value
+ {
+ get
+ {
+ if (_isValueParsed) return field;
+ _isValueParsed = true;
+ return field = ParseValueText();
+ }
+ protected init
+ {
+ field = value;
+ _isValueParsed = true;
+ }
+ } = default!;
+
+ public override bool TryCastValue([NotNullWhen(true)] out T value)
+ {
+ if (Value is T v)
+ {
+ value = v;
+ return true;
+ }
+ value = default!;
+ if (typeof(T) == typeof(string))
+ {
+ Unsafe.As(ref value) = ValueText;
+#pragma warning disable CS8762 // The analyzer sucks.
+ return true;
+#pragma warning restore CS8762
+ }
+ return false;
+ }
+}
diff --git a/launcher/PCL-CE/PCL.Core/App/Cli/CommandLine.cs b/launcher/PCL-CE/PCL.Core/App/Cli/CommandLine.cs
new file mode 100644
index 0000000..5ac19c1
--- /dev/null
+++ b/launcher/PCL-CE/PCL.Core/App/Cli/CommandLine.cs
@@ -0,0 +1,266 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace PCL.Core.App.Cli;
+
+///
+/// 命令行模型
+///
+[JsonConverter(typeof(CommandLineJsonConverter))]
+public class CommandLine
+{
+ ///
+ /// 命令文本
+ ///
+ public required string CommandText { get; init; }
+
+ ///
+ /// 子命令
+ ///
+ public CommandLine? Subcommand { get; init; } = null;
+
+ ///
+ /// 子命令文本
+ ///
+ public string? SubcommandText => Subcommand?.CommandText;
+
+ ///
+ /// 参数字典
+ ///
+ public required IReadOnlyDictionary Arguments { get; init; }
+
+ ///
+ /// 尝试获取参数值
+ ///
+ /// 参数键
+ /// 参数值,若获取失败则为对应类型默认值
+ /// 参数值的类型
+ /// 是否存在该键; 存在该键时值的类型是否匹配
+ public (bool exists, bool isTypeMatch) TryGetArgumentValue(string key, out TValue? value)
+ {
+ var exists = Arguments.TryGetValue(key, out var arg);
+ var isTypeMatch = false;
+ if (exists && (isTypeMatch = arg!.TryCastValue(out TValue? typedValue)))
+ {
+ value = typedValue;
+ return (true, true);
+ }
+ value = default;
+ return (exists, isTypeMatch);
+ }
+
+ ///
+ /// 解析参数数组,第一个元素会被视为主命令
+ ///
+ /// 参数数组
+ /// 各级子命令列表
+ /// 命令行模型实例
+ public static CommandLine Parse(ReadOnlySpan args, IEnumerable? subcommands = null)
+ {
+ subcommands ??= [];
+ SubcommandDefinition root = (args[0], subcommands);
+ return CommandLineParser.Parse(args, root);
+ }
+
+ public override string ToString()
+ {
+ var sb = new StringBuilder();
+ sb.Append(CommandText).Append(" [");
+ if (Arguments.Count > 0) sb.AppendLine();
+ foreach (var arg in Arguments.Values)
+ {
+ sb.Append(" --").Append(arg.Key);
+ var value = arg.ValueKind switch
+ {
+ ArgumentValueKind.Bool => arg.CastValue() ? "true" : "false",
+ ArgumentValueKind.Decimal => arg.CastValue().ToString(CultureInfo.InvariantCulture),
+ ArgumentValueKind.Text => arg.ValueText,
+ _ => null
+ };
+ if (value is not null) sb.Append(": ").Append(value);
+ sb.AppendLine();
+ }
+ sb.Append(']');
+ if (Subcommand is not null) sb.AppendLine().Append("-> ").Append(Subcommand.ToString().Replace("\n", "\n "));
+ return sb.ToString();
+ }
+}
+
+file static class CommandLineParser
+{
+ private static (CommandArgument, bool) _ParseArgument(string key, string possibleValueText)
+ {
+ if (key.StartsWith("--")) key = key[2..];
+ if (possibleValueText.Length == 0 || possibleValueText.StartsWith("--"))
+ return (new BoolArgument { Key = key, ValueText = string.Empty }, false);
+ if (possibleValueText.ToLowerInvariant() is "true" or "false")
+ return (new BoolArgument { Key = key, ValueText = possibleValueText }, true);
+ if (decimal.TryParse(possibleValueText, out var d))
+ return (new DecimalArgument { Key = key, ValueText = possibleValueText, Value = d }, true);
+ return (new TextArgument { Key = key, ValueText = possibleValueText }, true);
+ }
+
+ public static CommandLine Parse(ReadOnlySpan args, SubcommandDefinition subcommands)
+ {
+ if (args.IsEmpty) throw new ArgumentException("The argument span must contain at least 1 element", nameof(args));
+ var i = 1;
+ var commandText = args[0];
+ var argumentList = new Dictionary();
+ CommandLine? subcommand = null;
+ while (i < args.Length)
+ {
+ var currentText = args[i];
+ if (subcommands.Contains(currentText))
+ {
+ subcommand = Parse(args[i..], subcommands.SubcommandMap[currentText]);
+ break;
+ }
+ var (commandArgument, hasValueText) = _ParseArgument(currentText,
+ (i == args.Length - 1) || (subcommands.Contains(args[i + 1])) ? "" : args[i + 1]);
+ argumentList[commandArgument.Key] = commandArgument;
+ i += hasValueText ? 2 : 1;
+ }
+ return new CommandLine
+ {
+ CommandText = commandText,
+ Arguments = argumentList.AsReadOnly(),
+ Subcommand = subcommand
+ };
+ }
+}
+
+///
+/// 用于 的 JSON 转换器
+///
+// Generated by gpt-5.3-codex (20260218)
+public sealed class CommandLineJsonConverter : JsonConverter
+{
+ public override CommandLine? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.Null) return null;
+ if (reader.TokenType != JsonTokenType.StartObject) throw new JsonException("Expected object for CommandLine.");
+
+ string? commandText = null;
+ CommandLine? subcommand = null;
+ var arguments = new Dictionary();
+
+ while (reader.Read())
+ {
+ if (reader.TokenType == JsonTokenType.EndObject) break;
+ if (reader.TokenType != JsonTokenType.PropertyName) throw new JsonException("Expected property name.");
+
+ var propName = reader.GetString();
+ if (!reader.Read()) throw new JsonException("Unexpected end of json.");
+
+ switch (propName)
+ {
+ case "cmd":
+ commandText = reader.GetString() ?? throw new JsonException("cmd cannot be null.");
+ break;
+ case "sub":
+ subcommand = JsonSerializer.Deserialize(ref reader, options);
+ break;
+ case "args":
+ _ReadArguments(ref reader, arguments);
+ break;
+ default:
+ // 忽略未知字段,提升前向兼容
+ reader.Skip();
+ break;
+ }
+ }
+
+ if (string.IsNullOrEmpty(commandText)) throw new JsonException("Missing required property: cmd.");
+
+ return new CommandLine
+ {
+ CommandText = commandText,
+ Arguments = arguments.AsReadOnly(),
+ Subcommand = subcommand
+ };
+ }
+
+ public override void Write(Utf8JsonWriter writer, CommandLine value, JsonSerializerOptions options)
+ {
+ writer.WriteStartObject();
+ writer.WriteString("cmd", value.CommandText);
+ if (value.Arguments.Count > 0)
+ {
+ writer.WritePropertyName("args");
+ writer.WriteStartArray();
+ foreach (var arg in value.Arguments.Values)
+ {
+ writer.WriteStartObject();
+ writer.WriteString("k", arg.Key);
+ writer.WriteNumber("t", (int)arg.ValueKind);
+ writer.WriteString("v", arg.ValueText);
+ writer.WriteEndObject();
+ }
+ writer.WriteEndArray();
+ }
+ if (value.Subcommand is not null)
+ {
+ writer.WritePropertyName("sub");
+ JsonSerializer.Serialize(writer, value.Subcommand, options);
+ }
+ writer.WriteEndObject();
+ }
+
+ private static void _ReadArguments(ref Utf8JsonReader reader, Dictionary target)
+ {
+ if (reader.TokenType != JsonTokenType.StartArray) throw new JsonException("args must be an array.");
+
+ while (reader.Read())
+ {
+ if (reader.TokenType == JsonTokenType.EndArray) return;
+ if (reader.TokenType != JsonTokenType.StartObject) throw new JsonException("Argument item must be an object.");
+
+ string? key = null;
+ ArgumentValueKind? kind = null;
+ string? valueText = null;
+
+ while (reader.Read())
+ {
+ if (reader.TokenType == JsonTokenType.EndObject) break;
+ if (reader.TokenType != JsonTokenType.PropertyName) throw new JsonException("Expected argument property name.");
+
+ var name = reader.GetString();
+ if (!reader.Read()) throw new JsonException("Unexpected end of json.");
+
+ switch (name)
+ {
+ case "k":
+ key = reader.GetString();
+ break;
+ case "t":
+ kind = (ArgumentValueKind)reader.GetInt32();
+ break;
+ case "v":
+ valueText = reader.GetString() ?? string.Empty;
+ break;
+ default:
+ reader.Skip();
+ break;
+ }
+ }
+
+ if (string.IsNullOrEmpty(key)) throw new JsonException("Argument missing key(k).");
+ if (kind is null) throw new JsonException("Argument missing type(t).");
+ valueText ??= string.Empty;
+
+ CommandArgument arg = kind.Value switch
+ {
+ ArgumentValueKind.Bool => new BoolArgument { Key = key, ValueText = valueText },
+ ArgumentValueKind.Decimal => new DecimalArgument { Key = key, ValueText = valueText },
+ ArgumentValueKind.Text => new TextArgument { Key = key, ValueText = valueText },
+ _ => throw new JsonException($"Unsupported argument type: {(int)kind.Value}")
+ };
+ target[arg.Key] = arg;
+ }
+ throw new JsonException("args array is not closed.");
+ }
+}
diff --git a/launcher/PCL-CE/PCL.Core/App/Cli/DecimalArgument.cs b/launcher/PCL-CE/PCL.Core/App/Cli/DecimalArgument.cs
new file mode 100644
index 0000000..d140332
--- /dev/null
+++ b/launcher/PCL-CE/PCL.Core/App/Cli/DecimalArgument.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+
+namespace PCL.Core.App.Cli;
+
+public class DecimalArgument : CommandArgument
+{
+ public override ArgumentValueKind ValueKind => ArgumentValueKind.Decimal;
+
+ protected override decimal ParseValueText() => decimal.Parse(ValueText);
+
+ public new decimal Value
+ {
+ get => base.Value;
+ init => base.Value = value;
+ }
+
+ public override bool TryCastValue([NotNullWhen(true)] out T value)
+ {
+ if (base.TryCastValue(out value)) return true;
+ var type = typeof(T);
+ try
+ {
+ if (type == typeof(int)) Unsafe.As(ref value) = Convert.ToInt32(Value);
+ else if (type == typeof(long)) Unsafe.As(ref value) = Convert.ToInt64(Value);
+ else if (type == typeof(double)) Unsafe.As(ref value) = Convert.ToDouble(Value);
+ else if (type == typeof(float)) Unsafe.As(ref value) = Convert.ToSingle(Value);
+ else if (type == typeof(short)) Unsafe.As(ref value) = Convert.ToInt16(Value);
+ else if (type == typeof(sbyte)) Unsafe.As(ref value) = Convert.ToSByte(Value);
+ else if (type == typeof(ulong)) Unsafe.As(ref value) = Convert.ToUInt64(Value);
+ else if (type == typeof(uint)) Unsafe.As(ref value) = Convert.ToUInt32(Value);
+ else if (type == typeof(ushort)) Unsafe.As(ref value) = Convert.ToUInt16(Value);
+ else if (type == typeof(byte)) Unsafe.As(ref value) = Convert.ToByte(Value);
+ else if (type == typeof(nint)) Unsafe.As(ref value) = checked((nint)Convert.ToInt64(Value));
+ else if (type == typeof(nuint)) Unsafe.As(ref value) = checked((nuint)Convert.ToUInt64(Value));
+ else return false;
+#pragma warning disable CS8762 // The analyzer sucks.
+ return true;
+#pragma warning restore CS8762
+ }
+ catch (Exception ex) when (ex is OverflowException or InvalidCastException or FormatException)
+ {
+ return false;
+ }
+ }
+}
diff --git a/launcher/PCL-CE/PCL.Core/App/Cli/SubcommandDefinition.cs b/launcher/PCL-CE/PCL.Core/App/Cli/SubcommandDefinition.cs
new file mode 100644
index 0000000..e5eceb1
--- /dev/null
+++ b/launcher/PCL-CE/PCL.Core/App/Cli/SubcommandDefinition.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+
+namespace PCL.Core.App.Cli;
+
+public class SubcommandDefinition
+{
+ public required string CommandText { get; init; }
+
+ public required IEnumerable Subcommands { private get; init; }
+
+ public IReadOnlyDictionary SubcommandMap
+ {
+ get
+ {
+ if (field is not null) return field;
+ var map = new Dictionary();
+ foreach (var c in Subcommands) map[c.CommandText] = c;
+ return field = map.AsReadOnly();
+ }
+ } = null!;
+
+ public bool Contains(string subcommandText)
+ {
+ return SubcommandMap.ContainsKey(subcommandText);
+ }
+
+ public static implicit operator SubcommandDefinition((string commandText, IEnumerable subcommands) tuple)
+ {
+ return new SubcommandDefinition
+ {
+ CommandText = tuple.commandText,
+ Subcommands = tuple.subcommands
+ };
+ }
+
+ public static implicit operator SubcommandDefinition(string commandText)
+ {
+ return new SubcommandDefinition
+ {
+ CommandText = commandText,
+ Subcommands = []
+ };
+ }
+}
diff --git a/launcher/PCL-CE/PCL.Core/App/Cli/TextArgument.cs b/launcher/PCL-CE/PCL.Core/App/Cli/TextArgument.cs
new file mode 100644
index 0000000..0b8c1f2
--- /dev/null
+++ b/launcher/PCL-CE/PCL.Core/App/Cli/TextArgument.cs
@@ -0,0 +1,8 @@
+namespace PCL.Core.App.Cli;
+
+public class TextArgument : CommandArgument
+{
+ public override ArgumentValueKind ValueKind => ArgumentValueKind.Text;
+
+ protected override string ParseValueText() => ValueText;
+}
diff --git a/launcher/PCL-CE/PCL.Core/App/Config.cs b/launcher/PCL-CE/PCL.Core/App/Config.cs
new file mode 100644
index 0000000..1137f4e
--- /dev/null
+++ b/launcher/PCL-CE/PCL.Core/App/Config.cs
@@ -0,0 +1,642 @@
+using PCL.Core.App.Configuration;
+
+namespace PCL.Core.App;
+
+///
+/// 全局配置类。
+///
+// ReSharper disable InconsistentNaming
+public static partial class Config
+{
+ ///
+ /// 系统配置。
+ ///
+ [ConfigGroup("System")] partial class SystemConfigGroup
+ {
+ // ///
+ // /// 系统缓存目录。
+ // ///
+ // [ConfigItem("SystemSystemCache", "")] public partial string CacheDirectory { get; set; }
+
+ ///
+ /// 禁用硬件加速。
+ ///
+ [ConfigItem("SystemDisableHardwareAcceleration", false)] public partial bool DisableHardwareAcceleration { get; set; }
+
+ ///
+ /// 遥测。
+ ///
+ [ConfigItem("SystemTelemetry", false)] public partial bool Telemetry { get; set; }
+
+ ///
+ /// 实时日志最大行数。
+ ///
+ [ConfigItem("SystemMaxLog", 13)] public partial int MaxGameLog { get; set; }
+
+ ///
+ /// 动画帧率上限。
+ ///
+ [ConfigItem("UiAniFPS", 59)] public partial int AnimationFpsLimit { get; set; }
+ }
+
+ ///
+ /// 网络配置。
+ ///
+ [ConfigGroup("Network")] partial class NetworkConfigGroup
+ {
+ [ConfigItem("SystemNetEnableDoH", true)] public partial bool EnableDoH { get; set; }
+
+ [ConfigGroup("HttpProxy")] partial class HttpProxyConfigGroup
+ {
+ [ConfigItem("SystemHttpProxy", "", ConfigSource.SharedEncrypt)] public partial string CustomAddress { get; set; }
+ [ConfigItem("SystemHttpProxyType", 1)] public partial int Type { get; set; }
+ [ConfigItem("SystemHttpProxyCustomUsername", "")] public partial string CustomUsername { get; set; }
+ [ConfigItem("SystemHttpProxyCustomPassword", "")] public partial string CustomPassword { get; set; }
+ }
+ }
+
+ ///
+ /// 调试配置
+ ///
+ [ConfigGroup("Debug")] partial class DebugConfigGroup
+ {
+ [ConfigItem("SystemDebugMode", false)] public partial bool Enabled { get; set; }
+ [ConfigItem("SystemDebugAnim", 9)] public partial int AnimationSpeed { get; set; }
+ [ConfigItem("SystemDebugDelay", false)] public partial bool AddRandomDelay { get; set; }
+ [ConfigItem("SystemDebugSkipCopy", false)] public partial bool DontCopy { get; set; }
+ [ConfigItem("SystemDebugAllowRestrictedFeature", false)] public partial bool AllowRestrictedFeature { get; set; }
+ }
+
+ ///
+ /// 下载配置。
+ ///
+ [ConfigGroup("Download")] partial class DownloadConfigGroup
+ {
+ [ConfigItem("ToolDownloadThread", 63)] public partial int ThreadLimit { get; set; }
+ [ConfigItem("ToolDownloadSpeed", 42)] public partial int SpeedLimit { get; set; }
+ [ConfigItem("ToolDownloadSource", 1)] public partial int FileSource { get; set; }
+ [ConfigItem("ToolDownloadVersion", 1)] public partial int VersionListSource { get; set; }
+ [ConfigItem("ToolDownloadAutoSelectVersion", true)] public partial bool AutoSelectInstance { get; set; }
+ [ConfigItem("ToolFixAuthlib", true)] public partial bool FixAuthLib { get; set; }
+
+ ///
+ /// 第三方资源配置。
+ ///
+ [ConfigGroup("Comp")] partial class CompConfigGroup
+ {
+ [ConfigItem("ToolDownloadTranslate", 0)] public partial int NameFormatV1 { get; set; }
+ [ConfigItem("ToolDownloadTranslateV2", 1)] public partial int NameFormatV2 { get; set; }
+ [ConfigItem("ToolDownloadIgnoreQuilt", true)] public partial bool IgnoreQuilt { get; set; }
+ [ConfigItem("ToolDownloadAutoInstallDependencies", true)] public partial bool AutoInstallDependencies { get; set; }
+ [ConfigItem("ToolDownloadClipboard", false)] public partial bool ReadClipboard { get; set; }
+ [ConfigItem("ToolDownloadMod", 1)] public partial int CompSourceSolution { get; set; }
+ [ConfigItem("ToolModLocalNameStyle", 0)] public partial int UiCompNameSolution { get; set; }
+ [ConfigItem("ToolDownloadQuickBehavior", 0)] public partial int QuickDownloadBehavior { get; set; }
+ }
+ }
+
+ ///
+ /// 工具配置。
+ ///
+ [ConfigGroup("Tool")] partial class ToolConfigGroup
+ {
+ [ConfigItem("ToolHelpChinese", true)] public partial bool AutoChangeLanguage { get; set; }
+ // [ConfigItem("ToolUpdateAlpha", 0, ConfigSource.SharedEncrypt)] public partial int Alpha { get; set; }
+ [ConfigItem("ToolUpdateRelease", false)] public partial bool ReleaseNotification { get; set; }
+ [ConfigItem("ToolUpdateSnapshot", false)] public partial bool SnapshotNotification { get; set; }
+ }
+
+ ///
+ /// 更新配置。
+ ///
+ [ConfigGroup("Update")] partial class UpdateConfigGroup
+ {
+ ///
+ /// 自动更新行为。
+ ///
+ [ConfigItem("SystemSystemUpdate", LauncherAutoUpdateBehavior.DownloadAndAnnounce, ConfigSource.Local)] public partial LauncherAutoUpdateBehavior UpdateMode { get; set; }
+
+ ///
+ /// 更新分支。
+ ///
+ [ConfigItem("SystemUpdateChannel", UpdateChannel.Release, ConfigSource.Local)] public partial UpdateChannel UpdateChannel { get; set; }
+
+ ///
+ /// Mirror 酱 CDK。
+ ///
+ [ConfigItem("SystemMirrorChyanKey", "", ConfigSource.SharedEncrypt)] public partial string MirrorChyanKey { get; set; }
+ }
+
+ ///
+ /// 联机大厅配置。
+ ///
+ [ConfigGroup("Link")] partial class LinkConfigGroup
+ {
+ ///
+ /// 大厅用户名。
+ ///
+ [ConfigItem("LinkUsername", "")] public partial string Username { get; set; }
+
+ ///
+ /// 中继方式。
+ ///
+ [ConfigItem("LinkRelayType", LinkRelayBehavior.Default)] public partial LinkRelayBehavior RelayType { get; set; }
+
+ ///
+ /// 中继服务器类型 (社区/自有)。
+ ///
+ [ConfigItem("LinkServerType", 1)] public partial int ServerType { get; set; }
+
+ ///
+ /// 延迟优先模式。
+ ///
+ [ConfigItem("LinkLatencyFirstMode", true)] public partial bool UseLatencyFirstMode { get; set; }
+
+ ///
+ /// 自定义中继服务器。
+ ///
+ [ConfigItem("LinkRelayServer", "")] public partial string CustomRelayServer { get; set; }
+
+ ///
+ /// 传输协议优先策略。
+ ///
+ [ConfigItem("LinkProtocolPreference", LinkProtocolPreference.Tcp)] public partial LinkProtocolPreference ProtocolPreference { get; set; }
+
+ ///
+ /// 尝试使用端口猜测打通对称性 NAT。
+ ///
+ [ConfigItem("LinkTryPunchSym", true)] public partial bool TryPunchSym { get; set; }
+
+ ///
+ /// 启用 IPv6。
+ ///
+ [ConfigItem("LinkEnableIPv6", true)] public partial bool EnableIPv6 { get; set; }
+
+ ///
+ /// 在日志中输出 Cli 信息以用于调试。
+ ///
+ [ConfigItem("LinkEnableCliOutput", false)] public partial bool EnableCliOutput { get; set; }
+ }
+
+ ///
+ /// 个性化配置。
+ ///
+ [ConfigGroup("Preference")] partial class PreferenceConfigGroup
+ {
+ ///
+ /// 启动时显示 Logo。
+ ///
+ [ConfigItem("UiLauncherLogo", true, ConfigSource.Local)] public partial bool ShowStartupLogo { get; set; }
+
+ ///
+ /// 锁定窗口大小。
+ ///
+ [ConfigItem("UiLockWindowSize", false)] public partial bool LockWindowSize { get; set; }
+
+ ///
+ /// 在启动游戏时显示你知道吗。
+ ///
+ [ConfigItem("UiShowLaunchingHint", true, ConfigSource.Local)] public partial bool ShowLaunchingHint { get; set; }
+
+ ///
+ /// 标题内容类型。
+ ///
+ [ConfigItem("UiLogoType", LauncherTitleType.Default, ConfigSource.Local)] public partial LauncherTitleType WindowTitleType { get; set; }
+
+ ///
+ /// 窗口标题文本。
+ ///
+ [ConfigItem("UiLogoText", "", ConfigSource.Local)] public partial string WindowTitleCustomText { get; set; }
+
+ ///
+ /// 导航栏居左。
+ ///
+ [ConfigItem("UiLogoLeft", false, ConfigSource.Local)] public partial bool TopBarLeftAlign { get; set; }
+
+ ///
+ /// 全局字体。
+ ///
+ [ConfigItem("UiFont", "", ConfigSource.Local)] public partial string Font { get; set; }
+
+ ///
+ /// MOTD 字体。
+ ///
+ [ConfigItem("UiMotdFont", "", ConfigSource.Local)] public partial string MotdFont { get; set; }
+
+ ///
+ /// 详细实例分类。
+ ///
+ [ConfigItem("DetailedInstanceClassification", false, ConfigSource.Local)] public partial bool DetailedInstanceClassification { get; set; }
+
+ ///
+ /// 本地化配置。
+ ///
+ [ConfigGroup("Localization", ConfigSource.Local)] partial class LocalizationConfigGroup
+ {
+ ///
+ /// UI 语言。auto 表示跟随系统语言。
+ ///
+ [ConfigItem("UiLanguage", "auto")] public partial string Language { get; set; }
+
+ ///
+ /// UI 展示格式所使用的区域性。auto 表示跟随系统区域格式。
+ ///
+ [ConfigItem("UiFormatCulture", "auto")] public partial string FormatCulture { get; set; }
+
+ ///
+ /// 区域覆盖。auto 表示自动判断。
+ ///
+ [ConfigItem("UiRegion", "auto")] public partial string Region { get; set; }
+ }
+
+ ///
+ /// 界面主题配置。
+ ///
+ [ConfigGroup("Theme")] partial class ThemeConfigGroup
+ {
+ ///
+ /// 配色主题模式。
+ ///
+ [ConfigItem("UiDarkMode", ColorMode.System)] public partial ColorMode ColorMode { get; set; }
+
+ ///
+ /// 暗色配色主题。
+ ///
+ [ConfigItem("UiDarkColor", ColorTheme.CatBlue)] public partial ColorTheme DarkColor { get; set; }
+
+ ///
+ /// 亮色配色主题。
+ ///
+ [ConfigItem("UiLightColor", ColorTheme.CatBlue)] public partial ColorTheme LightColor { get; set; }
+
+ ///
+ /// 窗口透明度。
+ ///
+ [ConfigItem("UiLauncherTransparent", 600, ConfigSource.Local)] public partial int WindowOpacity { get; set; }
+
+ ///
+ /// 自定义主题:色相 (H)。
+ ///
+ [ConfigItem("UiLauncherHue", 180, ConfigSource.Local)] public partial int WindowHue { get; set; }
+
+ ///
+ /// 自定义主题:饱和度 (S)。
+ ///
+ [ConfigItem("UiLauncherSat", 80, ConfigSource.Local)] public partial int WindowSat { get; set; }
+
+ ///
+ /// 自定义主题:明度 (L)。
+ ///
+ [ConfigItem("UiLauncherLight", 20, ConfigSource.Local)] public partial int WindowLight { get; set; }
+
+ ///
+ /// 自定义主题:色相渐变。
+ ///
+ [ConfigItem("UiLauncherDelta", 90, ConfigSource.Local)] public partial int WindowDelta { get; set; }
+
+ ///
+ /// 传说中的主题选择,但是没卵用。
+ ///
+ [ConfigItem("UiLauncherTheme", 0, ConfigSource.Local)] public partial int ThemeSelected { get; set; }
+ }
+
+ ///
+ /// 背景内容。
+ ///
+ [ConfigGroup("Background")] partial class BackgroundConfigGroup
+ {
+ ///
+ /// 彩色底部填充。
+ ///
+ [ConfigItem("UiBackgroundColorful", true, ConfigSource.Local)] public partial bool BackgroundColorful { get; set; }
+
+ ///
+ /// 透明度。
+ ///
+ [ConfigItem("UiBackgroundOpacity", 1000, ConfigSource.Local)] public partial int WallpaperOpacity { get; set; }
+
+ ///
+ /// 旋转。
+ ///
+ [ConfigItem("UiBackgroundCarousel", 1000, ConfigSource.Local)] public partial int WallpaperCarousel { get; set; }
+
+ ///
+ /// 模糊遮罩。
+ ///
+ [ConfigItem("UiBackgroundBlur", 0, ConfigSource.Local)] public partial int WallpaperBlurRadius { get; set; }
+
+ ///
+ /// 内容裁剪模式。
+ ///
+ [ConfigItem("UiBackgroundSuit", 0, ConfigSource.Local)] public partial int WallpaperSuitMode { get; set; }
+
+ ///
+ /// 视频自动暂停。
+ ///
+ [ConfigItem("UiAutoPauseVideo", true, ConfigSource.Local)] public partial bool AutoPauseVideo { get; set; }
+ }
+
+ ///
+ /// 高级材质。
+ ///
+ [ConfigGroup("Blur")] partial class BlurConfigGroup
+ {
+ ///
+ /// 是否启用。
+ ///
+ [ConfigItem("UiBlur", false, ConfigSource.Local)] public partial bool IsEnabled { get; set; }
+
+ ///
+ /// 模糊半径。
+ ///
+ [ConfigItem("UiBlurValue", 16, ConfigSource.Local)] public partial int Radius { get; set; }
+
+ ///
+ /// 采样率。
+ ///
+ [ConfigItem("UiBlurSamplingRate", 70, ConfigSource.Local)] public partial int SamplingRate { get; set; }
+
+ ///
+ /// 模糊方法。
+ ///
+ [ConfigItem("UiBlurType", 0, ConfigSource.Local)] public partial int KernelType { get; set; }
+ }
+
+ ///
+ /// 自定义主页。
+ ///
+ [ConfigGroup("Homepage")] partial class HomepageConfigGroup
+ {
+ ///
+ /// 主页来源类型。
+ ///
+ [ConfigItem("UiCustomType", 0, ConfigSource.Local)] public partial int Type { get; set; }
+
+ ///
+ /// 预设选项。
+ ///
+ [ConfigItem("UiCustomPreset", 0, ConfigSource.Local)] public partial int SelectedPreset { get; set; }
+
+ ///
+ /// 自定义 URL。
+ ///
+ [ConfigItem("UiCustomNet", "", ConfigSource.Local)] public partial string CustomUrl { get; set; }
+ }
+
+ ///
+ /// 背景音乐。
+ ///
+ [ConfigGroup("Music")] partial class MusicConfigGroup
+ {
+ ///
+ /// 音量。
+ ///
+ [ConfigItem("UiMusicVolume", 500, ConfigSource.Local)] public partial int Volume { get; set; }
+
+ ///
+ /// 启动游戏后自动暂停。
+ ///
+ [ConfigItem("UiMusicStop", false, ConfigSource.Local)] public partial bool StopInGame { get; set; }
+
+ ///
+ /// 启动游戏后自动开始播放。
+ ///
+ [ConfigItem("UiMusicStart", false, ConfigSource.Local)] public partial bool StartInGame { get; set; }
+
+ ///
+ /// 自动开始播放。
+ ///
+ [ConfigItem("UiMusicAuto", true, ConfigSource.Local)] public partial bool StartOnStartup { get; set; }
+
+ ///
+ /// 随机播放。
+ ///
+ [ConfigItem("UiMusicRandom", true, ConfigSource.Local)] public partial bool ShufflePlayback { get; set; }
+
+ ///
+ /// 启用 SMTC。
+ ///
+ [ConfigItem("UiMusicSMTC", true, ConfigSource.Local)] public partial bool EnableSMTC { get; set; }
+ }
+
+ ///
+ /// 功能隐藏。
+ ///
+ [ConfigGroup("Hide")]
+ partial class HideConfigGroup
+ {
+ // 主页面
+ [ConfigItem("UiHiddenPageDownload", false, ConfigSource.Local)] public partial bool PageDownload { get; set; }
+ [ConfigItem("UiHiddenPageSetup", false, ConfigSource.Local)] public partial bool PageSetup { get; set; }
+ [ConfigItem("UiHiddenPageTools", false, ConfigSource.Local)] public partial bool PageTools { get; set; }
+
+ // 子页面 设置
+ [ConfigItem("UiHiddenSetupLaunch", false, ConfigSource.Local)] public partial bool SetupLaunch { get; set; }
+ [ConfigItem("UiHiddenSetupUi", false, ConfigSource.Local)] public partial bool SetupUi { get; set; }
+ [ConfigItem("UiHiddenSetupLauncherLanguage", false, ConfigSource.Local)] public partial bool SetupLauncherLanguage { get; set; }
+ [ConfigItem("UiHiddenSetupLauncherMisc", false, ConfigSource.Local)] public partial bool SetupLauncherMisc { get; set; }
+ [ConfigItem("UiHiddenSetupGameManage", false, ConfigSource.Local)] public partial bool SetupGameManage { get; set; }
+ [ConfigItem("UiHiddenSetupJava", false, ConfigSource.Local)] public partial bool SetupJava { get; set; }
+ [ConfigItem("UiHiddenSetupUpdate", false, ConfigSource.Local)] public partial bool SetupUpdate { get; set; }
+ [ConfigItem("UiHiddenSetupGameLink", false, ConfigSource.Local)] public partial bool SetupGameLink { get; set; } // 新增
+ [ConfigItem("UiHiddenSetupAbout", false, ConfigSource.Local)] public partial bool SetupAbout { get; set; } // 修正名称
+ [ConfigItem("UiHiddenSetupFeedback", false, ConfigSource.Local)] public partial bool SetupFeedback { get; set; } // 修正名称
+ [ConfigItem("UiHiddenSetupLog", false, ConfigSource.Local)] public partial bool SetupLog { get; set; } // 修正名称
+
+ // 子页面 工具
+ [ConfigItem("UiHiddenToolsGameLink", false, ConfigSource.Local)] public partial bool ToolsGameLink { get; set; } // 新增
+ [ConfigItem("UiHiddenToolsTest", false, ConfigSource.Local)] public partial bool ToolsTest { get; set; } // 新增
+
+ // 子页面 实例设置
+ [ConfigItem("UiHiddenVersionEdit", false, ConfigSource.Local)] public partial bool InstanceEdit { get; set; }
+ [ConfigItem("UiHiddenVersionExport", false, ConfigSource.Local)] public partial bool InstanceExport { get; set; }
+ [ConfigItem("UiHiddenVersionSave", false, ConfigSource.Local)] public partial bool InstanceSave { get; set; }
+ [ConfigItem("UiHiddenVersionScreenshot", false, ConfigSource.Local)] public partial bool InstanceScreenshot { get; set; }
+ [ConfigItem("UiHiddenVersionMod", false, ConfigSource.Local)] public partial bool InstanceMod { get; set; }
+ [ConfigItem("UiHiddenVersionResourcePack", false, ConfigSource.Local)] public partial bool InstanceResourcePack { get; set; }
+ [ConfigItem("UiHiddenVersionShader", false, ConfigSource.Local)] public partial bool InstanceShader { get; set; }
+ [ConfigItem("UiHiddenVersionSchematic", false, ConfigSource.Local)] public partial bool InstanceSchematic { get; set; }
+ [ConfigItem("UiHiddenVersionServer", false, ConfigSource.Local)] public partial bool InstanceServer { get; set; }
+
+ // 特定功能
+ [ConfigItem("UiHiddenFunctionSelect", false, ConfigSource.Local)] public partial bool FunctionSelect { get; set; }
+ [ConfigItem("UiHiddenFunctionModUpdate", false, ConfigSource.Local)] public partial bool FunctionModUpdate { get; set; }
+ [ConfigItem("UiHiddenFunctionHidden", false, ConfigSource.Local)] public partial bool FunctionHidden { get; set; }
+ }
+ }
+
+ ///
+ /// 启动配置。
+ ///
+ [ConfigGroup("Launch")] partial class LaunchConfigGroup
+ {
+ ///
+ /// 内存分配模式。
+ ///
+ [ConfigItem("LaunchRamType", 0, ConfigSource.Local)] public partial int MemoryAllocationMode { get; set; }
+
+ ///
+ /// 自定义内存分配大小。
+ ///
+ [ConfigItem("LaunchRamCustom", 15, ConfigSource.Local)] public partial int CustomMemorySize { get; set; }
+
+ ///
+ /// 是否固定堆大小:启用后额外追加 -Xms 并使其等于 -Xmx,隐式禁用内存归还以降低延迟抖动、利于 ZGC。见 #3282。
+ ///
+ [ConfigItem("LaunchAdvanceLockMemory", false, ConfigSource.Local)] public partial bool LockMemory { get; set; }
+
+ ///
+ /// 优先 IP 协议栈。
+ ///
+ [ConfigItem("LaunchPreferredIpStack", JvmPreferredIpStack.Default)] public partial JvmPreferredIpStack PreferredIpStack { get; set; }
+
+ ///
+ /// 附加 JVM 参数。
+ ///
+ [ConfigItem("LaunchAdvanceJvm", "-XX:+UseG1GC -XX:-UseAdaptiveSizePolicy -XX:-OmitStackTraceInFastThrow -Djdk.lang.Process.allowAmbiguousCommands=true -Dfml.ignoreInvalidMinecraftCertificates=True -Dfml.ignorePatchDiscrepancies=True -Dlog4j2.formatMsgNoLookups=true", ConfigSource.Local)] public partial string JvmArgs { get; set; }
+
+ ///
+ /// 附加游戏参数。
+ ///
+ [ConfigItem("LaunchAdvanceGame", "", ConfigSource.Local)] public partial string GameArgs { get; set; }
+
+ ///
+ /// 预启动指令。
+ ///
+ [ConfigItem("LaunchAdvanceRun", "", ConfigSource.Local)] public partial string PreLaunchCommand { get; set; }
+
+ ///
+ /// 是否等待预启动指令完成。
+ ///
+ [ConfigItem("LaunchAdvanceRunWait", true, ConfigSource.Local)] public partial bool PreLaunchCommandWait { get; set; }
+
+ ///
+ /// 禁用 Java Launch Wrapper。
+ ///
+ [ConfigItem("LaunchAdvanceDisableJLW", true, ConfigSource.Local)] public partial bool DisableJlw { get; set; }
+
+ ///
+ /// 禁用 LegacyFix
+ ///
+ [ConfigItem("LaunchAdvanceDisableLF", false, ConfigSource.Local)] public partial bool DisableLF { get; set; }
+
+ ///
+ /// 强制使用高性能显卡。
+ ///
+ [ConfigItem("LaunchAdvanceGraphicCard", true)] public partial bool SetGpuPreference { get; set; }
+
+ ///
+ /// 使用 java 而不是 javaw。
+ ///
+ [ConfigItem("LaunchAdvanceNoJavaw", false)] public partial bool NoJavaw { get; set; }
+
+ ///
+ /// 禁用 LWJGL Unsafe Agent。
+ ///
+ [ConfigItem("LaunchAdvanceDisableLwjglUnsafeAgent", false)] public partial bool DisableLwjglUnsafeAgent { get; set; }
+
+ ///
+ /// 禁用自动崩溃分析。
+ ///
+ [ConfigItem("LaunchAdvanceDisableCrashAnalysis", false, ConfigSource.Local)] public partial bool DisableCrashAnalysis { get; set; }
+
+ ///
+ /// 渲染器。
+ ///
+ [ConfigItem("LaunchAdvanceRenderer", 0 ,ConfigSource.Local)] public partial int Renderer { get; set; }
+
+ ///
+ /// 游戏窗口标题。
+ ///
+ [ConfigItem("LaunchArgumentTitle", "", ConfigSource.Local)] public partial string Title { get; set; }
+
+ ///
+ /// 自定义左下角版本信息。
+ ///
+ [ConfigItem("LaunchArgumentInfo", "PCLCE", ConfigSource.Local)] public partial string TypeInfo { get; set; }
+
+ ///
+ /// 选择的默认 Java 实例。
+ ///
+ [ConfigItem("LaunchArgumentJavaSelect", "")] public partial string SelectedJava { get; set; }
+
+ ///
+ /// 版本隔离 V2。
+ ///
+ [ConfigItem("LaunchArgumentIndieV2", 4, ConfigSource.Local)] public partial int IndieSolutionV2 { get; set; }
+
+ ///
+ /// 游戏启动后启动器可见性。
+ ///
+ [ConfigItem("LaunchArgumentVisible", LauncherVisibility.DoNothing)] public partial LauncherVisibility LauncherVisibility { get; set; }
+
+ ///
+ /// 游戏进程优先级。
+ ///
+ [ConfigItem("LaunchArgumentPriority", GameProcessPriority.Normal)] public partial GameProcessPriority ProcessPriority { get; set; }
+
+ ///
+ /// 游戏窗口宽度。
+ ///
+ [ConfigItem("LaunchArgumentWindowWidth", 854, ConfigSource.Local)] public partial int GameWindowWidth { get; set; }
+
+ ///