{"action":"created","issue":{"url":"https://api.github.com/repos/TartaricAcid/BakaDanmaku/issues/4","repository_url":"https://api.github.com/repos/TartaricAcid/BakaDanmaku","labels_url":"https://api.github.com/repos/TartaricAcid/BakaDanmaku/issues/4/labels{/name}","comments_url":"https://api.github.com/repos/TartaricAcid/BakaDanmaku/issues/4/comments","events_url":"https://api.github.com/repos/TartaricAcid/BakaDanmaku/issues/4/events","html_url":"https://github.com/TartaricAcid/BakaDanmaku/pull/4","id":369914599,"node_id":"MDExOlB1bGxSZXF1ZXN0MjIyNzIzNzQz","number":4,"title":"Quick Fix","user":{"login":"Yesterday17","id":8667822,"node_id":"MDQ6VXNlcjg2Njc4MjI=","avatar_url":"https://avatars2.githubusercontent.com/u/8667822?v=4","gravatar_id":"","url":"https://api.github.com/users/Yesterday17","html_url":"https://github.com/Yesterday17","followers_url":"https://api.github.com/users/Yesterday17/followers","following_url":"https://api.github.com/users/Yesterday17/following{/other_user}","gists_url":"https://api.github.com/users/Yesterday17/gists{/gist_id}","starred_url":"https://api.github.com/users/Yesterday17/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Yesterday17/subscriptions","organizations_url":"https://api.github.com/users/Yesterday17/orgs","repos_url":"https://api.github.com/users/Yesterday17/repos","events_url":"https://api.github.com/users/Yesterday17/events{/privacy}","received_events_url":"https://api.github.com/users/Yesterday17/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":0,"created_at":"2018-10-14T15:44:23Z","updated_at":"2018-10-14T17:14:11Z","closed_at":null,"author_association":"CONTRIBUTOR","pull_request":{"url":"https://api.github.com/repos/TartaricAcid/BakaDanmaku/pulls/4","html_url":"https://github.com/TartaricAcid/BakaDanmaku/pull/4","diff_url":"https://github.com/TartaricAcid/BakaDanmaku/pull/4.diff","patch_url":"https://github.com/TartaricAcid/BakaDanmaku/pull/4.patch"},"body":"内容见 commit message 。"},"comment":{"url":"https://api.github.com/repos/TartaricAcid/BakaDanmaku/issues/comments/429643994","html_url":"https://github.com/TartaricAcid/BakaDanmaku/pull/4#issuecomment-429643994","issue_url":"https://api.github.com/repos/TartaricAcid/BakaDanmaku/issues/4","id":429643994,"node_id":"MDEyOklzc3VlQ29tbWVudDQyOTY0Mzk5NA==","user":{"login":"Yesterday17","id":8667822,"node_id":"MDQ6VXNlcjg2Njc4MjI=","avatar_url":"https://avatars2.githubusercontent.com/u/8667822?v=4","gravatar_id":"","url":"https://api.github.com/users/Yesterday17","html_url":"https://github.com/Yesterday17","followers_url":"https://api.github.com/users/Yesterday17/followers","following_url":"https://api.github.com/users/Yesterday17/following{/other_user}","gists_url":"https://api.github.com/users/Yesterday17/gists{/gist_id}","starred_url":"https://api.github.com/users/Yesterday17/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Yesterday17/subscriptions","organizations_url":"https://api.github.com/users/Yesterday17/orgs","repos_url":"https://api.github.com/users/Yesterday17/repos","events_url":"https://api.github.com/users/Yesterday17/events{/privacy}","received_events_url":"https://api.github.com/users/Yesterday17/received_events","type":"User","site_admin":false},"created_at":"2018-10-14T17:14:11Z","updated_at":"2018-10-14T17:14:11Z","author_association":"CONTRIBUTOR","body":"## 基本结构说明\r\n最基本的结构是 `DanmakuThread`，`Event` 和 `Handler`。\r\n\r\n### DanmakuThread\r\nhttps://github.com/TartaricAcid/BakaDanmaku/blob/ccd07e8302d46aa07f0e654a062895acdae51f43/src/main/java/github/tartaricacid/bakadanmaku/api/thread/BaseDanmakuThread.java#L8\r\n对于 `DanmakuThread` ，它其实并不是一个线程，而是一个 `implements Runnable` 的类。当我们新建真正的线程时，通过它来表示线程内运行的内容。\r\n\r\n### Event\r\n`Event` 是整个模组运行的核心，它负责传递 `DanmakuThread` 接收到的信息。\r\n\r\n### Handler\r\n`Hander` 决定的是接收到信息的显示方式。以当前存在的两种（一种//TODO:）Handler 来看，有显示在游戏聊天栏与屏幕滚动两种。\r\n\r\n## 独立性\r\n对于上述的三种结构，其各自是相对独立的。也就是说，`DanmakuThread` 不应该与特定 `Event` 存在关联，而 `Event` 也不应该与特定 `Handler` 存在依赖关系。\r\n举个例子，比如现在有 `ADanmakuThread`，接收到了一条来自 `A` 平台的弹幕信息，`post` 了一个 `DanmakuEvent`。如果在 `ADanmakuThread` 中，其已经完成了从弹幕文字到 `TextComponentString` 的转换，那么虽然对于 `Event` 而言并没有什么区别，但对于 `Handler` 来说，这条弹幕的内容便*限定*给了 `ChatMsgHandler` 处理。对于 `ScreenMsgHandler` 而言，这样的弹幕信息是无法接受的。\r\n\r\n## 简单加载流程\r\n### 加载各大平台的 DanmakuThread\r\nhttps://github.com/TartaricAcid/BakaDanmaku/blob/ccd07e8302d46aa07f0e654a062895acdae51f43/src/main/java/github/tartaricacid/bakadanmaku/BakaDanmaku.java#L35\r\n注意这里指的**各大平台**其实不一定完全是平台，比如https://github.com/Yesterday17/BakaDanmakuCQ ，其对于的其实是我架设在腾讯云上的QQ机器人。\r\n### 注册事件处理器\r\n在这里，需要注册的事件有两类。一类负责处理玩家登入登出时的事件，而另一类负责处理 `DanmakuThread` 接收到消息后发出的事件。\r\n### 存在于主类中的EntityPlayer\r\nhttps://github.com/TartaricAcid/BakaDanmaku/blob/ccd07e8302d46aa07f0e654a062895acdae51f43/src/main/java/github/tartaricacid/bakadanmaku/BakaDanmaku.java#L27 \r\n`BakaDanmaku` 类（主类）维护 `player` ，供各线程调用。由于 `player` 只会在玩家进入或会出游戏时更新，因此只需要维护一个全局的即可。\r\n\r\n## 登入登出\r\n### 玩家登入\r\n玩家登录后，可以看到，代码被简化成了：\r\n```java\r\nBakaDanmaku.player = event.player;\r\nDanmakuThreadFactory.restartThreads();\r\n```\r\n第一行负责将全局维护的 `EntityPlayer` 更新，而第二行通过 `DanmakuThreadFactory` 负责启动线程。此处的 `restart` 其实代表的不仅是重启，当当前不存在正在运行的线程的时候，它也可以用来启动线程。\r\n### 玩家登出\r\n```java\r\nBakaDanmaku.player = null;\r\nDanmakuThreadFactory.stopAllThreads();\r\n```\r\n代码简单，含义明了，这里不再阐释。\r\n### 抽象&封装的目的\r\n在原来的实现中，进入世界（或者更早，`init`）时线程的启动和修改配置后线程的启动代码是有大量重复的。从本质上来说，后者只是比前者多了一个*停止*的动作而已，因此将其封装到 `DanmakuThreadFactory` 中。\r\n\r\n## 抽象工厂类\r\n### 起源\r\n这个类的出现本来只是想简单地统筹规划一下 `DanmakuThread`，即使用最常见的方式：\r\n```java\r\nswitch(platform){\r\n    case \"bilibili\":\r\n        return new BilibiliDanmakuThread();\r\n        break;\r\n    case \"douyu\":\r\n        ...\r\n}\r\n```\r\n但这是违反[开闭原则](https://zh.wikipedia.org/wiki/%E5%BC%80%E9%97%AD%E5%8E%9F%E5%88%99)的，有损于模组的开放性。我希望这个 Mod 能够构建出一个完整的，可拓展性强的框架。因此我选择了 `HashMap`，任何本模组甚至以本模组的附属都可以通过简单的 `danmakuThreads.set` 实现对现有 `DanmakuThread`的扩充，甚至覆盖（这又何尝不是一个bug呢（逃））。\r\n### 命名缺陷\r\n从某种意义上来说，这个类已经脱离了单纯工厂类的范畴，而是 `DanmakuThreadFactory` 和 `DanmakuThreadUtils` 的结合体。如果有需要，可以将其分为两个文件以方便理解。\r\n### 新出现的 realDanmakuThreads ？\r\n从上面的介绍我们可以看出，所谓的 `danmakuThreads` 其实并不是 `Thread`。在原来的版本中，这个类中是存在一个 `runningThread` 变量负责存储当前正在运行的线程的引用的。然而……\r\n#### 一个线程不够吗？\r\n从直播平台来看，很少会有人同时推流到两个平台，需要同时接收多个平台的**弹幕**。\r\n但是，上文我们也提到过，**各大平台**其实不一定必须完全是平台。我们在游戏中可能也需要接收一些其他信息，比如QQ群的信息，`Github` 的信息等等。这时，就需要另一个线程负责处理这些不同的信息。\r\n#### 每种 `DanmakuThread` 的运行个数上限？\r\n还记得#2 吗？在那时，我们封装了一个 `BaseDanmakuThread` ，其保证了每一类 `DanmakuThread` 都可以只需要 `new` 一个实例就可以正常运作。这样的设计避免了重复创建 `DanmakuThread` 带来的消耗。因此，对于**每一种** `DanmakuThread`，只会有一个它的实例运行。\r\n#### realDanmakuThreads\r\n这里的 `realDanmakuThreads`，其实就是为了记录当前正在运行的 `Thread` 而生的。它取代了原来 `Bakadanmaku` 中的 `t`，而是对于每一类 `DanmakuThread` 都存在一个。\r\n它和 `danmakuThreads` 中对 `DanmakuThread` 的标识方法一样。因此，如果一个 `DanmakuThread` 正在运行，那你一定可以在 `realDanmakuThreads` 中找到它，而寻找方法，与 `danmakuThreads` 相同。\r\n### 方法介绍\r\n#### setDanmakuThread\r\n```java\r\n    /**\r\n     * 向 danmakuThreads 中添加新的 DanmakuThread 类型\r\n     * 通常在 Mod 的 init 阶段使用\r\n     *\r\n     * @param name   DanmakuThread 的名称\r\n     * @param thread DanmakuThread 的实例\r\n     */\r\n    public static void setDanmakuThread(String name, BaseDanmakuThread thread) {\r\n        danmakuThreads.put(name, thread);\r\n    }\r\n```\r\n只是单纯地往 `HashMap` 里 put 内容罢了。\r\n#### getDanmakuThread\r\n```java\r\n    /**\r\n     * 获得指定平台的 DanmakuThread\r\n     *\r\n     * @param platform 平台名\r\n     * @return BaseDanmakuThread 实例\r\n     */\r\n    public static BaseDanmakuThread getDanmakuThread(String platform) {\r\n        return danmakuThreads.getOrDefault(platform, null);\r\n    }\r\n```\r\n尝试获得 `danmakuThreads` 中对应 `platform` 的 `DanmakuThread`，如果不存在则返回 `null` 。\r\n#### getRunningDanmakuThread\r\n```java\r\n/**\r\n     * 获得当前正在运行的线程\r\n     *\r\n     * @return 当前正在运行的线程\r\n     */\r\n    public static ArrayList<String> getRunningDanmakuThread() {\r\n        return new ArrayList<>(realDanmakuThreads.keySet());\r\n    }\r\n```\r\n如上文所述，`realDanmakuThreads` 的 `key` 即为正在运行的线程名的集合。用 `ArrayList` 的原因不明（当时想到的）。\r\n#### runThread\r\n```java\r\n    /**\r\n     * 启动指定平台的 DanmakuThread\r\n     *\r\n     * @param platform 平台名\r\n     */\r\n    public static void runThread(String platform) {\r\n        BaseDanmakuThread dmThread = getDanmakuThread(platform);\r\n\r\n        if (dmThread != null) {\r\n            dmThread.keepRunning = true;\r\n\r\n            Thread threadToRun = new Thread(dmThread, platform + \"DanmakuThread\");\r\n            threadToRun.start();\r\n\r\n            realDanmakuThreads.put(platform, threadToRun);\r\n        } else {\r\n            BakaDanmaku.logger.error(\"平台 [\" + platform + \"] 不存在！请检查配置文件或已安装 Mod！\");\r\n            // TODO: 红色字体\r\n            BaseDanmakuThread.sendChatMessage(\"弹幕姬错误：\");\r\n            BaseDanmakuThread.sendChatMessage(\"平台 [\" + platform + \"] 不存在！请检查配置文件或已安装 Mod！\");\r\n        }\r\n    }\r\n```\r\n首先尝试获得指定平台的 `DanmakuThread`，如果获取成功，就启动该线程，并且将其放入 `realDanmakuThreads` 中；如果获取失败，则输出错误信息。\r\n#### stopThread\r\n```java\r\n    /**\r\n     * 停止指定平台的 DanmakuThread\r\n     *\r\n     * @param platform 平台名\r\n     */\r\n    public static void stopThread(String platform) {\r\n        BaseDanmakuThread th = getDanmakuThread(platform);\r\n\r\n        if (th != null) {\r\n            th.keepRunning = false; // 关闭线程\r\n            while (realDanmakuThreads.get(platform).isAlive()) ;\r\n            th.clear(); // 清空线程\r\n        }\r\n    }\r\n```\r\n同理，停止线程。\r\n#### stopAllThreads\r\n```java\r\n    /**\r\n     * 停止现在正在运行的所有 DanmakuThread\r\n     */\r\n    public static void stopAllThreads() {\r\n        realDanmakuThreads.forEach((platform, thread) -> {\r\n            stopThread(platform);\r\n        });\r\n    }\r\n```\r\n通过 `forEach`，停止了 `realDanmakuThreads` 中的所有线程，即为停止了所有正在运行的 `DanmakuThread`。\r\n#### isThreadRunning\r\n```java\r\n    /**\r\n     * 判断指定 platform 的 DanmakuThread 是否正在运行\r\n     *\r\n     * @param platform 平台名\r\n     * @return 指定 platform 的 DanmakuThread 是否正在运行\r\n     */\r\n    public static boolean isThreadRunning(String platform) {\r\n        return realDanmakuThreads.containsKey(platform);\r\n    }\r\n```\r\n暂时没有哪里用到，指不定什么时候回用到呢……\r\n#### restartThreads\r\n```java\r\n    /**\r\n     * 重启所有线程 同样可以用于初始化时线程的启动\r\n     */\r\n    public static void restartThreads() {\r\n        getRunningDanmakuThread().forEach(DanmakuThreadFactory::stopThread);\r\n        for (String p : BakaDanmakuConfig.general.platform.split(\",\")) {\r\n            runThread(p.trim());\r\n        }\r\n    }\r\n```\r\n首先将所有正在运行的线程关闭，然后再通过读取配置文件中的 `platform`，重新启动线程。\r\n\r\n## 配置文件\r\n配置文件目前分为通用、哔哩哔哩直播间、聊天栏弹幕以及网络四方面的配置。\r\n### 哔哩哔哩直播间配置\r\n对于哔哩哔哩直播间配置，其仅与 `BilibiliDanmakuThread` 关联，负责存储 `BilibiliDanmakuThread` 中必须的选项。\r\n## 聊天栏弹幕配置\r\n对于聊天栏弹幕配置，其负责呈现渲染的样式等。渲染的方式**与Thread无关**\r\n\r\n其余二者不多阐述。\r\n\r\n## BakaDanmakuEvent\r\n这是整个 Mod 中其他所有类的父类，抽象出了一层 `platform`。在最新的 commit 中，已经删去了因为偷懒而使人难以理解的部分。\r\n\r\n# 总结\r\nhmmm，怎么说呢，如果有问题继续 Comment/QQ 吧（"}}
{"actor":{"display_login":"Yesterday17"}}