Repository navigation
Expand file tree
/
Copy pathHostModerationModule.cs
More file actions
393 lines (346 loc) · 14.7 KB
/
Copy pathHostModerationModule.cs
File metadata and controls
393 lines (346 loc) · 14.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
using System.Collections.Generic;
using FishNet;
using FishNet.Broadcast;
using FishNet.Connection;
using FishNet.Transporting;
using UnityEngine;
namespace HTFToolkit
{
/// <summary>
/// 房主管理模块(v2.0)—— Mod 协作协议 + 房主管理页。
///
/// 第一层(协作协议):
/// 客户端通过 FishNet Broadcast 向主机自报 ModFlag(能力位标志);
/// 原版主机未注册该消息处理器 → 静默忽略,零兼容性影响;
/// 装了本 mod 的主机维护"玩家能力表",在菜单反作弊页按玩家/按功能下发禁令;
/// 客户端收到 BanList 后自我禁用对应模块(会话内持久,热键与菜单操作均被拦截)。
/// 第二层(行为校验,防不上报的客户端)为后续版本计划,见 CHANGELOG。
/// </summary>
internal static class HostModerationModule
{
// ---------------- 协议 ----------------
public struct ModFlagMsg : IBroadcast
{
public byte ProtoVer;
public ushort Features;
}
public struct BanListMsg : IBroadcast
{
public ushort BanMask;
}
public const byte ProtoVer = 1;
// 能力位
public const ushort F_Mod = 1 << 0;
public const ushort F_OneHit = 1 << 1;
public const ushort F_Fly = 1 << 2;
public const ushort F_Speed = 1 << 3;
public const ushort F_Damage = 1 << 4;
public const ushort F_Money = 1 << 5;
public const ushort F_All = F_Mod | F_OneHit | F_Fly | F_Speed | F_Damage | F_Money;
// ---------------- 客户端状态 ----------------
private static ushort _myBanMask;
/// <summary>某能力是否被房主禁止(客户端侧检查入口)。</summary>
public static bool IsBanned(ushort featureBit) => (_myBanMask & featureBit) != 0;
private static double _nextHeartbeat;
private static bool _registered;
// ---------------- 主机状态 ----------------
private static readonly Dictionary<NetworkConnection, ushort> _flags = new Dictionary<NetworkConnection, ushort>();
private static readonly Dictionary<NetworkConnection, string> _names = new Dictionary<NetworkConnection, string>();
private static readonly Dictionary<NetworkConnection, ushort> _perBan = new Dictionary<NetworkConnection, ushort>();
private static ushort _globalBan;
public static bool IsRegistered => _registered;
// ---------------- 初始化 / 心跳 ----------------
public static void Init()
{
if (_registered)
return;
try
{
InstanceFinder.ClientManager.RegisterBroadcast<BanListMsg>(OnBanList);
InstanceFinder.ServerManager.RegisterBroadcast<ModFlagMsg>(OnModFlag);
_registered = true;
Plugin.Log?.LogInfo("[Moderation] broadcast channels registered");
}
catch (System.Exception e)
{
Plugin.Log?.LogWarning($"[Moderation] init failed: {e.Message}");
}
}
/// <summary>客户端心跳:定期向主机自报能力位(进房 + 每 12 秒重报,防漏收)。</summary>
public static void Tick()
{
if (!_registered)
return;
double now = Time.realtimeSinceStartupAsDouble;
if (now < _nextHeartbeat)
return;
_nextHeartbeat = now + 12.0;
try
{
if (InstanceFinder.IsHost || InstanceFinder.IsServerStarted)
return; // 主机不自报
if (!InstanceFinder.ClientManager.Started)
return;
if (Player.LocalPlayer == null)
return;
InstanceFinder.ClientManager.Broadcast(new ModFlagMsg { ProtoVer = ProtoVer, Features = F_All });
}
catch
{
// 网络未就绪,下个心跳重试
}
}
// ---------------- 主机:接收 Flag ----------------
private static void OnModFlag(NetworkConnection conn, ModFlagMsg msg, Channel channel)
{
if (conn == null || !conn.IsValid || msg.ProtoVer != ProtoVer)
return;
if (conn.IsLocalClient)
return; // 房主自己
_flags[conn] = msg.Features;
string name = null;
try
{
foreach (var p in PlayerManager.Players)
{
if (p != null && p.Owner != null && p.Owner == conn)
{
name = p.SteamName;
break;
}
}
}
catch
{
name = null;
}
_names[conn] = string.IsNullOrEmpty(name) ? $"玩家#{conn.ClientId}" : name;
Plugin.Log?.LogInfo($"[Moderation] flag from {_names[conn]}: 0x{msg.Features:X4}");
// 该玩家此前被下过单人禁令 → 重新下发(重连/重报场景)
if (_perBan.TryGetValue(conn, out ushort pb) && pb != 0)
SendBanTo(conn);
}
// ---------------- 主机:下发禁令 ----------------
private static void SendBanTo(NetworkConnection conn)
{
try
{
ushort mask = _globalBan;
if (_perBan.TryGetValue(conn, out ushort pb))
mask |= pb;
if (conn != null && conn.IsValid)
InstanceFinder.ServerManager.Broadcast(conn, new BanListMsg { BanMask = mask });
}
catch (System.Exception e)
{
Plugin.Log?.LogWarning($"[Moderation] send ban failed: {e.Message}");
}
}
/// <summary>全局策略切换后:向所有已上报客户端广播。</summary>
private static void PushGlobal()
{
foreach (var conn in _flags.Keys)
SendBanTo(conn);
}
/// <summary>全局禁某能力(反作弊页开关)。</summary>
public static void ToggleGlobalBan(ushort featureBit)
{
_globalBan ^= featureBit;
PushGlobal();
Plugin.Log?.LogInfo($"[Moderation] global ban 0x{featureBit:X4} -> now 0x{_globalBan:X4}");
}
public static bool IsGlobalBanned(ushort featureBit) => (_globalBan & featureBit) != 0;
/// <summary>单人全禁/恢复。</summary>
public static void TogglePerPlayerBan(NetworkConnection conn)
{
if (conn == null)
return;
if (_perBan.TryGetValue(conn, out ushort cur) && cur == F_All)
{
_perBan[conn] = 0;
}
else
{
_perBan[conn] = F_All;
SendBanTo(conn);
}
SendBanTo(conn);
}
public static bool IsPerPlayerBanned(NetworkConnection conn)
=> conn != null && _perBan.TryGetValue(conn, out ushort cur) && cur == F_All;
// ---------------- 客户端:接收禁令 ----------------
private static void OnBanList(BanListMsg msg, Channel channel)
{
_myBanMask = msg.BanMask;
// 已开启的被禁模块:立即关闭并提示
if (IsBanned(F_Fly) && FlyModule.Enabled)
{
FlyModule.Enabled = false;
ToastModule.Show("房主已禁用:飞行", new Color(1f, 0.75f, 0.4f));
}
if (IsBanned(F_Speed) && SpeedModule.Enabled)
{
SpeedModule.Reset();
ToastModule.Show("房主已禁用:移速倍率", new Color(1f, 0.75f, 0.4f));
}
if (IsBanned(F_OneHit) && OneHitKillModule.Enabled)
{
OneHitKillModule.Enabled = false;
ToastModule.Show("房主已禁用:一击必杀", new Color(1f, 0.75f, 0.4f));
}
if (IsBanned(F_Damage) && DamageBoostModule.Enabled)
{
DamageBoostModule.Reset();
ToastModule.Show("房主已禁用:伤害倍率", new Color(1f, 0.75f, 0.4f));
}
}
// ---------------- 主机 UI 数据 ----------------
public static int ClientCount => _flags.Count;
public static IEnumerable<KeyValuePair<NetworkConnection, ushort>> FlaggedClients => _flags;
public static string ClientName(NetworkConnection conn)
=> _names.TryGetValue(conn, out var n) ? n : "未知玩家";
public static bool IsConnected(NetworkConnection conn)
{
try
{
return conn != null && conn.IsValid;
}
catch
{
return false;
}
}
/// <summary>渲染前清理已断开的连接。</summary>
public static void PruneDisconnected()
{
List<NetworkConnection> dead = null;
foreach (var kv in _flags)
{
if (!IsConnected(kv.Key))
{
(dead ??= new List<NetworkConnection>()).Add(kv.Key);
}
}
if (dead != null)
{
foreach (var c in dead)
{
_flags.Remove(c);
_names.Remove(c);
_perBan.Remove(c);
}
}
}
// ---------------- 反作弊页 UI(仅主机)----------------
private static GUIStyle _titleStyle;
private static GUIStyle _rowStyle;
private static GUIStyle _hintStyle;
public static void DrawPanel()
{
bool isHost = false;
try
{
isHost = InstanceFinder.IsHost || InstanceFinder.IsServerStarted;
}
catch
{
isHost = false;
}
if (!isHost)
{
GUILayout.Label("此页面仅主机可用。", UIScale.ButtonStyle);
return;
}
PruneDisconnected();
if (_titleStyle == null)
{
_titleStyle = new GUIStyle(GUI.skin.label)
{
fontStyle = FontStyle.Bold,
richText = true,
wordWrap = false
};
_rowStyle = new GUIStyle(GUI.skin.label) { richText = true };
_hintStyle = new GUIStyle(GUI.skin.label)
{
normal = { textColor = new Color(0.75f, 0.75f, 0.75f) }
};
}
_titleStyle.fontSize = UIScale.TitleFont;
_rowStyle.fontSize = UIScale.TextFont;
_hintStyle.fontSize = UIScale.HintFont;
GUILayout.Space(UIScale.ButtonH * 0.2f);
GUILayout.Label($"<color=#7fd4ff>房主管理</color> <size={UIScale.HintFont}>已上报客户端 {ClientCount} 人</size>", _titleStyle);
GUILayout.Space(UIScale.ButtonH * 0.2f);
// ---- 全局策略(对所有装了本 mod 的客户端生效)----
GUILayout.Label("全局策略", _rowStyle);
GUILayout.Space(UIScale.ButtonH * 0.15f);
DrawBanRow("禁一击必杀", F_OneHit);
DrawBanRow("禁飞行", F_Fly);
DrawBanRow("禁移速倍率", F_Speed);
DrawBanRow("禁伤害倍率", F_Damage);
DrawBanRow("禁金钱修改", F_Money);
GUILayout.Space(UIScale.ButtonH * 0.25f);
// ---- 玩家列表 ----
GUILayout.Label($"玩家列表(已上报 {ClientCount} 人)", _rowStyle);
GUILayout.Space(UIScale.ButtonH * 0.15f);
if (ClientCount == 0)
{
GUILayout.Label("暂无客户端上报。未上报的玩家可能未安装本 mod。", _hintStyle);
}
else
{
foreach (var kv in FlaggedClients)
{
GUILayout.BeginHorizontal();
bool banned = IsPerPlayerBanned(kv.Key);
string name = ClientName(kv.Key);
GUI.color = banned ? new Color(1f, 0.55f, 0.45f) : Color.white;
GUILayout.Label(name, _rowStyle, GUILayout.Width(UIScale.W * 0.34f));
GUI.color = Color.white;
GUI.backgroundColor = banned ? new Color(0.75f, 0.25f, 0.2f) : new Color(0.35f, 0.35f, 0.35f);
if (GUILayout.Button(banned ? "已全禁" : "正常", UIScale.ButtonStyle, GUILayout.Height(UIScale.ButtonH * 0.9f), GUILayout.Width(UIScale.W * 0.14f)))
{
TogglePerPlayerBan(kv.Key);
}
GUI.backgroundColor = new Color(0.35f, 0.35f, 0.35f);
if (GUILayout.Button("踢出房间", UIScale.ButtonStyle, GUILayout.Height(UIScale.ButtonH * 0.9f), GUILayout.Width(UIScale.W * 0.16f)))
{
Kick(kv.Key);
}
GUI.backgroundColor = Color.white;
GUILayout.EndHorizontal();
}
}
GUILayout.FlexibleSpace();
GUILayout.Label("全局策略对所有装了本 mod 的客户端生效;未上报玩家可能未装本 mod。", _hintStyle);
}
private static void DrawBanRow(string name, ushort featureBit)
{
GUILayout.BeginHorizontal();
GUILayout.Label(name, _rowStyle, GUILayout.Width(UIScale.W * 0.26f));
GUI.backgroundColor = IsGlobalBanned(featureBit) ? new Color(0.75f, 0.25f, 0.2f) : new Color(0.2f, 0.7f, 0.2f);
if (GUILayout.Button(IsGlobalBanned(featureBit) ? "已禁止" : "允许", UIScale.ButtonStyle, GUILayout.Height(UIScale.ButtonH * 0.9f), GUILayout.Width(UIScale.W * 0.24f)))
{
ToggleGlobalBan(featureBit);
}
GUI.backgroundColor = Color.white;
GUILayout.EndHorizontal();
}
/// <summary>断开指定客户端的连接(踢出房间)。</summary>
public static void Kick(NetworkConnection conn)
{
try
{
string name = ClientName(conn);
conn.Disconnect(false);
ToastModule.Show($"已踢出: {name}", Color.white);
Plugin.Log?.LogInfo($"[Moderation] kicked {name}");
}
catch (System.Exception e)
{
Plugin.Log?.LogWarning($"[Moderation] kick failed: {e.Message}");
}
}
}
}