二次开发指南
本文档将介绍如何对 CCXC Engine 的后端服务 ccxc-backend
进行二次开发和定制。
何时需要二次开发?
作为 Puzzle Hunt 很难避免二次开发。例如你可能需要:
- 实现定制的进度推进逻辑
- 添加特殊的题目交互
- 实现特殊的分区、题目、剧情的展示
- 更换已有的模块(如邮件系统) 等
关于二次开发
CCXC Engine 框架提供了完整的基础设施,然而 Puzzle Hunt 的特殊性使得需要开发独特的机制几乎是必然的选择。因此我们在开发框架时就考虑如何方便二次开发。
当前的版本需要修改的文件还比较分散,今后我们会继续整合,将自定义逻辑进一步抽出。
开发环境准备
在开始二次开发之前,请确保你已经:
- 完成了开发环境搭建
- 熟悉 C# 和 .NET 开发
- 了解 Puzzle Hunt 的基本概念和流程
- 准备好你的创意想法!
代码结构
让我们先来了解一下 ccxc-backend
的整体架构:
.
├─ccxc-backend
│ ├─Config # 配置文件和配置加载器
│ ├─Controllers # HTTP API 控制器
│ │ ├─Admin # 管理后台相关接口
│ │ ├─Announcements # 公告相关接口
│ │ ├─Game # 比赛核心逻辑接口
│ │ ├─Groups # 队伍管理接口
│ │ ├─Invites # 邀请系统接口
│ │ ├─Mail # 邮件服务接口
│ │ ├─System # 系统全局接口
│ │ └─Users # 用户管理接口
│ ├─DataModels # 数据库实体模型
│ ├─DataServices # 数据访问层服务
│ └─Functions # 业务逻辑辅助函数
├─Ccxc.Core.DbOrm # 底层库:数据库 ORM 封装
├─Ccxc.Core.HttpServer # 底层库:HTTP 服务器框架
├─Ccxc.Core.Plugins # 底层库:插件开发工具包
└─Ccxc.Core.Utils # 底层库:通用工具集
可以很方便的找到某个功能应该在哪里:
- Controllers 层处理 HTTP 请求和响应
- DataModels 层定义数据结构
- DataServices 层封装数据库操作
- Functions 层包含可复用的业务逻辑
存档
SaveData 结构
CCXC Engine 使用一个名为 SaveData
的核心数据结构来追踪每支队伍的进度状态。这个类位于 ccxc-backend/DataModels/progress.cs
:
public class SaveData
{
/// <summary>
/// 已完成的题目(pid)
/// </summary>
public HashSet<int> FinishedProblems { get; set; } = new HashSet<int>();
/// <summary>
/// 已解锁的小题(pid)
/// </summary>
public HashSet<int> UnlockedProblems { get; set; } = new HashSet<int>();
/// <summary>
/// 已完成的分区(pgid)
/// </summary>
public HashSet<int> FinishedGroups { get; set; } = new HashSet<int>();
/// <summary>
/// 已解锁的分区(pgid)
/// </summary>
public HashSet<int> UnlockedGroups { get; set; } = new HashSet<int>();
/// <summary>
/// 题目解锁时间(pid -> 解锁时间)
/// </summary>
public Dictionary<int, DateTime> ProblemUnlockTime { get; set; } = new Dictionary<int, DateTime>();
/// <summary>
/// 各题目的答案提交次数(pid -> 次数)
/// </summary>
public Dictionary<int, int> ProblemAnswerSubmissionsCount { get; set; } = new Dictionary<int, int>();
/// <summary>
/// 已购买的额外题目回答次数(pid -> 次数)
/// </summary>
public Dictionary<int, int> AdditionalProblemAttemptsCount { get; set; } = new Dictionary<int, int>();
/// <summary>
/// 已兑换过的提示(pid -> (提示id))
/// </summary>
public Dictionary<int, HashSet<int>> OpenedHints { get; set; } = new Dictionary<int, HashSet<int>>();
/// <summary>
/// 题目当前状态(pid -> (状态名 -> 状态值))
/// </summary>
public Dictionary<int, Dictionary<string, string>> ProblemStatus { get; set; } = new Dictionary<int, Dictionary<string, string>>();
}
存档设计思路
存档系统采用了"一切皆状态"的设计理念。ProblemStatus
字典允许你为每道题目存储任意的键值对状态,这为复杂的题目交互提供了无限可能。
数值配置中心
CCXC Engine 提供了一个基于 Redis 的数值配置中心,将一些需要快速调整和配置的参数存储其中。
所有的配置项都定义在 ccxc-backend/Functions/RedisNumberCenter.cs
中:
以“初始信用点” InitialPowerPoint
为例
/// <summary>
/// 初始信用点
/// </summary>
public static int InitialPowerPoint
{
get
{
return GetInt("initial_power_point").GetAwaiter().GetResult();
}
set
{
SetInt("initial_power_point", value).GetAwaiter().GetResult();
}
}
常用配置项说明
配置项 | 作用 | 默认值建议 |
---|---|---|
InitialPowerPoint | 初始信用点数量 | 100 |
PowerIncreaseRate | 信用点增长速率(每分钟) | 10 |
InitialGroupCount | 开局解锁的分区数量 | 1 |
UnlockTipFunctionAfter | 提示功能解锁延迟(分钟) | 30 |
添加自定义数值
如果你需要添加新的数值配置项,按照以下步骤:
- 在
RedisNumberCenter.cs
中添加属性定义 - 在
ccxc-backend/Controllers/Admin/DynamicNumericalController.cs
对应的管理 API 中添加这个配置项的读写逻辑。 - 在管理后台界面中添加配置项显示
- 在你需要的地方使用
RedisNumberCenter.<配置项名称>
引用它。
注意
修改配置项时,记得同步更新管理后台的读写 API,否则管理员将无法在后台修改这些数值。
解锁流程定制
查看文件 ccxc-backend/Controllers/Game/GameProgressExtend.cs
。这个文件包含许多解锁流程相关的内容,这是二次开发经常会修改的部分。
初始存档生成
每当新队伍首次进入游戏时,系统会调用 NewSaveData
函数生成初始存档。
在这个函数中:
- 我们首先为当前用户初始化了一份新的
SaveData
对象。 - 然后查看数值中心的初始分区数量。
- 对于每个初始分区,我们都查看了数值中心中关于此分区初始解锁的题目数量。
- 对于每个初始分区,我们按照PID顺序找到对应题目数量的小题,标记它们解锁。
这一逻辑可以适用于大多数情况,但是有时你仍然需要定制它。
Meta 和 Final Meta 配置
为了开发方便,我们没有通过读取数据库或者配置文件的方式,而是直接在源码中指定了 MetaMeta 和 Final meta 的 PID 。
这些条件在判题函数中推进进度时会被引用。
// 返回 Meta Meta 的 PID
public static int GetMMPid => 58;
// 返回 Final Meta 的 PID
public static int GetFMPid => 59;
以及 IsFinishedFinalMeta
和 IsFMOpen
函数。
在编写自己的 Puzzle Hunt 时,你几乎必须修改它们。
剧情查看
在 GetOpenPuzzleArticleId
函数中返回了当前允许查看哪些“题目文章”,而 CanReadPuzzleArticle
函数则是判断给出的题目文章的 Key 现在是否可查看。
默认的逻辑是:
g1-prologue
没有任何限制g${gpid}-prologue
在gpid
分区解锁时可见。g${pgid}-end
在gpid
分区完成时(解出分区 Meta 时)可见。main-open
在第 1 区完成时可见。finalend
在 Final meta 完成时可见。
题目解锁
在 UnlockGroup
UnlockNextPuzzle
和 UnlockSinglePuzzle
中实现了解锁逻辑。
在需要解锁分区时,会调用
UnlockGroup
函数。在向SaveData.UnlockedGroups
标记分区解锁的同时,也会一并解锁该分区的初始小题。在推进下一小题的解锁进度时,会调用
UnlockNextPuzzle
函数。该函数会找出当前分区按 PID 顺序的下一小题,并调用UnlockSinglePuzzle
解锁。当需要解锁指定的小题时,会调用
UnlockSinglePuzzle
函数。
你可以修改这些函数实现特定的解锁逻辑。
判题系统
判题流程的代码实现在 ccxc-backend/Controllers/Game/OperateController.cs
中。这里有两个函数:
- CheckAnswer API
- 进度推进
判题流程概览
进度推进定制
进度推进的核心逻辑在 PushNextHelper
函数中。这是你最可能需要修改的地方:
public static async Task<(int code, int answerStatus, int extendFlag, string message, string location)> PushNextHelper(
progress progress, Progress progressDb,
puzzle puzzleItem, List<puzzle> puzzleList, answer_log answerLog, AnswerLog answerLogDb,
int uid, int gid, string username,
DateTime now, string extraMessage
)
{
// 在判断回答正确后所有的逻辑都要放在这里
}
题目展示逻辑
题目展示的核心逻辑在 ccxc-backend/Controllers/Game/GameController.cs
中。这里包含了几个关键的 API:
主页信息 API
GetMainInfo
方法决定了玩家在主页看到的内容:
[HttpHandler("POST", "/play/get-main-info")]
public async Task GetMainInfo(Request request, Response response)
{
// 根据用户进度生成导航栏和可见区域
var navBar = new List<NavbarItem>
{
new NavbarItem
{
title = Config.SystemConfigLoader.Config.ProjectName,
path = "/main",
}
};
// 根据进度添加更多导航项
if (progressData.IsFinishedFinalMeta())
{
navBar.Add(new NavbarItem
{
title = "尾声",
path = "/article/finalend"
});
}
}
分区信息 API
GetPuzzleInfo
方法控制每个分区内显示哪些题目:
// 将分区已解锁的题目插入题目列表
var puzzleList = new List<DetailBasicInfo>();
foreach (var puzzle in puzzleBasicList)
{
var isUnlocked = progressData.UnlockedProblems.Contains(puzzle.pid);
var isPgid3Meta = (pgid == 3 && puzzle.answer_type == 1); // 特殊逻辑示例
if (isUnlocked && !isPgid3Meta)
{
puzzleList.Add(new DetailBasicInfo
{
pid = puzzle.pid,
title = puzzle.title,
puzzle_type = puzzle.answer_type,
is_finished = progressData.FinishedProblems.Contains(puzzle.pid) ? 1 : 0,
answer = GetShowedAnswer(puzzle.pid, puzzle.answer, progressData),
is_unlocked = isUnlocked ? 1 : 0,
extend_data = puzzle.extend_data
});
}
}
动态答案显示
你可以实现动态的答案显示逻辑。比如某些题目解答后不显示真实答案,而是显示特殊文本:
string GetShowedAnswer(int pid, string answer, SaveData progressData)
{
if (!progressData.FinishedProblems.Contains(pid))
{
return null;
}
if (progressData.ProblemStatus.ContainsKey(pid))
{
if (progressData.ProblemStatus[pid].ContainsKey("__$$ShowAnswer"))
{
return progressData.ProblemStatus[pid]["__$$ShowAnswer"];
}
}
return answer;
}
扩展功能开发
邮件系统定制
邮件发送功能在 ccxc-backend/Functions/EmailSender.cs
中实现。默认使用阿里云邮件服务,你可以根据需要替换为其他服务商:
// 示例:集成 SendGrid
public static async Task SendActivationEmailViaSendGrid(string toEmail, string activationLink)
{
var client = new SendGridClient("your-api-key");
var from = new EmailAddress("noreply@yoursite.com", "Your Puzzle Hunt");
var to = new EmailAddress(toEmail);
var msg = MailHelper.CreateSingleEmail(from, to, "激活你的账号",
$"请点击链接激活:{activationLink}");
await client.SendEmailAsync(msg);
}
SSO 安全配置
如果你使用单点登录功能,需要在 ccxc-backend/Controllers/Users/SsoController.cs
中配置可信域名:
if (host.EndsWith("yoursite.com", StringComparison.OrdinalIgnoreCase) ||
host.EndsWith("yourdomain.net", StringComparison.OrdinalIgnoreCase))
{
await response.OK();
return;
}
二次开发建议
在二次开发的过程中请经常进行测试。思考用户进行何种操作之后会到达这里的代码,并通过调试器观察代码执行是否符合预期。
调试
- 使用断点调试:在关键的判题和解锁逻辑处设置断点
- 日志记录:在关键步骤添加详细的日志输出
- 存档检查:定期检查 SaveData 的状态是否符合预期
保持兼容
在修改原有逻辑,特别是 SaveData
时,需要注意与原有逻辑的兼容。
🚀
需要帮助?
如果在二次开发过程中遇到问题,欢迎查看项目的 GitHub 仓库,或者联系开发团队获取支持。