feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
CI / Go Backend (push) Canceled after 0s

初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块

Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
xyou
2026-08-08 14:07:56 +08:00
parent 9500c4c80a
commit f70b061d1a
1972 changed files with 159760 additions and 6 deletions
+15 -4
View File
@@ -35,10 +35,10 @@ client/Assets/Plugins/Editor/JetBrains*
.vs/ .vs/
.idea/ .idea/
# Autogenerated solution & project files # Autogenerated solution & project files (Unity only)
*.csproj client/*.csproj
*.unityproj client/*.unityproj
*.sln client/*.sln
*.suo *.suo
*.tmp *.tmp
*.user *.user
@@ -51,6 +51,12 @@ client/Assets/Plugins/Editor/JetBrains*
*.opendb *.opendb
*.VC.db *.VC.db
# .NET build artifacts
**/bin/
**/obj/
launcher/PCL-CE/**/bin/
launcher/PCL-CE/**/obj/
# Unity meta for generated # Unity meta for generated
*.pidb.meta *.pidb.meta
*.pdb.meta *.pdb.meta
@@ -69,5 +75,10 @@ sysinfo.txt
Thumbs.db Thumbs.db
desktop.ini desktop.ini
# ===================== .NET SDK (local install) =====================
.dotnet/
.nuget/
.userdata/
# ===================== Docker ===================== # ===================== Docker =====================
docker-compose.override.yml docker-compose.override.yml
+4 -1
View File
@@ -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: dev:
@@ -30,6 +30,9 @@ run-social:
run-relay: run-relay:
cd server && go run ./cmd/relay-server cd server && go run ./cmd/relay-server
run-ai:
cd server && go run ./cmd/ai-service
# 后端:工具 # 后端:工具
tidy: tidy:
cd server && go mod tidy cd server && go mod tidy
@@ -0,0 +1,140 @@
using System.Collections.Generic;
using UnityEngine;
namespace RedCircuit.AIAssistant
{
/// <summary>
/// 操作执行器,将 AI 操作指令转换为游戏内动作。
/// 所有操作必须经用户确认后才执行,支持撤销。
/// </summary>
public class AIActionExecutor : MonoBehaviour
{
/// <summary>待确认的操作队列</summary>
private readonly Queue<AIAction> _pendingQueue = new();
/// <summary>已执行的操作批次 (用于撤销)</summary>
private readonly Stack<ActionBatch> _executedBatches = new();
/// <summary>单次操作元件上限</summary>
private const int MaxActionsPerBatch = 20;
/// <summary>将 AI 操作加入待确认队列</summary>
public void QueueActions(List<AIAction> 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);
}
/// <summary>执行队列中所有待确认的操作</summary>
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();
}
/// <summary>取消所有待确认的操作</summary>
public void CancelQueuedActions()
{
var count = _pendingQueue.Count;
_pendingQueue.Clear();
ClearPreviews();
Debug.Log($"[AIAction] 已取消 {count} 个待确认操作");
}
/// <summary>撤销上一次 AI 操作批次</summary>
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<AIAction> 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 快照恢复画布状态
}
}
}
@@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
namespace RedCircuit.AIAssistant
{
/// <summary>
/// AI 助搭核心协调器,管理 AI 会话生命周期,协调各子系统。
/// </summary>
public class AIAssistantManager : MonoBehaviour
{
public static AIAssistantManager Instance { get; private set; }
/// <summary>当前会话 ID</summary>
public string SessionId { get; private set; } = Guid.NewGuid().ToString("N")[..16];
/// <summary>AI 是否就绪</summary>
public bool IsReady { get; private set; }
/// <summary>聊天历史记录</summary>
public List<ChatMessage> 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<AIChatController>() ?? gameObject.AddComponent<AIChatController>();
_advisor = GetComponent<AICircuitAdvisor>() ?? gameObject.AddComponent<AICircuitAdvisor>();
_executor = GetComponent<AIActionExecutor>() ?? gameObject.AddComponent<AIActionExecutor>();
_contextProvider = GetComponent<AIContextProvider>() ?? gameObject.AddComponent<AIContextProvider>();
IsReady = true;
Debug.Log("[AIAssistant] Manager initialized");
}
/// <summary>
/// 处理用户输入的消息。
/// </summary>
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<AIResponse> 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<AIAction>(),
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;
}
}
/// <summary>用户确认执行 AI 操作</summary>
public void ConfirmActions()
{
_executor.ExecuteQueuedActions();
}
/// <summary>用户拒绝执行 AI 操作</summary>
public void RejectActions()
{
_executor.CancelQueuedActions();
}
/// <summary>撤销上一次 AI 操作</summary>
public void UndoLastAIAction()
{
_executor.UndoLastBatch();
}
}
}
@@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace RedCircuit.AIAssistant
{
/// <summary>
/// 聊天 UI 控制器,处理消息收发与流式显示。
/// </summary>
public class AIChatController : MonoBehaviour
{
/// <summary>聊天面板是否可见</summary>
public bool IsVisible { get; private set; }
/// <summary>消息列表 (用于 UI 渲染)</summary>
public List<ChatMessage> Messages { get; } = new();
/// <summary>显示聊天面板</summary>
public void Show()
{
IsVisible = true;
// TODO: 播放面板展开动画
}
/// <summary>隐藏聊天面板</summary>
public void Hide()
{
IsVisible = false;
// TODO: 播放面板收起动画
}
/// <summary>追加用户消息到 UI</summary>
public void AppendUserMessage(string content)
{
var msg = new ChatMessage
{
Role = MessageRole.User,
Content = content,
Timestamp = DateTime.Now
};
Messages.Add(msg);
// TODO: 实例化用户消息气泡 UI
}
/// <summary>追加 AI 消息到 UI (支持流式追加)</summary>
public void AppendAIMessage(string content)
{
var msg = new ChatMessage
{
Role = MessageRole.Assistant,
Content = content,
Timestamp = DateTime.Now
};
Messages.Add(msg);
// TODO: 实例化 AI 消息气泡 UI,支持打字机效果
}
/// <summary>追加系统消息</summary>
public void AppendSystemMessage(string content)
{
var msg = new ChatMessage
{
Role = MessageRole.System,
Content = content,
Timestamp = DateTime.Now
};
Messages.Add(msg);
// TODO: 实例化系统消息 (居中灰色样式)
}
/// <summary>追加错误消息</summary>
public void AppendErrorMessage(string content)
{
AppendSystemMessage($"[错误] {content}");
}
/// <summary>显示操作确认提示</summary>
public void ShowConfirmationPrompt(int actionCount)
{
// TODO: 显示 "AI 建议执行 N 个操作,是否确认?" 确认栏
Debug.Log($"[AIChat] 等待用户确认 {actionCount} 个操作");
}
/// <summary>清空聊天记录</summary>
public void ClearHistory()
{
Messages.Clear();
// TODO: 清空 UI 消息列表
}
}
}
@@ -0,0 +1,59 @@
using System.Collections.Generic;
using UnityEngine;
namespace RedCircuit.AIAssistant
{
/// <summary>
/// 电路分析与建议,解析 AI 返回的电路方案并展示诊断报告。
/// </summary>
public class AICircuitAdvisor : MonoBehaviour
{
/// <summary>显示电路分析报告</summary>
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 上展示分析报告面板
}
/// <summary>显示电路搭建方案供用户选择</summary>
public void ShowCircuitPlan(List<CircuitPlan> 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 上展示方案对比卡片
}
/// <summary>高亮显示问题位置</summary>
public void HighlightIssue(GridPosition location, IssueSeverity severity)
{
// TODO: 在画布上高亮显示问题格子
// Error -> 红色边框, Warning -> 黄色边框
}
/// <summary>清除所有高亮</summary>
public void ClearHighlights()
{
// TODO: 移除画布上的所有问题高亮
}
}
}
@@ -0,0 +1,83 @@
using UnityEngine;
namespace RedCircuit.AIAssistant
{
/// <summary>
/// 上下文采集器,收集当前游戏状态供 AI 参考。
/// </summary>
public class AIContextProvider : MonoBehaviour
{
/// <summary>采集当前游戏上下文</summary>
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;
}
}
}
@@ -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<AIAction> 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<CircuitIssue> Issues = new();
public List<CircuitOptimization> 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<AIAction> Steps = new();
}
// ==================== 辅助类型 ====================
[Serializable]
public struct GridPosition
{
public int X;
public int Y;
public GridPosition(int x, int y) { X = x; Y = y; }
}
/// <summary>操作批次 (用于撤销)</summary>
public class ActionBatch
{
public List<AIAction> Actions = new();
public string SnapshotBefore;
public string SnapshotAfter;
}
}
@@ -0,0 +1,10 @@
{
"name": "RedCircuit.AIAssistant",
"rootNamespace": "RedCircuit.AIAssistant",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"autoReferenced": true,
"defineConstraints": []
}
+292
View File
@@ -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 服务器) |
+33
View File
@@ -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
+6
View File
@@ -0,0 +1,6 @@
<Application x:Class="MRCC.Launcher.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Application.Resources>
</Application.Resources>
</Application>
+7
View File
@@ -0,0 +1,7 @@
using System.Windows;
namespace MRCC.Launcher;
public partial class App : Application
{
}
@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<RootNamespace>MRCC.Launcher</RootNamespace>
<AssemblyName>MRCC.Launcher</AssemblyName>
<Nullable>enable</Nullable>
<LangVersion>14.0</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<ApplicationManifest>app.manifest</ApplicationManifest>
<StartupObject>MRCC.Launcher.Program</StartupObject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PCL-CE\PCL.Core\PCL.Core.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="metadata.json">
<LogicalName>PCL.metadata.json</LogicalName>
</EmbeddedResource>
</ItemGroup>
</Project>
+128
View File
@@ -0,0 +1,128 @@
<Window x:Class="MRCC.Launcher.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MRCC Launcher"
Height="600" Width="900"
WindowStartupLocation="CenterScreen"
Background="#0E1116"
WindowStyle="SingleBorderWindow"
ResizeMode="CanResize"
MinHeight="500" MinWidth="700">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="220"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 侧边栏 -->
<Border Grid.Column="0" Background="#161B22" BorderBrush="#2A3441" BorderThickness="0,0,1,0">
<DockPanel Margin="0,20,0,0">
<!-- Logo -->
<StackPanel DockPanel.Dock="Top" Margin="20,0,20,20">
<TextBlock Text="MRCC" FontSize="20" FontWeight="Bold" Foreground="#E83229"
FontFamily="Consolas"/>
<TextBlock Text="MineRedCircuitcraft" FontSize="10" Foreground="#6B7280"
Margin="0,2,0,0"/>
</StackPanel>
<!-- 导航菜单 -->
<StackPanel DockPanel.Dock="Top" Margin="10,0">
<RadioButton x:Name="NavHome" Content="首页" IsChecked="True"
Style="{StaticResource NavRadioButtonStyle}"
Checked="OnNavChecked" Tag="home"/>
<RadioButton Content="下载"
Style="{StaticResource NavRadioButtonStyle}"
Checked="OnNavChecked" Tag="download"/>
<RadioButton Content="设置"
Style="{StaticResource NavRadioButtonStyle}"
Checked="OnNavChecked" Tag="settings"/>
<RadioButton Content="关于"
Style="{StaticResource NavRadioButtonStyle}"
Checked="OnNavChecked" Tag="about"/>
</StackPanel>
<!-- 底部版本信息 -->
<TextBlock DockPanel.Dock="Bottom" Text="v1.0.0-dev" Foreground="#4B5563"
FontSize="10" Margin="20,0,20,10" VerticalAlignment="Bottom"/>
</DockPanel>
</Border>
<!-- 主内容区 -->
<Grid Grid.Column="1">
<!-- 首页 -->
<StackPanel x:Name="PageHome" Margin="32,28" Visibility="Visible">
<TextBlock Text="欢迎来到 MRCC" FontSize="24" FontWeight="Bold" Foreground="#E8EAED"/>
<TextBlock Text="红石逻辑 x 像素方块 x 模拟电路" FontSize="13" Foreground="#6B7280"
Margin="0,6,0,24"/>
<Border Background="#161B22" CornerRadius="8" Padding="20" Margin="0,0,0,12">
<StackPanel>
<TextBlock Text="开始游戏" FontSize="14" FontWeight="Bold" Foreground="#E8EAED"/>
<TextBlock Text="点击下方按钮启动 MRCC 客户端" FontSize="11" Foreground="#6B7280"
Margin="0,4,0,12"/>
<Button Content="启动游戏" Width="120" Height="36" HorizontalAlignment="Left"
Background="#E83229" Foreground="White" BorderThickness="0"
FontSize="13" FontWeight="Bold" Click="OnLaunchGame"/>
</StackPanel>
</Border>
<Border Background="#161B22" CornerRadius="8" Padding="20" Margin="0,0,0,12">
<StackPanel>
<TextBlock Text="最新动态" FontSize="14" FontWeight="Bold" Foreground="#E8EAED"/>
<TextBlock Text="项目初始化中..." FontSize="11" Foreground="#6B7280" Margin="0,4,0,0"/>
</StackPanel>
</Border>
</StackPanel>
<!-- 下载页 -->
<StackPanel x:Name="PageDownload" Margin="32,28" Visibility="Collapsed">
<TextBlock Text="下载管理" FontSize="24" FontWeight="Bold" Foreground="#E8EAED"/>
<TextBlock Text="暂无下载任务" FontSize="13" Foreground="#6B7280" Margin="0,12,0,0"/>
</StackPanel>
<!-- 设置页 -->
<StackPanel x:Name="PageSettings" Margin="32,28" Visibility="Collapsed">
<TextBlock Text="设置" FontSize="24" FontWeight="Bold" Foreground="#E8EAED"/>
<TextBlock Text="设置项开发中..." FontSize="13" Foreground="#6B7280" Margin="0,12,0,0"/>
</StackPanel>
<!-- 关于页 -->
<StackPanel x:Name="PageAbout" Margin="32,28" Visibility="Collapsed">
<TextBlock Text="关于 MRCC" FontSize="24" FontWeight="Bold" Foreground="#E8EAED"/>
<TextBlock Text="MineRedCircuitcraft - 我的世界红石衍生版" FontSize="13" Foreground="#6B7280"
Margin="0,6,0,20"/>
<TextBlock Text="电路仿真游戏,红石逻辑 x 像素方块 x 模拟电路" FontSize="12" Foreground="#9BA3AE"/>
<TextBlock Text="核心库: PCL.Core (Apache 2.0)" FontSize="11" Foreground="#4B5563"
Margin="0,12,0,0"/>
</StackPanel>
</Grid>
</Grid>
<Window.Resources>
<Style x:Key="NavRadioButtonStyle" TargetType="RadioButton">
<Setter Property="Foreground" Value="#6B7280"/>
<Setter Property="FontSize" Value="13"/>
<Setter Property="Padding" Value="16,10"/>
<Setter Property="Margin" Value="0,2"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RadioButton">
<Border x:Name="bd" Background="Transparent" CornerRadius="6" Padding="{TemplateBinding Padding}">
<ContentPresenter VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="bd" Property="Background" Value="#1C2230"/>
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="bd" Property="Background" Value="#1C2230"/>
<Setter Property="Foreground" Value="#E83229"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
</Window>
+29
View File
@@ -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);
}
}
+26
View File
@@ -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();
}
}
@@ -0,0 +1,29 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace MRCC.Launcher.ViewModels;
/// <summary>
/// 主窗口 ViewModel,管理导航状态与页面数据。
/// </summary>
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;
/// <summary>
/// 启动游戏命令(后续实现具体逻辑)
/// </summary>
public void LaunchGame()
{
// TODO: 检查游戏安装状态 -> 启动 Unity 客户端
}
}
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="MRCC.Launcher"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
</application>
</compatibility>
</assembly>
+17
View File
@@ -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"
}
]
}
+4
View File
@@ -0,0 +1,4 @@
[*.cs]
# IDE0028: 简化集合初始化
dotnet_style_collection_initializer = true:silent
+63
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
# These are supported funding model platforms
custom: ['https://afdian.com/a/LTCat']
+52
View File
@@ -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
+47
View File
@@ -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
+67
View File
@@ -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
@@ -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
@@ -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
+49
View File
@@ -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
@@ -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
@@ -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
+17
View File
@@ -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."
+38
View File
@@ -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
@@ -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 }}
@@ -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 }}
@@ -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 }}
@@ -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 }}
+57
View File
@@ -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/**
+348
View File
@@ -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
+161
View File
@@ -0,0 +1,161 @@
# 如何为项目做贡献
Wiki 页面:
[开发指南](https://github.com/PCL-Community/PCL2-CE/wiki/开发指南)
[技术规范](https://github.com/PCL-Community/PCL2-CE/wiki/技术规范)
<details>
<summary>原 CONTRIBUTING.md 内容</summary>
## 开始之前
查看 [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 "<type>(scope): <subject>"
```
5. 推送分支到你的 Fork
```bash
git push origin your-branch
```
6. 创建 Pull Request
- 指向上游仓库的 `dev` 分支
- 详细填写 PR 信息
- 关联 Issue(如有)
## 开发规范
### 测试要求
- 提交前请在本地编译通过确保无误后提交
### Angular 规范
基本格式如下
```commit message
<type>(scope?): <subject>
<body>
<footer>
```
每次提交**必须包含页眉内容**,可以选用正文(`body`)和页脚(`footer`
每次提交的信息不超过 `100` 个字符
#### 页眉(`header`
页眉需包含提交类型(`type`)、作用域(`scope`,可选)和主题(`subject`
##### 提交类型(`type`
提交类型需指定为下面其中一个:
1. `build`:对构建系统或者外部依赖项进行修改
2. `chore`: 用于对非业务性代码进行修改,例如修改构建流程或者工具配置等
3. `ci`:对 CI 配置文件或脚本进行修改
4. `docs`:对文档进行修改
5. `feat`:增加新的特性
6. `fix`:修复 bug
7. `pref`:提高性能的代码更改
8. `refactor`:既不修复 bug 也不是添加特性的代码重构
9. `style`:不影响代码含义的修改,比如空格、格式化、缺失的分号等
10. `test`:增加缺失的测试或者修正已存在的测试
##### 作用域(`scope`
范围可以是任何指定提交更改位置的内容
##### 主题(`subject`
主题包括了对本次修改的简洁描述,有以下准则
1. 使用命令式与现在时态:`改变` 而不是 `已改变`,也不是 `改变了`
2. 不要大写首字母(若使用英文)
3. 不要在末尾添加句号
#### 正文(`body`
同主题,使用命令式与现在时态
应包含修改的动机以及和之前行为的对比
#### 页脚(`footer`
##### Breaking Changes
破坏性修改指的是本次提交使用了不兼容之前版本的 API 或者环境变量
所有不兼容修改都必须在页脚中作为破坏性修改提到,以 `BREAKING CHANGE:` 开头,后跟一个空格或者换行符,其余的信息就是对此次修改的描述、理由和注释
##### 引用完成的 Issue
如果本次提交目的是完成 Issue 的话,需在页脚引用该 Issue
以关键字 `Closes` 开头,如
```footer
Closes #1145
```
修改了多个 bug 以半角逗号和空格隔开
```footer
Closes #114, #514, #1919
```
#### 回滚(`revert`
若此次提交包含回滚(`revert`)操作,那么页眉需以 `revert:` 开头,同时在正文中添加 `本次提交回滚到 commit <hash>`,其中 `<hash>` 值表示被回滚前的提交
```commit message
revert:<type>(<scope>): <subject>
本次提交回滚到 commit <hash>
<body>
<footer>
```
## 提交上游
若你要将他人在 CE 中实现的功能或修复提交到上游,请用 Co-authored 来指明原始贡献者
</details>
Binary file not shown.
+207
View File
@@ -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.
@@ -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<ItemModel>().ToImmutableArray();
var groupList = groups.Cast<GroupModel>().ToImmutableArray();
var configList = configs.Cast<ConfigModel>().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<ItemModel>().OrderBy(i => i.DeclOrder).ToList();
var groups = tuple.Left.Right.Cast<GroupModel>().ToList();
var events = tuple.Right.Cast<EventRegisterModel>().OrderBy(e => e.DeclOrder).ToList();
// 两者都为空则不生成
if (items.Count == 0 && events.Count == 0) return;
var groupLookup = new Dictionary<INamedTypeSymbol, GroupModel>(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<ItemModel> items,
ImmutableArray<GroupModel> 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("// <auto-generated />");
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<ItemModel> items,
IReadOnlyList<EventRegisterModel> events,
IReadOnlyDictionary<INamedTypeSymbol, GroupModel> 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("// <auto-generated />");
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<string> 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<INamedTypeSymbol, GroupModel?> 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<StringBuilder>? _EmitItem(
StringBuilder sb,
ItemModel item,
int indent,
bool isTopLevel,
Func<ItemModel, string> resolveSource)
{
Action<StringBuilder>? 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<ItemModel, string> 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("/// <inheritdoc cref=\"").Append(typeName).AppendLine("\" />");
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<Action<StringBuilder>> 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<string> CheckScope(IReadOnlySet<string> keys)");
sb.Append(indentStr).AppendLine(" {");
sb.Append(indentStr).AppendLine(" IEnumerable<string> 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<GroupNode> Children { get; } = [];
public List<ItemModel> 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<ItemModel> TopItems { get; set; } = [];
public List<GroupNode> TopGroups { get; set; } = [];
public Func<ItemModel, string> ResolveSourceCode { get; set; } = static _ => "ConfigSource.Shared";
}
private sealed class EventRegisterModel
{
public IPropertySymbol Property { get; set; } = null!;
public int DeclOrder { get; set; }
}
}
@@ -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<DependencyMatchResult> 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<CollectorInfo>();
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<INamedTypeSymbol, List<CollectorInfo>>(attr, infos);
})
.Where(x => x.Key is not null)
.Collect()
// 此处合并到 dictionary 以优化后续查找性能
.Select(static (pairs, _) =>
{
var dict = new Dictionary<INamedTypeSymbol, List<CollectorInfo>>(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<DependencyMatchResult>();
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<DependencyMatchResult> matches)
{
var dependencyMap = new Dictionary<CollectorInfo, Dictionary<AttributeTargets, List<DependencyMatchResult>>>();
foreach (var dep in matches)
{
var info = dep.Info;
if (!dependencyMap.TryGetValue(info, out var map))
{
map = new Dictionary<AttributeTargets, List<DependencyMatchResult>>
{
[AttributeTargets.Class] = [],
[AttributeTargets.Method] = [],
[AttributeTargets.Property] = []
};
dependencyMap[info] = map;
}
map[dep.TargetType].Add(dep);
}
var sb = new StringBuilder(1024);
sb.AppendLine("// <auto-generated />");
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<string, Dictionary<AttributeTargets, DependencyGroup>> _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<string>)(() =>
{
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<InjectionPointMatchResult> matches)
{
foreach (var match in matches)
{
var sb = new StringBuilder(1024);
// file header
sb.AppendLine("// 此文件由 Source Generator 自动生成,请勿手动修改");
sb.AppendLine("// <auto-generated />");
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());
}
}
}
@@ -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<string>()
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("// <auto-generated />");
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<string, string?> 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("\"", "\"\"") + "\"";
}
}
@@ -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<string> _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<ScopeMethodModel> 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<Type> _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("// <auto-generated />");
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<ScopeMethodModel> 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<string> _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<string>();
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<string> args)
=> $"{prefix}{(model.Awaitable ? "await " : "")}{model.MethodName}({string.Join(", ", args)});";
}
}
@@ -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<string>(), [..data.Right.Where(x => x is not null).Select(x => x!)]));
}
private static List<string>? _GetValidLifecycleStates(string? content)
{
if (string.IsNullOrEmpty(content))
return null;
var validStates = new List<string>();
// 提取枚举定义块
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<string> validStates, ImmutableArray<LifecycleServiceInfo> 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("// <auto-generated />");
sb.AppendLine("// 此文件由 Source Generator 自动生成,请勿手动修改");
sb.AppendLine();
sb.AppendLine("using System;");
sb.AppendLine();
sb.AppendLine("namespace PCL.Core.App.IoC;");
sb.AppendLine();
sb.AppendLine("/// <summary>");
sb.AppendLine("/// 包含所有使用 LifecycleService 注解的类型,按 StartState 分类并按 Priority 降序排序");
sb.AppendLine("/// </summary>");
sb.AppendLine("public static class LifecycleServiceTypes");
sb.AppendLine("{");
// 为每个状态生成数组
foreach (var group in groupedServices)
{
var sortedServices = group.OrderByDescending(s => s.Priority).ToList();
sb.AppendLine($" /// <summary>");
sb.AppendLine($" /// {group.Key} 状态的生命周期服务类型");
sb.AppendLine($" /// </summary>");
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(" /// <summary>");
sb.AppendLine(" /// 获取指定生命周期状态的所有服务类型");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" /// <param name=\"state\">生命周期状态</param>");
sb.AppendLine(" /// <returns>该状态下的所有服务类型数组</returns>");
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(" /// <summary>");
sb.AppendLine(" /// 获取所有生命周期服务类型的状态映射");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" /// <returns>状态到类型数组的字典</returns>");
sb.AppendLine(" public static System.Collections.Generic.Dictionary<LifecycleState, Type[]> GetAllServiceTypes() => new()");
sb.AppendLine(" {");
foreach (var group in groupedServices)
{
sb.AppendLine($" [LifecycleState.{group.Key}] = {group.Key},");
}
sb.AppendLine(" };");
sb.AppendLine();
// 生成统计信息方法
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// 获取生命周期服务的统计信息");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" /// <returns>包含状态数量和总服务数量的统计信息</returns>");
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;
}
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<IncludeBuildOutput>false</IncludeBuildOutput>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<Platforms>AnyCPU;x64;ARM64</Platforms>
<Configurations>Debug;CI;Release;Beta</Configurations>
<!-- 确保源代码生成器在所有平台架构下都能正常工作 -->
<PlatformTarget>AnyCPU</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="5.6.0" PrivateAssets="all" />
</ItemGroup>
</Project>
@@ -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";
}
@@ -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<INamedTypeSymbol>();
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<string>();
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();
}
}
}
@@ -0,0 +1,10 @@
namespace System.Runtime.CompilerServices;
using ComponentModel;
/// <summary>
/// Reserved to be used by the compiler for tracking metadata.
/// This class should not be used by developers in source code.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit;
+177
View File
@@ -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
+410
View File
@@ -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
+195
View File
@@ -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;
/// <summary>
/// 基础工具集。
/// </summary>
public static class Basics
{
#region
/// <summary>
/// 启动器元数据。
/// </summary>
public static MetadataModel Metadata { get; } = JsonSerializer.Deserialize<MetadataModel>(
Assembly.GetEntryAssembly()!.GetManifestResourceStream("PCL.metadata.json")!, JsonCompat.SerializerOptions)!;
/// <summary>
/// 版本名称。
/// </summary>
public static string VersionName => Metadata.Version.BaseName;
/// <summary>
/// 版本内部代号。
/// </summary>
public static int VersionCode => Metadata.Version.Code;
/// <summary>
/// 版本分支名。
/// </summary>
public static string VersionBranch => Metadata.Version.BranchName;
/// <summary>
/// 当前日期是否为愚人节。
/// </summary>
public static bool IsAprilFool => DateTime.Now is { Month: 4, Day: 1 };
#endregion
#region
/// <summary>
/// 当前进程实例。
/// </summary>
public static Process CurrentProcess { get; } = Process.GetCurrentProcess();
/// <summary>
/// 当前进程 ID。
/// </summary>
public static int CurrentProcessId { get; } = CurrentProcess.Id;
/// <summary>
/// 当前进程可执行文件的绝对路径。
/// </summary>
public static string ExecutablePath { get; } = Environment.ProcessPath!;
/// <summary>
/// 当前进程可执行文件所在的目录。若有需求,请使用 <see cref="Path.Combine(string[])"/> 而不是自行拼接路径。
/// </summary>
public static string ExecutableDirectory { get; } = GetParentPath(ExecutablePath) ?? CurrentDirectory;
/// <summary>
/// 当前进程可执行文件的名称,含扩展名。
/// </summary>
public static string ExecutableName { get; } = Path.GetFileName(ExecutablePath);
/// <summary>
/// 当前进程可执行文件的名称,不含扩展名。
/// </summary>
public static string ExecutableNameWithoutExtension { get; } = Path.GetFileNameWithoutExtension(ExecutablePath);
/// <summary>
/// 当前进程包括第一个参数(文件名)的完整命令行参数。
/// </summary>
public static string[] FullCommandLineArguments { get; } = Environment.GetCommandLineArgs();
/// <summary>
/// 当前进程不包括第一个参数(文件名)的命令行参数。
/// </summary>
public static string[] CommandLineArguments { get; } = FullCommandLineArguments[1..];
/// <summary>
/// 实时获取的当前目录。若要在可执行文件目录中存放文件等内容,请使用更准确的 <see cref="ExecutableDirectory"/> 而不是这个目录。
/// </summary>
public static string CurrentDirectory => Environment.CurrentDirectory;
#endregion
#region 线
/// <summary>
/// 在新的工作线程运行指定委托。
/// </summary>
/// <param name="action">要运行的委托</param>
/// <param name="name">线程名,默认为 <c>WorkerThread@[ThreadId]</c></param>
/// <param name="priority">线程优先级</param>
/// <returns>新创建的线程实例</returns>
public static Thread RunInNewThread(Action action, string? name = null, ThreadPriority priority = ThreadPriority.Normal)
{
var threadName = new AtomicVariable<string>(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
/// <summary>
/// 获取某个路径的父路径/目录。
/// </summary>
/// <param name="path">路径文本</param>
/// <returns>父路径文本,可能为 <c>null</c></returns>
public static string? GetParentPath(string path) => Path.GetDirectoryName(path) ?? Path.GetPathRoot(path);
/// <summary>
/// 获取某个路径的父路径/目录。
/// </summary>
/// <param name="path">路径文本</param>
/// <returns>父路径文本,或空白</returns>
public static string GetParentPathOrEmpty(string path) => GetParentPath(path) ?? string.Empty;
/// <summary>
/// 获取某个路径的父路径/目录。
/// </summary>
/// <param name="path">路径文本</param>
/// <returns>父路径文本,或默认 (<see cref="CurrentDirectory"/>)</returns>
public static string GetParentPathOrDefault(string path) => GetParentPath(path) ?? CurrentDirectory;
/// <summary>
/// 以默认方式打开一个路径 (文件或目录)
/// </summary>
/// <param name="path">路径文本</param>
/// <param name="workingDirectory">执行工作目录</param>
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
/// <summary>
/// 获取程序打包资源的输入流。该资源必须声明为 <c>Resource</c> 类型,否则将会报错,<c>Images</c>
/// 和 <c>Resources</c> 目录已默认声明该类型。
/// </summary>
/// <param name="path">资源路径,例如 "Resources/java-wrapper.jar"</param>
/// <returns>资源输入流,若资源不存在则为 <c>null</c></returns>
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
}
@@ -0,0 +1,8 @@
namespace PCL.Core.App.Cli;
public enum ArgumentValueKind
{
Bool,
Decimal,
Text,
}
@@ -0,0 +1,37 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace PCL.Core.App.Cli;
public class BoolArgument : CommandArgument<bool>
{
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<T>([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<T, byte>(ref value) = Unsafe.As<bool, byte>(ref v);
#pragma warning disable CS8762 // The analyzer sucks.
return true;
#pragma warning restore CS8762
}
}
@@ -0,0 +1,92 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace PCL.Core.App.Cli;
/// <summary>
/// 无泛型的命令行参数模型
/// </summary>
/// <seealso cref="CommandArgument{TValue}"/>
public abstract class CommandArgument
{
/// <summary>
/// 参数键
/// </summary>
public required string Key { get; init; }
/// <summary>
/// 参数值文本
/// </summary>
public required string ValueText { get; init; }
/// <summary>
/// 参数值类型
/// </summary>
public abstract ArgumentValueKind ValueKind { get; }
/// <summary>
/// 尝试以指定类型获取参数值
/// </summary>
/// <param name="value">参数值,若尝试失败则为该类型默认值</param>
/// <typeparam name="T">参数值的类型</typeparam>
/// <returns>是否成功,若类型不匹配则失败</returns>
public abstract bool TryCastValue<T>([NotNullWhen(true)] out T? value);
public T? CastValue<T>()
{
var result = TryCastValue(out T? value);
return result ? value : throw new InvalidCastException("Value type mismatch or cannot cast");
}
}
/// <summary>
/// 命令行参数模型
/// </summary>
/// <typeparam name="TValue">参数值的类型</typeparam>
public abstract class CommandArgument<TValue> : CommandArgument
{
/// <summary>
/// 从参数值文本中解析参数类型
/// </summary>
/// <returns>对应类型的参数值</returns>
protected abstract TValue ParseValueText();
private bool _isValueParsed = false;
/// <summary>
/// 参数值
/// </summary>
public TValue Value
{
get
{
if (_isValueParsed) return field;
_isValueParsed = true;
return field = ParseValueText();
}
protected init
{
field = value;
_isValueParsed = true;
}
} = default!;
public override bool TryCastValue<T>([NotNullWhen(true)] out T value)
{
if (Value is T v)
{
value = v;
return true;
}
value = default!;
if (typeof(T) == typeof(string))
{
Unsafe.As<T, string>(ref value) = ValueText;
#pragma warning disable CS8762 // The analyzer sucks.
return true;
#pragma warning restore CS8762
}
return false;
}
}
@@ -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;
/// <summary>
/// 命令行模型
/// </summary>
[JsonConverter(typeof(CommandLineJsonConverter))]
public class CommandLine
{
/// <summary>
/// 命令文本
/// </summary>
public required string CommandText { get; init; }
/// <summary>
/// 子命令
/// </summary>
public CommandLine? Subcommand { get; init; } = null;
/// <summary>
/// 子命令文本
/// </summary>
public string? SubcommandText => Subcommand?.CommandText;
/// <summary>
/// 参数字典
/// </summary>
public required IReadOnlyDictionary<string, CommandArgument> Arguments { get; init; }
/// <summary>
/// 尝试获取参数值
/// </summary>
/// <param name="key">参数键</param>
/// <param name="value">参数值,若获取失败则为对应类型默认值</param>
/// <typeparam name="TValue">参数值的类型</typeparam>
/// <returns>是否存在该键; 存在该键时值的类型是否匹配</returns>
public (bool exists, bool isTypeMatch) TryGetArgumentValue<TValue>(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);
}
/// <summary>
/// 解析参数数组,第一个元素会被视为主命令
/// </summary>
/// <param name="args">参数数组</param>
/// <param name="subcommands">各级子命令列表</param>
/// <returns>命令行模型实例</returns>
public static CommandLine Parse(ReadOnlySpan<string> args, IEnumerable<SubcommandDefinition>? 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<bool>() ? "true" : "false",
ArgumentValueKind.Decimal => arg.CastValue<decimal>().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<string> 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<string, CommandArgument>();
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
};
}
}
/// <summary>
/// 用于 <see cref="CommandLine"/> 的 JSON 转换器
/// </summary>
// Generated by gpt-5.3-codex (20260218)
public sealed class CommandLineJsonConverter : JsonConverter<CommandLine>
{
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<string, CommandArgument>();
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<CommandLine>(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<string, CommandArgument> 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.");
}
}
@@ -0,0 +1,47 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
namespace PCL.Core.App.Cli;
public class DecimalArgument : CommandArgument<decimal>
{
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<T>([NotNullWhen(true)] out T value)
{
if (base.TryCastValue(out value)) return true;
var type = typeof(T);
try
{
if (type == typeof(int)) Unsafe.As<T, int>(ref value) = Convert.ToInt32(Value);
else if (type == typeof(long)) Unsafe.As<T, long>(ref value) = Convert.ToInt64(Value);
else if (type == typeof(double)) Unsafe.As<T, double>(ref value) = Convert.ToDouble(Value);
else if (type == typeof(float)) Unsafe.As<T, float>(ref value) = Convert.ToSingle(Value);
else if (type == typeof(short)) Unsafe.As<T, short>(ref value) = Convert.ToInt16(Value);
else if (type == typeof(sbyte)) Unsafe.As<T, sbyte>(ref value) = Convert.ToSByte(Value);
else if (type == typeof(ulong)) Unsafe.As<T, ulong>(ref value) = Convert.ToUInt64(Value);
else if (type == typeof(uint)) Unsafe.As<T, uint>(ref value) = Convert.ToUInt32(Value);
else if (type == typeof(ushort)) Unsafe.As<T, ushort>(ref value) = Convert.ToUInt16(Value);
else if (type == typeof(byte)) Unsafe.As<T, byte>(ref value) = Convert.ToByte(Value);
else if (type == typeof(nint)) Unsafe.As<T, nint>(ref value) = checked((nint)Convert.ToInt64(Value));
else if (type == typeof(nuint)) Unsafe.As<T, nuint>(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;
}
}
}
@@ -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<SubcommandDefinition> Subcommands { private get; init; }
public IReadOnlyDictionary<string, SubcommandDefinition> SubcommandMap
{
get
{
if (field is not null) return field;
var map = new Dictionary<string, SubcommandDefinition>();
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<SubcommandDefinition> subcommands) tuple)
{
return new SubcommandDefinition
{
CommandText = tuple.commandText,
Subcommands = tuple.subcommands
};
}
public static implicit operator SubcommandDefinition(string commandText)
{
return new SubcommandDefinition
{
CommandText = commandText,
Subcommands = []
};
}
}
@@ -0,0 +1,8 @@
namespace PCL.Core.App.Cli;
public class TextArgument : CommandArgument<string>
{
public override ArgumentValueKind ValueKind => ArgumentValueKind.Text;
protected override string ParseValueText() => ValueText;
}
+642
View File
@@ -0,0 +1,642 @@
using PCL.Core.App.Configuration;
namespace PCL.Core.App;
/// <summary>
/// 全局配置类。
/// </summary>
// ReSharper disable InconsistentNaming
public static partial class Config
{
/// <summary>
/// 系统配置。
/// </summary>
[ConfigGroup("System")] partial class SystemConfigGroup
{
// /// <summary>
// /// 系统缓存目录。
// /// </summary>
// [ConfigItem<string>("SystemSystemCache", "")] public partial string CacheDirectory { get; set; }
/// <summary>
/// 禁用硬件加速。
/// </summary>
[ConfigItem<bool>("SystemDisableHardwareAcceleration", false)] public partial bool DisableHardwareAcceleration { get; set; }
/// <summary>
/// 遥测。
/// </summary>
[ConfigItem<bool>("SystemTelemetry", false)] public partial bool Telemetry { get; set; }
/// <summary>
/// 实时日志最大行数。
/// </summary>
[ConfigItem<int>("SystemMaxLog", 13)] public partial int MaxGameLog { get; set; }
/// <summary>
/// 动画帧率上限。
/// </summary>
[ConfigItem<int>("UiAniFPS", 59)] public partial int AnimationFpsLimit { get; set; }
}
/// <summary>
/// 网络配置。
/// </summary>
[ConfigGroup("Network")] partial class NetworkConfigGroup
{
[ConfigItem<bool>("SystemNetEnableDoH", true)] public partial bool EnableDoH { get; set; }
[ConfigGroup("HttpProxy")] partial class HttpProxyConfigGroup
{
[ConfigItem<string>("SystemHttpProxy", "", ConfigSource.SharedEncrypt)] public partial string CustomAddress { get; set; }
[ConfigItem<int>("SystemHttpProxyType", 1)] public partial int Type { get; set; }
[ConfigItem<string>("SystemHttpProxyCustomUsername", "")] public partial string CustomUsername { get; set; }
[ConfigItem<string>("SystemHttpProxyCustomPassword", "")] public partial string CustomPassword { get; set; }
}
}
/// <summary>
/// 调试配置
/// </summary>
[ConfigGroup("Debug")] partial class DebugConfigGroup
{
[ConfigItem<bool>("SystemDebugMode", false)] public partial bool Enabled { get; set; }
[ConfigItem<int>("SystemDebugAnim", 9)] public partial int AnimationSpeed { get; set; }
[ConfigItem<bool>("SystemDebugDelay", false)] public partial bool AddRandomDelay { get; set; }
[ConfigItem<bool>("SystemDebugSkipCopy", false)] public partial bool DontCopy { get; set; }
[ConfigItem<bool>("SystemDebugAllowRestrictedFeature", false)] public partial bool AllowRestrictedFeature { get; set; }
}
/// <summary>
/// 下载配置。
/// </summary>
[ConfigGroup("Download")] partial class DownloadConfigGroup
{
[ConfigItem<int>("ToolDownloadThread", 63)] public partial int ThreadLimit { get; set; }
[ConfigItem<int>("ToolDownloadSpeed", 42)] public partial int SpeedLimit { get; set; }
[ConfigItem<int>("ToolDownloadSource", 1)] public partial int FileSource { get; set; }
[ConfigItem<int>("ToolDownloadVersion", 1)] public partial int VersionListSource { get; set; }
[ConfigItem<bool>("ToolDownloadAutoSelectVersion", true)] public partial bool AutoSelectInstance { get; set; }
[ConfigItem<bool>("ToolFixAuthlib", true)] public partial bool FixAuthLib { get; set; }
/// <summary>
/// 第三方资源配置。
/// </summary>
[ConfigGroup("Comp")] partial class CompConfigGroup
{
[ConfigItem<int>("ToolDownloadTranslate", 0)] public partial int NameFormatV1 { get; set; }
[ConfigItem<int>("ToolDownloadTranslateV2", 1)] public partial int NameFormatV2 { get; set; }
[ConfigItem<bool>("ToolDownloadIgnoreQuilt", true)] public partial bool IgnoreQuilt { get; set; }
[ConfigItem<bool>("ToolDownloadAutoInstallDependencies", true)] public partial bool AutoInstallDependencies { get; set; }
[ConfigItem<bool>("ToolDownloadClipboard", false)] public partial bool ReadClipboard { get; set; }
[ConfigItem<int>("ToolDownloadMod", 1)] public partial int CompSourceSolution { get; set; }
[ConfigItem<int>("ToolModLocalNameStyle", 0)] public partial int UiCompNameSolution { get; set; }
[ConfigItem<int>("ToolDownloadQuickBehavior", 0)] public partial int QuickDownloadBehavior { get; set; }
}
}
/// <summary>
/// 工具配置。
/// </summary>
[ConfigGroup("Tool")] partial class ToolConfigGroup
{
[ConfigItem<bool>("ToolHelpChinese", true)] public partial bool AutoChangeLanguage { get; set; }
// [ConfigItem<int>("ToolUpdateAlpha", 0, ConfigSource.SharedEncrypt)] public partial int Alpha { get; set; }
[ConfigItem<bool>("ToolUpdateRelease", false)] public partial bool ReleaseNotification { get; set; }
[ConfigItem<bool>("ToolUpdateSnapshot", false)] public partial bool SnapshotNotification { get; set; }
}
/// <summary>
/// 更新配置。
/// </summary>
[ConfigGroup("Update")] partial class UpdateConfigGroup
{
/// <summary>
/// 自动更新行为。
/// </summary>
[ConfigItem<LauncherAutoUpdateBehavior>("SystemSystemUpdate", LauncherAutoUpdateBehavior.DownloadAndAnnounce, ConfigSource.Local)] public partial LauncherAutoUpdateBehavior UpdateMode { get; set; }
/// <summary>
/// 更新分支。
/// </summary>
[ConfigItem<UpdateChannel>("SystemUpdateChannel", UpdateChannel.Release, ConfigSource.Local)] public partial UpdateChannel UpdateChannel { get; set; }
/// <summary>
/// Mirror 酱 CDK。
/// </summary>
[ConfigItem<string>("SystemMirrorChyanKey", "", ConfigSource.SharedEncrypt)] public partial string MirrorChyanKey { get; set; }
}
/// <summary>
/// 联机大厅配置。
/// </summary>
[ConfigGroup("Link")] partial class LinkConfigGroup
{
/// <summary>
/// 大厅用户名。
/// </summary>
[ConfigItem<string>("LinkUsername", "")] public partial string Username { get; set; }
/// <summary>
/// 中继方式。
/// </summary>
[ConfigItem<LinkRelayBehavior>("LinkRelayType", LinkRelayBehavior.Default)] public partial LinkRelayBehavior RelayType { get; set; }
/// <summary>
/// 中继服务器类型 (社区/自有)。
/// </summary>
[ConfigItem<int>("LinkServerType", 1)] public partial int ServerType { get; set; }
/// <summary>
/// 延迟优先模式。
/// </summary>
[ConfigItem<bool>("LinkLatencyFirstMode", true)] public partial bool UseLatencyFirstMode { get; set; }
/// <summary>
/// 自定义中继服务器。
/// </summary>
[ConfigItem<string>("LinkRelayServer", "")] public partial string CustomRelayServer { get; set; }
/// <summary>
/// 传输协议优先策略。
/// </summary>
[ConfigItem<LinkProtocolPreference>("LinkProtocolPreference", LinkProtocolPreference.Tcp)] public partial LinkProtocolPreference ProtocolPreference { get; set; }
/// <summary>
/// 尝试使用端口猜测打通对称性 NAT。
/// </summary>
[ConfigItem<bool>("LinkTryPunchSym", true)] public partial bool TryPunchSym { get; set; }
/// <summary>
/// 启用 IPv6。
/// </summary>
[ConfigItem<bool>("LinkEnableIPv6", true)] public partial bool EnableIPv6 { get; set; }
/// <summary>
/// 在日志中输出 Cli 信息以用于调试。
/// </summary>
[ConfigItem<bool>("LinkEnableCliOutput", false)] public partial bool EnableCliOutput { get; set; }
}
/// <summary>
/// 个性化配置。
/// </summary>
[ConfigGroup("Preference")] partial class PreferenceConfigGroup
{
/// <summary>
/// 启动时显示 Logo。
/// </summary>
[ConfigItem<bool>("UiLauncherLogo", true, ConfigSource.Local)] public partial bool ShowStartupLogo { get; set; }
/// <summary>
/// 锁定窗口大小。
/// </summary>
[ConfigItem<bool>("UiLockWindowSize", false)] public partial bool LockWindowSize { get; set; }
/// <summary>
/// 在启动游戏时显示你知道吗。
/// </summary>
[ConfigItem<bool>("UiShowLaunchingHint", true, ConfigSource.Local)] public partial bool ShowLaunchingHint { get; set; }
/// <summary>
/// 标题内容类型。
/// </summary>
[ConfigItem<LauncherTitleType>("UiLogoType", LauncherTitleType.Default, ConfigSource.Local)] public partial LauncherTitleType WindowTitleType { get; set; }
/// <summary>
/// 窗口标题文本。
/// </summary>
[ConfigItem<string>("UiLogoText", "", ConfigSource.Local)] public partial string WindowTitleCustomText { get; set; }
/// <summary>
/// 导航栏居左。
/// </summary>
[ConfigItem<bool>("UiLogoLeft", false, ConfigSource.Local)] public partial bool TopBarLeftAlign { get; set; }
/// <summary>
/// 全局字体。
/// </summary>
[ConfigItem<string>("UiFont", "", ConfigSource.Local)] public partial string Font { get; set; }
/// <summary>
/// MOTD 字体。
/// </summary>
[ConfigItem<string>("UiMotdFont", "", ConfigSource.Local)] public partial string MotdFont { get; set; }
/// <summary>
/// 详细实例分类。
/// </summary>
[ConfigItem<bool>("DetailedInstanceClassification", false, ConfigSource.Local)] public partial bool DetailedInstanceClassification { get; set; }
/// <summary>
/// 本地化配置。
/// </summary>
[ConfigGroup("Localization", ConfigSource.Local)] partial class LocalizationConfigGroup
{
/// <summary>
/// UI 语言。auto 表示跟随系统语言。
/// </summary>
[ConfigItem<string>("UiLanguage", "auto")] public partial string Language { get; set; }
/// <summary>
/// UI 展示格式所使用的区域性。auto 表示跟随系统区域格式。
/// </summary>
[ConfigItem<string>("UiFormatCulture", "auto")] public partial string FormatCulture { get; set; }
/// <summary>
/// 区域覆盖。auto 表示自动判断。
/// </summary>
[ConfigItem<string>("UiRegion", "auto")] public partial string Region { get; set; }
}
/// <summary>
/// 界面主题配置。
/// </summary>
[ConfigGroup("Theme")] partial class ThemeConfigGroup
{
/// <summary>
/// 配色主题模式。
/// </summary>
[ConfigItem<ColorMode>("UiDarkMode", ColorMode.System)] public partial ColorMode ColorMode { get; set; }
/// <summary>
/// 暗色配色主题。
/// </summary>
[ConfigItem<ColorTheme>("UiDarkColor", ColorTheme.CatBlue)] public partial ColorTheme DarkColor { get; set; }
/// <summary>
/// 亮色配色主题。
/// </summary>
[ConfigItem<ColorTheme>("UiLightColor", ColorTheme.CatBlue)] public partial ColorTheme LightColor { get; set; }
/// <summary>
/// 窗口透明度。
/// </summary>
[ConfigItem<int>("UiLauncherTransparent", 600, ConfigSource.Local)] public partial int WindowOpacity { get; set; }
/// <summary>
/// 自定义主题:色相 (H)。
/// </summary>
[ConfigItem<int>("UiLauncherHue", 180, ConfigSource.Local)] public partial int WindowHue { get; set; }
/// <summary>
/// 自定义主题:饱和度 (S)。
/// </summary>
[ConfigItem<int>("UiLauncherSat", 80, ConfigSource.Local)] public partial int WindowSat { get; set; }
/// <summary>
/// 自定义主题:明度 (L)。
/// </summary>
[ConfigItem<int>("UiLauncherLight", 20, ConfigSource.Local)] public partial int WindowLight { get; set; }
/// <summary>
/// 自定义主题:色相渐变。
/// </summary>
[ConfigItem<int>("UiLauncherDelta", 90, ConfigSource.Local)] public partial int WindowDelta { get; set; }
/// <summary>
/// 传说中的主题选择,但是没卵用。
/// </summary>
[ConfigItem<int>("UiLauncherTheme", 0, ConfigSource.Local)] public partial int ThemeSelected { get; set; }
}
/// <summary>
/// 背景内容。
/// </summary>
[ConfigGroup("Background")] partial class BackgroundConfigGroup
{
/// <summary>
/// 彩色底部填充。
/// </summary>
[ConfigItem<bool>("UiBackgroundColorful", true, ConfigSource.Local)] public partial bool BackgroundColorful { get; set; }
/// <summary>
/// 透明度。
/// </summary>
[ConfigItem<int>("UiBackgroundOpacity", 1000, ConfigSource.Local)] public partial int WallpaperOpacity { get; set; }
/// <summary>
/// 旋转。
/// </summary>
[ConfigItem<int>("UiBackgroundCarousel", 1000, ConfigSource.Local)] public partial int WallpaperCarousel { get; set; }
/// <summary>
/// 模糊遮罩。
/// </summary>
[ConfigItem<int>("UiBackgroundBlur", 0, ConfigSource.Local)] public partial int WallpaperBlurRadius { get; set; }
/// <summary>
/// 内容裁剪模式。
/// </summary>
[ConfigItem<int>("UiBackgroundSuit", 0, ConfigSource.Local)] public partial int WallpaperSuitMode { get; set; }
/// <summary>
/// 视频自动暂停。
/// </summary>
[ConfigItem<bool>("UiAutoPauseVideo", true, ConfigSource.Local)] public partial bool AutoPauseVideo { get; set; }
}
/// <summary>
/// 高级材质。
/// </summary>
[ConfigGroup("Blur")] partial class BlurConfigGroup
{
/// <summary>
/// 是否启用。
/// </summary>
[ConfigItem<bool>("UiBlur", false, ConfigSource.Local)] public partial bool IsEnabled { get; set; }
/// <summary>
/// 模糊半径。
/// </summary>
[ConfigItem<int>("UiBlurValue", 16, ConfigSource.Local)] public partial int Radius { get; set; }
/// <summary>
/// 采样率。
/// </summary>
[ConfigItem<int>("UiBlurSamplingRate", 70, ConfigSource.Local)] public partial int SamplingRate { get; set; }
/// <summary>
/// 模糊方法。
/// </summary>
[ConfigItem<int>("UiBlurType", 0, ConfigSource.Local)] public partial int KernelType { get; set; }
}
/// <summary>
/// 自定义主页。
/// </summary>
[ConfigGroup("Homepage")] partial class HomepageConfigGroup
{
/// <summary>
/// 主页来源类型。
/// </summary>
[ConfigItem<int>("UiCustomType", 0, ConfigSource.Local)] public partial int Type { get; set; }
/// <summary>
/// 预设选项。
/// </summary>
[ConfigItem<int>("UiCustomPreset", 0, ConfigSource.Local)] public partial int SelectedPreset { get; set; }
/// <summary>
/// 自定义 URL。
/// </summary>
[ConfigItem<string>("UiCustomNet", "", ConfigSource.Local)] public partial string CustomUrl { get; set; }
}
/// <summary>
/// 背景音乐。
/// </summary>
[ConfigGroup("Music")] partial class MusicConfigGroup
{
/// <summary>
/// 音量。
/// </summary>
[ConfigItem<int>("UiMusicVolume", 500, ConfigSource.Local)] public partial int Volume { get; set; }
/// <summary>
/// 启动游戏后自动暂停。
/// </summary>
[ConfigItem<bool>("UiMusicStop", false, ConfigSource.Local)] public partial bool StopInGame { get; set; }
/// <summary>
/// 启动游戏后自动开始播放。
/// </summary>
[ConfigItem<bool>("UiMusicStart", false, ConfigSource.Local)] public partial bool StartInGame { get; set; }
/// <summary>
/// 自动开始播放。
/// </summary>
[ConfigItem<bool>("UiMusicAuto", true, ConfigSource.Local)] public partial bool StartOnStartup { get; set; }
/// <summary>
/// 随机播放。
/// </summary>
[ConfigItem<bool>("UiMusicRandom", true, ConfigSource.Local)] public partial bool ShufflePlayback { get; set; }
/// <summary>
/// 启用 SMTC。
/// </summary>
[ConfigItem<bool>("UiMusicSMTC", true, ConfigSource.Local)] public partial bool EnableSMTC { get; set; }
}
/// <summary>
/// 功能隐藏。
/// </summary>
[ConfigGroup("Hide")]
partial class HideConfigGroup
{
// 主页面
[ConfigItem<bool>("UiHiddenPageDownload", false, ConfigSource.Local)] public partial bool PageDownload { get; set; }
[ConfigItem<bool>("UiHiddenPageSetup", false, ConfigSource.Local)] public partial bool PageSetup { get; set; }
[ConfigItem<bool>("UiHiddenPageTools", false, ConfigSource.Local)] public partial bool PageTools { get; set; }
// 子页面 设置
[ConfigItem<bool>("UiHiddenSetupLaunch", false, ConfigSource.Local)] public partial bool SetupLaunch { get; set; }
[ConfigItem<bool>("UiHiddenSetupUi", false, ConfigSource.Local)] public partial bool SetupUi { get; set; }
[ConfigItem<bool>("UiHiddenSetupLauncherLanguage", false, ConfigSource.Local)] public partial bool SetupLauncherLanguage { get; set; }
[ConfigItem<bool>("UiHiddenSetupLauncherMisc", false, ConfigSource.Local)] public partial bool SetupLauncherMisc { get; set; }
[ConfigItem<bool>("UiHiddenSetupGameManage", false, ConfigSource.Local)] public partial bool SetupGameManage { get; set; }
[ConfigItem<bool>("UiHiddenSetupJava", false, ConfigSource.Local)] public partial bool SetupJava { get; set; }
[ConfigItem<bool>("UiHiddenSetupUpdate", false, ConfigSource.Local)] public partial bool SetupUpdate { get; set; }
[ConfigItem<bool>("UiHiddenSetupGameLink", false, ConfigSource.Local)] public partial bool SetupGameLink { get; set; } // 新增
[ConfigItem<bool>("UiHiddenSetupAbout", false, ConfigSource.Local)] public partial bool SetupAbout { get; set; } // 修正名称
[ConfigItem<bool>("UiHiddenSetupFeedback", false, ConfigSource.Local)] public partial bool SetupFeedback { get; set; } // 修正名称
[ConfigItem<bool>("UiHiddenSetupLog", false, ConfigSource.Local)] public partial bool SetupLog { get; set; } // 修正名称
// 子页面 工具
[ConfigItem<bool>("UiHiddenToolsGameLink", false, ConfigSource.Local)] public partial bool ToolsGameLink { get; set; } // 新增
[ConfigItem<bool>("UiHiddenToolsTest", false, ConfigSource.Local)] public partial bool ToolsTest { get; set; } // 新增
// 子页面 实例设置
[ConfigItem<bool>("UiHiddenVersionEdit", false, ConfigSource.Local)] public partial bool InstanceEdit { get; set; }
[ConfigItem<bool>("UiHiddenVersionExport", false, ConfigSource.Local)] public partial bool InstanceExport { get; set; }
[ConfigItem<bool>("UiHiddenVersionSave", false, ConfigSource.Local)] public partial bool InstanceSave { get; set; }
[ConfigItem<bool>("UiHiddenVersionScreenshot", false, ConfigSource.Local)] public partial bool InstanceScreenshot { get; set; }
[ConfigItem<bool>("UiHiddenVersionMod", false, ConfigSource.Local)] public partial bool InstanceMod { get; set; }
[ConfigItem<bool>("UiHiddenVersionResourcePack", false, ConfigSource.Local)] public partial bool InstanceResourcePack { get; set; }
[ConfigItem<bool>("UiHiddenVersionShader", false, ConfigSource.Local)] public partial bool InstanceShader { get; set; }
[ConfigItem<bool>("UiHiddenVersionSchematic", false, ConfigSource.Local)] public partial bool InstanceSchematic { get; set; }
[ConfigItem<bool>("UiHiddenVersionServer", false, ConfigSource.Local)] public partial bool InstanceServer { get; set; }
// 特定功能
[ConfigItem<bool>("UiHiddenFunctionSelect", false, ConfigSource.Local)] public partial bool FunctionSelect { get; set; }
[ConfigItem<bool>("UiHiddenFunctionModUpdate", false, ConfigSource.Local)] public partial bool FunctionModUpdate { get; set; }
[ConfigItem<bool>("UiHiddenFunctionHidden", false, ConfigSource.Local)] public partial bool FunctionHidden { get; set; }
}
}
/// <summary>
/// 启动配置。
/// </summary>
[ConfigGroup("Launch")] partial class LaunchConfigGroup
{
/// <summary>
/// 内存分配模式。
/// </summary>
[ConfigItem<int>("LaunchRamType", 0, ConfigSource.Local)] public partial int MemoryAllocationMode { get; set; }
/// <summary>
/// 自定义内存分配大小。
/// </summary>
[ConfigItem<int>("LaunchRamCustom", 15, ConfigSource.Local)] public partial int CustomMemorySize { get; set; }
/// <summary>
/// 是否固定堆大小:启用后额外追加 -Xms 并使其等于 -Xmx,隐式禁用内存归还以降低延迟抖动、利于 ZGC。见 #3282。
/// </summary>
[ConfigItem<bool>("LaunchAdvanceLockMemory", false, ConfigSource.Local)] public partial bool LockMemory { get; set; }
/// <summary>
/// 优先 IP 协议栈。
/// </summary>
[ConfigItem<JvmPreferredIpStack>("LaunchPreferredIpStack", JvmPreferredIpStack.Default)] public partial JvmPreferredIpStack PreferredIpStack { get; set; }
/// <summary>
/// 附加 JVM 参数。
/// </summary>
[ConfigItem<string>("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; }
/// <summary>
/// 附加游戏参数。
/// </summary>
[ConfigItem<string>("LaunchAdvanceGame", "", ConfigSource.Local)] public partial string GameArgs { get; set; }
/// <summary>
/// 预启动指令。
/// </summary>
[ConfigItem<string>("LaunchAdvanceRun", "", ConfigSource.Local)] public partial string PreLaunchCommand { get; set; }
/// <summary>
/// 是否等待预启动指令完成。
/// </summary>
[ConfigItem<bool>("LaunchAdvanceRunWait", true, ConfigSource.Local)] public partial bool PreLaunchCommandWait { get; set; }
/// <summary>
/// 禁用 Java Launch Wrapper。
/// </summary>
[ConfigItem<bool>("LaunchAdvanceDisableJLW", true, ConfigSource.Local)] public partial bool DisableJlw { get; set; }
/// <summary>
/// 禁用 LegacyFix
/// </summary>
[ConfigItem<bool>("LaunchAdvanceDisableLF", false, ConfigSource.Local)] public partial bool DisableLF { get; set; }
/// <summary>
/// 强制使用高性能显卡。
/// </summary>
[ConfigItem<bool>("LaunchAdvanceGraphicCard", true)] public partial bool SetGpuPreference { get; set; }
/// <summary>
/// 使用 java 而不是 javaw。
/// </summary>
[ConfigItem<bool>("LaunchAdvanceNoJavaw", false)] public partial bool NoJavaw { get; set; }
/// <summary>
/// 禁用 LWJGL Unsafe Agent。
/// </summary>
[ConfigItem<bool>("LaunchAdvanceDisableLwjglUnsafeAgent", false)] public partial bool DisableLwjglUnsafeAgent { get; set; }
/// <summary>
/// 禁用自动崩溃分析。
/// </summary>
[ConfigItem<bool>("LaunchAdvanceDisableCrashAnalysis", false, ConfigSource.Local)] public partial bool DisableCrashAnalysis { get; set; }
/// <summary>
/// 渲染器。
/// </summary>
[ConfigItem<int>("LaunchAdvanceRenderer", 0 ,ConfigSource.Local)] public partial int Renderer { get; set; }
/// <summary>
/// 游戏窗口标题。
/// </summary>
[ConfigItem<string>("LaunchArgumentTitle", "", ConfigSource.Local)] public partial string Title { get; set; }
/// <summary>
/// 自定义左下角版本信息。
/// </summary>
[ConfigItem<string>("LaunchArgumentInfo", "PCLCE", ConfigSource.Local)] public partial string TypeInfo { get; set; }
/// <summary>
/// 选择的默认 Java 实例。
/// </summary>
[ConfigItem<string>("LaunchArgumentJavaSelect", "")] public partial string SelectedJava { get; set; }
/// <summary>
/// 版本隔离 V2。
/// </summary>
[ConfigItem<int>("LaunchArgumentIndieV2", 4, ConfigSource.Local)] public partial int IndieSolutionV2 { get; set; }
/// <summary>
/// 游戏启动后启动器可见性。
/// </summary>
[ConfigItem<LauncherVisibility>("LaunchArgumentVisible", LauncherVisibility.DoNothing)] public partial LauncherVisibility LauncherVisibility { get; set; }
/// <summary>
/// 游戏进程优先级。
/// </summary>
[ConfigItem<GameProcessPriority>("LaunchArgumentPriority", GameProcessPriority.Normal)] public partial GameProcessPriority ProcessPriority { get; set; }
/// <summary>
/// 游戏窗口宽度。
/// </summary>
[ConfigItem<int>("LaunchArgumentWindowWidth", 854, ConfigSource.Local)] public partial int GameWindowWidth { get; set; }
/// <summary>
/// 游戏窗口高度。
/// </summary>
[ConfigItem<int>("LaunchArgumentWindowHeight", 480, ConfigSource.Local)] public partial int GameWindowHeight { get; set; }
/// <summary>
/// 游戏窗口模式 (正常/最大化/全屏)。
/// </summary>
[ConfigItem<GameWindowSizeMode>("LaunchArgumentWindowType", GameWindowSizeMode.Default, ConfigSource.Local)] public partial GameWindowSizeMode GameWindowMode { get; set; }
/// <summary>
/// 正版登录方式。
/// </summary>
[ConfigItem<int>("LoginMsAuthType", 1)] public partial int LoginMsAuthType { get; set; }
}
/// <summary>
/// 实例独立配置。<br/>
/// 懒得写注释了,自己理解吧。
/// </summary>
[ConfigGroup("Instance", ConfigSource.GameInstance)] partial class InstanceConfigGroup
{
[ConfigItem<string>("VersionAdvanceJvm", "")] public partial ArgConfig<string> JvmArgs { get; }
[ConfigItem<string>("VersionAdvanceGame", "")] public partial ArgConfig<string> GameArgs { get; }
[ConfigItem<int>("VersionAdvanceRenderer", 0)] public partial ArgConfig<int> Renderer { get; }
[ConfigItem<int>("VersionAdvanceAssets", 0)] public partial ArgConfig<int> AssetVerifySolutionV1 { get; }
[ConfigItem<bool>("VersionAdvanceAssetsV2", false)] public partial ArgConfig<bool> DisableAssetVerifyV2 { get; }
[ConfigItem<bool>("VersionAdvanceJava", false)] public partial ArgConfig<bool> IgnoreJavaCompatibility { get; }
[ConfigItem<bool>("VersionAdvanceDisableJlw", false)] public partial ArgConfig<bool> DisableJlwObsolete { get; }
[ConfigItem<string>("VersionAdvanceRun", "")] public partial ArgConfig<string> PreLaunchCommand { get; }
[ConfigItem<string>("VersionAdvanceClasspathHead", "")] public partial ArgConfig<string> ClasspathHead { get; }
[ConfigItem<bool>("VersionAdvanceRunWait", true)] public partial ArgConfig<bool> PreLaunchCommandWait { get; }
[ConfigItem<bool>("VersionAdvanceDisableJLW", false)] public partial ArgConfig<bool> DisableJlw { get; }
[ConfigItem<bool>("VersionAdvanceDisableLwjglUnsafeAgent", false)] public partial ArgConfig<bool> DisableLwjglUnsafeAgent { get; }
[ConfigItem<bool>("VersionAdvanceUseProxyV2", false)] public partial ArgConfig<bool> UseProxy { get; }
[ConfigItem<bool>("VersionAdvanceDisableLF", false)] public partial ArgConfig<bool> DisableLF { get; }
[ConfigItem<bool>("VersionUseDebugLog4j2Config", false)] public partial ArgConfig<bool> UseDebugLof4j2Config { get; }
[ConfigItem<int>("VersionRamType", 2)] public partial ArgConfig<int> MemorySolution { get; }
[ConfigItem<int>("VersionRamCustom", 15)] public partial ArgConfig<int> CustomMemorySize { get; }
[ConfigItem<string>("VersionArgumentTitle", "")] public partial ArgConfig<string> Title { get; }
[ConfigItem<bool>("VersionArgumentTitleEmpty", false)] public partial ArgConfig<bool> UseGlobalTitle { get; }
[ConfigItem<string>("VersionArgumentInfo", "")] public partial ArgConfig<string> TypeInfo { get; }
[ConfigItem<int>("VersionArgumentIndie", -1)] public partial ArgConfig<int> IndieV1 { get; }
[ConfigItem<bool>("VersionArgumentIndieV2", false)] public partial ArgConfig<bool> IndieV2 { get; }
[ConfigItem<string>("VersionArgumentJavaSelect", "使用全局设置")] public partial ArgConfig<string> SelectedJava { get; }
[ConfigItem<string>("VersionServerEnter", "")] public partial ArgConfig<string> ServerToEnter { get; }
}
/// <summary>
/// 实例独立配置的认证部分
/// </summary>
[ConfigGroup("InstanceAuth", ConfigSource.GameInstance)] partial class InstanceAuthConfigGroup
{
[ConfigItem<int>("VersionServerLoginRequire", 0)] public partial ArgConfig<int> LoginRequirementSolution { get; }
[ConfigItem<string>("VersionServerAuthRegister", "")] public partial ArgConfig<string> AuthRegisterAddress { get; }
[ConfigItem<string>("VersionServerAuthName", "")] public partial ArgConfig<string> AuthServerDisplayName { get; }
[ConfigItem<string>("VersionServerAuthServer", "")] public partial ArgConfig<string> AuthServerAddress { get; }
[ConfigItem<bool>("VersionServerLoginLock", false)] public partial ArgConfig<bool> AuthLocked { get; }
}
}
+116
View File
@@ -0,0 +1,116 @@
namespace PCL.Core.App;
/// <summary>
/// 联机协议偏好
/// </summary>
public enum LinkProtocolPreference
{
Tcp,
Udp
}
/// <summary>
/// 主题模式(亮/暗/系统)
/// </summary>
public enum ColorMode
{
Light = 0,
Dark = 1,
System = 2
}
/// <summary>
/// 配色主题
/// </summary>
public enum ColorTheme
{
SkyBlue = 0,
CatBlue = 1,
DeathBlue = 2,
HmclBlue = 3
}
/// <summary>
/// 更新通道
/// </summary>
public enum UpdateChannel
{
Release = 0,
Beta = 1,
Dev = 2
}
/// <summary>
/// 游戏窗口大小模式
/// </summary>
public enum GameWindowSizeMode
{
Fullscreen = 0,
Default = 1,
Launcher = 2,
Custom = 3,
Maximized = 4
}
/// <summary>
/// 游戏进程优先级
/// </summary>
public enum GameProcessPriority
{
AboveNormal = 0,
Normal = 1,
BelowNormal = 2,
High = 3,
RealTime = 4
}
/// <summary>
/// 游戏启动后启动器可见性
/// </summary>
public enum LauncherVisibility
{
ExitImmediately = 0,
ObsoleteCaseDoNotUse = 1,
HideAndExit = 2,
HideAndReopen = 3,
MinimizeAndReopen = 4,
DoNothing = 5
}
/// <summary>
/// JVM 优先 IP 栈类型
/// </summary>
public enum JvmPreferredIpStack
{
PreferV4 = 0,
Default = 1,
PreferV6 = 2
}
/// <summary>
/// 联机中继行为
/// </summary>
public enum LinkRelayBehavior
{
Default = 0,
ForceRelay = 1
}
/// <summary>
/// 启动器更新行为
/// </summary>
public enum LauncherAutoUpdateBehavior
{
DownloadAndInstall = 0,
DownloadAndAnnounce = 1,
AnnounceOnly = 2,
Disable = 3
}
public enum LauncherTitleType
{
None = 0,
Default = 1,
Text = 2,
Image = 3
}
@@ -0,0 +1,13 @@
using System;
using PCL.Core.Utils;
namespace PCL.Core.App.Configuration;
public class ArgConfig<TValue> : ParameterizedProperty<object, TValue>
{
public ArgConfig(Func<object?, TValue> getter, Action<object?, TValue> setter)
{
GetValue = getter;
SetValue = setter;
}
}
@@ -0,0 +1,40 @@
using System;
namespace PCL.Core.App.Configuration;
#pragma warning disable CS9113 // Parameter is unread.
/// <summary>
/// 标记一个 partial 属性,以添加对应配置项并自动生成访问器。
/// </summary>
/// <param name="key">配置键</param>
/// <param name="defaultValue">默认值</param>
/// <param name="source">配置来源</param>
/// <typeparam name="TValue">值类型</typeparam>
[AttributeUsage(AttributeTargets.Property)]
public sealed class ConfigItemAttribute<TValue>(string key, TValue? defaultValue, ConfigSource source = default) : Attribute;
/// <summary>
/// 标记一个 partial 属性,以添加对应配置项并自动生成访问器。
/// <p>注意:默认值将调用指定类型的无参构造器来获取,以解决 C# attribute 在 2025 年仍然不支持隔壁 JVM 在 2015
/// 年就支持的极其先进的自定义类型参数的问题。因此,值的类型必须有公开的无参构造器,否则运行时将会抛出异常。</p>
/// </summary>
/// <param name="key">配置键</param>
/// <param name="source">配置来源</param>
/// <typeparam name="TValue">值类型</typeparam>
[AttributeUsage(AttributeTargets.Property)]
public sealed class AnyConfigItemAttribute<TValue>(string key, ConfigSource source = default) : Attribute;
/// <summary>
/// 标记一个 partial 类为配置组,以自动实现 <see cref="IConfigScope"/> 并生成对应的作用域检查方法。
/// </summary>
/// <param name="name">组名,需符合 C# 标识符规范</param>
/// <param name="source">组级别的默认配置来源</param>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class ConfigGroupAttribute(string name, ConfigSource source = default) : Attribute;
/// <summary>
/// 标记一个类型为 <see cref="ConfigEventRegistry"/> 的 public static 属性,以注册配置项事件。
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public sealed class RegisterConfigEventAttribute : Attribute;
@@ -0,0 +1,60 @@
using System;
namespace PCL.Core.App.Configuration;
/// <summary>
/// 配置项事件。
/// </summary>
[Flags]
public enum ConfigEvent
{
/// <summary>
/// 初始化,当且仅当程序初始化时调用一次。
/// </summary>
Init = 0b00001,
/// <summary>
/// 获取。
/// </summary>
Get = 0b00010,
/// <summary>
/// 设置值。
/// </summary>
Set = 0b00100,
/// <summary>
/// 重置值。
/// </summary>
Reset = 0b01000,
/// <summary>
/// 检查是否为默认值。
/// </summary>
CheckDefault = 0b10000,
/// <summary>
/// 保留备用。
/// </summary>
None = 0,
/// <summary>
/// 所有读取操作。
/// </summary>
Read = Get | CheckDefault,
/// <summary>
/// 所有更新操作。
/// </summary>
Update = Set | Reset,
/// <summary>
/// 所有改变操作。
/// </summary>
Changed = Init | Update,
/// <summary>
/// 所有操作。没事别监听这个,一点风吹草动都会触发它。
/// </summary>
All = Read | Changed
}
@@ -0,0 +1,32 @@
namespace PCL.Core.App.Configuration;
/// <summary>
/// 配置项事件参数。
/// </summary>
/// <param name="Item">配置项。</param>
/// <param name="Event">触发事件。</param>
/// <param name="Argument">上下文参数。</param>
/// <param name="OldValue">旧值。</param>
/// <param name="NewValue">新值。</param>
public record ConfigEventArgs(
ConfigItem Item,
ConfigEvent Event,
object? Argument,
object? OldValue,
object? NewValue
) {
/// <summary>
/// 设置一个新值代替原来的值进行对应操作,只在 <see cref="ConfigObserver.IsPreview"/> 为 <c>true</c> 时有效。
/// </summary>
public object? NewValueReplacement { get; set; } = null;
/// <summary>
/// 是否取消事件,只在 <see cref="ConfigObserver.IsPreview"/> 为 <c>true</c> 时有效。
/// </summary>
public bool Cancelled { get; set; } = false;
/// <summary>
/// 配置当前的值。
/// </summary>
public object? Value => NewValueReplacement ?? NewValue;
}
@@ -0,0 +1,7 @@
namespace PCL.Core.App.Configuration;
/// <summary>
/// 配置项监听委托。
/// </summary>
/// <param name="e">事件参数</param>
public delegate void ConfigEventHandler(ConfigEventArgs e);
@@ -0,0 +1,24 @@
using System.Collections.Generic;
namespace PCL.Core.App.Configuration;
public class ConfigEventRegistry(
IEnumerable<IConfigScope> scope,
ConfigEventHandler handler,
ConfigEvent trigger = ConfigEvent.Changed,
bool isPreview = false)
{
public IEnumerable<IConfigScope> Scopes => scope;
public ConfigEvent Trigger => trigger;
public ConfigEventHandler Handler => handler;
public bool IsPreview => isPreview;
public ConfigEventRegistry(
IConfigScope scope,
ConfigEventHandler handler,
ConfigEvent trigger = ConfigEvent.Changed,
bool isPreview = false
) : this([scope], handler, trigger, isPreview) { }
public ConfigObserver ToObserver() => new(trigger, handler, isPreview);
}
@@ -0,0 +1,369 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using PCL.Core.Utils.Exts;
namespace PCL.Core.App.Configuration;
/// <summary>
/// 配置项。
/// </summary>
/// <typeparam name="TValue">值类型</typeparam>
public class ConfigItem<TValue>(
string key,
Func<TValue> defaultValue,
ConfigSource source
) : IConfigScope, ConfigItem
{
public string Key { get; } = key;
public ConfigSource Source { get; } = source;
public Type Type => typeof(TValue);
private Func<TValue>? _defaultValueConstructor = defaultValue;
private TValue? _defaultValue;
private bool _defaultValueHasSet = false;
#region
private TValue _GetDefaultValue()
{
if (_defaultValueHasSet) return _defaultValue!;
_defaultValue = _defaultValueConstructor!();
_defaultValueHasSet = true;
_defaultValueConstructor = null;
return _defaultValue;
}
/// <summary>
/// 默认值。
/// </summary>
public TValue DefaultValue => _GetDefaultValue();
public object DefaultValueNoType => DefaultValue ?? default!;
#endregion
public ConfigItem(string key, TValue defaultValue, ConfigSource source)
: this(key, () => defaultValue, source) { }
public IEnumerable<string> CheckScope(IReadOnlySet<string> keys) => keys.Contains(Key) ? [Key] : [];
#region
private IConfigProvider _Provider { get => field ??= ConfigService.GetProvider(Source); } = null!;
private ConfigValueCache<TValue> _valueCache = new();
/// <summary>
/// 指定是否启用缓存。<br/>
/// <b>NOTE</b>: 禁用缓存将造成一些功能(如自动监听内容更改)不按预期工作,请仅在真正需要的时候禁用。
/// </summary>
public bool EnableCache
{
get;
set
{
if (!field) _valueCache.InvalidateAll();
field = value;
}
} = true;
/// <summary>
/// 处理看起来是新的值,并返回是否真的是新的。<br/>
/// 只有启用缓存时该方法才会生效,未启用缓存将始终直接返回 <see langword="true"/>。
/// </summary>
private bool _ProcessNewCache(TValue newCache, object? argument, bool force = false)
{
if (!EnableCache) return true;
if (!force)
{
// 判断是否是新值
var existsOld = _valueCache.TryRead(out var oldCache, argument);
if (existsOld && EqualityComparer<TValue>.Default.Equals(oldCache, newCache)) return false;
}
// 对新缓存值执行准备工作
if (newCache is INotifyPropertyChanged reactive)
reactive.PropertyChanged += (_, _) => OnContentChanged();
else if (newCache is INotifyCollectionChanged reactiveCollection)
reactiveCollection.CollectionChanged += (_, _) => OnContentChanged();
// 写入缓存
_valueCache.Write(newCache, argument);
return true;
void OnContentChanged() => SetValue(newCache, argument, bypassCache: true);
}
/// <summary>
/// 获取配置值。
/// </summary>
/// <param name="argument">上下文参数</param>
/// <returns>已设置的配置值或默认值</returns>
public TValue GetValue(object? argument = null)
{
TValue? value = default; // 这个初始化是多余的,但是煞笔巨硬不初始化会报错
var exists = EnableCache && _valueCache.TryRead(out value, argument);
var newValue = false;
if (!exists)
{
newValue = true;
exists = _Provider.GetValue(Key, out value, argument);
}
var e = _TriggerEvent(ConfigEvent.Get, argument, value, true);
if (e is not null)
{
if (e.Cancelled) return DefaultValue;
if (e.NewValueReplacement is not null) return (TValue)e.NewValueReplacement;
}
if (!exists) value = DefaultValue;
if (newValue) _ProcessNewCache(value!, argument);
return value!;
}
public object GetValueNoType(object? argument = null)
{
return GetValue(argument) ?? default!;
}
/// <summary>
/// 设置配置值。
/// </summary>
/// <param name="value">用于设置的值</param>
/// <param name="argument">上下文参数</param>
/// <param name="forceNewValue">强制将传入的值视为新值,不检查缓存,仅在 <see cref="EnableCache"/> 为 <see langword="true"/> 时生效</param>
/// <param name="bypassCache">跳过缓存检查和写入,相当于对本次操作临时将 <see cref="EnableCache"/> 设为 <see langword="false"/></param>
/// <returns>是否成功设置值,若成功则为 <c>true</c></returns>
public bool SetValue(TValue value, object? argument = null, bool forceNewValue = false, bool bypassCache = false)
{
var e = _TriggerEvent(ConfigEvent.Set, argument, value, isPreview: true);
if (e is not null)
{
if (e.Cancelled) return false;
if (e.NewValueReplacement is not null) value = (TValue)e.NewValueReplacement;
}
if (bypassCache || _ProcessNewCache(value, argument, forceNewValue))
_Provider.SetValue(Key, value, argument);
_TriggerEvent(ConfigEvent.Set, argument, value, e: e, isPreview: false);
return true;
}
public bool SetValueNoType(object value, object? argument = null)
{
try
{
return SetValue((TValue)value, argument);
}
catch (InvalidCastException)
{
// 兼容龙猫妙妙小代码直接传入 string 值的行为
if (value is string v) return SetValue(v.Convert<TValue>()!, argument);
var msg = $"Value convert failed (required: {Type.FullName}, provided: {value.GetType().FullName})";
throw new InvalidCastException(msg);
}
}
public bool SetDefaultValue(object? argument = null, bool? forceNewValue = null)
{
return SetValue(DefaultValue, argument, forceNewValue ?? IsDefault(argument));
}
public bool Reset(object? argument = null)
{
var e = _TriggerEvent(ConfigEvent.Reset, argument, null, isPreview: true);
if (e is { Cancelled: true }) return false;
_Provider.Delete(Key, argument);
if (EnableCache) _valueCache.Invalidate(argument);
_TriggerEvent(ConfigEvent.Reset, argument, DefaultValueNoType, isPreview: false);
return true;
}
public bool IsDefault(object? argument = null)
{
var result = !_Provider.Exists(Key, argument);
var e = _TriggerEvent(ConfigEvent.CheckDefault, argument, result);
if (e is { NewValueReplacement: not null }) result = (bool)e.NewValueReplacement;
return result;
}
#endregion
#region
private readonly HashSet<ConfigObserver> _observers = [];
private readonly HashSet<ConfigObserver> _previewObservers = [];
public void Observe(ConfigObserver observer)
{
if (observer.IsPreview) _previewObservers.Add(observer);
else _observers.Add(observer);
}
public bool Unobserve(ConfigObserver observer)
=> observer.IsPreview ? _previewObservers.Remove(observer) : _observers.Remove(observer);
// 获取值,若未设置则返回 null
private object? _GetValueOrNull(object? argument)
{
var exists = _Provider.GetValue<TValue>(Key, out var value, argument);
return exists ? value : null;
}
public ConfigEventArgs? TriggerEvent(
ConfigEvent trigger, object? argument,
bool bypassOldValue = false, bool fillNewValue = false)
{
return _TriggerEvent(trigger, argument, null, bypassOldValue, fillNewValue);
}
private ConfigEventArgs? _TriggerEvent(
ConfigEvent trigger, object? argument, object? newValue,
bool bypassOldValue = false, bool fillNewValue = false,
ConfigEventArgs? e = null, bool? isPreview = null)
{
var replaceNewValue = false;
foreach (var observer in (
from observer in (isPreview is { } p ? (p ? _previewObservers : _observers) : _previewObservers.Concat(_observers))
let logic = (int)observer.Event & (int)trigger
where logic > 0
select observer
)) {
if (e is null)
{
if (isPreview == false && !bypassOldValue) bypassOldValue = true;
var currentValue = (fillNewValue || !bypassOldValue) ? _GetValueOrNull(argument) : null;
if (newValue is null && fillNewValue) newValue = currentValue ?? DefaultValue;
e = new ConfigEventArgs(this, trigger, argument, bypassOldValue ? null : currentValue, newValue);
}
observer.Handler(e);
// 对 preview 的特殊处理
if (observer.IsPreview)
{
if (e.NewValueReplacement is not null) replaceNewValue = true; // 记录替换操作
if (e.Cancelled) return e;
}
// 防止非 preview 事件传递替换值
else if (!replaceNewValue && e.NewValueReplacement is not null) e.NewValueReplacement = null;
}
// 防止非 preview 事件传递取消状态
if (e is { Cancelled: true }) e.Cancelled = false;
return e;
}
#endregion
}
/// <summary>
/// <see cref="ConfigItem{TValue}"/> 的非泛型方法抽象层,用于手动解决巨硬
/// 2025 年仍未支持的极其先进的隐式去泛型化。
/// </summary>
// ReSharper disable once InconsistentNaming
public interface ConfigItem
{
/// <summary>
/// 配置键。
/// </summary>
public string Key { get; }
/// <summary>
/// 配置来源。
/// </summary>
public ConfigSource Source { get; }
/// <summary>
/// 配置的 CLR 类型。
/// </summary>
public Type Type { get; }
/// <summary>
/// 传入事件观察器以观察事件。
/// </summary>
public void Observe(ConfigObserver observer);
/// <summary>
/// 取消观察事件。
/// </summary>
public bool Unobserve(ConfigObserver observer);
/// <summary>
/// 触发配置项事件。
/// </summary>
/// <param name="trigger">触发事件</param>
/// <param name="argument">上下文参数</param>
/// <param name="bypassOldValue">若为 <c>true</c> 则向事件参数的旧值传递 <c>null</c>,否则传递当前值</param>
/// <param name="fillNewValue">若为 <c>true</c>,当新值为 <c>null</c> 时将传递当前值或默认值</param>
/// <returns></returns>
public ConfigEventArgs? TriggerEvent(
ConfigEvent trigger,
object? argument,
bool bypassOldValue = false,
bool fillNewValue = false
);
/// <summary>
/// 传入事件类型与处理委托以观察事件。
/// </summary>
public ConfigObserver Observe(ConfigEvent trigger, ConfigEventHandler handler, bool isPreview = false)
{
var observer = new ConfigObserver(trigger, handler, isPreview);
Observe(observer);
return observer;
}
/// <summary>
/// 传统的用于兼容的值改变事件。<br/>
/// 请尽可能避免使用,而是使用 <see cref="RegisterConfigEventAttribute"/>
/// 来声明事件观察,或使用 <see cref="Observe(ConfigObserver)"/> 和
/// <see cref="Unobserve(ConfigObserver)"/> 来灵活管理事件。
/// </summary>
public event ConfigEventHandler Changed
{
add => Observe(ConfigEvent.Changed, value);
remove => throw new NotSupportedException("Please use Observe() and Unobserve() to access advanced event management");
}
/// <summary>
/// 重置配置值,使其变为未设置状态。
/// </summary>
/// <param name="argument">上下文参数</param>
/// <returns>是否成功重置值,若成功则为 <c>true</c></returns>
public bool Reset(object? argument = null);
/// <summary>
/// 检查配置值是否为默认值 (未设置状态)
/// </summary>
/// <param name="argument">上下文参数</param>
public bool IsDefault(object? argument = null);
/// <summary>
/// 将配置项的值设置为默认值,设置后 <see cref="IsDefault"/> 将返回 <c>false</c>。
/// </summary>
/// <param name="argument">上下文参数</param>
/// <param name="forceNewValue">强制视为新值,不检查缓存,仅在 <see cref="EnableCache"/> 为 <see langword="true"/> 时生效</param>
/// <returns>是否成功设置值,若成功则为 <c>true</c></returns>
public bool SetDefaultValue(object? argument = null, bool? forceNewValue = null);
/// <summary>
/// 没有泛型的 <see cref="ConfigItem{T}.GetValue"/>。<br/>
/// 我们都不想给非引用类型装箱,但是龙猫想。
/// </summary>
public object GetValueNoType(object? argument = null);
/// <summary>
/// 没有泛型的 <see cref="ConfigItem{T}.SetValue"/>。<br/>
/// 我们都不想给非引用类型装箱,但是龙猫想。
/// </summary>
public bool SetValueNoType(object value, object? argument = null);
/// <summary>
/// 没有泛型的 <see cref="ConfigItem{T}.DefaultValue"/>。<br/>
/// 我们都不想给非引用类型装箱,但是龙猫想。
/// </summary>
public object DefaultValueNoType { get; }
/// <summary>
/// 是否启用值缓存,默认为 <c>true</c>。设为 <c>false</c> 将清除已存在的缓存。
/// </summary>
public bool EnableCache { get; set; }
}
@@ -0,0 +1,178 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
namespace PCL.Core.App.Configuration;
public delegate void ConfigMigrationHandler(string from, string to);
/// <summary>
/// 配置文件迁移模型与工具。
/// </summary>
public class ConfigMigration
{
/// <summary>
/// 来源路径。
/// </summary>
public required string From { get; init; }
/// <summary>
/// 目标路径。
/// </summary>
public required string To { get; init; }
/// <summary>
/// 优先级 (或称"权重"),数字越大越容易被使用。
/// </summary>
public int Priority { get; init; } = 0;
/// <summary>
/// 迁移实现。
/// </summary>
public required ConfigMigrationHandler OnMigration { get; init; }
/// <summary>
/// 执行配置文件迁移。
/// </summary>
/// <param name="target">最终目标路径</param>
/// <param name="migrations">可用的迁移过程</param>
/// <returns>若找到最简方案并迁移成功,则为 <c>true</c>,否则为 <c>false</c></returns>
public static bool Migrate(string target, IEnumerable<ConfigMigration> migrations)
{
migrations = migrations as ConfigMigration[] ?? migrations.ToArray();
IEnumerable<ConfigMigration>? solution = null;
var found = (
from migration in migrations.Reverse()
let path = migration.From
where File.Exists(path) && _TryFindShortestPath(path, target, migrations, out solution)
select path
).Any();
if (!found) return false;
foreach (var migration in solution!) migration.OnMigration(migration.From, migration.To);
return true;
}
// 寻找最短路径
// Partly generated by gpt-5 (20250904)
// ReSharper disable InvertIf, ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
private static bool _TryFindShortestPath(string start, string end,
IEnumerable<ConfigMigration> paths, [NotNullWhen(true)] out IEnumerable<ConfigMigration>? result)
{
// 起点即终点:最短过程为 0 条边
if (start == end)
{
result = [];
return true;
}
// 构建邻接表(有向图)
var adj = new Dictionary<string, List<ConfigMigration>>(StringComparer.Ordinal);
foreach (var p in paths)
{
if (!adj.TryGetValue(p.From, out var list))
{
list = [];
adj[p.From] = list;
}
list.Add(p);
}
// 第一阶段:BFS 计算从 start 到各点的最短边数 dist
var dist = new Dictionary<string, int>(StringComparer.Ordinal);
var queue = new Queue<string>();
dist[start] = 0;
queue.Enqueue(start);
while (queue.Count > 0)
{
var u = queue.Dequeue();
if (!adj.TryGetValue(u, out var outgoing))
continue;
var du = dist[u];
foreach (var e in outgoing)
{
var v = e.To;
if (!dist.ContainsKey(v))
{
dist[v] = du + 1;
queue.Enqueue(v);
}
}
}
// 不可达
if (!dist.ContainsKey(end))
{
result = null;
return false;
}
// 将节点按深度分层,便于第二阶段的 DP
var nodesByDepth = new Dictionary<int, List<string>>();
foreach (var kv in dist)
{
if (!nodesByDepth.TryGetValue(kv.Value, out var list))
{
list = [];
nodesByDepth[kv.Value] = list;
}
list.Add(kv.Key);
}
// 第二阶段:在所有最短路径子图上,选择累计 Priority 之和最大的路径
var bestPriority = new Dictionary<string, long>(StringComparer.Ordinal); // 累计优先级和
var prevNode = new Dictionary<string, string>(StringComparer.Ordinal); // 重建路径
var prevEdge = new Dictionary<string, ConfigMigration>(StringComparer.Ordinal);
bestPriority[start] = 0;
var maxDepth = dist[end];
for (var d = 0; d < maxDepth; d++)
{
if (!nodesByDepth.TryGetValue(d, out var layer))
continue;
foreach (var u in layer)
{
if (!bestPriority.ContainsKey(u)) continue;
if (!adj.TryGetValue(u, out var outgoing)) continue;
foreach (var e in outgoing)
{
if (!dist.TryGetValue(e.To, out var dv) || dv != d + 1)
continue; // 只考虑保持最短性的边
var candidate = bestPriority[u] + e.Priority;
if (!bestPriority.TryGetValue(e.To, out var cur) || candidate > cur)
{
bestPriority[e.To] = candidate;
prevNode[e.To] = u;
prevEdge[e.To] = e;
}
}
}
}
// 重建从 start 到 end 的路径(边序列)
if (!prevEdge.ContainsKey(end)) { // 理论上不应发生
result = null;
return false;
}
var path = new List<ConfigMigration>();
var curr = end;
while (curr != start)
{
var edge = prevEdge[curr];
path.Add(edge);
curr = prevNode[curr];
}
path.Reverse();
result = path;
return true;
}
// ReSharper restore InvertIf, ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
}
@@ -0,0 +1,13 @@
namespace PCL.Core.App.Configuration;
/// <summary>
/// 配置事件观察器。
/// </summary>
/// <param name="Event">观察的事件。</param>
/// <param name="Handler">事件处理委托。</param>
/// <param name="IsPreview">指定是否预览事件处理,预览事件可覆盖原有值或取消事件处理过程。</param>
public record ConfigObserver(
ConfigEvent Event,
ConfigEventHandler Handler,
bool IsPreview = false
);
@@ -0,0 +1,364 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using PCL.Core.App.Configuration.Storage;
using PCL.Core.App.Localization;
using PCL.Core.App.IoC;
using PCL.Core.Logging;
using PCL.Core.Utils.Exts;
namespace PCL.Core.App.Configuration;
/// <summary>
/// 全局配置服务。
/// </summary>
[LifecycleService(LifecycleState.Loading, Priority = 1919810)]
[LifecycleScope("config", "配置")]
public sealed partial class ConfigService
{
private static readonly Dictionary<string, ConfigItem> _Items = [];
private static readonly HashSet<string> _KeySet = [];
/// <summary>
/// 配置键的集合。
/// </summary>
public static IReadOnlySet<string> KeySet => _KeySet;
/// <summary>
/// 全局配置文件的版本号。
/// </summary>
[ConfigItem<int>("FileVersion", 1)] public static partial int SharedVersion { get; set; }
/// <summary>
/// 本地配置文件的版本号。
/// </summary>
[ConfigItem<int>("LocalFileVersion", 1, ConfigSource.Local)] public static partial int LocalVersion { get; set; }
/// <summary>
/// 全局共享配置文件路径。
/// </summary>
public static string SharedConfigPath { get; } = Path.Combine(Paths.SharedData, "config.v1.json");
/// <summary>
/// 本地配置文件路径。
/// </summary>
public static string LocalConfigPath { get; } = Path.Combine(Paths.Data, "config.v1.yml");
#region Getters & Setters
/// <summary>
/// 尝试获取无泛型的配置项。
/// </summary>
/// <param name="key">配置键</param>
/// <param name="item">返回可观察对象</param>
/// <returns>若配置键存在,则为 <c>true</c>,否则为 <c>false</c></returns>
public static bool TryGetConfigItemNoType(string key, [NotNullWhen(true)] out ConfigItem? item)
=> _Items.TryGetValue(key, out item);
/// <summary>
/// 尝试获取配置项。
/// </summary>
/// <param name="key">配置键</param>
/// <param name="item">返回配置项,若类型不匹配则为 <c>null</c></param>
/// <typeparam name="TValue">配置项的值类型</typeparam>
/// <returns>若配置键存在,则为 <c>true</c>,否则为 <c>false</c></returns>
/// <exception cref="InvalidOperationException">配置项尚未初始化完成</exception>
public static bool TryGetConfigItem<TValue>(string key, out ConfigItem<TValue>? item)
{
if (!_isConfigItemsInitialized) throw new InvalidOperationException("Not initialized");
var result = TryGetConfigItemNoType(key, out var value);
item = result ? (value as ConfigItem<TValue>) : null;
return result;
}
/// <summary>
/// 获取配置项。
/// </summary>
/// <param name="key">配置键</param>
/// <typeparam name="TValue">配置项的值类型</typeparam>
/// <returns>配置项实例</returns>
/// <exception cref="InvalidOperationException">配置项尚未初始化完成</exception>
/// <exception cref="KeyNotFoundException">配置键不存在</exception>
/// <exception cref="InvalidCastException">值类型参数与实际类型不匹配</exception>
public static ConfigItem<TValue> GetConfigItem<TValue>(string key)
{
var result = TryGetConfigItem<TValue>(key, out var item);
if (!result) throw new KeyNotFoundException($"Config key not found: '{key}'");
return item ?? throw new InvalidCastException($"Type of '{key}' is incompatible with {typeof(TValue).FullName}");
}
/// <summary>
/// 按键设置配置值,自动处理类型匹配。若键不存在则静默失败。
/// </summary>
/// <param name="key">配置键</param>
/// <param name="value">配置值</param>
/// <param name="argument">上下文参数(实例路径等)</param>
public static void TrySetValue(string key, object value, object? argument = null)
{
if (!TryGetConfigItemNoType(key, out var item)) return;
if (item.Type.IsEnum && value is not string)
item.SetValueNoType(Enum.ToObject(item.Type, value), argument);
else
item.SetValueNoType(value, argument);
}
/// <summary>
/// 向指定作用域批量注册事件观察器。
/// </summary>
/// <param name="scope"><see cref="IConfigScope"/> 实例</param>
/// <param name="observer">观察器实例</param>
public static void RegisterObserver(IConfigScope scope, ConfigObserver observer)
{
var itemKeys = scope.CheckScope(KeySet);
foreach (var key in itemKeys)
{
var item = _Items[key];
item.Observe(observer);
}
}
#endregion
#region Providers
private static ConfigStorage? _sharedConfigProvider;
private static ConfigStorage? _sharedEncryptedConfigProvider;
private static ConfigStorage? _localConfigProvider;
private static ConfigStorage? _instanceConfigProvider;
/// <summary>
/// 获取配置提供方。
/// </summary>
/// <param name="source">来源定义</param>
/// <returns>提供方实例</returns>
/// <exception cref="InvalidOperationException">配置提供方尚未初始化完成</exception>
/// <exception cref="ArgumentException">来源定义无效</exception>
public static IConfigProvider GetProvider(ConfigSource source)
{
if (!_isProvidersInitialized) throw new InvalidOperationException("Not initialized");
return source switch
{
ConfigSource.Shared => _sharedConfigProvider!,
ConfigSource.SharedEncrypt => _sharedEncryptedConfigProvider!,
ConfigSource.Local => _localConfigProvider!,
ConfigSource.GameInstance => _instanceConfigProvider!,
_ => throw new ArgumentException($"Invalid source: {source}")
};
}
private static void _InitializeProviders()
{
Action[] inits = [
() => // shared config file
{
// try migrate
if (!File.Exists(SharedConfigPath))
{
string[] oldPaths = [
Path.Combine(Paths.OldSharedData, "Config.json"),
Path.Combine(Paths.SharedData, "config.json")
];
_TryMigrate(SharedConfigPath, oldPaths.Select(path =>
new ConfigMigration { From = path, To = SharedConfigPath, OnMigration = SharedJsonMigration }));
}
// load
var fileProvider = new JsonFileProvider(SharedConfigPath);
var storage = new FileConfigStorage(fileProvider);
_sharedConfigProvider = storage;
_sharedEncryptedConfigProvider = new EncryptedFileConfigStorage(storage);
},
() => // local config file
{
// try migrate
if (!File.Exists(LocalConfigPath)) _TryMigrate(LocalConfigPath, [
new ConfigMigration
{
From = Path.Combine(Paths.Data, "setup.ini"),
To = LocalConfigPath,
OnMigration = CatIniMigration
}
]);
// load
var fileProvider = new YamlFileProvider(LocalConfigPath);
_localConfigProvider = new FileConfigStorage(fileProvider);
},
() => // instance config file(s)
{
_instanceConfigProvider = new DynamicCacheConfigStorage
{
StorageFactory = argument =>
{
ArgumentNullException.ThrowIfNull(argument);
var dir = Path.GetFullPath(argument.ToString()!);
var configPath = Path.Combine(dir, "PCL", "config.v1.yml");
if (!File.Exists(dir)) _TryMigrate(dir, [
new ConfigMigration
{
From = Path.Combine(dir, "PCL", "setup.ini"),
To = configPath,
OnMigration = CatIniMigration
}
]);
var fileProvider = new YamlFileProvider(configPath);
var storage = new FileConfigStorage(fileProvider);
return storage;
}
};
}
];
try { Task.WaitAll(inits.Select(Task.Run).ToArray()); }
catch (AggregateException ex) { throw ex.GetBaseException(); }
return;
void SharedJsonMigration(string from, string to)
{
File.Copy(from, to);
}
void CatIniMigration(string from, string to)
{
var lines = File.ReadAllLines(from);
var yamlProvider = new YamlFileProvider(to);
foreach (var line in lines)
{
if (line.IsNullOrWhiteSpace()) continue;
var kv = line.Split(':', 2);
if (kv.Length != 2) continue;
yamlProvider.Set(kv[0], kv[1]);
}
yamlProvider.Sync();
}
}
private static void _TryMigrate(string target, IEnumerable<ConfigMigration> migrations)
{
Context.Info($"Try migrating config: {target}");
try
{
var result = ConfigMigration.Migrate(target, migrations);
if (!result) Context.Info("No migration solution available");
}
catch (Exception ex)
{
Context.Warn("Migration failed", ex);
}
}
#endregion
#region Lifecycle & Initialization
/// <summary>
/// 配置服务是否已加载完成。未加载完成时,调用与配置项相关的方法可能会抛出 <see cref="InvalidOperationException"/>。
/// </summary>
public static bool IsInitialized { get; private set; } = false;
private static bool _isProvidersInitialized = false;
private static bool _isConfigItemsInitialized = false;
[LifecycleStart]
private static void _Start()
{
if (IsInitialized) return;
#if TRACE
var timer = new Stopwatch();
timer.Start();
#endif
Context.Info("Config initialization started");
try
{
Context.Trace("Initializing config items...");
_InitializeConfigItems();
Context.Debug($"Finished initialize {_Items.Count} item(s)");
_isConfigItemsInitialized = true;
Context.Trace("Initializing providers...");
_InitializeProviders();
_isProvidersInitialized = true;
Context.Trace("Initializing observers...");
_InitializeObservers();
Context.Info("Invoking init events...");
foreach (var (_, item) in _Items)
{
item.TriggerEvent(ConfigEvent.Init, null, true, true);
}
IsInitialized = true;
}
catch (Exception ex)
{
var currentSection = _isConfigItemsInitialized ? "OBSERVER" :
_isProvidersInitialized ? "CONFIG_ITEM" : "PROVIDER";
string msg;
#if DEBUG
msg = Lang.Text("Config.Error.LoadFailed.DebugMessage", currentSection);
#else
if (ex is ConfigFileInitException e)
{
var filePath = e.Path;
var backupPath = e.Path + ".failbackup";
var bakPath = e.Path + ".bak";
File.Move(filePath, backupPath, true);
if (File.Exists(bakPath)) File.Copy(bakPath, filePath, true);
msg = Lang.Text(
"Config.Error.InvalidFormat.RecoveryMessage",
currentSection,
filePath,
backupPath);
}
else
{
msg = Lang.Text("Config.Error.LoadFailed.Message", currentSection);
}
#endif
Context.Fatal(msg, ex);
}
#if TRACE
timer.Stop();
Context.Info($"Config initialization finished in {timer.ElapsedMilliseconds} ms");
#endif
}
[LifecycleStop]
private static void _Stop()
{
// 检测是否初始化出错
if (Lifecycle.GetServiceLastException(Service.Identifier) is { } ex)
{
Context.Fatal(Lang.Text("Config.Error.LoadFailed.Title"), ex);
return;
}
Context.Info("Saving config...");
// 停止物流中心并释放资源
_sharedConfigProvider?.Stop();
_localConfigProvider?.Stop();
_instanceConfigProvider?.Stop();
}
[RegisterConfigEvent]
public static ConfigEventRegistry SharedVersionInit => new(
SharedVersionConfig,
trigger: ConfigEvent.Init,
handler: e => _UpdateConfigVersion(SharedVersionConfig, "全局", (int)e.NewValue!)
);
[RegisterConfigEvent]
public static ConfigEventRegistry LocalVersionInit => new(
scope: LocalVersionConfig,
trigger: ConfigEvent.Init,
handler: e => _UpdateConfigVersion(LocalVersionConfig, "本地", (int)e.NewValue!)
);
private static void _UpdateConfigVersion(ConfigItem<int> versionConfig, string name, int fileVersion)
{
var targetVersion = versionConfig.DefaultValue;
var isUnset = versionConfig.IsDefault();
LogWrapper.Info($"{name}配置: 文件版本 {(isUnset ? "UNSET" : fileVersion)}, 目标版本 {targetVersion}");
if (isUnset || targetVersion != fileVersion) versionConfig.SetValue(targetVersion);
}
#endregion
}
@@ -0,0 +1,27 @@
namespace PCL.Core.App.Configuration;
/// <summary>
/// 配置来源。
/// </summary>
public enum ConfigSource
{
/// <summary>
/// 全局共享配置。
/// </summary>
Shared,
/// <summary>
/// 加密的全局共享配置。
/// </summary>
SharedEncrypt,
/// <summary>
/// 本地配置。
/// </summary>
Local,
/// <summary>
/// 游戏实例特定配置。
/// </summary>
GameInstance
}
@@ -0,0 +1,86 @@
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
namespace PCL.Core.App.Configuration;
public struct ConfigValueCache<TValue>()
{
private TValue? _cachedValue;
private bool _hasCachedValue = false;
private readonly ConcurrentDictionary<object, TValue> _cacheWithContext = [];
/// <summary>
/// 检查指定上下文参数的缓存是否存在。
/// </summary>
/// <param name="argument">上下文参数</param>
public bool Exists(object? argument = null)
{
return (argument is null) ? _hasCachedValue : _cacheWithContext.ContainsKey(argument);
}
/// <summary>
/// 尝试读取缓存值。
/// </summary>
/// <param name="value">若已缓存则为输出值,否则为默认值</param>
/// <param name="argument">上下文参数</param>
/// <returns>若已缓存则为 <c>true</c>,否则为 <c>false</c></returns>
public bool TryRead(
[NotNullWhen(true)] out TValue? value,
object? argument = null)
{
bool result;
if (argument is not null) result = _cacheWithContext.TryGetValue(argument, out value);
else
{
if (_hasCachedValue)
{
value = _cachedValue!;
result = true;
}
else
{
value = default;
result = false;
}
}
return result;
}
/// <summary>
/// 写入缓存值。
/// </summary>
/// <param name="value">输入值</param>
/// <param name="argument">上下文参数</param>
public void Write(TValue value, object? argument = null)
{
if (argument is not null)
{
_cacheWithContext[argument] = value;
return;
}
_hasCachedValue = true;
_cachedValue = value;
}
/// <summary>
/// 清除缓存值。<br/>
/// 若缓存存在则清除并返回 <c>true</c>,否则返回 <c>false</c>。
/// </summary>
/// <param name="argument">上下文参数</param>
public bool Invalidate(object? argument)
{
if (argument is not null) return _cacheWithContext.TryRemove(argument, out _);
if (!_hasCachedValue) return false;
_cachedValue = default;
_hasCachedValue = false;
return true;
}
public void InvalidateAll()
{
_cacheWithContext.Clear();
_cachedValue = default;
_hasCachedValue = false;
}
}
@@ -0,0 +1,40 @@
using System.Diagnostics.CodeAnalysis;
namespace PCL.Core.App.Configuration;
public interface IConfigProvider
{
/// <summary>
/// 获取一个值。
/// </summary>
/// <param name="key">键</param>
/// <param name="value">返回值,若不存在则为该类型默认值</param>
/// <param name="argument">上下文参数</param>
/// <typeparam name="T">值的类型</typeparam>
/// <returns>值是否存在,若存在则为 <c>true</c></returns>
public bool GetValue<T>(string key, [NotNullWhen(true)] out T? value, object? argument = null);
/// <summary>
/// 设置一个值。
/// </summary>
/// <param name="key">键</param>
/// <param name="value">新值</param>
/// <param name="argument">上下文参数</param>
/// <typeparam name="T">值的类型</typeparam>
public void SetValue<T>(string key, T value, object? argument = null);
/// <summary>
/// 删除一个值。
/// </summary>
/// <param name="key">键</param>
/// <param name="argument">上下文参数</param>
public void Delete(string key, object? argument = null);
/// <summary>
/// 判断一个值是否存在。
/// </summary>
/// <param name="key">键</param>
/// <param name="argument">上下文参数</param>
/// <returns>值是否存在,若存在则为 <c>true</c></returns>
public bool Exists(string key, object? argument = null);
}
@@ -0,0 +1,30 @@
using System.Collections.Generic;
namespace PCL.Core.App.Configuration;
/// <summary>
/// 配置作用域。
/// </summary>
public interface IConfigScope
{
/// <summary>
/// 检查指定的多个配置项是否在该作用域中。
/// </summary>
/// <param name="keys">配置键</param>
/// <returns>所有存在于该作用域中的键的集合</returns>
public IEnumerable<string> CheckScope(IReadOnlySet<string> keys);
/// <summary>
/// 重置作用域,将使作用域中的所有值回到默认值状态。
/// </summary>
/// <param name="argument">上下文参数</param>
/// <returns>是否成功重置作用域,若成功则为 <c>true</c></returns>
public bool Reset(object? argument = null);
/// <summary>
/// 检查作用域是否为默认。
/// </summary>
/// <param name="argument">上下文参数</param>
/// <returns>若作用域中的所有值均为默认值,则为 <c>true</c>,否则为 <c>false</c></returns>
public bool IsDefault(object? argument = null);
}
@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using PCL.Core.Utils.Exts;
namespace PCL.Core.App.Configuration.Storage;
/// <summary>
/// 提供 LTCat-style ini 格式的键值文件读写。
/// </summary>
public class CatIniFileProvider : CommonFileProvider, IEnumerableKeyProvider
{
private readonly Dictionary<string, string> _dict = [];
public CatIniFileProvider(string path) : base(path)
{
if (!File.Exists(path)) return;
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var reader = new StreamReader(stream, Encoding.UTF8);
while (reader.ReadLine() is { } line)
{
if (line.IsNullOrWhiteSpace()) continue;
var split = line.Split(':', 2);
_dict[split[0]] = split[1];
}
}
public IEnumerable<string> Keys => _dict.Keys;
public override T Get<T>(string key)
{
if (!_dict.TryGetValue(key, out var value)) throw new KeyNotFoundException($"Not found: '{key}");
return value.Convert<T>() ?? throw new NullReferenceException();
}
public override void Set<T>(string key, T value)
=> _dict[key] = value.ConvertToString() ?? throw new NullReferenceException();
public override bool Exists(string key) => _dict.ContainsKey(key);
public override void Remove(string key) => _dict.Remove(key);
protected override void WriteToStream(Stream stream)
{
var writer = new StreamWriter(stream, Encoding.UTF8);
foreach (var (key, value) in _dict)
{
var keyStr = key.ReplaceLineBreak();
var valueStr = value.ReplaceLineBreak();
writer.WriteLine($"{keyStr}:{valueStr}");
}
writer.Flush();
}
}
@@ -0,0 +1,31 @@
using System.IO;
using PCL.Core.Utils;
namespace PCL.Core.App.Configuration.Storage;
public abstract class CommonFileProvider(string path) : IKeyValueFileProvider
{
public string FilePath { get; set; } = path;
public abstract T Get<T>(string key);
public abstract void Set<T>(string key, T value);
public abstract bool Exists(string key);
public abstract void Remove(string key);
protected abstract void WriteToStream(Stream stream);
public void Sync()
{
if (!File.Exists(FilePath)) Directory.CreateDirectory(Basics.GetParentPath(FilePath)!);
var tmpFile = $"{FilePath}.tmp{RandomUtils.NextInt(1, 99999):00000}";
var bakFile = $"{FilePath}.bak";
using (var stream = new FileStream(tmpFile, FileMode.Create, FileAccess.Write, FileShare.Read))
{
WriteToStream(stream);
stream.Flush(true);
}
if (File.Exists(FilePath)) File.Replace(tmpFile, FilePath, bakFile);
else File.Move(tmpFile, FilePath);
}
}
@@ -0,0 +1,12 @@
using System;
namespace PCL.Core.App.Configuration.Storage;
public class ConfigFileInitException(string path, string message, Exception? inner = null)
: Exception(message, inner)
{
/// <summary>
/// Relative file path.
/// </summary>
public string Path { get; } = path;
}
@@ -0,0 +1,146 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text.Encodings.Web;
using System.Text.Json;
using PCL.Core.App.IoC;
using PCL.Core.Logging;
using PCL.Core.Utils.Diagnostics;
using PCL.Core.Utils;
namespace PCL.Core.App.Configuration.Storage;
public enum StorageAction
{
Get,
Exists,
Set,
Delete
}
/// <summary>
/// 存取仓库模型实现与底层抽象。
/// </summary>
public abstract class ConfigStorage : IConfigProvider
{
protected abstract bool OnAccess<TKey, TValue>(
StorageAction action,
ref TKey key,
[NotNullWhen(true)] ref TValue value,
object? argument);
protected virtual void OnStop() { }
/// <summary>
/// 停止存取工作,保存并释放资源。
/// </summary>
public void Stop() => OnStop();
#if DEBUG
private static readonly bool _EnableTrace = Basics.CommandLineArguments.Contains("--trace-traffic");
#endif
/// <summary>
/// 执行存取操作。
/// </summary>
/// <param name="action">操作类型</param>
/// <param name="key">键</param>
/// <param name="value">值,若无值则为该类型默认值</param>
/// <param name="argument">上下文参数</param>
/// <typeparam name="TKey">键的类型</typeparam>
/// <typeparam name="TValue">值的类型</typeparam>
/// <returns>是否有输出值</returns>
public bool Access<TKey, TValue>(
StorageAction action,
ref TKey key,
[NotNullWhen(true)] ref TValue value,
object? argument)
{
const string logModule = "Config";
var hasOutput = false;
try
{
hasOutput = OnAccess(action, ref key, ref value, argument);
}
catch (Exception ex)
{
var msg = $"Config Storage Error Report\n" +
$"A exception was thrown while processing an access.\n\n" +
$"[Diagnostics Info]\n{_GenerateDiagnosticsInfo(action, key, value, hasOutput, argument, true)}\n\n" +
$"[Exception Details]\n{ex}";
LogWrapper.Fatal(logModule, msg);
Lifecycle.ForceShutdown(-2);
}
#if DEBUG
if (_EnableTrace)
{
LogWrapper.Trace(logModule, _GenerateDiagnosticsInfo(action, key, value, hasOutput, argument));
}
#endif
return hasOutput;
}
private static readonly JsonSerializerOptions _SerializerOptions = new(JsonCompat.SerializerOptions)
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
private string _GenerateDiagnosticsInfo<TKey, TValue>(
StorageAction accessAction,
TKey? accessKey,
TValue? accessValue,
bool accessHasValue,
object? accessContext,
bool appendCallStack = false)
{
#if TRACE
const bool needFileInfo = true;
#else
const bool needFileInfo = false;
#endif
var context = JsonSerializer.Serialize(accessContext, _SerializerOptions);
var key = JsonSerializer.Serialize(accessKey, _SerializerOptions);
var value = JsonSerializer.Serialize(accessValue, _SerializerOptions);
var caller = appendCallStack
? "Stack:\n|=> " + string.Join("\n|=> ", StackHelper.GetStack(includeParameters: true, needFileInfo: needFileInfo).Skip(1))
: "Caller: " + StackHelper.GetDirectCallerName(includeParameters: true, skipAppFrames: 2);
var msg = $"Storage Access: {accessAction} {ToString()}\n" +
$"|- Context: {(accessContext is null ? "" : "(" + accessContext.GetType().Name + ") ")}{context}\n" +
$"|- Key: ({typeof(TKey).Name}) {key}\n" +
$"|- Value: ({typeof(TValue).Name}) {(accessHasValue ? value : "undefined")}\n" +
$"|- {caller}";
return msg;
}
public bool GetValue<T>(string key, [NotNullWhen(true)] out T? value, object? argument = null)
{
var keyRef = key;
T? valueRef = default;
var hasValue = Access(StorageAction.Get, ref keyRef, ref valueRef, argument);
value = valueRef;
return hasValue;
}
public void SetValue<T>(string key, T value, object? argument = null)
{
var keyRef = key;
var valueRef = value;
Access(StorageAction.Set, ref keyRef, ref valueRef, argument);
}
public void Delete(string key, object? argument = null)
{
var keyRef = key;
object? valueRef = null;
Access(StorageAction.Delete, ref keyRef, ref valueRef, argument);
}
public bool Exists(string key, object? argument = null)
{
var keyRef = key;
var resultRef = false;
return Access(StorageAction.Exists, ref keyRef, ref resultRef, argument) && resultRef;
}
public override string ToString() => $"{GetType().Name}@{GetHashCode()}";
}
@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace PCL.Core.App.Configuration.Storage;
public class DynamicCacheConfigStorage : ConfigStorage
{
private readonly Dictionary<object, ConfigStorage> _cache = [];
private ConfigStorage? _nullContextCache;
/// <summary>
/// 存取仓库工厂。在没有匹配的上下文实例时将被调用,以创建新的上下文实例。
/// </summary>
public required Func<object?, ConfigStorage> StorageFactory { get; init; }
protected override bool OnAccess<TKey, TValue>(StorageAction action, ref TKey key, [NotNullWhen(true)] ref TValue value, object? context)
{
ConfigStorage? storage;
if (context is null) storage = _nullContextCache;
else _cache.TryGetValue(context, out storage);
if (storage is null)
{
try
{
storage = StorageFactory(context);
if (context is null) _nullContextCache = storage;
else _cache[context] = storage;
}
catch (Exception ex)
{
throw new Exception("Failed to invoke storage factory", ex);
}
}
return storage.Access(action, ref key, ref value, context);
}
protected override void OnStop()
{
foreach (var item in _cache.Values) item.Stop();
_cache.Clear();
}
public bool InvalidateCache(object context)
{
var result = _cache.TryGetValue(context, out var center);
if (result)
{
center?.Stop();
_cache.Remove(context);
}
return result;
}
}
@@ -0,0 +1,64 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using PCL.Core.Logging;
using PCL.Core.Utils.Secret;
using PCL.Core.Utils;
namespace PCL.Core.App.Configuration.Storage;
public class EncryptedFileConfigStorage(ConfigStorage source) : ConfigStorage
{
public ConfigStorage Source { get; } = source;
private static readonly JsonSerializerOptions _SerializerOptions = new(JsonCompat.SerializerOptions)
{
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
AllowOutOfOrderMetadataProperties = true,
};
protected override bool OnAccess<TKey, TValue>(StorageAction action, ref TKey key, [NotNullWhen(true)] ref TValue value, object? argument)
{
try
{
switch (action)
{
case StorageAction.Set:
{
// 序列化
var type = typeof(TValue);
string strValue;
if (type == typeof(string)) strValue = value?.ToString() ?? string.Empty;
else strValue = JsonSerializer.Serialize(value, _SerializerOptions);
// 加密
strValue = EncryptHelper.SecretEncrypt(strValue);
return Source.Access(StorageAction.Set, ref key, ref strValue, argument);
}
case StorageAction.Get:
{
// 获取加密值
string? raw = null;
var hasOutput = Source.Access(StorageAction.Get, ref key, ref raw, argument);
if (!hasOutput) return false;
// 解密
var decrypted = EncryptHelper.SecretDecrypt(raw);
// 反序列化
var type = typeof(TValue);
if (type == typeof(bool)) Unsafe.As<TValue, bool>(ref value) = decrypted.ToLowerInvariant() is "true" or "1";
else if (type == typeof(string)) Unsafe.As<TValue, string>(ref value) = decrypted;
else value = JsonSerializer.Deserialize<TValue>(decrypted, _SerializerOptions) ?? throw new NullReferenceException("Decryption produced a null reference");
return hasOutput;
}
default: return Source.Access(action, ref key, ref value, argument);
}
}
catch (Exception ex)
{
LogWrapper.Error(ex, "Config", "无法处理加解密");
return false;
}
}
}
@@ -0,0 +1,153 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using PCL.Core.App.Localization;
using PCL.Core.Logging;
using PCL.Core.UI;
namespace PCL.Core.App.Configuration.Storage;
/// <summary>
/// 文件存取仓库。
/// </summary>
public class FileConfigStorage : ConfigStorage
{
/// <summary>
/// 键值文件实例。
/// </summary>
public IKeyValueFileProvider File { get; }
private readonly Channel<(string, Action)> _writeActionChannel;
private readonly CancellationTokenSource _writeActionCts;
private readonly ManualResetEventSlim _writeStopEvent = new(true);
public FileConfigStorage(IKeyValueFileProvider file)
{
File = file;
_writeActionChannel = Channel.CreateUnbounded<(string, Action)>();
_writeActionCts = new CancellationTokenSource();
Task.Run(async () =>
{
_writeStopEvent.Reset();
const long syncInterval = 10000; // ms
var lastSyncTick = 0L;
var cancelToken = _writeActionCts.Token;
var writeActionMap = new Dictionary<string, Action>();
var reader = _writeActionChannel.Reader;
try
{
while (!cancelToken.IsCancellationRequested)
{
// 读入并合并暂存操作
var (key, action) = await reader.ReadAsync(cancelToken);
writeActionMap[key] = action;
if (Environment.TickCount64 - lastSyncTick < syncInterval || cancelToken.IsCancellationRequested) continue;
// 同步文件
Sync();
lastSyncTick = Environment.TickCount64;
writeActionMap.Clear();
}
}
catch (OperationCanceledException) { /* ignoring*/ }
finally
{
// 结束时执行一次同步
Sync();
}
_writeStopEvent.Set();
return;
void Sync()
{
try
{
LogWrapper.Trace("Config", $"正在保存 {File.FilePath}");
foreach (var action in writeActionMap.Values) action();
File.Sync();
}
catch (Exception ex)
{
LogWrapper.Error(ex, "Config", "配置文件保存失败");
var summary = Lang.Text("Config.Error.SaveFailed.Message", File.FilePath);
var message = ExceptionDetails.Compose(summary, ex);
MsgBoxWrapper.Show(
message,
Lang.Text("Config.Error.SaveFailed.Title"),
MsgBoxTheme.Error);
}
}
});
}
protected override void OnStop()
{
_writeActionCts.Cancel();
_writeStopEvent.Wait();
_writeStopEvent.Dispose();
}
protected override bool OnAccess<TKey, TValue>(
StorageAction action,
ref TKey key,
[NotNullWhen(true)] ref TValue value,
object? argument)
{
if (key is not string strKey) throw new NotSupportedException($"Key '{key}' is not supported");
#pragma warning disable CS8762 // Parameter must have a non-null value when exiting in some condition.
switch (action)
{
case StorageAction.Get:
if (!File.Exists(strKey)) return false;
try
{
value = File.Get<TValue>(strKey);
}
catch (Exception ex) when (ex is JsonException
or InvalidCastException
or FormatException
or OverflowException
or ArgumentException
or KeyNotFoundException
or InvalidDataException)
{
LogWrapper.Warn(ex, "Config", $"配置项 {strKey} 读取失败(可能已损坏),重置为默认值");
if (!_writeActionChannel.Writer.TryWrite((strKey, () => File.Remove(strKey))))
{
LogWrapper.Warn("Config", $"配置项 {strKey} 清理任务入队失败,改为同步删除");
try
{
File.Remove(strKey);
File.Sync();
}
catch (Exception cleanupEx)
{
LogWrapper.Error(cleanupEx, "Config", $"配置项 {strKey} 同步删除失败,可能需人工处理");
}
}
return false;
}
return true;
case StorageAction.Exists:
// 由于 Exists 的 value 类型一定是 bool,此处可 unsafe 直接赋值
if (typeof(TValue) == typeof(bool)) Unsafe.As<TValue, bool>(ref value) = File.Exists(strKey);
else throw new InvalidOperationException($"Storage action '{StorageAction.Exists}' must have a boolean value");
return true;
case StorageAction.Set:
var localValue = value;
_writeActionChannel.Writer.TryWrite((strKey, () => File.Set(strKey, localValue)));
return false;
case StorageAction.Delete:
_writeActionChannel.Writer.TryWrite((strKey, () => File.Remove(strKey)));
return false;
default: throw new InvalidOperationException($"Invalid storage action: {action}");
}
#pragma warning restore CS8762 // Parameter must have a non-null value when exiting in some condition.
}
public override string ToString() => $"{base.ToString()} ({File.FilePath})";
}
@@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace PCL.Core.App.Configuration.Storage;
public interface IEnumerableKeyProvider
{
/// <summary>
/// 获取文件包含的所有键。通常是一个耗时操作,慎用。
/// </summary>
public IEnumerable<string> Keys { get; }
}
@@ -0,0 +1,37 @@
namespace PCL.Core.App.Configuration.Storage;
/// <summary>
/// 键值文件模型。
/// </summary>
public interface IKeyValueFileProvider
{
/// <summary>
/// 文件路径。
/// </summary>
public string FilePath { get; }
/// <summary>
/// 获取一个值。
/// </summary>
public T Get<T>(string key);
/// <summary>
/// 设置一个值。
/// </summary>
public void Set<T>(string key, T value);
/// <summary>
/// 判断一个值是否存在。
/// </summary>
public bool Exists(string key);
/// <summary>
/// 移除一个值。
/// </summary>
public void Remove(string key);
/// <summary>
/// 写入文件。
/// </summary>
public void Sync();
}
@@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using PCL.Core.Utils;
namespace PCL.Core.App.Configuration.Storage;
/// <summary>
/// 提供 JSON 格式的键值文件读写。
/// </summary>
public class JsonFileProvider : CommonFileProvider, IEnumerableKeyProvider
{
private readonly JsonObject _rootElement;
private static readonly JsonDocumentOptions _DocumentOptions = JsonCompat.DocumentOptions;
private static readonly JsonSerializerOptions _SerializerOptions = new(JsonCompat.SerializerOptions)
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
AllowOutOfOrderMetadataProperties = true,
};
private static readonly JsonWriterOptions _WriterOptions = new()
{
Indented = true
};
public JsonFileProvider(string path) : base(path)
{
try
{
if (File.Exists(path))
{
using var stream = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
var parseResult = JsonNode.Parse(stream, JsonCompat.NodeOptions, _DocumentOptions);
if (parseResult is not JsonObject root)
throw new ConfigFileInitException(path,
$"Invalid root element type: {parseResult?.GetValueKind().ToString() ?? "Empty"}");
_rootElement = root;
}
else
{
using var stream = new FileStream(FilePath, FileMode.CreateNew, FileAccess.Write, FileShare.ReadWrite);
_rootElement = new JsonObject();
JsonSerializer.Serialize(stream, _rootElement, _SerializerOptions);
}
}
catch (Exception ex)
{
if (ex is ConfigFileInitException) throw;
throw new ConfigFileInitException(path, "Failed to read JSON file", ex);
}
}
public override T Get<T>(string key)
{
var result = _rootElement[key];
if (result is null) throw new KeyNotFoundException($"Not found: '{key}'");
try
{
var r = result.Deserialize<T>(_SerializerOptions);
return r ?? throw GetNullException();
}
catch (JsonException)
{
T fallback;
var type = typeof(T);
if (type == typeof(string)) fallback = (T)(object)result.ToString();
else
{
var jsonStr = result.Deserialize<string>(_SerializerOptions)!;
if (type == typeof(bool)) fallback = (T)(object)(jsonStr.ToLowerInvariant() is "true" or "1");
else fallback = JsonSerializer.Deserialize<T>(jsonStr, _SerializerOptions) ?? throw GetNullException();
}
Set(key, fallback);
return fallback;
}
Exception GetNullException() => new InvalidDataException($"Deserialized value is null: '{key}'");
}
public override void Set<T>(string key, T value)
{
_rootElement[key] = JsonSerializer.SerializeToNode(value, _SerializerOptions);
}
public override bool Exists(string key)
{
return _rootElement.ContainsKey(key);
}
public override void Remove(string key)
{
_rootElement.Remove(key);
}
protected override void WriteToStream(Stream stream)
{
var writer = new Utf8JsonWriter(stream, _WriterOptions);
_rootElement.WriteTo(writer, _SerializerOptions);
writer.Flush();
}
public IEnumerable<string> Keys => _rootElement.Select(pair => pair.Key);
}
@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using YamlDotNet.Serialization;
using PCL.Core.Utils;
namespace PCL.Core.App.Configuration.Storage;
// Partly generated by gpt-5-mini (20250903)
public static class JsonToYamlConverter
{
/// <summary>
/// 从 jsonInput 读取 JSON,转换为 YAML 写入 yamlOutput。
/// </summary>
/// <param name="jsonInput">可读的 JSON 输入流</param>
/// <param name="yamlOutput">可写的 YAML 输出流</param>
/// <param name="leaveOpen">是否在返回时保留输出流打开</param>
public static void Convert(Stream jsonInput, Stream yamlOutput, bool leaveOpen = false)
{
ArgumentNullException.ThrowIfNull(jsonInput);
ArgumentNullException.ThrowIfNull(yamlOutput);
if (!jsonInput.CanRead) throw new ArgumentException("must be readable", nameof(jsonInput));
if (!yamlOutput.CanWrite) throw new ArgumentException("must be writable", nameof(yamlOutput));
using var doc = JsonDocument.Parse(jsonInput, JsonCompat.DocumentOptions);
var obj = _ConvertElement(doc.RootElement);
var serializer = new SerializerBuilder().Build();
using var writer = new StreamWriter(yamlOutput, new UTF8Encoding(false), 8192, leaveOpen);
serializer.Serialize(writer, obj);
writer.Flush();
}
/// <summary>
/// 异步从 jsonInput 读取 JSON,转换为 YAML 写入 yamlOutput。
/// </summary>
/// <param name="jsonInput">可读的 JSON 输入流</param>
/// <param name="yamlOutput">可写的 YAML 输出流</param>
/// <param name="leaveOpen">是否在返回时保留输出流打开</param>
public static async Task ConvertAsync(Stream jsonInput, Stream yamlOutput, bool leaveOpen = false)
{
ArgumentNullException.ThrowIfNull(jsonInput);
ArgumentNullException.ThrowIfNull(yamlOutput);
if (!jsonInput.CanRead) throw new ArgumentException("jsonInput must be readable", nameof(jsonInput));
if (!yamlOutput.CanWrite) throw new ArgumentException("yamlOutput must be writable", nameof(yamlOutput));
using var doc = await JsonDocument.ParseAsync(jsonInput, JsonCompat.DocumentOptions).ConfigureAwait(false);
var obj = _ConvertElement(doc.RootElement);
var serializer = new SerializerBuilder().Build();
await using var streamWriter = new StreamWriter(yamlOutput, new UTF8Encoding(false), 8192, leaveOpen);
serializer.Serialize(streamWriter, obj);
await streamWriter.FlushAsync().ConfigureAwait(false);
}
private static object? _ConvertElement(JsonElement element)
{
switch (element.ValueKind)
{
case JsonValueKind.Object:
{
var dict = new Dictionary<string, object?>(StringComparer.Ordinal);
foreach (var prop in element.EnumerateObject())
{
dict[prop.Name] = _ConvertElement(prop.Value);
}
return dict;
}
case JsonValueKind.Array:
{
return element.EnumerateArray().Select(_ConvertElement).ToList();
}
case JsonValueKind.String: return element.GetString();
case JsonValueKind.Number:
{
// 尽量保留数值类型:先尝试 Int64,再尝试 decimal(避免浮点精度丢失),最后尝试 double
if (element.TryGetInt64(out var l)) return l;
var raw = element.GetRawText();
if (decimal.TryParse(raw, NumberStyles.Any, CultureInfo.InvariantCulture, out var dec)) return dec;
if (element.TryGetDouble(out var d)) return d;
// 兜底:返回原始文本
return raw;
}
case JsonValueKind.True: return true;
case JsonValueKind.False: return false;
case JsonValueKind.Null:
case JsonValueKind.Undefined:
default:
return null;
}
}
}
@@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using PCL.Core.Logging;
using YamlDotNet.RepresentationModel;
using YamlDotNet.Serialization;
namespace PCL.Core.App.Configuration.Storage;
/// <summary>
/// 提供 YAML 格式的键值文件读写。当提供的文件找不到时,将尝试读取其它同名文件并将其转换到 YAML。
/// </summary>
public class YamlFileProvider : CommonFileProvider, IEnumerableKeyProvider
{
private readonly YamlMappingNode _rootNode;
private static readonly IDeserializer _Deserializer = new DeserializerBuilder()
.IgnoreUnmatchedProperties().WithEnforceRequiredMembers().Build();
private static readonly ISerializer _Serializer = new SerializerBuilder()
.DisableAliases().Build();
private static YamlMappingNode? _LoadFile(string path)
{
if (!File.Exists(path)) return null;
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var reader = new StreamReader(stream, Encoding.UTF8);
try
{
var yaml = new YamlStream();
yaml.Load(reader);
if (yaml.Documents.Count == 0) return [];
var rootNode = yaml.Documents[0].RootNode;
return rootNode as YamlMappingNode ?? throw new ConfigFileInitException(path, $"Invalid root node type: {rootNode.NodeType}");
}
catch (Exception ex)
{
if (ex is ConfigFileInitException) throw;
throw new ConfigFileInitException(path, "Failed to load YAML content", ex);
}
}
public YamlFileProvider(string path) : base(path)
{
var rootNode = _LoadFile(path);
if (rootNode is not null)
{
_rootNode = rootNode;
return;
}
try // 尝试从 JSON 和 LTCat-style ini 转换
{
var jsonPath = Path.Combine(Basics.GetParentPath(path)!, Path.GetFileNameWithoutExtension(path) + ".json");
if (File.Exists(jsonPath))
{
using var jsonStream = new FileStream(jsonPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var yamlStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
JsonToYamlConverter.Convert(jsonStream, yamlStream); // yamlStream 会被自动关闭
_rootNode = _LoadFile(path)!;
return;
}
}
catch (Exception ex)
{
LogWrapper.Warn(ex, "转换失败,已忽略");
}
_rootNode = [];
}
public override T Get<T>(string key)
{
var result = _rootNode.Children[key];
var parser = result.ConvertToEventStream().GetParser();
try
{
return _Deserializer.Deserialize<T>(parser);
}
catch (Exception)
{
var type = typeof(T);
var graphStr = result.ToString();
var fallback = (type == typeof(bool))
? (T)(object)(graphStr.ToLowerInvariant() is "true" or "1")
: (T)(object)graphStr;
Set(key, fallback);
return fallback;
}
}
public override void Set<T>(string key, T value)
{
var emitter = new YamlNodeEmitter();
_Serializer.Serialize(emitter, value);
_rootNode.Children[key] = emitter.SingleRootNode;
}
public override bool Exists(string key)
{
return _rootNode.Children.ContainsKey(key);
}
public override void Remove(string key)
{
_rootNode.Children.Remove(key);
}
protected override void WriteToStream(Stream stream)
{
var writer = new StreamWriter(stream, Encoding.UTF8);
_Serializer.Serialize(writer, _rootNode);
writer.Flush();
}
public IEnumerable<string> Keys => _rootNode.Select(pair => pair.Key.ToString());
}
@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.RepresentationModel;
namespace PCL.Core.App.Configuration.Storage;
// 修改自: https://dotnetfiddle.net/jaG1i1
// 来源: https://stackoverflow.com/a/40727087
// 原作者: Antoine Aubry (是 YamlDotNet 的作者, 真不懂都造出来了为什么不直接把它写到库里)
public static class YamlNodeConverter
{
public class EventStreamParserAdapter(IEnumerable<ParsingEvent> events) : IParser
{
private readonly IEnumerator<ParsingEvent> _enumerator = events.GetEnumerator();
public ParsingEvent Current => _enumerator.Current;
public bool MoveNext() => _enumerator.MoveNext();
}
public static IParser GetParser(this IEnumerable<ParsingEvent> eventStream)
{
return new EventStreamParserAdapter(eventStream);
}
public static IEnumerable<ParsingEvent> ConvertToEventStream(this YamlStream stream)
{
yield return new StreamStart();
foreach (var document in stream.Documents)
{
foreach (var evt in document.ConvertToEventStream())
{
yield return evt;
}
}
yield return new StreamEnd();
}
public static IEnumerable<ParsingEvent> ConvertToEventStream(this YamlDocument document)
{
yield return new DocumentStart();
foreach (var evt in document.RootNode.ConvertToEventStream())
{
yield return evt;
}
yield return new DocumentEnd(false);
}
public static IEnumerable<ParsingEvent> ConvertToEventStream(this YamlNode node)
{
if (node is YamlScalarNode scalar)
{
return _ConvertToEventStream(scalar);
}
if (node is YamlSequenceNode sequence)
{
return _ConvertToEventStream(sequence);
}
if (node is YamlMappingNode mapping)
{
return _ConvertToEventStream(mapping);
}
throw new NotSupportedException($"Unsupported node type: {node.GetType().Name}");
}
private static IEnumerable<ParsingEvent> _ConvertToEventStream(YamlScalarNode scalar)
{
yield return new Scalar(scalar.Anchor, scalar.Tag, scalar.Value!, scalar.Style, false, false);
}
private static IEnumerable<ParsingEvent> _ConvertToEventStream(YamlSequenceNode sequence)
{
yield return new SequenceStart(sequence.Anchor, sequence.Tag, false, sequence.Style);
foreach (var node in sequence.Children)
{
foreach (var evt in node.ConvertToEventStream())
{
yield return evt;
}
}
yield return new SequenceEnd();
}
private static IEnumerable<ParsingEvent> _ConvertToEventStream(YamlMappingNode mapping)
{
yield return new MappingStart(mapping.Anchor, mapping.Tag, false, mapping.Style);
foreach (var pair in mapping.Children)
{
foreach (var evt in pair.Key.ConvertToEventStream())
{
yield return evt;
}
foreach (var evt in pair.Value.ConvertToEventStream())
{
yield return evt;
}
}
yield return new MappingEnd();
}
}
@@ -0,0 +1,197 @@
using System;
using System.Collections.Generic;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.RepresentationModel;
namespace PCL.Core.App.Configuration.Storage;
// Partly generated by gpt-5 (20250903)
public sealed class YamlNodeEmitter : IEmitter
{
private readonly List<YamlDocument> _documents = [];
private readonly Stack<Container> _stack = new();
private readonly Dictionary<string, YamlNode> _anchors = new(StringComparer.Ordinal);
private bool _inStream;
public IReadOnlyList<YamlDocument> Documents => _documents;
public YamlNode SingleRootNode => _documents.Count switch
{
0 => throw new InvalidOperationException("尚未产生任何文档。"),
1 => _documents[0].RootNode,
_ => throw new InvalidOperationException("存在多个文档,请使用 Documents 访问。")
};
public void Reset()
{
_documents.Clear();
_stack.Clear();
_anchors.Clear();
_inStream = false;
}
public void Emit(ParsingEvent @event)
{
switch (@event)
{
case StreamStart:
Reset();
_inStream = true;
break;
case StreamEnd:
_inStream = false;
if (_stack.Count != 0)
throw new InvalidOperationException("事件结束时堆栈未清空,事件序列不平衡。");
break;
case DocumentStart:
_RequireStream();
_stack.Push(Container.Document());
break;
case DocumentEnd:
{
_RequireStream();
if (_stack.Count == 0 || _stack.Peek().Kind != ContainerKind.Document)
throw new InvalidOperationException("DocumentEnd 前缺少对应的 DocumentStart。");
var doc = _stack.Pop();
if (doc.Node is null)
throw new InvalidOperationException("空文档:未设置根节点。");
_documents.Add(new YamlDocument(doc.Node));
}
break;
case MappingStart mapStart:
{
var map = new YamlMappingNode();
_ApplyAnchor(mapStart, map);
_AttachToParent(map);
_stack.Push(Container.Mapping(map));
}
break;
case MappingEnd:
{
if (_stack.Count == 0 || _stack.Peek().Kind != ContainerKind.Mapping)
throw new InvalidOperationException("MappingEnd 前缺少对应的 MappingStart。");
var finished = _stack.Pop();
// no-op: 已在 Start 时挂接到父级
if (finished.PendingKey is not null)
throw new InvalidOperationException("映射以键结尾,缺少对应的值。");
}
break;
case SequenceStart seqStart:
{
var seq = new YamlSequenceNode();
_ApplyAnchor(seqStart, seq);
_AttachToParent(seq);
_stack.Push(Container.Sequence(seq));
}
break;
case SequenceEnd:
{
if (_stack.Count == 0 || _stack.Peek().Kind != ContainerKind.Sequence)
throw new InvalidOperationException("SequenceEnd 前缺少对应的 SequenceStart。");
_stack.Pop(); // 已在 Start 时挂接到父级
}
break;
case Scalar scalar:
{
var node = new YamlScalarNode(scalar.Value);
_ApplyAnchor(scalar, node);
_AttachToParent(node);
}
break;
case AnchorAlias alias:
{
var anchorName = alias.Value.Value; // AnchorName.Value -> string
if (!_anchors.TryGetValue(anchorName, out var target))
throw new InvalidOperationException($"未找到锚点 '{anchorName}' 的定义。");
_AttachToParent(target);
}
break;
}
}
private void _RequireStream()
{
if (!_inStream)
throw new InvalidOperationException("必须在 StreamStart 与 StreamEnd 之间接收事件。");
}
private void _ApplyAnchor(NodeEvent nodeEvent, YamlNode node)
{
// 仅在存在锚点时登记;Tag/Style 等可按需扩展
if (nodeEvent.Anchor.IsEmpty) return;
var name = nodeEvent.Anchor.Value;
node.Anchor = name;
// 最新 YamlDotNet 表示模型允许同名锚点复用同一节点引用;
// 若重复定义同名锚点,认为是非法
if (!_anchors.TryAdd(name, node))
throw new InvalidOperationException($"锚点 '{name}' 被重复定义。");
}
private void _AttachToParent(YamlNode node)
{
if (_stack.Count == 0)
{
throw new InvalidOperationException("缺少 DocumentStart:无法确定根节点所属文档。");
}
var parent = _stack.Peek();
switch (parent.Kind)
{
case ContainerKind.Document:
if (parent.Node is not null)
throw new InvalidOperationException("一个文档只能包含一个根节点。");
parent.Node = node;
_stack.Pop();
_stack.Push(parent); // 写回修改
break;
case ContainerKind.Sequence:
((YamlSequenceNode)parent.Node!).Add(node);
break;
case ContainerKind.Mapping:
if (parent.PendingKey is null)
{
parent.PendingKey = node; // 作为键
}
else
{
((YamlMappingNode)parent.Node!).Add(parent.PendingKey, node);
parent.PendingKey = null;
}
_stack.Pop();
_stack.Push(parent); // 写回修改
break;
default:
throw new ArgumentOutOfRangeException();
}
}
private enum ContainerKind { Document, Mapping, Sequence }
private struct Container
{
public ContainerKind Kind;
public YamlNode? Node;
public YamlNode? PendingKey;
public static Container Document() => new() { Kind = ContainerKind.Document, Node = null, PendingKey = null };
public static Container Mapping(YamlMappingNode map) => new() { Kind = ContainerKind.Mapping, Node = map, PendingKey = null };
public static Container Sequence(YamlSequenceNode seq) => new() { Kind = ContainerKind.Sequence, Node = seq, PendingKey = null };
}
}
@@ -0,0 +1,48 @@
using System;
using LiteDB;
namespace PCL.Core.App.Database;
/// <summary>
/// Base database connention entry.
/// </summary>
public abstract class DatabaseEntry : IDisposable
{
protected readonly LiteDatabase Db;
private bool _disposed = false;
/// <exception cref="ArgumentNullException">Throw if connection path is invalid.</exception>
protected DatabaseEntry(string connPath)
{
var db = DatabaseService.GetConnection(connPath);
Db = db ?? throw new ArgumentNullException(nameof(db), "Database variable can not be null.");
}
/// <exception cref="ArgumentNullException">Throw if connection path is invalid.</exception>
protected DatabaseEntry(LiteDatabase database)
{
Db = database ?? throw new ArgumentNullException(nameof(database), "Database variable can not be null.");
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
// NOTE: do not dispose Db, because it is managed by DatabaseService
}
_disposed = true;
}
}
@@ -0,0 +1,39 @@
using System;
using System.Collections.Concurrent;
using LiteDB;
using PCL.Core.App.IoC;
namespace PCL.Core.App.Database;
[LifecycleService(LifecycleState.Loading)]
public class DatabaseService() : GeneralService("database", "数据库管理")
{
private static readonly ConcurrentDictionary<string, LiteDatabase> _Instances = new();
/// <inheritdoc />
public override void Stop()
{
foreach (var instance in _Instances.Values)
{
instance.Dispose();
}
_Instances.Clear();
}
/// <summary>
/// Get the database connenction from specified connection path.<br/>
/// If not exists, a new connection will be created and cached.
/// </summary>
/// <returns>Getted connenction instance.</returns>
/// <exception cref="ArgumentException">Throw if connection path is invalid.</exception>
public static LiteDatabase GetConnection(string connectionPath)
{
if (string.IsNullOrWhiteSpace(connectionPath))
{
throw new ArgumentException("Connection string canot be null or whitespace.", nameof(connectionPath));
}
return _Instances.GetOrAdd(connectionPath, cp => new LiteDatabase(cp));
}
}
@@ -0,0 +1,35 @@
# Database Guildline
This is a light database management guildline for PCL.Core. It is not a strict rule, but a recommendation to follow.
## How to create a new database
1. You need create a class that inheritance from `DatabaseEntry`.
2. Then make a ctor that call `base(<connPath>)` with the connection path.
3. Now this class is ready to use. (Yes it is that simple, and will automatically create the database file if not exists)
## DatabaseEntry
The `DatabaseEntry` class is the entrance to use the database.
User must implement a class that inheritance from `DatabaseEntry` to use it.
And the child class should implement there own method to provide some ways to interact with the database.
## A Normal Example
```charp
public class UserDatabase : DatabaseEntry
{
public UserDatabase() : base("example.db")
{
// You can do some initialization here.
// Like create table if not exists,
// or anything else.
}
public List<(int Id, string Name, int Age)> GetAllUsers()
{
// In your method, you shoul provide some way to interact with the database.
}
}
```
Then, you can use it like this:
```charp
var db = new UserDatabase();
var users = db.GetAlUsers();
```
@@ -0,0 +1,65 @@
using System;
using System.Threading;
using System.Windows;
using System.Windows.Threading;
using PCL.Core.App.IoC;
namespace PCL.Core.App.Essentials;
[LifecycleService(LifecycleState.BeforeLoading, Priority = int.MinValue)]
[LifecycleScope("application", "应用程序", false)]
public sealed partial class ApplicationService
{
public static Func<Application>? Loading { private get; set; }
[LifecycleStart]
private static void _Start()
{
Context.Debug("正在初始化 WPF 应用程序容器");
var app = Loading!.Invoke();
app.DispatcherUnhandledException += (_, e) => Lifecycle.OnException(e.Exception);
app.Startup += (_, _) => Lifecycle.OnLoading();
Lifecycle.CurrentApplication = app;
Loading = null;
Context.Trace("应用程序容器初始化完毕");
}
[LifecycleStop]
private static void _Stop()
{
var app = Lifecycle.CurrentApplication;
var dispatcher = app.Dispatcher;
if (Lifecycle.IsForceShutdown)
{
Context.Warn("已指定强制关闭,跳过 WPF 标准关闭流程");
return;
}
if (dispatcher is null || dispatcher.HasShutdownFinished) return;
using var exited = new ManualResetEventSlim();
dispatcher.BeginInvoke(DispatcherPriority.Send, () =>
{
app.Exit += Exited;
if (dispatcher.HasShutdownStarted) return;
Context.Debug("发起 WPF 退出流程");
app.Shutdown();
});
try
{
Context.Debug("正在等待应用程序容器退出");
var result = exited.Wait(5000);
if (result) Context.Trace("应用程序容器已退出");
else Context.Warn("应用程序容器退出超时,停止等待");
}
finally
{
dispatcher.BeginInvoke(DispatcherPriority.Send, () => app.Exit -= Exited);
}
return;
void Exited(object? sender, EventArgs e)
{
// ReSharper disable once AccessToDisposedClosure
exited.Set();
}
}
}
@@ -0,0 +1,24 @@
using System;
using System.Windows;
using PCL.Core.App.IoC;
namespace PCL.Core.App.Essentials;
[LifecycleService(LifecycleState.WindowCreating, Priority = int.MaxValue)]
public sealed class MainWindowService : GeneralService
{
public static Func<Window>? Loading { private get; set; }
private static LifecycleContext? _context;
private static LifecycleContext Context => _context!;
private MainWindowService() : base("window", "主窗体", false) { _context = ServiceContext; }
public override void Start()
{
Context.Debug("正在初始化 WPF 窗体");
var window = Loading!.Invoke();
window.Loaded += (_, _) => Lifecycle.OnWindowCreated();
Lifecycle.CurrentApplication.MainWindow = window;
Context.Trace("窗体创建完毕");
}
}
@@ -0,0 +1,340 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Text.Json;
using System.Threading;
using PCL.Core.App.IoC;
using PCL.Core.IO;
using PCL.Core.Logging;
using PCL.Core.Utils.OS;
using PCL.Core.Utils;
namespace PCL.Core.App.Essentials;
public delegate string? PromoteOperationFunction(string? arg);
/// <summary>
/// 标记一个方法,使其能够被提权进程调用,方法签名需符合 <see cref="PromoteOperationFunction"/>。
/// </summary>
/// <param name="name">提权操作名</param>
[DependencyCollector<PromoteOperationFunction>("promote", AttributeTargets.Method)]
[AttributeUsage(AttributeTargets.Method)]
public sealed class PromoteOperationAttribute(string name) : Attribute;
[LifecycleService(LifecycleState.BeforeLoading, Priority = -10)]
[LifecycleScope("promote", "提权服务", false)]
public sealed partial class PromoteService
{
private static Process? _promoteProcess;
private static NamedPipeServerStream? _promotePipeServer;
private static readonly ConcurrentQueue<PromoteOperation> _PendingOperations = [];
private readonly record struct PromoteOperation(string Command, Action<string?>? Callback, bool DetailLog);
/// <summary>
/// 提权进程是否正在运行。
/// </summary>
public static bool IsPromoteProcessRunning => _promoteProcess is not null;
/// <summary>
/// 当前进程是否是提权进程。
/// </summary>
public static bool IsCurrentProcessPromoted { get; private set; }
private static string _GetPromotePipeName(int processId) => $"PCLCE_PM@{processId}";
private static readonly Dictionary<string, PromoteOperationFunction> _OperationFunctions = new();
/// <summary>
/// 添加提权操作,仅在提权进程中有效。
/// </summary>
/// <param name="name">操作名</param>
/// <param name="operation">操作实现,接收参数并返回结果,返回值会被自动压缩为单行</param>
/// <returns>是否添加成功,若在主进程中调用或已存在相同操作名,则为 <c>false</c></returns>
public static bool AddOperationFunction(string name, PromoteOperationFunction operation)
{
return IsCurrentProcessPromoted && _OperationFunctions.TryAdd(name, operation);
}
/// <summary>
/// 添加自动将参数 JSON 反序列化的提权操作,仅在提权进程中有效。
/// </summary>
/// <param name="name">操作名</param>
/// <param name="operation">操作实现,接收反序列化的并返回结果,返回值会被自动压缩为单行</param>
/// <typeparam name="TValue">反序列化的目标类型</typeparam>
/// <returns>是否添加成功,若在主进程中调用或已存在相同操作名,则为 <c>false</c></returns>
public static bool AddJsonOperationFunction<TValue>(string name, Func<TValue?, string?> operation)
{
return AddOperationFunction(name, arg =>
{
if (arg is null) return OperationErrEmpty;
var obj = JsonSerializer.Deserialize<TValue>(arg, JsonCompat.SerializerOptions);
return operation(obj);
});
}
private const string OperationErrNotFound = "ERR_OPERATION_NOT_FOUND";
private const string OperationErrInvalidArgument = "ERR_ILLEGAL_ARGUMENT";
private const string OperationErrExceptionThrown = "ERR_UNHANDLED_EXCEPTION";
private const string OperationErrEmpty = "ERR_EMPTY";
/// <summary>
/// 提权进程接收到操作请求时触发的事件,接收一个字符串作为操作命令并返回一个字符串作为结果。<br/>
/// <b>注意:如果你不知道这是做什么的,请勿覆盖默认实现。</b>请使用 <see cref="AddOperationFunction"/>。
/// </summary>
public static Func<string, string?> Operate {
private get => field ??= command =>
{
var split = command.Split([' '], 2);
_OperationFunctions.TryGetValue(split[0], out var operation);
if (operation is null) return OperationErrNotFound;
try
{
return operation(split.Length > 1 ? split[1] : null) ?? OperationErrEmpty;
}
catch (Exception ex)
{
Context.Warn("操作出错", ex);
return OperationErrExceptionThrown;
}
};
set;
} = null!;
private static string _ShortenString(string str)
{
#if TRACE
const int maxLength = 40;
#else
const int maxLength = 15;
#endif
if (str.Length <= maxLength) return str;
return str[..maxLength] + "...";
}
// 提权进程: 连接管道开始通信
private static void _PerformAsPromoteProcess(string pid)
{
Context.Info("正在连接提权通信管道");
var process = Process.GetProcessById(int.Parse(pid));
// 验证来源
var mainProcessPath = Path.GetFullPath(process.MainModule!.FileName);
if (!string.Equals(mainProcessPath, Basics.ExecutablePath, StringComparison.OrdinalIgnoreCase))
{
Context.Error("来源验证失败,正在退出");
return;
}
// 连接管道
var pipeName = _GetPromotePipeName(process.Id);
var pipe = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut);
pipe.Connect(10000);
Context.Info("已连接,开始通信");
var reader = new StreamReader(pipe);
var writer = new StreamWriter(pipe);
while (true)
{
var command = reader.ReadLine();
if (string.IsNullOrEmpty(command))
{
Context.Info("管道已关闭,正在退出");
break;
}
Context.Debug($"正在执行: {_ShortenString(command)}");
var result = Operate(command) ?? OperationErrEmpty;
Context.Trace($"返回结果: {_ShortenString(result)}");
writer.WriteLine(result.Replace("\r\n", " ").Replace('\n', ' ').Replace('\r', ' '));
writer.Flush();
Context.Trace("返回成功");
}
}
private static readonly AutoResetEvent _ActivateEvent = new(false);
// 主进程: 管道连接回调
private static bool _PromotePipeCallback(StreamReader reader, StreamWriter writer, Process? client)
{
while (IsPromoteProcessRunning)
{
if (!_PendingOperations.TryDequeue(out var operation))
{
_ActivateEvent.WaitOne();
continue;
}
var command = operation.Command.Replace("\r\n", " ").Replace('\n', ' ').Replace('\r', ' ');
var commandLog = operation.DetailLog ? command : _ShortenString(command);
Context.Debug($"正在执行: {commandLog}");
writer.WriteLine(command);
writer.Flush();
var result = reader.ReadLine();
if (result is null)
{
Context.Warn("管道输入流已结束");
break;
}
var resultLog = operation.DetailLog ? result : _ShortenString(result);
Context.Trace($"执行结果: {resultLog}");
if (result == OperationErrEmpty) result = null;
operation.Callback?.Invoke(result);
}
return false;
}
// 主进程: 初始化提权后台服务
private static bool _StartPromoteProcess()
{
// 启动提权进程
_promoteProcess = ProcessInterop.Start(
Basics.ExecutablePath, $"promote {Basics.CurrentProcessId}", true);
if (_promoteProcess is null)
{
Context.Warn("提权进程启动失败");
return false;
}
_promoteProcess.Exited += (_, _) => _promoteProcess = null;
// 启动提权通信管道服务端
_promotePipeServer ??= PipeComm.StartPipeServer(
"Promote", _GetPromotePipeName(Basics.CurrentProcessId), _PromotePipeCallback,
() => _promotePipeServer = null, true, [_promoteProcess.Id]);
return true;
}
/// <summary>
/// 向等待区添加操作。
/// </summary>
/// <param name="command">操作命令</param>
/// <param name="callback">结果返回后的回调</param>
/// <param name="detailLog">指定是否打印详细日志,若为 <c>false</c>,则日志仅保留前 40 或 15 字符(取决于是否为调试构建)</param>
public static void Append(string command, Action<string?>? callback = null, bool detailLog = true)
{
_PendingOperations.Enqueue(new PromoteOperation(command, callback, detailLog));
}
/// <summary>
/// 尝试启动提权进程并开始执行操作。
/// </summary>
/// <returns>是否成功开始执行,若提权进程启动失败则为 <c>false</c></returns>
public static bool Activate()
{
if (!IsPromoteProcessRunning && !_StartPromoteProcess())
{
_PendingOperations.Clear();
return false;
}
_ActivateEvent.Set();
return true;
}
private static readonly Dictionary<string, Process> _RunningProcesses = new();
// name: kill
// arg: process-id [timeout]
// return: kill result (false over timeout)
[PromoteOperation("kill")]
public static string? KillProcess(string? arg)
{
if (arg is null) return OperationErrInvalidArgument;
var split = arg.Split(' ');
if (!_RunningProcesses.TryGetValue(split[0], out var process)) return null;
process.Kill();
if (split.Length > 1)
{
int.TryParse(split[1], out var timeout);
return process.WaitForExit(timeout).ToString();
}
process.WaitForExit();
return true.ToString();
}
// name: start
// arg: path\to\executable[.] ; arguments
// return: process id
[PromoteOperation("start")]
public static string? StartProcess(string? arg)
{
if (arg is null) return OperationErrInvalidArgument;
var split = arg.Split([" ; "], 2, StringSplitOptions.RemoveEmptyEntries);
var createNoWindow = false;
if (split[0].EndsWith('.'))
{
split[0] = split[0][..^1];
createNoWindow = true;
}
var psi = new ProcessStartInfo(split[0]);
if (createNoWindow)
{
psi.CreateNoWindow = true;
psi.UseShellExecute = false;
psi.RedirectStandardInput = true;
psi.RedirectStandardOutput = true;
psi.RedirectStandardError = true;
}
if (split.Length > 1) psi.Arguments = split[1];
return _StartProcessWithInfo(psi);
}
// name: start-json
// arg: {...}
// return: process id
private static string? _StartProcessWithInfo(ProcessStartInfo? info)
{
if (info is null) return OperationErrInvalidArgument;
var process = Process.Start(info);
if (process is null) return null;
var id = process.Id.ToString();
process.Exited += (_, _) => _RunningProcesses.Remove(id);
_RunningProcesses[id] = process;
return id;
}
[DependencyInjectionPoint("promote", false)]
private static void _CollectOperationFunction(PromoteOperationFunction operation, string name)
=> AddOperationFunction(name, operation);
[LifecycleStart]
private static void _Start()
{
var args = Basics.CommandLineArguments;
if (args is ["promote", _])
{
Context.Info("当前进程为提权进程");
IsCurrentProcessPromoted = true;
// 预定义操作
Context.Info("正在加载提权操作");
_CollectOperationFunction_InvokeInjection_Promote();
AddJsonOperationFunction<ProcessStartInfo>("start-json", _StartProcessWithInfo);
// 结束生命周期管理,启动提权操作线程
Lifecycle.PendingLogFileName = "LastPending_Promote.log";
LogWrapper.OnLog += (level, msg, module, ex) => Context.CustomLog($"[{module}] {msg}", ex, level);
Context.Info("已接管通用日志");
Context.Info("正在启动服务线程");
new Thread(() => _PerformAsPromoteProcess(args[1])) { Name = "Promote" }.Start();
Context.RequestStopLoading();
Context.DeclareStopped();
}
else
{
Context.Info("当前进程为主进程");
IsCurrentProcessPromoted = false;
// TODO 提权进程自动启动
}
}
[LifecycleStop]
private static void _Stop()
{
if (_promotePipeServer is not null)
{
Context.Debug("正在结束提权管道服务");
_promotePipeServer.Dispose();
}
if (_promoteProcess is not null && !_promoteProcess.WaitForExit(3000))
{
Context.Debug("正在结束提权进程");
ProcessInterop.Kill(_promoteProcess, 0, true);
}
}
}
@@ -0,0 +1,9 @@
namespace PCL.Core.App.Essentials;
/// <summary>
/// RPC 函数<br/>
/// 接收参数并返回响应内容
/// </summary>
/// <param name="argument">参数</param>
/// <returns>响应内容</returns>
public delegate RpcResponse RpcFunction(string? argument, string? content, bool indent);
@@ -0,0 +1,58 @@
using System;
namespace PCL.Core.App.Essentials;
public class RpcPropertyOperationFailedException : Exception;
/// <summary>
/// RPC 属性<br/>
/// 大多数时候只需要使用构造方法,其他结构保留供内部使用
/// </summary>
public class RpcProperty
{
public delegate void GetValueDelegate(out string? outValue);
public event GetValueDelegate GetValue;
public delegate void SetValueDelegate(string? value, ref bool success);
public event SetValueDelegate? SetValue;
public readonly string Name;
public readonly bool Settable = true;
public string? Value
{
get
{
GetValue.Invoke(out var value);
return value;
}
set
{
var success = true;
SetValue?.Invoke(value, ref success);
if (!success)
throw new RpcPropertyOperationFailedException();
}
}
/// <param name="name">属性名称</param>
/// <param name="onGetValue">默认的 <c>GetValue</c> 回调</param>
/// <param name="onSetValue">默认的 <c>SetValue</c> 回调</param>
/// <param name="settable">指定该属性是否可更改,若该值为 <c>false</c> 的同时 <paramref name="onSetValue"/> 为 <c>null</c>,则该属性成为只读属性</param>
public RpcProperty(string name, Func<string?> onGetValue, Action<string?>? onSetValue = null, bool settable = false)
{
Name = name;
GetValue += (out outValue) => { outValue = onGetValue(); };
if (onSetValue is not null)
{
SetValue += (value, ref _) => { onSetValue(value); };
}
else if (!settable)
{
Settable = false;
SetValue += (_, ref success) => { success = false; };
}
}
}
@@ -0,0 +1,73 @@
using System;
using System.IO;
namespace PCL.Core.App.Essentials;
public enum RpcResponseStatus
{
Success,
Failure,
Err
}
public enum RpcResponseType
{
Empty,
Text,
Json,
Base64
}
/// <summary>
/// Pipe RPC 响应
/// </summary>
public class RpcResponse
{
public RpcResponseStatus Status { get; }
public RpcResponseType Type { get; }
public string? Name { get; }
public string? Content { get; }
public RpcResponse(RpcResponseStatus status, RpcResponseType type = RpcResponseType.Empty, string? content = null,
string? name = null)
{
if (content is not null && type == RpcResponseType.Empty)
throw new ArgumentException("Empty response with non-null content");
Status = status;
Type = type;
Content = content;
Name = name;
}
// STATUS type [name]
// [content]
public void Response(StreamWriter writer)
{
var nameArea = Name is null ? "" : $" {Name}";
writer.WriteLine($"{Status.ToString().ToUpperInvariant()} {Type.ToString().ToLowerInvariant()}{nameArea}");
if (Content is not null)
writer.WriteLine(Content);
}
public static readonly RpcResponse EmptySuccess = new RpcResponse(RpcResponseStatus.Success);
public static readonly RpcResponse EmptyFailure = new RpcResponse(RpcResponseStatus.Failure);
public static RpcResponse Err(string content, string? name = null)
{
return new RpcResponse(RpcResponseStatus.Err, RpcResponseType.Text, content, name);
}
public static RpcResponse Success(RpcResponseType type, string content, string? name = null)
{
return new RpcResponse(RpcResponseStatus.Success, type, content, name);
}
public static RpcResponse Failure(RpcResponseType type, string content, string? name = null)
{
return new RpcResponse(RpcResponseStatus.Failure, type, content, name);
}
}

Some files were not shown because too many files have changed in this diff Show More