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