1
This commit is contained in:
+46
-46
@@ -1,107 +1,107 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<configuration>
|
<configuration>
|
||||||
<!--配置更改后重启才能生效-->
|
<!--配置更改后重启才能生效-->
|
||||||
<appSettings>
|
<appSettings>
|
||||||
<!--过滤Websocket数据源进程,可用','进行分隔,程序将会监听以下进程的弹幕信息-->
|
<!--过滤Websocket数据源进程,可用','进行分隔,程序将会监听以下进程的弹幕信息-->
|
||||||
<add key="processFilter" value="直播伴侣,douyin,chrome,msedge,QQBrowser,360se,firefox,2345explorer,iexplore" />
|
<add key="processFilter" value="直播伴侣,douyin,kuaishou,kwailive,chrome,msedge,QQBrowser,360se,firefox,2345explorer,iexplore"/>
|
||||||
<!--Websocket监听端口-->
|
<!--Websocket监听端口-->
|
||||||
<add key="wsListenPort" value="8888" />
|
<add key="wsListenPort" value="8888"/>
|
||||||
<!--true:监听在0.0.0.0,接受任意Ip连接,false:监听在127.0.0.1,仅接受本机连接-->
|
<!--true:监听在0.0.0.0,接受任意Ip连接,false:监听在127.0.0.1,仅接受本机连接-->
|
||||||
<add key="listenAny" value="true" />
|
<add key="listenAny" value="true"/>
|
||||||
<!--系统代理端口-->
|
<!--系统代理端口-->
|
||||||
<add key="proxyPort" value="8827" />
|
<add key="proxyPort" value="8827"/>
|
||||||
<!--是否启用系统代理,若设置为false 则需要在程序手动指定代理地址 -->
|
<!--是否启用系统代理,若设置为false 则需要在程序手动指定代理地址 -->
|
||||||
<add key="sysProxy" value="true" />
|
<add key="sysProxy" value="true"/>
|
||||||
<!--上游代理地址,例如开启了系统代理,但是需要将其他无关请求转发到VPN工具中,例如:127.0.0.1:11223,不要带http://-->
|
<!--上游代理地址,例如开启了系统代理,但是需要将其他无关请求转发到VPN工具中,例如:127.0.0.1:11223,不要带http://-->
|
||||||
<add key="upstreamProxy" value="" />
|
<add key="upstreamProxy" value=""/>
|
||||||
<!--在控制台输出弹幕-->
|
<!--在控制台输出弹幕-->
|
||||||
<add key="printBarrage" value="true" />
|
<add key="printBarrage" value="true"/>
|
||||||
<!--要在控制台打印的弹幕类型,多个使用','分隔,(空代表不过滤) 1[普通弹幕],2[点赞消息],3[进入直播间],4[关注消息],5[礼物消息],6[统计消息],7[粉丝团消息],8[直播间分享],9[下播]-->
|
<!--要在控制台打印的弹幕类型,多个使用','分隔,(空代表不过滤) 1[普通弹幕],2[点赞消息],3[进入直播间],4[关注消息],5[礼物消息],6[统计消息],7[粉丝团消息],8[直播间分享],9[下播]-->
|
||||||
<add key="printFilter" value="" />
|
<add key="printFilter" value=""/>
|
||||||
<!--要推送的弹幕消息类型,多个使用','分隔,同上-->
|
<!--要推送的弹幕消息类型,多个使用','分隔,同上-->
|
||||||
<add key="pushFilter" value="" />
|
<add key="pushFilter" value=""/>
|
||||||
<!--要日志记录的弹幕消息类型,多个使用','分隔,同上-->
|
<!--要日志记录的弹幕消息类型,多个使用','分隔,同上-->
|
||||||
<add key="logFilter" value="1,2,4,5,6,7,8" />
|
<add key="logFilter" value="1,2,4,5,6,7,8"/>
|
||||||
<!--要进行过滤的Web房间ID,多个使用','分隔,根据缓存来过滤的,直播伴侣不支持 -->
|
<!--要进行过滤的Web房间ID,多个使用','分隔,根据缓存来过滤的,直播伴侣不支持 -->
|
||||||
<add key="webRoomIds" value="" />
|
<add key="webRoomIds" value=""/>
|
||||||
<!--开启内置的域名过滤,设置为false会解包所有https请求,cpu占用很高,建议在无法获取弹幕数据时调整 -->
|
<!--开启内置的域名过滤,设置为false会解包所有https请求,cpu占用很高,建议在无法获取弹幕数据时调整 -->
|
||||||
<add key="filterHostName" value="true" />
|
<add key="filterHostName" value="true"/>
|
||||||
<!--已知的弹幕域名列表 ','分隔 用作过滤规则中,凡是webcast开头的域名程序都会自动列入白名单-->
|
<!--已知的弹幕域名列表 ','分隔 用作过滤规则中,凡是webcast开头的域名程序都会自动列入白名单-->
|
||||||
<add key="hostNameFilter" value="" />
|
<add key="hostNameFilter" value=""/>
|
||||||
<!--隐藏控制台-->
|
<!--隐藏控制台-->
|
||||||
<add key="hideConsole" value="false" />
|
<add key="hideConsole" value="false"/>
|
||||||
<!--弹幕文件日志-->
|
<!--弹幕文件日志-->
|
||||||
<add key="barrageFileLog" value="false" />
|
<add key="barrageFileLog" value="false"/>
|
||||||
<!--显示窗体-->
|
<!--显示窗体-->
|
||||||
<add key="showWindow" value="false" />
|
<add key="showWindow" value="false"/>
|
||||||
<!--进入直播间自动暂停播放-->
|
<!--进入直播间自动暂停播放-->
|
||||||
<add key="autoPause" value="true" />
|
<add key="autoPause" value="true"/>
|
||||||
<!--强制启用轮询模式获取弹幕 (对于容易断开连接或者更加追求稳定的直播间,可以启用这个开关,虽然响应速度不如WebSocket,但是绝对稳定!)-->
|
<!--强制启用轮询模式获取弹幕 (对于容易断开连接或者更加追求稳定的直播间,可以启用这个开关,虽然响应速度不如WebSocket,但是绝对稳定!)-->
|
||||||
<add key="forcePolling" value="false" />
|
<add key="forcePolling" value="false"/>
|
||||||
<!--弹幕轮询间隔,当 forcePolling 为 true 时生效 (毫秒,1000毫秒=1秒,不建议小于1000毫秒,太小可能会被封IP,值越小,弹幕流越丝滑,对于观众多的直播间可以改小)-->
|
<!--弹幕轮询间隔,当 forcePolling 为 true 时生效 (毫秒,1000毫秒=1秒,不建议小于1000毫秒,太小可能会被封IP,值越小,弹幕流越丝滑,对于观众多的直播间可以改小)-->
|
||||||
<add key="pollingInterval" value="3000" />
|
<add key="pollingInterval" value="3000"/>
|
||||||
<!--禁用直播页浏览器脚本缓存 (如果需要确保脚本每次能够正常匹配替换,则启用它,可能会损失一定的页面加载速度)-->
|
<!--禁用直播页浏览器脚本缓存 (如果需要确保脚本每次能够正常匹配替换,则启用它,可能会损失一定的页面加载速度)-->
|
||||||
<add key="disableLivePageScriptCache" value="false" />
|
<add key="disableLivePageScriptCache" value="false"/>
|
||||||
<!-- 指定要发送的COM串口 格式:'COM1:9600' 代表用COM1发送,波特率9600,若不需要使用串口抄送服务请留空,(启用后程序会自动生成过滤器模板文件到 scripts/comPortFilter.js) -->
|
<!-- 指定要发送的COM串口 格式:'COM1:9600' 代表用COM1发送,波特率9600,若不需要使用串口抄送服务请留空,(启用后程序会自动生成过滤器模板文件到 scripts/comPortFilter.js) -->
|
||||||
<add key="comPort" value="" />
|
<add key="comPort" value=""/>
|
||||||
<!--直播伴侣exe文件位置(除非程序找不到,否则请留空)-->
|
<!--直播伴侣exe文件位置(除非程序找不到,否则请留空)-->
|
||||||
<add key="liveCompanPath" value="" />
|
<add key="liveCompanPath" value=""/>
|
||||||
<!--直播伴侣进程代理开关-->
|
<!--直播伴侣进程代理开关-->
|
||||||
<add key="liveCompanHookSwitch" value="false" />
|
<add key="liveCompanHookSwitch" value="false"/>
|
||||||
</appSettings>
|
</appSettings>
|
||||||
<runtime>
|
<runtime>
|
||||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
<dependentAssembly>
|
<dependentAssembly>
|
||||||
<assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
<assemblyIdentity name="System.Memory" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||||
<bindingRedirect oldVersion="0.0.0.0-4.0.1.2" newVersion="4.0.1.2" />
|
<bindingRedirect oldVersion="0.0.0.0-4.0.1.2" newVersion="4.0.1.2"/>
|
||||||
</dependentAssembly>
|
</dependentAssembly>
|
||||||
<dependentAssembly>
|
<dependentAssembly>
|
||||||
<assemblyIdentity name="System.Net.Http" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
<assemblyIdentity name="System.Net.Http" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0" />
|
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0"/>
|
||||||
</dependentAssembly>
|
</dependentAssembly>
|
||||||
<dependentAssembly>
|
<dependentAssembly>
|
||||||
<assemblyIdentity name="System.IO.Compression" publicKeyToken="b77a5c561934e089" culture="neutral" />
|
<assemblyIdentity name="System.IO.Compression" publicKeyToken="b77a5c561934e089" culture="neutral"/>
|
||||||
<bindingRedirect oldVersion="0.0.0.0-4.1.2.0" newVersion="4.1.2.0" />
|
<bindingRedirect oldVersion="0.0.0.0-4.1.2.0" newVersion="4.1.2.0"/>
|
||||||
</dependentAssembly>
|
</dependentAssembly>
|
||||||
<dependentAssembly>
|
<dependentAssembly>
|
||||||
<assemblyIdentity name="System.Runtime" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
<assemblyIdentity name="System.Runtime" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0" />
|
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0"/>
|
||||||
</dependentAssembly>
|
</dependentAssembly>
|
||||||
<dependentAssembly>
|
<dependentAssembly>
|
||||||
<assemblyIdentity name="System.Diagnostics.Tracing" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
<assemblyIdentity name="System.Diagnostics.Tracing" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0" />
|
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0"/>
|
||||||
</dependentAssembly>
|
</dependentAssembly>
|
||||||
<dependentAssembly>
|
<dependentAssembly>
|
||||||
<assemblyIdentity name="System.Reflection" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
<assemblyIdentity name="System.Reflection" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0" />
|
<bindingRedirect oldVersion="0.0.0.0-4.1.1.0" newVersion="4.1.1.0"/>
|
||||||
</dependentAssembly>
|
</dependentAssembly>
|
||||||
<dependentAssembly>
|
<dependentAssembly>
|
||||||
<assemblyIdentity name="System.Runtime.InteropServices" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
<assemblyIdentity name="System.Runtime.InteropServices" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
<bindingRedirect oldVersion="0.0.0.0-4.1.0.0" newVersion="4.1.0.0" />
|
<bindingRedirect oldVersion="0.0.0.0-4.1.0.0" newVersion="4.1.0.0"/>
|
||||||
</dependentAssembly>
|
</dependentAssembly>
|
||||||
<dependentAssembly>
|
<dependentAssembly>
|
||||||
<assemblyIdentity name="System.Diagnostics.DiagnosticSource" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
|
<assemblyIdentity name="System.Diagnostics.DiagnosticSource" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||||
<bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0" />
|
<bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0"/>
|
||||||
</dependentAssembly>
|
</dependentAssembly>
|
||||||
<dependentAssembly>
|
<dependentAssembly>
|
||||||
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
|
<assemblyIdentity name="System.Runtime.CompilerServices.Unsafe" publicKeyToken="b03f5f7f11d50a3a" culture="neutral"/>
|
||||||
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" />
|
<bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0"/>
|
||||||
</dependentAssembly>
|
</dependentAssembly>
|
||||||
</assemblyBinding>
|
</assemblyBinding>
|
||||||
</runtime>
|
</runtime>
|
||||||
<startup>
|
<startup>
|
||||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.2" />
|
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8"/>
|
||||||
</startup>
|
</startup>
|
||||||
<system.web>
|
<system.web>
|
||||||
<membership defaultProvider="ClientAuthenticationMembershipProvider">
|
<membership defaultProvider="ClientAuthenticationMembershipProvider">
|
||||||
<providers>
|
<providers>
|
||||||
<add name="ClientAuthenticationMembershipProvider" type="System.Web.ClientServices.Providers.ClientFormsAuthenticationMembershipProvider, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" serviceUri="" />
|
<add name="ClientAuthenticationMembershipProvider" type="System.Web.ClientServices.Providers.ClientFormsAuthenticationMembershipProvider, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" serviceUri=""/>
|
||||||
</providers>
|
</providers>
|
||||||
</membership>
|
</membership>
|
||||||
<roleManager defaultProvider="ClientRoleProvider" enabled="true">
|
<roleManager defaultProvider="ClientRoleProvider" enabled="true">
|
||||||
<providers>
|
<providers>
|
||||||
<add name="ClientRoleProvider" type="System.Web.ClientServices.Providers.ClientRoleProvider, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" serviceUri="" cacheTimeout="86400" />
|
<add name="ClientRoleProvider" type="System.Web.ClientServices.Providers.ClientRoleProvider, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" serviceUri="" cacheTimeout="86400"/>
|
||||||
</providers>
|
</providers>
|
||||||
</roleManager>
|
</roleManager>
|
||||||
</system.web>
|
</system.web>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
// 是否隐藏控制台
|
// 是否隐藏控制台
|
||||||
"hideConsole": false,
|
"hideConsole": false,
|
||||||
// 显示窗体
|
// 显示窗体
|
||||||
"showWindow": false
|
"showWindow": true,
|
||||||
|
// 直播平台: "douyin" 或 "kuaishou"
|
||||||
|
"platform": "douyin"
|
||||||
},
|
},
|
||||||
"network": {
|
"network": {
|
||||||
"proxy": {
|
"proxy": {
|
||||||
@@ -25,7 +27,7 @@
|
|||||||
},
|
},
|
||||||
"filtering": {
|
"filtering": {
|
||||||
// 过滤Websocket数据源进程,可用','进行分隔,程序将会监听以下进程的弹幕信息
|
// 过滤Websocket数据源进程,可用','进行分隔,程序将会监听以下进程的弹幕信息
|
||||||
"processFilter": "直播伴侣,douyin,chrome,msedge,QQBrowser,360se,firefox,2345explorer,iexplore",
|
"processFilter": "直播伴侣,douyin,kuaishou,kwailive,bilibili,chrome,msedge,QQBrowser,360se,firefox,2345explorer,iexplore",
|
||||||
// 开启内置的域名过滤,设置为false会解包所有https请求,cpu占用很高,建议在无法获取弹幕数据时调整
|
// 开启内置的域名过滤,设置为false会解包所有https请求,cpu占用很高,建议在无法获取弹幕数据时调整
|
||||||
"hostNameEnabled": true,
|
"hostNameEnabled": true,
|
||||||
// 已知的弹幕域名列表 ','分隔 用作过滤规则中,凡是webcast开头的域名程序都会自动列入白名单
|
// 已知的弹幕域名列表 ','分隔 用作过滤规则中,凡是webcast开头的域名程序都会自动列入白名单
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BarrageGrab.Modles.JsonEntity;
|
using BarrageGrab.Modles.JsonEntity;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using static System.Configuration.ConfigurationManager;
|
using static System.Configuration.ConfigurationManager;
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ namespace BarrageGrab
|
|||||||
|
|
||||||
ConfigComPort();
|
ConfigComPort();
|
||||||
ConfigFilter();
|
ConfigFilter();
|
||||||
|
LoadFromJson();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -72,8 +74,11 @@ namespace BarrageGrab
|
|||||||
// 读取JSON文件内容(保留注释)
|
// 读取JSON文件内容(保留注释)
|
||||||
string jsonContent = File.ReadAllText(jsonFilePath);
|
string jsonContent = File.ReadAllText(jsonFilePath);
|
||||||
|
|
||||||
// 解析JSON(Newtonsoft.Json支持JSONC格式,会自动忽略注释)
|
// 解析JSON(忽略注释)
|
||||||
JObject config = JObject.Parse(jsonContent);
|
JObject config = JObject.Parse(jsonContent, new JsonLoadSettings
|
||||||
|
{
|
||||||
|
CommentHandling = CommentHandling.Ignore
|
||||||
|
});
|
||||||
|
|
||||||
// 从JSON读取应用配置
|
// 从JSON读取应用配置
|
||||||
var app = config["app"];
|
var app = config["app"];
|
||||||
@@ -81,7 +86,16 @@ namespace BarrageGrab
|
|||||||
// 常规配置
|
// 常规配置
|
||||||
var general = app["general"];
|
var general = app["general"];
|
||||||
HideConsole = general["hideConsole"]?.Value<bool>() ?? false;
|
HideConsole = general["hideConsole"]?.Value<bool>() ?? false;
|
||||||
ShowWindow = general["showWindow"]?.Value<bool>() ?? false;
|
ShowWindow = general["showWindow"]?.Value<bool>() ?? false;
|
||||||
|
string platformStr = general["platform"]?.Value<string>() ?? "douyin";
|
||||||
|
if (platformStr.ToLower() == "kuaishou")
|
||||||
|
{
|
||||||
|
Platform = LivePlatform.Kuaishou;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Platform = LivePlatform.Douyin;
|
||||||
|
}
|
||||||
|
|
||||||
// 网络配置
|
// 网络配置
|
||||||
var network = app["network"];
|
var network = app["network"];
|
||||||
@@ -640,6 +654,11 @@ namespace BarrageGrab
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string LiveCompanPath { get; set; } = string.Empty;
|
public string LiveCompanPath { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 直播平台 (Douyin / Kuaishou)
|
||||||
|
/// </summary>
|
||||||
|
public LivePlatform Platform { get; set; } = LivePlatform.Douyin;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// hook直播伴侣代理开关
|
/// hook直播伴侣代理开关
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
@echo off
|
||||||
|
REM JiaKong 配置编辑器编译脚本
|
||||||
|
REM 在 VS 中打开 WssBarrageService.csproj 编译即可
|
||||||
|
REM 或者双击此脚本(需要安装 Visual Studio 2022)
|
||||||
|
|
||||||
|
echo 正在查找 MSBuild...
|
||||||
|
|
||||||
|
REM VS 2022 MSBuild 路径
|
||||||
|
set MSBUILD="C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe"
|
||||||
|
|
||||||
|
if not exist %MSBUILD% (
|
||||||
|
set MSBUILD="C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not exist %MSBUILD% (
|
||||||
|
echo 未找到 MSBuild.exe,请在 Visual Studio 中打开项目并编译。
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo 正在编译 WssBarrageService...
|
||||||
|
%MSBUILD% WssBarrageService.csproj /t:Build /p:Configuration=Debug /nologo
|
||||||
|
|
||||||
|
if %ERRORLEVEL% EQU 0 (
|
||||||
|
echo.
|
||||||
|
echo 编译成功!
|
||||||
|
echo 输出: bin\Debug\WssBarrageServer.exe
|
||||||
|
) else (
|
||||||
|
echo.
|
||||||
|
echo 编译失败,请检查错误信息。
|
||||||
|
)
|
||||||
|
pause
|
||||||
@@ -0,0 +1,523 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Data;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
|
||||||
|
namespace BarrageGrab.Forms
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 游戏配置编辑器 —— 可视化编辑5个配置文件
|
||||||
|
/// 1. effect_config.txt - 基础效果库(下拉框选项源)
|
||||||
|
/// 2. macro_config.txt - 组合效果(宏)
|
||||||
|
/// 3. gift_config.txt - 礼物/事件→效果映射
|
||||||
|
/// 4. command_config.txt - 弹幕指令触发词
|
||||||
|
/// 5. fkey_config.txt - F键→效果映射
|
||||||
|
/// </summary>
|
||||||
|
public partial class ConfigEditorForm : Form
|
||||||
|
{
|
||||||
|
private DataGridView dgvEffects, dgvMacros, dgvGifts, dgvCommands, dgvFkeys;
|
||||||
|
private TabControl tabMain;
|
||||||
|
private string ConfigDir { get { return Program.ConfigDir; } }
|
||||||
|
private Label lblStatus;
|
||||||
|
|
||||||
|
// 文件路径
|
||||||
|
private string EffectPath => Program.EffectPath;
|
||||||
|
private string MacroPath => Program.MacroPath;
|
||||||
|
private string GiftPath => Program.GiftPath;
|
||||||
|
private string CmdPath => Program.CommandPath;
|
||||||
|
private string FkeyPath => Program.FkeyPath;
|
||||||
|
|
||||||
|
private readonly string[] FKeyOptions = { "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12" };
|
||||||
|
|
||||||
|
// CommandType 列表(用于指令Tab)
|
||||||
|
private readonly string[] CommandTypes = {
|
||||||
|
"SelectState", "SpawnTroop", "Gold", "Population", "Destiny",
|
||||||
|
"Mobilize", "Attack", "Alliance", "ResearchTech", "SetIdeology",
|
||||||
|
"ApplyBuff", "Info"
|
||||||
|
};
|
||||||
|
|
||||||
|
public ConfigEditorForm()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
this.Text = "游戏配置编辑器 (5文件)";
|
||||||
|
this.ClientSize = new Size(950, 650);
|
||||||
|
this.StartPosition = FormStartPosition.CenterParent;
|
||||||
|
BuildUI();
|
||||||
|
LoadAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
this.SuspendLayout();
|
||||||
|
this.AutoScaleMode = AutoScaleMode.Font;
|
||||||
|
this.ResumeLayout(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════ UI 构建 ═══════════════
|
||||||
|
|
||||||
|
void BuildUI()
|
||||||
|
{
|
||||||
|
tabMain = new TabControl { Dock = DockStyle.Fill, Name = "tabMain" };
|
||||||
|
this.Controls.Add(tabMain);
|
||||||
|
|
||||||
|
// Tab1: 基础效果
|
||||||
|
var tabEffect = new TabPage { Text = "基础效果", Name = "tabEffect" };
|
||||||
|
dgvEffects = CreateEffectGrid();
|
||||||
|
tabEffect.Controls.Add(dgvEffects);
|
||||||
|
tabEffect.Controls.Add(CreateButtonPanel(() => AddRow(dgvEffects, "新效果", "出兵 3"),
|
||||||
|
"格式: 效果名称|游戏指令 | 原子效果,下拉框选项源"));
|
||||||
|
tabEffect.Controls[1].Dock = DockStyle.Bottom;
|
||||||
|
dgvEffects.Dock = DockStyle.Fill;
|
||||||
|
tabMain.TabPages.Add(tabEffect);
|
||||||
|
|
||||||
|
// Tab2: 组合宏
|
||||||
|
var tabMacro = new TabPage { Text = "组合宏", Name = "tabMacro" };
|
||||||
|
dgvMacros = CreateMacroGrid();
|
||||||
|
tabMacro.Controls.Add(dgvMacros);
|
||||||
|
tabMacro.Controls.Add(CreateButtonPanel(() => AddRow(dgvMacros, "新宏", "", "出兵 5|金币 100"),
|
||||||
|
"格式: 宏名称|触发词(可空)|指令1|指令2 | 指令支持$1 $2 $*"));
|
||||||
|
tabMacro.Controls[1].Dock = DockStyle.Bottom;
|
||||||
|
dgvMacros.Dock = DockStyle.Fill;
|
||||||
|
tabMain.TabPages.Add(tabMacro);
|
||||||
|
|
||||||
|
// Tab3: 礼物事件
|
||||||
|
var tabGift = new TabPage { Text = "礼物事件", Name = "tabGift" };
|
||||||
|
dgvGifts = CreateGiftGrid();
|
||||||
|
tabGift.Controls.Add(dgvGifts);
|
||||||
|
tabGift.Controls.Add(CreateButtonPanel(() => AddRow(dgvGifts, "新礼物", "", "{0} 送了礼物", ""),
|
||||||
|
"格式: 关键词|效果名称|公告|间隔 | 效果名称从下拉框选"));
|
||||||
|
tabGift.Controls[1].Dock = DockStyle.Bottom;
|
||||||
|
dgvGifts.Dock = DockStyle.Fill;
|
||||||
|
tabMain.TabPages.Add(tabGift);
|
||||||
|
|
||||||
|
// Tab4: 弹幕指令
|
||||||
|
var tabCmd = new TabPage { Text = "弹幕指令", Name = "tabCmd" };
|
||||||
|
dgvCommands = CreateCommandGrid();
|
||||||
|
tabCmd.Controls.Add(dgvCommands);
|
||||||
|
tabCmd.Controls.Add(CreateButtonPanel(() => AddRow(dgvCommands, "SelectState", "选择,选,select"),
|
||||||
|
"格式: [CommandType]|触发词1,触发词2 | 宏在'组合宏'Tab配置"));
|
||||||
|
tabCmd.Controls[1].Dock = DockStyle.Bottom;
|
||||||
|
dgvCommands.Dock = DockStyle.Fill;
|
||||||
|
tabMain.TabPages.Add(tabCmd);
|
||||||
|
|
||||||
|
// Tab5: F键
|
||||||
|
var tabFkey = new TabPage { Text = "F键", Name = "tabFkey" };
|
||||||
|
dgvFkeys = CreateFkeyGrid();
|
||||||
|
tabFkey.Controls.Add(dgvFkeys);
|
||||||
|
tabFkey.Controls.Add(CreateButtonPanel(() => AddRow(dgvFkeys, "F2", ""),
|
||||||
|
"格式: F键|效果名称 | 效果名称从下拉框选 | F1固定初始化"));
|
||||||
|
tabFkey.Controls[1].Dock = DockStyle.Bottom;
|
||||||
|
dgvFkeys.Dock = DockStyle.Fill;
|
||||||
|
tabMain.TabPages.Add(tabFkey);
|
||||||
|
}
|
||||||
|
|
||||||
|
Panel CreateButtonPanel(Action addAction, string hint)
|
||||||
|
{
|
||||||
|
var p = new Panel { Height = 40 };
|
||||||
|
var btnAdd = new Button { Text = "+ 新增", Left = 5, Top = 8, Width = 70 };
|
||||||
|
btnAdd.Click += (s, e) => addAction();
|
||||||
|
p.Controls.Add(btnAdd);
|
||||||
|
|
||||||
|
var btnDel = new Button { Text = "删除选中", Left = 80, Top = 8, Width = 70 };
|
||||||
|
btnDel.Click += (s, e) =>
|
||||||
|
{
|
||||||
|
var dgv = GetCurrentGrid();
|
||||||
|
if (dgv?.SelectedRows.Count > 0)
|
||||||
|
foreach (DataGridViewRow row in dgv.SelectedRows)
|
||||||
|
if (!row.IsNewRow) dgv.Rows.Remove(row);
|
||||||
|
};
|
||||||
|
p.Controls.Add(btnDel);
|
||||||
|
|
||||||
|
var btnReload = new Button { Text = "重新加载", Left = 155, Top = 8, Width = 80 };
|
||||||
|
btnReload.Click += (s, e) => { LoadAll(); };
|
||||||
|
p.Controls.Add(btnReload);
|
||||||
|
|
||||||
|
var btnSave = new Button { Text = "全部保存", Left = 240, Top = 8, Width = 90, BackColor = Color.LightGreen };
|
||||||
|
btnSave.Click += (s, e) => SaveAll();
|
||||||
|
p.Controls.Add(btnSave);
|
||||||
|
|
||||||
|
lblStatus = new Label { AutoSize = true, Left = 340, Top = 12, Text = "" };
|
||||||
|
p.Controls.Add(lblStatus);
|
||||||
|
|
||||||
|
var lblHint = new Label { AutoSize = true, Left = 500, Top = 12, ForeColor = Color.Gray, Text = hint };
|
||||||
|
p.Controls.Add(lblHint);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
DataGridView GetCurrentGrid()
|
||||||
|
{
|
||||||
|
switch (tabMain.SelectedIndex)
|
||||||
|
{
|
||||||
|
case 0: return dgvEffects;
|
||||||
|
case 1: return dgvMacros;
|
||||||
|
case 2: return dgvGifts;
|
||||||
|
case 3: return dgvCommands;
|
||||||
|
case 4: return dgvFkeys;
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AddRow(DataGridView dgv, params object[] values) => dgv.Rows.Add(values);
|
||||||
|
|
||||||
|
// ═══════════════ 表格创建 ═══════════════
|
||||||
|
|
||||||
|
DataGridView CreateEffectGrid()
|
||||||
|
{
|
||||||
|
var dgv = CreateBaseGrid("dgvEffects");
|
||||||
|
dgv.Columns.Add(new DataGridViewTextBoxColumn { HeaderText = "效果名称", Name = "Name", Width = 150 });
|
||||||
|
dgv.Columns.Add(new DataGridViewTextBoxColumn { HeaderText = "游戏指令", Name = "Command", Width = 300 });
|
||||||
|
return dgv;
|
||||||
|
}
|
||||||
|
|
||||||
|
DataGridView CreateMacroGrid()
|
||||||
|
{
|
||||||
|
var dgv = CreateBaseGrid("dgvMacros");
|
||||||
|
dgv.Columns.Add(new DataGridViewTextBoxColumn { HeaderText = "宏名称", Name = "Name", Width = 120 });
|
||||||
|
dgv.Columns.Add(new DataGridViewTextBoxColumn { HeaderText = "触发词(逗号分隔,可空)", Name = "Triggers", Width = 150 });
|
||||||
|
dgv.Columns.Add(new DataGridViewTextBoxColumn { HeaderText = "指令(多条用|分隔,支持$1 $2 $*)", Name = "Commands", Width = 350 });
|
||||||
|
return dgv;
|
||||||
|
}
|
||||||
|
|
||||||
|
DataGridView CreateGiftGrid()
|
||||||
|
{
|
||||||
|
var dgv = CreateBaseGrid("dgvGifts");
|
||||||
|
dgv.Columns.Add(new DataGridViewTextBoxColumn { HeaderText = "关键词(礼物名/事件)", Name = "Keyword", Width = 120 });
|
||||||
|
var colEffect = new DataGridViewComboBoxColumn { HeaderText = "效果名称", Name = "Effect", Width = 180 };
|
||||||
|
colEffect.AutoComplete = true;
|
||||||
|
dgv.Columns.Add(colEffect);
|
||||||
|
dgv.DataError += (s, e) => { e.ThrowException = false; };
|
||||||
|
dgv.Columns.Add(new DataGridViewTextBoxColumn { HeaderText = "公告文本", Name = "Announce", Width = 200 });
|
||||||
|
dgv.Columns.Add(new DataGridViewTextBoxColumn { HeaderText = "间隔", Name = "Interval", Width = 50 });
|
||||||
|
return dgv;
|
||||||
|
}
|
||||||
|
|
||||||
|
DataGridView CreateCommandGrid()
|
||||||
|
{
|
||||||
|
var dgv = CreateBaseGrid("dgvCommands");
|
||||||
|
var colType = new DataGridViewComboBoxColumn { HeaderText = "CommandType", Name = "CmdType", Width = 120 };
|
||||||
|
colType.Items.AddRange(CommandTypes);
|
||||||
|
dgv.Columns.Add(colType);
|
||||||
|
dgv.DataError += (s, e) => { e.ThrowException = false; };
|
||||||
|
dgv.Columns.Add(new DataGridViewTextBoxColumn { HeaderText = "触发词(逗号分隔)", Name = "Triggers", Width = 400 });
|
||||||
|
return dgv;
|
||||||
|
}
|
||||||
|
|
||||||
|
DataGridView CreateFkeyGrid()
|
||||||
|
{
|
||||||
|
var dgv = CreateBaseGrid("dgvFkeys");
|
||||||
|
var colKey = new DataGridViewComboBoxColumn { HeaderText = "F键", Name = "FKey", Width = 80 };
|
||||||
|
colKey.Items.AddRange(FKeyOptions);
|
||||||
|
dgv.Columns.Add(colKey);
|
||||||
|
var colEffect = new DataGridViewComboBoxColumn { HeaderText = "效果名称", Name = "Effect", Width = 250 };
|
||||||
|
colEffect.AutoComplete = true;
|
||||||
|
dgv.Columns.Add(colEffect);
|
||||||
|
dgv.DataError += (s, e) => { e.ThrowException = false; };
|
||||||
|
return dgv;
|
||||||
|
}
|
||||||
|
|
||||||
|
DataGridView CreateBaseGrid(string name)
|
||||||
|
{
|
||||||
|
return new DataGridView
|
||||||
|
{
|
||||||
|
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill,
|
||||||
|
AllowUserToAddRows = false,
|
||||||
|
AllowUserToDeleteRows = false,
|
||||||
|
RowHeadersVisible = false,
|
||||||
|
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
|
||||||
|
Name = name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════ 效果下拉刷新 ═══════════════
|
||||||
|
|
||||||
|
/// <summary>刷新礼物/F键Tab的效果下拉框(从effect_config + macro_config读取)</summary>
|
||||||
|
void RefreshEffectDropdowns()
|
||||||
|
{
|
||||||
|
var effectNames = new List<string>();
|
||||||
|
|
||||||
|
// 从基础效果表读取
|
||||||
|
foreach (DataGridViewRow row in dgvEffects.Rows)
|
||||||
|
{
|
||||||
|
if (row.IsNewRow) continue;
|
||||||
|
var name = row.Cells["Name"].Value?.ToString();
|
||||||
|
if (!string.IsNullOrEmpty(name)) effectNames.Add(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从宏表读取
|
||||||
|
foreach (DataGridViewRow row in dgvMacros.Rows)
|
||||||
|
{
|
||||||
|
if (row.IsNewRow) continue;
|
||||||
|
var name = row.Cells["Name"].Value?.ToString();
|
||||||
|
if (!string.IsNullOrEmpty(name)) effectNames.Add(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新礼物Tab
|
||||||
|
var giftCol = dgvGifts.Columns["Effect"] as DataGridViewComboBoxColumn;
|
||||||
|
if (giftCol != null)
|
||||||
|
{
|
||||||
|
var oldValues = new List<string>();
|
||||||
|
foreach (DataGridViewRow row in dgvGifts.Rows)
|
||||||
|
{
|
||||||
|
if (row.IsNewRow) continue;
|
||||||
|
var v = row.Cells["Effect"].Value?.ToString();
|
||||||
|
if (!string.IsNullOrEmpty(v)) oldValues.Add(v);
|
||||||
|
}
|
||||||
|
giftCol.Items.Clear();
|
||||||
|
foreach (var n in effectNames) giftCol.Items.Add(n);
|
||||||
|
foreach (var v in oldValues)
|
||||||
|
if (!giftCol.Items.Contains(v)) giftCol.Items.Add(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新F键Tab
|
||||||
|
var fkeyCol = dgvFkeys.Columns["Effect"] as DataGridViewComboBoxColumn;
|
||||||
|
if (fkeyCol != null)
|
||||||
|
{
|
||||||
|
var oldValues = new List<string>();
|
||||||
|
foreach (DataGridViewRow row in dgvFkeys.Rows)
|
||||||
|
{
|
||||||
|
if (row.IsNewRow) continue;
|
||||||
|
var v = row.Cells["Effect"].Value?.ToString();
|
||||||
|
if (!string.IsNullOrEmpty(v)) oldValues.Add(v);
|
||||||
|
}
|
||||||
|
fkeyCol.Items.Clear();
|
||||||
|
foreach (var n in effectNames) fkeyCol.Items.Add(n);
|
||||||
|
foreach (var v in oldValues)
|
||||||
|
if (!fkeyCol.Items.Contains(v)) fkeyCol.Items.Add(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════ 加载 ═══════════════
|
||||||
|
|
||||||
|
void LoadAll()
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(ConfigDir)) Directory.CreateDirectory(ConfigDir);
|
||||||
|
LoadEffects();
|
||||||
|
LoadMacros();
|
||||||
|
LoadGifts();
|
||||||
|
LoadCommands();
|
||||||
|
LoadFkeys();
|
||||||
|
RefreshEffectDropdowns();
|
||||||
|
lblStatus.Text = "配置已加载";
|
||||||
|
lblStatus.ForeColor = Color.Black;
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoadEffects()
|
||||||
|
{
|
||||||
|
dgvEffects.Rows.Clear();
|
||||||
|
if (!File.Exists(EffectPath)) return;
|
||||||
|
foreach (var line in File.ReadAllLines(EffectPath, Encoding.UTF8))
|
||||||
|
{
|
||||||
|
var t = line.Trim();
|
||||||
|
if (string.IsNullOrEmpty(t) || t.StartsWith("#")) continue;
|
||||||
|
int pipe = t.IndexOf('|');
|
||||||
|
if (pipe <= 0) continue;
|
||||||
|
dgvEffects.Rows.Add(t.Substring(0, pipe).Trim(), t.Substring(pipe + 1).Trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoadMacros()
|
||||||
|
{
|
||||||
|
dgvMacros.Rows.Clear();
|
||||||
|
if (!File.Exists(MacroPath)) return;
|
||||||
|
foreach (var line in File.ReadAllLines(MacroPath, Encoding.UTF8))
|
||||||
|
{
|
||||||
|
var t = line.Trim();
|
||||||
|
if (string.IsNullOrEmpty(t) || t.StartsWith("#")) continue;
|
||||||
|
var parts = t.Split('|');
|
||||||
|
if (parts.Length < 3) continue;
|
||||||
|
var name = parts[0].Trim();
|
||||||
|
var triggers = parts[1].Trim();
|
||||||
|
var cmds = string.Join("|", parts.Skip(2).Select(p => p.Trim()));
|
||||||
|
dgvMacros.Rows.Add(name, triggers, cmds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoadGifts()
|
||||||
|
{
|
||||||
|
dgvGifts.Rows.Clear();
|
||||||
|
if (!File.Exists(GiftPath)) return;
|
||||||
|
foreach (var line in File.ReadAllLines(GiftPath, Encoding.UTF8))
|
||||||
|
{
|
||||||
|
var t = line.Trim();
|
||||||
|
if (string.IsNullOrEmpty(t) || t.StartsWith("#")) continue;
|
||||||
|
var parts = t.Split('|');
|
||||||
|
if (parts.Length < 2) continue;
|
||||||
|
var keyword = parts[0].Trim();
|
||||||
|
var effect = parts[1].Trim();
|
||||||
|
var announce = parts.Length > 2 ? parts[2].Trim() : "";
|
||||||
|
var interval = parts.Length > 3 ? parts[3].Trim() : "";
|
||||||
|
dgvGifts.Rows.Add(keyword, effect, announce, interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoadCommands()
|
||||||
|
{
|
||||||
|
dgvCommands.Rows.Clear();
|
||||||
|
if (!File.Exists(CmdPath)) return;
|
||||||
|
foreach (var line in File.ReadAllLines(CmdPath, Encoding.UTF8))
|
||||||
|
{
|
||||||
|
var t = line.Trim();
|
||||||
|
if (string.IsNullOrEmpty(t) || t.StartsWith("#")) continue;
|
||||||
|
if (!t.StartsWith("[")) continue;
|
||||||
|
int end = t.IndexOf(']');
|
||||||
|
if (end <= 1) continue;
|
||||||
|
var typeName = t.Substring(1, end - 1).Trim();
|
||||||
|
var triggers = t.Substring(end + 1).TrimStart('|').Trim();
|
||||||
|
dgvCommands.Rows.Add(typeName, triggers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoadFkeys()
|
||||||
|
{
|
||||||
|
dgvFkeys.Rows.Clear();
|
||||||
|
if (!File.Exists(FkeyPath))
|
||||||
|
{
|
||||||
|
// 默认值
|
||||||
|
var defaults = new[] {
|
||||||
|
("F2","出兵5"), ("F3","金币300"), ("F4","出兵3"), ("F5","金币500"),
|
||||||
|
("F6","攻击力x2_30s"), ("F7","人口1000"), ("F8","天命200"),
|
||||||
|
("F9","科技全开"), ("F10","特征全开"), ("F11","动员5"), ("F12","大礼包")
|
||||||
|
};
|
||||||
|
foreach (var d in defaults) dgvFkeys.Rows.Add(d.Item1, d.Item2);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
foreach (var line in File.ReadAllLines(FkeyPath, Encoding.UTF8))
|
||||||
|
{
|
||||||
|
var t = line.Trim();
|
||||||
|
if (string.IsNullOrEmpty(t) || t.StartsWith("#")) continue;
|
||||||
|
int pipe = t.IndexOf('|');
|
||||||
|
if (pipe <= 0) continue;
|
||||||
|
dgvFkeys.Rows.Add(t.Substring(0, pipe).Trim(), t.Substring(pipe + 1).Trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════ 保存 ═══════════════
|
||||||
|
|
||||||
|
void SaveAll()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(ConfigDir)) Directory.CreateDirectory(ConfigDir);
|
||||||
|
SaveEffects();
|
||||||
|
SaveMacros();
|
||||||
|
SaveGifts();
|
||||||
|
SaveCommands();
|
||||||
|
SaveFkeys();
|
||||||
|
RefreshEffectDropdowns();
|
||||||
|
lblStatus.Text = "全部保存成功";
|
||||||
|
lblStatus.ForeColor = Color.Green;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
lblStatus.Text = $"保存失败: {ex.Message}";
|
||||||
|
lblStatus.ForeColor = Color.Red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SaveEffects()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("# 游戏基础效果配置 v1");
|
||||||
|
sb.AppendLine("# 格式: 效果名称|游戏指令");
|
||||||
|
sb.AppendLine("# 一个效果 = 一条原子指令(多指令组合用 macro_config)");
|
||||||
|
sb.AppendLine();
|
||||||
|
foreach (DataGridViewRow row in dgvEffects.Rows)
|
||||||
|
{
|
||||||
|
if (row.IsNewRow) continue;
|
||||||
|
var name = row.Cells["Name"].Value?.ToString() ?? "";
|
||||||
|
var cmd = row.Cells["Command"].Value?.ToString() ?? "";
|
||||||
|
if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(cmd))
|
||||||
|
sb.AppendLine($"{name}|{cmd}");
|
||||||
|
}
|
||||||
|
File.WriteAllText(EffectPath, sb.ToString(), Encoding.UTF8);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SaveMacros()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("# 游戏组合效果(宏)配置 v1");
|
||||||
|
sb.AppendLine("# 格式: 宏名称|触发词1,触发词2,...|指令1|指令2|...");
|
||||||
|
sb.AppendLine("# 触发词可选(留空则只能被礼物/F键引用)");
|
||||||
|
sb.AppendLine("# 指令支持 $1 $2 $* 参数替换(仅弹幕触发时)");
|
||||||
|
sb.AppendLine();
|
||||||
|
foreach (DataGridViewRow row in dgvMacros.Rows)
|
||||||
|
{
|
||||||
|
if (row.IsNewRow) continue;
|
||||||
|
var name = row.Cells["Name"].Value?.ToString() ?? "";
|
||||||
|
var triggers = row.Cells["Triggers"].Value?.ToString() ?? "";
|
||||||
|
var cmds = row.Cells["Commands"].Value?.ToString() ?? "";
|
||||||
|
if (string.IsNullOrEmpty(name)) continue;
|
||||||
|
// cmds 中的 | 是指令分隔符,保持原样
|
||||||
|
sb.AppendLine($"{name}|{triggers}|{cmds}");
|
||||||
|
}
|
||||||
|
File.WriteAllText(MacroPath, sb.ToString(), Encoding.UTF8);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SaveGifts()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("# 游戏礼物/事件配置 v1");
|
||||||
|
sb.AppendLine("# 格式: 关键词|效果名称|公告文本|触发间隔(可选)");
|
||||||
|
sb.AppendLine("# 效果名称引用 effect_config 或 macro_config(下拉框选择)");
|
||||||
|
sb.AppendLine("# {0}=昵称 {1}=数量");
|
||||||
|
sb.AppendLine("# 特殊关键词: 点赞/关注/进房/分享/粉丝团");
|
||||||
|
sb.AppendLine();
|
||||||
|
foreach (DataGridViewRow row in dgvGifts.Rows)
|
||||||
|
{
|
||||||
|
if (row.IsNewRow) continue;
|
||||||
|
var keyword = row.Cells["Keyword"].Value?.ToString() ?? "";
|
||||||
|
var effect = row.Cells["Effect"].Value?.ToString() ?? "";
|
||||||
|
var msg = row.Cells["Announce"].Value?.ToString() ?? "";
|
||||||
|
var interval = row.Cells["Interval"].Value?.ToString() ?? "";
|
||||||
|
if (string.IsNullOrEmpty(keyword) || string.IsNullOrEmpty(effect)) continue;
|
||||||
|
var parts = new List<string> { keyword, effect };
|
||||||
|
if (!string.IsNullOrEmpty(msg)) parts.Add(msg);
|
||||||
|
if (!string.IsNullOrEmpty(interval)) parts.Add(interval);
|
||||||
|
sb.AppendLine(string.Join("|", parts));
|
||||||
|
}
|
||||||
|
File.WriteAllText(GiftPath, sb.ToString(), Encoding.UTF8);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SaveCommands()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("# 游戏弹幕指令触发词配置 v1");
|
||||||
|
sb.AppendLine("# 格式: [CommandType]|触发词1,触发词2,...");
|
||||||
|
sb.AppendLine("# 注: 宏指令在 macro_config.txt 配置");
|
||||||
|
sb.AppendLine();
|
||||||
|
foreach (DataGridViewRow row in dgvCommands.Rows)
|
||||||
|
{
|
||||||
|
if (row.IsNewRow) continue;
|
||||||
|
var type = row.Cells["CmdType"].Value?.ToString() ?? "";
|
||||||
|
var triggers = row.Cells["Triggers"].Value?.ToString() ?? "";
|
||||||
|
if (string.IsNullOrEmpty(type)) continue;
|
||||||
|
sb.AppendLine($"[{type}]|{triggers}");
|
||||||
|
}
|
||||||
|
File.WriteAllText(CmdPath, sb.ToString(), Encoding.UTF8);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SaveFkeys()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
sb.AppendLine("# 游戏 F键配置 v1");
|
||||||
|
sb.AppendLine("# 格式: F键|效果名称");
|
||||||
|
sb.AppendLine("# 效果名称引用 effect_config 或 macro_config");
|
||||||
|
sb.AppendLine("# F1 固定为初始化,不可修改");
|
||||||
|
sb.AppendLine();
|
||||||
|
foreach (DataGridViewRow row in dgvFkeys.Rows)
|
||||||
|
{
|
||||||
|
if (row.IsNewRow) continue;
|
||||||
|
var key = row.Cells["FKey"].Value?.ToString() ?? "";
|
||||||
|
var effect = row.Cells["Effect"].Value?.ToString() ?? "";
|
||||||
|
if (string.IsNullOrEmpty(key)) continue;
|
||||||
|
sb.AppendLine($"{key}|{effect}");
|
||||||
|
}
|
||||||
|
File.WriteAllText(FkeyPath, sb.ToString(), Encoding.UTF8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+38
-37
@@ -26,6 +26,7 @@
|
|||||||
/// Required method for Designer support - do not modify
|
/// Required method for Designer support - do not modify
|
||||||
/// the contents of this method with the code editor.
|
/// the contents of this method with the code editor.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
||||||
private void InitializeComponent()
|
private void InitializeComponent()
|
||||||
{
|
{
|
||||||
this.rich_output = new System.Windows.Forms.RichTextBox();
|
this.rich_output = new System.Windows.Forms.RichTextBox();
|
||||||
@@ -36,6 +37,8 @@
|
|||||||
this.flowLayoutPanel1 = new System.Windows.Forms.FlowLayoutPanel();
|
this.flowLayoutPanel1 = new System.Windows.Forms.FlowLayoutPanel();
|
||||||
this.tabPage_Ws = new System.Windows.Forms.TabPage();
|
this.tabPage_Ws = new System.Windows.Forms.TabPage();
|
||||||
this.flowLayoutPanel2 = new System.Windows.Forms.FlowLayoutPanel();
|
this.flowLayoutPanel2 = new System.Windows.Forms.FlowLayoutPanel();
|
||||||
|
this.tabPage_Log = new System.Windows.Forms.TabPage();
|
||||||
|
this.flowLayoutPanel3 = new System.Windows.Forms.FlowLayoutPanel();
|
||||||
this.label1 = new System.Windows.Forms.Label();
|
this.label1 = new System.Windows.Forms.Label();
|
||||||
this.list_roomCaches = new System.Windows.Forms.ListBox();
|
this.list_roomCaches = new System.Windows.Forms.ListBox();
|
||||||
this.cbx_enableProxy = new System.Windows.Forms.CheckBox();
|
this.cbx_enableProxy = new System.Windows.Forms.CheckBox();
|
||||||
@@ -43,20 +46,18 @@
|
|||||||
this.panel2 = new System.Windows.Forms.Panel();
|
this.panel2 = new System.Windows.Forms.Panel();
|
||||||
this.cbx_barrageLog = new System.Windows.Forms.CheckBox();
|
this.cbx_barrageLog = new System.Windows.Forms.CheckBox();
|
||||||
this.panel3 = new System.Windows.Forms.Panel();
|
this.panel3 = new System.Windows.Forms.Panel();
|
||||||
|
this.txb_wsaddr = new System.Windows.Forms.TextBox();
|
||||||
this.btn_updateUpProxy = new System.Windows.Forms.Button();
|
this.btn_updateUpProxy = new System.Windows.Forms.Button();
|
||||||
this.txb_upstreamProxy = new System.Windows.Forms.TextBox();
|
this.txb_upstreamProxy = new System.Windows.Forms.TextBox();
|
||||||
this.label5 = new System.Windows.Forms.Label();
|
this.label5 = new System.Windows.Forms.Label();
|
||||||
this.label4 = new System.Windows.Forms.Label();
|
this.label4 = new System.Windows.Forms.Label();
|
||||||
this.txb_wsaddr = new System.Windows.Forms.TextBox();
|
|
||||||
this.tabPage_Log = new System.Windows.Forms.TabPage();
|
|
||||||
this.flowLayoutPanel3 = new System.Windows.Forms.FlowLayoutPanel();
|
|
||||||
this.panel1.SuspendLayout();
|
this.panel1.SuspendLayout();
|
||||||
this.tab_filters.SuspendLayout();
|
this.tab_filters.SuspendLayout();
|
||||||
this.tabPage_Console.SuspendLayout();
|
this.tabPage_Console.SuspendLayout();
|
||||||
this.tabPage_Ws.SuspendLayout();
|
this.tabPage_Ws.SuspendLayout();
|
||||||
|
this.tabPage_Log.SuspendLayout();
|
||||||
this.panel2.SuspendLayout();
|
this.panel2.SuspendLayout();
|
||||||
this.panel3.SuspendLayout();
|
this.panel3.SuspendLayout();
|
||||||
this.tabPage_Log.SuspendLayout();
|
|
||||||
this.SuspendLayout();
|
this.SuspendLayout();
|
||||||
//
|
//
|
||||||
// rich_output
|
// rich_output
|
||||||
@@ -115,7 +116,7 @@
|
|||||||
// tabPage_Console
|
// tabPage_Console
|
||||||
//
|
//
|
||||||
this.tabPage_Console.Controls.Add(this.flowLayoutPanel1);
|
this.tabPage_Console.Controls.Add(this.flowLayoutPanel1);
|
||||||
this.tabPage_Console.Font = new System.Drawing.Font("SimSun", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
this.tabPage_Console.Font = new System.Drawing.Font("宋体", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(134)));
|
||||||
this.tabPage_Console.Location = new System.Drawing.Point(22, 4);
|
this.tabPage_Console.Location = new System.Drawing.Point(22, 4);
|
||||||
this.tabPage_Console.Name = "tabPage_Console";
|
this.tabPage_Console.Name = "tabPage_Console";
|
||||||
this.tabPage_Console.Padding = new System.Windows.Forms.Padding(3);
|
this.tabPage_Console.Padding = new System.Windows.Forms.Padding(3);
|
||||||
@@ -153,6 +154,27 @@
|
|||||||
this.flowLayoutPanel2.Size = new System.Drawing.Size(57, 257);
|
this.flowLayoutPanel2.Size = new System.Drawing.Size(57, 257);
|
||||||
this.flowLayoutPanel2.TabIndex = 6;
|
this.flowLayoutPanel2.TabIndex = 6;
|
||||||
//
|
//
|
||||||
|
// tabPage_Log
|
||||||
|
//
|
||||||
|
this.tabPage_Log.Controls.Add(this.flowLayoutPanel3);
|
||||||
|
this.tabPage_Log.Location = new System.Drawing.Point(22, 4);
|
||||||
|
this.tabPage_Log.Name = "tabPage_Log";
|
||||||
|
this.tabPage_Log.Size = new System.Drawing.Size(63, 263);
|
||||||
|
this.tabPage_Log.TabIndex = 2;
|
||||||
|
this.tabPage_Log.Text = "日志 ";
|
||||||
|
this.tabPage_Log.UseVisualStyleBackColor = true;
|
||||||
|
//
|
||||||
|
// flowLayoutPanel3
|
||||||
|
//
|
||||||
|
this.flowLayoutPanel3.BackColor = System.Drawing.SystemColors.Control;
|
||||||
|
this.flowLayoutPanel3.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
|
this.flowLayoutPanel3.Location = new System.Drawing.Point(0, 0);
|
||||||
|
this.flowLayoutPanel3.Margin = new System.Windows.Forms.Padding(2);
|
||||||
|
this.flowLayoutPanel3.Name = "flowLayoutPanel3";
|
||||||
|
this.flowLayoutPanel3.Padding = new System.Windows.Forms.Padding(0, 8, 0, 0);
|
||||||
|
this.flowLayoutPanel3.Size = new System.Drawing.Size(63, 263);
|
||||||
|
this.flowLayoutPanel3.TabIndex = 7;
|
||||||
|
//
|
||||||
// label1
|
// label1
|
||||||
//
|
//
|
||||||
this.label1.AutoSize = true;
|
this.label1.AutoSize = true;
|
||||||
@@ -246,6 +268,16 @@
|
|||||||
this.panel3.Size = new System.Drawing.Size(429, 159);
|
this.panel3.Size = new System.Drawing.Size(429, 159);
|
||||||
this.panel3.TabIndex = 13;
|
this.panel3.TabIndex = 13;
|
||||||
//
|
//
|
||||||
|
// txb_wsaddr
|
||||||
|
//
|
||||||
|
this.txb_wsaddr.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
||||||
|
this.txb_wsaddr.Location = new System.Drawing.Point(85, 14);
|
||||||
|
this.txb_wsaddr.Margin = new System.Windows.Forms.Padding(2);
|
||||||
|
this.txb_wsaddr.Name = "txb_wsaddr";
|
||||||
|
this.txb_wsaddr.ReadOnly = true;
|
||||||
|
this.txb_wsaddr.Size = new System.Drawing.Size(164, 21);
|
||||||
|
this.txb_wsaddr.TabIndex = 16;
|
||||||
|
//
|
||||||
// btn_updateUpProxy
|
// btn_updateUpProxy
|
||||||
//
|
//
|
||||||
this.btn_updateUpProxy.Location = new System.Drawing.Point(254, 48);
|
this.btn_updateUpProxy.Location = new System.Drawing.Point(254, 48);
|
||||||
@@ -286,37 +318,6 @@
|
|||||||
this.label4.TabIndex = 10;
|
this.label4.TabIndex = 10;
|
||||||
this.label4.Text = "WS服务端口:";
|
this.label4.Text = "WS服务端口:";
|
||||||
//
|
//
|
||||||
// txb_wsaddr
|
|
||||||
//
|
|
||||||
this.txb_wsaddr.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
|
|
||||||
this.txb_wsaddr.Location = new System.Drawing.Point(85, 14);
|
|
||||||
this.txb_wsaddr.Margin = new System.Windows.Forms.Padding(2);
|
|
||||||
this.txb_wsaddr.Name = "txb_wsaddr";
|
|
||||||
this.txb_wsaddr.ReadOnly = true;
|
|
||||||
this.txb_wsaddr.Size = new System.Drawing.Size(164, 21);
|
|
||||||
this.txb_wsaddr.TabIndex = 16;
|
|
||||||
//
|
|
||||||
// tabPage_Log
|
|
||||||
//
|
|
||||||
this.tabPage_Log.Controls.Add(this.flowLayoutPanel3);
|
|
||||||
this.tabPage_Log.Location = new System.Drawing.Point(22, 4);
|
|
||||||
this.tabPage_Log.Name = "tabPage_Log";
|
|
||||||
this.tabPage_Log.Size = new System.Drawing.Size(63, 263);
|
|
||||||
this.tabPage_Log.TabIndex = 2;
|
|
||||||
this.tabPage_Log.Text = "日志 ";
|
|
||||||
this.tabPage_Log.UseVisualStyleBackColor = true;
|
|
||||||
//
|
|
||||||
// flowLayoutPanel3
|
|
||||||
//
|
|
||||||
this.flowLayoutPanel3.BackColor = System.Drawing.SystemColors.Control;
|
|
||||||
this.flowLayoutPanel3.Dock = System.Windows.Forms.DockStyle.Fill;
|
|
||||||
this.flowLayoutPanel3.Location = new System.Drawing.Point(0, 0);
|
|
||||||
this.flowLayoutPanel3.Margin = new System.Windows.Forms.Padding(2);
|
|
||||||
this.flowLayoutPanel3.Name = "flowLayoutPanel3";
|
|
||||||
this.flowLayoutPanel3.Padding = new System.Windows.Forms.Padding(0, 8, 0, 0);
|
|
||||||
this.flowLayoutPanel3.Size = new System.Drawing.Size(63, 263);
|
|
||||||
this.flowLayoutPanel3.TabIndex = 7;
|
|
||||||
//
|
|
||||||
// FormView
|
// FormView
|
||||||
//
|
//
|
||||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
|
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
|
||||||
@@ -335,11 +336,11 @@
|
|||||||
this.tab_filters.ResumeLayout(false);
|
this.tab_filters.ResumeLayout(false);
|
||||||
this.tabPage_Console.ResumeLayout(false);
|
this.tabPage_Console.ResumeLayout(false);
|
||||||
this.tabPage_Ws.ResumeLayout(false);
|
this.tabPage_Ws.ResumeLayout(false);
|
||||||
|
this.tabPage_Log.ResumeLayout(false);
|
||||||
this.panel2.ResumeLayout(false);
|
this.panel2.ResumeLayout(false);
|
||||||
this.panel2.PerformLayout();
|
this.panel2.PerformLayout();
|
||||||
this.panel3.ResumeLayout(false);
|
this.panel3.ResumeLayout(false);
|
||||||
this.panel3.PerformLayout();
|
this.panel3.PerformLayout();
|
||||||
this.tabPage_Log.ResumeLayout(false);
|
|
||||||
this.ResumeLayout(false);
|
this.ResumeLayout(false);
|
||||||
this.PerformLayout();
|
this.PerformLayout();
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
@@ -13,6 +14,7 @@ using BarrageGrab.Forms;
|
|||||||
using BarrageGrab.Forms.Models;
|
using BarrageGrab.Forms.Models;
|
||||||
using BarrageGrab.Modles.JsonEntity;
|
using BarrageGrab.Modles.JsonEntity;
|
||||||
using BarrageGrab.Proxy;
|
using BarrageGrab.Proxy;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace BarrageGrab
|
namespace BarrageGrab
|
||||||
{
|
{
|
||||||
@@ -24,14 +26,85 @@ namespace BarrageGrab
|
|||||||
WssBarrageGrab grab = AppRuntime.WsServer.Grab;
|
WssBarrageGrab grab = AppRuntime.WsServer.Grab;
|
||||||
ISystemProxy proxy = AppRuntime.WsServer.Grab.Proxy;
|
ISystemProxy proxy = AppRuntime.WsServer.Grab.Proxy;
|
||||||
|
|
||||||
|
private Panel testPanel;
|
||||||
|
private ComboBox cmbPlayers;
|
||||||
|
private ComboBox cmbEventType;
|
||||||
|
private PictureBox picAvatar;
|
||||||
|
private TextBox txtPlayerName;
|
||||||
|
private Label lblPlayerid;
|
||||||
|
private Button btnGeneratePlayers;
|
||||||
|
private Panel inputPanel;
|
||||||
|
private Label lblDynamic1, lblDynamic2;
|
||||||
|
private TextBox txtDynamic1, txtDynamic2;
|
||||||
|
private Button btnSendTest;
|
||||||
|
private Label lblStatus;
|
||||||
|
|
||||||
public FormView()
|
public FormView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
barServer.OnPrint += WssService_OnPrint;
|
barServer.OnPrint += WssService_OnPrint;
|
||||||
barServer.Grab.Proxy.OnProxyStatus += Proxy_OnProxyStatus;
|
barServer.Grab.Proxy.OnProxyStatus += Proxy_OnProxyStatus;
|
||||||
AppRuntime.RoomCaches.OnCache += RoomCaches_OnCache;
|
AppRuntime.RoomCaches.OnCache += RoomCaches_OnCache;
|
||||||
|
|
||||||
|
// 扩大form和panel2高度
|
||||||
|
this.ClientSize = new Size(687, 650);
|
||||||
|
this.panel2.Height = 390;
|
||||||
|
this.panel1.Top = 392;
|
||||||
|
this.panel1.Height = this.ClientSize.Height - this.panel1.Top - 20;
|
||||||
|
|
||||||
|
// 固定panel3高度,不让它随panel2拉伸
|
||||||
|
this.panel3.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
|
||||||
|
this.panel3.Height = 159;
|
||||||
|
|
||||||
|
BuildTestPanel();
|
||||||
|
|
||||||
|
// 游戏配置按钮
|
||||||
|
var btnConfig = new Button
|
||||||
|
{
|
||||||
|
Text = "游戏配置",
|
||||||
|
Width = 100,
|
||||||
|
Left = 10,
|
||||||
|
Top = 90
|
||||||
|
};
|
||||||
|
btnConfig.Click += (s, e) => { using (var f = new Forms.ConfigEditorForm()) f.ShowDialog(this); };
|
||||||
|
this.panel3.Controls.Add(btnConfig);
|
||||||
|
|
||||||
|
// Settings button
|
||||||
|
var btnSettings = new Button
|
||||||
|
{
|
||||||
|
Text = "设置",
|
||||||
|
Width = 60,
|
||||||
|
Left = 120,
|
||||||
|
Top = 90
|
||||||
|
};
|
||||||
|
btnSettings.Click += (s, e) => { using (var f = new Forms.SettingsForm()) f.ShowDialog(this); };
|
||||||
|
this.panel3.Controls.Add(btnSettings);
|
||||||
|
|
||||||
|
|
||||||
|
// 打开游戏按钮
|
||||||
|
var btnLaunchGame = new Button
|
||||||
|
{
|
||||||
|
Text = "打开游戏",
|
||||||
|
Width = 80,
|
||||||
|
Left = 190,
|
||||||
|
Top = 90
|
||||||
|
};
|
||||||
|
btnLaunchGame.Click += (s, e) => { Program.LaunchGame(); };
|
||||||
|
this.panel3.Controls.Add(btnLaunchGame);
|
||||||
|
|
||||||
|
// 重新打开游戏按钮
|
||||||
|
var btnRestartGame = new Button
|
||||||
|
{
|
||||||
|
Text = "重启游戏",
|
||||||
|
Width = 80,
|
||||||
|
Left = 280,
|
||||||
|
Top = 90
|
||||||
|
};
|
||||||
|
btnRestartGame.Click += (s, e) => { Program.RestartGame(); };
|
||||||
|
this.panel3.Controls.Add(btnRestartGame);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void InitTabPages()
|
private void InitTabPages()
|
||||||
{
|
{
|
||||||
var msgTypes = GetMsgTypes();
|
var msgTypes = GetMsgTypes();
|
||||||
@@ -235,5 +308,351 @@ namespace BarrageGrab
|
|||||||
AppSetting.Current.BarrageLog = checker.Checked;
|
AppSetting.Current.BarrageLog = checker.Checked;
|
||||||
AppSetting.Current.Save();
|
AppSetting.Current.Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 测试面板 ====================
|
||||||
|
|
||||||
|
private readonly string DefaultAvatarUrl = "https://aliyun_id_photo_bucket.oss.aliyuncs.com/default_handsome_intl.jpg";
|
||||||
|
private readonly List<(long Id, string Name)> _testPlayers = new List<(long Id, string Name)>();
|
||||||
|
private static readonly string[] SampleNames = {
|
||||||
|
"小明", "小红", "玩家001", "铁柱", "翠花", "老六", "大佬666",
|
||||||
|
"战神", "小可爱", "路人甲", "氪金玩家", "欧皇", "非酋", "主播铁粉",
|
||||||
|
"水友123", "弹幕刷屏王", "白嫖党", "榜一大哥", "守塔小兵", "运营总监"
|
||||||
|
};
|
||||||
|
|
||||||
|
private void BuildTestPanel()
|
||||||
|
{
|
||||||
|
// 测试面板标题
|
||||||
|
int yBase = 195;
|
||||||
|
var lblTitle = new Label
|
||||||
|
{
|
||||||
|
Text = "📡 WebSocket 模拟测试",
|
||||||
|
Location = new Point(250, yBase),
|
||||||
|
Size = new Size(200, 18),
|
||||||
|
Font = new Font("微软雅黑", 10, FontStyle.Bold),
|
||||||
|
ForeColor = Color.DarkBlue
|
||||||
|
};
|
||||||
|
panel2.Controls.Add(lblTitle);
|
||||||
|
|
||||||
|
// ── 生成按钮 ──
|
||||||
|
btnGeneratePlayers = new Button
|
||||||
|
{
|
||||||
|
Text = "随机生成玩家",
|
||||||
|
Location = new Point(250, yBase + 20),
|
||||||
|
Size = new Size(130, 24),
|
||||||
|
BackColor = Color.LightSteelBlue,
|
||||||
|
UseVisualStyleBackColor = true
|
||||||
|
};
|
||||||
|
btnGeneratePlayers.Click += BtnGeneratePlayers_Click;
|
||||||
|
panel2.Controls.Add(btnGeneratePlayers);
|
||||||
|
|
||||||
|
// ── 玩家下拉(含头像) ──
|
||||||
|
cmbPlayers = new ComboBox
|
||||||
|
{
|
||||||
|
Location = new Point(250, yBase + 46),
|
||||||
|
Size = new Size(310, 22),
|
||||||
|
DropDownStyle = ComboBoxStyle.DropDownList,
|
||||||
|
DropDownWidth = 400
|
||||||
|
};
|
||||||
|
cmbPlayers.SelectedIndexChanged += CmbPlayers_SelectedIndexChanged;
|
||||||
|
panel2.Controls.Add(cmbPlayers);
|
||||||
|
|
||||||
|
lblPlayerid = new Label
|
||||||
|
{
|
||||||
|
Text = "ID: --",
|
||||||
|
Location = new Point(565, yBase + 48),
|
||||||
|
Size = new Size(110, 18),
|
||||||
|
ForeColor = Color.Gray
|
||||||
|
};
|
||||||
|
panel2.Controls.Add(lblPlayerid);
|
||||||
|
|
||||||
|
// ── 事件类型下拉 ──
|
||||||
|
var lblEvent = new Label { Text = "事件:", Location = new Point(250, yBase + 74), Size = new Size(40, 20) };
|
||||||
|
panel2.Controls.Add(lblEvent);
|
||||||
|
|
||||||
|
cmbEventType = new ComboBox
|
||||||
|
{
|
||||||
|
Location = new Point(290, yBase + 72),
|
||||||
|
Size = new Size(130, 22),
|
||||||
|
DropDownStyle = ComboBoxStyle.DropDownList
|
||||||
|
};
|
||||||
|
cmbEventType.Items.AddRange(new object[] { "弹幕消息", "点赞消息", "礼物消息", "关注消息", "进直播间", "直播间分享", "粉丝团消息" });
|
||||||
|
cmbEventType.SelectedIndex = 0;
|
||||||
|
cmbEventType.SelectedIndexChanged += CmbEventType_SelectedIndexChanged;
|
||||||
|
panel2.Controls.Add(cmbEventType);
|
||||||
|
|
||||||
|
// ── 发送按钮 ──
|
||||||
|
btnSendTest = new Button
|
||||||
|
{
|
||||||
|
Text = "发送",
|
||||||
|
Location = new Point(430, yBase + 70),
|
||||||
|
Size = new Size(130, 26),
|
||||||
|
BackColor = Color.Orange,
|
||||||
|
Font = new Font("微软雅黑", 9, FontStyle.Bold),
|
||||||
|
UseVisualStyleBackColor = true
|
||||||
|
};
|
||||||
|
btnSendTest.Click += BtnSendTest_Click;
|
||||||
|
panel2.Controls.Add(btnSendTest);
|
||||||
|
|
||||||
|
// ── 动态输入面板 ──
|
||||||
|
inputPanel = new Panel
|
||||||
|
{
|
||||||
|
Location = new Point(248, yBase + 100),
|
||||||
|
Size = new Size(430, 50),
|
||||||
|
BorderStyle = BorderStyle.FixedSingle
|
||||||
|
};
|
||||||
|
panel2.Controls.Add(inputPanel);
|
||||||
|
|
||||||
|
lblDynamic1 = new Label { Location = new Point(8, 8), Size = new Size(50, 18), Text = "内容:" };
|
||||||
|
inputPanel.Controls.Add(lblDynamic1);
|
||||||
|
txtDynamic1 = new TextBox { Location = new Point(60, 6), Size = new Size(200, 21), Text = "测试弹幕666" };
|
||||||
|
inputPanel.Controls.Add(txtDynamic1);
|
||||||
|
|
||||||
|
lblDynamic2 = new Label { Location = new Point(8, 30), Size = new Size(50, 18), Text = "" };
|
||||||
|
inputPanel.Controls.Add(lblDynamic2);
|
||||||
|
txtDynamic2 = new TextBox { Location = new Point(60, 28), Size = new Size(80, 21), Text = "", Visible = false };
|
||||||
|
inputPanel.Controls.Add(txtDynamic2);
|
||||||
|
|
||||||
|
// ── 状态 ──
|
||||||
|
lblStatus = new Label
|
||||||
|
{
|
||||||
|
Location = new Point(250, yBase + 155),
|
||||||
|
Size = new Size(425, 18),
|
||||||
|
ForeColor = Color.DimGray
|
||||||
|
};
|
||||||
|
panel2.Controls.Add(lblStatus);
|
||||||
|
|
||||||
|
// 初始刷新
|
||||||
|
RefreshInputPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BtnGeneratePlayers_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
_testPlayers.Clear();
|
||||||
|
cmbPlayers.Items.Clear();
|
||||||
|
|
||||||
|
var rnd = new Random();
|
||||||
|
// 生成28个完全随机玩家
|
||||||
|
for (int i = 0; i < 28; i++)
|
||||||
|
{
|
||||||
|
long id = 10000 + rnd.Next(1, 99999);
|
||||||
|
string name = SampleNames[rnd.Next(SampleNames.Length)];
|
||||||
|
if (rnd.Next(2) == 0) name += rnd.Next(10, 999).ToString();
|
||||||
|
_testPlayers.Add((id, name));
|
||||||
|
cmbPlayers.Items.Add($"{name} (ID:{id})");
|
||||||
|
}
|
||||||
|
|
||||||
|
cmbPlayers.SelectedIndex = 0;
|
||||||
|
lblStatus.Text = $"已生成 {_testPlayers.Count} 个测试玩家";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CmbPlayers_SelectedIndexChanged(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (cmbPlayers.SelectedIndex >= 0 && cmbPlayers.SelectedIndex < _testPlayers.Count)
|
||||||
|
{
|
||||||
|
var (id, name) = _testPlayers[cmbPlayers.SelectedIndex];
|
||||||
|
lblPlayerid.Text = $"ID: {id}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CmbEventType_SelectedIndexChanged(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
RefreshInputPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshInputPanel()
|
||||||
|
{
|
||||||
|
string evt = cmbEventType.SelectedItem?.ToString() ?? "";
|
||||||
|
switch (evt)
|
||||||
|
{
|
||||||
|
case "弹幕消息":
|
||||||
|
lblDynamic1.Text = "内容:";
|
||||||
|
txtDynamic1.Visible = true;
|
||||||
|
txtDynamic1.Text = "测试弹幕666";
|
||||||
|
lblDynamic2.Text = "";
|
||||||
|
txtDynamic2.Visible = false;
|
||||||
|
break;
|
||||||
|
case "点赞消息":
|
||||||
|
lblDynamic1.Text = "点赞数:";
|
||||||
|
txtDynamic1.Visible = true;
|
||||||
|
txtDynamic1.Text = "10";
|
||||||
|
lblDynamic2.Text = "";
|
||||||
|
txtDynamic2.Visible = false;
|
||||||
|
break;
|
||||||
|
case "礼物消息":
|
||||||
|
lblDynamic1.Text = "礼物名:";
|
||||||
|
txtDynamic1.Visible = true;
|
||||||
|
txtDynamic1.Text = "小心心";
|
||||||
|
lblDynamic2.Text = "数量:";
|
||||||
|
txtDynamic2.Visible = true;
|
||||||
|
txtDynamic2.Text = "1";
|
||||||
|
break;
|
||||||
|
case "直播间分享":
|
||||||
|
lblDynamic1.Text = "次数:";
|
||||||
|
txtDynamic1.Visible = true;
|
||||||
|
txtDynamic1.Text = "1";
|
||||||
|
lblDynamic2.Text = "";
|
||||||
|
txtDynamic2.Visible = false;
|
||||||
|
break;
|
||||||
|
default: // 关注/进房/粉丝团
|
||||||
|
lblDynamic1.Text = "无额外参数";
|
||||||
|
txtDynamic1.Visible = false;
|
||||||
|
lblDynamic2.Text = "";
|
||||||
|
txtDynamic2.Visible = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BtnSendTest_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (cmbPlayers.SelectedIndex < 0 || cmbPlayers.SelectedIndex >= _testPlayers.Count)
|
||||||
|
{
|
||||||
|
lblStatus.Text = "❌ 请先选择玩家";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var (pid, pname) = _testPlayers[cmbPlayers.SelectedIndex];
|
||||||
|
string evt = cmbEventType.SelectedItem?.ToString() ?? "弹幕消息";
|
||||||
|
|
||||||
|
MsgUser user = new MsgUser
|
||||||
|
{
|
||||||
|
Id = pid,
|
||||||
|
Nickname = pname,
|
||||||
|
HeadImgUrl = DefaultAvatarUrl,
|
||||||
|
DisplayId = pid.ToString(),
|
||||||
|
Level = 10,
|
||||||
|
Gender = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
BarrageMsgPack pack = null;
|
||||||
|
string desc = "";
|
||||||
|
|
||||||
|
switch (evt)
|
||||||
|
{
|
||||||
|
case "弹幕消息":
|
||||||
|
{
|
||||||
|
var msg = new Msg
|
||||||
|
{
|
||||||
|
MsgId = DateTime.Now.Ticks,
|
||||||
|
User = user,
|
||||||
|
Content = txtDynamic1.Text,
|
||||||
|
RoomId = "123456"
|
||||||
|
};
|
||||||
|
pack = new BarrageMsgPack(JsonConvert.SerializeObject(msg), PackMsgType.弹幕消息, "test");
|
||||||
|
desc = $"弹幕: {txtDynamic1.Text}";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "点赞消息":
|
||||||
|
{
|
||||||
|
long cnt = long.TryParse(txtDynamic1.Text, out var c) ? c : 1;
|
||||||
|
var msg = new LikeMsg
|
||||||
|
{
|
||||||
|
MsgId = DateTime.Now.Ticks,
|
||||||
|
User = user,
|
||||||
|
Count = cnt,
|
||||||
|
Total = cnt + 100,
|
||||||
|
RoomId = "123456"
|
||||||
|
};
|
||||||
|
pack = new BarrageMsgPack(JsonConvert.SerializeObject(msg), PackMsgType.点赞消息, "test");
|
||||||
|
desc = $"点赞 x{cnt}";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "礼物消息":
|
||||||
|
{
|
||||||
|
long cnt = long.TryParse(txtDynamic2.Text, out var gc) ? gc : 1;
|
||||||
|
var msg = new GiftMsg
|
||||||
|
{
|
||||||
|
MsgId = DateTime.Now.Ticks,
|
||||||
|
User = user,
|
||||||
|
GiftName = txtDynamic1.Text,
|
||||||
|
GiftCount = cnt,
|
||||||
|
GiftId = 100,
|
||||||
|
DiamondCount = 1,
|
||||||
|
RoomId = "123456"
|
||||||
|
};
|
||||||
|
pack = new BarrageMsgPack(JsonConvert.SerializeObject(msg), PackMsgType.礼物消息, "test");
|
||||||
|
desc = $"礼物: {txtDynamic1.Text} x{cnt}";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "关注消息":
|
||||||
|
{
|
||||||
|
var msg = new Msg
|
||||||
|
{
|
||||||
|
MsgId = DateTime.Now.Ticks,
|
||||||
|
User = user,
|
||||||
|
Content = "关注了主播",
|
||||||
|
RoomId = "123456"
|
||||||
|
};
|
||||||
|
pack = new BarrageMsgPack(JsonConvert.SerializeObject(msg), PackMsgType.关注消息, "test");
|
||||||
|
desc = "关注主播";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "进直播间":
|
||||||
|
{
|
||||||
|
var msg = new MemberMessage
|
||||||
|
{
|
||||||
|
MsgId = DateTime.Now.Ticks,
|
||||||
|
User = user,
|
||||||
|
CurrentCount = 999,
|
||||||
|
RoomId = "123456"
|
||||||
|
};
|
||||||
|
pack = new BarrageMsgPack(JsonConvert.SerializeObject(msg), PackMsgType.进直播间, "test");
|
||||||
|
desc = "进入直播间";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "直播间分享":
|
||||||
|
{
|
||||||
|
long cnt = long.TryParse(txtDynamic1.Text, out var sc) ? sc : 1;
|
||||||
|
var msg = new ShareMessage
|
||||||
|
{
|
||||||
|
MsgId = DateTime.Now.Ticks,
|
||||||
|
User = user,
|
||||||
|
ShareType = BarrageGrab.Modles.JsonEntity.ShareType.微信,
|
||||||
|
RoomId = "123456"
|
||||||
|
};
|
||||||
|
desc = $"分享 x{cnt}";
|
||||||
|
// 分享用次数送多次
|
||||||
|
for (int i = 0; i < cnt; i++)
|
||||||
|
{
|
||||||
|
pack = new BarrageMsgPack(JsonConvert.SerializeObject(msg), PackMsgType.直播间分享, "test");
|
||||||
|
barServer.Broadcast(pack);
|
||||||
|
}
|
||||||
|
lblStatus.Text = $"✅ 已发送: {pname}(ID:{pid}) → {desc}";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "粉丝团消息":
|
||||||
|
{
|
||||||
|
var msg = new FansclubMsg
|
||||||
|
{
|
||||||
|
MsgId = DateTime.Now.Ticks,
|
||||||
|
User = user,
|
||||||
|
Type = 2, // 加入
|
||||||
|
Level = 1,
|
||||||
|
RoomId = "123456"
|
||||||
|
};
|
||||||
|
pack = new BarrageMsgPack(JsonConvert.SerializeObject(msg), PackMsgType.粉丝团消息, "test");
|
||||||
|
desc = "加入粉丝团";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pack != null)
|
||||||
|
{
|
||||||
|
barServer.Broadcast(pack);
|
||||||
|
lblStatus.Text = $"✅ 已发送: {pname}(ID:{pid}) → {desc}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
lblStatus.Text = $"❌ 发送失败: {ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,630 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace BarrageGrab.Forms
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 完整设置界面 —— 可视化编辑 AppConfig.json + 游戏集成
|
||||||
|
/// 配置项分8个Tab: 常规/网络/弹幕过滤/轮询/进程域名/直播伴侣/串口/游戏启动项
|
||||||
|
/// 游戏启动项 Tab 管理启动项 + 5个配置表路径
|
||||||
|
/// </summary>
|
||||||
|
public class SettingsForm : Form
|
||||||
|
{
|
||||||
|
private TabControl tabMain;
|
||||||
|
private Label lblStatus;
|
||||||
|
|
||||||
|
private string ConfigPath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "appConfig.json");
|
||||||
|
private string GameSettingsPath => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "game_settings.json");
|
||||||
|
|
||||||
|
// 常规
|
||||||
|
private ComboBox cmbPlatform;
|
||||||
|
private CheckBox cbHideConsole, cbShowWindow;
|
||||||
|
// 网络
|
||||||
|
private NumericUpDown nudWsPort, nudProxyPort;
|
||||||
|
private CheckBox cbListenAny, cbProxyEnabled;
|
||||||
|
private TextBox txtUpstreamProxy;
|
||||||
|
// 弹幕过滤
|
||||||
|
private CheckBox cbPrintBarrage, cbBarrageLog;
|
||||||
|
private CheckedListBox clbPrintFilter, clbPushFilter, clbLogFilter;
|
||||||
|
// 轮询
|
||||||
|
private CheckBox cbForcePolling;
|
||||||
|
private NumericUpDown nudPollingInterval;
|
||||||
|
// 进程/域名
|
||||||
|
private TextBox txtProcessFilter, txtHostNameList, txtWebRoomIds;
|
||||||
|
private CheckBox cbFilterHostName;
|
||||||
|
// 直播伴侣
|
||||||
|
private TextBox txtLiveCompanPath;
|
||||||
|
private CheckBox cbLiveCompanHook, cbAutoPause;
|
||||||
|
// 串口
|
||||||
|
private TextBox txtComPort, txtComBaud;
|
||||||
|
// 游戏启动项
|
||||||
|
private TextBox txtGameExePath, txtGameWorkDir;
|
||||||
|
private CheckBox cbAutoLaunchGame;
|
||||||
|
// 游戏配置表路径
|
||||||
|
private TextBox txtConfigDir;
|
||||||
|
private TextBox txtEffectFile, txtMacroFile, txtGiftFile, txtCmdFile, txtFkeyFile;
|
||||||
|
|
||||||
|
private readonly Tuple<int, string>[] MsgTypeOptions = {
|
||||||
|
Tuple.Create(1, "1-弹幕消息"),
|
||||||
|
Tuple.Create(2, "2-点赞消息"),
|
||||||
|
Tuple.Create(3, "3-进入直播间"),
|
||||||
|
Tuple.Create(4, "4-关注消息"),
|
||||||
|
Tuple.Create(5, "5-礼物消息"),
|
||||||
|
Tuple.Create(6, "6-统计消息"),
|
||||||
|
Tuple.Create(7, "7-粉丝团消息"),
|
||||||
|
Tuple.Create(8, "8-直播间分享"),
|
||||||
|
Tuple.Create(9, "9-下播"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 默认值
|
||||||
|
private const string DefaultGameExe = @"..\Fantasy.Map.Simulator\FantasyMapSimulator.exe";
|
||||||
|
private const string DefaultGameDir = @"..\Fantasy.Map.Simulator";
|
||||||
|
private const string DefaultConfigDir = @"..\Fantasy.Map.Simulator\BepInEx\config";
|
||||||
|
|
||||||
|
public SettingsForm()
|
||||||
|
{
|
||||||
|
this.Text = "弹幕插件设置";
|
||||||
|
this.ClientSize = new Size(620, 620);
|
||||||
|
this.StartPosition = FormStartPosition.CenterParent;
|
||||||
|
this.FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||||
|
this.MaximizeBox = false;
|
||||||
|
BuildUI();
|
||||||
|
LoadAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildUI()
|
||||||
|
{
|
||||||
|
tabMain = new TabControl { Dock = DockStyle.Top, Height = 540, Name = "tabMain" };
|
||||||
|
this.Controls.Add(tabMain);
|
||||||
|
|
||||||
|
var pnlBottom = new Panel { Dock = DockStyle.Bottom, Height = 50 };
|
||||||
|
this.Controls.Add(pnlBottom);
|
||||||
|
|
||||||
|
var btnSave = new Button { Text = "保存并应用", Width = 110, Height = 30, Left = 140, Top = 10, BackColor = Color.LightGreen };
|
||||||
|
btnSave.Click += (s, e) => SaveAll();
|
||||||
|
pnlBottom.Controls.Add(btnSave);
|
||||||
|
|
||||||
|
var btnCancel = new Button { Text = "取消", Width = 80, Height = 30, Left = 260, Top = 10 };
|
||||||
|
btnCancel.Click += (s, e) => this.Close();
|
||||||
|
pnlBottom.Controls.Add(btnCancel);
|
||||||
|
|
||||||
|
var btnReload = new Button { Text = "重新加载", Width = 90, Height = 30, Left = 350, Top = 10 };
|
||||||
|
btnReload.Click += (s, e) => LoadAll();
|
||||||
|
pnlBottom.Controls.Add(btnReload);
|
||||||
|
|
||||||
|
lblStatus = new Label { AutoSize = true, Left = 450, Top = 18, ForeColor = Color.DimGray, Text = "" };
|
||||||
|
pnlBottom.Controls.Add(lblStatus);
|
||||||
|
|
||||||
|
BuildGeneralTab();
|
||||||
|
BuildNetworkTab();
|
||||||
|
BuildBarrageFilterTab();
|
||||||
|
BuildPollingTab();
|
||||||
|
BuildProcessDomainTab();
|
||||||
|
BuildLiveCompanionTab();
|
||||||
|
BuildComPortTab();
|
||||||
|
BuildGameTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildGeneralTab()
|
||||||
|
{
|
||||||
|
var page = new TabPage("常规");
|
||||||
|
page.Controls.Add(new Label { Text = "直播平台:", Left = 20, Top = 20, AutoSize = true });
|
||||||
|
cmbPlatform = new ComboBox { Left = 120, Top = 17, Width = 150, DropDownStyle = ComboBoxStyle.DropDownList };
|
||||||
|
cmbPlatform.Items.AddRange(new object[] { "douyin", "kuaishou" });
|
||||||
|
page.Controls.Add(cmbPlatform);
|
||||||
|
|
||||||
|
cbHideConsole = new CheckBox { Text = "隐藏控制台", Left = 20, Top = 60, AutoSize = true };
|
||||||
|
page.Controls.Add(cbHideConsole);
|
||||||
|
|
||||||
|
cbShowWindow = new CheckBox { Text = "显示主窗体", Left = 20, Top = 90, AutoSize = true };
|
||||||
|
page.Controls.Add(cbShowWindow);
|
||||||
|
|
||||||
|
page.Controls.Add(new Label { Text = "提示: 平台切换需重启程序生效", Left = 20, Top = 150, AutoSize = true, ForeColor = Color.Gray });
|
||||||
|
tabMain.TabPages.Add(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildNetworkTab()
|
||||||
|
{
|
||||||
|
var page = new TabPage("网络");
|
||||||
|
page.Controls.Add(new Label { Text = "WebSocket 端口:", Left = 20, Top = 20, AutoSize = true });
|
||||||
|
nudWsPort = new NumericUpDown { Left = 150, Top = 17, Width = 80, Minimum = 1, Maximum = 65535 };
|
||||||
|
page.Controls.Add(nudWsPort);
|
||||||
|
|
||||||
|
cbListenAny = new CheckBox { Text = "监听任意IP(0.0.0.0)", Left = 20, Top = 55, AutoSize = true };
|
||||||
|
page.Controls.Add(cbListenAny);
|
||||||
|
|
||||||
|
page.Controls.Add(new Label { Text = "系统代理端口:", Left = 20, Top = 90, AutoSize = true });
|
||||||
|
nudProxyPort = new NumericUpDown { Left = 150, Top = 87, Width = 80, Minimum = 1, Maximum = 65535 };
|
||||||
|
page.Controls.Add(nudProxyPort);
|
||||||
|
|
||||||
|
cbProxyEnabled = new CheckBox { Text = "启用系统代理", Left = 20, Top = 125, AutoSize = true };
|
||||||
|
page.Controls.Add(cbProxyEnabled);
|
||||||
|
|
||||||
|
page.Controls.Add(new Label { Text = "上游代理地址:", Left = 20, Top = 160, AutoSize = true });
|
||||||
|
txtUpstreamProxy = new TextBox { Left = 150, Top = 157, Width = 250 };
|
||||||
|
page.Controls.Add(txtUpstreamProxy);
|
||||||
|
page.Controls.Add(new Label { Text = "(例如: 127.0.0.1:11223, 不要带http://)", Left = 150, Top = 183, AutoSize = true, ForeColor = Color.Gray });
|
||||||
|
tabMain.TabPages.Add(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildBarrageFilterTab()
|
||||||
|
{
|
||||||
|
var page = new TabPage("弹幕过滤");
|
||||||
|
cbPrintBarrage = new CheckBox { Text = "控制台打印弹幕", Left = 10, Top = 10, AutoSize = true };
|
||||||
|
page.Controls.Add(cbPrintBarrage);
|
||||||
|
cbBarrageLog = new CheckBox { Text = "弹幕写入日志文件", Left = 200, Top = 10, AutoSize = true };
|
||||||
|
page.Controls.Add(cbBarrageLog);
|
||||||
|
|
||||||
|
page.Controls.Add(new Label { Text = "打印过滤器:", Left = 10, Top = 40, AutoSize = true });
|
||||||
|
clbPrintFilter = CreateFilterListBox();
|
||||||
|
clbPrintFilter.Location = new Point(10, 60);
|
||||||
|
page.Controls.Add(clbPrintFilter);
|
||||||
|
|
||||||
|
page.Controls.Add(new Label { Text = "推送过滤器:", Left = 220, Top = 40, AutoSize = true });
|
||||||
|
clbPushFilter = CreateFilterListBox();
|
||||||
|
clbPushFilter.Location = new Point(220, 60);
|
||||||
|
page.Controls.Add(clbPushFilter);
|
||||||
|
|
||||||
|
page.Controls.Add(new Label { Text = "日志过滤器:", Left = 430, Top = 40, AutoSize = true });
|
||||||
|
clbLogFilter = CreateFilterListBox();
|
||||||
|
clbLogFilter.Location = new Point(430, 60);
|
||||||
|
page.Controls.Add(clbLogFilter);
|
||||||
|
tabMain.TabPages.Add(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckedListBox CreateFilterListBox()
|
||||||
|
{
|
||||||
|
var clb = new CheckedListBox { Width = 200, Height = 200 };
|
||||||
|
foreach (var opt in MsgTypeOptions) clb.Items.Add(opt.Item2, false);
|
||||||
|
return clb;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildPollingTab()
|
||||||
|
{
|
||||||
|
var page = new TabPage("轮询");
|
||||||
|
cbForcePolling = new CheckBox { Text = "强制启用轮询模式获取弹幕", Left = 20, Top = 20, AutoSize = true };
|
||||||
|
page.Controls.Add(cbForcePolling);
|
||||||
|
page.Controls.Add(new Label { Text = "轮询间隔(ms):", Left = 20, Top = 60, AutoSize = true });
|
||||||
|
nudPollingInterval = new NumericUpDown { Left = 150, Top = 57, Width = 80, Minimum = 500, Maximum = 60000, Increment = 500 };
|
||||||
|
page.Controls.Add(nudPollingInterval);
|
||||||
|
page.Controls.Add(new Label { Text = "轮询模式更稳定但响应较慢", Left = 20, Top = 100, AutoSize = true, ForeColor = Color.Gray });
|
||||||
|
page.Controls.Add(new Label { Text = "不建议小于1000ms", Left = 20, Top = 120, AutoSize = true, ForeColor = Color.Gray });
|
||||||
|
tabMain.TabPages.Add(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildProcessDomainTab()
|
||||||
|
{
|
||||||
|
var page = new TabPage("进程/域名");
|
||||||
|
page.Controls.Add(new Label { Text = "进程过滤器(逗号分隔):", Left = 20, Top = 20, AutoSize = true });
|
||||||
|
txtProcessFilter = new TextBox { Left = 20, Top = 40, Width = 550, Multiline = true, Height = 40 };
|
||||||
|
page.Controls.Add(txtProcessFilter);
|
||||||
|
page.Controls.Add(new Label { Text = "只监听这些进程的弹幕数据", Left = 20, Top = 85, AutoSize = true, ForeColor = Color.Gray });
|
||||||
|
|
||||||
|
cbFilterHostName = new CheckBox { Text = "启用域名过滤(降低CPU占用)", Left = 20, Top = 115, AutoSize = true };
|
||||||
|
page.Controls.Add(cbFilterHostName);
|
||||||
|
|
||||||
|
page.Controls.Add(new Label { Text = "域名白名单(逗号分隔):", Left = 20, Top = 145, AutoSize = true });
|
||||||
|
txtHostNameList = new TextBox { Left = 20, Top = 165, Width = 550 };
|
||||||
|
page.Controls.Add(txtHostNameList);
|
||||||
|
|
||||||
|
page.Controls.Add(new Label { Text = "Web房间ID过滤(逗号分隔):", Left = 20, Top = 195, AutoSize = true });
|
||||||
|
txtWebRoomIds = new TextBox { Left = 20, Top = 215, Width = 550 };
|
||||||
|
page.Controls.Add(txtWebRoomIds);
|
||||||
|
page.Controls.Add(new Label { Text = "只监听指定房间,直播伴侣不支持", Left = 20, Top = 240, AutoSize = true, ForeColor = Color.Gray });
|
||||||
|
tabMain.TabPages.Add(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildLiveCompanionTab()
|
||||||
|
{
|
||||||
|
var page = new TabPage("直播伴侣");
|
||||||
|
page.Controls.Add(new Label { Text = "直播伴侣exe路径:", Left = 20, Top = 20, AutoSize = true });
|
||||||
|
txtLiveCompanPath = new TextBox { Left = 20, Top = 40, Width = 450 };
|
||||||
|
page.Controls.Add(txtLiveCompanPath);
|
||||||
|
var btnBrowse = new Button { Text = "浏览...", Left = 480, Top = 39, Width = 70 };
|
||||||
|
btnBrowse.Click += (s, e) =>
|
||||||
|
{
|
||||||
|
using (var dlg = new OpenFileDialog { Filter = "EXE文件|*.exe|所有文件|*.*" })
|
||||||
|
if (dlg.ShowDialog() == DialogResult.OK) txtLiveCompanPath.Text = dlg.FileName;
|
||||||
|
};
|
||||||
|
page.Controls.Add(btnBrowse);
|
||||||
|
page.Controls.Add(new Label { Text = "(留空则程序自动查找)", Left = 20, Top = 65, AutoSize = true, ForeColor = Color.Gray });
|
||||||
|
|
||||||
|
cbLiveCompanHook = new CheckBox { Text = "Hook直播伴侣代理", Left = 20, Top = 95, AutoSize = true };
|
||||||
|
page.Controls.Add(cbLiveCompanHook);
|
||||||
|
cbAutoPause = new CheckBox { Text = "进入直播间自动暂停", Left = 20, Top = 125, AutoSize = true };
|
||||||
|
page.Controls.Add(cbAutoPause);
|
||||||
|
tabMain.TabPages.Add(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildComPortTab()
|
||||||
|
{
|
||||||
|
var page = new TabPage("串口");
|
||||||
|
page.Controls.Add(new Label { Text = "COM端口:", Left = 20, Top = 20, AutoSize = true });
|
||||||
|
txtComPort = new TextBox { Left = 120, Top = 17, Width = 100 };
|
||||||
|
page.Controls.Add(txtComPort);
|
||||||
|
page.Controls.Add(new Label { Text = "例如: COM1", Left = 230, Top = 20, AutoSize = true, ForeColor = Color.Gray });
|
||||||
|
page.Controls.Add(new Label { Text = "波特率:", Left = 20, Top = 55, AutoSize = true });
|
||||||
|
txtComBaud = new TextBox { Left = 120, Top = 52, Width = 100 };
|
||||||
|
page.Controls.Add(txtComBaud);
|
||||||
|
page.Controls.Add(new Label { Text = "例如: 9600", Left = 230, Top = 55, AutoSize = true, ForeColor = Color.Gray });
|
||||||
|
page.Controls.Add(new Label { Text = "留空则不启用串口抄送服务", Left = 20, Top = 95, AutoSize = true, ForeColor = Color.Gray });
|
||||||
|
tabMain.TabPages.Add(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildGameTab()
|
||||||
|
{
|
||||||
|
var page = new TabPage("游戏启动项");
|
||||||
|
int y = 15;
|
||||||
|
|
||||||
|
// === 启动项 ===
|
||||||
|
page.Controls.Add(new Label { Text = "── 启动项 ──", Left = 20, Top = y, AutoSize = true, ForeColor = Color.SteelBlue, Font = new Font(Font, FontStyle.Bold) });
|
||||||
|
y += 25;
|
||||||
|
|
||||||
|
page.Controls.Add(new Label { Text = "游戏exe路径:", Left = 20, Top = y, AutoSize = true });
|
||||||
|
txtGameExePath = new TextBox { Left = 120, Top = y - 3, Width = 380 };
|
||||||
|
page.Controls.Add(txtGameExePath);
|
||||||
|
var btnBrowse1 = new Button { Text = "浏览...", Left = 510, Top = y - 4, Width = 70 };
|
||||||
|
btnBrowse1.Click += (s, e) =>
|
||||||
|
{
|
||||||
|
using (var dlg = new OpenFileDialog { Filter = "EXE文件|*.exe|所有文件|*.*" })
|
||||||
|
if (dlg.ShowDialog() == DialogResult.OK)
|
||||||
|
{
|
||||||
|
txtGameExePath.Text = ToRelativePath(dlg.FileName);
|
||||||
|
var dir = Path.GetDirectoryName(dlg.FileName);
|
||||||
|
if (!string.IsNullOrEmpty(dir)) txtGameWorkDir.Text = ToRelativePath(dir);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
page.Controls.Add(btnBrowse1);
|
||||||
|
y += 27;
|
||||||
|
|
||||||
|
page.Controls.Add(new Label { Text = "工作目录:", Left = 20, Top = y, AutoSize = true });
|
||||||
|
txtGameWorkDir = new TextBox { Left = 120, Top = y - 3, Width = 380 };
|
||||||
|
page.Controls.Add(txtGameWorkDir);
|
||||||
|
y += 27;
|
||||||
|
|
||||||
|
cbAutoLaunchGame = new CheckBox { Text = "启动时自动打开游戏", Left = 20, Top = y, AutoSize = true };
|
||||||
|
page.Controls.Add(cbAutoLaunchGame);
|
||||||
|
y += 30;
|
||||||
|
|
||||||
|
// === 配置表路径 ===
|
||||||
|
page.Controls.Add(new Label { Text = "── 配置表路径 ──", Left = 20, Top = y, AutoSize = true, ForeColor = Color.SteelBlue, Font = new Font(Font, FontStyle.Bold) });
|
||||||
|
y += 25;
|
||||||
|
|
||||||
|
page.Controls.Add(new Label { Text = "配置表目录:", Left = 20, Top = y, AutoSize = true });
|
||||||
|
txtConfigDir = new TextBox { Left = 120, Top = y - 3, Width = 380 };
|
||||||
|
page.Controls.Add(txtConfigDir);
|
||||||
|
var btnBrowse2 = new Button { Text = "浏览...", Left = 510, Top = y - 4, Width = 70 };
|
||||||
|
btnBrowse2.Click += (s, e) =>
|
||||||
|
{
|
||||||
|
using (var dlg = new FolderBrowserDialog())
|
||||||
|
if (dlg.ShowDialog() == DialogResult.OK) txtConfigDir.Text = ToRelativePath(dlg.SelectedPath);
|
||||||
|
};
|
||||||
|
page.Controls.Add(btnBrowse2);
|
||||||
|
y += 30;
|
||||||
|
|
||||||
|
// 5个配置表文件名
|
||||||
|
txtEffectFile = AddConfigFileRow(page, ref y, "基础效果(effect):", "effect_config.txt");
|
||||||
|
txtMacroFile = AddConfigFileRow(page, ref y, "组合宏(macro):", "macro_config.txt");
|
||||||
|
txtGiftFile = AddConfigFileRow(page, ref y, "礼物事件(gift):", "gift_config.txt");
|
||||||
|
txtCmdFile = AddConfigFileRow(page, ref y, "弹幕指令(command):", "command_config.txt");
|
||||||
|
txtFkeyFile = AddConfigFileRow(page, ref y, "F键映射(fkey):", "fkey_config.txt");
|
||||||
|
y += 5;
|
||||||
|
|
||||||
|
// === 功能按钮 ===
|
||||||
|
var btnOpenGameConfig = new Button { Text = "打开配置编辑器", Left = 20, Top = y, Width = 130, Height = 30 };
|
||||||
|
btnOpenGameConfig.Click += (s, e) => { using (var f = new ConfigEditorForm()) f.ShowDialog(this); };
|
||||||
|
page.Controls.Add(btnOpenGameConfig);
|
||||||
|
|
||||||
|
var btnOpenConfigDir = new Button { Text = "打开配置目录", Left = 160, Top = y, Width = 110 };
|
||||||
|
btnOpenConfigDir.Click += (s, e) =>
|
||||||
|
{
|
||||||
|
var dir = Program.ResolveFullPath(txtConfigDir.Text.Trim());
|
||||||
|
if (!string.IsNullOrEmpty(dir) && Directory.Exists(dir))
|
||||||
|
System.Diagnostics.Process.Start("explorer.exe", dir);
|
||||||
|
else
|
||||||
|
MessageBox.Show($"配置目录不存在:\n{dir}", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||||
|
};
|
||||||
|
page.Controls.Add(btnOpenConfigDir);
|
||||||
|
|
||||||
|
var btnResetDefaults = new Button { Text = "恢复默认", Left = 280, Top = y, Width = 80 };
|
||||||
|
btnResetDefaults.Click += (s, e) => ResetGameDefaults();
|
||||||
|
page.Controls.Add(btnResetDefaults);
|
||||||
|
|
||||||
|
tabMain.TabPages.Add(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextBox AddConfigFileRow(TabPage page, ref int y, string label, string defaultName)
|
||||||
|
{
|
||||||
|
page.Controls.Add(new Label { Text = label, Left = 30, Top = y, AutoSize = true });
|
||||||
|
var txt = new TextBox { Left = 180, Top = y - 3, Width = 200, Text = defaultName };
|
||||||
|
page.Controls.Add(txt);
|
||||||
|
y += 24;
|
||||||
|
return txt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>将绝对路径转为相对插件目录的相对路径;若转换失败或已是相对路径则原样返回。</summary>
|
||||||
|
static string ToRelativePath(string absPath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(absPath)) return absPath;
|
||||||
|
if (!Path.IsPathRooted(absPath)) return absPath; // 已是相对路径
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var baseDir = AppDomain.CurrentDomain.BaseDirectory;
|
||||||
|
var fullBase = Path.GetFullPath(baseDir.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
|
||||||
|
var fullAbs = Path.GetFullPath(absPath);
|
||||||
|
var baseUri = new Uri(fullBase + Path.DirectorySeparatorChar);
|
||||||
|
var absUri = new Uri(fullAbs);
|
||||||
|
var rel = baseUri.MakeRelativeUri(absUri).ToString();
|
||||||
|
// Uri 返回的是用 / 分隔的相对路径,统一为 Windows 分隔符
|
||||||
|
rel = Uri.UnescapeDataString(rel).Replace('/', Path.DirectorySeparatorChar);
|
||||||
|
return rel;
|
||||||
|
}
|
||||||
|
catch { return absPath; }
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResetGameDefaults()
|
||||||
|
{
|
||||||
|
txtGameExePath.Text = DefaultGameExe;
|
||||||
|
txtGameWorkDir.Text = DefaultGameDir;
|
||||||
|
txtConfigDir.Text = DefaultConfigDir;
|
||||||
|
txtEffectFile.Text = "effect_config.txt";
|
||||||
|
txtMacroFile.Text = "macro_config.txt";
|
||||||
|
txtGiftFile.Text = "gift_config.txt";
|
||||||
|
txtCmdFile.Text = "command_config.txt";
|
||||||
|
txtFkeyFile.Text = "fkey_config.txt";
|
||||||
|
cbAutoLaunchGame.Checked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════ 加载 ═══════════════
|
||||||
|
|
||||||
|
void LoadAll()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LoadAppConfig();
|
||||||
|
LoadGameSettings();
|
||||||
|
lblStatus.Text = "已加载";
|
||||||
|
lblStatus.ForeColor = Color.DimGray;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
lblStatus.Text = $"加载失败: {ex.Message}";
|
||||||
|
lblStatus.ForeColor = Color.Red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoadAppConfig()
|
||||||
|
{
|
||||||
|
if (!File.Exists(ConfigPath)) return;
|
||||||
|
var config = JObject.Parse(File.ReadAllText(ConfigPath), new JsonLoadSettings { CommentHandling = CommentHandling.Ignore });
|
||||||
|
var app = config["app"];
|
||||||
|
|
||||||
|
var general = app["general"];
|
||||||
|
cmbPlatform.SelectedItem = general["platform"]?.Value<string>() ?? "douyin";
|
||||||
|
cbHideConsole.Checked = general["hideConsole"]?.Value<bool>() ?? false;
|
||||||
|
cbShowWindow.Checked = general["showWindow"]?.Value<bool>() ?? false;
|
||||||
|
|
||||||
|
var proxy = app["network"]["proxy"];
|
||||||
|
nudProxyPort.Value = proxy["port"]?.Value<int>() ?? 8827;
|
||||||
|
cbProxyEnabled.Checked = proxy["enabled"]?.Value<bool>() ?? true;
|
||||||
|
txtUpstreamProxy.Text = proxy["upstreamAddress"]?.Value<string>() ?? "";
|
||||||
|
var ws = app["network"]["websocket"];
|
||||||
|
nudWsPort.Value = ws["listenPort"]?.Value<int>() ?? 8888;
|
||||||
|
cbListenAny.Checked = ws["listenAny"]?.Value<bool>() ?? true;
|
||||||
|
|
||||||
|
var barrage = app["barrage"];
|
||||||
|
cbPrintBarrage.Checked = barrage["printEnabled"]?.Value<bool>() ?? true;
|
||||||
|
cbBarrageLog.Checked = barrage["barrageFileLog"]?.Value<bool>() ?? false;
|
||||||
|
SetFilterChecks(clbPrintFilter, barrage["printFilter"]?.Value<string>() ?? "");
|
||||||
|
SetFilterChecks(clbPushFilter, barrage["pushFilter"]?.Value<string>() ?? "");
|
||||||
|
SetFilterChecks(clbLogFilter, barrage["logFilter"]?.Value<string>() ?? "1,2,4,5,6,7,8");
|
||||||
|
|
||||||
|
var polling = barrage["polling"];
|
||||||
|
cbForcePolling.Checked = polling["enabled"]?.Value<bool>() ?? false;
|
||||||
|
nudPollingInterval.Value = polling["interval"]?.Value<int>() ?? 3000;
|
||||||
|
|
||||||
|
var filtering = app["filtering"];
|
||||||
|
txtProcessFilter.Text = filtering["processFilter"]?.Value<string>() ?? "";
|
||||||
|
cbFilterHostName.Checked = filtering["hostNameEnabled"]?.Value<bool>() ?? true;
|
||||||
|
txtHostNameList.Text = filtering["hostNameList"]?.Value<string>() ?? "";
|
||||||
|
txtWebRoomIds.Text = filtering["webRoomIds"]?.Value<string>() ?? "";
|
||||||
|
|
||||||
|
var liveCompanion = app["liveCompanion"];
|
||||||
|
txtLiveCompanPath.Text = liveCompanion["path"]?.Value<string>() ?? "";
|
||||||
|
cbLiveCompanHook.Checked = liveCompanion["hookEnabled"]?.Value<bool>() ?? false;
|
||||||
|
cbAutoPause.Checked = liveCompanion["autoPause"]?.Value<bool>() ?? true;
|
||||||
|
|
||||||
|
var comPortStr = app["comPort"]["config"]?.Value<string>() ?? "";
|
||||||
|
if (!string.IsNullOrWhiteSpace(comPortStr))
|
||||||
|
{
|
||||||
|
var parts = comPortStr.Split(':');
|
||||||
|
if (parts.Length >= 2) { txtComPort.Text = parts[0].Trim(); txtComBaud.Text = parts[1].Trim(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void LoadGameSettings()
|
||||||
|
{
|
||||||
|
// 默认值
|
||||||
|
txtGameExePath.Text = DefaultGameExe;
|
||||||
|
txtGameWorkDir.Text = DefaultGameDir;
|
||||||
|
cbAutoLaunchGame.Checked = true;
|
||||||
|
txtConfigDir.Text = DefaultConfigDir;
|
||||||
|
txtEffectFile.Text = "effect_config.txt";
|
||||||
|
txtMacroFile.Text = "macro_config.txt";
|
||||||
|
txtGiftFile.Text = "gift_config.txt";
|
||||||
|
txtCmdFile.Text = "command_config.txt";
|
||||||
|
txtFkeyFile.Text = "fkey_config.txt";
|
||||||
|
|
||||||
|
if (File.Exists(GameSettingsPath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = JObject.Parse(File.ReadAllText(GameSettingsPath));
|
||||||
|
txtGameExePath.Text = json["gameExePath"]?.Value<string>() ?? txtGameExePath.Text;
|
||||||
|
txtGameWorkDir.Text = json["gameWorkDir"]?.Value<string>() ?? txtGameWorkDir.Text;
|
||||||
|
cbAutoLaunchGame.Checked = json["autoLaunchGame"]?.Value<bool>() ?? true;
|
||||||
|
txtConfigDir.Text = json["configDir"]?.Value<string>() ?? txtConfigDir.Text;
|
||||||
|
|
||||||
|
var cfg = json["configFiles"];
|
||||||
|
if (cfg != null)
|
||||||
|
{
|
||||||
|
txtEffectFile.Text = cfg["effect"]?.Value<string>() ?? txtEffectFile.Text;
|
||||||
|
txtMacroFile.Text = cfg["macro"]?.Value<string>() ?? txtMacroFile.Text;
|
||||||
|
txtGiftFile.Text = cfg["gift"]?.Value<string>() ?? txtGiftFile.Text;
|
||||||
|
txtCmdFile.Text = cfg["command"]?.Value<string>() ?? txtCmdFile.Text;
|
||||||
|
txtFkeyFile.Text = cfg["fkey"]?.Value<string>() ?? txtFkeyFile.Text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
Program.AutoLaunchGame = cbAutoLaunchGame.Checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetFilterChecks(CheckedListBox clb, string filterStr)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < clb.Items.Count; i++) clb.SetItemChecked(i, false);
|
||||||
|
if (string.IsNullOrWhiteSpace(filterStr)) return;
|
||||||
|
var ids = filterStr.Split(',').Select(s => s.Trim()).Where(s => !string.IsNullOrEmpty(s)).ToList();
|
||||||
|
for (int i = 0; i < clb.Items.Count; i++)
|
||||||
|
{
|
||||||
|
if (ids.Contains(MsgTypeOptions[i].Item1.ToString()))
|
||||||
|
clb.SetItemChecked(i, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string GetFilterString(CheckedListBox clb)
|
||||||
|
{
|
||||||
|
var ids = new List<string>();
|
||||||
|
for (int i = 0; i < clb.Items.Count; i++)
|
||||||
|
if (clb.GetItemChecked(i)) ids.Add(MsgTypeOptions[i].Item1.ToString());
|
||||||
|
return string.Join(",", ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════ 保存 ═══════════════
|
||||||
|
|
||||||
|
void SaveAll()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SaveAppConfig();
|
||||||
|
SaveGameSettings();
|
||||||
|
AppSetting.Current.LoadFromJson(ConfigPath);
|
||||||
|
lblStatus.Text = "已保存(部分设置需重启生效)";
|
||||||
|
lblStatus.ForeColor = Color.Green;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
lblStatus.Text = $"保存失败: {ex.Message}";
|
||||||
|
lblStatus.ForeColor = Color.Red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SaveAppConfig()
|
||||||
|
{
|
||||||
|
if (!File.Exists(ConfigPath))
|
||||||
|
{
|
||||||
|
var json = new JObject(new JProperty("app", new JObject(
|
||||||
|
new JProperty("general", new JObject(
|
||||||
|
new JProperty("hideConsole", cbHideConsole.Checked),
|
||||||
|
new JProperty("showWindow", cbShowWindow.Checked),
|
||||||
|
new JProperty("platform", cmbPlatform.SelectedItem?.ToString() ?? "douyin"))),
|
||||||
|
new JProperty("network", new JObject(
|
||||||
|
new JProperty("proxy", new JObject(
|
||||||
|
new JProperty("port", (int)nudProxyPort.Value),
|
||||||
|
new JProperty("enabled", cbProxyEnabled.Checked),
|
||||||
|
new JProperty("upstreamAddress", txtUpstreamProxy.Text.Trim()))),
|
||||||
|
new JProperty("websocket", new JObject(
|
||||||
|
new JProperty("listenPort", (int)nudWsPort.Value),
|
||||||
|
new JProperty("listenAny", cbListenAny.Checked))))),
|
||||||
|
new JProperty("filtering", new JObject(
|
||||||
|
new JProperty("processFilter", txtProcessFilter.Text.Trim()),
|
||||||
|
new JProperty("hostNameEnabled", cbFilterHostName.Checked),
|
||||||
|
new JProperty("hostNameList", txtHostNameList.Text.Trim()),
|
||||||
|
new JProperty("webRoomIds", txtWebRoomIds.Text.Trim()))),
|
||||||
|
new JProperty("barrage", new JObject(
|
||||||
|
new JProperty("printEnabled", cbPrintBarrage.Checked),
|
||||||
|
new JProperty("printFilter", GetFilterString(clbPrintFilter)),
|
||||||
|
new JProperty("pushFilter", GetFilterString(clbPushFilter)),
|
||||||
|
new JProperty("barrageFileLog", cbBarrageLog.Checked),
|
||||||
|
new JProperty("logFilter", GetFilterString(clbLogFilter)),
|
||||||
|
new JProperty("polling", new JObject(
|
||||||
|
new JProperty("enabled", cbForcePolling.Checked),
|
||||||
|
new JProperty("interval", (int)nudPollingInterval.Value))))),
|
||||||
|
new JProperty("liveCompanion", new JObject(
|
||||||
|
new JProperty("path", txtLiveCompanPath.Text.Trim()),
|
||||||
|
new JProperty("hookEnabled", cbLiveCompanHook.Checked),
|
||||||
|
new JProperty("autoPause", cbAutoPause.Checked))),
|
||||||
|
new JProperty("comPort", new JObject(
|
||||||
|
new JProperty("config", BuildComPortConfig()))))));
|
||||||
|
File.WriteAllText(ConfigPath, json.ToString(), Encoding.UTF8);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = JObject.Parse(File.ReadAllText(ConfigPath), new JsonLoadSettings { CommentHandling = CommentHandling.Ignore });
|
||||||
|
var app = config["app"];
|
||||||
|
app["general"]["platform"] = cmbPlatform.SelectedItem?.ToString() ?? "douyin";
|
||||||
|
app["general"]["hideConsole"] = cbHideConsole.Checked;
|
||||||
|
app["general"]["showWindow"] = cbShowWindow.Checked;
|
||||||
|
app["network"]["proxy"]["port"] = (int)nudProxyPort.Value;
|
||||||
|
app["network"]["proxy"]["enabled"] = cbProxyEnabled.Checked;
|
||||||
|
app["network"]["proxy"]["upstreamAddress"] = txtUpstreamProxy.Text.Trim();
|
||||||
|
app["network"]["websocket"]["listenPort"] = (int)nudWsPort.Value;
|
||||||
|
app["network"]["websocket"]["listenAny"] = cbListenAny.Checked;
|
||||||
|
app["filtering"]["processFilter"] = txtProcessFilter.Text.Trim();
|
||||||
|
app["filtering"]["hostNameEnabled"] = cbFilterHostName.Checked;
|
||||||
|
app["filtering"]["hostNameList"] = txtHostNameList.Text.Trim();
|
||||||
|
app["filtering"]["webRoomIds"] = txtWebRoomIds.Text.Trim();
|
||||||
|
app["barrage"]["printEnabled"] = cbPrintBarrage.Checked;
|
||||||
|
app["barrage"]["printFilter"] = GetFilterString(clbPrintFilter);
|
||||||
|
app["barrage"]["pushFilter"] = GetFilterString(clbPushFilter);
|
||||||
|
app["barrage"]["barrageFileLog"] = cbBarrageLog.Checked;
|
||||||
|
app["barrage"]["logFilter"] = GetFilterString(clbLogFilter);
|
||||||
|
app["barrage"]["polling"]["enabled"] = cbForcePolling.Checked;
|
||||||
|
app["barrage"]["polling"]["interval"] = (int)nudPollingInterval.Value;
|
||||||
|
app["liveCompanion"]["path"] = txtLiveCompanPath.Text.Trim();
|
||||||
|
app["liveCompanion"]["hookEnabled"] = cbLiveCompanHook.Checked;
|
||||||
|
app["liveCompanion"]["autoPause"] = cbAutoPause.Checked;
|
||||||
|
app["comPort"]["config"] = BuildComPortConfig();
|
||||||
|
File.WriteAllText(ConfigPath, config.ToString(), Encoding.UTF8);
|
||||||
|
}
|
||||||
|
|
||||||
|
string BuildComPortConfig()
|
||||||
|
{
|
||||||
|
var port = txtComPort.Text.Trim();
|
||||||
|
var baud = txtComBaud.Text.Trim();
|
||||||
|
if (string.IsNullOrEmpty(port)) return "";
|
||||||
|
if (string.IsNullOrEmpty(baud)) baud = "9600";
|
||||||
|
return $"{port}:{baud}";
|
||||||
|
}
|
||||||
|
|
||||||
|
void SaveGameSettings()
|
||||||
|
{
|
||||||
|
var json = new JObject(
|
||||||
|
new JProperty("gameExePath", txtGameExePath.Text.Trim()),
|
||||||
|
new JProperty("gameWorkDir", txtGameWorkDir.Text.Trim()),
|
||||||
|
new JProperty("autoLaunchGame", cbAutoLaunchGame.Checked),
|
||||||
|
new JProperty("configDir", txtConfigDir.Text.Trim()),
|
||||||
|
new JProperty("configFiles", new JObject(
|
||||||
|
new JProperty("effect", txtEffectFile.Text.Trim()),
|
||||||
|
new JProperty("macro", txtMacroFile.Text.Trim()),
|
||||||
|
new JProperty("gift", txtGiftFile.Text.Trim()),
|
||||||
|
new JProperty("command", txtCmdFile.Text.Trim()),
|
||||||
|
new JProperty("fkey", txtFkeyFile.Text.Trim()))));
|
||||||
|
File.WriteAllText(GameSettingsPath, json.ToString(), Encoding.UTF8);
|
||||||
|
|
||||||
|
// 同步到 Program
|
||||||
|
Program.AutoLaunchGame = cbAutoLaunchGame.Checked;
|
||||||
|
Program.SetGamePaths(txtGameExePath.Text.Trim(), txtGameWorkDir.Text.Trim());
|
||||||
|
Program.SetConfigPaths(txtConfigDir.Text.Trim(),
|
||||||
|
txtEffectFile.Text.Trim(), txtMacroFile.Text.Trim(),
|
||||||
|
txtGiftFile.Text.Trim(), txtCmdFile.Text.Trim(),
|
||||||
|
txtFkeyFile.Text.Trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace BarrageGrab.Modles.BiliProtoEntity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// B站直播弹幕WS协议操作码
|
||||||
|
/// </summary>
|
||||||
|
public enum BiliOperation
|
||||||
|
{
|
||||||
|
/// <summary>客户端心跳</summary>
|
||||||
|
HEARTBEAT = 2,
|
||||||
|
/// <summary>服务器心跳回复 (人气值)</summary>
|
||||||
|
HEARTBEAT_REPLY = 3,
|
||||||
|
/// <summary>普通消息 (弹幕/礼物/进房等)</summary>
|
||||||
|
MESSAGE = 5,
|
||||||
|
/// <summary>客户端认证请求</summary>
|
||||||
|
AUTH = 7,
|
||||||
|
/// <summary>服务器认证回复</summary>
|
||||||
|
AUTH_REPLY = 8
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// B站WS协议版本 (影响Payload压缩方式)
|
||||||
|
/// </summary>
|
||||||
|
public enum BiliProtocolVersion
|
||||||
|
{
|
||||||
|
/// <summary>JSON明文</summary>
|
||||||
|
PLAIN = 0,
|
||||||
|
/// <summary>认证用JSON</summary>
|
||||||
|
PLAIN_AUTH = 1,
|
||||||
|
/// <summary>Zlib压缩 (内部可能含多个包)</summary>
|
||||||
|
ZLIB = 2,
|
||||||
|
/// <summary>Brotli压缩 (内部可能含多个包)</summary>
|
||||||
|
BROTLI = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// B站WS协议帧头 (16字节, Big-Endian)
|
||||||
|
/// </summary>
|
||||||
|
public struct BiliSocketFrame
|
||||||
|
{
|
||||||
|
/// <summary>整个包长度 (header+body)</summary>
|
||||||
|
public uint PacketLength;
|
||||||
|
/// <summary>头长度 (固定16)</summary>
|
||||||
|
public ushort HeaderLength;
|
||||||
|
/// <summary>协议版本</summary>
|
||||||
|
public BiliProtocolVersion ProtocolVersion;
|
||||||
|
/// <summary>操作码</summary>
|
||||||
|
public BiliOperation Operation;
|
||||||
|
/// <summary>序列号 (固定1)</summary>
|
||||||
|
public uint SequenceId;
|
||||||
|
|
||||||
|
public const int HEADER_SIZE = 16;
|
||||||
|
|
||||||
|
/// <summary>从字节缓冲区解析帧头</summary>
|
||||||
|
public static bool TryParse(byte[] buffer, int offset, int count, out BiliSocketFrame frame)
|
||||||
|
{
|
||||||
|
frame = default(BiliSocketFrame);
|
||||||
|
if (buffer == null || count < HEADER_SIZE) return false;
|
||||||
|
// Big-Endian 解析
|
||||||
|
frame.PacketLength = ReadUInt32BE(buffer, offset);
|
||||||
|
frame.HeaderLength = ReadUInt16BE(buffer, offset + 4);
|
||||||
|
frame.ProtocolVersion = (BiliProtocolVersion)ReadUInt16BE(buffer, offset + 6);
|
||||||
|
frame.Operation = (BiliOperation)ReadUInt32BE(buffer, offset + 8);
|
||||||
|
frame.SequenceId = ReadUInt32BE(buffer, offset + 12);
|
||||||
|
return frame.HeaderLength >= HEADER_SIZE && frame.PacketLength >= HEADER_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>帧体数据 (从 HeaderLength 开始到 PacketLength)</summary>
|
||||||
|
public static byte[] GetPayload(byte[] buffer, int offset, BiliSocketFrame frame)
|
||||||
|
{
|
||||||
|
int bodyLen = (int)frame.PacketLength - frame.HeaderLength;
|
||||||
|
if (bodyLen <= 0) return new byte[0];
|
||||||
|
var data = new byte[bodyLen];
|
||||||
|
System.Array.Copy(buffer, offset + frame.HeaderLength, data, 0, bodyLen);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static uint ReadUInt32BE(byte[] buf, int off)
|
||||||
|
{
|
||||||
|
return (uint)((buf[off] << 24) | (buf[off + 1] << 16) | (buf[off + 2] << 8) | buf[off + 3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ushort ReadUInt16BE(byte[] buf, int off)
|
||||||
|
{
|
||||||
|
return (ushort)((buf[off] << 8) | buf[off + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// B站弹幕消息 (cmd字段用于路由)
|
||||||
|
/// </summary>
|
||||||
|
public class BiliMessage
|
||||||
|
{
|
||||||
|
/// <summary>消息类型 (DANMU_MSG / SEND_GIFT / INTERACT_WORD 等)</summary>
|
||||||
|
public string Cmd { get; set; }
|
||||||
|
|
||||||
|
/// <summary>原始数据</summary>
|
||||||
|
public JObject Raw { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// B站弹幕消息 cmd 列表 (用于识别已知类型)
|
||||||
|
/// </summary>
|
||||||
|
public static class BiliCmd
|
||||||
|
{
|
||||||
|
public const string DANMU_MSG = "DANMU_MSG";
|
||||||
|
public const string SEND_GIFT = "SEND_GIFT";
|
||||||
|
public const string INTERACT_WORD = "INTERACT_WORD";
|
||||||
|
public const string LIKE_INFO_V3_CLICK = "LIKE_INFO_V3_CLICK";
|
||||||
|
public const string ONLINE_RANK_COUNT = "ONLINE_RANK_COUNT";
|
||||||
|
public const string WATCHED_CHANGE = "WATCHED_CHANGE";
|
||||||
|
public const string LIVE = "LIVE";
|
||||||
|
public const string PREPARING = "PREPARING";
|
||||||
|
public const string ROOM_REAL_TIME_MESSAGE_UPDATE = "ROOM_REAL_TIME_MESSAGE_UPDATE";
|
||||||
|
public const string SUPER_CHAT_MESSAGE = "SUPER_CHAT_MESSAGE";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// B站弹幕消息(DANMU_MSG)简化结构
|
||||||
|
/// </summary>
|
||||||
|
public class BiliDanmuMsg
|
||||||
|
{
|
||||||
|
/// <summary>弹幕内容 (info[1])</summary>
|
||||||
|
public string Content { get; set; }
|
||||||
|
/// <summary>用户UID (info[2][0])</summary>
|
||||||
|
public long Uid { get; set; }
|
||||||
|
/// <summary>用户昵称 (info[2][1])</summary>
|
||||||
|
public string Uname { get; set; }
|
||||||
|
/// <summary>管理员? (info[2][2]==1)</summary>
|
||||||
|
public bool IsAdmin { get; set; }
|
||||||
|
/// <summary>主播? (info[2][4]存在)</summary>
|
||||||
|
public bool IsAnchor { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// B站礼物消息(SEND_GIFT.data)简化结构
|
||||||
|
/// </summary>
|
||||||
|
public class BiliGiftData
|
||||||
|
{
|
||||||
|
public long Uid { get; set; }
|
||||||
|
public string Uname { get; set; }
|
||||||
|
public string Face { get; set; }
|
||||||
|
public uint GiftId { get; set; }
|
||||||
|
public string GiftName { get; set; }
|
||||||
|
/// <summary>礼物数量</summary>
|
||||||
|
public uint Num { get; set; }
|
||||||
|
/// <summary>本批次礼物数 (连击时为本次增量)</summary>
|
||||||
|
public uint ComboNum { get; set; }
|
||||||
|
/// <summary>礼物连击总数</summary>
|
||||||
|
public uint SuperBatchNum { get; set; }
|
||||||
|
/// <summary>价格 (1 = 1000金瓜子)</summary>
|
||||||
|
public long Price { get; set; }
|
||||||
|
/// <summary>金币类型 gold=付费 silver=免费</summary>
|
||||||
|
public string CoinType { get; set; }
|
||||||
|
/// <summary>礼物图片</summary>
|
||||||
|
public string GifUrl { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// B站进房/关注消息(INTERACT_WORD.data)简化结构
|
||||||
|
/// </summary>
|
||||||
|
public class BiliInteractData
|
||||||
|
{
|
||||||
|
public long Uid { get; set; }
|
||||||
|
public string Uname { get; set; }
|
||||||
|
/// <summary>1=进入 2=关注 3=分享</summary>
|
||||||
|
public int MsgType { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// B站点赞消息(LIKE_INFO_V3_CLICK.data)简化结构
|
||||||
|
/// </summary>
|
||||||
|
public class BiliLikeData
|
||||||
|
{
|
||||||
|
public long Uid { get; set; }
|
||||||
|
public string Uname { get; set; }
|
||||||
|
public string LikeText { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// B站房间统计(ONLINE_RANK_COUNT.data / ROOM_REAL_TIME_MESSAGE_UPDATE.data)简化结构
|
||||||
|
/// </summary>
|
||||||
|
public class BiliRoomStats
|
||||||
|
{
|
||||||
|
/// <summary>在线人数</summary>
|
||||||
|
public long Online { get; set; }
|
||||||
|
/// <summary>看过人数</summary>
|
||||||
|
public long Watched { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
@@ -6,6 +6,25 @@ using Org.BouncyCastle.Asn1.Crmf;
|
|||||||
|
|
||||||
namespace BarrageGrab.Modles.JsonEntity
|
namespace BarrageGrab.Modles.JsonEntity
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 直播平台
|
||||||
|
/// </summary>
|
||||||
|
public enum LivePlatform
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 抖音
|
||||||
|
/// </summary>
|
||||||
|
Douyin = 0,
|
||||||
|
/// <summary>
|
||||||
|
/// 快手
|
||||||
|
/// </summary>
|
||||||
|
Kuaishou = 1,
|
||||||
|
/// <summary>
|
||||||
|
/// 哔哩哔哩
|
||||||
|
/// </summary>
|
||||||
|
Bilibili = 2
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 弹幕消息类型
|
/// 弹幕消息类型
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -96,6 +115,11 @@ namespace BarrageGrab.Modles.JsonEntity
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string ProcessName { get; set; }
|
public string ProcessName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 直播平台
|
||||||
|
/// </summary>
|
||||||
|
public LivePlatform Platform { get; set; } = LivePlatform.Douyin;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 消息对象
|
/// 消息对象
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -106,11 +130,12 @@ namespace BarrageGrab.Modles.JsonEntity
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public BarrageMsgPack(string data, PackMsgType type, string processName)
|
public BarrageMsgPack(string data, PackMsgType type, string processName, LivePlatform platform = LivePlatform.Douyin)
|
||||||
{
|
{
|
||||||
Data = data;
|
Data = data;
|
||||||
Type = type;
|
Type = type;
|
||||||
ProcessName = processName;
|
ProcessName = processName;
|
||||||
|
Platform = platform;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -0,0 +1,358 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using ProtoBuf;
|
||||||
|
|
||||||
|
namespace BarrageGrab.Modles.KsProtoEntity
|
||||||
|
{
|
||||||
|
[ProtoContract]
|
||||||
|
public class KsSocketMessage
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public KsPayloadType PayloadType { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public KsCompressionType CompressionType { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public byte[] Payload { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum KsCompressionType
|
||||||
|
{
|
||||||
|
UNKNOWN = 0,
|
||||||
|
NONE = 1,
|
||||||
|
GZIP = 2,
|
||||||
|
AES = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum KsPayloadType
|
||||||
|
{
|
||||||
|
UNKNOWN = 0,
|
||||||
|
CS_HEARTBEAT = 1,
|
||||||
|
CS_ERROR = 3,
|
||||||
|
CS_PING = 4,
|
||||||
|
PS_HOST_INFO = 51,
|
||||||
|
SC_HEARTBEAT_ACK = 101,
|
||||||
|
SC_ECHO = 102,
|
||||||
|
SC_ERROR = 103,
|
||||||
|
SC_PING_ACK = 104,
|
||||||
|
SC_INFO = 105,
|
||||||
|
CS_ENTER_ROOM = 200,
|
||||||
|
CS_USER_PAUSE = 201,
|
||||||
|
CS_USER_EXIT = 202,
|
||||||
|
CS_AUTHOR_PUSH_TRAFFIC_ZERO = 203,
|
||||||
|
CS_HORSE_RACING = 204,
|
||||||
|
CS_RACE_LOSE = 205,
|
||||||
|
CS_VOIP_SIGNAL = 206,
|
||||||
|
SC_ENTER_ROOM_ACK = 300,
|
||||||
|
SC_AUTHOR_PAUSE = 301,
|
||||||
|
SC_AUTHOR_RESUME = 302,
|
||||||
|
SC_AUTHOR_PUSH_TRAFFIC_ZERO = 303,
|
||||||
|
SC_AUTHOR_HEARTBEAT_MISS = 304,
|
||||||
|
SC_PIP_STARTED = 305,
|
||||||
|
SC_PIP_ENDED = 306,
|
||||||
|
SC_HORSE_RACING_ACK = 307,
|
||||||
|
SC_VOIP_SIGNAL = 308,
|
||||||
|
SC_FEED_PUSH = 310,
|
||||||
|
SC_ASSISTANT_STATUS = 311,
|
||||||
|
SC_REFRESH_WALLET = 312,
|
||||||
|
SC_LIVE_CHAT_CALL = 320,
|
||||||
|
SC_LIVE_CHAT_CALL_ACCEPTED = 321,
|
||||||
|
SC_LIVE_CHAT_CALL_REJECTED = 322,
|
||||||
|
SC_LIVE_CHAT_READY = 323,
|
||||||
|
SC_LIVE_CHAT_GUEST_END = 324,
|
||||||
|
SC_LIVE_CHAT_ENDED = 325,
|
||||||
|
SC_RENDERING_MAGIC_FACE_DISABLE = 326,
|
||||||
|
SC_RENDERING_MAGIC_FACE_ENABLE = 327,
|
||||||
|
SC_RED_PACK_FEED = 330,
|
||||||
|
SC_LIVE_WATCHING_LIST = 340,
|
||||||
|
SC_LIVE_QUIZ_QUESTION_ASKED = 350,
|
||||||
|
SC_LIVE_QUIZ_QUESTION_REVIEWED = 351,
|
||||||
|
SC_LIVE_QUIZ_SYNC = 352,
|
||||||
|
SC_LIVE_QUIZ_ENDED = 353,
|
||||||
|
SC_LIVE_QUIZ_WINNERS = 354,
|
||||||
|
SC_SUSPECTED_VIOLATION = 355,
|
||||||
|
SC_SHOP_OPENED = 360,
|
||||||
|
SC_SHOP_CLOSED = 361,
|
||||||
|
SC_GUESS_OPENED = 370,
|
||||||
|
SC_GUESS_CLOSED = 371,
|
||||||
|
SC_PK_INVITATION = 380,
|
||||||
|
SC_PK_STATISTIC = 381,
|
||||||
|
SC_RIDDLE_OPENED = 390,
|
||||||
|
SC_RIDDLE_CLOESED = 391,
|
||||||
|
SC_RIDE_CHANGED = 412,
|
||||||
|
SC_BET_CHANGED = 441,
|
||||||
|
SC_BET_CLOSED = 442,
|
||||||
|
SC_LIVE_SPECIAL_ACCOUNT_CONFIG_STATE = 645
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoContract]
|
||||||
|
public class KsSCWebFeedPush
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public string DisplayWatchingCount { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public string DisplayLikeCount { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public ulong PendingLikeCount { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(4)]
|
||||||
|
public ulong PushInterval { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(5)]
|
||||||
|
public List<KsWebCommentFeed> CommentFeeds { get; } = new List<KsWebCommentFeed>();
|
||||||
|
|
||||||
|
[ProtoMember(6)]
|
||||||
|
public string CommentCursor { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(7)]
|
||||||
|
public List<KsWebComboCommentFeed> ComboCommentFeed { get; } = new List<KsWebComboCommentFeed>();
|
||||||
|
|
||||||
|
[ProtoMember(8)]
|
||||||
|
public List<KsWebLikeFeed> LikeFeeds { get; } = new List<KsWebLikeFeed>();
|
||||||
|
|
||||||
|
[ProtoMember(9)]
|
||||||
|
public List<KsWebGiftFeed> GiftFeeds { get; } = new List<KsWebGiftFeed>();
|
||||||
|
|
||||||
|
[ProtoMember(10)]
|
||||||
|
public string GiftCursor { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(11)]
|
||||||
|
public List<KsWebSystemNoticeFeed> SystemNoticeFeeds { get; } = new List<KsWebSystemNoticeFeed>();
|
||||||
|
|
||||||
|
[ProtoMember(12)]
|
||||||
|
public List<KsWebShareFeed> ShareFeeds { get; } = new List<KsWebShareFeed>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoContract]
|
||||||
|
public class KsWebCommentFeed
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public KsSimpleUserInfo User { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public string Content { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(4)]
|
||||||
|
public string DeviceHash { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(5)]
|
||||||
|
public ulong SortRank { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(6)]
|
||||||
|
public string Color { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(7)]
|
||||||
|
public KsWebCommentFeedShowType ShowType { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum KsWebCommentFeedShowType
|
||||||
|
{
|
||||||
|
FEED_SHOW_UNKNOWN = 0,
|
||||||
|
FEED_SHOW_NORMAL = 1,
|
||||||
|
FEED_HIDDEN = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoContract]
|
||||||
|
public class KsWebComboCommentFeed
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public string Content { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public uint ComboCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoContract]
|
||||||
|
public class KsWebLikeFeed
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public KsSimpleUserInfo User { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public ulong SortRank { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(4)]
|
||||||
|
public string DeviceHash { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoContract]
|
||||||
|
public class KsWebGiftFeed
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public KsSimpleUserInfo User { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public ulong Time { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(4)]
|
||||||
|
public uint GiftId { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(5)]
|
||||||
|
public ulong SortRank { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(6)]
|
||||||
|
public string MergeKey { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(7)]
|
||||||
|
public uint BatchSize { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(8)]
|
||||||
|
public uint ComboCount { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(9)]
|
||||||
|
public uint Rank { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(10)]
|
||||||
|
public ulong ExpireDuration { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(11)]
|
||||||
|
public ulong ClientTimestamp { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(12)]
|
||||||
|
public ulong SlotDisplayDuration { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(13)]
|
||||||
|
public uint StarLevel { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(14)]
|
||||||
|
public KsGiftStyleType StyleType { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(15)]
|
||||||
|
public KsWebLiveAssistantType LiveAssistantType { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(16)]
|
||||||
|
public string DeviceHash { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(17)]
|
||||||
|
public bool DanmakuDisplay { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum KsGiftStyleType
|
||||||
|
{
|
||||||
|
UNKNOWN_STYLE = 0,
|
||||||
|
BATCH_STAR_0 = 1,
|
||||||
|
BATCH_STAR_1 = 2,
|
||||||
|
BATCH_STAR_2 = 3,
|
||||||
|
BATCH_STAR_3 = 4,
|
||||||
|
BATCH_STAR_4 = 5,
|
||||||
|
BATCH_STAR_5 = 6,
|
||||||
|
BATCH_STAR_6 = 7
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum KsWebLiveAssistantType
|
||||||
|
{
|
||||||
|
UNKNOWN_ASSISTANT_TYPE = 0,
|
||||||
|
SUPER = 1,
|
||||||
|
JUNIOR = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoContract]
|
||||||
|
public class KsWebSystemNoticeFeed
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public KsSimpleUserInfo User { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public ulong Time { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(4)]
|
||||||
|
public string Content { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(5)]
|
||||||
|
public ulong DisplayDuration { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(6)]
|
||||||
|
public ulong SortRank { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(7)]
|
||||||
|
public KsDisplayType DisplayType { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum KsDisplayType
|
||||||
|
{
|
||||||
|
UNKNOWN_DISPLAY_TYPE = 0,
|
||||||
|
COMMENT = 1,
|
||||||
|
ALERT = 2,
|
||||||
|
TOAST = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoContract]
|
||||||
|
public class KsWebShareFeed
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public KsSimpleUserInfo User { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public ulong Time { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(4)]
|
||||||
|
public uint ThirdPartyPlatform { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(5)]
|
||||||
|
public ulong SortRank { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(6)]
|
||||||
|
public KsWebLiveAssistantType LiveAssistantType { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(7)]
|
||||||
|
public string DeviceHash { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoContract]
|
||||||
|
public class KsSimpleUserInfo
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public string PrincipalId { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public string UserName { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public string HeadUrl { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[ProtoContract]
|
||||||
|
public class KsCSWebEnterRoom
|
||||||
|
{
|
||||||
|
[ProtoMember(1)]
|
||||||
|
public string Token { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(2)]
|
||||||
|
public string LiveStreamId { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(3)]
|
||||||
|
public uint ReconnectCount { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(4)]
|
||||||
|
public uint LastErrorCode { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(5)]
|
||||||
|
public string ExpTag { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(6)]
|
||||||
|
public string Attach { get; set; }
|
||||||
|
|
||||||
|
[ProtoMember(7)]
|
||||||
|
public string PageId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
+245
-94
@@ -1,14 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
|
||||||
using System.Net.Sockets;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
|
using BarrageGrab.Modles.JsonEntity;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
using Jint.Runtime;
|
using Jint.Runtime;
|
||||||
|
|
||||||
namespace BarrageGrab
|
namespace BarrageGrab
|
||||||
@@ -21,16 +19,225 @@ namespace BarrageGrab
|
|||||||
static WinApi.ControlCtrlDelegate controlCtr = ControlCtrlHandle;
|
static WinApi.ControlCtrlDelegate controlCtr = ControlCtrlHandle;
|
||||||
static Mutex mutex = new Mutex(false, "DyBarrageGrab");
|
static Mutex mutex = new Mutex(false, "DyBarrageGrab");
|
||||||
|
|
||||||
|
// 插件所在目录(用于解析相对路径)
|
||||||
|
public static string BaseDir => AppDomain.CurrentDomain.BaseDirectory;
|
||||||
|
|
||||||
|
// === 启动项 (支持相对路径,相对于插件所在目录) ===
|
||||||
|
public static bool AutoLaunchGame = true;
|
||||||
|
public static string GameExePath = @"..\Fantasy.Map.Simulator\FantasyMapSimulator.exe";
|
||||||
|
public static string GameWorkDir = @"..\Fantasy.Map.Simulator";
|
||||||
|
|
||||||
|
// === 配置表路径 (支持相对路径) ===
|
||||||
|
public static string ConfigDir = @"..\Fantasy.Map.Simulator\BepInEx\config";
|
||||||
|
public static string EffectFile = "effect_config.txt";
|
||||||
|
public static string MacroFile = "macro_config.txt";
|
||||||
|
public static string GiftFile = "gift_config.txt";
|
||||||
|
public static string CommandFile = "command_config.txt";
|
||||||
|
public static string FkeyFile = "fkey_config.txt";
|
||||||
|
|
||||||
|
/// <summary>将相对路径解析为绝对路径(基于插件所在目录)。已是绝对路径则原样返回。</summary>
|
||||||
|
public static string ResolveFullPath(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path)) return path;
|
||||||
|
return Path.IsPathRooted(path) ? path : Path.GetFullPath(Path.Combine(BaseDir, path));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析后的绝对路径(只读)
|
||||||
|
public static string GameExeFullPath => ResolveFullPath(GameExePath);
|
||||||
|
public static string GameWorkFullPath => ResolveFullPath(GameWorkDir);
|
||||||
|
public static string ConfigDirFullPath => ResolveFullPath(ConfigDir);
|
||||||
|
|
||||||
|
// 配置表完整路径(只读,基于 ConfigDirFullPath)
|
||||||
|
public static string EffectPath => Path.Combine(ConfigDirFullPath, EffectFile);
|
||||||
|
public static string MacroPath => Path.Combine(ConfigDirFullPath, MacroFile);
|
||||||
|
public static string GiftPath => Path.Combine(ConfigDirFullPath, GiftFile);
|
||||||
|
public static string CommandPath => Path.Combine(ConfigDirFullPath, CommandFile);
|
||||||
|
public static string FkeyPath => Path.Combine(ConfigDirFullPath, FkeyFile);
|
||||||
|
|
||||||
|
public static void SetGamePaths(string exePath, string workDir)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(exePath)) GameExePath = exePath;
|
||||||
|
if (!string.IsNullOrWhiteSpace(workDir)) GameWorkDir = workDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SetConfigPaths(string configDir,
|
||||||
|
string effectFile, string macroFile, string giftFile, string cmdFile, string fkeyFile)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(configDir)) ConfigDir = configDir;
|
||||||
|
if (!string.IsNullOrWhiteSpace(effectFile)) EffectFile = effectFile;
|
||||||
|
if (!string.IsNullOrWhiteSpace(macroFile)) MacroFile = macroFile;
|
||||||
|
if (!string.IsNullOrWhiteSpace(giftFile)) GiftFile = giftFile;
|
||||||
|
if (!string.IsNullOrWhiteSpace(cmdFile)) CommandFile = cmdFile;
|
||||||
|
if (!string.IsNullOrWhiteSpace(fkeyFile)) FkeyFile = fkeyFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>从 GameExeFullPath 提取进程名(不带扩展名)</summary>
|
||||||
|
static string GameProcessName
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
try { return Path.GetFileNameWithoutExtension(GameExeFullPath); }
|
||||||
|
catch { return "FantasyMapSimulator"; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>关闭所有同名游戏进程,返回是否已无残留</summary>
|
||||||
|
static bool CloseGameProcesses()
|
||||||
|
{
|
||||||
|
var procName = GameProcessName;
|
||||||
|
var procs = Process.GetProcessesByName(procName);
|
||||||
|
if (procs.Length == 0)
|
||||||
|
{
|
||||||
|
Logger.PrintColor("未发现运行中的游戏进程 (" + procName + ")", ConsoleColor.DarkGray);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.PrintColor("发现 " + procs.Length + " 个游戏进程 (" + procName + "),正在关闭...", ConsoleColor.Yellow);
|
||||||
|
int killed = 0, failed = 0;
|
||||||
|
foreach (var p in procs)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!p.HasExited)
|
||||||
|
{
|
||||||
|
try { p.CloseMainWindow(); } catch { }
|
||||||
|
if (!p.WaitForExit(2000)) { p.Kill(); p.WaitForExit(3000); }
|
||||||
|
}
|
||||||
|
killed++;
|
||||||
|
}
|
||||||
|
catch { failed++; }
|
||||||
|
finally { try { p.Close(); } catch { } }
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.Sleep(500);
|
||||||
|
var remain = Process.GetProcessesByName(procName);
|
||||||
|
if (remain.Length > 0)
|
||||||
|
{
|
||||||
|
Logger.PrintColor("仍有 " + remain.Length + " 个进程未退出,强制结束...", ConsoleColor.Yellow);
|
||||||
|
foreach (var r in remain)
|
||||||
|
{
|
||||||
|
try { if (!r.HasExited) r.Kill(); } catch { }
|
||||||
|
finally { try { r.Close(); } catch { } }
|
||||||
|
}
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool allClosed = Process.GetProcessesByName(procName).Length == 0;
|
||||||
|
Logger.PrintColor("关闭完成 (成功" + killed + ", 失败" + failed + ")", allClosed ? ConsoleColor.Green : ConsoleColor.Yellow);
|
||||||
|
return allClosed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void LaunchGame()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(GameExeFullPath))
|
||||||
|
{
|
||||||
|
MessageBox.Show("找不到游戏文件:\n" + GameExeFullPath + "\n(配置路径: " + GameExePath + ")", "启动失败", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var existing = Process.GetProcessesByName(GameProcessName);
|
||||||
|
if (existing.Length > 0)
|
||||||
|
{
|
||||||
|
Logger.PrintColor("游戏已在运行中,跳过启动", ConsoleColor.Yellow);
|
||||||
|
foreach (var p in existing) try { p.Close(); } catch { }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
StartGameProcess();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "启动游戏失败: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RestartGame()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(GameExeFullPath))
|
||||||
|
{
|
||||||
|
MessageBox.Show("找不到游戏文件:\n" + GameExeFullPath + "\n(配置路径: " + GameExePath + ")", "启动失败", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.PrintColor("── 重启游戏:关闭进程 ──", ConsoleColor.Cyan);
|
||||||
|
bool closed = CloseGameProcesses();
|
||||||
|
if (!closed)
|
||||||
|
{
|
||||||
|
Logger.PrintColor("警告: 部分游戏进程未能完全关闭,仍尝试重新启动", ConsoleColor.Yellow);
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.Sleep(1500);
|
||||||
|
|
||||||
|
Logger.PrintColor("── 重启游戏:重新启动 ──", ConsoleColor.Cyan);
|
||||||
|
StartGameProcess();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "重启游戏失败: " + ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static void StartGameProcess()
|
||||||
|
{
|
||||||
|
var psi = new ProcessStartInfo(GameExeFullPath)
|
||||||
|
{
|
||||||
|
WorkingDirectory = GameWorkFullPath,
|
||||||
|
UseShellExecute = true
|
||||||
|
};
|
||||||
|
Process.Start(psi);
|
||||||
|
Logger.PrintColor("游戏已启动", ConsoleColor.Green);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>加载 game_settings.json (启动项 + 配置表路径)</summary>
|
||||||
|
private static void LoadGameSettings()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "game_settings.json");
|
||||||
|
if (!File.Exists(path)) return;
|
||||||
|
var json = JObject.Parse(File.ReadAllText(path));
|
||||||
|
|
||||||
|
// 启动项
|
||||||
|
string exe = json["gameExePath"]?.Value<string>();
|
||||||
|
string dir = json["gameWorkDir"]?.Value<string>();
|
||||||
|
bool auto = json["autoLaunchGame"]?.Value<bool>() ?? true;
|
||||||
|
if (!string.IsNullOrWhiteSpace(exe)) GameExePath = exe;
|
||||||
|
if (!string.IsNullOrWhiteSpace(dir)) GameWorkDir = dir;
|
||||||
|
AutoLaunchGame = auto;
|
||||||
|
|
||||||
|
// 配置表路径
|
||||||
|
string configDir = json["configDir"]?.Value<string>();
|
||||||
|
if (!string.IsNullOrWhiteSpace(configDir)) ConfigDir = configDir;
|
||||||
|
var cfg = json["configFiles"];
|
||||||
|
if (cfg != null)
|
||||||
|
{
|
||||||
|
string eff = cfg["effect"]?.Value<string>();
|
||||||
|
string mac = cfg["macro"]?.Value<string>();
|
||||||
|
string gift = cfg["gift"]?.Value<string>();
|
||||||
|
string cmd = cfg["command"]?.Value<string>();
|
||||||
|
string fkey = cfg["fkey"]?.Value<string>();
|
||||||
|
if (!string.IsNullOrWhiteSpace(eff)) EffectFile = eff;
|
||||||
|
if (!string.IsNullOrWhiteSpace(mac)) MacroFile = mac;
|
||||||
|
if (!string.IsNullOrWhiteSpace(gift)) GiftFile = gift;
|
||||||
|
if (!string.IsNullOrWhiteSpace(cmd)) CommandFile = cmd;
|
||||||
|
if (!string.IsNullOrWhiteSpace(fkey)) FkeyFile = fkey;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.PrintColor($"已加载游戏设置: 自动启动={AutoLaunchGame}, 配置目录={ConfigDir}", ConsoleColor.DarkGray);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.PrintColor($"加载游戏设置失败: {ex.Message}", ConsoleColor.Yellow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private const string WatchdogArg = "--watchdog";
|
private const string WatchdogArg = "--watchdog";
|
||||||
private const string WatchdogPidArg = "--pid";
|
private const string WatchdogPidArg = "--pid";
|
||||||
|
|
||||||
static void Main(string[] args)
|
static void Main(string[] args)
|
||||||
{
|
{
|
||||||
if (IsWatchdogMode(args))
|
if (IsWatchdogMode(args)) { RunWatchdog(args); return; }
|
||||||
{
|
|
||||||
RunWatchdog(args);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mutex.WaitOne(TimeSpan.Zero, true))
|
if (!mutex.WaitOne(TimeSpan.Zero, true))
|
||||||
{
|
{
|
||||||
@@ -39,14 +246,11 @@ namespace BarrageGrab
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SetTitle("抖音弹幕监听推送");
|
SetTitle("弹幕监听推送");
|
||||||
|
|
||||||
StartWatchdog();
|
StartWatchdog();
|
||||||
|
LoadGameSettings();
|
||||||
|
|
||||||
try
|
try { Init(); }
|
||||||
{
|
|
||||||
Init();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, $"程序初始化错误,{ex.Message}");
|
Logger.LogError(ex, $"程序初始化错误,{ex.Message}");
|
||||||
@@ -54,18 +258,11 @@ namespace BarrageGrab
|
|||||||
exited = true;
|
exited = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (!exited)
|
while (!exited) Thread.Sleep(500);
|
||||||
{
|
|
||||||
Thread.Sleep(500);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!AppRuntime.WsServer.IsDisposed)
|
|
||||||
{
|
|
||||||
AppRuntime.WsServer.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (!AppRuntime.WsServer.IsDisposed) AppRuntime.WsServer.Dispose();
|
||||||
Logger.PrintColor("服务器已关闭...");
|
Logger.PrintColor("服务器已关闭...");
|
||||||
WinApi.SetConsoleCtrlHandler(controlCtr, false);//反注册捕获控制台关闭
|
WinApi.SetConsoleCtrlHandler(controlCtr, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void StartWatchdog()
|
private static void StartWatchdog()
|
||||||
@@ -74,21 +271,12 @@ namespace BarrageGrab
|
|||||||
{
|
{
|
||||||
var exePath = Application.ExecutablePath;
|
var exePath = Application.ExecutablePath;
|
||||||
if (string.IsNullOrWhiteSpace(exePath) || !File.Exists(exePath)) return;
|
if (string.IsNullOrWhiteSpace(exePath) || !File.Exists(exePath)) return;
|
||||||
|
|
||||||
var pid = Process.GetCurrentProcess().Id;
|
var pid = Process.GetCurrentProcess().Id;
|
||||||
var args = string.Format("{0} {1} {2}", WatchdogArg, WatchdogPidArg, pid);
|
var args = string.Format("{0} {1} {2}", WatchdogArg, WatchdogPidArg, pid);
|
||||||
var psi = new ProcessStartInfo(exePath, args)
|
var psi = new ProcessStartInfo(exePath, args) { UseShellExecute = false, CreateNoWindow = true, WindowStyle = ProcessWindowStyle.Hidden };
|
||||||
{
|
|
||||||
UseShellExecute = false,
|
|
||||||
CreateNoWindow = true,
|
|
||||||
WindowStyle = ProcessWindowStyle.Hidden
|
|
||||||
};
|
|
||||||
Process.Start(psi);
|
Process.Start(psi);
|
||||||
}
|
}
|
||||||
catch
|
catch { }
|
||||||
{
|
|
||||||
// best-effort only
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsWatchdogMode(string[] args)
|
private static bool IsWatchdogMode(string[] args)
|
||||||
@@ -116,106 +304,69 @@ namespace BarrageGrab
|
|||||||
{
|
{
|
||||||
var pid = TryGetWatchdogPid(args);
|
var pid = TryGetWatchdogPid(args);
|
||||||
if (pid == null || pid <= 0) return;
|
if (pid == null || pid <= 0) return;
|
||||||
|
|
||||||
Process target;
|
Process target;
|
||||||
try
|
try { target = Process.GetProcessById(pid.Value); }
|
||||||
{
|
catch { SafeCloseSystemProxy(); return; }
|
||||||
target = Process.GetProcessById(pid.Value);
|
for (; ; ) { if (target.HasExited) break; Thread.Sleep(1000); }
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
SafeCloseSystemProxy();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (; ; )
|
|
||||||
{
|
|
||||||
if (target.HasExited) break;
|
|
||||||
Thread.Sleep(1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
SafeCloseSystemProxy();
|
SafeCloseSystemProxy();
|
||||||
}
|
}
|
||||||
catch
|
catch { try { SafeCloseSystemProxy(); } catch { } }
|
||||||
{
|
|
||||||
try { SafeCloseSystemProxy(); } catch { }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SafeCloseSystemProxy()
|
private static void SafeCloseSystemProxy()
|
||||||
{
|
{
|
||||||
try
|
try { if (!AppSetting.Current.UsedProxy) return; WinApi.CloseSystemProxy(); }
|
||||||
{
|
catch { }
|
||||||
if (!AppSetting.Current.UsedProxy) return;
|
|
||||||
WinApi.CloseSystemProxy();
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Init()
|
private static void Init()
|
||||||
{
|
{
|
||||||
AppRuntime.Init();
|
AppRuntime.Init();
|
||||||
LiveCompanHelper.SwitchSetup();
|
LiveCompanHelper.SwitchSetup();
|
||||||
WinApi.SetConsoleCtrlHandler(controlCtr, true);//捕获控制台关闭
|
WinApi.SetConsoleCtrlHandler(controlCtr, true);
|
||||||
WinApi.DisableQuickEditMode();//禁用控制台快速编辑模式
|
WinApi.DisableQuickEditMode();
|
||||||
AppRuntime.DisplayConsole(!AppSetting.Current.HideConsole);//控制控制台可见
|
AppRuntime.DisplayConsole(!AppSetting.Current.HideConsole);
|
||||||
AppRuntime.WsServer.Grab.Proxy.SetUpstreamProxy(AppSetting.Current.UpstreamProxy);//设置上游代理
|
AppRuntime.WsServer.Grab.Proxy.SetUpstreamProxy(AppSetting.Current.UpstreamProxy);
|
||||||
AppRuntime.WsServer.OnClose += (s, e) =>
|
AppRuntime.WsServer.OnClose += (s, e) => { exited = true; };
|
||||||
{
|
|
||||||
exited = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
//串口写入服务
|
|
||||||
if (!AppSetting.Current.ComPort.IsNullOrWhiteSpace())
|
if (!AppSetting.Current.ComPort.IsNullOrWhiteSpace())
|
||||||
{
|
|
||||||
AppRuntime.ComPortServer.OpenStart();
|
AppRuntime.ComPortServer.OpenStart();
|
||||||
}
|
|
||||||
|
|
||||||
//显示窗体
|
|
||||||
if (AppSetting.Current.ShowWindow)
|
if (AppSetting.Current.ShowWindow)
|
||||||
{
|
{
|
||||||
var uiThread = new Thread(new ThreadStart(() =>
|
var uiThread = new Thread(new ThreadStart(() =>
|
||||||
{
|
{
|
||||||
mainForm = new FormView();
|
mainForm = new FormView();
|
||||||
//开启消息循环
|
|
||||||
System.Windows.Forms.Application.Run(mainForm);
|
System.Windows.Forms.Application.Run(mainForm);
|
||||||
formExited = true;
|
formExited = true;
|
||||||
AppRuntime.WsServer.Dispose();//Close后自动释放资源
|
AppRuntime.WsServer.Dispose();
|
||||||
}));
|
}));
|
||||||
uiThread.SetApartmentState(ApartmentState.STA);
|
uiThread.SetApartmentState(ApartmentState.STA);
|
||||||
uiThread.IsBackground = true;
|
uiThread.IsBackground = true;
|
||||||
uiThread.Start();
|
uiThread.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
AppRuntime.WsServer.StartListen();//启动WS以及代理服务
|
AppRuntime.WsServer.StartListen();
|
||||||
Logger.PrintColor($"{AppRuntime.WsServer.ServerLocation} 弹幕服务已启动,其他端可通过此地址获取到弹幕流信息", ConsoleColor.Green);
|
Logger.PrintColor($"{AppRuntime.WsServer.ServerLocation} 弹幕服务已启动", ConsoleColor.Green);
|
||||||
|
|
||||||
|
if (AutoLaunchGame) LaunchGame();
|
||||||
|
|
||||||
Version version = System.Reflection.Assembly.GetAssembly(typeof(Program)).GetName().Version;
|
Version version = System.Reflection.Assembly.GetAssembly(typeof(Program)).GetName().Version;
|
||||||
SetTitle($"抖音弹幕监听推送 v{version} [{AppRuntime.WsServer.ServerLocation}]");
|
SetTitle($"弹幕监听推送 v{version} [{AppRuntime.WsServer.ServerLocation}]");
|
||||||
}
|
}
|
||||||
|
|
||||||
//检测设置控制台标题
|
|
||||||
private static void SetTitle(string title)
|
private static void SetTitle(string title)
|
||||||
{
|
{
|
||||||
if (WinApi.GetConsoleWindow() != IntPtr.Zero)
|
if (WinApi.GetConsoleWindow() != IntPtr.Zero) Console.Title = title;
|
||||||
{
|
|
||||||
Console.Title = title;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//监听控制台消息事件
|
|
||||||
private static bool ControlCtrlHandle(int CtrlType)
|
private static bool ControlCtrlHandle(int CtrlType)
|
||||||
{
|
{
|
||||||
switch (CtrlType)
|
switch (CtrlType)
|
||||||
{
|
{
|
||||||
case 0:
|
case 0: break;
|
||||||
//Logger.PrintColor("0工具被强制关闭"); //Ctrl+C关闭
|
|
||||||
//server.Close();
|
|
||||||
break;
|
|
||||||
case 2:
|
case 2:
|
||||||
Logger.PrintColor("2工具被强制关闭");//按控制台关闭按钮关闭
|
Logger.PrintColor("2工具被强制关闭");
|
||||||
AppRuntime.WsServer.Dispose();
|
AppRuntime.WsServer.Dispose();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,73 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
<assemblyIdentity version="1.0.0.0" name="MyApplication.app" />
|
<assemblyIdentity version="1.0.0.0" name="MyApplication.app" />
|
||||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||||
<security>
|
<security>
|
||||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
<!-- UAC 清单选项
|
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||||
如果想要更改 Windows 用户帐户控制级别,请使用
|
|
||||||
以下节点之一替换 requestedExecutionLevel 节点。
|
|
||||||
|
|
||||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
|
||||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
|
||||||
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
|
|
||||||
|
|
||||||
指定 requestedExecutionLevel 元素将禁用文件和注册表虚拟化。
|
|
||||||
如果你的应用程序需要此虚拟化来实现向后兼容性,则移除此
|
|
||||||
元素。
|
|
||||||
-->
|
|
||||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
|
||||||
</requestedPrivileges>
|
</requestedPrivileges>
|
||||||
<applicationRequestMinimum>
|
|
||||||
<defaultAssemblyRequest permissionSetReference="Custom" />
|
|
||||||
<PermissionSet class="System.Security.PermissionSet" version="1" ID="Custom" SameSite="site" Unrestricted="true" />
|
|
||||||
</applicationRequestMinimum>
|
|
||||||
</security>
|
</security>
|
||||||
</trustInfo>
|
</trustInfo>
|
||||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
|
||||||
<application>
|
|
||||||
<!-- 设计此应用程序与其一起工作且已针对此应用程序进行测试的
|
|
||||||
Windows 版本的列表。取消评论适当的元素,
|
|
||||||
Windows 将自动选择最兼容的环境。 -->
|
|
||||||
<!-- Windows Vista -->
|
|
||||||
<!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
|
|
||||||
<!-- Windows 7 -->
|
|
||||||
<!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->
|
|
||||||
<!-- Windows 8 -->
|
|
||||||
<!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->
|
|
||||||
<!-- Windows 8.1 -->
|
|
||||||
<!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->
|
|
||||||
<!-- Windows 10 -->
|
|
||||||
<!--<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />-->
|
|
||||||
</application>
|
|
||||||
</compatibility>
|
|
||||||
<!-- 指示该应用程序可感知 DPI 且 Windows 在 DPI 较高时将不会对其进行
|
|
||||||
自动缩放。Windows Presentation Foundation (WPF)应用程序自动感知 DPI,无需
|
|
||||||
选择加入。选择加入此设置的 Windows 窗体应用程序(面向 .NET Framework 4.6)还应
|
|
||||||
在其 app.config 中将 "EnableWindowsFormsHighDpiAutoResizing" 设置设置为 "true"。
|
|
||||||
|
|
||||||
将应用程序设为感知长路径。请参阅 https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation -->
|
|
||||||
<!--
|
|
||||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
|
||||||
<windowsSettings>
|
|
||||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
|
||||||
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
|
|
||||||
</windowsSettings>
|
|
||||||
</application>
|
|
||||||
-->
|
|
||||||
<!-- 启用 Windows 公共控件和对话框的主题(Windows XP 和更高版本) -->
|
|
||||||
<!--
|
|
||||||
<dependency>
|
|
||||||
<dependentAssembly>
|
|
||||||
<assemblyIdentity
|
|
||||||
type="win32"
|
|
||||||
name="Microsoft.Windows.Common-Controls"
|
|
||||||
version="6.0.0.0"
|
|
||||||
processorArchitecture="*"
|
|
||||||
publicKeyToken="6595b64144ccf1df"
|
|
||||||
language="*"
|
|
||||||
/>
|
|
||||||
</dependentAssembly>
|
|
||||||
</dependency>
|
|
||||||
-->
|
|
||||||
</assembly>
|
</assembly>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using BarrageGrab.Proxy.ProxyEventArgs;
|
using BarrageGrab.Proxy.ProxyEventArgs;
|
||||||
@@ -9,6 +9,11 @@ namespace BarrageGrab.Proxy
|
|||||||
{
|
{
|
||||||
event EventHandler<WsMessageEventArgs> OnWebSocketData;
|
event EventHandler<WsMessageEventArgs> OnWebSocketData;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 客户端发送的WebSocket数据事件 (用于解析认证包等)
|
||||||
|
/// </summary>
|
||||||
|
event EventHandler<WsMessageEventArgs> OnWebSocketSentData;
|
||||||
|
|
||||||
event EventHandler<HttpResponseEventArgs> OnFetchResponse;
|
event EventHandler<HttpResponseEventArgs> OnFetchResponse;
|
||||||
|
|
||||||
event EventHandler<SystemProxyChangeEventArgs> OnProxyStatus;
|
event EventHandler<SystemProxyChangeEventArgs> OnProxyStatus;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using BarrageGrab.Modles.JsonEntity;
|
||||||
|
|
||||||
namespace BarrageGrab.Proxy.ProxyEventArgs
|
namespace BarrageGrab.Proxy.ProxyEventArgs
|
||||||
{
|
{
|
||||||
@@ -32,5 +33,10 @@ namespace BarrageGrab.Proxy.ProxyEventArgs
|
|||||||
/// 是否需要解压缩
|
/// 是否需要解压缩
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool NeedDecompress { get; set; } = true;
|
public bool NeedDecompress { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 直播平台
|
||||||
|
/// </summary>
|
||||||
|
public LivePlatform Platform { get; set; } = LivePlatform.Douyin;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -19,6 +19,11 @@ namespace BarrageGrab.Proxy
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public event EventHandler<WsMessageEventArgs> OnWebSocketData;
|
public event EventHandler<WsMessageEventArgs> OnWebSocketData;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 客户端发送的WebSocket数据事件 (用于解析认证包等)
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<WsMessageEventArgs> OnWebSocketSentData;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 接收到http响应事件
|
/// 接收到http响应事件
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -78,6 +83,15 @@ namespace BarrageGrab.Proxy
|
|||||||
OnWebSocketData?.Invoke(this, args);
|
OnWebSocketData?.Invoke(this, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 触发客户端发送的websocket数据事件
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args"></param>
|
||||||
|
protected void FireWsSentEvent(WsMessageEventArgs args)
|
||||||
|
{
|
||||||
|
OnWebSocketSentData?.Invoke(this, args);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 触发弹幕http弹幕事件
|
/// 触发弹幕http弹幕事件
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -43,7 +43,13 @@ namespace BarrageGrab.Proxy
|
|||||||
const string BARRAGE_POOL_PATH = "/webcast/im/fetch";
|
const string BARRAGE_POOL_PATH = "/webcast/im/fetch";
|
||||||
const string LIVE_SCRIPT_PATH = "/obj/static/webcast/douyin_live";
|
const string LIVE_SCRIPT_PATH = "/obj/static/webcast/douyin_live";
|
||||||
const string WEBCAST_AMEMV_HOST = "webcast.amemv.com";
|
const string WEBCAST_AMEMV_HOST = "webcast.amemv.com";
|
||||||
|
// B站直播页面域名
|
||||||
|
const string BILI_LIVE_HOST = "live.bilibili.com";
|
||||||
private readonly Regex webcastBarrageReg = new Regex(@"webcast\d+-ws-web-\w+\.(douyin|amemv)\.com");
|
private readonly Regex webcastBarrageReg = new Regex(@"webcast\d+-ws-web-\w+\.(douyin|amemv)\.com");
|
||||||
|
// 2026-05 快手WS域名变更为 livejs-ws.kuaishou.cn
|
||||||
|
private readonly Regex kuaishouWsReg = new Regex(@"livejs-ws\.kuaishou\.cn|(?:livemate|pclivemate)\.pull\.yximgs\.com", RegexOptions.IgnoreCase);
|
||||||
|
// B站直播弹幕WS域名 (broadcast-msg.chat.bilibili.com 主域名, *-live-comet.chat.bilibili.com 新版comet服务器, live-trace.com 备用, ws.live.bilibili.com 历史域名)
|
||||||
|
private readonly Regex biliWsReg = new Regex(@"chat\.bilibili\.com|live-trace\.com|ws\.live\.bilibili\.com", RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
public override string HttpUpstreamProxy { get { return proxyServer?.UpStreamHttpProxy?.ToString() ?? ""; } }
|
public override string HttpUpstreamProxy { get { return proxyServer?.UpStreamHttpProxy?.ToString() ?? ""; } }
|
||||||
|
|
||||||
@@ -196,7 +202,7 @@ namespace BarrageGrab.Proxy
|
|||||||
|
|
||||||
private bool CheckBrowser(string processName)
|
private bool CheckBrowser(string processName)
|
||||||
{
|
{
|
||||||
return AppSetting.Current.ProcessFilter.Contains(processName) && processName != "直播伴侣" && processName != "douyin";
|
return AppSetting.Current.ProcessFilter.Contains(processName) && processName != "直播伴侣" && processName != "douyin" && processName != "kwailive" && processName != "bilibili";
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ProxyServer_BeforeResponse(object sender, SessionEventArgs e)
|
private async Task ProxyServer_BeforeResponse(object sender, SessionEventArgs e)
|
||||||
@@ -221,6 +227,37 @@ namespace BarrageGrab.Proxy
|
|||||||
|
|
||||||
//处理脚本拦截修改
|
//处理脚本拦截修改
|
||||||
await HookScriptAsync(e);
|
await HookScriptAsync(e);
|
||||||
|
|
||||||
|
//快手礼物映射嗅探
|
||||||
|
await HookKsGiftSniffer(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 快手礼物映射嗅探 - 拦截包含礼物数据的JSON响应
|
||||||
|
private async Task HookKsGiftSniffer(SessionEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string hostname = e.HttpClient.Request.RequestUri.Host;
|
||||||
|
if (hostname.IndexOf("kuaishou", StringComparison.OrdinalIgnoreCase) < 0 &&
|
||||||
|
hostname.IndexOf("yximgs", StringComparison.OrdinalIgnoreCase) < 0 &&
|
||||||
|
hostname.IndexOf("kskwai", StringComparison.OrdinalIgnoreCase) < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var contentType = e.HttpClient.Response.ContentType ?? "";
|
||||||
|
|
||||||
|
// 仅检查 JSON 响应
|
||||||
|
if (!contentType.Contains("json") && !contentType.Contains("javascript"))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var body = await e.GetResponseBodyAsString();
|
||||||
|
if (string.IsNullOrWhiteSpace(body) || body.Length < 20) return;
|
||||||
|
|
||||||
|
if (KsGiftHelper.TryParseGiftData(body))
|
||||||
|
{
|
||||||
|
KsGiftHelper.SaveMapping();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { /* 嗅探失败不影响主流程 */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hook 直播伴侣开播信息并更新
|
// Hook 直播伴侣开播信息并更新
|
||||||
@@ -341,6 +378,28 @@ namespace BarrageGrab.Proxy
|
|||||||
Logger.LogInfo($"订阅到新的弹幕流地址,roomid:{roomid}");
|
Logger.LogInfo($"订阅到新的弹幕流地址,roomid:{roomid}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//快手 ws 方式
|
||||||
|
if (
|
||||||
|
e.HttpClient.ConnectRequest?.TunnelType == TunnelType.Websocket &&
|
||||||
|
kuaishouWsReg.IsMatch(uri)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
e.DataReceived += WebSocket_DataReceived;
|
||||||
|
e.DataSent += WebSocket_DataSent;
|
||||||
|
Logger.LogInfo($"订阅到快手弹幕流地址,url:{uri}");
|
||||||
|
}
|
||||||
|
|
||||||
|
//B站 ws 方式
|
||||||
|
if (
|
||||||
|
e.HttpClient.ConnectRequest?.TunnelType == TunnelType.Websocket &&
|
||||||
|
biliWsReg.IsMatch(uri)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
e.DataReceived += WebSocket_DataReceived;
|
||||||
|
e.DataSent += WebSocket_DataSent;
|
||||||
|
Logger.LogInfo($"订阅到B站弹幕流地址,url:{uri}");
|
||||||
|
}
|
||||||
|
|
||||||
//轮询方式(当抖音ws连接断开后,客户端也会降级使用轮询模式获取弹幕)
|
//轮询方式(当抖音ws连接断开后,客户端也会降级使用轮询模式获取弹幕)
|
||||||
if (uri.Contains(BARRAGE_POOL_PATH) && contentType.Contains("application/protobuffer"))
|
if (uri.Contains(BARRAGE_POOL_PATH) && contentType.Contains("application/protobuffer"))
|
||||||
{
|
{
|
||||||
@@ -400,6 +459,9 @@ namespace BarrageGrab.Proxy
|
|||||||
var processName = base.GetProcessName(processid);
|
var processName = base.GetProcessName(processid);
|
||||||
var liveRoomMactch = Regex.Match(uri.Trim(), @".*:\/\/live.douyin\.com\/([0-9a-zA-Z_]+)");
|
var liveRoomMactch = Regex.Match(uri.Trim(), @".*:\/\/live.douyin\.com\/([0-9a-zA-Z_]+)");
|
||||||
var liveHomeMatch = Regex.Match(uri.Trim(), @".*:\/\/live.douyin\.com\/?$");
|
var liveHomeMatch = Regex.Match(uri.Trim(), @".*:\/\/live.douyin\.com\/?$");
|
||||||
|
var ksLiveRoomMatch = Regex.Match(uri.Trim(), @".*:\/\/live\.kuaishou\.com\/u\/([^/?]+)");
|
||||||
|
// B站直播间 URL: https://live.bilibili.com/<roomId> (roomId 纯数字)
|
||||||
|
var biliLiveRoomMatch = Regex.Match(uri.Trim(), @".*:\/\/live\.bilibili\.com\/(\d+)");
|
||||||
var contentType = e.HttpClient.Response.ContentType ?? "";
|
var contentType = e.HttpClient.Response.ContentType ?? "";
|
||||||
|
|
||||||
//检测是否为dom页,用于脚本注入
|
//检测是否为dom页,用于脚本注入
|
||||||
@@ -457,9 +519,123 @@ namespace BarrageGrab.Proxy
|
|||||||
}
|
}
|
||||||
|
|
||||||
e.SetResponseBodyString(html);
|
e.SetResponseBodyString(html);
|
||||||
}
|
}
|
||||||
|
else if (ksLiveRoomMatch.Success)
|
||||||
|
{
|
||||||
|
string userId = ksLiveRoomMatch.Groups[1].Value;
|
||||||
|
var html = await e.GetResponseBodyAsString();
|
||||||
|
|
||||||
|
var ksInfo = KsApiHelper.ParseRoomInfoFromHtml(html, userId);
|
||||||
|
if (ksInfo != null)
|
||||||
|
{
|
||||||
|
ksInfo.LiveUrl = url;
|
||||||
|
var roominfo = new RoomInfo()
|
||||||
|
{
|
||||||
|
WebRoomId = userId,
|
||||||
|
RoomId = ksInfo.LiveStreamId ?? userId,
|
||||||
|
Title = ksInfo.Nickname + "\u7684\u76f4\u64ad\u95f4",
|
||||||
|
IsLive = ksInfo.IsLive,
|
||||||
|
UserCount = ksInfo.WatchingCount,
|
||||||
|
LikeCount = ksInfo.LikeCount,
|
||||||
|
TotalUserCount = ksInfo.WatchingCountStr ?? "",
|
||||||
|
Cover = ksInfo.CoverUrl ?? "",
|
||||||
|
LiveUrl = url,
|
||||||
|
Owner = new RoomInfo.RoomAnchor()
|
||||||
|
{
|
||||||
|
Nickname = ksInfo.Nickname,
|
||||||
|
UserId = userId,
|
||||||
|
HeadUrl = ksInfo.HeadUrl ?? ""
|
||||||
|
}
|
||||||
|
};
|
||||||
|
AppRuntime.RoomCaches.AddRoomInfoCache(roominfo);
|
||||||
|
Logger.LogInfo("\u5feb\u624b\u76f4\u64ad\u9875" + userId + " [" + ksInfo.Nickname + "]\u7684\u76f4\u64ad\u95f4\uff0c\u623f\u95f4\u4fe1\u606f\u5df2\u91c7\u96c6\u5230\u7f13\u5b58");
|
||||||
|
Logger.LogInfo($"KS房间 LiveStreamId={ksInfo.LiveStreamId} Watching={ksInfo.WatchingCountStr} Liked={ksInfo.LikeCountStr} Avatar={(!string.IsNullOrEmpty(ksInfo.HeadUrl))}");
|
||||||
|
}
|
||||||
|
|
||||||
|
e.SetResponseBodyString(html);
|
||||||
|
|
||||||
|
// 从HTML中嗅探礼物映射(登录后__INITIAL_STATE__含allGifts等)
|
||||||
|
if (html.Contains("allGifts") || html.Contains("giftList") || html.Contains("giftPanel"))
|
||||||
|
{
|
||||||
|
KsGiftHelper.TryParseGiftData(html);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (biliLiveRoomMatch.Success)
|
||||||
|
{
|
||||||
|
// B站直播间页面: B站改版后页面不再注入 __INITIAL_STATE__, 改用官方API获取房间信息
|
||||||
|
string webRoomId = biliLiveRoomMatch.Groups[1].Value;
|
||||||
|
var html = await e.GetResponseBodyAsString();
|
||||||
|
|
||||||
|
// 先尝试从HTML快速解析 (兼容旧版页面)
|
||||||
|
var biliInfo = BiliApiHelper.ParseRoomInfoFromHtml(html, webRoomId);
|
||||||
|
|
||||||
|
if (biliInfo != null && !string.IsNullOrWhiteSpace(biliInfo.RoomId) && !string.IsNullOrWhiteSpace(biliInfo.Uid))
|
||||||
|
{
|
||||||
|
CacheBiliRoomInfo(biliInfo, webRoomId, url);
|
||||||
|
}
|
||||||
|
else if (!rinfoRequestings.Contains(webRoomId))
|
||||||
|
{
|
||||||
|
// HTML解析失败, 异步调用B站官方API获取房间信息
|
||||||
|
lock (rinfoRequestings) rinfoRequestings.Add(webRoomId);
|
||||||
|
|
||||||
|
var cookie = e.HttpClient.Request.Headers.GetFirstHeader("Cookie")?.Value;
|
||||||
|
BiliApiHelper.GetRoomInfoByApi(webRoomId, cookie).ContinueWith(t =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var info = t.Result;
|
||||||
|
if (info != null && !string.IsNullOrWhiteSpace(info.RoomId))
|
||||||
|
{
|
||||||
|
CacheBiliRoomInfo(info, webRoomId, url);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogWarn($"B站API获取房间 {webRoomId} 信息失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, $"B站API获取房间 {webRoomId} 信息异常: {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
lock (rinfoRequestings) rinfoRequestings.Remove(webRoomId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
e.SetResponseBodyString(html);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>缓存B站房间信息</summary>
|
||||||
|
private void CacheBiliRoomInfo(BiliRoomInfo biliInfo, string webRoomId, string url)
|
||||||
|
{
|
||||||
|
biliInfo.LiveUrl = url;
|
||||||
|
var roominfo = new RoomInfo()
|
||||||
|
{
|
||||||
|
WebRoomId = webRoomId,
|
||||||
|
RoomId = biliInfo.RoomId,
|
||||||
|
Title = string.IsNullOrWhiteSpace(biliInfo.Title) ? $"{biliInfo.Nickname}的直播间" : biliInfo.Title,
|
||||||
|
IsLive = biliInfo.IsLive,
|
||||||
|
UserCount = biliInfo.Online,
|
||||||
|
TotalUserCount = biliInfo.Online.ToString(),
|
||||||
|
Cover = biliInfo.CoverUrl ?? "",
|
||||||
|
LiveUrl = url,
|
||||||
|
Owner = new RoomInfo.RoomAnchor()
|
||||||
|
{
|
||||||
|
UserId = biliInfo.Uid ?? "",
|
||||||
|
Nickname = biliInfo.Nickname ?? "",
|
||||||
|
HeadUrl = biliInfo.HeadUrl ?? ""
|
||||||
|
}
|
||||||
|
};
|
||||||
|
AppRuntime.RoomCaches.AddRoomInfoCache(roominfo);
|
||||||
|
Logger.LogInfo($"B站直播页 {webRoomId} [{biliInfo.Nickname}]的直播间,房间信息已采集到缓存 (RoomId={biliInfo.RoomId})");
|
||||||
|
|
||||||
|
// 同时把 WebRoomId 关联到 RoomId
|
||||||
|
AppRuntime.RoomCaches.SetRoomCache(biliInfo.RoomId, webRoomId);
|
||||||
|
}
|
||||||
|
|
||||||
//生成注入上下文
|
//生成注入上下文
|
||||||
private string BuildContext(IDictionary<string, string> constVals)
|
private string BuildContext(IDictionary<string, string> constVals)
|
||||||
{
|
{
|
||||||
@@ -489,7 +665,7 @@ namespace BarrageGrab.Proxy
|
|||||||
//https://lf-webcast-platform.bytetos.com/obj/webcast-platform-cdn/webcast/douyin_live/chunks/island_a74ce.b55095a0.js
|
//https://lf-webcast-platform.bytetos.com/obj/webcast-platform-cdn/webcast/douyin_live/chunks/island_a74ce.b55095a0.js
|
||||||
//https://lf-webcast-platform.bytetos.com/obj/webcast-platform-cdn/webcast/douyin_live/chunks/PausePop.ec38fcf8.js 2026/04/09
|
//https://lf-webcast-platform.bytetos.com/obj/webcast-platform-cdn/webcast/douyin_live/chunks/PausePop.ec38fcf8.js 2026/04/09
|
||||||
//判断响应内容是否为js application/javascript
|
//判断响应内容是否为js application/javascript
|
||||||
if (processName != "直播伴侣" && processName != "douyin"
|
if (processName != "直播伴侣" && processName != "douyin" && processName != "kwailive"
|
||||||
&& fileName.ToLower().StartsWith("pausepop")
|
&& fileName.ToLower().StartsWith("pausepop")
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
@@ -543,7 +719,8 @@ namespace BarrageGrab.Proxy
|
|||||||
string url = e.HttpClient.Request.RequestUri.ToString();
|
string url = e.HttpClient.Request.RequestUri.ToString();
|
||||||
string hostname = e.HttpClient.Request.RequestUri.Host;
|
string hostname = e.HttpClient.Request.RequestUri.Host;
|
||||||
|
|
||||||
e.DecryptSsl = CheckHost(hostname);
|
bool decrypt = CheckHost(hostname);
|
||||||
|
e.DecryptSsl = decrypt;
|
||||||
|
|
||||||
|
|
||||||
//ws 方式
|
//ws 方式
|
||||||
@@ -566,7 +743,12 @@ namespace BarrageGrab.Proxy
|
|||||||
LIVE_HOST,
|
LIVE_HOST,
|
||||||
WEBCAST_AMEMV_HOST , //直播伴侣开播请求地址
|
WEBCAST_AMEMV_HOST , //直播伴侣开播请求地址
|
||||||
"*-webcast-platform.bytetos.com", //新的脚本地址
|
"*-webcast-platform.bytetos.com", //新的脚本地址
|
||||||
"*webcast*" //所有带webcast的域名
|
"*webcast*", //所有带webcast的域名
|
||||||
|
"*kuaishou.com", //快手直播页面
|
||||||
|
"*kuaishou.cn", //快手WS弹幕流地址 (livejs-ws.kuaishou.cn)
|
||||||
|
"*.yximgs.com", //快手CDN (视频流/封面等)
|
||||||
|
"*bilibili.com", //B站直播页面及WS弹幕流
|
||||||
|
"live-trace.com" //B站WS备用域名
|
||||||
};
|
};
|
||||||
|
|
||||||
if (decryptSsls.Contains(hostname))
|
if (decryptSsls.Contains(hostname))
|
||||||
@@ -617,6 +799,19 @@ namespace BarrageGrab.Proxy
|
|||||||
|
|
||||||
var str = Encoding.UTF8.GetString(payload);
|
var str = Encoding.UTF8.GetString(payload);
|
||||||
|
|
||||||
|
// B站: 客户端发送的数据(认证包/心跳),触发事件供解析器提取roomid
|
||||||
|
if (biliWsReg.IsMatch(hostname))
|
||||||
|
{
|
||||||
|
base.FireWsSentEvent(new WsMessageEventArgs()
|
||||||
|
{
|
||||||
|
ProcessID = processid,
|
||||||
|
HostName = hostname,
|
||||||
|
Payload = payload,
|
||||||
|
Platform = LivePlatform.Bilibili,
|
||||||
|
ProcessName = base.GetProcessName(processid)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
//base.FireWsEvent(new WsMessageEventArgs()
|
//base.FireWsEvent(new WsMessageEventArgs()
|
||||||
//{
|
//{
|
||||||
// ProcessID = processid,
|
// ProcessID = processid,
|
||||||
@@ -629,6 +824,7 @@ namespace BarrageGrab.Proxy
|
|||||||
}
|
}
|
||||||
|
|
||||||
//WebSocket 流读取
|
//WebSocket 流读取
|
||||||
|
private static int wsDecodeErrCount = 0;
|
||||||
private async void WebSocket_DataReceived(object sender, DataEventArgs e)
|
private async void WebSocket_DataReceived(object sender, DataEventArgs e)
|
||||||
{
|
{
|
||||||
var args = (SessionEventArgs)sender;
|
var args = (SessionEventArgs)sender;
|
||||||
@@ -663,11 +859,15 @@ namespace BarrageGrab.Proxy
|
|||||||
payload = frame.Data.ToArray();
|
payload = frame.Data.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var platform = biliWsReg.IsMatch(hostname) ? LivePlatform.Bilibili
|
||||||
|
: kuaishouWsReg.IsMatch(hostname) ? LivePlatform.Kuaishou
|
||||||
|
: LivePlatform.Douyin;
|
||||||
base.FireWsEvent(new WsMessageEventArgs()
|
base.FireWsEvent(new WsMessageEventArgs()
|
||||||
{
|
{
|
||||||
ProcessID = processid,
|
ProcessID = processid,
|
||||||
HostName = hostname,
|
HostName = hostname,
|
||||||
Payload = payload,
|
Payload = payload,
|
||||||
|
Platform = platform,
|
||||||
ProcessName = base.GetProcessName(processid)
|
ProcessName = base.GetProcessName(processid)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -675,7 +875,11 @@ namespace BarrageGrab.Proxy
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.PrintColor("解析某个WebSocket包出错:" + ex.Message);
|
if (wsDecodeErrCount < 3)
|
||||||
|
{
|
||||||
|
Logger.PrintColor("解析某个WebSocket包出错:" + ex.Message);
|
||||||
|
wsDecodeErrCount++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (messageData.Count > 0)
|
if (messageData.Count > 0)
|
||||||
|
|||||||
@@ -0,0 +1,405 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using BarrageGrab.Modles.BiliProtoEntity;
|
||||||
|
using BarrageGrab.Proxy.ProxyEventArgs;
|
||||||
|
using BrotliSharpLib;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace BarrageGrab
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 哔哩哔哩弹幕抓取器
|
||||||
|
/// 协议格式: 自定义16字节Big-Endian帧头 + JSON body (body可能为zlib/brotli压缩)
|
||||||
|
/// </summary>
|
||||||
|
public class BiliBarrageGrab
|
||||||
|
{
|
||||||
|
// B站直播弹幕WS域名
|
||||||
|
// broadcast-msg.chat.bilibili.com (主域名)
|
||||||
|
// *-live-comet.chat.bilibili.com (新版comet服务器, 如 zj-cn-live-comet.chat.bilibili.com)
|
||||||
|
// live-trace.com (备用)
|
||||||
|
// ws.live.bilibili.com (历史域名)
|
||||||
|
private readonly Regex biliWsReg = new Regex(@"chat\.bilibili\.com|live-trace\.com|ws\.live\.bilibili\.com", RegexOptions.IgnoreCase);
|
||||||
|
private const string BILI_LIVE_HOST = "live.bilibili.com";
|
||||||
|
|
||||||
|
/// <summary>弹幕消息</summary>
|
||||||
|
public event EventHandler<BiliRoomMessageEventArgs<BiliDanmuMsg>> OnChatMessage;
|
||||||
|
/// <summary>点赞消息</summary>
|
||||||
|
public event EventHandler<BiliRoomMessageEventArgs<BiliLikeData>> OnLikeMessage;
|
||||||
|
/// <summary>礼物消息</summary>
|
||||||
|
public event EventHandler<BiliRoomMessageEventArgs<BiliGiftData>> OnGiftMessage;
|
||||||
|
/// <summary>进入直播间/关注消息</summary>
|
||||||
|
public event EventHandler<BiliRoomMessageEventArgs<BiliInteractData>> OnInteractMessage;
|
||||||
|
/// <summary>房间统计消息</summary>
|
||||||
|
public event EventHandler<BiliRoomMessageEventArgs<BiliRoomStats>> OnRoomStatsMessage;
|
||||||
|
/// <summary>分享消息 (INTERACT_WORD中msg_type=3)</summary>
|
||||||
|
public event EventHandler<BiliRoomMessageEventArgs<BiliInteractData>> OnShareMessage;
|
||||||
|
|
||||||
|
public BiliBarrageGrab()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前认证的房间ID (从AUTH包解析,供消息处理器读取)
|
||||||
|
/// </summary>
|
||||||
|
public string CurrentRoomId { get; private set; } = "";
|
||||||
|
|
||||||
|
public bool IsBilibiliHost(string hostname)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(hostname)) return false;
|
||||||
|
hostname = hostname.Trim().ToLower();
|
||||||
|
return biliWsReg.IsMatch(hostname) || hostname == BILI_LIVE_HOST;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsBilibiliWebSocket(string uri)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(uri)) return false;
|
||||||
|
return biliWsReg.IsMatch(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 处理代理收到的B站WS数据
|
||||||
|
/// </summary>
|
||||||
|
public void HandleWebSocketData(WsMessageEventArgs e)
|
||||||
|
{
|
||||||
|
var buff = e.Payload;
|
||||||
|
if (buff == null || buff.Length == 0) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 一个WS帧可能包含多个B站数据包,需要循环解析
|
||||||
|
ProcessBuffer(buff, 0, buff.Length, e.ProcessName);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (biliErrCount < 3)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, $"处理B站弹幕数据包时出错:{ex.Message}");
|
||||||
|
biliErrCount++;
|
||||||
|
}
|
||||||
|
if (biliErrCount == 3)
|
||||||
|
{
|
||||||
|
Logger.LogWarn("B站弹幕解析持续失败,已抑制后续错误日志");
|
||||||
|
biliErrCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 处理客户端发送的B站WS数据 (解析AUTH认证包提取roomid)
|
||||||
|
/// B站客户端连接WS后会发送AUTH包: {uid,roomid,protover,platform,type,key}
|
||||||
|
/// </summary>
|
||||||
|
public void HandleSentData(WsMessageEventArgs e)
|
||||||
|
{
|
||||||
|
var buff = e.Payload;
|
||||||
|
if (buff == null || buff.Length < BiliSocketFrame.HEADER_SIZE) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
BiliSocketFrame frame;
|
||||||
|
if (!BiliSocketFrame.TryParse(buff, 0, buff.Length, out frame)) return;
|
||||||
|
|
||||||
|
// 只关注认证请求包 (Operation=7)
|
||||||
|
if (frame.Operation != BiliOperation.AUTH) return;
|
||||||
|
|
||||||
|
var payload = BiliSocketFrame.GetPayload(buff, 0, frame);
|
||||||
|
if (payload == null || payload.Length == 0) return;
|
||||||
|
|
||||||
|
var json = System.Text.Encoding.UTF8.GetString(payload);
|
||||||
|
var jobj = JObject.Parse(json);
|
||||||
|
var roomIdToken = jobj["roomid"];
|
||||||
|
if (roomIdToken == null) return;
|
||||||
|
|
||||||
|
// roomid可能是整数或字符串
|
||||||
|
string roomId = roomIdToken.Type == JTokenType.Integer
|
||||||
|
? roomIdToken.Value<long>().ToString()
|
||||||
|
: roomIdToken.Value<string>() ?? "";
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(roomId))
|
||||||
|
{
|
||||||
|
CurrentRoomId = roomId;
|
||||||
|
Logger.LogInfo($"B站认证包解析成功,当前房间ID: {roomId}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarn($"解析B站AUTH认证包失败: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int biliErrCount = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 递归解析缓冲区中可能包含的多个B站数据包
|
||||||
|
/// </summary>
|
||||||
|
private void ProcessBuffer(byte[] buffer, int offset, int count, string processName)
|
||||||
|
{
|
||||||
|
int processed = 0;
|
||||||
|
while (processed + BiliSocketFrame.HEADER_SIZE <= count)
|
||||||
|
{
|
||||||
|
BiliSocketFrame frame;
|
||||||
|
if (!BiliSocketFrame.TryParse(buffer, offset + processed, count - processed, out frame))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 包长超过剩余缓冲区,说明数据不完整,等下次再处理
|
||||||
|
if (processed + frame.PacketLength > count)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload = BiliSocketFrame.GetPayload(buffer, offset + processed, frame);
|
||||||
|
processed += (int)frame.PacketLength;
|
||||||
|
|
||||||
|
// 只处理服务器推送的消息和心跳回复
|
||||||
|
if (frame.Operation == BiliOperation.HEARTBEAT_REPLY)
|
||||||
|
{
|
||||||
|
// 心跳回复: body是4字节big-endian人气值
|
||||||
|
if (payload != null && payload.Length >= 4)
|
||||||
|
{
|
||||||
|
var online = (long)BiliSocketFrame.ReadUInt32BE(payload, 0);
|
||||||
|
var stats = new BiliRoomStats { Online = online, Watched = 0 };
|
||||||
|
OnRoomStatsMessage?.Invoke(this, new BiliRoomMessageEventArgs<BiliRoomStats>(processName, stats));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame.Operation != BiliOperation.MESSAGE) continue;
|
||||||
|
|
||||||
|
// 根据协议版本解压
|
||||||
|
byte[] jsonBytes;
|
||||||
|
switch (frame.ProtocolVersion)
|
||||||
|
{
|
||||||
|
case BiliProtocolVersion.PLAIN:
|
||||||
|
case BiliProtocolVersion.PLAIN_AUTH:
|
||||||
|
jsonBytes = payload;
|
||||||
|
break;
|
||||||
|
case BiliProtocolVersion.ZLIB:
|
||||||
|
jsonBytes = ZlibDecompress(payload);
|
||||||
|
break;
|
||||||
|
case BiliProtocolVersion.BROTLI:
|
||||||
|
jsonBytes = BrotliDecompress(payload);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
jsonBytes = payload;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonBytes == null || jsonBytes.Length == 0) continue;
|
||||||
|
|
||||||
|
// 解压后可能仍然是多个数据包 (Zlib/Brotli压缩的通常是合并包)
|
||||||
|
if (frame.ProtocolVersion == BiliProtocolVersion.ZLIB ||
|
||||||
|
frame.ProtocolVersion == BiliProtocolVersion.BROTLI)
|
||||||
|
{
|
||||||
|
ProcessBuffer(jsonBytes, 0, jsonBytes.Length, processName);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 明文JSON: 解析为消息
|
||||||
|
DispatchJsonMessage(jsonBytes, processName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将JSON字节解析为B站消息并触发对应事件
|
||||||
|
/// </summary>
|
||||||
|
private void DispatchJsonMessage(byte[] jsonBytes, string processName)
|
||||||
|
{
|
||||||
|
string json;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
json = System.Text.Encoding.UTF8.GetString(jsonBytes);
|
||||||
|
}
|
||||||
|
catch { return; }
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(json)) return;
|
||||||
|
|
||||||
|
JObject jobj;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
jobj = JObject.Parse(json);
|
||||||
|
}
|
||||||
|
catch { return; }
|
||||||
|
|
||||||
|
var cmdToken = jobj["cmd"];
|
||||||
|
if (cmdToken == null) return;
|
||||||
|
string cmd = cmdToken.Value<string>();
|
||||||
|
if (string.IsNullOrWhiteSpace(cmd)) return;
|
||||||
|
|
||||||
|
// 兼容cmd带数字后缀的版本,如 DANMU_MSG-4b1cb6f3
|
||||||
|
int dashIdx = cmd.IndexOf('-');
|
||||||
|
if (dashIdx > 0) cmd = cmd.Substring(0, dashIdx);
|
||||||
|
|
||||||
|
switch (cmd)
|
||||||
|
{
|
||||||
|
case BiliCmd.DANMU_MSG:
|
||||||
|
{
|
||||||
|
var msg = ParseDanmuMsg(jobj);
|
||||||
|
if (msg != null)
|
||||||
|
{
|
||||||
|
OnChatMessage?.Invoke(this, new BiliRoomMessageEventArgs<BiliDanmuMsg>(processName, msg));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case BiliCmd.SEND_GIFT:
|
||||||
|
{
|
||||||
|
var msg = jobj["data"]?.ToObject<BiliGiftData>();
|
||||||
|
if (msg != null)
|
||||||
|
{
|
||||||
|
OnGiftMessage?.Invoke(this, new BiliRoomMessageEventArgs<BiliGiftData>(processName, msg));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case BiliCmd.INTERACT_WORD:
|
||||||
|
{
|
||||||
|
var msg = jobj["data"]?.ToObject<BiliInteractData>();
|
||||||
|
if (msg != null)
|
||||||
|
{
|
||||||
|
// msg_type: 1=进入 2=关注 3=分享
|
||||||
|
if (msg.MsgType == 3)
|
||||||
|
{
|
||||||
|
OnShareMessage?.Invoke(this, new BiliRoomMessageEventArgs<BiliInteractData>(processName, msg));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OnInteractMessage?.Invoke(this, new BiliRoomMessageEventArgs<BiliInteractData>(processName, msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case BiliCmd.LIKE_INFO_V3_CLICK:
|
||||||
|
{
|
||||||
|
var msg = jobj["data"]?.ToObject<BiliLikeData>();
|
||||||
|
if (msg != null)
|
||||||
|
{
|
||||||
|
OnLikeMessage?.Invoke(this, new BiliRoomMessageEventArgs<BiliLikeData>(processName, msg));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case BiliCmd.ONLINE_RANK_COUNT:
|
||||||
|
{
|
||||||
|
var count = jobj["data"]?["count"]?.Value<long>() ?? 0;
|
||||||
|
var stats = new BiliRoomStats { Online = count, Watched = 0 };
|
||||||
|
OnRoomStatsMessage?.Invoke(this, new BiliRoomMessageEventArgs<BiliRoomStats>(processName, stats));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case BiliCmd.WATCHED_CHANGE:
|
||||||
|
{
|
||||||
|
var num = jobj["data"]?["num"]?.Value<long>() ?? 0;
|
||||||
|
var stats = new BiliRoomStats { Online = 0, Watched = num };
|
||||||
|
OnRoomStatsMessage?.Invoke(this, new BiliRoomMessageEventArgs<BiliRoomStats>(processName, stats));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case BiliCmd.ROOM_REAL_TIME_MESSAGE_UPDATE:
|
||||||
|
{
|
||||||
|
// 同时包含在线人数和看过人数
|
||||||
|
var online = jobj["data"]?["online"]?.Value<long>() ?? 0;
|
||||||
|
var watched = jobj["data"]?["watched_show"]?["num"]?.Value<long>() ?? 0;
|
||||||
|
var stats = new BiliRoomStats { Online = online, Watched = watched };
|
||||||
|
OnRoomStatsMessage?.Invoke(this, new BiliRoomMessageEventArgs<BiliRoomStats>(processName, stats));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// 未知消息类型忽略
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析DANMU_MSG消息: info[1]=内容, info[2]=[uid,uname,isAdmin,rank,?anchor]
|
||||||
|
/// </summary>
|
||||||
|
private BiliDanmuMsg ParseDanmuMsg(JObject jobj)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var info = jobj["info"] as JArray;
|
||||||
|
if (info == null || info.Count < 3) return null;
|
||||||
|
|
||||||
|
var content = info[1]?.Value<string>() ?? "";
|
||||||
|
var userInfo = info[2] as JArray;
|
||||||
|
if (userInfo == null || userInfo.Count < 2) return null;
|
||||||
|
|
||||||
|
var msg = new BiliDanmuMsg
|
||||||
|
{
|
||||||
|
Content = content,
|
||||||
|
Uid = userInfo[0]?.Value<long>() ?? 0,
|
||||||
|
Uname = userInfo[1]?.Value<string>() ?? "",
|
||||||
|
IsAdmin = userInfo.Count > 2 && userInfo[2]?.Value<int>() == 1,
|
||||||
|
IsAnchor = userInfo.Count > 4 && userInfo[4] != null && userInfo[4].Type != JTokenType.Null
|
||||||
|
};
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Zlib解压</summary>
|
||||||
|
private byte[] ZlibDecompress(byte[] data)
|
||||||
|
{
|
||||||
|
if (data == null || data.Length == 0) return null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var ms = new MemoryStream(data))
|
||||||
|
using (var zlib = new DeflateStream(ms, CompressionMode.Decompress))
|
||||||
|
using (var outMs = new MemoryStream())
|
||||||
|
{
|
||||||
|
// zlib前2字节是header(0x78 0x9c等),DeflateStream需要跳过
|
||||||
|
// 但 .NET Core 2.1+ 的 DeflateStream 可以处理 zlib header, .NET Framework 需要跳过
|
||||||
|
if (data.Length >= 2 && data[0] == 0x78)
|
||||||
|
{
|
||||||
|
ms.Position = 2;
|
||||||
|
}
|
||||||
|
zlib.CopyTo(outMs);
|
||||||
|
return outMs.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarn($"B站Zlib解压失败: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Brotli解压 (使用 BrotliSharpLib)</summary>
|
||||||
|
private byte[] BrotliDecompress(byte[] data)
|
||||||
|
{
|
||||||
|
if (data == null || data.Length == 0) return null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (var ms = new MemoryStream(data))
|
||||||
|
using (var brotli = new BrotliSharpLib.BrotliStream(ms, CompressionMode.Decompress))
|
||||||
|
using (var outMs = new MemoryStream())
|
||||||
|
{
|
||||||
|
brotli.CopyTo(outMs);
|
||||||
|
return outMs.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarn($"B站Brotli解压失败: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>B站消息事件参数</summary>
|
||||||
|
public class BiliRoomMessageEventArgs<T> : EventArgs where T : class
|
||||||
|
{
|
||||||
|
public string Process { get; set; }
|
||||||
|
public T Message { get; set; }
|
||||||
|
|
||||||
|
public BiliRoomMessageEventArgs() { }
|
||||||
|
|
||||||
|
public BiliRoomMessageEventArgs(string process, T data)
|
||||||
|
{
|
||||||
|
this.Process = process;
|
||||||
|
this.Message = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using BarrageGrab.Modles.KsProtoEntity;
|
||||||
|
using BarrageGrab.Proxy.ProxyEventArgs;
|
||||||
|
using ProtoBuf;
|
||||||
|
|
||||||
|
namespace BarrageGrab
|
||||||
|
{
|
||||||
|
public class KsBarrageGrab
|
||||||
|
{
|
||||||
|
// 2026-05 快手WS域名变更为 livejs-ws.kuaishou.cn
|
||||||
|
private readonly Regex kuaishouWsReg = new Regex(@"livejs-ws\.kuaishou\.cn|(?:livemate|pclivemate)\.pull\.yximgs\.com", RegexOptions.IgnoreCase);
|
||||||
|
private const string KUAISHOU_LIVE_HOST = "live.kuaishou.com";
|
||||||
|
|
||||||
|
public event EventHandler<KsRoomMessageEventArgs<KsWebCommentFeed>> OnChatMessage;
|
||||||
|
public event EventHandler<KsRoomMessageEventArgs<KsWebLikeFeed>> OnLikeMessage;
|
||||||
|
public event EventHandler<KsRoomMessageEventArgs<KsWebGiftFeed>> OnGiftMessage;
|
||||||
|
public event EventHandler<KsRoomMessageEventArgs<KsWebShareFeed>> OnShareMessage;
|
||||||
|
public event EventHandler<KsRoomMessageEventArgs<KsSCWebFeedPush>> OnRoomStatsMessage;
|
||||||
|
|
||||||
|
public KsBarrageGrab()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsKuaishouHost(string hostname)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(hostname)) return false;
|
||||||
|
hostname = hostname.Trim().ToLower();
|
||||||
|
return kuaishouWsReg.IsMatch(hostname) || hostname == KUAISHOU_LIVE_HOST;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsKuaishouWebSocket(string uri)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(uri)) return false;
|
||||||
|
return kuaishouWsReg.IsMatch(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void HandleWebSocketData(WsMessageEventArgs e)
|
||||||
|
{
|
||||||
|
var buff = e.Payload;
|
||||||
|
if (buff == null || buff.Length == 0) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var socketMsg = Serializer.Deserialize<KsSocketMessage>(new ReadOnlyMemory<byte>(buff));
|
||||||
|
if (socketMsg == null) return;
|
||||||
|
|
||||||
|
if (socketMsg.PayloadType != KsPayloadType.SC_FEED_PUSH) return;
|
||||||
|
|
||||||
|
byte[] payload = socketMsg.Payload;
|
||||||
|
if (socketMsg.CompressionType == KsCompressionType.GZIP)
|
||||||
|
{
|
||||||
|
payload = Decompress(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
var feedPush = Serializer.Deserialize<KsSCWebFeedPush>(new ReadOnlyMemory<byte>(payload));
|
||||||
|
if (feedPush == null) return;
|
||||||
|
|
||||||
|
foreach (var comment in feedPush.CommentFeeds)
|
||||||
|
{
|
||||||
|
OnChatMessage?.Invoke(this, new KsRoomMessageEventArgs<KsWebCommentFeed>(e.ProcessName, comment));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var like in feedPush.LikeFeeds)
|
||||||
|
{
|
||||||
|
OnLikeMessage?.Invoke(this, new KsRoomMessageEventArgs<KsWebLikeFeed>(e.ProcessName, like));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var gift in feedPush.GiftFeeds)
|
||||||
|
{
|
||||||
|
OnGiftMessage?.Invoke(this, new KsRoomMessageEventArgs<KsWebGiftFeed>(e.ProcessName, gift));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var share in feedPush.ShareFeeds)
|
||||||
|
{
|
||||||
|
OnShareMessage?.Invoke(this, new KsRoomMessageEventArgs<KsWebShareFeed>(e.ProcessName, share));
|
||||||
|
}
|
||||||
|
|
||||||
|
OnRoomStatsMessage?.Invoke(this, new KsRoomMessageEventArgs<KsSCWebFeedPush>(e.ProcessName, feedPush));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, $"处理快手弹幕数据包时出错:{ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] Decompress(byte[] zippedData)
|
||||||
|
{
|
||||||
|
using (var ms = new MemoryStream(zippedData))
|
||||||
|
using (var gzip = new GZipStream(ms, CompressionMode.Decompress))
|
||||||
|
using (var outBuffer = new MemoryStream())
|
||||||
|
{
|
||||||
|
byte[] block = new byte[1024];
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
int bytesRead = gzip.Read(block, 0, block.Length);
|
||||||
|
if (bytesRead <= 0) break;
|
||||||
|
outBuffer.Write(block, 0, bytesRead);
|
||||||
|
}
|
||||||
|
return outBuffer.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class KsRoomMessageEventArgs<T> : EventArgs where T : class
|
||||||
|
{
|
||||||
|
public string Process { get; set; }
|
||||||
|
public T Message { get; set; }
|
||||||
|
|
||||||
|
public KsRoomMessageEventArgs() { }
|
||||||
|
|
||||||
|
public KsRoomMessageEventArgs(string process, T data)
|
||||||
|
{
|
||||||
|
this.Process = process;
|
||||||
|
this.Message = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
@@ -39,7 +39,9 @@ namespace BarrageGrab
|
|||||||
ConcurrentDictionary<string, Tuple<int, DateTime>> giftCountCache = new ConcurrentDictionary<string, Tuple<int, DateTime>>();//礼物计数缓存
|
ConcurrentDictionary<string, Tuple<int, DateTime>> giftCountCache = new ConcurrentDictionary<string, Tuple<int, DateTime>>();//礼物计数缓存
|
||||||
Timer dieout = new Timer(10000);//离线客户端清理计时器
|
Timer dieout = new Timer(10000);//离线客户端清理计时器
|
||||||
Timer giftCountTimer = new Timer(10000);//礼物缓存清理计时器
|
Timer giftCountTimer = new Timer(10000);//礼物缓存清理计时器
|
||||||
WssBarrageGrab grab = new WssBarrageGrab();//弹幕解析器核心
|
WssBarrageGrab grab = new WssBarrageGrab();//抖音弹幕解析器核心
|
||||||
|
KsBarrageGrab ksGrab = new KsBarrageGrab();//快手弹幕解析器核心
|
||||||
|
BiliBarrageGrab biliGrab = new BiliBarrageGrab();//B站弹幕解析器核心
|
||||||
AppSetting Appsetting = AppSetting.Current;//全局配置文件实例
|
AppSetting Appsetting = AppSetting.Current;//全局配置文件实例
|
||||||
static int printCount = 0; //控制台输出计数,用于判断清理控制台
|
static int printCount = 0; //控制台输出计数,用于判断清理控制台
|
||||||
|
|
||||||
@@ -92,6 +94,43 @@ namespace BarrageGrab
|
|||||||
this.grab.OnControlMessage += Grab_OnControlMessage;
|
this.grab.OnControlMessage += Grab_OnControlMessage;
|
||||||
this.grab.Proxy.OnLiveCompanAction += Proxy_OnLiveCompanAction;
|
this.grab.Proxy.OnLiveCompanAction += Proxy_OnLiveCompanAction;
|
||||||
|
|
||||||
|
//快手事件订阅
|
||||||
|
this.ksGrab.OnChatMessage += KsGrab_OnChatMessage;
|
||||||
|
this.ksGrab.OnLikeMessage += KsGrab_OnLikeMessage;
|
||||||
|
this.ksGrab.OnGiftMessage += KsGrab_OnGiftMessage;
|
||||||
|
this.ksGrab.OnShareMessage += KsGrab_OnShareMessage;
|
||||||
|
this.ksGrab.OnRoomStatsMessage += KsGrab_OnRoomStatsMessage;
|
||||||
|
|
||||||
|
//B站事件订阅
|
||||||
|
this.biliGrab.OnChatMessage += BiliGrab_OnChatMessage;
|
||||||
|
this.biliGrab.OnLikeMessage += BiliGrab_OnLikeMessage;
|
||||||
|
this.biliGrab.OnGiftMessage += BiliGrab_OnGiftMessage;
|
||||||
|
this.biliGrab.OnInteractMessage += BiliGrab_OnInteractMessage;
|
||||||
|
this.biliGrab.OnShareMessage += BiliGrab_OnShareMessage;
|
||||||
|
this.biliGrab.OnRoomStatsMessage += BiliGrab_OnRoomStatsMessage;
|
||||||
|
|
||||||
|
//多平台WS数据路由 - 将代理收到的WS数据按平台分发到对应解析器
|
||||||
|
this.grab.Proxy.OnWebSocketData += (s, e) =>
|
||||||
|
{
|
||||||
|
if (e.Platform == LivePlatform.Kuaishou)
|
||||||
|
{
|
||||||
|
this.ksGrab.HandleWebSocketData(e);
|
||||||
|
}
|
||||||
|
else if (e.Platform == LivePlatform.Bilibili)
|
||||||
|
{
|
||||||
|
this.biliGrab.HandleWebSocketData(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//B站发送数据路由 - 解析AUTH认证包提取roomid
|
||||||
|
this.grab.Proxy.OnWebSocketSentData += (s, e) =>
|
||||||
|
{
|
||||||
|
if (e.Platform == LivePlatform.Bilibili)
|
||||||
|
{
|
||||||
|
this.biliGrab.HandleSentData(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
this.socketServer = socket;
|
this.socketServer = socket;
|
||||||
//dieout.Start();
|
//dieout.Start();
|
||||||
giftCountTimer.Start();
|
giftCountTimer.Start();
|
||||||
@@ -633,6 +672,319 @@ namespace BarrageGrab
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region 快手消息处理
|
||||||
|
|
||||||
|
private MsgUser KsUserToMsgUser(Modles.KsProtoEntity.KsSimpleUserInfo user)
|
||||||
|
{
|
||||||
|
if (user == null) return null;
|
||||||
|
return new MsgUser()
|
||||||
|
{
|
||||||
|
Id = long.TryParse(user.PrincipalId, out var pid) ? pid : 0,
|
||||||
|
DisplayId = user.PrincipalId ?? "",
|
||||||
|
Nickname = user.UserName ?? "用户" + user.PrincipalId,
|
||||||
|
HeadImgUrl = user.HeadUrl ?? "",
|
||||||
|
Gender = 0,
|
||||||
|
Level = 0,
|
||||||
|
PayLevel = 0,
|
||||||
|
ShortId = 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string KsRoomId;
|
||||||
|
|
||||||
|
private void KsGrab_OnChatMessage(object sender, KsRoomMessageEventArgs<Modles.KsProtoEntity.KsWebCommentFeed> e)
|
||||||
|
{
|
||||||
|
var feed = e.Message;
|
||||||
|
var enty = new Msg()
|
||||||
|
{
|
||||||
|
MsgId = long.TryParse(feed.Id, out var id) ? id : DateTime.Now.Ticks,
|
||||||
|
User = KsUserToMsgUser(feed.User),
|
||||||
|
Content = feed.Content ?? "",
|
||||||
|
RoomId = KsRoomId ?? "",
|
||||||
|
WebRoomId = KsRoomId ?? "",
|
||||||
|
Appid = "0"
|
||||||
|
};
|
||||||
|
|
||||||
|
var msgType = PackMsgType.弹幕消息;
|
||||||
|
PrintMsg(enty, msgType);
|
||||||
|
var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Kuaishou);
|
||||||
|
Broadcast(pack);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void KsGrab_OnLikeMessage(object sender, KsRoomMessageEventArgs<Modles.KsProtoEntity.KsWebLikeFeed> e)
|
||||||
|
{
|
||||||
|
var feed = e.Message;
|
||||||
|
var enty = new LikeMsg()
|
||||||
|
{
|
||||||
|
MsgId = long.TryParse(feed.Id, out var id) ? id : DateTime.Now.Ticks,
|
||||||
|
User = KsUserToMsgUser(feed.User),
|
||||||
|
Content = $"{feed.User?.UserName ?? "用户"} 为主播点了赞",
|
||||||
|
Count = 1,
|
||||||
|
Total = 0,
|
||||||
|
RoomId = KsRoomId ?? "",
|
||||||
|
WebRoomId = KsRoomId ?? "",
|
||||||
|
Appid = "0"
|
||||||
|
};
|
||||||
|
|
||||||
|
var msgType = PackMsgType.点赞消息;
|
||||||
|
PrintMsg(enty, msgType);
|
||||||
|
var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Kuaishou);
|
||||||
|
Broadcast(pack);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void KsGrab_OnGiftMessage(object sender, KsRoomMessageEventArgs<Modles.KsProtoEntity.KsWebGiftFeed> e)
|
||||||
|
{
|
||||||
|
var feed = e.Message;
|
||||||
|
var enty = new GiftMsg()
|
||||||
|
{
|
||||||
|
MsgId = long.TryParse(feed.Id, out var id) ? id : DateTime.Now.Ticks,
|
||||||
|
User = KsUserToMsgUser(feed.User),
|
||||||
|
GiftId = feed.GiftId,
|
||||||
|
GiftName = KsGiftHelper.GetGiftName(feed.GiftId) ?? $"礼物#{feed.GiftId}",
|
||||||
|
GroupId = 0,
|
||||||
|
GiftCount = feed.ComboCount > 0 ? feed.ComboCount : 1,
|
||||||
|
RepeatCount = feed.ComboCount > 0 ? feed.ComboCount : 1,
|
||||||
|
DiamondCount = 0,
|
||||||
|
Combo = feed.ComboCount > 1,
|
||||||
|
Content = $"{feed.User?.UserName ?? "用户"} 送出 {KsGiftHelper.GetGiftName(feed.GiftId) ?? $"礼物#{feed.GiftId}"} x {feed.ComboCount}",
|
||||||
|
RoomId = KsRoomId ?? "",
|
||||||
|
WebRoomId = KsRoomId ?? "",
|
||||||
|
Appid = "0"
|
||||||
|
};
|
||||||
|
|
||||||
|
var msgType = PackMsgType.礼物消息;
|
||||||
|
PrintMsg(enty, msgType);
|
||||||
|
var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Kuaishou);
|
||||||
|
Broadcast(pack);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void KsGrab_OnShareMessage(object sender, KsRoomMessageEventArgs<Modles.KsProtoEntity.KsWebShareFeed> e)
|
||||||
|
{
|
||||||
|
var feed = e.Message;
|
||||||
|
var enty = new ShareMessage()
|
||||||
|
{
|
||||||
|
MsgId = long.TryParse(feed.Id, out var id) ? id : DateTime.Now.Ticks,
|
||||||
|
User = KsUserToMsgUser(feed.User),
|
||||||
|
Content = $"{feed.User?.UserName ?? "用户"} 分享了直播间",
|
||||||
|
ShareType = ShareType.未知,
|
||||||
|
RoomId = KsRoomId ?? "",
|
||||||
|
WebRoomId = KsRoomId ?? "",
|
||||||
|
Appid = "0"
|
||||||
|
};
|
||||||
|
|
||||||
|
var msgType = PackMsgType.直播间分享;
|
||||||
|
PrintMsg(enty, msgType);
|
||||||
|
var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Kuaishou);
|
||||||
|
Broadcast(pack);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void KsGrab_OnRoomStatsMessage(object sender, KsRoomMessageEventArgs<Modles.KsProtoEntity.KsSCWebFeedPush> e)
|
||||||
|
{
|
||||||
|
var feed = e.Message;
|
||||||
|
var enty = new UserSeqMsg()
|
||||||
|
{
|
||||||
|
MsgId = DateTime.Now.Ticks,
|
||||||
|
Content = $"当前直播间人数 {feed.DisplayWatchingCount},点赞 {feed.DisplayLikeCount}",
|
||||||
|
OnlineUserCount = long.TryParse(feed.DisplayWatchingCount, out var watching) ? watching : 0,
|
||||||
|
TotalUserCount = 0,
|
||||||
|
OnlineUserCountStr = feed.DisplayWatchingCount ?? "0",
|
||||||
|
TotalUserCountStr = "0",
|
||||||
|
RoomId = KsRoomId ?? "",
|
||||||
|
WebRoomId = KsRoomId ?? "",
|
||||||
|
Appid = "0"
|
||||||
|
};
|
||||||
|
|
||||||
|
var msgType = PackMsgType.直播间统计;
|
||||||
|
PrintMsg(enty, msgType);
|
||||||
|
var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Kuaishou);
|
||||||
|
Broadcast(pack);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region B站消息处理
|
||||||
|
|
||||||
|
private MsgUser BiliUserToMsgUser(Modles.BiliProtoEntity.BiliDanmuMsg user)
|
||||||
|
{
|
||||||
|
if (user == null) return null;
|
||||||
|
return new MsgUser()
|
||||||
|
{
|
||||||
|
Id = user.Uid,
|
||||||
|
DisplayId = user.Uid.ToString(),
|
||||||
|
Nickname = user.Uname ?? "用户" + user.Uid,
|
||||||
|
IsAdmin = user.IsAdmin,
|
||||||
|
IsAnchor = user.IsAnchor,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private MsgUser BiliUserToMsgUser(long uid, string uname)
|
||||||
|
{
|
||||||
|
return new MsgUser()
|
||||||
|
{
|
||||||
|
Id = uid,
|
||||||
|
DisplayId = uid.ToString(),
|
||||||
|
Nickname = string.IsNullOrWhiteSpace(uname) ? "用户" + uid : uname,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//B站弹幕
|
||||||
|
private void BiliGrab_OnChatMessage(object sender, BiliRoomMessageEventArgs<Modles.BiliProtoEntity.BiliDanmuMsg> e)
|
||||||
|
{
|
||||||
|
var msg = e.Message;
|
||||||
|
var enty = new Msg()
|
||||||
|
{
|
||||||
|
MsgId = DateTime.Now.Ticks,
|
||||||
|
User = BiliUserToMsgUser(msg),
|
||||||
|
Content = msg.Content ?? "",
|
||||||
|
RoomId = biliGrab.CurrentRoomId,
|
||||||
|
WebRoomId = biliGrab.CurrentRoomId,
|
||||||
|
Appid = "0"
|
||||||
|
};
|
||||||
|
|
||||||
|
var msgType = PackMsgType.弹幕消息;
|
||||||
|
PrintMsg(enty, msgType);
|
||||||
|
var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Bilibili);
|
||||||
|
Broadcast(pack);
|
||||||
|
}
|
||||||
|
|
||||||
|
//B站点赞
|
||||||
|
private void BiliGrab_OnLikeMessage(object sender, BiliRoomMessageEventArgs<Modles.BiliProtoEntity.BiliLikeData> e)
|
||||||
|
{
|
||||||
|
var msg = e.Message;
|
||||||
|
var enty = new LikeMsg()
|
||||||
|
{
|
||||||
|
MsgId = DateTime.Now.Ticks,
|
||||||
|
User = BiliUserToMsgUser(msg.Uid, msg.Uname),
|
||||||
|
Content = $"{msg.Uname ?? "用户"} 为主播点了赞",
|
||||||
|
Count = 1,
|
||||||
|
Total = 0,
|
||||||
|
RoomId = biliGrab.CurrentRoomId,
|
||||||
|
WebRoomId = biliGrab.CurrentRoomId,
|
||||||
|
Appid = "0"
|
||||||
|
};
|
||||||
|
|
||||||
|
var msgType = PackMsgType.点赞消息;
|
||||||
|
PrintMsg(enty, msgType);
|
||||||
|
var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Bilibili);
|
||||||
|
Broadcast(pack);
|
||||||
|
}
|
||||||
|
|
||||||
|
//B站礼物
|
||||||
|
private void BiliGrab_OnGiftMessage(object sender, BiliRoomMessageEventArgs<Modles.BiliProtoEntity.BiliGiftData> e)
|
||||||
|
{
|
||||||
|
var msg = e.Message;
|
||||||
|
var count = msg.Num > 0 ? msg.Num : 1;
|
||||||
|
var enty = new GiftMsg()
|
||||||
|
{
|
||||||
|
MsgId = DateTime.Now.Ticks,
|
||||||
|
User = BiliUserToMsgUser(msg.Uid, msg.Uname),
|
||||||
|
GiftId = msg.GiftId,
|
||||||
|
GiftName = msg.GiftName ?? $"礼物#{msg.GiftId}",
|
||||||
|
GroupId = 0,
|
||||||
|
GiftCount = count,
|
||||||
|
RepeatCount = msg.SuperBatchNum > 0 ? msg.SuperBatchNum : count,
|
||||||
|
DiamondCount = (int)(msg.Price * 1000),
|
||||||
|
Combo = msg.ComboNum > 1,
|
||||||
|
ImgUrl = msg.GifUrl ?? "",
|
||||||
|
Content = $"{msg.Uname ?? "用户"} 送出 {msg.GiftName ?? $"礼物#{msg.GiftId}"} x {count}",
|
||||||
|
RoomId = biliGrab.CurrentRoomId,
|
||||||
|
WebRoomId = biliGrab.CurrentRoomId,
|
||||||
|
Appid = "0"
|
||||||
|
};
|
||||||
|
|
||||||
|
var msgType = PackMsgType.礼物消息;
|
||||||
|
PrintMsg(enty, msgType);
|
||||||
|
var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Bilibili);
|
||||||
|
Broadcast(pack);
|
||||||
|
}
|
||||||
|
|
||||||
|
//B站进房/关注
|
||||||
|
private void BiliGrab_OnInteractMessage(object sender, BiliRoomMessageEventArgs<Modles.BiliProtoEntity.BiliInteractData> e)
|
||||||
|
{
|
||||||
|
var msg = e.Message;
|
||||||
|
// msg_type: 1=进入 2=关注
|
||||||
|
if (msg.MsgType == 2)
|
||||||
|
{
|
||||||
|
var enty = new Msg()
|
||||||
|
{
|
||||||
|
MsgId = DateTime.Now.Ticks,
|
||||||
|
User = BiliUserToMsgUser(msg.Uid, msg.Uname),
|
||||||
|
Content = $"{msg.Uname ?? "用户"} 关注了主播",
|
||||||
|
RoomId = biliGrab.CurrentRoomId,
|
||||||
|
WebRoomId = biliGrab.CurrentRoomId,
|
||||||
|
Appid = "0"
|
||||||
|
};
|
||||||
|
var msgType = PackMsgType.关注消息;
|
||||||
|
PrintMsg(enty, msgType);
|
||||||
|
var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Bilibili);
|
||||||
|
Broadcast(pack);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var enty = new Modles.JsonEntity.MemberMessage()
|
||||||
|
{
|
||||||
|
MsgId = DateTime.Now.Ticks,
|
||||||
|
User = BiliUserToMsgUser(msg.Uid, msg.Uname),
|
||||||
|
Content = $"{msg.Uname ?? "用户"} 来了直播间",
|
||||||
|
CurrentCount = 0,
|
||||||
|
EnterTipType = 0,
|
||||||
|
RoomId = biliGrab.CurrentRoomId,
|
||||||
|
WebRoomId = biliGrab.CurrentRoomId,
|
||||||
|
Appid = "0"
|
||||||
|
};
|
||||||
|
var msgType = PackMsgType.进直播间;
|
||||||
|
PrintMsg(enty, msgType);
|
||||||
|
var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Bilibili);
|
||||||
|
Broadcast(pack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//B站分享
|
||||||
|
private void BiliGrab_OnShareMessage(object sender, BiliRoomMessageEventArgs<Modles.BiliProtoEntity.BiliInteractData> e)
|
||||||
|
{
|
||||||
|
var msg = e.Message;
|
||||||
|
var enty = new ShareMessage()
|
||||||
|
{
|
||||||
|
MsgId = DateTime.Now.Ticks,
|
||||||
|
User = BiliUserToMsgUser(msg.Uid, msg.Uname),
|
||||||
|
Content = $"{msg.Uname ?? "用户"} 分享了直播间",
|
||||||
|
ShareType = ShareType.未知,
|
||||||
|
RoomId = biliGrab.CurrentRoomId,
|
||||||
|
WebRoomId = biliGrab.CurrentRoomId,
|
||||||
|
Appid = "0"
|
||||||
|
};
|
||||||
|
|
||||||
|
var msgType = PackMsgType.直播间分享;
|
||||||
|
PrintMsg(enty, msgType);
|
||||||
|
var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Bilibili);
|
||||||
|
Broadcast(pack);
|
||||||
|
}
|
||||||
|
|
||||||
|
//B站房间统计
|
||||||
|
private void BiliGrab_OnRoomStatsMessage(object sender, BiliRoomMessageEventArgs<Modles.BiliProtoEntity.BiliRoomStats> e)
|
||||||
|
{
|
||||||
|
var msg = e.Message;
|
||||||
|
var enty = new UserSeqMsg()
|
||||||
|
{
|
||||||
|
MsgId = DateTime.Now.Ticks,
|
||||||
|
Content = $"当前直播间人数 {msg.Online},看过 {msg.Watched}",
|
||||||
|
OnlineUserCount = msg.Online,
|
||||||
|
TotalUserCount = msg.Watched,
|
||||||
|
OnlineUserCountStr = msg.Online.ToString(),
|
||||||
|
TotalUserCountStr = msg.Watched.ToString(),
|
||||||
|
RoomId = biliGrab.CurrentRoomId,
|
||||||
|
WebRoomId = biliGrab.CurrentRoomId,
|
||||||
|
Appid = "0"
|
||||||
|
};
|
||||||
|
|
||||||
|
var msgType = PackMsgType.直播间统计;
|
||||||
|
PrintMsg(enty, msgType);
|
||||||
|
var pack = new BarrageMsgPack(enty.ToJson(), msgType, e.Process, LivePlatform.Bilibili);
|
||||||
|
Broadcast(pack);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
//监听用户连接
|
//监听用户连接
|
||||||
private void Listen(IWebSocketConnection socket)
|
private void Listen(IWebSocketConnection socket)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using System.IO.Compression;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using BarrageGrab.Modles.JsonEntity;
|
||||||
using BarrageGrab.Modles.ProtoEntity;
|
using BarrageGrab.Modles.ProtoEntity;
|
||||||
using BarrageGrab.Proxy;
|
using BarrageGrab.Proxy;
|
||||||
using BarrageGrab.Proxy.ProxyEventArgs;
|
using BarrageGrab.Proxy.ProxyEventArgs;
|
||||||
@@ -24,7 +25,7 @@ namespace BarrageGrab
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 进入直播间
|
/// 进入直播间
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event EventHandler<RoomMessageEventArgs<MemberMessage>> OnMemberMessage;
|
public event EventHandler<RoomMessageEventArgs<Modles.ProtoEntity.MemberMessage>> OnMemberMessage;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 关注
|
/// 关注
|
||||||
@@ -103,8 +104,10 @@ namespace BarrageGrab
|
|||||||
}
|
}
|
||||||
|
|
||||||
//ws数据处理
|
//ws数据处理
|
||||||
|
private static int douyinErrCount = 0;
|
||||||
private void Proxy_OnWebSocketData(object sender, WsMessageEventArgs e)
|
private void Proxy_OnWebSocketData(object sender, WsMessageEventArgs e)
|
||||||
{
|
{
|
||||||
|
if (e.Platform != LivePlatform.Douyin) return;
|
||||||
if (!appsetting.ProcessFilter.Contains(e.ProcessName)) return;
|
if (!appsetting.ProcessFilter.Contains(e.ProcessName)) return;
|
||||||
var buff = e.Payload;
|
var buff = e.Payload;
|
||||||
if (buff.Length == 0) return;
|
if (buff.Length == 0) return;
|
||||||
@@ -129,7 +132,17 @@ namespace BarrageGrab
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, $"处理弹幕数据包时出错:{ex.Message}");
|
// 限流:同类错误最多打印3次,防止刷屏和连锁崩溃
|
||||||
|
if (douyinErrCount < 3)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, $"处理抖音弹幕数据包时出错:{ex.Message}");
|
||||||
|
douyinErrCount++;
|
||||||
|
}
|
||||||
|
if (douyinErrCount == 3)
|
||||||
|
{
|
||||||
|
Logger.LogWarn("抖音弹幕解析持续失败,已抑制后续错误日志(不影响快手弹幕)");
|
||||||
|
douyinErrCount++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,8 +196,8 @@ namespace BarrageGrab
|
|||||||
//来了
|
//来了
|
||||||
case "WebcastMemberMessage":
|
case "WebcastMemberMessage":
|
||||||
{
|
{
|
||||||
var arg = Serializer.Deserialize<MemberMessage>(new ReadOnlyMemory<byte>(msg.Payload));
|
var arg = Serializer.Deserialize<Modles.ProtoEntity.MemberMessage>(new ReadOnlyMemory<byte>(msg.Payload));
|
||||||
this.OnMemberMessage?.Invoke(this, new RoomMessageEventArgs<MemberMessage>(processName, arg));
|
this.OnMemberMessage?.Invoke(this, new RoomMessageEventArgs<Modles.ProtoEntity.MemberMessage>(processName, arg));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
//关注
|
//关注
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BarrageGrab.Modles;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace BarrageGrab
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// B站直播API帮助类
|
||||||
|
/// </summary>
|
||||||
|
internal class BiliApiHelper
|
||||||
|
{
|
||||||
|
private const string UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过B站官方API获取房间信息 (推荐方式,稳定可靠)
|
||||||
|
/// 流程: get_info API 获取房间基本信息 -> Master/info API 获取主播昵称/头像
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<BiliRoomInfo> GetRoomInfoByApi(string webRoomId, string cookie = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(webRoomId)) return null;
|
||||||
|
|
||||||
|
var client = CreateHttpClient(cookie);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 1. 获取房间信息 (get_info 支持短ID/长ID自动转换)
|
||||||
|
string roomInfoUrl = $"https://api.live.bilibili.com/room/v1/Room/get_info?room_id={webRoomId}";
|
||||||
|
var roomResp = await client.GetStringAsync(roomInfoUrl);
|
||||||
|
var roomJson = JObject.Parse(roomResp);
|
||||||
|
|
||||||
|
if (roomJson["code"]?.Value<int>() != 0)
|
||||||
|
{
|
||||||
|
Logger.LogWarn($"B站 get_info API 返回错误: {roomJson["message"]?.Value<string>()}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = roomJson["data"];
|
||||||
|
var info = new BiliRoomInfo
|
||||||
|
{
|
||||||
|
WebRoomId = webRoomId,
|
||||||
|
RoomId = data["room_id"]?.Value<long>().ToString() ?? webRoomId,
|
||||||
|
ShortId = data["short_id"]?.Value<long>().ToString() ?? "0",
|
||||||
|
Uid = data["uid"]?.Value<long>().ToString() ?? "",
|
||||||
|
Title = data["title"]?.Value<string>() ?? "",
|
||||||
|
IsLive = (data["live_status"]?.Value<int>() ?? 0) == 1,
|
||||||
|
Online = data["online"]?.Value<long>() ?? 0,
|
||||||
|
AreaName = data["area_name"]?.Value<string>() ?? "",
|
||||||
|
ParentAreaName = data["parent_area_name"]?.Value<string>() ?? "",
|
||||||
|
CoverUrl = data["user_cover"]?.Value<string>()
|
||||||
|
?? data["keyframe"]?.Value<string>()
|
||||||
|
?? ""
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. 获取主播昵称和头像
|
||||||
|
if (!string.IsNullOrWhiteSpace(info.Uid))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string masterUrl = $"https://api.live.bilibili.com/live_user/v1/Master/info?uid={info.Uid}";
|
||||||
|
var masterResp = await client.GetStringAsync(masterUrl);
|
||||||
|
var masterJson = JObject.Parse(masterResp);
|
||||||
|
|
||||||
|
if (masterJson["code"]?.Value<int>() == 0)
|
||||||
|
{
|
||||||
|
var masterData = masterJson["data"];
|
||||||
|
info.Nickname = masterData["info"]?["uname"]?.Value<string>() ?? "";
|
||||||
|
info.HeadUrl = masterData["info"]?["face"]?.Value<string>() ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarn($"获取B站主播信息失败: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(info.Nickname))
|
||||||
|
{
|
||||||
|
info.Nickname = $"B站用户{info.Uid}";
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogInfo($"B站API获取房间信息成功: WebRoomId={webRoomId}, RoomId={info.RoomId}, Uname={info.Nickname}, Online={info.Online}, IsLive={info.IsLive}");
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, $"B站API获取房间信息失败: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从B站直播间页面HTML中解析房间信息 (备用方案)
|
||||||
|
/// 注意: B站前端改版后页面可能不再注入 __INITIAL_STATE__, 此方法可能失效
|
||||||
|
/// </summary>
|
||||||
|
public static BiliRoomInfo ParseRoomInfoFromHtml(string html, string webRoomId)
|
||||||
|
{
|
||||||
|
var info = new BiliRoomInfo { WebRoomId = webRoomId };
|
||||||
|
if (string.IsNullOrWhiteSpace(html)) return info;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 用括号配对算法可靠提取 __INITIAL_STATE__ 的 JSON
|
||||||
|
string jsonStr = ExtractJsonObject(html, "__INITIAL_STATE__");
|
||||||
|
if (string.IsNullOrWhiteSpace(jsonStr))
|
||||||
|
{
|
||||||
|
// B站改版后可能不再注入 __INITIAL_STATE__, 返回空对象由调用方走API
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
JObject root;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
root = JObject.Parse(jsonStr);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarn($"B站 __INITIAL_STATE__ JSON 解析失败: {ex.Message}");
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
var roomInfoNode = root["roomInfo"]?["roomInfo"];
|
||||||
|
var anchorInfoNode = root["roomInfo"]?["anchorInfo"];
|
||||||
|
|
||||||
|
if (roomInfoNode != null)
|
||||||
|
{
|
||||||
|
info.RoomId = TokenToString(roomInfoNode["room_id"], webRoomId);
|
||||||
|
info.ShortId = TokenToString(roomInfoNode["short_id"], "0");
|
||||||
|
info.Uid = TokenToString(roomInfoNode["uid"], "");
|
||||||
|
info.Title = roomInfoNode["title"]?.Value<string>() ?? "";
|
||||||
|
info.IsLive = (roomInfoNode["live_id"]?.Value<long>() ?? 0) != 0;
|
||||||
|
info.Online = roomInfoNode["online"]?.Value<long>() ?? 0;
|
||||||
|
info.AreaName = roomInfoNode["area_name"]?.Value<string>() ?? "";
|
||||||
|
info.ParentAreaName = roomInfoNode["parent_area_name"]?.Value<string>() ?? "";
|
||||||
|
info.CoverUrl = roomInfoNode["cover"]?.Value<string>()
|
||||||
|
?? roomInfoNode["keyframe"]?.Value<string>()
|
||||||
|
?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anchorInfoNode != null)
|
||||||
|
{
|
||||||
|
var baseInfo = anchorInfoNode["base_info"];
|
||||||
|
info.Nickname = baseInfo?["uname"]?.Value<string>()
|
||||||
|
?? anchorInfoNode["uname"]?.Value<string>()
|
||||||
|
?? "";
|
||||||
|
info.HeadUrl = baseInfo?["face"]?.Value<string>()
|
||||||
|
?? anchorInfoNode["face"]?.Value<string>()
|
||||||
|
?? "";
|
||||||
|
|
||||||
|
var relationInfo = anchorInfoNode["relation_info"];
|
||||||
|
if (relationInfo != null)
|
||||||
|
{
|
||||||
|
info.Attention = relationInfo["attribute"]?.Value<int>() ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.LogInfo($"B站HTML解析房间成功: WebRoomId={webRoomId}, RoomId={info.RoomId}, Uname={info.Nickname}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarn($"解析B站页面房间信息时出错: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从HTML中提取 __INITIAL_STATE__ 后的完整JSON对象 (括号配对算法)
|
||||||
|
/// 不受 < 字符或 ;(function 等干扰, 比正则更可靠
|
||||||
|
/// </summary>
|
||||||
|
private static string ExtractJsonObject(string html, string marker)
|
||||||
|
{
|
||||||
|
int idx = html.IndexOf(marker);
|
||||||
|
if (idx < 0) return null;
|
||||||
|
|
||||||
|
// 找到 = 后面的第一个 {
|
||||||
|
int eqIdx = html.IndexOf('=', idx);
|
||||||
|
if (eqIdx < 0) return null;
|
||||||
|
|
||||||
|
int braceIdx = html.IndexOf('{', eqIdx);
|
||||||
|
if (braceIdx < 0) return null;
|
||||||
|
|
||||||
|
// 逐字符扫描计算 { } 嵌套层级, 同时处理字符串和转义
|
||||||
|
int depth = 0;
|
||||||
|
bool inString = false;
|
||||||
|
bool escape = false;
|
||||||
|
|
||||||
|
for (int i = braceIdx; i < html.Length; i++)
|
||||||
|
{
|
||||||
|
char c = html[i];
|
||||||
|
|
||||||
|
if (escape) { escape = false; continue; }
|
||||||
|
if (c == '\\') { escape = true; continue; }
|
||||||
|
if (c == '"') { inString = !inString; continue; }
|
||||||
|
if (inString) continue;
|
||||||
|
|
||||||
|
if (c == '{') depth++;
|
||||||
|
else if (c == '}')
|
||||||
|
{
|
||||||
|
depth--;
|
||||||
|
if (depth == 0)
|
||||||
|
{
|
||||||
|
return html.Substring(braceIdx, i - braceIdx + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>JSON token 转字符串 (兼容整数/字符串类型)</summary>
|
||||||
|
private static string TokenToString(JToken token, string defaultValue)
|
||||||
|
{
|
||||||
|
if (token == null) return defaultValue;
|
||||||
|
return token.Type == JTokenType.Integer
|
||||||
|
? token.Value<long>().ToString()
|
||||||
|
: token.Value<string>() ?? defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HttpClient CreateHttpClient(string cookie)
|
||||||
|
{
|
||||||
|
var client = new HttpClient();
|
||||||
|
client.DefaultRequestHeaders.Add("User-Agent", UA);
|
||||||
|
client.DefaultRequestHeaders.Add("Accept", "application/json, text/plain, */*");
|
||||||
|
client.DefaultRequestHeaders.Add("Accept-Language", "zh-CN,zh;q=0.9");
|
||||||
|
client.DefaultRequestHeaders.Add("Referer", "https://live.bilibili.com/");
|
||||||
|
if (!string.IsNullOrWhiteSpace(cookie))
|
||||||
|
{
|
||||||
|
client.DefaultRequestHeaders.Add("Cookie", cookie);
|
||||||
|
}
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// B站房间信息
|
||||||
|
/// </summary>
|
||||||
|
public class BiliRoomInfo
|
||||||
|
{
|
||||||
|
/// <summary>用户访问的房间号 (URL中的ID, 可能是短ID或真实room_id)</summary>
|
||||||
|
public string WebRoomId { get; set; }
|
||||||
|
/// <summary>真实的房间长ID</summary>
|
||||||
|
public string RoomId { get; set; }
|
||||||
|
/// <summary>短ID (无短ID时为"0")</summary>
|
||||||
|
public string ShortId { get; set; }
|
||||||
|
/// <summary>主播UID</summary>
|
||||||
|
public string Uid { get; set; }
|
||||||
|
/// <summary>直播间标题</summary>
|
||||||
|
public string Title { get; set; }
|
||||||
|
/// <summary>主播昵称</summary>
|
||||||
|
public string Nickname { get; set; }
|
||||||
|
/// <summary>主播头像</summary>
|
||||||
|
public string HeadUrl { get; set; }
|
||||||
|
/// <summary>直播间封面</summary>
|
||||||
|
public string CoverUrl { get; set; }
|
||||||
|
/// <summary>是否直播中</summary>
|
||||||
|
public bool IsLive { get; set; }
|
||||||
|
/// <summary>在线人数</summary>
|
||||||
|
public long Online { get; set; }
|
||||||
|
/// <summary>子分区名</summary>
|
||||||
|
public string AreaName { get; set; }
|
||||||
|
/// <summary>父分区名</summary>
|
||||||
|
public string ParentAreaName { get; set; }
|
||||||
|
/// <summary>关注状态 (0未关注 2已关注 6已互关)</summary>
|
||||||
|
public int Attention { get; set; }
|
||||||
|
/// <summary>直播间地址</summary>
|
||||||
|
public string LiveUrl { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BarrageGrab.Modles;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace BarrageGrab
|
||||||
|
{
|
||||||
|
internal class KsApiHelper
|
||||||
|
{
|
||||||
|
public static async Task<KsRoomInfo> GetRoomInfo(string userId, string cookie = null)
|
||||||
|
{
|
||||||
|
string url = $"https://live.kuaishou.com/u/{userId}";
|
||||||
|
|
||||||
|
var client = new HttpClient();
|
||||||
|
client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0");
|
||||||
|
client.DefaultRequestHeaders.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
|
||||||
|
client.DefaultRequestHeaders.Add("Accept-Language", "zh-CN,zh;q=0.9");
|
||||||
|
if (!string.IsNullOrWhiteSpace(cookie))
|
||||||
|
{
|
||||||
|
client.DefaultRequestHeaders.Add("Cookie", cookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await client.GetStringAsync(url);
|
||||||
|
return ParseRoomInfoFromHtml(response, userId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError($"获取快手房间信息失败: {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static KsRoomInfo ParseRoomInfoFromHtml(string html, string userId)
|
||||||
|
{
|
||||||
|
var info = new KsRoomInfo { UserId = userId };
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 2026-05 快手页面结构变更: __INITIAL_STATE__ JSON 中的字段名和旧版本不同
|
||||||
|
// liveStreamId → 嵌套在 "liveStream":{"id":"xxx"}
|
||||||
|
var liveStreamIdReg = new Regex(@"""liveStream""\s*:\s*\{[^}]*?""id""\s*:\s*""([^""]+)""");
|
||||||
|
var liveStreamMatch = liveStreamIdReg.Match(html);
|
||||||
|
if (liveStreamMatch.Success)
|
||||||
|
{
|
||||||
|
info.LiveStreamId = liveStreamMatch.Groups[1].Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直播封面 → liveStream.poster
|
||||||
|
var posterReg = new Regex(@"""liveStream""\s*:\s*\{[^}]*?""poster""\s*:\s*""([^""]+)""");
|
||||||
|
var posterMatch = posterReg.Match(html);
|
||||||
|
if (posterMatch.Success)
|
||||||
|
{
|
||||||
|
info.CoverUrl = UnescapeJson(posterMatch.Groups[1].Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// token
|
||||||
|
var tokenReg = new Regex(@"""token""\s*:\s*""([^""]*)""");
|
||||||
|
var tokenMatch = tokenReg.Match(html);
|
||||||
|
if (tokenMatch.Success && !string.IsNullOrWhiteSpace(tokenMatch.Groups[1].Value))
|
||||||
|
{
|
||||||
|
info.Token = tokenMatch.Groups[1].Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主播昵称 → 嵌套在 "author":{"...","name":"xxx"}
|
||||||
|
var nickReg = new Regex(@"""author""\s*:\s*\{[^}]*?""name""\s*:\s*""([^""]+)""");
|
||||||
|
var nickMatch = nickReg.Match(html);
|
||||||
|
if (nickMatch.Success)
|
||||||
|
{
|
||||||
|
info.Nickname = nickMatch.Groups[1].Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主播头像 → author.avatar
|
||||||
|
var avatarReg = new Regex(@"""author""\s*:\s*\{[^}]*?""avatar""\s*:\s*""([^""]+)""");
|
||||||
|
var avatarMatch = avatarReg.Match(html);
|
||||||
|
if (avatarMatch.Success)
|
||||||
|
{
|
||||||
|
info.HeadUrl = UnescapeJson(avatarMatch.Groups[1].Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 观看人数 → 现在是字符串如 "1万+",不是纯数字
|
||||||
|
var watchingReg = new Regex(@"""watchingCount""\s*:\s*""([^""]+)""");
|
||||||
|
var watchingMatch = watchingReg.Match(html);
|
||||||
|
if (watchingMatch.Success)
|
||||||
|
{
|
||||||
|
info.WatchingCountStr = watchingMatch.Groups[1].Value;
|
||||||
|
info.WatchingCount = ParseCount(info.WatchingCountStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 点赞数 → 字段名已变更为 liked
|
||||||
|
var likeReg = new Regex(@"""liked""\s*:\s*""([^""]+)""");
|
||||||
|
var likeMatch = likeReg.Match(html);
|
||||||
|
if (likeMatch.Success)
|
||||||
|
{
|
||||||
|
info.LikeCountStr = likeMatch.Groups[1].Value;
|
||||||
|
info.LikeCount = ParseCount(info.LikeCountStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直播状态 → 现在是 isLiving 布尔值
|
||||||
|
var liveReg = new Regex(@"""isLiving""\s*:\s*(true|false)");
|
||||||
|
var liveMatch = liveReg.Match(html);
|
||||||
|
if (liveMatch.Success)
|
||||||
|
{
|
||||||
|
info.IsLive = liveMatch.Groups[1].Value == "true";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarn($"解析快手页面房间信息时出错: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将 "1万+" "2853" "6.7w" 等转为 long
|
||||||
|
private static long ParseCount(string str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(str)) return 0;
|
||||||
|
str = str.Trim();
|
||||||
|
// 含"万" → 乘以10000
|
||||||
|
if (str.Contains("万"))
|
||||||
|
{
|
||||||
|
var numStr = Regex.Replace(str, @"[^\d\.]", "");
|
||||||
|
if (double.TryParse(numStr, out var d)) return (long)(d * 10000);
|
||||||
|
}
|
||||||
|
// 含"w" → 乘以10000
|
||||||
|
if (str.ToLower().Contains("w"))
|
||||||
|
{
|
||||||
|
var numStr = Regex.Replace(str, @"[^\d\.]", "");
|
||||||
|
if (double.TryParse(numStr, out var d)) return (long)(d * 10000);
|
||||||
|
}
|
||||||
|
// 纯数字
|
||||||
|
if (long.TryParse(str, out var l)) return l;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 反转义 JSON Unicode 转义序列 (\\u002F → /)
|
||||||
|
private static string UnescapeJson(string str)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(str)) return str;
|
||||||
|
return Regex.Replace(str, @"\\u([0-9a-fA-F]{4})", m =>
|
||||||
|
{
|
||||||
|
int code = int.Parse(m.Groups[1].Value, System.Globalization.NumberStyles.HexNumber);
|
||||||
|
return ((char)code).ToString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class KsRoomInfo
|
||||||
|
{
|
||||||
|
public string UserId { get; set; }
|
||||||
|
public string LiveStreamId { get; set; }
|
||||||
|
public string Token { get; set; }
|
||||||
|
public string Nickname { get; set; }
|
||||||
|
public long WatchingCount { get; set; }
|
||||||
|
public long LikeCount { get; set; }
|
||||||
|
public bool IsLive { get; set; }
|
||||||
|
public string PageId { get; set; } = Guid.NewGuid().ToString("N").Substring(0, 16);
|
||||||
|
public string LiveUrl { get; set; }
|
||||||
|
// 字符串格式的原始值(用于展示 "1万+" 等)
|
||||||
|
public string WatchingCountStr { get; set; }
|
||||||
|
public string LikeCountStr { get; set; }
|
||||||
|
// 头像和封面
|
||||||
|
public string HeadUrl { get; set; }
|
||||||
|
public string CoverUrl { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace BarrageGrab
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 快手礼物ID→名称映射表,通过代理嗅探自动收集
|
||||||
|
/// </summary>
|
||||||
|
public static class KsGiftHelper
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<uint, string> GiftNames = new ConcurrentDictionary<uint, string>();
|
||||||
|
private static readonly string GiftCachePath;
|
||||||
|
|
||||||
|
static KsGiftHelper()
|
||||||
|
{
|
||||||
|
GiftCachePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ks_gifts.json");
|
||||||
|
LoadMapping();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取礼物名称,找不到返回 null
|
||||||
|
/// </summary>
|
||||||
|
public static string GetGiftName(uint giftId)
|
||||||
|
{
|
||||||
|
GiftNames.TryGetValue(giftId, out var name);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 尝试从任意 JSON 字符串中解析礼物映射
|
||||||
|
/// 支持多种快手 API 返回格式:
|
||||||
|
/// - giftList: [{id:1, name:"小红花"}, ...]
|
||||||
|
/// - giftPanelList: [{id:1, giftName:"小红花"}, ...]
|
||||||
|
/// - allGifts: {"1": {name:"小红花"}, ...}
|
||||||
|
/// 返回是否解析到了新数据
|
||||||
|
/// </summary>
|
||||||
|
public static bool TryParseGiftData(string json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(json)) return false;
|
||||||
|
json = json.Trim();
|
||||||
|
if (json.Length < 10) return false;
|
||||||
|
|
||||||
|
bool added = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 如果 JSON 以数组开头,需要特殊处理
|
||||||
|
if (json.StartsWith("["))
|
||||||
|
{
|
||||||
|
var arr = JArray.Parse(json);
|
||||||
|
added = ParseGiftArray(arr) || added;
|
||||||
|
return added;
|
||||||
|
}
|
||||||
|
|
||||||
|
var obj = JObject.Parse(json);
|
||||||
|
|
||||||
|
// 方式1: giftList → [{id, name}] 或 giftList → [{id, giftName}]
|
||||||
|
var giftList = obj["giftList"] as JArray;
|
||||||
|
if (giftList != null)
|
||||||
|
{
|
||||||
|
added = ParseGiftArray(giftList) || added;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方式2: giftPanelList → [{id, name}]
|
||||||
|
var giftPanelList = obj["giftPanelList"] as JArray;
|
||||||
|
if (giftPanelList != null)
|
||||||
|
{
|
||||||
|
added = ParseGiftArray(giftPanelList) || added;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方式3: allGifts → {"1": {name:"xxx"}, "2": {...}}
|
||||||
|
var allGifts = obj["allGifts"] as JObject;
|
||||||
|
if (allGifts != null)
|
||||||
|
{
|
||||||
|
foreach (var kv in allGifts)
|
||||||
|
{
|
||||||
|
if (uint.TryParse(kv.Key, out var gid))
|
||||||
|
{
|
||||||
|
var name = kv.Value?["name"]?.Value<string>()
|
||||||
|
?? kv.Value?["giftName"]?.Value<string>()
|
||||||
|
?? kv.Value?["title"]?.Value<string>();
|
||||||
|
if (!string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
added = UpdateMapping(gid, name) || added;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方式4: data.giftList / data.giftPanelList / result.giftList
|
||||||
|
var data = obj["data"] ?? obj["result"];
|
||||||
|
if (data is JObject dataObj)
|
||||||
|
{
|
||||||
|
var dataGiftList = dataObj["giftList"] as JArray;
|
||||||
|
if (dataGiftList != null)
|
||||||
|
{
|
||||||
|
added = ParseGiftArray(dataGiftList) || added;
|
||||||
|
}
|
||||||
|
var dataGiftPanel = dataObj["giftPanelList"] as JArray;
|
||||||
|
if (dataGiftPanel != null)
|
||||||
|
{
|
||||||
|
added = ParseGiftArray(dataGiftPanel) || added;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 不是有效 JSON,忽略
|
||||||
|
}
|
||||||
|
|
||||||
|
return added;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ParseGiftArray(JArray arr)
|
||||||
|
{
|
||||||
|
bool added = false;
|
||||||
|
foreach (var item in arr)
|
||||||
|
{
|
||||||
|
if (item is JObject giftObj)
|
||||||
|
{
|
||||||
|
var idToken = giftObj["id"] ?? giftObj["giftId"];
|
||||||
|
var nameToken = giftObj["giftName"] ?? giftObj["name"] ?? giftObj["title"] ?? giftObj["displayName"];
|
||||||
|
|
||||||
|
if (idToken != null && nameToken != null)
|
||||||
|
{
|
||||||
|
uint id = 0;
|
||||||
|
if (idToken.Type == JTokenType.Integer)
|
||||||
|
id = idToken.Value<uint>();
|
||||||
|
else if (idToken.Type == JTokenType.String && uint.TryParse(idToken.Value<string>(), out var sid))
|
||||||
|
id = sid;
|
||||||
|
else
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var name = nameToken.Value<string>();
|
||||||
|
if (!string.IsNullOrWhiteSpace(name) && id > 0)
|
||||||
|
{
|
||||||
|
added = UpdateMapping(id, name) || added;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return added;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool UpdateMapping(uint giftId, string giftName)
|
||||||
|
{
|
||||||
|
if (GiftNames.TryGetValue(giftId, out var existing) && existing == giftName)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
GiftNames[giftId] = giftName;
|
||||||
|
Logger.LogInfo($"KS礼物映射: #{giftId} → {giftName}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存映射到本地文件
|
||||||
|
/// </summary>
|
||||||
|
public static void SaveMapping()
|
||||||
|
{
|
||||||
|
if (GiftNames.Count == 0) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dict = new Dictionary<uint, string>();
|
||||||
|
foreach (var kv in GiftNames) dict[kv.Key] = kv.Value;
|
||||||
|
var json = JsonConvert.SerializeObject(dict, Formatting.Indented);
|
||||||
|
File.WriteAllText(GiftCachePath, json);
|
||||||
|
Logger.LogInfo($"KS礼物映射已保存 ({GiftNames.Count} 个) → {GiftCachePath}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarn($"保存KS礼物映射失败: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从本地文件加载映射
|
||||||
|
/// </summary>
|
||||||
|
private static void LoadMapping()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(GiftCachePath)) return;
|
||||||
|
|
||||||
|
var json = File.ReadAllText(GiftCachePath);
|
||||||
|
var dict = JsonConvert.DeserializeObject<Dictionary<uint, string>>(json);
|
||||||
|
if (dict != null)
|
||||||
|
{
|
||||||
|
foreach (var kv in dict)
|
||||||
|
{
|
||||||
|
GiftNames[kv.Key] = kv.Value;
|
||||||
|
}
|
||||||
|
Logger.LogInfo($"KS礼物映射已加载 ({GiftNames.Count} 个) → {GiftCachePath}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarn($"加载KS礼物映射失败: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前映射数量
|
||||||
|
/// </summary>
|
||||||
|
public static int Count => GiftNames.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
<Import Project="..\packages\Costura.Fody.5.7.0\build\Costura.Fody.props" Condition="Exists('..\packages\Costura.Fody.5.7.0\build\Costura.Fody.props')" />
|
<Import Project="..\packages\Costura.Fody.5.7.0\build\Costura.Fody.props" Condition="Exists('..\packages\Costura.Fody.5.7.0\build\Costura.Fody.props')" />
|
||||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||||
<RootNamespace>BarrageGrab</RootNamespace>
|
<RootNamespace>BarrageGrab</RootNamespace>
|
||||||
<AssemblyName>WssBarrageServer</AssemblyName>
|
<AssemblyName>WssBarrageServer</AssemblyName>
|
||||||
<TargetFrameworkVersion>v4.6.2</TargetFrameworkVersion>
|
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
||||||
<FileAlignment>512</FileAlignment>
|
<FileAlignment>512</FileAlignment>
|
||||||
<Deterministic>true</Deterministic>
|
<Deterministic>true</Deterministic>
|
||||||
<IsWebBootstrapper>false</IsWebBootstrapper>
|
<IsWebBootstrapper>false</IsWebBootstrapper>
|
||||||
@@ -28,9 +28,10 @@
|
|||||||
<UpdatePeriodically>false</UpdatePeriodically>
|
<UpdatePeriodically>false</UpdatePeriodically>
|
||||||
<UpdateRequired>false</UpdateRequired>
|
<UpdateRequired>false</UpdateRequired>
|
||||||
<MapFileExtensions>true</MapFileExtensions>
|
<MapFileExtensions>true</MapFileExtensions>
|
||||||
<ApplicationRevision>0</ApplicationRevision>
|
<ApplicationRevision>1</ApplicationRevision>
|
||||||
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
|
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
|
||||||
<UseApplicationTrust>false</UseApplicationTrust>
|
<UseApplicationTrust>false</UseApplicationTrust>
|
||||||
|
<PublishWizardCompleted>true</PublishWizardCompleted>
|
||||||
<BootstrapperEnabled>true</BootstrapperEnabled>
|
<BootstrapperEnabled>true</BootstrapperEnabled>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
@@ -54,18 +55,18 @@
|
|||||||
<StartupObject />
|
<StartupObject />
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetZone>LocalIntranet</TargetZone>
|
<GenerateManifests>true</GenerateManifests>
|
||||||
</PropertyGroup>
|
|
||||||
<PropertyGroup>
|
|
||||||
<GenerateManifests>false</GenerateManifests>
|
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup />
|
<PropertyGroup />
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<ApplicationManifest>Properties\app.manifest</ApplicationManifest>
|
<!-- ApplicationManifest disabled -->
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<ApplicationIcon>logo.ico</ApplicationIcon>
|
<ApplicationIcon>logo.ico</ApplicationIcon>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
<PropertyGroup>
|
||||||
|
<SignManifests>false</SignManifests>
|
||||||
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="Acornima, Version=1.1.0.0, Culture=neutral, PublicKeyToken=496b38436bb9edeb, processorArchitecture=MSIL">
|
<Reference Include="Acornima, Version=1.1.0.0, Culture=neutral, PublicKeyToken=496b38436bb9edeb, processorArchitecture=MSIL">
|
||||||
<HintPath>..\packages\Acornima.1.1.0\lib\net462\Acornima.dll</HintPath>
|
<HintPath>..\packages\Acornima.1.1.0\lib\net462\Acornima.dll</HintPath>
|
||||||
@@ -233,6 +234,12 @@
|
|||||||
<DependentUpon>FormView.cs</DependentUpon>
|
<DependentUpon>FormView.cs</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="Forms\Models\RoomCacheItem.cs" />
|
<Compile Include="Forms\Models\RoomCacheItem.cs" />
|
||||||
|
<Compile Include="Forms\ConfigEditorForm.cs">
|
||||||
|
<SubType>Form</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Include="Forms\SettingsForm.cs">
|
||||||
|
<SubType>Form</SubType>
|
||||||
|
</Compile>
|
||||||
<Compile Include="Forms\RoomDetail.cs">
|
<Compile Include="Forms\RoomDetail.cs">
|
||||||
<SubType>Form</SubType>
|
<SubType>Form</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
@@ -259,6 +266,13 @@
|
|||||||
<Compile Include="Proxy\TitaniumProxy.cs" />
|
<Compile Include="Proxy\TitaniumProxy.cs" />
|
||||||
<Compile Include="Utility\WinApi.cs" />
|
<Compile Include="Utility\WinApi.cs" />
|
||||||
<Compile Include="Server\WssBarrageGrab.cs" />
|
<Compile Include="Server\WssBarrageGrab.cs" />
|
||||||
|
<Compile Include="Server\KsBarrageGrab.cs" />
|
||||||
|
<Compile Include="Server\BiliBarrageGrab.cs" />
|
||||||
|
<Compile Include="Modles\KsProtoEntity\KsMessages.cs" />
|
||||||
|
<Compile Include="Modles\BiliProtoEntity\BiliMessages.cs" />
|
||||||
|
<Compile Include="Utility\KsApiHelper.cs" />
|
||||||
|
<Compile Include="Utility\KsGiftHelper.cs" />
|
||||||
|
<Compile Include="Utility\BiliApiHelper.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="Server\WsBarrageServer.cs" />
|
<Compile Include="Server\WsBarrageServer.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -267,9 +281,12 @@
|
|||||||
<EmbeddedResource Include="Configs\nlog.config">
|
<EmbeddedResource Include="Configs\nlog.config">
|
||||||
<CustomToolNamespace>WssBarrageService</CustomToolNamespace>
|
<CustomToolNamespace>WssBarrageService</CustomToolNamespace>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
<None Include="AppConfig.json" />
|
<None Include="AppConfig.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
<None Include="packages.config" />
|
<None Include="packages.config" />
|
||||||
<None Include="Properties\app.manifest" />
|
<None Include="Properties\app.manifest" />
|
||||||
|
<None Include="WssBarrageService_TemporaryKey.pfx" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="FodyWeavers.xml" />
|
<Content Include="FodyWeavers.xml" />
|
||||||
@@ -327,4 +344,4 @@
|
|||||||
rmdir /s /q "$(TargetDir)crts"
|
rmdir /s /q "$(TargetDir)crts"
|
||||||
)</PostBuildEvent>
|
)</PostBuildEvent>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"gameExePath": "D:\\UnityProjects\\jiakong\\Fantasy.Map.Simulator\\FantasyMapSimulator.exe",
|
||||||
|
"gameWorkDir": "D:\\UnityProjects\\jiakong\\Fantasy.Map.Simulator",
|
||||||
|
"autoLaunchGame": true,
|
||||||
|
"configDir": "D:\\UnityProjects\\jiakong\\Fantasy.Map.Simulator\\BepInEx\\config",
|
||||||
|
"configFiles": {
|
||||||
|
"effect": "effect_config.txt",
|
||||||
|
"macro": "macro_config.txt",
|
||||||
|
"gift": "gift_config.txt",
|
||||||
|
"command": "command_config.txt",
|
||||||
|
"fkey": "fkey_config.txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user