This commit is contained in:
2026-08-04 00:10:33 +08:00
parent c9343da287
commit 6601f14db6
26 changed files with 4370 additions and 274 deletions
+46 -46
View File
@@ -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>
+4 -2
View File
@@ -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开头的域名程序都会自动列入白名单
+22 -3
View File
@@ -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>
+32
View File
@@ -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
+523
View File
@@ -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);
}
}
}
+38 -37
View File
@@ -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();
+420 -1
View File
@@ -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}";
}
}
} }
} }
+630
View File
@@ -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
View File
@@ -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;
} }
+2 -64
View File
@@ -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>
+6 -1
View File
@@ -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;
} }
} }
+15 -1
View File
@@ -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>
+211 -7
View File
@@ -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)
+405
View File
@@ -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;
}
}
}
+120
View File
@@ -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;
}
}
}
+354 -2
View File
@@ -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)
{ {
+17 -4
View File
@@ -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;
} }
//关注 //关注
+271
View File
@@ -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对象 (括号配对算法)
/// 不受 &lt; 字符或 ;(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; }
}
}
+173
View File
@@ -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; }
}
}
+213
View File
@@ -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;
}
}
+27 -10
View File
@@ -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>
+13
View File
@@ -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"
}
}