diff --git a/.github/workflows/sphinx.yml b/.github/workflows/sphinx.yml
new file mode 100644
index 00000000..5db967a3
--- /dev/null
+++ b/.github/workflows/sphinx.yml
@@ -0,0 +1,19 @@
+name: Deploy Sphinx documentation to Pages
+
+# Runs on pushes targeting the default branch
+on:
+ push:
+ branches: [master]
+
+jobs:
+ pages:
+ runs-on: ubuntu-20.04
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ permissions:
+ pages: write
+ id-token: write
+ steps:
+ - id: deployment
+ uses: sphinx-notes/pages@v3
diff --git a/doc/dev/compile.md b/doc/dev/compile.md
deleted file mode 100644
index 0f0e69b3..00000000
--- a/doc/dev/compile.md
+++ /dev/null
@@ -1,134 +0,0 @@
-# 编译 FreeKill
-
-> [dev](./index.md) > 编译
-
-___
-
-## 全平台通用步骤
-
-FreeKill采用最新的Qt进行构建,因此需要先安装Qt6的开发环境。
-
-无论是Win还是Linux,都建议用[Qt官方的下载器](https://download.qt.io/official_releases/online_installers/)进行安装。当然了,在一些软件更新很频繁的Linux发行版里面,可能已经能从包管理器安装Qt6,对此后文细说。这个环节介绍用Qt安装器安装的步骤。
-
-Qt安装的流程不赘述。为了编译FreeKill,至少需要安装以下的组件:
-- Qt 6: MinGW 11.2.0 64-bit (不支持MSVC)
-- Qt 6: Qt5 Compat
-- Qt 6: Shader Tools (为了使用GraphicalEffects)
-- Qt 6: Multimedia
-- QtCreator(这个是安装器强制要你安装的)
-- CMake、Ninja
-- OpenSSL 1.1.1
-
-接下来根据平台的不同,步骤也稍有区别。
-
-___
-
-## Windows
-
-从网络上下载swig、flex、bison。swig在其官网可以下载,flex和bison可在[github](https://github.com/lexxmark/winflexbison/releases/)或者SourceForge下载。
-
-全都下载完成之后,将含有swig.exe、win_flex.exe、win_bison.exe的文件夹全部都设置到Path环境变量里面去。
-
-接下来使用QtCreator打开项目,然后尝试编译。
-
-这时遇到cmake报错:OpenSSL:Crypto not found. 这是因为我们还没有告诉编译器OpenSSL的位置,点左侧“项目”,查看构建选项,在CMake的Initial Configuration中,点击添加按钮,新增String型环境变量OPENSSL_ROOT_DIR,将其值设为跟Qt一同安装的OpenSSL的位置(如C:/Qt/Tools/OpenSSL/Win_x64)。然后点下方的Re-configure with Initial Parameters,这样就能正常编译了。
-
-运行的话,在Qt Creator的项目选项->运行中,先将工作目录改为项目所在的目录(git仓库的目录)。然后先将编译好了的FreeKill.exe放到项目目录中,在目录下打开CMD,执行windeployqt FreeKill.exe。调整目录下的dll文件直到能运行起来为止,之后就可以在Qt Creator中正常运行和调试了。
-
-___
-
-## Linux
-
-通过包管理器安装一些额外软件包方可编译。
-
-Debian一家子:
-
-```sh
-$ sudo apt install liblua5.4-dev libsqlite3-dev libssl-dev swig flex bison
-```
-
-Arch Linux:
-
-```sh
-$ sudo pacman -Sy lua sqlite swig openssl flex bison
-```
-
-然后使用配置好的QtCreator环境即可编译。
-
-如果你不想用Qt安装器的话,可以用包管理器安装依赖,下面仅举例Arch:
-
-```sh
-$ sudo pacman -S qt6-base qt6-declarative qt6-5compat qt6-multimedia
-$ sudo pacman -S cmake lua sqlite swig openssl swig flex bison
-```
-
-然后可以用命令行编译:
-
-```sh
-$ mkdir build && cd build
-$ cmake ..
-$ make -j8
-```
-
-___
-
-## Linux服务器
-
-一般来说Linux服务器的包管理器都没新到提供Qt6下载,这个时候想编译服务端的话,需要在尽可能安装完Qt5环境的情况下,对FreeKill的Qt版本降一下等级。
-
-首先将根目录和src下面的两个CMakeLists.txt的Qt6都改成Qt5,然后试图进行编译。
-
-编译器会报告大概不超过10处错误,将它们修改成Qt5可以接受的形式就行了。
-
-___
-
-## MacOS
-
-大致与Windows类似,但尚且缺少确切的方案。
-
-___
-
-## 编译安卓版
-
-用Qt安装器装好Android库,然后配置一下android-sdk就能编译了。
-
-(Qt 6.4的刘海屏bug,手动往QActivity.java的onCreate函数追加如下代码即可实现完全全屏。这里做个笔记方便复制粘贴,等Qt修了再说)
-
-```java
-getWindow().addFlags(LayoutParams.FLAG_FULLSCREEN);
-if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
- getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
-}
-if (Build.VERSION.SDK_INT > 28) {
- WindowManager.LayoutParams lp = getWindow().getAttributes();
- lp.layoutInDisplayCutoutMode = LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
- getWindow().setAttributes(lp);
-}
-```
-
-___
-
-## WASM下编译
-
-WASM大概就是能在浏览器中跑C++。编译用Qt Creator即可。
-
-### 1. 条件与局限性
-
-如果程序运行在网页上的话,那么理应只有客户端,然后提供网页的服务器上自然也运行着一个后端服务器。所以说在编译时应该舍弃掉服务端相关的代码。因此依赖库就不再需要sqlite3。
-
-总之是编译个纯客户端的FK。
-
-### 2. 编译OpenSSL
-
-进入OpenSSL的src目录,然后
-
- $ ./config -no-asm -no-engine -no-dso
- $ emmake make -j8 build_generated libssl.a libcrypto.a
-
-编译Lua的话直接emmake make就行了,总之库已经传到仓库了。
-
-### 3. 部署资源文件
-
-由于CMake中`file(GLOB_RECURSE)`所带来的缺陷,每当资源文件变动时,需要手动更新。
-
-把构建目录中的.rcc目录删掉然后重新执行CMake->make即可。每次编译资源文件总要消耗相当多的时间。
diff --git a/doc/dev/index.md b/doc/dev/index.md
deleted file mode 100644
index 829007fb..00000000
--- a/doc/dev/index.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# FreeKill 开发文档
-
-> dev
-
-___
-
-FreeKill采用Qt框架提供底层支持,在上层使用lua语言开发。在UI方面使用的是Qt Quick。
-
-- [编译](./compile.md)
-- [通信](./protocol.md)
-- [游戏逻辑](./gamelogic.md)
-- [数据库](./database.md)
-- [UI](./ui.md)
-- [包管理](./package.md)
-- [AI](./ai.md)
diff --git a/doc/diy/01-env.md b/doc/diy/01-env.md
deleted file mode 100644
index 7129c121..00000000
--- a/doc/diy/01-env.md
+++ /dev/null
@@ -1,227 +0,0 @@
-# Fk DIY - 环境搭建
-
-> [diy](./index.md) > 环境搭建
-
-* [DIY总览](#diy总览)
-* [环境搭建](#环境搭建)
- * [Fk](#fk)
- * [代码编辑器](#代码编辑器)
- * [git](#git)
- * [安装git](#安装git)
-* [新增mod](#新增mod)
-* [发布mod](#发布mod)
- * [将终端切换为Git Bash](#将终端切换为git-bash)
- * [配置ssh key](#配置ssh-key)
- * [新建git仓库](#新建git仓库)
- * [让他人安装并游玩你的mod](#让他人安装并游玩你的mod)
- * [更新mod](#更新mod)
-
-___
-
-## DIY总览
-
-正如[项目README](../../README.md)所言,FreeKill“试图打造一个最适合diy玩家游玩的民间三国杀”。即便是最开始游戏功能尚未完善,FreeKill也已经具备了对DIY的支持。所有拓展包都列在packages/文件夹下,感兴趣者可以自行查看。
-
-欲为FreeKill进行DIY,需要使用的编程语言为Lua。若您对Lua语言完全不熟悉,推荐去[菜鸟教程](https://www.runoob.com/lua/lua-tutorial.html)速通一遍基本语法。剩下的就基本是在实践中慢慢领会了。
-
-FreeKill本体中自带有标准包和标准卡牌包,可作为DIY时候的例子。事实上,其他DIY包也是像这样子组织的。
-
-接下来讲述如何配置环境。
-
-___
-
-## 环境搭建
-
-### Fk
-
-Fk是游戏本身,也是拓展包运行的平台。事实上这份文档应该与Fk一同发布的,如果您正在阅读这份文档,那么您理应已经接收到了Fk本身。
-
-### 代码编辑器
-
-代码编辑器任选一种即可,但一定要确保以下几点:
-
-- 至少要是一款**代码**编辑器,要有语法高亮功能
-- 需要有EmmyLua插件的支持
-- 需要默认UTF-8格式保存代码文件
-
-> EmmyLua是一种特别的Lua注释方式,可以为本来弱类型的Lua语言提供类型支持,这对于像FreeKill这种稍有规模的Lua项目是十分必要的。目前能提供开箱即用的EmmyLua插件编辑器主要有IntelliJ IDEA和Visual Studio Code。EmmyLua也能以LSP的方式运行,因此支持LSP的编辑器(这种就多了,比如vim, sublime)也能符合条件。
-
-编辑器的具体安装以及插件配置不在此赘述。
-
-> 出于易用性和免费的考虑,推荐用VSCode进行拓展。下文将以VSCode为编辑器进行进一步说明。
-
-### git
-
-git就不必多介绍了吧,这里说说为什么需要配置git。这是因为在Fk中,拓展包拥有在线安装/在线更新的功能,这种功能都是依托于git进行的,因此如果你打算将自己的拓展包发布出去的话,就需要将其创建git仓库,并托管到git托管网站去。
-
-> 考虑到国内绝大部分人的访问速度,综合国内几家git托管平台,建议使用gitee。
-
-大多数人可能从未用过git,并且git上手的门槛并不低,因此以下会对涉及git的操作进行详尽的解说。
-
-#### 安装git
-
-前往[官网](https://git-scm.com/download/win)下载git,下载64-bit Git for Windows Setup。这样应该会为您下载一个exe安装包。
-
-考虑到官网的下载链接实际上指向github,而且可能连官网的都进不去,所以也考虑[从清华源下载Git](https://mirrors.tuna.tsinghua.edu.cn/github-release/git-for-windows/git/)。
-
-欲验证安装是否完成,可以按下Win+R -> cmd弹出命令行窗口,输入git命令,如果出来一长串英文说明安装成功了。
-
-___
-
-## 新增mod
-
-这只是新增mod的一个例子。当然了,以后有啥要做的实例也会继续用这个拓展包的。
-
-首先前往packages下,新建名为fk_study的文件夹。
-
-再在fk_study下新建init.lua文件,写入以下内容:
-
-```lua
-local extension = Package("fk_study")
-
-Fk:loadTranslationTable{
- ["fk_study"] = "fk学习包",
-}
-
-return { extension }
-```
-
-保存退出,打开Fk,进武将一览。你现在应该能在武将一览里面看到“fk学习包”了,但也仅此而已了,毕竟这还只是个空壳包而已。
-
-至此我们已经创建了最为简单的mod。mod的文件结构如下:
-
- fk_study
- └── init.lua
-
-___
-
-## 发布mod
-
-一种最常见的发布mod方式是把mod打包成zip,发到公共平台上供玩家下载。这种办法虽然可行,但并不是fk推荐的做法。
-
-> 以下介绍的其实就是新建仓库并推送到gitee的办法,熟悉git者请跳过。
-
-下面着重介绍用git发布mod的办法。使用git进行发布的话,就可以让用户体验在线安装、在线更新等便捷之处。
-
-以下假设你使用vscode进行代码编辑。你是先用vscode打开了整个FreeKill文件夹,再在其中新建文件夹和文件、然后进行编辑的。
-
-菜单栏 -> 终端 -> 新建终端。我们接下来的工作都在终端中完成。
-
-### 将终端切换为Git Bash
-
-启动终端后,终端的内容大概是:
-
-```plain
-Mincrosoft Windows 10 [版本号啥的]
-xxxxxxxx 保留所有权利。
-
-C:\FreeKill>
-```
-
-这个是Windows自带的cmd,我们不使用这个,而是去用git bash。此时终端上面应该有这么一条:
-
-```plain
-问题 输出 调试控制台 _终端_ cmd + v 分屏 删除
- 注意这个加号
-```
-
-这时候点击加号右边那个下拉箭头,选择"Git Bash"。这样就成功的切换到了git bash中,终端看起来应该像这样:
-
-```plain
-xxx@xxxxx MINGW64 /c/FreeKill
-$
-```
-
-### 配置ssh key
-
-你应该已经注册好了自己的gitee账号。首先在Git bash中输入这些命令(#号后面的是命令注释,不用照搬;命令开头的\$符号是模拟shell的界面,不要输入进去):
-
-```sh
-$ cd ~/.ssh
-$ ssh-keygen -t rsa -C "你注册用的邮箱地址" # 换成自己真正的邮箱
- # 出来一堆东西,一路点回车就是了
-$ cat id_rsa.pub
- # 出来一堆乱七八糟的东西:ssh-rsa <一大堆乱七八糟的内容> <你的邮箱>
-$ cd -
-```
-
-在cat id_rsa.pub中,出来的那一堆以ssh-rsa的输出,就是这里要用到的“公钥”。然后在gitee中:
-
-1. 点右上角你的头像,点账号设置
-2. 点左侧栏中 安全设置 - SSH公钥
-3. 此时弹出公钥添加界面,标题任选,下面公钥那一栏中,将刚刚生成的公钥复制粘贴上去
-4. 点确定
-
-这样就配置好了ssh公钥。进行验证,在bash中使用命令:
-
-```sh
-$ ssh -T git@gitee.com
-Hi xxxx! You've successfully authenticated, but GITEE.COM does not provide shell access.
-```
-
-输出像Hi xxx!这样的信息,就说明配置成功了。否则需要进一步检查自己的操作,上网查一下吧。
-
-### 新建git仓库
-
-现在终端的工作目录应该还是FreeKill根目录,我们先切换到mod的目录去,然后再在shell中进行一系列操作。
-
-```sh
-$ cd packages/fk_study
-$ git init # 创建新的空仓库
-$ git add . # 将文件夹中所有的文件都加入暂存区
-$ git commit -m "init" # 提交目前所有的文件,这样文件就正式存在于仓库里面了
-作者身份未知
-*** 请告诉我您是谁。
-运行
- git config --global user.email "you@example.com"
- git config --global user.name "Your Name"
-
-来设置您账号的缺省身份标识。如果仅在本仓库设置身份标识,则省略 --global 参数。
-```
-
-看来我们初次安装Git,Git还不知道我们的身份呢,不过git已经告诉了配置所需的命令了。运行前一条命令告知自己的名字,运行后一条命令告知自己的邮箱。如此就OK了,然后再commit一次。
-
-然后在gitee中也新建一个仓库,取名为fk_study。接下来回到终端里面:
-
-```sh
-$ git remote add origin git@gitee.com:xxx/fk_study # 其中这个xxx是你的用户名
-$ git push -u origin master
-```
-
-OK了,刷新你新建的那个仓库的页面,可以看到里面已经有init.lua了。此时距离发布mod只有最后一步,那就是把仓库设置为开源。请自行在gitee中设置吧。
-
-### 让他人安装并游玩你的mod
-
-注意到Fk初始界面里面的“管理拓展包”了不?这个就是让你安装、删除、更新拓展包用的。在那个页面里面有个输入框,在浏览器中复制仓库的地址(比如https://gitee.com/xxx/fk_study/ ),粘贴到输入框,然后单击“从URL安装”即可安装拓展包了。
-
-### 更新mod
-
-现在mod要发生更新了,更新内容为一个武将。先在init.lua中新增武将吧。
-
-```lua
-local study_sunce = General(extension, "study_sunce", "wu", 4)
-Fk:loadTranslationTable{
- ["study_sunce"] = "孙伯符",
-}
-```
-
-保存,此时注意vscode左侧栏变成了:
-
- v fk_study
- └── init.lua M
-
-init.lua后面出现了“M”,并且文件名字也变成了黄色,这表示这个文件已经被修改过了,接下来我们把修改文件提交到仓库中:
-
-```sh
-$ git add . # 将当前目录下的文件暂存
-$ git commit -m "add general sunce" # 提交更改,提交说明为add general sunce
-$ git push # “推”到远端,也就是把本地的更新传给远端
-```
-
-不喜欢用命令行的话,也可以用vscode自带的git支持完成这些操作,这里就不赘述了。做完git push后,实际上就已经完成更新了,可以让大伙点点更新按钮来更新你的新版本了。
-
-___
-
-以上介绍了大致的创建mod以及更新的流程。至于资源文件组织等等杂七杂八的问题,请参考已有的例子拓展包。
-
-下一篇: [fk技能类型总览](./02-skilltype.md)
diff --git a/doc/diy/02-skilltype.md b/doc/diy/02-skilltype.md
deleted file mode 100644
index 4d4cad90..00000000
--- a/doc/diy/02-skilltype.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# fk技能类型总览
-
-> [diy](./index.md) > fk技能类型总览
-
-___
-
-fk的目的是便于三国杀的DIY,而三国杀DIY的核心就是制作各种技能了。
-
-fk的技能分为两大类,这两大类又各自细分为更小的分类:
-
-(关于这部分的源码详见lua/core/skill.lua和lua/core/skill_type下的所有文件)
-
-* 可使用类技能(UsableSkill)
- * 触发技(TriggerSkill):在满足一定条件时,能够通过被动触发发挥效果的技能
- * 主动技(ActiveSkill):玩家主动发动的技能
- * 视为技(ViewAsSkill):将一张牌当做另一张牌的技能
-* 状态技(StatusSkill)
- * 距离技(DistanceSkill):影响距离计算的技能
- * 攻击范围技(DistanceSkill):影响攻击范围计算的技能
- * 手牌上限技(MaxCardsSkill):影响手牌上限计算的技能
- * 禁止技(ProhibitSkill):禁止成为卡牌目标的技能
- * 卡牌增强技(TargetModSkill):影响卡牌使用次数上限、目标上限、距离限制等等的技能
- * 锁定视为技(FilterSkill):让一张牌强制视为另一张牌的技能
-
-其中,触发技的逻辑最为复杂,但是[已经在这里分析过了](../dev/gamelogic.md),故不再赘述。
-
-主动技和状态技应该不算难,先按下不表。视为技与神杀有所区别,区别如下:
-
-在神杀中,视为技是否可响应是专门写在enabled_at_response的,fk则不然,看倾国的代码:
-
-```lua
-local qingguo = fk.CreateViewAsSkill{
- name = "qingguo",
- anim_type = "defensive",
- pattern = "jink",
- card_filter = function(self, to_select, selected)
- -- ...
- end,
- view_as = function(self, cards)
- -- ...
- end,
-}
-```
-
-可见并没有编写跟响应时候有关的函数,也没有声明出牌阶段不可用。其中的奥妙就在于pattern中,视为技可以转化的卡牌都应该写在pattern里面,Fk会根据pattern的内容判断技能出牌阶段是否可用、是否能够响应等。
diff --git a/doc/diy/03-events.md b/doc/diy/03-events.md
deleted file mode 100644
index 0d28d1ce..00000000
--- a/doc/diy/03-events.md
+++ /dev/null
@@ -1,9 +0,0 @@
-# fk中的游戏事件
-
-在进行DIY时,需要对三国杀的规则有一定了解;在编写技能时,也要熟悉游戏提供的各种事件,他的触发方式、触发时机、相关数据。必须要知道这些才能写出正确的代码。
-
-- [与游戏流程相关的事件](./event/gameflow.md)
-- [与体力值相关的事件](./event/hp.md)
-- [与卡牌使用有关的事件](./event/usecard.md)
-- [与移动牌有关的事件](./event/movecard.md)
-- [杂项](./event/misc.md)
diff --git a/doc/diy/event/gameflow.md b/doc/diy/event/gameflow.md
deleted file mode 100644
index 985a3af6..00000000
--- a/doc/diy/event/gameflow.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# 与游戏流程有关的事件
-
-先来看游戏流程本身。以下节选自lua/server/gamelogic.lua
-
-```lua
-function GameLogic:action()
- self:trigger(fk.GameStart)
- local room = self.room
-
- for _, p in ipairs(room.alive_players) do
- self:trigger(fk.DrawInitialCards, p, { num = 4 })
- end
-
- local function checkNoHuman()
- -- 如果房里已经没有人类玩家了就结束游戏
- end
-
- while true do
- self:trigger(fk.TurnStart, room.current)
- if room.game_finished then break end
- room.current = room.current:getNextAlive()
- if checkNoHuman() then
- room:gameOver("")
- end
- end
-end
-```
-
-以上这段代码,述说的就是整个游戏流程的核心。首先开始游戏、摸初始手牌,然后按照座位顺序每人依次执行回合直到游戏结束。
-
-___
-
-TODO
diff --git a/doc/diy/event/hp.md b/doc/diy/event/hp.md
deleted file mode 100644
index 3bd7708e..00000000
--- a/doc/diy/event/hp.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# 与体力值相关的事件
-
-___
-
-## 伤害
-
-## 失去体力/体力上限
-
-## 回复体力
-
-## 濒死和死亡
diff --git a/doc/diy/event/misc.md b/doc/diy/event/misc.md
deleted file mode 100644
index e69de29b..00000000
diff --git a/doc/diy/event/movecard.md b/doc/diy/event/movecard.md
deleted file mode 100644
index e69de29b..00000000
diff --git a/doc/diy/event/usecard.md b/doc/diy/event/usecard.md
deleted file mode 100644
index e69de29b..00000000
diff --git a/doc/diy/index.md b/doc/diy/index.md
deleted file mode 100644
index e992b84d..00000000
--- a/doc/diy/index.md
+++ /dev/null
@@ -1,16 +0,0 @@
-# 如何用FreeKill实现diy武将
-
-> diy
-
-___
-
-以下是一系列文档,旨在向从未接触过FreeKill(以后简称为Fk)的DIYer介绍Fk的DIY接口,以及如何打包、发布。
-
-本系列文档针对对神杀Lua有基础的读者编写。
-
-由于对于Win系统而言,fk仅仅支持Win 10及以上的64位系统,因此本文档假设您正使用Windows 10作为操作系统,且使用着64位的处理器。
-
-文档中蓝色字均为超链接。
-
-1. [环境搭建](./01-env.md)
-2. [fk技能类型总览](./02-skilltype.md)
diff --git a/doc/index.md b/doc/index.md
deleted file mode 100644
index 50906e52..00000000
--- a/doc/index.md
+++ /dev/null
@@ -1,6 +0,0 @@
-# FreeKill 文档
-
-___
-
-- [开发者文档](./dev/index.md)
-- [DIY玩家文档](./diy/index.md)
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 00000000..ed880990
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,20 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line, and also
+# from the environment for the first two.
+SPHINXOPTS ?=
+SPHINXBUILD ?= sphinx-build
+SOURCEDIR = .
+BUILDDIR = build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
diff --git a/docs/api/client.rst b/docs/api/client.rst
new file mode 100644
index 00000000..5cd51a1b
--- /dev/null
+++ b/docs/api/client.rst
@@ -0,0 +1,2 @@
+Client
+============
diff --git a/docs/api/core.rst b/docs/api/core.rst
new file mode 100644
index 00000000..07d64d1a
--- /dev/null
+++ b/docs/api/core.rst
@@ -0,0 +1,15 @@
+Core
+========
+
+.. toctree::
+ :maxdepth: 1
+ :caption: API
+
+ core/engine.rst
+ core/card.rst
+ core/general.rst
+ core/package.rst
+ core/skill.rst
+ core/game_mode.rst
+ core/player.rst
+
diff --git a/docs/api/core/card.rst b/docs/api/core/card.rst
new file mode 100644
index 00000000..35299e1e
--- /dev/null
+++ b/docs/api/core/card.rst
@@ -0,0 +1,4 @@
+Card
+==============
+
+.. lua:autoclass:: Card
diff --git a/docs/api/core/engine.rst b/docs/api/core/engine.rst
new file mode 100644
index 00000000..bc1420ab
--- /dev/null
+++ b/docs/api/core/engine.rst
@@ -0,0 +1,4 @@
+Engine
+==============
+
+.. lua:autoclass:: Engine
diff --git a/docs/api/core/game_mode.rst b/docs/api/core/game_mode.rst
new file mode 100644
index 00000000..abb1ac4c
--- /dev/null
+++ b/docs/api/core/game_mode.rst
@@ -0,0 +1,5 @@
+GameMode
+==============
+
+.. lua:autoclass:: GameMode
+
diff --git a/docs/api/core/general.rst b/docs/api/core/general.rst
new file mode 100644
index 00000000..cd739381
--- /dev/null
+++ b/docs/api/core/general.rst
@@ -0,0 +1,5 @@
+General
+==============
+
+.. lua:autoclass:: General
+
diff --git a/docs/api/core/package.rst b/docs/api/core/package.rst
new file mode 100644
index 00000000..273ff3dd
--- /dev/null
+++ b/docs/api/core/package.rst
@@ -0,0 +1,24 @@
+Package
+==============
+
+.. lua:autoclass:: Package
+
+详细信息
+~~~~~~~~~~~~~~
+
+.. _extension name:
+
+``extensionName`` 指的是这个Package所属的mod的名称。
+
+一般来说,一个mod(即packages/下面的一个文件夹)只含有一个拓展包,典型的例子就是fk自带的几个拓展包。FreeKill在寻找武将的图片、配音等素材的时候,就会根据这个mod的名字去寻找。
+
+在大多数情况下,Package的名字和mod的名字都是一致的(默认情况下也是如此),但有时候一个mod可能会含有好几个拓展包,比如神话再临mod里面就含有不少拓展包,这时候就要手动把extensionName设为mod的名字。以下是定义风包的代码:
+
+.. highlight:: lua
+
+::
+
+ local extension = Package:new("wind")
+ extension.extensionName = "shzl"
+
+这段代码定义了名为wind的拓展包,但是他所属的mod文件夹名是shzl,所以需要手动指定。
diff --git a/docs/api/core/player.rst b/docs/api/core/player.rst
new file mode 100644
index 00000000..996b7114
--- /dev/null
+++ b/docs/api/core/player.rst
@@ -0,0 +1,5 @@
+Player
+==============
+
+.. lua:autoclass:: Player
+
diff --git a/docs/api/core/skill.rst b/docs/api/core/skill.rst
new file mode 100644
index 00000000..8ef04e3d
--- /dev/null
+++ b/docs/api/core/skill.rst
@@ -0,0 +1,4 @@
+Skill
+==============
+
+.. lua:autoclass:: Skill
diff --git a/docs/api/index.rst b/docs/api/index.rst
new file mode 100644
index 00000000..69326107
--- /dev/null
+++ b/docs/api/index.rst
@@ -0,0 +1,9 @@
+API文档
+============
+
+.. toctree::
+ :maxdepth: 1
+
+ core.rst
+ server.rst
+ client.rst
diff --git a/docs/api/server.rst b/docs/api/server.rst
new file mode 100644
index 00000000..fc65c98e
--- /dev/null
+++ b/docs/api/server.rst
@@ -0,0 +1,2 @@
+Server
+============
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 00000000..3262ac09
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,80 @@
+# Configuration file for the Sphinx documentation builder.
+#
+# This file only contains a selection of the most common options. For a full
+# list see the documentation:
+# https://www.sphinx-doc.org/en/master/usage/configuration.html
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+# import os
+# import sys
+# sys.path.insert(0, os.path.abspath('.'))
+
+
+# -- Project information -----------------------------------------------------
+
+project = 'FreeKill'
+copyright = '2023, Notify'
+author = 'Notify'
+
+
+# -- General configuration ---------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.doctest',
+ 'sphinx.ext.intersphinx',
+ 'sphinx.ext.todo',
+ 'sphinx.ext.coverage',
+ 'sphinx.ext.mathjax',
+ 'sphinx.ext.ifconfig',
+ 'sphinx.ext.viewcode',
+ 'sphinx.ext.githubpages',
+ 'sphinxcontrib.luadomain',
+ 'sphinx_lua',
+]
+
+lua_source_path = [
+ "../lua",
+ "../packages",
+]
+
+lua_source_encoding = 'utf8'
+lua_source_comment_prefix = '---'
+lua_source_use_emmy_lua_syntax = True
+lua_source_private_prefix = '_'
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = 'zh_CN'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = []
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+#
+html_theme = 'sphinx_rtd_theme'
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
diff --git a/doc/dev/ai.md b/docs/dev/ai.rst
similarity index 73%
rename from doc/dev/ai.md
rename to docs/dev/ai.rst
index ac318c8f..17c21695 100644
--- a/doc/dev/ai.md
+++ b/docs/dev/ai.rst
@@ -1,19 +1,18 @@
-# FreeKill 的 AI 系统
+FreeKill 的 AI 系统
+===================
-> [dev](./index.md) > AI
-
-___
-
-## 概述
+概述
+----
备选算法:
-- MCTS
-- 神杀算法
+- MCTS
+- 神杀算法
-___
+--------------
-## MCTS实现
+MCTS实现
+--------
实现该算法的最大难点在于如何模拟。
@@ -25,12 +24,17 @@ ___
1. 首先Room初始化的时候也初始化一个AI用的Room
2. Room内要能够录像,记录所有的request结果和random生成的值。为此可能要自定义一个random函数对自带的math.random进行封装。
-3. 在录像的时候,AI Room也跟着录像的内容进行更新。AI Room本质上也就是一个Room而已,或者可以是Room的子类,反正他的内容就是用这个方式和真Room即时同步的。
+3. 在录像的时候,AI Room也跟着录像的内容进行更新。AI
+ Room本质上也就是一个Room而已,或者可以是Room的子类,反正他的内容就是用这个方式和真Room即时同步的。
4. 在AI即将处理问题的时候,首先获得所有可行选项。根据算法,需要对某个节点进行randomplay。
5. randomplay的话如果直接用AI Room,那么回溯的时候如何回到先前的状态呢?
- 1. 考虑新建一个新的AI Room,然后重放录像以达到开始状态。这样每次randomplay之前都要先回复一下状态,而随着录像的加长这个过程也可能变长,导致AI越来越慢
- 2. 考虑真Room的所有字段全部复制给AI Room一份。但有个问题在于如何把程序控制流和栈也跳转到一样的地方。所以这个是很难实现的。
+
+ 1. 考虑新建一个新的AI
+ Room,然后重放录像以达到开始状态。这样每次randomplay之前都要先回复一下状态,而随着录像的加长这个过程也可能变长,导致AI越来越慢
+ 2. 考虑真Room的所有字段全部复制给AI
+ Room一份。但有个问题在于如何把程序控制流和栈也跳转到一样的地方。所以这个是很难实现的。
3. 所以考虑用方案1。为了缓解太慢的情况,可以把1和2结合起来。约定好在某个时间点(比如GameLogic:action中的那个死循环执行)就与Room交换数据,然后这时候复盘录像的起始时间点修改。这样的话为了从randomplay恢复状态,就有必要将此时交换的数据额外保存一份。为了能让Logic平安跑到那个时间点,从人凑齐直到那个时间点的录像也要保存一份。
+
6. 解决了模拟和回溯的问题的话,就可以考虑实现该算法了。
那么为了模拟,首先得实现一个RandomAI才行。
diff --git a/docs/dev/compile.rst b/docs/dev/compile.rst
new file mode 100644
index 00000000..9b97fc7f
--- /dev/null
+++ b/docs/dev/compile.rst
@@ -0,0 +1,146 @@
+编译 FreeKill
+=============
+
+全平台通用步骤
+--------------
+
+FreeKill采用最新的Qt进行构建,因此需要先安装Qt6的开发环境。
+
+无论是Win还是Linux,都建议用\ `Qt官方的下载器 `__\ 进行安装。当然了,在一些软件更新很频繁的Linux发行版里面,可能已经能从包管理器安装Qt6,对此后文细说。这个环节介绍用Qt安装器安装的步骤。
+
+Qt安装的流程不赘述。为了编译FreeKill,至少需要安装以下的组件: - Qt 6:
+MinGW 11.2.0 64-bit (不支持MSVC) - Qt 6: Qt5 Compat - Qt 6: Shader
+Tools (为了使用GraphicalEffects) - Qt 6: Multimedia -
+QtCreator(这个是安装器强制要你安装的) - CMake、Ninja - OpenSSL 1.1.1
+
+接下来根据平台的不同,步骤也稍有区别。
+
+--------------
+
+Windows
+-------
+
+从网络上下载swig、flex、bison。swig在其官网可以下载,flex和bison可在\ `github `__\ 或者SourceForge下载。
+
+全都下载完成之后,将含有swig.exe、win_flex.exe、win_bison.exe的文件夹全部都设置到Path环境变量里面去。
+
+接下来使用QtCreator打开项目,然后尝试编译。
+
+这时遇到cmake报错:OpenSSL:Crypto not found.
+这是因为我们还没有告诉编译器OpenSSL的位置,点左侧“项目”,查看构建选项,在CMake的Initial
+Configuration中,点击添加按钮,新增String型环境变量OPENSSL_ROOT_DIR,将其值设为跟Qt一同安装的OpenSSL的位置(如C:/Qt/Tools/OpenSSL/Win_x64)。然后点下方的Re-configure
+with Initial Parameters,这样就能正常编译了。
+
+运行的话,在Qt
+Creator的项目选项->运行中,先将工作目录改为项目所在的目录(git仓库的目录)。然后先将编译好了的FreeKill.exe放到项目目录中,在目录下打开CMD,执行windeployqt
+FreeKill.exe。调整目录下的dll文件直到能运行起来为止,之后就可以在Qt
+Creator中正常运行和调试了。
+
+--------------
+
+Linux
+-----
+
+通过包管理器安装一些额外软件包方可编译。
+
+Debian一家子:
+
+.. code:: sh
+
+ $ sudo apt install liblua5.4-dev libsqlite3-dev libssl-dev swig flex bison
+
+Arch Linux:
+
+.. code:: sh
+
+ $ sudo pacman -Sy lua sqlite swig openssl flex bison
+
+然后使用配置好的QtCreator环境即可编译。
+
+如果你不想用Qt安装器的话,可以用包管理器安装依赖,下面仅举例Arch:
+
+.. code:: sh
+
+ $ sudo pacman -S qt6-base qt6-declarative qt6-5compat qt6-multimedia
+ $ sudo pacman -S cmake lua sqlite swig openssl swig flex bison
+
+然后可以用命令行编译:
+
+.. code:: sh
+
+ $ mkdir build && cd build
+ $ cmake ..
+ $ make -j8
+
+--------------
+
+Linux服务器
+-----------
+
+一般来说Linux服务器的包管理器都没新到提供Qt6下载,这个时候想编译服务端的话,需要在尽可能安装完Qt5环境的情况下,对FreeKill的Qt版本降一下等级。
+
+首先将根目录和src下面的两个CMakeLists.txt的Qt6都改成Qt5,然后试图进行编译。
+
+编译器会报告大概不超过10处错误,将它们修改成Qt5可以接受的形式就行了。
+
+--------------
+
+MacOS
+-----
+
+大致与Windows类似,但尚且缺少确切的方案。
+
+--------------
+
+编译安卓版
+----------
+
+用Qt安装器装好Android库,然后配置一下android-sdk就能编译了。
+
+(Qt
+6.4的刘海屏bug,手动往QActivity.java的onCreate函数追加如下代码即可实现完全全屏。这里做个笔记方便复制粘贴,等Qt修了再说)
+
+.. code:: java
+
+ getWindow().addFlags(LayoutParams.FLAG_FULLSCREEN);
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
+ getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
+ }
+ if (Build.VERSION.SDK_INT > 28) {
+ WindowManager.LayoutParams lp = getWindow().getAttributes();
+ lp.layoutInDisplayCutoutMode = LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
+ getWindow().setAttributes(lp);
+ }
+
+--------------
+
+WASM下编译
+----------
+
+WASM大概就是能在浏览器中跑C++。编译用Qt Creator即可。
+
+1. 条件与局限性
+~~~~~~~~~~~~~~~
+
+如果程序运行在网页上的话,那么理应只有客户端,然后提供网页的服务器上自然也运行着一个后端服务器。所以说在编译时应该舍弃掉服务端相关的代码。因此依赖库就不再需要sqlite3。
+
+总之是编译个纯客户端的FK。
+
+2. 编译OpenSSL
+~~~~~~~~~~~~~~
+
+进入OpenSSL的src目录,然后
+
+::
+
+ $ ./config -no-asm -no-engine -no-dso
+ $ emmake make -j8 build_generated libssl.a libcrypto.a
+
+编译Lua的话直接emmake make就行了,总之库已经传到仓库了。
+
+3. 部署资源文件
+~~~~~~~~~~~~~~~
+
+由于CMake中\ ``file(GLOB_RECURSE)``\ 所带来的缺陷,每当资源文件变动时,需要手动更新。
+
+把构建目录中的.rcc目录删掉然后重新执行CMake->make即可。每次编译资源文件总要消耗相当多的时间。
diff --git a/doc/dev/database.md b/docs/dev/database.rst
similarity index 52%
rename from doc/dev/database.md
rename to docs/dev/database.rst
index 310b16dc..4cfab213 100644
--- a/doc/dev/database.md
+++ b/docs/dev/database.rst
@@ -1,15 +1,14 @@
-# FreeKill 的数据库
-
-> [dev](./index.md) > 数据库
-
-___
+FreeKill 的数据库
+=================
FreeKill 使用 sqlite3 数据库。
关于数据库的组织,详见server/init.sql。单纯存个用户名和密码而已
-## 服务端用来管理用户的数据库
+服务端用来管理用户的数据库
+--------------------------
保存用户名与密码而已。
-## 包管理用的数据库
+包管理用的数据库
+----------------
diff --git a/doc/dev/gameevent.md b/docs/dev/gameevent.rst
similarity index 64%
rename from doc/dev/gameevent.md
rename to docs/dev/gameevent.rst
index b4ad91af..d3223acc 100644
--- a/doc/dev/gameevent.md
+++ b/docs/dev/gameevent.rst
@@ -1,28 +1,29 @@
-# Fk的游戏事件
+Fk的游戏事件
+============
-___
-
-在Fk中,“事件”指的大约是像`room:judge`, `room:damage`之类的操作。这些操作一般和某个游戏术语挂钩(“判定”、“伤害”),然后其中包含着一系列操作,比如伤害事件包含了与伤害事件有关的各种触发时机、以及扣减等实际的动作等。
+在Fk中,“事件”指的大约是像\ ``room:judge``,
+``room:damage``\ 之类的操作。这些操作一般和某个游戏术语挂钩(“判定”、“伤害”),然后其中包含着一系列操作,比如伤害事件包含了与伤害事件有关的各种触发时机、以及扣减等实际的动作等。
之所以要把事件单独挑出来聊聊,是因为有以下几点需求:
-* 事件要能够被半路中止。
-* 对于被中止的事件,要能判断它能不能中止事件栈中的更低层事件。
-* 对于被中止的事件,需要做“垃圾回收”(例如将处于处理区的相关卡牌移动到弃牌堆等等)
+- 事件要能够被半路中止。
+- 对于被中止的事件,要能判断它能不能中止事件栈中的更低层事件。
+- 对于被中止的事件,需要做“垃圾回收”(例如将处于处理区的相关卡牌移动到弃牌堆等等)
-___
+--------------
-## 对于如何实现的构想
+对于如何实现的构想
+------------------
(施工完成后再来修改这一节)
首先是如何实现事件类。初步构想一下,应该有以下的属性/方法:
-* 事件名(也可以是枚举值)
-* 事件数据(一个表,内含所有要用到的数据)
-* 事件id(在一局游戏中唯一标记某个事件)
-* 事件的函数体,也就是具体要做的事情
-* 事件被中止或者正常结束时,用来清理现场的函数
+- 事件名(也可以是枚举值)
+- 事件数据(一个表,内含所有要用到的数据)
+- 事件id(在一局游戏中唯一标记某个事件)
+- 事件的函数体,也就是具体要做的事情
+- 事件被中止或者正常结束时,用来清理现场的函数
问题来了,既然事件本质上还是个函数体,那要怎么才能中止呢?
@@ -30,104 +31,113 @@ ___
事件必然会发生嵌套。所以对此更要慎重考虑。
-现在只是设想!假设以`room:judge`入手:
+现在只是设想!假设以\ ``room:judge``\ 入手:
-```lua
-function Room:judge(judgeStruct)
- local judgeEvent = GameEvent:new(GameEvent.Judge, judgeStruct)
- judgeEvent:exec()
-end
-```
+.. code:: lua
+
+ function Room:judge(judgeStruct)
+ local judgeEvent = GameEvent:new(GameEvent.Judge, judgeStruct)
+ judgeEvent:exec()
+ end
总之可能是这样的吧。exec()就是实际执行事件,可能如下:
-```lua
-function GameEvent:exec()
- local event_f = self.event_f
- local co = coroutine.create(event_f)
- while true do
- local yield_result = coroutine.resume(co)
- if yield_result == "__handleRequest" then
- -- 正常yield掉,如此层层yield最后到Room的主循环,然后进requestLoop协程处理事务
- -- 最后返回这里继续resume
- coroutine.yield(yield_result)
- else
- -- 事件被中止,考虑做点什么
- break
- end
- end
-end
-```
+.. code:: lua
+
+ function GameEvent:exec()
+ local event_f = self.event_f
+ local co = coroutine.create(event_f)
+ while true do
+ local yield_result = coroutine.resume(co)
+ if yield_result == "__handleRequest" then
+ -- 正常yield掉,如此层层yield最后到Room的主循环,然后进requestLoop协程处理事务
+ -- 最后返回这里继续resume
+ coroutine.yield(yield_result)
+ else
+ -- 事件被中止,考虑做点什么
+ break
+ end
+ end
+ end
现在考虑嵌套的情况,event1和event2嵌套,也就是event1的体里面又创建了event2并exec,此时的协程调用关系如下:
- RoomLogic -> event1 -> event2 | RequestLoop
+::
+
+ RoomLogic -> event1 -> event2 | RequestLoop
此时event2中调用了某个耗时的函数,比如room:delay或者各种request,这时候就触发了yield。然后在上面的函数中就获取了yield返回值,然后判断是正常yield后,就进一步yield,此时协程到了event1中。event1继续yield,于是到了Room主协程,主协程其实也调用了exec,所以被yield切回到真正的主线程,然后执行requestloop的协程。乍一看似乎没问题,除了这个跳跃链有够长的。
-这种开销看起来应该不大吧,而且在AI Random Play这种非常需要性能的场所也根本不会发生这种类型的yield(画饼),总之先不考虑这块。
+这种开销看起来应该不大吧,而且在AI Random
+Play这种非常需要性能的场所也根本不会发生这种类型的yield(画饼),总之先不考虑这块。
-如果事件被中止(按目前的实现来说就是在特定的时机return true了),那么事件的本体也应该调用yield(这就如同在目前--v0.0.1实现中,那些函数常见的遇到true就直接返回了一样),这样相当于从函数返回了。这种yield就会进入那个else分支,进行这个事件类的有关清扫工作。
+如果事件被中止(按目前的实现来说就是在特定的时机return
+true了),那么事件的本体也应该调用yield(这就如同在目前–v0.0.1实现中,那些函数常见的遇到true就直接返回了一样),这样相当于从函数返回了。这种yield就会进入那个else分支,进行这个事件类的有关清扫工作。
-___
+--------------
-## 构想2 类似无懈可击的事件
+构想2 类似无懈可击的事件
+------------------------
考虑一类特殊的事件:“取消其他事件的事件”。它和普通事件一样,能被中止之类的,而它的作用在于取消掉其他事件。
接着上面的else分支继续考虑:
-```lua
- else
- local cancelEvent = GameEvent:new(GameEvent.CancelEvent, self)
- local ret = cancelEvent:exec()
- if ret then break end
-```
+.. code:: lua
+
+ else
+ local cancelEvent = GameEvent:new(GameEvent.CancelEvent, self)
+ local ret = cancelEvent:exec()
+ if ret then break end
似乎也没什么非常特殊的内容啊。
exec()的返回值哪里来?这好像真的是个问题呢。可以考虑返回布尔值表示事件是否中止了?或者更详细的,返回一个状态码,毕竟本质上身为协程自然能有协程该有的种种状态。这里只是初步考虑而已,就考虑前者好了。
-___
+--------------
-## 落实 - 手杀皇甫嵩
+落实 - 手杀皇甫嵩
+-----------------
-手杀皇甫嵩是重构整个事件体系的罪魁祸首(雾)。其技能为:若blah blah,你可以终止本次判定,然后blah blah。
+手杀皇甫嵩是重构整个事件体系的罪魁祸首(雾)。其技能为:若blah
+blah,你可以终止本次判定,然后blah blah。
而终止本次判定是目前的体系做不到的。
考虑如下技能片段:
-```lua
- on_effect = function(xxx)
- local judge = {}
- room:judge(judge)
- if judge.card.number > 5 then xxx end
- end
-```
+.. code:: lua
+
+ on_effect = function(xxx)
+ local judge = {}
+ room:judge(judge)
+ if judge.card.number > 5 then xxx end
+ end
皇甫嵩能终止判定,就算他在fk.Judge时机返回true算了。前文已经考虑过judge了,他创建了新event并执行之。而今judge事件遭到打断,room:judge可能可以返回一个返回值来告诉玩家已经被中断之类的。但是Luaer,特别是像我这样的Luaer,懒得考虑事件的合法性之类的,而既然judge已经被终止,那么judge.card就不应该被使用才行。
-为此可以为judge表添加__index元方法,当对key="card"进行取值时,就直接yield掉,除此之外的就rawget。
+为此可以为judge表添加\__index元方法,当对key=“card”进行取值时,就直接yield掉,除此之外的就rawget。
还有更复杂的情况呢。当皇甫嵩判乐的时候,如果是黑桃,那么他发动技能终止了判定,然后像个无事人一样出牌呢。乐都还贴在他头上。考察一下Fk里面的乐是怎么写的,哦,原来是on_effect的末尾才移走啊,那没事了。也就是说,如果对judge.card的非法访问使得事件被中止了,那么照这个逻辑,乐是下不来的,符合手刹了这下。
-___
+--------------
-## 考虑 事件为何中止?
+考虑 事件为何中止?
+-------------------
事件是协程,因此协程中止的方法就是事件中止的方法。有这两种:
-* yield, 落实到Fk就是触发技的各种返回true
-* error, 这不就是我经常发生的事情吗
+- yield, 落实到Fk就是触发技的各种返回true
+- error, 这不就是我经常发生的事情吗
前面也提到过发生yield的时候会有cancelEvent产生,方便玩家反悔中止这次事件,但因为error而中断事件是无法恢复的。试图resume一个报错的协程的话,他会立刻因为error而自动yield。这个可以在exec函数里面多加考虑,如果resume函数返回了true和特定值,那就是正常情况。否则就是报错,输出错误信息并返回。
那前文那个judge.card怎么办呢?这种严格来说得算在error的范畴,因为不是人为中止本次effect的。但是error的话势必要输出到屏幕,而我个人聚德直接拿judge.card算是合法行为。这种情况或许可以定一个约定好的特殊错误信息,在处理错误的时候如果是这个错误的话就不输出。
-___
+--------------
-## 考虑 有哪些事件
+考虑 有哪些事件
+---------------
在最开始的时候,“依赖关系”这个现象的存在使得触发技多了个on_cost(消耗),但是现在on_cost已经成为界定skill是否发动了的标准。而在skill的effect环节,依然存在着一环扣一环的关系,比如前面举的room:judge例子。
@@ -138,23 +148,27 @@ ___
总之,事件不止room.lua里面那些。就拿前面的考虑来说,由于要中断on_effect,所以on_effect肯定会算成一个事件,可能叫SkillEffect事件吧。
再考虑万恶之源武将——老朱然,直接结束你的回合。(他只要回合内造成了伤害就能结束回合,但没说在谁的回合造成了伤害)所以进行回合也理应算是个事件。
-___
+--------------
-## 考虑 老朱然
+考虑 老朱然
+-----------
对于老朱然这种人而言,他想要杀掉的是回合事件,而能发动这个技能的时候,事件栈想必已经很深了,稍微模拟一下这个情景:老朱然杀界徐盛并打掉他一滴血,此时事件栈大概如下(还没正式设计各种事件,所以可能不妥):
-* 伤害事件 - room:damage - 询问技能:是否发动胆守,点确定
-* 技能生效事件 - activeskill:onEffect - 【杀】的effect
-* 使用牌事件 - room:useCard - 出杀
-* 进行阶段事件 - ? - 在出牌阶段
-* 回合事件 - ? - 在回合
+- 伤害事件 - room:damage - 询问技能:是否发动胆守,点确定
+- 技能生效事件 - activeskill:onEffect - 【杀】的effect
+- 使用牌事件 - room:useCard - 出杀
+- 进行阶段事件 - ? - 在出牌阶段
+- 回合事件 - ? - 在回合
我们的限制条件:无法获得room:damage的返回值,或者说根本没想去获得,其他同理。
coroutine.yield的功能也只有挂起协程并让相应的resume调用返回而已,那么该怎么办呢?由于以上种种限制的存在(主要还是想把Luaer惯着),我们不能对杀的onEffect下手,其他函数都是核心函数,改改也无妨咯。
-还是结合情景考虑吧。胆守点了确定,此时最直接的感受应该是return true。但是return true的意思是防止伤害(都已经是“造成伤害后”了,怎么防止哦,return true也不会有人管你的),所以这里要另辟蹊径。考虑直接yield:此时会处于DamageEvent的exec()中,也就是处于room:damage中,他在处理中止信息。正常的中止的话,会使用break跳出循环;那么如果我访问调用栈,直接让他一路yield到我们想要的那个事件,如同yield到requestLoop那样呢?
+还是结合情景考虑吧。胆守点了确定,此时最直接的感受应该是return
+true。但是return
+true的意思是防止伤害(都已经是“造成伤害后”了,怎么防止哦,return
+true也不会有人管你的),所以这里要另辟蹊径。考虑直接yield:此时会处于DamageEvent的exec()中,也就是处于room:damage中,他在处理中止信息。正常的中止的话,会使用break跳出循环;那么如果我访问调用栈,直接让他一路yield到我们想要的那个事件,如同yield到requestLoop那样呢?
没错,访问事件栈确实是个解决办法的可能方案。(这时候用id指示事件的重要性就出来了,可以传一个id表示事件,不过话说回来传那个事件本身也没有任何关系就是了咯)如果yield函数返回了一个GameEvent类的实例,那么就在处理环节将其和self进行比较,如果不同,就继续yield,直到退到相应的事件中。
@@ -162,9 +176,10 @@ coroutine.yield的功能也只有挂起协程并让相应的resume调用返回
总之这不考虑如何防止这种直接结束回合了,毕竟这种不断yield的方式无法用事件进行描述。
-___
+--------------
-## 考虑 内存泄漏的应对
+考虑 内存泄漏的应对
+-------------------
首先声明,Lua没有内存泄漏。但是如果有些东西用户不想要,但是又不告诉lua的话,lua就会觉得用户想要,然后一直保存着它,这在某种意义上也相当于内存泄漏了。拿实例来说,如果事件被中止了,那么在很多情况下确实就不需要了,但Lua会认为协程是挂起的,用户可能想要恢复,于是一直保存着。
diff --git a/doc/dev/gamelogic.md b/docs/dev/gamelogic.rst
similarity index 56%
rename from doc/dev/gamelogic.md
rename to docs/dev/gamelogic.rst
index 19f57339..5d950c0a 100644
--- a/doc/dev/gamelogic.md
+++ b/docs/dev/gamelogic.rst
@@ -1,52 +1,52 @@
-# 游戏逻辑
+游戏逻辑
+========
-> [dev](./index.md) > 游戏逻辑
-
-___
-
-## 概述
+概述
+----
FreeKill的游戏相关处理逻辑完全使用lua实现。在服务端上,每个Room都有自己的lua_State,并且只会在Room线程启动后才会去调用lua函数进行游戏逻辑处理。
本文档将简要介绍几个最为复杂的逻辑实现。
-___
+--------------
-## 触发技
+触发技
+------
在lua/fk_ex.lua中有对触发技的描述:
-```lua
----@alias TrigFunc fun(self: TriggerSkill, event: Event, target: ServerPlayer, player: ServerPlayer):boolean
----@class TriggerSkillSpec: SkillSpec
----@field global boolean
----@field events Event | Event[]
----@field refresh_events Event | Event[]
----@field priority number | table
----@field on_trigger TrigFunc
----@field can_trigger TrigFunc
----@field on_cost TrigFunc
----@field on_use TrigFunc
----@field on_refresh TrigFunc
-```
+.. code:: lua
-具体的`fk.CreateTriggerSkill`函数接受一个类型为如上所述的TriggerSkillSpec形式的表。这个表中的属性一共有一下这些:
+ ---@alias TrigFunc fun(self: TriggerSkill, event: Event, target: ServerPlayer, player: ServerPlayer):boolean
+ ---@class TriggerSkillSpec: SkillSpec
+ ---@field global boolean
+ ---@field events Event | Event[]
+ ---@field refresh_events Event | Event[]
+ ---@field priority number | table
+ ---@field on_trigger TrigFunc
+ ---@field can_trigger TrigFunc
+ ---@field on_cost TrigFunc
+ ---@field on_use TrigFunc
+ ---@field on_refresh TrigFunc
-- 所有技能通用的`name`、`anim_type`、`mute`。其中name为必需项。
-- global: 是否是全局技能。
-- events: 技能的所有触发时机
-- can_trigger: 技能能否被触发
-- on_trigger: 技能触发时具体的行为
-- on_cost: 技能如何执行消耗
-- on_use: 技能被发动后,具体的生效内容
-- priority: 技能的优先级。在同一时机有多个技能能够被触发时,先触发优先级高的。
+具体的\ ``fk.CreateTriggerSkill``\ 函数接受一个类型为如上所述的TriggerSkillSpec形式的表。这个表中的属性一共有一下这些:
+
+- 所有技能通用的\ ``name``\ 、\ ``anim_type``\ 、\ ``mute``\ 。其中name为必需项。
+- global: 是否是全局技能。
+- events: 技能的所有触发时机
+- can_trigger: 技能能否被触发
+- on_trigger: 技能触发时具体的行为
+- on_cost: 技能如何执行消耗
+- on_use: 技能被发动后,具体的生效内容
+- priority:
+ 技能的优先级。在同一时机有多个技能能够被触发时,先触发优先级高的。
refresh等一系列函数与前面同理,下面会对其展开细说。
首先先来看看触发技究竟是如何被触发的:(以下代码详见room.lua和gamelogic.lua,这里只是简单说明一下)
-1. 某处调用`logic:trigger(event, player, data)`
-2. 开始调用GameLogic:trigger,首先从所有符合该时机的技能中选出那个技能列表。这里说明一下,所有的触发技都保存在GameLogic的`skill_table`表中,这个表的键是相应的触发时机,值则是技能列表。每当GameLogic被创建时,首先会将全局触发技都加入到表中;然后,在游戏中每当有角色获得了一个触发技,就将这个技能加入到表中直到游戏结束。
+1. 某处调用\ ``logic:trigger(event, player, data)``
+2. 开始调用GameLogic:trigger,首先从所有符合该时机的技能中选出那个技能列表。这里说明一下,所有的触发技都保存在GameLogic的\ ``skill_table``\ 表中,这个表的键是相应的触发时机,值则是技能列表。每当GameLogic被创建时,首先会将全局触发技都加入到表中;然后,在游戏中每当有角色获得了一个触发技,就将这个技能加入到表中直到游戏结束。
3. 若调用trigger函数时对target参数传入了nil,表示这是一个通用型时机,没有特定的承担者,比如fk.GameStart时机。这时候会对技能进行can_trigger检测并直接触发。
4. 若target不是nil,那么将对整个Room中所有玩家进行遍历。在这个遍历过程中,对每个玩家分别判断其能否触发这个技能,若能的话就进行on_trigger的内容,中间的优先级和选择发动哪个技能暂且不说明,可以在代码中查看到。
5. 若on_trigger函数返回了true,那么就说明这个时机被中断了,此时trigger函数返回,否则就这样一直遍历完所有玩家为止。
@@ -55,62 +55,62 @@ refresh等一系列函数与前面同理,下面会对其展开细说。
这部分相关的代码位于core/skill_type/trigger.lua中。来看看这些函数的默认值:
-```lua
-function TriggerSkill:triggerable(event, target, player, data)
- return target and (target == player)
- and (self.global or (target:isAlive() and target:hasSkill(self)))
-end
+.. code:: lua
-function TriggerSkill:trigger(event, target, player, data)
- return self:doCost(event, target, player, data)
-end
-```
+ function TriggerSkill:triggerable(event, target, player, data)
+ return target and (target == player)
+ and (self.global or (target:isAlive() and target:hasSkill(self)))
+ end
+
+ function TriggerSkill:trigger(event, target, player, data)
+ return self:doCost(event, target, player, data)
+ end
这就是can_trigger和on_trigger的默认值了。can_trigger默认情况下判断遍历到的角色就是承担者角色,并且这个角色要拥有本技能才行。这种判断适用于绝大多数情况,比如英姿等技能。而on_trigger则是调用了TriggerSkill:doCost函数了。doCost函数并不是fk_ex.lua中的on_cost,而是triggerSkill中的一个特别的函数,其内容如下:
-```lua
-function TriggerSkill:doCost(event, target, player, data)
- local ret = self:cost(event, target, player, data)
- if ret then
- local room = player.room
- if not self.mute then
- room:broadcastSkillInvoke(self.name)
- end
- room:notifySkillInvoked(player, self.name)
- player:addSkillUseHistory(self.name)
- ret = self:use(event, target, player, data)
- return ret
- end
-end
-```
+.. code:: lua
+
+ function TriggerSkill:doCost(event, target, player, data)
+ local ret = self:cost(event, target, player, data)
+ if ret then
+ local room = player.room
+ if not self.mute then
+ room:broadcastSkillInvoke(self.name)
+ end
+ room:notifySkillInvoked(player, self.name)
+ player:addSkillUseHistory(self.name)
+ ret = self:use(event, target, player, data)
+ return ret
+ end
+ end
这个函数首先调用self:cost(即on_cost),判断是否返回了true。(返回true的话意味着玩家已经完成了消耗,技能被正式发动了)如果返回true的话,那么就认为技能发动了,这时会添加技能发动记录、播放配音等行为,然后正式执行self:use(即on_use)。这就是触发技完整的从触发到使用的过程。
现在以鬼才为例:(packages/standard/init.lua)
-```lua
-local guicai = fk.CreateTriggerSkill{
- name = "guicai",
- anim_type = "control",
- events = {fk.AskForRetrial},
- can_trigger = function(self, event, target, player, data)
- return player:hasSkill(self.name) and not player:isKongcheng()
- end,
- on_cost = function(self, event, target, player, data)
- local room = player.room
- local prompt = "#guicai-ask::" .. target.id
- local card = room:askForResponse(player, self.name, ".|.|.|hand", prompt, true)
- if card ~= nil then
- self.cost_data = card
- return true
- end
- end,
- on_use = function(self, event, target, player, data)
- local room = player.room
- room:retrial(self.cost_data, player, data, self.name)
- end,
-}
-```
+.. code:: lua
+
+ local guicai = fk.CreateTriggerSkill{
+ name = "guicai",
+ anim_type = "control",
+ events = {fk.AskForRetrial},
+ can_trigger = function(self, event, target, player, data)
+ return player:hasSkill(self.name) and not player:isKongcheng()
+ end,
+ on_cost = function(self, event, target, player, data)
+ local room = player.room
+ local prompt = "#guicai-ask::" .. target.id
+ local card = room:askForResponse(player, self.name, ".|.|.|hand", prompt, true)
+ if card ~= nil then
+ self.cost_data = card
+ return true
+ end
+ end,
+ on_use = function(self, event, target, player, data)
+ local room = player.room
+ room:retrial(self.cost_data, player, data, self.name)
+ end,
+ }
首先name和anim_type啥的不多说。技能的时机是AskForRetrial,这也就是询问改判的时机。由于鬼才的触发条件是只要自己有手牌就能触发,无需判定者是自己,因此这里没有用默认的can_trigger。on_trigger函数采用默认方案,直接只执行doCost。在on_cost环节,玩家需要选择是否打出一张手牌。如果确实打出牌了,那么就返回true,并把打出的牌保存到self.cost_data中。(self是这个技能本身,注意技能的本质其实就是一张表,因此可以像这样指定一个新的键值也是没问题的)在on_use,也就是技能的生效部分,才会正式执行改判这一动作。
@@ -118,25 +118,26 @@ on_trigger在非常多情况下仅仅只是简单的执行一下doCost而已,
在有些时候,只是想在特定的时机执行一些代码,而不想进行询问和发动技能流程时,可以使用on_refresh执行。在refresh的情况下,代码仅仅只是执行了一次,不会做出发动技能之类的动作、
-___
+--------------
-## 移动牌
+移动牌
+------
-移动牌的核心函数是`Room:moveCards(...)`。这是个变长参数函数,根据Emmy注解可知所有的参数都应该是CardsMoveInfo类型。CardsMoveInfo在[system_enum.lua](../../lua/server/system_enum.lua)里面有类型注解,来看看:
+移动牌的核心函数是\ ``Room:moveCards(...)``\ 。这是个变长参数函数,根据Emmy注解可知所有的参数都应该是CardsMoveInfo类型。CardsMoveInfo在\ `system_enum.lua <../../lua/server/system_enum.lua>`__\ 里面有类型注解,来看看:
-```lua
----@class CardsMoveInfo
----@field ids integer[]
----@field from integer|null
----@field to integer|null
----@field toArea CardArea
----@field moveReason CardMoveReason
----@field proposer integer
----@field skillName string|null
----@field moveVisible boolean|null
----@field specialName string|null
----@field specialVisible boolean|null
-```
+.. code:: lua
+
+ ---@class CardsMoveInfo
+ ---@field ids integer[]
+ ---@field from integer|null
+ ---@field to integer|null
+ ---@field toArea CardArea
+ ---@field moveReason CardMoveReason
+ ---@field proposer integer
+ ---@field skillName string|null
+ ---@field moveVisible boolean|null
+ ---@field specialName string|null
+ ---@field specialVisible boolean|null
moveCards函数的第一步是将参数中所有的moveInfo都转化为CardsMoveStruct。CardsMoveStruct与CardsMoveInfo几乎没有区别,除了它将每一张牌都单独划分出了一个moveinfo之外。这么做是为了在同时移动来源不同的牌的时候,让牌能该明牌明牌,该暗牌暗牌。
@@ -146,10 +147,11 @@ moveCards函数的第一步是将参数中所有的moveInfo都转化为CardsMove
然后,对所有的CardsMoveStruct进行遍历,根据move.from和move.fromArea获取这张牌的id实际所在的数组,然后将这个id移动到目标数组中。如此就在服务端的数据层面移动了一张牌。移牌OK后,Room会更新这张牌的位置信息,然后视情况更新这张牌的锁定视为技信息。如果是装备牌的话,那么就做一些跟装备技能有关的事情。
-___
+--------------
-## 使用牌
+使用牌
+------
使用一张牌应该是全游戏最复杂而又最常见的一种事件了。说他复杂,其实也是被狗卡各种乱七八糟的技能和规则搞得很复杂的。
-使用牌的核心函数是`Room:useCard`,接收的参数是CardUseStruct。不行太复杂了,过一阵子再来看吧。
+使用牌的核心函数是\ ``Room:useCard``\ ,接收的参数是CardUseStruct。不行太复杂了,过一阵子再来看吧。
diff --git a/docs/dev/index.rst b/docs/dev/index.rst
new file mode 100644
index 00000000..edb2ac51
--- /dev/null
+++ b/docs/dev/index.rst
@@ -0,0 +1,16 @@
+Dev文档
+============
+
+.. toctree::
+ :maxdepth: 1
+
+ ai.rst
+ compile.rst
+ database.rst
+ gameevent.rst
+ gamelogic.rst
+ package.rst
+ protocol.rst
+ scenario.rst
+ todo.rst
+ ui.rst
diff --git a/doc/dev/package.md b/docs/dev/package.rst
similarity index 80%
rename from doc/dev/package.md
rename to docs/dev/package.rst
index 6ae87739..94371e8c 100644
--- a/doc/dev/package.md
+++ b/docs/dev/package.rst
@@ -1,30 +1,32 @@
-# FreeKill 的包管理策略
-
-> [dev](./index.md) > 包管理
-
-___
+FreeKill 的包管理策略
+=====================
FreeKill使用git进行包管理,具体而言是使用libgit2库进行管理。
-## 包的组织
+包的组织
+--------
-所有拓展包都位于packages/目录下。其中,standard(标包)、standard_cards(标包卡牌)和manuvering_cards(军争卡牌, TODO )属于基础拓展,其直接处于FreeKill的项目仓库之下。其他所有的拓展均处于项目之外。
+所有拓展包都位于packages/目录下。其中,standard(标包)、standard_cards(标包卡牌)和manuvering_cards(军争卡牌,
+TODO
+)属于基础拓展,其直接处于FreeKill的项目仓库之下。其他所有的拓展均处于项目之外。
每个拓展包都是一个单独的文件夹,内含代码文件(init.lua,以及诸如其他lua文件和fkp文件等等)和音图等资源文件。关于具体如何组织各种文件,请参照已有的拓展包。
-## 包的管理
+包的管理
+--------
包管理使用git,以下使用类似git命令行的方式解说。
首先,packages中除了基本的三个包之外,其他的包都要从仓库中排除掉。这方面由一个.gitignore文件控制。
-然后,在packages目录下,有一个名为packages.db的文件统领所有拓展包。这是个sqlite数据库,结构详见[数据库](./database.md)。
+然后,在packages目录下,有一个名为packages.db的文件统领所有拓展包。这是个sqlite数据库,结构详见\ :doc:`./database`\ 。
下面从连接过程中简要分析这个文件的作用:
1. 当一个客户端尝试对服务端发起连接请求的时候,首先它们之间会先比较MD5值。
2. 如果MD5通过则无事发生,否则服务端会把自己的packages.db中的关键信息发送给客户端。
-3. 客户端根据文件内容检查自己的拓展包。如果那个文件夹存在,那么就git fetch -> git checkout \。
+3. 客户端根据文件内容检查自己的拓展包。如果那个文件夹存在,那么就git
+ fetch -> git checkout 。
4. 如果文件夹不存在,那么先git clone,然后再checkout。
5. 做完这些后,客户端再次发起请求。若仍不通过,则向用户通知错误信息。
@@ -32,20 +34,24 @@ FreeKill使用git进行包管理,具体而言是使用libgit2库进行管理
有时候客户端会包含服务端所没有的拓展包,这时候比起直接删除之,更加明智的选择是将其标记为禁用。将拓展包文件夹的名字设为xxx.disabled即可将拓展包标记为禁用的拓展包。禁用的拓展包不会被游戏加载,也不会被MD5检测计入。
-## 包的托管
+包的托管
+--------
一般来说都是推荐将项目放在github上面的,但由于FreeKill暂且不考虑国际化且必须照顾广大玩家的体验,因此将拓展包托管到github可能不是一个明智的选择。推荐将拓展包托管到gitee平台,或者其他的好办法也行。
总之有一点要注意的是,packages.db中的url需要是国内访问比较方便的网站才行。
-## 包的部署
+包的部署
+--------
此处不讨论具体如何编写代码,单论在这个管理框架下如何进行开发。
-一般来说,在对一个仓库进行开发时,由于目前各托管平台都用SSH Key认证而非用户名密码,因此仓库的URL通常为git@gitxxx.com:xxxx/xxxx.git。这样的URL有一个问题就在于只有认证过的用户可以clone,而非所有人。
+一般来说,在对一个仓库进行开发时,由于目前各托管平台都用SSH
+Key认证而非用户名密码,因此仓库的URL通常为git@gitxxx.com:xxxx/xxxx.git。这样的URL有一个问题就在于只有认证过的用户可以clone,而非所有人。
因此在部署的时候,一定要保证所有url都是https://xxxx。这一点FreeKill是不会进行检测的。
-## 包的下载与更新(TODO)
+包的下载与更新(TODO)
+----------------------
客户端使用GUI,服务端使用Fk shell或者直接编辑packages.db。
diff --git a/doc/dev/protocol.md b/docs/dev/protocol.rst
similarity index 68%
rename from doc/dev/protocol.md
rename to docs/dev/protocol.rst
index 72604777..64359625 100644
--- a/doc/dev/protocol.md
+++ b/docs/dev/protocol.rst
@@ -1,49 +1,49 @@
-# FreeKill 的通信
+FreeKill 的通信
+===============
-> [dev](./index.md) > 通信
-
-___
-
-## 概述
+概述
+----
FreeKill使用UTF-8文本进行通信。基本的通信格式为JSON数组:
-`[requestId, packetType, command, jsonData]`
+``[requestId, packetType, command, jsonData]``
其中:
-- requestId用来在request型通信使用,用来确保收到的回复和发出的请求相对应。
-- packetType用来确定这条消息的类型以及发送的目的地。
-- command用来表示消息的类型。使用首字母大写的驼峰式命名,因为下划线命名会造成额外的网络开销。
-- jsonData保存着这个消息的额外信息,必须是一个JSON数组。数组中的具体内容详见源码及注释。
+- requestId用来在request型通信使用,用来确保收到的回复和发出的请求相对应。
+- packetType用来确定这条消息的类型以及发送的目的地。
+- command用来表示消息的类型。使用首字母大写的驼峰式命名,因为下划线命名会造成额外的网络开销。
+- jsonData保存着这个消息的额外信息,必须是一个JSON数组。数组中的具体内容详见源码及注释。
FreeKill通信有三大类型:请求(Request)、回复(Reply)和通知(Notification)。
-___
+--------------
-## 从连接上到进入大厅
+从连接上到进入大厅
+------------------
想要启动服务器,需要通过命令行终端:
-```sh
-$ ./FreeKill -s
-```
+.. code:: sh
-``是服务器运行的端口号,如果不带任何参数则启动GUI界面,在GUI界面里面只能加入服务器或者单机游戏。
+ $ ./FreeKill -s
+
+````\ 是服务器运行的端口号,如果不带任何参数则启动GUI界面,在GUI界面里面只能加入服务器或者单机游戏。
服务器以TCP方式监听。在默认情况下(比如单机启动),服务器的端口号是9527。
每当任何一个客户端连接上了之后,游戏会先进行以下流程:
-1. 检查IP是否被封禁。 // TODO: 数据库
+1. 检查IP是否被封禁。 // TODO: 数据库
2. 服务端将RSA公钥发给客户端,然后检查客户端的延迟是否小于30秒。
3. 在网络检测环节,若客户端网速达标的话,客户端应该会发回一个字符串。这个字符串保存着用户的用户名和RSA公钥加密后的密码,服务端检查这个字符串是否合法。如果合法,检查密码是否正确。
4. 上述检查都通过后,重连(TODO:)
-5. 不要重连的话,服务端便为新连接新建一个`ServerPlayer`对象,并将其添加到大厅中。
+5. 不要重连的话,服务端便为新连接新建一个\ ``ServerPlayer``\ 对象,并将其添加到大厅中。
-___
+--------------
-## 大厅和房间
+大厅和房间
+----------
大厅(Lobby)是一个比较特殊的房间。除了大厅之外,所有的房间都被作为游戏房间对待。
@@ -58,24 +58,27 @@ ___
1. 只要有玩家进入,就刷新一次房间列表。
2. 只要玩家变动,就更新大厅内人数(TODO:)
-> 因为上述特点都是通过信号槽实现的,通过阅读代码不易发现,故记录之。
+..
-___
+ 因为上述特点都是通过信号槽实现的,通过阅读代码不易发现,故记录之。
-## 对游戏内交互的实例分析
+--------------
+
+对游戏内交互的实例分析
+----------------------
下面围绕着askForSkillInvoke对游戏内的交互进行简析,其他交互也是一样的原理。
-```lua
-function Room:askForSkillInvoke(player, skill_name, data)
- local command = "AskForSkillInvoke"
- self:notifyMoveFocus(player, skill_name)
- local invoked = false
- local result = self:doRequest(player, command, skill_name)
- if result ~= "" then invoked = true end
- return invoked
-end
-```
+.. code:: lua
+
+ function Room:askForSkillInvoke(player, skill_name, data)
+ local command = "AskForSkillInvoke"
+ self:notifyMoveFocus(player, skill_name)
+ local invoked = false
+ local result = self:doRequest(player, command, skill_name)
+ if result ~= "" then invoked = true end
+ return invoked
+ end
在这期间,一共涉及两步走:
@@ -84,29 +87,29 @@ end
首先看第一步:通知。这里涉及的函数是doNotify。(调查notifyMoveFocus的代码即可知道)
-调查`ServerPlayer:doNotify`发现:
+调查\ ``ServerPlayer:doNotify``\ 发现:
-```lua
- self.serverplayer:doNotify(command, jsonData)
-```
+.. code:: lua
+
+ self.serverplayer:doNotify(command, jsonData)
这里的self.serverplayer,其实指的是C++中的ServerPlayer实例,因此这一行代码实际上调用的是C++中的ServerPlayer::doNotify。调查C++中对应的函数,发现实际上调用了Router::notify,调查Router::notify,发现发送了一个信号量,调查Router::setSocket发现这个信号量连接到了ClientSocket::send。调查ClientSocket::send后发现:
-```cpp
-void ClientSocket::send(const QByteArray &msg)
-{
- if (msg.length() >= 1024) {
- auto comp = qCompress(msg);
- auto _msg = "Compressed" + comp.toBase64() + "\n";
- socket->write(_msg);
- socket->flush();
- }
- socket->write(msg);
- if (!msg.endsWith("\n"))
- socket->write("\n");
- socket->flush();
-}
-```
+.. code:: cpp
+
+ void ClientSocket::send(const QByteArray &msg)
+ {
+ if (msg.length() >= 1024) {
+ auto comp = qCompress(msg);
+ auto _msg = "Compressed" + comp.toBase64() + "\n";
+ socket->write(_msg);
+ socket->flush();
+ }
+ socket->write(msg);
+ if (!msg.endsWith("\n"))
+ socket->write("\n");
+ socket->flush();
+ }
核心在于socket->write,这里其实就调用了QTcpSocket::write,正式向网络中发送数据。从前面的分析也慢慢可以发现,发送的其实就是json字符串。
@@ -116,55 +119,56 @@ void ClientSocket::send(const QByteArray &msg)
其中有这样的一段:
-```cpp
- if (type & TYPE_NOTIFICATION) {
- if (type & DEST_CLIENT) {
- ClientInstance->callLua(command, jsonData);
- }
-```
+.. code:: cpp
+
+ if (type & TYPE_NOTIFICATION) {
+ if (type & DEST_CLIENT) {
+ ClientInstance->callLua(command, jsonData);
+ }
调用了ClientInstance::callLua函数,这个函数不做详细追究,只要知道他调用了这个lua函数即可:
-```lua
- self.client.callback = function(_self, command, jsonData)
- local cb = fk.client_callback[command]
- if (type(cb) == "function") then
- cb(jsonData)
- else
- self:notifyUI(command, jsonData);
- end
- end
-```
+.. code:: lua
+
+ self.client.callback = function(_self, command, jsonData)
+ local cb = fk.client_callback[command]
+ if (type(cb) == "function") then
+ cb(jsonData)
+ else
+ self:notifyUI(command, jsonData);
+ end
+ end
至此,我们已经可以基本得出结论:Client在接收到信息时就根据信息的command类型调用相应的函数,若无则直接调用qml中的函数。
-接下来聊聊doRequest。和前面类似,doRequest最终也是向玩家发送了一个JSON字符串,但是然后它就进入了等待回复的状态。在此期间,可以使用waitForReply函数尝试获取对方的reply,若无则得到默认结果__notready,然后在Lua侧进行进一步处理。
+接下来聊聊doRequest。和前面类似,doRequest最终也是向玩家发送了一个JSON字符串,但是然后它就进入了等待回复的状态。在此期间,可以使用waitForReply函数尝试获取对方的reply,若无则得到默认结果\__notready,然后在Lua侧进行进一步处理。
客户在收到request类型的消息后,可以用reply对服务端进行答复。reply本身也是JSON字符串,服务端在handlePacket环节发觉这个是reply后,就知道自己已经收到回复了。这时用waitForReply即可得到正确的回复结果。
在Lua侧,对waitForReply其实有所封装:
-```lua
- while true do
- result = player.serverplayer:waitForReply(0)
- if result ~= "__notready" then
- return result
- end
- local rest = timeout * 1000 - (os.getms() - start) / 1000
- if timeout and rest <= 0 then
- return ""
- end
- coroutine.yield(rest)
- end
-```
+.. code:: lua
+
+ while true do
+ result = player.serverplayer:waitForReply(0)
+ if result ~= "__notready" then
+ return result
+ end
+ local rest = timeout * 1000 - (os.getms() - start) / 1000
+ if timeout and rest <= 0 then
+ return ""
+ end
+ coroutine.yield(rest)
+ end
这里就是一个死循环,不断的试图读取玩家的回复,直到超时为止。因为waitForReply指定的等待时间为0,所以会立刻返回(这也是为什么waitForReply在读取reply时需要加锁的原因,因为读取操作很频繁),此时若lua发现玩家并未给出答复,就会调用coroutine.yield切换到其他线程去做点别的事情(比如处理旁观请求,调用QThread::msleep睡眠一阵子等等),别的协程办完事情后再次切换回这个协程(yield函数返回),然后开启新一轮循环,如此往复直到等待时间耗尽或者收到了回复。
-___
+--------------
-## 对掉线的处理
+对掉线的处理
+------------
-因为每个连接都对应着一个`new ClientSocket`和`new ServerPlayer`,所以对于掉线的处理要慎重,处理不当会导致内存泄漏以及各种奇怪的错误。
+因为每个连接都对应着一个\ ``new ClientSocket``\ 和\ ``new ServerPlayer``\ ,所以对于掉线的处理要慎重,处理不当会导致内存泄漏以及各种奇怪的错误。
一般来说掉线有以下几种情况:
@@ -173,25 +177,26 @@ ___
3. 在未开始游戏的房间里面掉线。
4. 在已开始游戏的房间里掉线。
-首先对所有的这些情况,都应该把ClientSocket释放掉。这部分代码写在[server_socket.cpp](../../src/network/server_socket.cpp)里面。
+首先对所有的这些情况,都应该把ClientSocket释放掉。这部分代码写在\ `server_socket.cpp <../../src/network/server_socket.cpp>`__\ 里面。
对于2、3两种情况,都算是在游戏开始之前的房间中掉线。这种情况下直接从房间中删除这个玩家,并告诉其他玩家一声,然后从服务器玩家列表中也删除那名玩家。但对于情况3,因为从普通房间删除玩家的话,那名玩家会自动进入大厅,所以需要大厅再删除一次玩家。
对于情况4,因为游戏已经开始,所以不能直接删除玩家,需要把玩家的状态设为“离线”并继续游戏。在游戏结束后,若玩家仍未重连,则按情况2、3处理。
-> Note: 这部分处理见于ServerPlayer类的析构函数。
+ Note: 这部分处理见于ServerPlayer类的析构函数。
-___
+--------------
-## 断线重连
+断线重连
+--------
根据用户id找到掉线的那位玩家,将玩家的状态设置为“在线”,并将房间的状态都发送给他即可。
-但是为了[UI不出错](./ui.md#mainStack),依然需要对重连的玩家走一遍进大厅的流程。
+但是为了UI不出错,依然需要对重连的玩家走一遍进大厅的流程。
重连的流程应为:
-1. 总之先新建`ServerPlayer`并加到大厅
+1. 总之先新建\ ``ServerPlayer``\ 并加到大厅
2. 在默认的处理流程中,此时会提醒玩家“已经有同名玩家加入”,然后断掉连接。
3. 在这时可以改成:如果这个已经在线的玩家是Offline状态,那么就继续,否则断开。
4. pass之后,走一遍流程,把玩家加到大厅里面先。
@@ -204,10 +209,10 @@ ___
直接从UI着手:
-1. 首先EnterRoom消息,需要**人数**和**操作时长**。
-2. 既然需要人数了,那么就需要**所有玩家**。
+1. 首先EnterRoom消息,需要\ **人数**\ 和\ **操作时长**\ 。
+2. 既然需要人数了,那么就需要\ **所有玩家**\ 。
3. 此外还需要让玩家知道牌堆、弃牌堆、轮数之类的。
-4. 玩家的信息就更多了,武将、身份、血量、id...
+4. 玩家的信息就更多了,武将、身份、血量、id…
所以Lua要在某时候让出一段时间,处理重连等其他内容,可能还会处理一下AI。
@@ -215,28 +220,29 @@ ___
会阻塞住lua代码的函数有:
-- ServerPlayer:waitForReplay()
-- Room:delay()
+- ServerPlayer:waitForReplay()
+- Room:delay()
在这里让出主线程,然后调度函数查找目前的请求列表。事实上,整个Room的游戏主流程就是一个协程:
-```lua
--- room.lua:53
-local co_func = function()
- self:run()
-end
-local co = coroutine.create(co_func)
-while not self.game_finished do
- local ret, err_msg = coroutine.resume(co)
- ...
-end
-```
+.. code:: lua
+
+ -- room.lua:53
+ local co_func = function()
+ self:run()
+ end
+ local co = coroutine.create(co_func)
+ while not self.game_finished do
+ local ret, err_msg = coroutine.resume(co)
+ ...
+ end
如果在游戏流程中调用yield的话,那么这里的resume会返回true,然后可以带有额外的返回值。不过只要返回true就好了,这时候lua就可以做一些简单的任务。而这个简单的任务其实也可以另外写个协程解决。
-___
+--------------
-## 旁观(TODO)
+旁观(TODO)
+------------
因为房间不允许加入比玩家上限的玩家,可以考虑在房间里新建一个列表存储旁观中的玩家。但是这样或许会让某些处理(如掉线)变得复杂化。
diff --git a/doc/dev/scenario.md b/docs/dev/scenario.rst
similarity index 77%
rename from doc/dev/scenario.md
rename to docs/dev/scenario.rst
index badfd943..67b59dfc 100644
--- a/doc/dev/scenario.md
+++ b/docs/dev/scenario.rst
@@ -1,6 +1,5 @@
-# 关于扩展FreeKill玩法的思考
-
-___
+关于扩展FreeKill玩法的思考
+==========================
要扩展玩法,大概就这些:
@@ -14,14 +13,16 @@ ___
3. 加载GameRule后根据模式加载特殊规则
4. 开始玩
-___
+--------------
-## 拓展新规
+拓展新规
+--------
首先就是如何覆盖老规则,这个可以通过设置一个特殊tag
-___
+--------------
-## 拓展logic
+拓展logic
+---------
从GameLogic继承然后重写有关函数就行
diff --git a/doc/dev/todo.md b/docs/dev/todo.rst
similarity index 77%
rename from doc/dev/todo.md
rename to docs/dev/todo.rst
index 60e6c77e..54efe6a3 100644
--- a/doc/dev/todo.md
+++ b/docs/dev/todo.rst
@@ -1,29 +1,32 @@
-# TODO list
-
-___
+TODO list
+=========
本文档用来记载一些可能会需要实现但暂且无暇的想法。留待日后再做或者同伴帮忙做了。
-## 服务端的包验证
+服务端的包验证
+--------------
当客户端连接到服务端遇到MD5失败时,考虑:
1. 服务端除了告知失败之外还告知客户端自己的打包属性,即自己启用了哪些包,包的URL和版本等。
2. 客户端根据信息禁用掉不需要的包,下载没有的包,更新启用的包,将需要的包都切换到服务器提供的版本。
-___
+--------------
-## UI主题可拓展
+UI主题可拓展
+------------
考虑影响一下skin-bank.js,使其根据某个config的不同,将相应的值设为不同的路径。然后确保所有图片资源(关于页面和logo除外)都通过skin-bank.js访问所需图片。
由于所有拓展包都是只能通过init.lua访问,考虑为QmlBackend提供相应的Lua接口使其能够注册新的配置方案。配置文件本身的组织考虑JSON。
-考虑在skin-bank.js中加入更多信息,例如各个组件的x, y, width, height等。因为影响到的都是Image所以设置这些应该是够了。
+考虑在skin-bank.js中加入更多信息,例如各个组件的x, y, width,
+height等。因为影响到的都是Image所以设置这些应该是够了。
-___
+--------------
-## 对话框可拓展
+对话框可拓展
+------------
考虑劫营,严教等含有特殊交互框的例子。
@@ -33,10 +36,12 @@ ___
skin-bank.js的话依然可以用相对位置进行加载,这个理论上应该是不会被影响到。
-___
+--------------
-## 代码简洁化
+代码简洁化
+----------
-目前FK的lua代码中仍有不少地方的代码重用度不高,典型的例子是fk_ex.lua。考虑在这里用local function将重复代码合并一下。
+目前FK的lua代码中仍有不少地方的代码重用度不高,典型的例子是fk_ex.lua。考虑在这里用local
+function将重复代码合并一下。
-还有视为技/触发技/主动技这些能够被“发动”的技能(即继承于UsableSkill的基本),它们的技能生效环节都有很多重复,比如播放声音动画和具体生效等。考虑在某处用一个函数总结一下,至于具体生效部分可以包在一个函数里面`function() skill:onEffect(room, effect) end`然后作为参数传递到useskill函数中。如果后面要做SkillUsed之类的时机的话,这方面就更加重要了。
+还有视为技/触发技/主动技这些能够被“发动”的技能(即继承于UsableSkill的基本),它们的技能生效环节都有很多重复,比如播放声音动画和具体生效等。考虑在某处用一个函数总结一下,至于具体生效部分可以包在一个函数里面\ ``function() skill:onEffect(room, effect) end``\ 然后作为参数传递到useskill函数中。如果后面要做SkillUsed之类的时机的话,这方面就更加重要了。
diff --git a/doc/dev/ui.md b/docs/dev/ui.rst
similarity index 50%
rename from doc/dev/ui.md
rename to docs/dev/ui.rst
index b4821bfb..4bb23120 100644
--- a/doc/dev/ui.md
+++ b/docs/dev/ui.rst
@@ -1,86 +1,88 @@
-# FreeKill 的UI
+FreeKill 的UI
+=============
-> [dev](./index.md) > UI
+概述
+----
-___
+FreeKill的UI系统使用Qt
+Quick开发。UI依赖\ `QmlBackend <../../src/ui/qmlbackend.h>`__\ 调用需要的C++函数。关于这方面也可参考\ `main.cpp <../../src/main.cpp>`__\ 。
-## 概述
-
-FreeKill的UI系统使用Qt Quick开发。UI依赖[QmlBackend](../../src/ui/qmlbackend.h)调用需要的C++函数。关于这方面也可参考[main.cpp](../../src/main.cpp)。
-
-> Note: 我感觉QmlBackend这种实现方式很尴尬。
+ Note: 我感觉QmlBackend这种实现方式很尴尬。
整体UI采用StackView进行页面切换之类的。
-___
+--------------
-## mainStack
+mainStack
+---------
-mainStack定义于[main.qml](../../qml/main.qml)中。它以堆栈的形式保存着所有的页面,页面在栈中的顺序需要像这样排布:
+mainStack定义于\ `main.qml <../../qml/main.qml>`__\ 中。它以堆栈的形式保存着所有的页面,页面在栈中的顺序需要像这样排布:
-- (栈底)登录界面,Init.qml
-- 大厅,Lobby.qml
-- 别的什么页面
+- (栈底)登录界面,Init.qml
+- 大厅,Lobby.qml
+- 别的什么页面
-___
+--------------
-## config
+config
+------
Config.qml存储一些客户端需要用到的设置或者即将发送的数据,(TODO)
----
+--------------
-## Room和RoomLogic
+Room和RoomLogic
+---------------
这部分是整个UI体系中最复杂的一部分,其中尤以手牌区的操作为甚。下面来整理一下与出牌相关的UI逻辑。
首先要指明一个常用函数:
-```cpp
- Q_INVOKABLE QString callLuaFunction(const QString &func_name,
- QVariantList params);
-```
+.. code:: cpp
+
+ Q_INVOKABLE QString callLuaFunction(const QString &func_name,
+ QVariantList params);
该函数声明位于qmlbackend.h中,第一个参数是函数名,必须是lua的全局函数,第二个列表是参数列表。lua一侧应当返回字符串/数字/布尔值,然后再在这里转成QString并返回qml中。这就是qml调用lua函数的核心。
然后来说说Room。Room中一共有4种状态,分别是:
-- notactive: 平常的不活跃状态。在此期间牌都是暗的,不能操作。
-- playing: 出牌阶段主动出牌状态。
-- responding: 需要选择响应使用/打出的状态。
-- replying: 需要操作对话框以回应服务器的状态。
+- notactive: 平常的不活跃状态。在此期间牌都是暗的,不能操作。
+- playing: 出牌阶段主动出牌状态。
+- responding: 需要选择响应使用/打出的状态。
+- replying: 需要操作对话框以回应服务器的状态。
notactive和replying不是本次的重点,重点在于playing和responding中关于手牌区的操作。
先看Room.qml中关于切换到这两个状态后的动作是什么:
-```js
-Transition {
- from: "*"; to: "playing"
- ScriptAction {
- script: {
- dashboard.enableCards();
- dashboard.enableSkills();
- progress.visible = true;
- okCancel.visible = true;
- endPhaseButton.visible = true;
- respond_play = false;
- }
- }
-},
+.. code:: js
-Transition {
- from: "*"; to: "responding"
- ScriptAction {
- script: {
- dashboard.enableCards(responding_card);
- dashboard.enableSkills(responding_card);
- progress.visible = true;
- okCancel.visible = true;
- }
- }
-},
-```
+ Transition {
+ from: "*"; to: "playing"
+ ScriptAction {
+ script: {
+ dashboard.enableCards();
+ dashboard.enableSkills();
+ progress.visible = true;
+ okCancel.visible = true;
+ endPhaseButton.visible = true;
+ respond_play = false;
+ }
+ }
+ },
+
+ Transition {
+ from: "*"; to: "responding"
+ ScriptAction {
+ script: {
+ dashboard.enableCards(responding_card);
+ dashboard.enableSkills(responding_card);
+ progress.visible = true;
+ okCancel.visible = true;
+ }
+ }
+ },
其中,涉及到的值得注意的函数是enableCards和enableSkills,这里只关心前者。
diff --git a/docs/diy/01-env.rst b/docs/diy/01-env.rst
new file mode 100644
index 00000000..30864c2c
--- /dev/null
+++ b/docs/diy/01-env.rst
@@ -0,0 +1,239 @@
+Fk DIY - 环境搭建
+=================
+
+DIY总览
+-------
+
+正如项目README所言,FreeKill“试图打造一个最适合diy玩家游玩的民间三国杀”。即便是最开始游戏功能尚未完善,FreeKill也已经具备了对DIY的支持。所有拓展包都列在packages/文件夹下,感兴趣者可以自行查看。
+
+欲为FreeKill进行DIY,需要使用的编程语言为Lua。若您对Lua语言完全不熟悉,推荐去\ `菜鸟教程 `__\ 速通一遍基本语法。剩下的就基本是在实践中慢慢领会了。
+
+FreeKill本体中自带有标准包和标准卡牌包,可作为DIY时候的例子。事实上,其他DIY包也是像这样子组织的。
+
+接下来讲述如何配置环境。
+
+--------------
+
+环境搭建
+--------
+
+Fk
+~~
+
+Fk是游戏本身,也是拓展包运行的平台。事实上这份文档应该与Fk一同发布的,如果您正在阅读这份文档,那么您理应已经接收到了Fk本身。
+
+代码编辑器
+~~~~~~~~~~
+
+代码编辑器任选一种即可,但一定要确保以下几点:
+
+- 至少要是一款\ **代码**\ 编辑器,要有语法高亮功能
+- 需要有EmmyLua插件的支持
+- 需要默认UTF-8格式保存代码文件
+
+..
+
+ EmmyLua是一种特别的Lua注释方式,可以为本来弱类型的Lua语言提供类型支持,这对于像FreeKill这种稍有规模的Lua项目是十分必要的。目前能提供开箱即用的EmmyLua插件编辑器主要有IntelliJ
+ IDEA和Visual Studio
+ Code。EmmyLua也能以LSP的方式运行,因此支持LSP的编辑器(这种就多了,比如vim,
+ sublime)也能符合条件。
+
+编辑器的具体安装以及插件配置不在此赘述。
+
+ 出于易用性和免费的考虑,推荐用VSCode进行拓展。下文将以VSCode为编辑器进行进一步说明。
+
+git
+~~~
+
+git就不必多介绍了吧,这里说说为什么需要配置git。这是因为在Fk中,拓展包拥有在线安装/在线更新的功能,这种功能都是依托于git进行的,因此如果你打算将自己的拓展包发布出去的话,就需要将其创建git仓库,并托管到git托管网站去。
+
+ 考虑到国内绝大部分人的访问速度,综合国内几家git托管平台,建议使用gitee。
+
+大多数人可能从未用过git,并且git上手的门槛并不低,因此以下会对涉及git的操作进行详尽的解说。
+
+安装git
+^^^^^^^
+
+前往\ `官网 `__\ 下载git,下载64-bit
+Git for Windows Setup。这样应该会为您下载一个exe安装包。
+
+考虑到官网的下载链接实际上指向github,而且可能连官网的都进不去,所以也考虑\ `从清华源下载Git `__\ 。
+
+欲验证安装是否完成,可以按下Win+R ->
+cmd弹出命令行窗口,输入git命令,如果出来一长串英文说明安装成功了。
+
+--------------
+
+新增mod
+-------
+
+这只是新增mod的一个例子。当然了,以后有啥要做的实例也会继续用这个拓展包的。
+
+首先前往packages下,新建名为fk_study的文件夹。
+
+再在fk_study下新建init.lua文件,写入以下内容:
+
+.. code:: lua
+
+ local extension = Package("fk_study")
+
+ Fk:loadTranslationTable{
+ ["fk_study"] = "fk学习包",
+ }
+
+ return { extension }
+
+保存退出,打开Fk,进武将一览。你现在应该能在武将一览里面看到“fk学习包”了,但也仅此而已了,毕竟这还只是个空壳包而已。
+
+至此我们已经创建了最为简单的mod。mod的文件结构如下:
+
+::
+
+ fk_study
+ └── init.lua
+
+--------------
+
+发布mod
+-------
+
+一种最常见的发布mod方式是把mod打包成zip,发到公共平台上供玩家下载。这种办法虽然可行,但并不是fk推荐的做法。
+
+ 以下介绍的其实就是新建仓库并推送到gitee的办法,熟悉git者请跳过。
+
+下面着重介绍用git发布mod的办法。使用git进行发布的话,就可以让用户体验在线安装、在线更新等便捷之处。
+
+以下假设你使用vscode进行代码编辑。你是先用vscode打开了整个FreeKill文件夹,再在其中新建文件夹和文件、然后进行编辑的。
+
+菜单栏 -> 终端 -> 新建终端。我们接下来的工作都在终端中完成。
+
+将终端切换为Git Bash
+~~~~~~~~~~~~~~~~~~~~
+
+启动终端后,终端的内容大概是:
+
+.. code::
+
+ Mincrosoft Windows 10 [版本号啥的]
+ xxxxxxxx 保留所有权利。
+
+ C:\FreeKill>
+
+这个是Windows自带的cmd,我们不使用这个,而是去用git
+bash。此时终端上面应该有这么一条:
+
+.. code::
+
+ 问题 输出 调试控制台 _终端_ cmd + v 分屏 删除
+ 注意这个加号
+
+这时候点击加号右边那个下拉箭头,选择”Git Bash”。这样就成功的切换到了git
+bash中,终端看起来应该像这样:
+
+.. code::
+
+ xxx@xxxxx MINGW64 /c/FreeKill
+ $
+
+配置ssh key
+~~~~~~~~~~~
+
+你应该已经注册好了自己的gitee账号。首先在Git
+bash中输入这些命令(#号后面的是命令注释,不用照搬;命令开头的$符号是模拟shell的界面,不要输入进去):
+
+.. code:: bash
+
+ $ cd ~/.ssh
+ $ ssh-keygen -t rsa -C "你注册用的邮箱地址" # 换成自己真正的邮箱
+ # 出来一堆东西,一路点回车就是了
+ $ cat id_rsa.pub
+ # 出来一堆乱七八糟的东西:ssh-rsa <一大堆乱七八糟的内容> <你的邮箱>
+ $ cd -
+
+在cat
+id_rsa.pub中,出来的那一堆以ssh-rsa的输出,就是这里要用到的“公钥”。然后在gitee中:
+
+1. 点右上角你的头像,点账号设置
+2. 点左侧栏中 安全设置 - SSH公钥
+3. 此时弹出公钥添加界面,标题任选,下面公钥那一栏中,将刚刚生成的公钥复制粘贴上去
+4. 点确定
+
+这样就配置好了ssh公钥。进行验证,在bash中使用命令:
+
+::
+
+ $ ssh -T git@gitee.com
+ Hi xxxx! You've successfully authenticated, but GITEE.COM does not provide shell access.
+
+输出像Hi
+xxx!这样的信息,就说明配置成功了。否则需要进一步检查自己的操作,上网查一下吧。
+
+新建git仓库
+~~~~~~~~~~~
+
+现在终端的工作目录应该还是FreeKill根目录,我们先切换到mod的目录去,然后再在shell中进行一系列操作。
+
+.. code:: sh
+
+ $ cd packages/fk_study
+ $ git init # 创建新的空仓库
+ $ git add . # 将文件夹中所有的文件都加入暂存区
+ $ git commit -m "init" # 提交目前所有的文件,这样文件就正式存在于仓库里面了
+ 作者身份未知
+ *** 请告诉我您是谁。
+ 运行
+ git config --global user.email "you@example.com"
+ git config --global user.name "Your Name"
+
+ 来设置您账号的缺省身份标识。如果仅在本仓库设置身份标识,则省略 --global 参数。
+
+看来我们初次安装Git,Git还不知道我们的身份呢,不过git已经告诉了配置所需的命令了。运行前一条命令告知自己的名字,运行后一条命令告知自己的邮箱。如此就OK了,然后再commit一次。
+
+然后在gitee中也新建一个仓库,取名为fk_study。接下来回到终端里面:
+
+.. code:: sh
+
+ $ git remote add origin git@gitee.com:xxx/fk_study # 其中这个xxx是你的用户名
+ $ git push -u origin master
+
+OK了,刷新你新建的那个仓库的页面,可以看到里面已经有init.lua了。此时距离发布mod只有最后一步,那就是把仓库设置为开源。请自行在gitee中设置吧。
+
+让他人安装并游玩你的mod
+~~~~~~~~~~~~~~~~~~~~~~~
+
+注意到Fk初始界面里面的“管理拓展包”了不?这个就是让你安装、删除、更新拓展包用的。在那个页面里面有个输入框,在浏览器中复制仓库的地址(比如https://gitee.com/xxx/fk_study/
+),粘贴到输入框,然后单击“从URL安装”即可安装拓展包了。
+
+更新mod
+~~~~~~~
+
+现在mod要发生更新了,更新内容为一个武将。先在init.lua中新增武将吧。
+
+.. code:: lua
+
+ local study_sunce = General(extension, "study_sunce", "wu", 4)
+ Fk:loadTranslationTable{
+ ["study_sunce"] = "孙伯符",
+ }
+
+保存,此时注意vscode左侧栏变成了:
+
+::
+
+ v fk_study
+ └── init.lua M
+
+init.lua后面出现了“M”,并且文件名字也变成了黄色,这表示这个文件已经被修改过了,接下来我们把修改文件提交到仓库中:
+
+.. code:: sh
+
+ $ git add . # 将当前目录下的文件暂存
+ $ git commit -m "add general sunce" # 提交更改,提交说明为add general sunce
+ $ git push # “推”到远端,也就是把本地的更新传给远端
+
+不喜欢用命令行的话,也可以用vscode自带的git支持完成这些操作,这里就不赘述了。做完git
+push后,实际上就已经完成更新了,可以让大伙点点更新按钮来更新你的新版本了。
+
+--------------
+
+以上介绍了大致的创建mod以及更新的流程。至于资源文件组织等等杂七杂八的问题,请参考已有的例子拓展包。
diff --git a/docs/diy/02-skilltype.rst b/docs/diy/02-skilltype.rst
new file mode 100644
index 00000000..f1a4e2bd
--- /dev/null
+++ b/docs/diy/02-skilltype.rst
@@ -0,0 +1,45 @@
+fk技能类型总览
+==============
+
+fk的目的是便于三国杀的DIY,而三国杀DIY的核心就是制作各种技能了。
+
+fk的技能分为两大类,这两大类又各自细分为更小的分类:
+
+(关于这部分的源码详见lua/core/skill.lua和lua/core/skill_type下的所有文件)
+
+- 可使用类技能(UsableSkill)
+
+ - 触发技(TriggerSkill):在满足一定条件时,能够通过被动触发发挥效果的技能
+ - 主动技(ActiveSkill):玩家主动发动的技能
+ - 视为技(ViewAsSkill):将一张牌当做另一张牌的技能
+
+- 状态技(StatusSkill)
+
+ - 距离技(DistanceSkill):影响距离计算的技能
+ - 攻击范围技(DistanceSkill):影响攻击范围计算的技能
+ - 手牌上限技(MaxCardsSkill):影响手牌上限计算的技能
+ - 禁止技(ProhibitSkill):禁止成为卡牌目标的技能
+ - 卡牌增强技(TargetModSkill):影响卡牌使用次数上限、目标上限、距离限制等等的技能
+ - 锁定视为技(FilterSkill):让一张牌强制视为另一张牌的技能
+
+其中,触发技的逻辑最为复杂,但是\ `已经在这里分析过了 <../dev/gamelogic.rst>`__\ ,故不再赘述。
+
+主动技和状态技应该不算难,先按下不表。视为技与神杀有所区别,区别如下:
+
+在神杀中,视为技是否可响应是专门写在enabled_at_response的,fk则不然,看倾国的代码:
+
+.. code:: lua
+
+ local qingguo = fk.CreateViewAsSkill{
+ name = "qingguo",
+ anim_type = "defensive",
+ pattern = "jink",
+ card_filter = function(self, to_select, selected)
+ -- ...
+ end,
+ view_as = function(self, cards)
+ -- ...
+ end,
+ }
+
+可见并没有编写跟响应时候有关的函数,也没有声明出牌阶段不可用。其中的奥妙就在于pattern中,视为技可以转化的卡牌都应该写在pattern里面,Fk会根据pattern的内容判断技能出牌阶段是否可用、是否能够响应等。
diff --git a/docs/diy/03-events.rst b/docs/diy/03-events.rst
new file mode 100644
index 00000000..1307e16e
--- /dev/null
+++ b/docs/diy/03-events.rst
@@ -0,0 +1,14 @@
+fk中的游戏事件
+==============
+
+在进行DIY时,需要对三国杀的规则有一定了解;在编写技能时,也要熟悉游戏提供的各种事件,他的触发方式、触发时机、相关数据。必须要知道这些才能写出正确的代码。
+
+.. toctree::
+ :maxdepth: 1
+ :caption: 事件列表
+
+ event/gameflow.rst
+ event/hp.rst
+ event/usecard.rst
+ event/movecard.rst
+ event/misc.rst
diff --git a/docs/diy/03-newgeneral.rst b/docs/diy/03-newgeneral.rst
new file mode 100644
index 00000000..89dabad5
--- /dev/null
+++ b/docs/diy/03-newgeneral.rst
@@ -0,0 +1,199 @@
+创建武将并添加技能
+==================
+
+欲创建技能,必先有武将;而想要创建武将,先要创建拓展包。拓展包是一个武将的容身之处。
+
+之前的fk_study应该都还在吧?我们看看在搭建环境的那一章中,我们所编写的代码:
+
+.. code:: lua
+
+ local extension = Package("fk_study")
+
+ Fk:loadTranslationTable{
+ ["fk_study"] = "fk学习包",
+ }
+
+ local study_sunce = General(extension, "study_sunce", "wu", 4)
+ Fk:loadTranslationTable{
+ ["study_sunce"] = "孙伯符",
+ }
+
+ return { extension }
+
+在这个Lua文件中,我们创建了一个拓展包,并往拓展包中添加了一名武将。
+
+--------------
+
+创建拓展包
+----------
+
+创建拓展包的格式基本是固定的,在Lua文件的第一行写上这样的:
+
+::
+
+ local extension = Package("xxxx")
+
+其中xxxx为拓展包的名字,可以随意填写。然后在Lua的最后一行写上:
+
+::
+
+ return { extension }
+
+这样就能让fk知道这有个拓展包了,于是fk就能读取并将其加载到游戏里面。
+
+--------------
+
+翻译
+----
+
+fk的编程约定之一就是不要在代码中含有中文。需要显示为中文的部分,应该单独写在“翻译表”里面,而在主体代码涉及的字符串应使用英文,或者自定义的变量名。
+
+加载翻译表的基本格式为:
+
+.. code:: lua
+
+ Fk:loadTranslationTable{
+ ["源文本"] = "译文",
+ ......
+ }
+
+像这样就可以插入许多条翻译了。
+
+--------------
+
+创建武将
+--------
+
+创建武将的格式为:
+
+::
+
+ local xxx = General(拓展包, 武将名, 势力, 体力值, 体力上限, 性别)
+
+其中:
+
+- 拓展包表示武将所在的拓展包,无脑extension完事
+- 武将名是武将的内部名称,不要和别人重复了。如果你在做自己的拓展包的话建议加前缀
+- 势力是武将的势力,目前有这几种:\ ``"wei"``\ ,\ ``"shu"``\ ,\ ``"wu"``\ ,\ ``"qun"``\ ,\ ``"god"``\ ,分别代表魏蜀吴群神
+- 体力值是武将的初始体力值
+- 体力上限是武将的体力上限,可以不写,不写的话默认等于体力值
+- 性别是武将的性别,默认为男性,有以下几种取值可能:
+
+ - ``General.Male``\ :男性
+ - ``General.Female``\ :女性
+
+--------------
+
+为武将添加游戏已有技能
+----------------------
+
+fk本身不内置多少技能,但玩家还是可以给武将添加已有的技能,避免重复劳动。
+
+比如我们的fk_study包,现在要给白板孙伯符加一个技能“制衡”,那么可以这样写:
+
+::
+
+ study_sunce:addSkill("zhiheng")
+
+这样的一行代码必须在创建武将之后再添加。也就是说,添加之后,Lua文件大概像这样:
+
+::
+
+ local study_sunce = General(extension, "study_sunce", "wu", 4)
+ study_sunce:addSkill("zhiheng") -- 在这里新增
+ Fk:loadTranslationTable{
+ ["study_sunce"] = "孙伯符",
+ }
+
+保存一下,进游戏就能发现多了个技能。
+
+.. figure:: https://upload-images.jianshu.io/upload_images/21666547-da0d53b6996941de.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240
+ :alt: 添加已有技能
+
+ 添加已有技能
+
+--------------
+
+为武将制图
+----------
+
+此时我们还没有为武将制作图片。没有图片的武将,默认会用貂蝉的剪影作为图片。(其实手刹里面未知武将是周瑜的剪影,不过谁在乎呢)
+
+总之我们来为武将制图。首先找一张心仪的图片。然后我们要找一个切图的软件,用PS也好,gimp也好,都随意,这里不赘述怎么用软件。
+
+fk中,武将的图片应该为250x292分辨率,并且是jpg格式。为了观感舒适,武将的人脸应该位于图片的中上方。
+
+.. figure:: https://upload-images.jianshu.io/upload_images/21666547-7b08fd53820d4160.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240
+ :alt: 使用GIMP切图。我倾向于开5x5参考线,并让人脸位于2行3列的格子里面
+
+ 使用GIMP切图。我倾向于开5x5参考线,并让人脸位于2行3列的格子里面
+
+.. figure:: https://upload-images.jianshu.io/upload_images/21666547-a629150ce8a4eac8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240
+ :alt: 使用GIMP切图后,将尺寸缩到需要的分辨率
+
+ 使用GIMP切图后,将尺寸缩到需要的分辨率
+
+最后用jpg格式导出图片,图片的名字是武将的内部名称,在这里就是study_sunce。
+
+.. figure:: https://upload-images.jianshu.io/upload_images/21666547-7093b57e9cb53118.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240
+ :alt: 导出JPG
+
+ 导出JPG
+
+注意了,JPG图片的质量不能拉到100%,不然图片体积会很大,给他人下载你的拓展包带来不便。一般质量为90为好,此时图片大约三四十KB大小。这里图像质量只调了60,这样看起来不至于完全失真,图片的体积也相当较小。
+
+至此我们做好了图片,接下来就是把图片放到游戏去。
+
+去我们的拓展包文件夹,新建文件夹image,再在里面新建文件夹generals,把图丢进去。这样一来,拓展包的文件结构如下:
+
+::
+
+ packages/fk_study
+ ├── image
+ │ └── generals
+ │ └── study_sunce.jpg
+ └── init.lua
+
+然后打开游戏就能看到武将的图片了:
+
+.. figure:: https://upload-images.jianshu.io/upload_images/21666547-faafcd3e899f241b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240
+ :alt: 效果还不错吧
+
+ 效果还不错吧
+
+--------------
+
+为武将制作阵亡语音
+------------------
+
+每个武将都有自己的阵亡语音。fk采用mp3格式保存语音。
+
+怎么处理mp3音频就不叙述了,可以考虑用audacity这款软件调节mp3的音量、去掉首尾的延迟等等。但是依然需要注意一点——mp3语音的体积不能太大了。为此我的建议是使用格式工厂对mp3文件再进行一次格式转换,将转换后mp3文件的码率设为128kbps,这样一来一句语音差不多就是三四十KB的感觉,而音质却不至于非常模糊。
+
+阵亡语音放到拓展包文件夹下的audio/death里面,命名规则是武将的内部名称。如图所示:
+
+.. figure:: https://upload-images.jianshu.io/upload_images/21666547-1ce5c371b425638e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240
+ :alt: 阵亡语音的命名,以及存放位置
+
+ 阵亡语音的命名,以及存放位置
+
+--------------
+
+更新拓展包
+----------
+
+我们也做了这么多了,是时候更新一下了。
+
+在我们拓展包文件夹那里右键一下,Git Bash Here,然后:
+
+.. code:: sh
+
+ $ git add .
+ $ git commit -m "image and audio for sunce"
+ $ git push
+
+至此,就完成了拓展包的更新。其他使用你的拓展包的人此时就能通过fk拓展包管理的“更新拓展包”功能,更新到你所做的这个状态。
+
+(我自己在写这一系列的文章的时候,也是确实创建了一个拓展包仓库的。
+https://gitee.com/notify-ctrl/fk_study
+如有疑问,可以去查看那个仓库是怎么弄的。)
diff --git a/docs/diy/04-newskill.rst b/docs/diy/04-newskill.rst
new file mode 100644
index 00000000..2dd35ced
--- /dev/null
+++ b/docs/diy/04-newskill.rst
@@ -0,0 +1,108 @@
+创建新技能
+==========
+
+fk体量实在太小,只有标包,欲玩到更多技能,还是得自己亲自动手才行。
+
+如何快速上手编写技能?答案是——复制粘贴。直接复制已有的技能代码以为己用,并且推敲那段技能代码的原理,无疑是上手的最快方法。
+
+本文将实现这样一个技能:“摸牌阶段,你可以多摸4张牌。”
+
+很明显,这个技能就是一个加强版的英姿。将英姿的代码复制过来,稍微改改即可。接下来和大家细说怎么来复制粘贴。
+
+--------------
+
+首先,fk所有的技能都是Lua编写的,而本着开源精神也没有对Lua代码进行任何特殊处理,因此你可以直接在fk的release版中找到lua代码。英姿是标包的,因此去packages/standard/init.lua,可以找到英姿的代码:
+
+.. code:: lua
+
+ local yingzi = fk.CreateTriggerSkill{
+ name = "yingzi",
+ anim_type = "drawcard",
+ events = {fk.DrawNCards},
+ on_use = function(self, event, target, player, data)
+ data.n = data.n + 1
+ end,
+ }
+
+好的,复制过来。注意到标包lua里面,英姿在定义武将周瑜的前面,所以我们也把技能粘贴到武将的前面。完事之后,把技能加给武将。至此我们的lua会像这样:
+
+.. code:: lua
+
+ local yingzi = fk.CreateTriggerSkill{
+ name = "yingzi",
+ anim_type = "drawcard",
+ events = {fk.DrawNCards},
+ on_use = function(self, event, target, player, data)
+ data.n = data.n + 1
+ end,
+ }
+ local study_sunce = General(extension, "study_sunce", "wu", 4)
+ study_sunce:addSkill("zhiheng")
+ study_sunce:addSkill(yingzi)
+ Fk:loadTranslationTable{
+ ["study_sunce"] = "孙伯符",
+ }
+
+启动游戏试试看,却给我们甩了个报错:
+
+.. image:: https://upload-images.jianshu.io/upload_images/21666547-b032b4f43ad13b58.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240
+
+原来是这个复制粘贴的技能和已有的英姿重复了。解法很简单,换个名字就行了,这里改名为“激姿”好了。按照命名习惯,为他起一个内部名称”study_jizi”。然后把所有的yingzi都改成这个名,改名后如下:
+
+.. code:: lua
+
+ local study_jizi = fk.CreateTriggerSkill{
+ name = "study_jizi",
+ anim_type = "drawcard",
+ events = {fk.DrawNCards},
+ on_use = function(self, event, target, player, data)
+ data.n = data.n + 1
+ end,
+ }
+ local study_sunce = General(extension, "study_sunce", "wu", 4)
+ study_sunce:addSkill("zhiheng")
+ study_sunce:addSkill(study_jizi)
+
+重新启动一下游戏,发现正常了,但是只能多摸一张。解法很简单,那句\ ``data.to = data.to + 1``\ 不就是让摸牌数+1吗,那我改成+4就行了,直接把1改成4:
+
+.. code:: lua
+
+ data.n = data.n + 4
+
+还有一件事,我们没给技能加翻译,往翻译表加上:
+
+.. code:: lua
+
+ ["study_jizi"] = "激姿",
+ [":study_jizi"] = "摸牌阶段,你可以多摸4张牌。",
+
+至此完事了。别忘了更新一下git,后面不赘述关于git的事情了。
+
+.. figure:: https://upload-images.jianshu.io/upload_images/21666547-f4c76ee91f8c15ae.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240
+ :alt: 搞定,一摸就是6张,薄纱神郭嘉
+
+ 搞定,一摸就是6张,薄纱神郭嘉
+
+--------------
+
+稍微解说一下创建技能的语法
+--------------------------
+
+我们再来回头看看刚才复制粘贴的代码。
+
+首先可以看出,技能是通过\ ``fk.CreateTriggerSkill``\ 创建的。在这个函数名中,Create意为创建,TriggerSkill则是我们要创建的技能类型——触发技。要创建其他技能也一样,都是通过CreateXXXSkill创建的。
+
+然后,对于所有技能,我们都要为其指派一个name,用来标记这个技能的名字。这个技能的名字必须是唯一的,不能和其他任何技能产生冲突,最广泛的避免重名的方法就是给技能加上一些前缀。
+
+然后有些技能还指派anim_type。这个其实可有可无,它控制的是技能发动时该播放哪种动效,有以下几种取值:
+
+- ``special``\ :留空anim_type时候的默认特效。看上去像一条龙的特效,一般用于定位模糊的技能。
+- ``drawcard``\ :看上去像是凤凰展翅的特效,用于主打摸牌的技能。
+- ``control``\ :看上去像草的特效,用于拆牌等控场类技能。
+- ``offensive``\ :看上去像火焰的特效,用于菜刀技能或者直伤等攻击性技能。
+- ``support``\ :看上去像莲花的特效,用于给牌、回血等辅助性技能。
+- ``defensive``\ :看上去像花的特效,用于防御流技能。
+- ``negative``\ :看上去像乌云的特效,用于负面技能。
+- ``masochism``\ :看上去像金色的花的特效,用于卖血类技能。(这个类型取名也是沿用了神杀的恶趣味啊)
+
+这些特效的图片素材位于image/anim/skillInvoke中。你可以改变技能的anim_type一一查看,或者直接去看素材也行。但是记住一点,这个属性除了控制技能触发的特效之外,和技能本身并没有任何联系,你想指定啥都行。
diff --git a/docs/diy/event/gameflow.rst b/docs/diy/event/gameflow.rst
new file mode 100644
index 00000000..66f7a881
--- /dev/null
+++ b/docs/diy/event/gameflow.rst
@@ -0,0 +1,34 @@
+与游戏流程有关的事件
+====================
+
+先来看游戏流程本身。以下节选自lua/server/gamelogic.lua
+
+.. code:: lua
+
+ function GameLogic:action()
+ self:trigger(fk.GameStart)
+ local room = self.room
+
+ for _, p in ipairs(room.alive_players) do
+ self:trigger(fk.DrawInitialCards, p, { num = 4 })
+ end
+
+ local function checkNoHuman()
+ -- 如果房里已经没有人类玩家了就结束游戏
+ end
+
+ while true do
+ self:trigger(fk.TurnStart, room.current)
+ if room.game_finished then break end
+ room.current = room.current:getNextAlive()
+ if checkNoHuman() then
+ room:gameOver("")
+ end
+ end
+ end
+
+以上这段代码,述说的就是整个游戏流程的核心。首先开始游戏、摸初始手牌,然后按照座位顺序每人依次执行回合直到游戏结束。
+
+--------------
+
+TODO
diff --git a/docs/diy/event/hp.rst b/docs/diy/event/hp.rst
new file mode 100644
index 00000000..88fd0b4a
--- /dev/null
+++ b/docs/diy/event/hp.rst
@@ -0,0 +1,14 @@
+与体力值相关的事件
+==================
+
+伤害
+----
+
+失去体力/体力上限
+-----------------
+
+回复体力
+--------
+
+濒死和死亡
+----------
diff --git a/docs/diy/event/misc.rst b/docs/diy/event/misc.rst
new file mode 100644
index 00000000..74edbb22
--- /dev/null
+++ b/docs/diy/event/misc.rst
@@ -0,0 +1,2 @@
+杂项事件
+=============
diff --git a/docs/diy/event/movecard.rst b/docs/diy/event/movecard.rst
new file mode 100644
index 00000000..34d4e4e8
--- /dev/null
+++ b/docs/diy/event/movecard.rst
@@ -0,0 +1,2 @@
+移动牌相关的事件
+=====================
diff --git a/docs/diy/event/usecard.rst b/docs/diy/event/usecard.rst
new file mode 100644
index 00000000..f0596449
--- /dev/null
+++ b/docs/diy/event/usecard.rst
@@ -0,0 +1,2 @@
+使用牌相关的事件
+====================
diff --git a/docs/diy/index.rst b/docs/diy/index.rst
new file mode 100644
index 00000000..0778341b
--- /dev/null
+++ b/docs/diy/index.rst
@@ -0,0 +1,11 @@
+Diy文档
+===============
+
+.. toctree::
+ :maxdepth: 1
+
+ 01-env.rst
+ 02-skilltype.rst
+ 03-newgeneral.rst
+ 04-newskill.rst
+ 03-events.rst
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 00000000..c31f303a
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,14 @@
+.. FreeKill documentation master file, created by
+ sphinx-quickstart on Sun Mar 26 02:58:53 2023.
+ You can adapt this file completely to your liking, but it should at least
+ contain the root `toctree` directive.
+
+欢迎来到FreeKill文档!
+====================================
+
+.. toctree::
+ :maxdepth: 1
+
+ diy/index.rst
+ dev/index.rst
+ api/index.rst
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 00000000..52e0f74b
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,2 @@
+sphinx-lua
+sphinx-rtd-theme
diff --git a/lua/client/client.lua b/lua/client/client.lua
index 41321537..275e233a 100644
--- a/lua/client/client.lua
+++ b/lua/client/client.lua
@@ -1,11 +1,11 @@
---@class Client
----@field client fk.Client
----@field players ClientPlayer[]
----@field alive_players ClientPlayer[]
----@field observers ClientPlayer[]
----@field current ClientPlayer
----@field discard_pile integer[]
----@field status_skills Skill[]
+---@field public client fk.Client
+---@field public players ClientPlayer[]
+---@field public alive_players ClientPlayer[]
+---@field public observers ClientPlayer[]
+---@field public current ClientPlayer
+---@field public discard_pile integer[]
+---@field public status_skills Skill[]
Client = class('Client')
-- load client classes
diff --git a/lua/client/client_util.lua b/lua/client/client_util.lua
index 8ea5cbc3..6eee0b18 100644
--- a/lua/client/client_util.lua
+++ b/lua/client/client_util.lua
@@ -161,7 +161,6 @@ end
---@param card string | integer
---@param to_select integer @ id of the target
---@param selected integer[] @ ids of selected targets
----@param selected_cards integer[] @ ids of selected cards
function CanUseCardToTarget(card, to_select, selected)
if ClientInstance:getPlayerById(to_select).dead then
return "false"
@@ -192,7 +191,6 @@ end
---@param card string | integer
---@param to_select integer @ id of a card not selected
----@param selected integer[] @ ids of selected cards
---@param selected_targets integer[] @ ids of selected players
function CanSelectCardForSkill(card, to_select, selected_targets)
local c ---@type Card
@@ -209,7 +207,6 @@ function CanSelectCardForSkill(card, to_select, selected_targets)
end
---@param card string | integer
----@param selected integer[] @ ids of selected cards
---@param selected_targets integer[] @ ids of selected players
function CardFeasible(card, selected_targets)
local c ---@type Card
diff --git a/lua/client/clientplayer.lua b/lua/client/clientplayer.lua
index b39d23a9..365a20f3 100644
--- a/lua/client/clientplayer.lua
+++ b/lua/client/clientplayer.lua
@@ -1,7 +1,7 @@
---@class ClientPlayer: Player
----@field player fk.Player
----@field known_cards integer[]
----@field global_known_cards integer[]
+---@field public player fk.Player
+---@field public known_cards integer[]
+---@field public global_known_cards integer[]
local ClientPlayer = Player:subclass("ClientPlayer")
function ClientPlayer:initialize(cp)
diff --git a/lua/core/card.lua b/lua/core/card.lua
index dfdf5f92..34c6aeda 100644
--- a/lua/core/card.lua
+++ b/lua/core/card.lua
@@ -1,18 +1,18 @@
---@class Card : Object
----@field package Package
----@field name string
----@field suit Suit
----@field number integer
----@field trueName string
----@field color Color
----@field id integer
----@field type CardType
----@field sub_type CardSubtype
----@field area CardArea
----@field subcards integer[]
----@field skillName string @ for virtual cards
----@field skill Skill
----@field special_skills string[] | nil
+---@field public id integer
+---@field public package Package
+---@field public name string
+---@field public suit Suit
+---@field public number integer
+---@field public trueName string
+---@field public color Color
+---@field public type CardType
+---@field public sub_type CardSubtype
+---@field public area CardArea
+---@field public subcards integer[]
+---@field public skillName string @ for virtual cards
+---@field public skill Skill
+---@field public special_skills string[] | nil
local Card = class("Card")
---@alias Suit integer
@@ -224,6 +224,10 @@ end
---@param c integer|integer[]|Card|Card[]
---@return integer[]
+function Card:getIdList(c)
+ error("This is a static method. Please use Card:getIdList instead")
+end
+
function Card.static:getIdList(c)
if type(c) == "number" then
return {c}
diff --git a/lua/core/card_type/equip.lua b/lua/core/card_type/equip.lua
index ddfa2dd5..1b474cfb 100644
--- a/lua/core/card_type/equip.lua
+++ b/lua/core/card_type/equip.lua
@@ -1,5 +1,5 @@
---@class EquipCard : Card
----@field equip_skill Skill
+---@field public equip_skill Skill
local EquipCard = Card:subclass("EquipCard")
function EquipCard:initialize(name, suit, number)
diff --git a/lua/core/engine.lua b/lua/core/engine.lua
index b0cd1e3f..7223999a 100644
--- a/lua/core/engine.lua
+++ b/lua/core/engine.lua
@@ -1,19 +1,29 @@
+--- Engine是整个FreeKill赖以运行的核心。
+---
+--- 它包含了FreeKill涉及的所有武将、卡牌、游戏模式等等
+---
+--- 同时也提供了许多常用的函数。
+---
---@class Engine : Object
----@field packages table
----@field package_names string[]
----@field skills table
----@field related_skills table
----@field global_trigger TriggerSkill[]
----@field global_status_skill table
----@field generals table
----@field same_generals table
----@field lords string[]
----@field cards Card[]
----@field translations table>
----@field game_modes table
----@field disabled_packs string[]
+---@field public packages table @ 所有拓展包的列表
+---@field public package_names string[] @ 含所有拓展包名字的数组,为了方便排序
+---@field public skills table @ 所有的技能
+---@field public related_skills table @ 所有技能的关联技能
+---@field public global_trigger TriggerSkill[] @ 所有的全局触发技
+---@field public global_status_skill table @ 所有的全局状态技
+---@field public generals table @ 所有武将
+---@field public same_generals table @ 所有同名武将组合
+---@field public lords string[] @ 所有主公武将,用于常备主公
+---@field public cards Card[] @ 所有卡牌
+---@field public translations table> @ 翻译表
+---@field public game_modes table @ 所有游戏模式
+---@field public disabled_packs string[] @ 禁用的拓展包列表
local Engine = class("Engine")
+--- Engine的构造函数。
+---
+--- 这个函数只应该被执行一次。执行了之后,会创建一个Engine实例,并放入全局变量Fk中。
+---@return nil
function Engine:initialize()
-- Engine should be singleton
if Fk ~= nil then
@@ -41,7 +51,10 @@ function Engine:initialize()
self:addSkills(AuxSkills)
end
----@param pack Package
+--- 向Engine中加载一个拓展包。
+---
+--- 会加载这个拓展包含有的所有武将、卡牌以及游戏模式。
+---@param pack Package @ 要加载的拓展包
function Engine:loadPackage(pack)
assert(pack:isInstanceOf(Package))
if self.packages[pack.name] ~= nil then
@@ -60,6 +73,14 @@ function Engine:loadPackage(pack)
self:addGameModes(pack.game_modes)
end
+--- 加载所有拓展包。
+---
+--- Engine会在packages/下搜索所有含有init.lua的文件夹,并把它们作为拓展包加载进来。
+---
+--- 这样的init.lua可以返回单个拓展包,也可以返回拓展包数组,或者什么都不返回。
+---
+--- 标包和标准卡牌包比较特殊,它们永远会在第一个加载。
+---@return nil
function Engine:loadPackages()
local directories = FileIO.ls("packages")
@@ -88,7 +109,9 @@ function Engine:loadPackages()
end
end
----@param t table
+--- 向翻译表中加载新的翻译表。
+---@param t table @ 要加载的翻译表,这是一个 原文 --> 译文 的键值对表
+---@param lang string|nil @ 目标语言,默认为zh_CN
function Engine:loadTranslationTable(t, lang)
assert(type(t) == "table")
lang = lang or "zh_CN"
@@ -98,6 +121,8 @@ function Engine:loadTranslationTable(t, lang)
end
end
+--- 翻译一段文本。其实就是从翻译表中去找
+---@param src string @ 要翻译的文本
function Engine:translate(src)
local lang = Config.language or "zh_CN"
if not self.translations[lang] then lang = "zh_CN" end
@@ -105,7 +130,12 @@ function Engine:translate(src)
return ret or src
end
----@param skill Skill
+--- 向Engine中加载一个技能。
+---
+--- 如果技能是global的,那么同时会将其放到那些global技能表中。
+---
+--- 如果技能有关联技能,那么递归地加载那些关联技能。
+---@param skill Skill @ 要加载的技能
function Engine:addSkill(skill)
assert(skill.class:isSubclassOf(Skill))
if self.skills[skill.name] ~= nil then
@@ -128,7 +158,8 @@ function Engine:addSkill(skill)
end
end
----@param skills Skill[]
+--- 加载一系列技能。
+---@param skills Skill[] @ 要加载的技能数组
function Engine:addSkills(skills)
assert(type(skills) == "table")
for _, skill in ipairs(skills) do
@@ -136,7 +167,10 @@ function Engine:addSkills(skills)
end
end
----@param general General
+--- 加载一个武将到Engine中。
+---
+--- 如果武将的trueName和name不同的话,那么也会将其加到同将清单中。
+---@param general General @ 要添加的武将
function Engine:addGeneral(general)
assert(general:isInstanceOf(General))
if self.generals[general.name] ~= nil then
@@ -151,7 +185,8 @@ function Engine:addGeneral(general)
end
end
----@param generals General[]
+--- 加载一系列武将。
+---@param generals General[] @ 要加载的武将列表
function Engine:addGenerals(generals)
assert(type(generals) == "table")
for _, general in ipairs(generals) do
@@ -159,7 +194,11 @@ function Engine:addGenerals(generals)
end
end
----@param name string
+--- 根据武将名称,获取它的同名武将。
+---
+--- 注意以此法返回的同名武将列表不包含他自己。
+---@param name string @ 要查询的武将名字
+---@return string[] @ 这个武将对应的同名武将列表
function Engine:getSameGenerals(name)
local tmp = name:split("__")
local tName = tmp[#tmp]
@@ -172,7 +211,11 @@ end
local cardId = 1
local _card_name_table = {}
----@param card Card
+
+--- 向Engine中加载一张卡牌。
+---
+--- 卡牌在加载的时候,会被赋予一个唯一的id。(从1开始)
+---@param card Card @ 要加载的卡牌
function Engine:addCard(card)
assert(card.class:isSubclassOf(Card))
card.id = cardId
@@ -183,16 +226,20 @@ function Engine:addCard(card)
end
end
----@param cards Card[]
+--- 向Engine中加载一系列卡牌。
+---@param cards Card[] @ 要加载的卡牌列表
function Engine:addCards(cards)
for _, card in ipairs(cards) do
self:addCard(card)
end
end
----@param name string
----@param suit Suit
----@param number integer
+--- 根据牌名、花色、点数,复制一张牌。
+---
+--- 返回的牌是一张虚拟牌。
+---@param name string @ 牌名
+---@param suit Suit @ 花色
+---@param number integer @ 点数
---@return Card
function Engine:cloneCard(name, suit, number)
local cd = _card_name_table[name]
@@ -202,14 +249,16 @@ function Engine:cloneCard(name, suit, number)
return ret
end
----@param game_modes GameMode[]
+--- 向Engine中添加一系列游戏模式。
+---@param game_modes GameMode[] @ 要添加的游戏模式列表
function Engine:addGameModes(game_modes)
for _, s in ipairs(game_modes) do
self:addGameMode(s)
end
end
----@param game_mode GameMode
+--- 向Engine中添加一个游戏模式。
+---@param game_mode GameMode @ 要添加的游戏模式
function Engine:addGameMode(game_mode)
assert(game_mode:isInstanceOf(GameMode))
if self.game_modes[game_mode.name] ~= nil then
@@ -218,11 +267,16 @@ function Engine:addGameMode(game_mode)
self.game_modes[game_mode.name] = game_mode
end
----@param num integer
----@param generalPool General[]
----@param except string[]
----@param filter function
----@return General[]
+--- 从已经开启的拓展包中,随机选出若干名武将。
+---
+--- 对于同名武将不会重复选取。
+---
+--- 如果符合条件的武将不够,那么就不能保证能选出那么多武将。
+---@param num integer @ 要选出的武将数量
+---@param generalPool General[] | nil @ 选择的范围,默认是已经启用的所有武将
+---@param except string[] | nil @ 特别要排除掉的武将名列表,默认是空表
+---@param filter fun(g: General): boolean | nil @ 可选参数,若这个函数返回true的话这个武将被排除在外
+---@return General[] @ 随机选出的武将列表
function Engine:getGeneralsRandomly(num, generalPool, except, filter)
if filter then
assert(type(filter) == "function")
@@ -263,8 +317,9 @@ function Engine:getGeneralsRandomly(num, generalPool, except, filter)
return result
end
----@param except General[]
----@return General[]
+--- 获取已经启用的所有武将的列表。
+---@param except General[] | nil @ 特别指明要排除在外的武将
+---@return General[] @ 所有武将的列表
function Engine:getAllGenerals(except)
local result = {}
for _, general in pairs(self.generals) do
@@ -278,8 +333,9 @@ function Engine:getAllGenerals(except)
return result
end
----@param except integer[]
----@return integer[]
+--- 获取当前已经启用的所有卡牌。
+---@param except integer[] | nil @ 特别指定要排除在外的id列表
+---@return integer[] @ 所有卡牌id的列表
function Engine:getAllCardIds(except)
local result = {}
for _, card in ipairs(self.cards) do
@@ -295,9 +351,10 @@ end
local filtered_cards = {}
----@param id integer
----@param ignoreFilter boolean
----@return Card
+--- 根据id返回相应的卡牌。
+---@param id integer @ 牌的id
+---@param ignoreFilter boolean @ 是否要无视掉锁定视为技,直接获得真牌
+---@return Card @ 这个id对应的卡牌
function Engine:getCardById(id, ignoreFilter)
local ret = self.cards[id]
if not ignoreFilter then
@@ -306,9 +363,10 @@ function Engine:getCardById(id, ignoreFilter)
return ret
end
----@param id integer
----@param player Player
----@param data any @ may be JudgeStruct
+--- 对那个id应用锁定视为技,将它变成要被锁定视为的牌。
+---@param id integer @ 要处理的id
+---@param player Player @ 和这张牌扯上关系的那名玩家
+---@param data any @ 随意,目前只用到JudgeStruct,为了影响判定牌
function Engine:filterCard(id, player, data)
local card = self:getCardById(id, true)
if player == nil then
@@ -370,6 +428,8 @@ function Engine:filterCard(id, player, data)
end
end
+--- 获知当前的Engine是跑在服务端还是客户端,并返回相应的实例。
+---@return Room | Client
function Engine:currentRoom()
if RoomInstance then
return RoomInstance
@@ -377,6 +437,11 @@ function Engine:currentRoom()
return ClientInstance
end
+--- 根据字符串获得这个技能或者这张牌的描述
+---
+--- 其实就是翻译了 ":" .. name 罢了
+---@param name string @ 要获得描述的名字
+---@return string @ 描述
function Engine:getDescription(name)
return self:translate(":" .. name)
end
diff --git a/lua/core/exppattern.lua b/lua/core/exppattern.lua
index 6459c2de..a949b85d 100644
--- a/lua/core/exppattern.lua
+++ b/lua/core/exppattern.lua
@@ -17,13 +17,13 @@
]]--
---@class Matcher
----@field name string[]
----@field number integer[]
----@field suit string[]
----@field place string[]
----@field generalName string[]
----@field cardType string[]
----@field id integer[]
+---@field public name string[]
+---@field public number integer[]
+---@field public suit string[]
+---@field public place string[]
+---@field public generalName string[]
+---@field public cardType string[]
+---@field public id integer[]
local numbertable = {
["A"] = 1,
@@ -205,7 +205,7 @@ local function parseMatcher(str)
end
---@class Exppattern: Object
----@field matchers Matcher[]
+---@field public matchers Matcher[]
local Exppattern = class("Exppattern")
function Exppattern:initialize(spec)
@@ -219,7 +219,12 @@ function Exppattern:initialize(spec)
end
end
----@param str string
+---@param pattern string
+---@return Exppattern
+function Exppattern:Parse(pattern)
+ error("This is a static method. Please use Exppattern:Parse instead")
+end
+
function Exppattern.static:Parse(str)
local ret = Exppattern:new()
local t = str:split(";")
diff --git a/lua/core/game_mode.lua b/lua/core/game_mode.lua
index 799329c8..44729729 100644
--- a/lua/core/game_mode.lua
+++ b/lua/core/game_mode.lua
@@ -1,9 +1,9 @@
---@class GameMode: Object
----@field name string
----@field minPlayer integer
----@field maxPlayer integer
----@field rule TriggerSkill
----@field logic fun()
+---@field public name string
+---@field public minPlayer integer
+---@field public maxPlayer integer
+---@field public rule TriggerSkill
+---@field public logic fun()
local GameMode = class("GameMode")
function GameMode:initialize(name, min, max)
diff --git a/lua/core/general.lua b/lua/core/general.lua
index 28e62f66..5a95439b 100644
--- a/lua/core/general.lua
+++ b/lua/core/general.lua
@@ -1,13 +1,13 @@
---@class General : Object
----@field package Package
----@field name string
----@field trueName string
----@field kingdom string
----@field hp integer
----@field maxHp integer
----@field gender Gender
----@field skills Skill[]
----@field other_skills string[]
+---@field public package Package
+---@field public name string
+---@field public trueName string
+---@field public kingdom string
+---@field public hp integer
+---@field public maxHp integer
+---@field public gender Gender
+---@field public skills Skill[]
+---@field public other_skills string[]
General = class("General")
---@alias Gender integer
diff --git a/lua/core/package.lua b/lua/core/package.lua
index 97ff11b8..cc560fff 100644
--- a/lua/core/package.lua
+++ b/lua/core/package.lua
@@ -1,12 +1,16 @@
+--- Package用来描述一个FreeKill拓展包。
+---
+--- 所谓拓展包,就是武将/卡牌/游戏模式的一个集合而已。
+---
---@class Package : Object
----@field name string
----@field extensionName string
----@field type PackageType
----@field generals General[]
----@field extra_skills Skill[]
----@field related_skills table
----@field cards Card[]
----@field game_modes GameMode[]
+---@field public name string @ 拓展包的名字
+---@field public extensionName string @ 拓展包对应的mod的名字。 `详情... `_
+---@field public type PackageType @ 拓展包的类别,只会影响到选择拓展包的界面
+---@field public generals General[] @ 拓展包包含的所有武将的列表
+---@field public extra_skills Skill[] @ 拓展包包含的额外技能,即不属于武将的技能
+---@field public related_skills table @ 对于额外技能而言的关联技能
+---@field public cards Card[] @ 拓展包包含的卡牌
+---@field public game_modes GameMode[] @ 拓展包包含的游戏模式
local Package = class("Package")
---@alias PackageType integer
@@ -15,6 +19,9 @@ Package.GeneralPack = 1
Package.CardPack = 2
Package.SpecialPack = 3
+--- 拓展包的构造函数。
+---@param name string @ 包的名字
+---@param _type integer|nil @ 包的类型,默认为武将包
function Package:initialize(name, _type)
assert(type(name) == "string")
assert(type(_type) == "nil" or type(_type) == "number")
@@ -29,6 +36,9 @@ function Package:initialize(name, _type)
self.game_modes = {}
end
+--- 获得这个包涉及的所有技能。
+---
+--- 这也就是说,所有的武将技能再加上和武将无关的技能。
---@return Skill[]
function Package:getSkills()
local ret = {table.unpack(self.related_skills)}
@@ -42,26 +52,31 @@ function Package:getSkills()
return ret
end
----@param general General
+--- 向拓展包中添加武将。
+---@param general General @ 要添加的武将
function Package:addGeneral(general)
assert(general.class and general:isInstanceOf(General))
table.insertIfNeed(self.generals, general)
end
----@param card Card
+--- 向拓展包中添加卡牌。
+---@param card Card @ 要添加的卡牌
function Package:addCard(card)
assert(card.class and card:isInstanceOf(Card))
card.package = self
table.insert(self.cards, card)
end
----@param cards Card[]
+--- 向拓展包中一次添加许多牌。
+---@param cards Card[] @ 要添加的卡牌的数组
function Package:addCards(cards)
for _, card in ipairs(cards) do
self:addCard(card)
end
end
+--- 向拓展包中添加游戏模式。
+---@param game_mode GameMode @ 要添加的游戏模式。
function Package:addGameMode(game_mode)
table.insert(self.game_modes, game_mode)
end
diff --git a/lua/core/player.lua b/lua/core/player.lua
index 63862431..05ad4396 100644
--- a/lua/core/player.lua
+++ b/lua/core/player.lua
@@ -1,31 +1,35 @@
+--- 玩家分为客户端要处理的玩家,以及服务端处理的玩家两种。
+---
+--- 客户端能知道的玩家的信息十分有限,而服务端知道一名玩家的所有细节。
+---
+--- Player类就是这两种玩家的基类,包含它们共用的部分。
+---
---@class Player : Object
----@field id integer
----@field hp integer
----@field maxHp integer
----@field kingdom string
----@field role string
----@field general string
----@field gender integer
----@field handcard_num integer
----@field seat integer
----@field next Player
----@field phase Phase
----@field faceup boolean
----@field chained boolean
----@field dying boolean
----@field dead boolean
----@field state string
----@field player_skills Skill[]
----@field derivative_skills table
----@field flag string[]
----@field tag table
----@field mark table
----@field player_cards table
----@field virtual_equips Card[]
----@field special_cards table
----@field cardUsedHistory table
----@field skillUsedHistory table
----@field fixedDistance table
+---@field public id integer @ 玩家的id,每名玩家的id是唯一的。机器人的id是负数。
+---@field public hp integer @ 体力值
+---@field public maxHp integer @ 体力上限
+---@field public kingdom string @ 势力
+---@field public role string @ 身份
+---@field public general string @ 武将
+---@field public gender integer @ 性别
+---@field public seat integer @ 座位号
+---@field public next Player @ 下家
+---@field public phase Phase @ 当前阶段
+---@field public faceup boolean @ 是否正面朝上
+---@field public chained boolean @ 是否被横直
+---@field public dying boolean @ 是否处于濒死
+---@field public dead boolean @ 是否死亡
+---@field public player_skills Skill[] @ 当前拥有的所有技能
+---@field public derivative_skills table @ 当前拥有的派生技能
+---@field public flag string[] @ 当前拥有的flag,不过好像没用过
+---@field public tag table @ 当前拥有的所有tag,好像也没用过
+---@field public mark table @ 当前拥有的所有标记,用烂了
+---@field public player_cards table @ 当前拥有的所有牌,键是区域,值是id列表
+---@field public virtual_equips Card[] @ 当前的虚拟装备牌,其实也包含着虚拟延时锦囊这种
+---@field public special_cards table @ 类似“屯田”这种的私人牌堆
+---@field public cardUsedHistory table @ 用牌次数历史记录
+---@field public skillUsedHistory table @ 发动技能次数的历史记录
+---@field public fixedDistance table @ 与其他玩家的固定距离列表
local Player = class("Player")
---@alias Phase integer
@@ -52,6 +56,7 @@ Player.HistoryTurn = 2
Player.HistoryRound = 3
Player.HistoryGame = 4
+--- 构造函数。总之这不是随便调用的函数
function Player:initialize()
self.id = 114514
self.hp = 0
diff --git a/lua/core/skill.lua b/lua/core/skill.lua
index 494429f0..10926d88 100644
--- a/lua/core/skill.lua
+++ b/lua/core/skill.lua
@@ -1,13 +1,13 @@
---@class Skill : Object
----@field name string
----@field trueName string
----@field package Package
----@field frequency Frequency
----@field visible boolean
----@field mute boolean
----@field anim_type string
----@field related_skills Skill[]
----@field attached_equip string
+---@field public name string
+---@field public trueName string
+---@field public package Package
+---@field public frequency Frequency
+---@field public visible boolean
+---@field public mute boolean
+---@field public anim_type string
+---@field public related_skills Skill[]
+---@field public attached_equip string
local Skill = class("Skill")
---@alias Frequency integer
diff --git a/lua/core/skill_type/active.lua b/lua/core/skill_type/active.lua
index efa09c31..7e81ed87 100644
--- a/lua/core/skill_type/active.lua
+++ b/lua/core/skill_type/active.lua
@@ -1,12 +1,12 @@
---@class ActiveSkill : UsableSkill
----@field min_target_num integer
----@field max_target_num integer
----@field target_num integer
----@field target_num_table integer[]
----@field min_card_num integer
----@field max_card_num integer
----@field card_num integer
----@field card_num_table integer[]
+---@field public min_target_num integer
+---@field public max_target_num integer
+---@field public target_num integer
+---@field public target_num_table integer[]
+---@field public min_card_num integer
+---@field public max_card_num integer
+---@field public card_num integer
+---@field public card_num_table integer[]
local ActiveSkill = UsableSkill:subclass("ActiveSkill")
function ActiveSkill:initialize(name)
diff --git a/lua/core/skill_type/status_skill.lua b/lua/core/skill_type/status_skill.lua
index 869c48f8..7bf2df30 100644
--- a/lua/core/skill_type/status_skill.lua
+++ b/lua/core/skill_type/status_skill.lua
@@ -1,5 +1,5 @@
---@class StatusSkill : Skill
----@field global boolean
+---@field public global boolean
local StatusSkill = Skill:subclass("StatusSkill")
function StatusSkill:initialize(name, frequency)
diff --git a/lua/core/skill_type/trigger.lua b/lua/core/skill_type/trigger.lua
index 19e78d6d..25bc5917 100644
--- a/lua/core/skill_type/trigger.lua
+++ b/lua/core/skill_type/trigger.lua
@@ -1,8 +1,8 @@
---@class TriggerSkill : UsableSkill
----@field global boolean
----@field events Event[]
----@field refresh_events Event[]
----@field priority_table table
+---@field public global boolean
+---@field public events Event[]
+---@field public refresh_events Event[]
+---@field public priority_table table
local TriggerSkill = UsableSkill:subclass("TriggerSkill")
function TriggerSkill:initialize(name, frequency)
diff --git a/lua/core/skill_type/usable_skill.lua b/lua/core/skill_type/usable_skill.lua
index d7edf41f..efb62aac 100644
--- a/lua/core/skill_type/usable_skill.lua
+++ b/lua/core/skill_type/usable_skill.lua
@@ -1,6 +1,6 @@
---@class UsableSkill : Skill
----@field max_use_time integer[]
----@field expand_pile string
+---@field public max_use_time integer[]
+---@field public expand_pile string
local UsableSkill = Skill:subclass("UsableSkill")
function UsableSkill:initialize(name, frequency)
diff --git a/lua/core/skill_type/view_as.lua b/lua/core/skill_type/view_as.lua
index aa787f70..efcc5513 100644
--- a/lua/core/skill_type/view_as.lua
+++ b/lua/core/skill_type/view_as.lua
@@ -1,5 +1,5 @@
---@class ViewAsSkill : UsableSkill
----@field pattern string @ cards that can be viewAs'ed by this skill
+---@field public pattern string @ cards that can be viewAs'ed by this skill
local ViewAsSkill = UsableSkill:subclass("ViewAsSkill")
function ViewAsSkill:initialize(name)
diff --git a/lua/core/util.lua b/lua/core/util.lua
index c5b5377a..2b9e9855 100644
--- a/lua/core/util.lua
+++ b/lua/core/util.lua
@@ -41,7 +41,7 @@ function table.filter(self, func)
end
---@param func fun(element, index, array)
-function table.map(self, func)
+function table:map(func)
local ret = {}
for i, v in ipairs(self) do
table.insert(ret, func(v, i, self))
@@ -142,11 +142,11 @@ end
---@param self T[]
---@param n integer
---@return T|T[]
-function table.random(tab, n)
+function table:random(n)
local n0 = n
n = n or 1
- if #tab == 0 then return nil end
- local tmp = {table.unpack(tab)}
+ if #self == 0 then return nil end
+ local tmp = {table.unpack(self)}
local ret = {}
while n > 0 and #tmp > 0 do
local i = math.random(1, #tmp)
diff --git a/lua/fk_ex.lua b/lua/fk_ex.lua
index b52fc2ab..6e3c6bbb 100644
--- a/lua/fk_ex.lua
+++ b/lua/fk_ex.lua
@@ -57,25 +57,25 @@ local function readStatusSpecToSkill(skill, spec)
end
---@class UsableSkillSpec: UsableSkill
----@field max_phase_use_time integer
----@field max_turn_use_time integer
----@field max_round_use_time integer
----@field max_game_use_time integer
+---@field public max_phase_use_time integer
+---@field public max_turn_use_time integer
+---@field public max_round_use_time integer
+---@field public max_game_use_time integer
---@class StatusSkillSpec: StatusSkill
---@alias TrigFunc fun(self: TriggerSkill, event: Event, target: ServerPlayer, player: ServerPlayer):boolean
---@class TriggerSkillSpec: UsableSkillSpec
----@field global boolean
----@field events Event | Event[]
----@field refresh_events Event | Event[]
----@field priority number | table
----@field on_trigger TrigFunc
----@field can_trigger TrigFunc
----@field on_cost TrigFunc
----@field on_use TrigFunc
----@field on_refresh TrigFunc
----@field can_refresh TrigFunc
+---@field public global boolean
+---@field public events Event | Event[]
+---@field public refresh_events Event | Event[]
+---@field public priority number | table
+---@field public on_trigger TrigFunc
+---@field public can_trigger TrigFunc
+---@field public on_cost TrigFunc
+---@field public on_use TrigFunc
+---@field public on_refresh TrigFunc
+---@field public can_refresh TrigFunc
---@param spec TriggerSkillSpec
---@return TriggerSkill
@@ -140,14 +140,14 @@ function fk.CreateTriggerSkill(spec)
end
---@class ActiveSkillSpec: UsableSkillSpec
----@field can_use fun(self: ActiveSkill, player: Player): boolean
----@field card_filter fun(self: ActiveSkill, to_select: integer, selected: integer[], selected_targets: integer[]): boolean
----@field target_filter fun(self: ActiveSkill, to_select: integer, selected: integer[], selected_cards: integer[]): boolean
----@field feasible fun(self: ActiveSkill, selected: integer[], selected_cards: integer[]): boolean
----@field on_use fun(self: ActiveSkill, room: Room, cardUseEvent: CardUseStruct): boolean
----@field about_to_effect fun(self: ActiveSkill, room: Room, cardEffectEvent: CardEffectEvent): boolean
----@field on_effect fun(self: ActiveSkill, room: Room, cardEffectEvent: CardEffectEvent): boolean
----@field on_nullified fun(self: ActiveSkill, room: Room, cardEffectEvent: CardEffectEvent): boolean
+---@field public can_use fun(self: ActiveSkill, player: Player): boolean
+---@field public card_filter fun(self: ActiveSkill, to_select: integer, selected: integer[], selected_targets: integer[]): boolean
+---@field public target_filter fun(self: ActiveSkill, to_select: integer, selected: integer[], selected_cards: integer[]): boolean
+---@field public feasible fun(self: ActiveSkill, selected: integer[], selected_cards: integer[]): boolean
+---@field public on_use fun(self: ActiveSkill, room: Room, cardUseEvent: CardUseStruct): boolean
+---@field public about_to_effect fun(self: ActiveSkill, room: Room, cardEffectEvent: CardEffectEvent): boolean
+---@field public on_effect fun(self: ActiveSkill, room: Room, cardEffectEvent: CardEffectEvent): boolean
+---@field public on_nullified fun(self: ActiveSkill, room: Room, cardEffectEvent: CardEffectEvent): boolean
---@param spec ActiveSkillSpec
---@return ActiveSkill
@@ -171,11 +171,11 @@ function fk.CreateActiveSkill(spec)
end
---@class ViewAsSkillSpec: UsableSkillSpec
----@field card_filter fun(self: ViewAsSkill, to_select: integer, selected: integer[]): boolean
----@field view_as fun(self: ViewAsSkill, cards: integer[])
----@field pattern string
----@field enabled_at_play fun(self: ViewAsSkill, player: Player): boolean
----@field enabled_at_response fun(self: ViewAsSkill, player: Player): boolean
+---@field public card_filter fun(self: ViewAsSkill, to_select: integer, selected: integer[]): boolean
+---@field public view_as fun(self: ViewAsSkill, cards: integer[])
+---@field public pattern string
+---@field public enabled_at_play fun(self: ViewAsSkill, player: Player): boolean
+---@field public enabled_at_response fun(self: ViewAsSkill, player: Player): boolean
---@param spec ViewAsSkillSpec
---@return ViewAsSkill
@@ -204,7 +204,7 @@ function fk.CreateViewAsSkill(spec)
end
---@class DistanceSpec: StatusSkillSpec
----@field correct_func fun(self: DistanceSkill, from: Player, to: Player)
+---@field public correct_func fun(self: DistanceSkill, from: Player, to: Player)
---@param spec DistanceSpec
---@return DistanceSkill
@@ -220,10 +220,10 @@ function fk.CreateDistanceSkill(spec)
end
---@class ProhibitSpec: StatusSkillSpec
----@field is_prohibited fun(self: ProhibitSkill, from: Player, to: Player, card: Card)
----@field prohibit_use fun(self: ProhibitSkill, player: Player, card: Card)
----@field prohibit_response fun(self: ProhibitSkill, player: Player, card: Card)
----@field prohibit_discard fun(self: ProhibitSkill, player: Player, card: Card)
+---@field public is_prohibited fun(self: ProhibitSkill, from: Player, to: Player, card: Card)
+---@field public prohibit_use fun(self: ProhibitSkill, player: Player, card: Card)
+---@field public prohibit_response fun(self: ProhibitSkill, player: Player, card: Card)
+---@field public prohibit_discard fun(self: ProhibitSkill, player: Player, card: Card)
---@param spec ProhibitSpec
---@return ProhibitSkill
@@ -242,7 +242,7 @@ function fk.CreateProhibitSkill(spec)
end
---@class AttackRangeSpec: StatusSkillSpec
----@field correct_func fun(self: AttackRangeSkill, from: Player, to: Player)
+---@field public correct_func fun(self: AttackRangeSkill, from: Player, to: Player)
---@param spec AttackRangeSpec
---@return AttackRangeSkill
@@ -258,8 +258,8 @@ function fk.CreateAttackRangeSkill(spec)
end
---@class MaxCardsSpec: StatusSkillSpec
----@field correct_func fun(self: MaxCardsSkill, player: Player)
----@field fixed_func fun(self: MaxCardsSkill, from: Player)
+---@field public correct_func fun(self: MaxCardsSkill, player: Player)
+---@field public fixed_func fun(self: MaxCardsSkill, from: Player)
---@param spec MaxCardsSpec
---@return MaxCardsSkill
@@ -280,9 +280,9 @@ function fk.CreateMaxCardsSkill(spec)
end
---@class TargetModSpec: StatusSkillSpec
----@field residue_func fun(self: TargetModSkill, player: Player, skill: ActiveSkill, scope: integer)
----@field distance_limit_func fun(self: TargetModSkill, player: Player, skill: ActiveSkill)
----@field extra_target_func fun(self: TargetModSkill, player: Player, skill: ActiveSkill)
+---@field public residue_func fun(self: TargetModSkill, player: Player, skill: ActiveSkill, scope: integer)
+---@field public distance_limit_func fun(self: TargetModSkill, player: Player, skill: ActiveSkill)
+---@field public extra_target_func fun(self: TargetModSkill, player: Player, skill: ActiveSkill)
---@param spec TargetModSpec
---@return TargetModSkill
@@ -305,8 +305,8 @@ function fk.CreateTargetModSkill(spec)
end
---@class FilterSpec: StatusSkillSpec
----@field card_filter fun(self: FilterSkill, card: Card)
----@field view_as fun(self: FilterSkill, card: Card)
+---@field public card_filter fun(self: FilterSkill, card: Card)
+---@field public view_as fun(self: FilterSkill, card: Card)
---@param spec FilterSpec
---@return FilterSkill
@@ -322,7 +322,7 @@ function fk.CreateFilterSkill(spec)
end
---@class InvaliditySpec: StatusSkillSpec
----@field invalidity_func fun(self: InvaliditySkill, from: Player, skill: Skill)
+---@field public invalidity_func fun(self: InvaliditySkill, from: Player, skill: Skill)
---@param spec InvaliditySpec
---@return InvaliditySkill
@@ -337,8 +337,8 @@ function fk.CreateInvaliditySkill(spec)
end
---@class CardSpec: Card
----@field skill Skill
----@field equip_skill Skill
+---@field public skill Skill
+---@field public equip_skill Skill
local defaultCardSkill = fk.CreateActiveSkill{
name = "default_card_skill",
diff --git a/lua/lsp/lib.lua b/lua/lsp/lib.lua
index 4c2d8917..15664d7d 100644
--- a/lua/lsp/lib.lua
+++ b/lua/lsp/lib.lua
@@ -1,7 +1,7 @@
---@meta
---@class class
----@field static any
+---@field public static any
--- middleclass
class = {}
@@ -10,7 +10,7 @@ class = {}
function class:isSubclassOf(class) end
---@class Object
----@field class class
+---@field public class class
Object = { static = {} }
---@generic T
diff --git a/lua/lsp/static.lua b/lua/lsp/static.lua
deleted file mode 100644
index 04d7c8cf..00000000
--- a/lua/lsp/static.lua
+++ /dev/null
@@ -1,9 +0,0 @@
----@meta
-
----@param c integer|integer[]|Card|Card[]
----@return integer[]
-function Card:getIdList(c) end
-
----@param pattern string
----@return Exppattern
-function Exppattern:Parse(pattern) end
diff --git a/lua/server/ai/ai.lua b/lua/server/ai/ai.lua
index b1eb2051..d8f6c5bf 100644
--- a/lua/server/ai/ai.lua
+++ b/lua/server/ai/ai.lua
@@ -2,11 +2,11 @@
-- Do nothing.
---@class AI: Object
----@field room Room
----@field player ServerPlayer
----@field command string
----@field jsonData string
----@field cb_table table
+---@field public room Room
+---@field public player ServerPlayer
+---@field public command string
+---@field public jsonData string
+---@field public cb_table table
local AI = class("AI")
function AI:initialize(player)
diff --git a/lua/server/gameevent.lua b/lua/server/gameevent.lua
index cf16a5a5..c77d7fd3 100644
--- a/lua/server/gameevent.lua
+++ b/lua/server/gameevent.lua
@@ -1,11 +1,11 @@
---@class GameEvent: Object
----@field room Room
----@field event integer
----@field data any
----@field main_func fun(self: GameEvent)
----@field clear_func fun(self: GameEvent)
----@field extra_clear_funcs any[]
----@field interrupted boolean
+---@field public room Room
+---@field public event integer
+---@field public data any
+---@field public main_func fun(self: GameEvent)
+---@field public clear_func fun(self: GameEvent)
+---@field public extra_clear_funcs any[]
+---@field public interrupted boolean
local GameEvent = class("GameEvent")
GameEvent.functions = {}
diff --git a/lua/server/gamelogic.lua b/lua/server/gamelogic.lua
index b4159c59..31edc41b 100644
--- a/lua/server/gamelogic.lua
+++ b/lua/server/gamelogic.lua
@@ -1,11 +1,11 @@
---@class GameLogic: Object
----@field room Room
----@field skill_table table
----@field refresh_skill_table table
----@field skills string[]
----@field event_stack Stack
----@field game_event_stack Stack
----@field role_table string[][]
+---@field public room Room
+---@field public skill_table table
+---@field public refresh_skill_table table
+---@field public skills string[]
+---@field public event_stack Stack
+---@field public game_event_stack Stack
+---@field public role_table string[][]
local GameLogic = class("GameLogic")
function GameLogic:initialize(room)
diff --git a/lua/server/room.lua b/lua/server/room.lua
index 78493b0e..713fdd78 100644
--- a/lua/server/room.lua
+++ b/lua/server/room.lua
@@ -1,21 +1,21 @@
---@class Room : Object
----@field room fk.Room
----@field players ServerPlayer[]
----@field alive_players ServerPlayer[]
----@field observers fk.ServerPlayer[]
----@field current ServerPlayer
----@field game_started boolean
----@field game_finished boolean
----@field timeout integer
----@field tag table
----@field draw_pile integer[]
----@field discard_pile integer[]
----@field processing_area integer[]
----@field void integer[]
----@field card_place table
----@field owner_map table
----@field status_skills Skill[]
----@field settings table
+---@field public room fk.Room
+---@field public players ServerPlayer[]
+---@field public alive_players ServerPlayer[]
+---@field public observers fk.ServerPlayer[]
+---@field public current ServerPlayer
+---@field public game_started boolean
+---@field public game_finished boolean
+---@field public timeout integer
+---@field public tag table
+---@field public draw_pile integer[]
+---@field public discard_pile integer[]
+---@field public processing_area integer[]
+---@field public void integer[]
+---@field public card_place table
+---@field public owner_map table
+---@field public status_skills Skill[]
+---@field public settings table
local Room = class("Room")
-- load classes used by the game
@@ -1707,7 +1707,7 @@ end
---@param player ServerPlayer
---@param num integer
---@param skillName string
----@param fromPlace "top"|"bottom"
+---@param fromPlace string
---@return integer[]
function Room:drawCards(player, num, skillName, fromPlace)
local topCards = self:getNCards(num, fromPlace)
@@ -1763,7 +1763,7 @@ end
---@param player ServerPlayer
---@param num integer
----@param reason "loseHp"|"damage"|"recover"|null
+---@param reason string|nil
---@param skillName string
---@param damageStruct DamageStruct|null
---@return boolean
diff --git a/lua/server/serverplayer.lua b/lua/server/serverplayer.lua
index a08a3408..bad9e454 100644
--- a/lua/server/serverplayer.lua
+++ b/lua/server/serverplayer.lua
@@ -1,19 +1,19 @@
---@class ServerPlayer : Player
----@field serverplayer fk.ServerPlayer
----@field room Room
----@field next ServerPlayer
----@field request_data string
----@field client_reply string
----@field default_reply string
----@field reply_ready boolean
----@field reply_cancel boolean
----@field phases Phase[]
----@field skipped_phases Phase[]
----@field phase_state table[]
----@field phase_index integer
----@field role_shown boolean
----@field ai AI
----@field ai_data any
+---@field public serverplayer fk.ServerPlayer
+---@field public room Room
+---@field public next ServerPlayer
+---@field public request_data string
+---@field public client_reply string
+---@field public default_reply string
+---@field public reply_ready boolean
+---@field public reply_cancel boolean
+---@field public phases Phase[]
+---@field public skipped_phases Phase[]
+---@field public phase_state table[]
+---@field public phase_index integer
+---@field public role_shown boolean
+---@field public ai AI
+---@field public ai_data any
local ServerPlayer = Player:subclass("ServerPlayer")
function ServerPlayer:initialize(_self)
diff --git a/lua/server/system_enum.lua b/lua/server/system_enum.lua
index 86531b0c..318867d5 100644
--- a/lua/server/system_enum.lua
+++ b/lua/server/system_enum.lua
@@ -1,45 +1,45 @@
---@class CardsMoveInfo
----@field ids integer[]
----@field from integer|null
----@field to integer|null
----@field toArea CardArea
----@field moveReason CardMoveReason
----@field proposer integer
----@field skillName string|null
----@field moveVisible boolean|null
----@field specialName string|null
----@field specialVisible boolean|null
+---@field public ids integer[]
+---@field public from integer|null
+---@field public to integer|null
+---@field public toArea CardArea
+---@field public moveReason CardMoveReason
+---@field public proposer integer
+---@field public skillName string|null
+---@field public moveVisible boolean|null
+---@field public specialName string|null
+---@field public specialVisible boolean|null
---@class MoveInfo
----@field cardId integer
----@field fromArea CardArea
----@field fromSpecialName string|null
+---@field public cardId integer
+---@field public fromArea CardArea
+---@field public fromSpecialName string|null
---@class CardsMoveStruct
----@field moveInfo MoveInfo[]
----@field from integer|null
----@field to integer|null
----@field toArea CardArea
----@field moveReason CardMoveReason
----@field proposer integer|null
----@field skillName string|null
----@field moveVisible boolean|null
----@field specialName string|null
----@field specialVisible boolean|null
+---@field public moveInfo MoveInfo[]
+---@field public from integer|null
+---@field public to integer|null
+---@field public toArea CardArea
+---@field public moveReason CardMoveReason
+---@field public proposer integer|null
+---@field public skillName string|null
+---@field public moveVisible boolean|null
+---@field public specialName string|null
+---@field public specialVisible boolean|null
---@class PindianResult
----@field toCard Card
----@field winner ServerPlayer|null
+---@field public toCard Card
+---@field public winner ServerPlayer|null
---@class HpChangedData
----@field num integer
----@field reason string
----@field skillName string
----@field damageEvent DamageStruct|null
+---@field public num integer
+---@field public reason string
+---@field public skillName string
+---@field public damageEvent DamageStruct|null
---@class HpLostData
----@field num integer
----@field skillName string
+---@field public num integer
+---@field public skillName string
---@alias DamageType integer
@@ -48,109 +48,109 @@ fk.ThunderDamage = 2
fk.FireDamage = 3
---@class DamageStruct
----@field from ServerPlayer|null
----@field to ServerPlayer
----@field damage integer
----@field card Card
----@field chain boolean
----@field damageType DamageType
----@field skillName string
----@field beginnerOfTheDamage boolean|null
+---@field public from ServerPlayer|null
+---@field public to ServerPlayer
+---@field public damage integer
+---@field public card Card
+---@field public chain boolean
+---@field public damageType DamageType
+---@field public skillName string
+---@field public beginnerOfTheDamage boolean|null
---@class RecoverStruct
----@field who ServerPlayer
----@field num integer
----@field recoverBy ServerPlayer|null
----@field skillName string|null
----@field card Card|null
+---@field public who ServerPlayer
+---@field public num integer
+---@field public recoverBy ServerPlayer|null
+---@field public skillName string|null
+---@field public card Card|null
---@class DyingStruct
----@field who integer
----@field damage DamageStruct
+---@field public who integer
+---@field public damage DamageStruct
---@class DeathStruct
----@field who integer
----@field damage DamageStruct
+---@field public who integer
+---@field public damage DamageStruct
---@class CardUseStruct
----@field from integer
----@field tos TargetGroup
----@field card Card
----@field toCard Card|null
----@field responseToEvent CardUseStruct|null
----@field nullifiedTargets interger[]|null
----@field extraUse boolean|null
----@field disresponsiveList integer[]|null
----@field unoffsetableList integer[]|null
----@field additionalDamage integer|null
----@field customFrom integer|null
----@field cardsResponded Card[]|null
+---@field public from integer
+---@field public tos TargetGroup
+---@field public card Card
+---@field public toCard Card|null
+---@field public responseToEvent CardUseStruct|null
+---@field public nullifiedTargets interger[]|null
+---@field public extraUse boolean|null
+---@field public disresponsiveList integer[]|null
+---@field public unoffsetableList integer[]|null
+---@field public additionalDamage integer|null
+---@field public customFrom integer|null
+---@field public cardsResponded Card[]|null
---@class AimStruct
----@field from integer
----@field card Card
----@field tos AimGroup
----@field to integer
----@field subTargets integer[]|null
----@field targetGroup TargetGroup|null
----@field nullifiedTargets integer[]|null
----@field firstTarget boolean
----@field additionalDamage integer|null
----@field disresponsive boolean|null
----@field unoffsetableList boolean|null
----@field additionalResponseTimes table|integer|null
----@field fixedAddTimesResponsors integer[]
+---@field public from integer
+---@field public card Card
+---@field public tos AimGroup
+---@field public to integer
+---@field public subTargets integer[]|null
+---@field public targetGroup TargetGroup|null
+---@field public nullifiedTargets integer[]|null
+---@field public firstTarget boolean
+---@field public additionalDamage integer|null
+---@field public disresponsive boolean|null
+---@field public unoffsetableList boolean|null
+---@field public additionalResponseTimes table|integer|null
+---@field public fixedAddTimesResponsors integer[]
---@class CardEffectEvent
----@field from integer
----@field to integer
----@field subTargets integer[]|null
----@field tos TargetGroup
----@field card Card
----@field toCard Card|null
----@field responseToEvent CardEffectEvent|null
----@field nullifiedTargets interger[]|null
----@field extraUse boolean|null
----@field disresponsiveList integer[]|null
----@field unoffsetableList integer[]|null
----@field additionalDamage integer|null
----@field customFrom integer|null
----@field cardsResponded Card[]|null
----@field disresponsive boolean|null
----@field unoffsetable boolean|null
----@field isCancellOut boolean|null
----@field fixedResponseTimes table|integer|null
----@field fixedAddTimesResponsors integer[]
+---@field public from integer
+---@field public to integer
+---@field public subTargets integer[]|null
+---@field public tos TargetGroup
+---@field public card Card
+---@field public toCard Card|null
+---@field public responseToEvent CardEffectEvent|null
+---@field public nullifiedTargets interger[]|null
+---@field public extraUse boolean|null
+---@field public disresponsiveList integer[]|null
+---@field public unoffsetableList integer[]|null
+---@field public additionalDamage integer|null
+---@field public customFrom integer|null
+---@field public cardsResponded Card[]|null
+---@field public disresponsive boolean|null
+---@field public unoffsetable boolean|null
+---@field public isCancellOut boolean|null
+---@field public fixedResponseTimes table|integer|null
+---@field public fixedAddTimesResponsors integer[]
---@class SkillEffectEvent
----@field from integer
----@field tos integer[]
----@field cards integer[]
+---@field public from integer
+---@field public tos integer[]
+---@field public cards integer[]
---@class JudgeStruct
----@field who ServerPlayer
----@field card Card
----@field reason string
----@field pattern string
+---@field public who ServerPlayer
+---@field public card Card
+---@field public reason string
+---@field public pattern string
---@class CardResponseEvent
----@field from integer
----@field card Card
----@field responseToEvent CardEffectEvent|null
----@field skipDrop boolean|null
----@field customFrom integer|null
+---@field public from integer
+---@field public card Card
+---@field public responseToEvent CardEffectEvent|null
+---@field public skipDrop boolean|null
+---@field public customFrom integer|null
---@class AskForCardUse
----@field user ServerPlayer
----@field cardName string
----@field pattern string
----@field result CardUseStruct
+---@field public user ServerPlayer
+---@field public cardName string
+---@field public pattern string
+---@field public result CardUseStruct
---@class AskForCardResponse
----@field user ServerPlayer
----@field cardName string
----@field pattern string
----@field result Card
+---@field public user ServerPlayer
+---@field public cardName string
+---@field public pattern string
+---@field public result Card
---@alias CardMoveReason integer
@@ -166,17 +166,17 @@ fk.ReasonUse = 9
fk.ReasonResonpse = 10
---@class PindianStruct
----@field from ServerPlayer
----@field tos ServerPlayer[]
----@field fromCard Card
----@field results table
----@field reason string
+---@field public from ServerPlayer
+---@field public tos ServerPlayer[]
+---@field public fromCard Card
+---@field public results table
+---@field public reason string
---@class LogMessage
----@field type string
----@field from integer
----@field to integer[]
----@field card integer[]
----@field arg any
----@field arg2 any
----@field arg3 any
+---@field public type string
+---@field public from integer
+---@field public to integer[]
+---@field public card integer[]
+---@field public arg any
+---@field public arg2 any
+---@field public arg3 any