Compare commits
62 Commits
413fc0b930
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b5e3c33d2 | ||
|
|
079716cc85 | ||
|
|
04337028b3 | ||
|
|
6b9910486b | ||
|
|
c320a07370 | ||
|
|
a0d1386712 | ||
|
|
ab960fc38b | ||
|
|
a40a605c37 | ||
|
|
b19f906216 | ||
|
|
3ecb7fb78b | ||
|
|
4e8931caba | ||
|
|
3adcdc46ba | ||
|
|
b6edc70594 | ||
| 9e7fb4d443 | |||
| 6ab8c38109 | |||
| 9a5d149241 | |||
|
|
f193d8a805 | ||
|
|
00960eb3f7 | ||
|
|
bc2d44f5cd | ||
|
|
c065b14f9d | ||
|
|
46f05b5489 | ||
|
|
3a9e903044 | ||
|
|
ca331d4ac8 | ||
|
|
692fc25d42 | ||
|
|
bec55e4ecc | ||
|
|
8a36e74c4b | ||
|
|
4b9c831c73 | ||
|
|
910c407e3d | ||
|
|
56caddcb65 | ||
|
|
c8abc7c265 | ||
|
|
1dc30bdabd | ||
|
|
14db933880 | ||
|
|
610cca5ef6 | ||
|
|
d9e01bbf0f | ||
|
|
31cf778524 | ||
|
|
07d2320c71 | ||
|
|
ab0df1ee0d | ||
|
|
74f9e1cf1c | ||
|
|
007c093df4 | ||
|
|
fd208de2db | ||
|
|
40795a2f49 | ||
|
|
e157e24236 | ||
|
|
dfb535e4b7 | ||
|
|
b98ae261d5 | ||
|
|
204fda4f59 | ||
|
|
023602e364 | ||
|
|
dde47af2d8 | ||
|
|
8d7d08b99e | ||
|
|
8262b70292 | ||
|
|
652693883b | ||
|
|
8261d243e6 | ||
|
|
4de0778dc8 | ||
|
|
eb27008e4d | ||
|
|
0b3a12f64d | ||
|
|
f44815c600 | ||
|
|
ca1f7c0637 | ||
|
|
be50401453 | ||
|
|
c6574c53c8 | ||
|
|
9a0b40d3ee | ||
|
|
530036cc30 | ||
|
|
3a2062d808 | ||
|
|
45884570e6 |
51
.gitea/workflows/dotnet.yml
Normal file
51
.gitea/workflows/dotnet.yml
Normal file
@@ -0,0 +1,51 @@
|
||||
name: Publish Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*' # 匹配 v 开头的标签,如 v1.0.0
|
||||
# 或者使用 '*' 匹配所有标签
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: http://MonBianG.lan:3000/action/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup .NET
|
||||
uses: http://MonBianG.lan:3000/action/setup-dotnet@v4.3.0
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
# 直接加代理得了
|
||||
env:
|
||||
http_proxy: "http://172.17.0.1:7890"
|
||||
https_proxy: "http://172.17.0.1:7890"
|
||||
no_proxy: "localhost,127.0.0.1,*.lan"
|
||||
|
||||
- name: Build
|
||||
run: dotnet build --no-restore --configuration Release
|
||||
|
||||
# 使用 Gitea Release Action 创建 Release
|
||||
- name: Create Release
|
||||
uses: http://MonBianG.lan:3000/action/gitea-release-action@main
|
||||
with:
|
||||
# 使用推送的标签名作为版本号
|
||||
tag: ${{ github.ref_name }}
|
||||
# 自动从标签名生成 Release 标题
|
||||
name: Release ${{ github.ref_name }}
|
||||
# 使用提交信息或自定义发布说明
|
||||
body: |
|
||||
## Changes in ${{ github.ref_name }}
|
||||
|
||||
Auto-generated release for tag ${{ github.ref_name }}.
|
||||
# 上传构建产物
|
||||
files: |
|
||||
./bin/Release/net35/stick.plugins.*.dll
|
||||
# 是否为预发布版本(如果是 alpha/beta/rc 标签)
|
||||
prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'rc') }}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace stick.plugins.playermanager;
|
||||
|
||||
public class BlackListRecorder : RecoderBase
|
||||
{
|
||||
protected override string FileName => "BlackList.txt";
|
||||
protected override PlayerRecordStatus RecordStatus => PlayerRecordStatus.Blacklisted;
|
||||
}
|
||||
52
Gui/DefaultWidgets.cs
Normal file
52
Gui/DefaultWidgets.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace stick.plugins.playermanager.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// 默认组件注册入口
|
||||
/// 在 Plugin.Awake() 末尾调用 RegisterAll()
|
||||
/// 配置判断在此集中处理,各组件文件只提供纯 Create() 工厂
|
||||
/// </summary>
|
||||
public static class DefaultWidgets
|
||||
{
|
||||
public static void RegisterAll()
|
||||
{
|
||||
// Header:房间公开性行(label + button 在同一行,各自独立可配置)
|
||||
var publicityWidgets = new List<IHeaderWidget>();
|
||||
if (PlayerManager.ShowRoomPublicityLabel.Value)
|
||||
publicityWidgets.Add(InternalWidgets.RoomPublicityLabel.Create());
|
||||
if (PlayerManager.ShowRoomPublicityButton.Value)
|
||||
publicityWidgets.Add(InternalWidgets.RoomPublicityButton.Create());
|
||||
|
||||
if (publicityWidgets.Count > 0)
|
||||
{
|
||||
RegisterManager.RegisterHeaderRow(new HeaderRow
|
||||
{
|
||||
Name = "RoomPublicity",
|
||||
Condition = () => PlayerManager.IsHost(PlayerManager.LocalPlayer),
|
||||
Children = publicityWidgets,
|
||||
});
|
||||
}
|
||||
|
||||
// 玩家行组件(始终注册)
|
||||
RegisterManager.RegisterPlayerRow(InternalWidgets.PlayerInfoButton.Create());
|
||||
|
||||
// 玩家行组件(按配置决定)
|
||||
if (PlayerManager.ShowRecordButton.Value)
|
||||
RegisterManager.RegisterPlayerRow(InternalWidgets.RecordButton.Create());
|
||||
|
||||
if (PlayerManager.ShowKickButton.Value)
|
||||
RegisterManager.RegisterPlayerRow(InternalWidgets.KickButton.Create());
|
||||
|
||||
if (PlayerManager.ShowNameToggleButton.Value)
|
||||
RegisterManager.RegisterPlayerRow(InternalWidgets.NameToggleButton.Create());
|
||||
|
||||
if (PlayerManager.ShowTransButton.Value)
|
||||
RegisterManager.RegisterPlayerRow(InternalWidgets.TransButton.Create());
|
||||
|
||||
if (PlayerManager.ShowMuteButton.Value)
|
||||
RegisterManager.RegisterPlayerRow(InternalWidgets.MuteButton.Create());
|
||||
if (PlayerManager.ShowAddFriendButton.Value)
|
||||
RegisterManager.RegisterPlayerRow(InternalWidgets.AddFriendButton.Create());
|
||||
}
|
||||
}
|
||||
55
Gui/GUIStyles.cs
Normal file
55
Gui/GUIStyles.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace stick.plugins.playermanager.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// 集中管理所有 GUIStyle 的静态类。
|
||||
/// 必须在 OnGUI 上下文中调用 Initialize() 初始化(依赖 GUI.skin)。
|
||||
/// 所有 GUI 组件通过此类的静态字段获取样式,避免重复创建。
|
||||
/// </summary>
|
||||
public static class GUIStyles
|
||||
{
|
||||
/// <summary>标签样式(fontSize=22)</summary>
|
||||
public static GUIStyle Label;
|
||||
|
||||
/// <summary>普通按钮样式(fontSize=22, richText=false)</summary>
|
||||
public static GUIStyle Button;
|
||||
|
||||
/// <summary>被按过的按钮样式,用于标识已经进入过的房间(fontSize=22)</summary>
|
||||
public static GUIStyle ButtonPressed;
|
||||
|
||||
/// <summary>黄色按钮样式(已记录玩家)</summary>
|
||||
public static GUIStyle YellowButton;
|
||||
|
||||
/// <summary>红色按钮样式(黑名单玩家)</summary>
|
||||
public static GUIStyle RedButton;
|
||||
|
||||
/// <summary>绿色按钮样式(好友玩家)</summary>
|
||||
public static GUIStyle GreenButton;
|
||||
|
||||
private static bool _initialized;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化所有样式。必须在 OnGUI 上下文中调用,允许多次调用(仅首次生效)。
|
||||
/// </summary>
|
||||
public static void Initialize()
|
||||
{
|
||||
if (_initialized) return;
|
||||
|
||||
Label = new GUIStyle(GUI.skin.label) { fontSize = 22 };
|
||||
Button = new GUIStyle(GUI.skin.button) { fontSize = 22, richText = false };
|
||||
ButtonPressed = new GUIStyle(GUI.skin.button) { fontSize = 22, richText = false };
|
||||
ButtonPressed.normal.textColor = Color.grey;
|
||||
|
||||
YellowButton = new GUIStyle(GUI.skin.button) { fontSize = 22, richText = false };
|
||||
YellowButton.normal.textColor = Color.yellow;
|
||||
|
||||
RedButton = new GUIStyle(GUI.skin.button) { fontSize = 22, richText = false };
|
||||
RedButton.normal.textColor = Color.red;
|
||||
|
||||
GreenButton = new GUIStyle(GUI.skin.button) { fontSize = 22, richText = false };
|
||||
GreenButton.normal.textColor = Color.green;
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
}
|
||||
26
Gui/InternalWidgets/AddFriendButton.cs
Normal file
26
Gui/InternalWidgets/AddFriendButton.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using Steamworks;
|
||||
using UnityEngine;
|
||||
using stick.plugins.playermanager.Recorder;
|
||||
|
||||
namespace stick.plugins.playermanager.Gui.InternalWidgets;
|
||||
|
||||
public static class AddFriendButton
|
||||
{
|
||||
public static IPlayerRowWidget Create()
|
||||
{
|
||||
return new PlayerRowButtonWidget
|
||||
{
|
||||
Name = "Add Friend",
|
||||
GetText = player => "加好友",
|
||||
Condition = player =>
|
||||
!PlayerManager.IsLocalPlayer(player)
|
||||
&& !player.IsFriend,
|
||||
OnClick = player =>
|
||||
{
|
||||
GUIUtility.systemCopyBuffer = player.ToString();
|
||||
SteamFriends.ActivateGameOverlayToUser("friendadd", player.SteamId);
|
||||
},
|
||||
Width = 100f,
|
||||
};
|
||||
}
|
||||
}
|
||||
21
Gui/InternalWidgets/KickButton.cs
Normal file
21
Gui/InternalWidgets/KickButton.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using MultiplayerBasicExample;
|
||||
|
||||
namespace stick.plugins.playermanager.Gui.InternalWidgets;
|
||||
|
||||
public static class KickButton
|
||||
{
|
||||
public static IPlayerRowWidget Create()
|
||||
{
|
||||
return new PlayerRowButtonWidget
|
||||
{
|
||||
Name = "Kick",
|
||||
Condition = player =>
|
||||
!PlayerManager.IsHost(player)
|
||||
&& !PlayerManager.IsLocalPlayer(player),
|
||||
GetText = _ => "Kick!",
|
||||
OnClick = player =>
|
||||
PlayerManager.KickPlayer(player.SteamId, MultiplayerManager.KickResponse.DidNotRecievePackages),
|
||||
Width = 100f,
|
||||
};
|
||||
}
|
||||
}
|
||||
29
Gui/InternalWidgets/MuteButton.cs
Normal file
29
Gui/InternalWidgets/MuteButton.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using MultiplayerBasicExample;
|
||||
|
||||
namespace stick.plugins.playermanager.Gui.InternalWidgets;
|
||||
|
||||
public static class MuteButton
|
||||
{
|
||||
public static IPlayerRowWidget Create()
|
||||
{
|
||||
return new PlayerRowButtonWidget
|
||||
{
|
||||
Name = "mute",
|
||||
Condition = player => !PlayerManager.IsLocalPlayer(player),
|
||||
GetText = player =>
|
||||
PlayerManager.IsMuted(player) ? "取消静音" : "静音",
|
||||
OnClick = player =>
|
||||
{
|
||||
if (PlayerManager.IsMuted(player))
|
||||
{
|
||||
PlayerManager.UnmutePlayer(player);
|
||||
}
|
||||
else
|
||||
{
|
||||
PlayerManager.MutePlayer(player);
|
||||
}
|
||||
},
|
||||
Width = 100f,
|
||||
};
|
||||
}
|
||||
}
|
||||
16
Gui/InternalWidgets/NameToggleButton.cs
Normal file
16
Gui/InternalWidgets/NameToggleButton.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace stick.plugins.playermanager.Gui.InternalWidgets;
|
||||
|
||||
public static class NameToggleButton
|
||||
{
|
||||
public static IPlayerRowWidget Create()
|
||||
{
|
||||
return new PlayerRowButtonWidget
|
||||
{
|
||||
Name = "NameToggle",
|
||||
Condition = player => player.HasRecordedName,
|
||||
GetText = player => player.DisplayNameRecorded ? "显示现用名" : "显示曾用名",
|
||||
OnClick = player => player.DisplayNameRecorded = !player.DisplayNameRecorded,
|
||||
Width = 120f,
|
||||
};
|
||||
}
|
||||
}
|
||||
32
Gui/InternalWidgets/PlayerInfoButton.cs
Normal file
32
Gui/InternalWidgets/PlayerInfoButton.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Steamworks;
|
||||
using UnityEngine;
|
||||
using stick.plugins.playermanager.Recorder;
|
||||
|
||||
namespace stick.plugins.playermanager.Gui.InternalWidgets;
|
||||
|
||||
public static class PlayerInfoButton
|
||||
{
|
||||
public static IPlayerRowWidget Create()
|
||||
{
|
||||
return new PlayerRowButtonWidget
|
||||
{
|
||||
Name = "PlayerInfo",
|
||||
GetText = player => player.ToString(),
|
||||
OnClick = player =>
|
||||
{
|
||||
GUIUtility.systemCopyBuffer = player.ToString();
|
||||
SteamFriends.ActivateGameOverlayToUser("steamid", player.SteamId);
|
||||
},
|
||||
Width = 500f,
|
||||
GetStyle = player =>
|
||||
{
|
||||
return player.RecordStatus switch
|
||||
{
|
||||
PlayerRecordStatus.Blacklisted => GUIStyles.RedButton,
|
||||
PlayerRecordStatus.Recorded => GUIStyles.YellowButton,
|
||||
_ => player.IsFriend ? GUIStyles.GreenButton : GUIStyles.Button,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
43
Gui/InternalWidgets/RecordButton.cs
Normal file
43
Gui/InternalWidgets/RecordButton.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using UnityEngine;
|
||||
using stick.plugins.playermanager.Recorder;
|
||||
|
||||
namespace stick.plugins.playermanager.Gui.InternalWidgets;
|
||||
|
||||
public static class RecordButton
|
||||
{
|
||||
public static IPlayerRowWidget Create()
|
||||
{
|
||||
return new PlayerRowButtonWidget
|
||||
{
|
||||
Name = "Record",
|
||||
GetText = player => player.RecordStatus switch
|
||||
{
|
||||
PlayerRecordStatus.NotRecorded => "记录",
|
||||
PlayerRecordStatus.Recorded => "拉黑",
|
||||
PlayerRecordStatus.Blacklisted => "取消",
|
||||
_ => "未知状态",
|
||||
},
|
||||
OnClick = player =>
|
||||
{
|
||||
switch (player.RecordStatus)
|
||||
{
|
||||
case PlayerRecordStatus.NotRecorded:
|
||||
PlayerManager.playerRecorder.RecordPlayer(player);
|
||||
break;
|
||||
case PlayerRecordStatus.Recorded:
|
||||
PlayerManager.blackListRecorder.RecordPlayer(player);
|
||||
break;
|
||||
case PlayerRecordStatus.Blacklisted:
|
||||
PlayerManager.playerRecorder.CancelRecordPlayer(player);
|
||||
PlayerManager.blackListRecorder.CancelRecordPlayer(player);
|
||||
break;
|
||||
default:
|
||||
Debug.LogError($"未知的记录状态: {player.RecordStatus}");
|
||||
return;
|
||||
}
|
||||
PlayerManager.OnLobbyMembersChanged();
|
||||
},
|
||||
Width = 100f,
|
||||
};
|
||||
}
|
||||
}
|
||||
15
Gui/InternalWidgets/RoomPublicityButton.cs
Normal file
15
Gui/InternalWidgets/RoomPublicityButton.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace stick.plugins.playermanager.Gui.InternalWidgets;
|
||||
|
||||
public static class RoomPublicityButton
|
||||
{
|
||||
public static IHeaderWidget Create()
|
||||
{
|
||||
return new HeaderButtonWidget
|
||||
{
|
||||
Name = "RoomPublicityButton",
|
||||
Text = "切换公开性",
|
||||
Action = () => PlayerManager.ToggleLobbyPublicity(),
|
||||
Width = 150f,
|
||||
};
|
||||
}
|
||||
}
|
||||
14
Gui/InternalWidgets/RoomPublicityLabel.cs
Normal file
14
Gui/InternalWidgets/RoomPublicityLabel.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace stick.plugins.playermanager.Gui.InternalWidgets;
|
||||
|
||||
public static class RoomPublicityLabel
|
||||
{
|
||||
public static IHeaderWidget Create()
|
||||
{
|
||||
return new HeaderLabelWidget
|
||||
{
|
||||
Name = "RoomPublicityLabel",
|
||||
Content = () => $"房间类型: {(PlayerManager.IsPublic ? "公开" : "好友可见")}",
|
||||
Width = 200f,
|
||||
};
|
||||
}
|
||||
}
|
||||
21
Gui/InternalWidgets/TransButton.cs
Normal file
21
Gui/InternalWidgets/TransButton.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using MultiplayerBasicExample;
|
||||
|
||||
namespace stick.plugins.playermanager.Gui.InternalWidgets;
|
||||
|
||||
public static class TransButton
|
||||
{
|
||||
public static IPlayerRowWidget Create()
|
||||
{
|
||||
return new PlayerRowButtonWidget
|
||||
{
|
||||
Name = "Trans Host",
|
||||
Condition = player =>
|
||||
PlayerManager.IsHost(PlayerManager.LocalPlayer)
|
||||
&& !PlayerManager.IsLocalPlayer(player),
|
||||
GetText = _ => "转让",
|
||||
OnClick = player =>
|
||||
PlayerManager.TransferLobbyOwnership(player),
|
||||
Width = 100f,
|
||||
};
|
||||
}
|
||||
}
|
||||
86
Gui/LayoutCursor.cs
Normal file
86
Gui/LayoutCursor.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace stick.plugins.playermanager.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// 布局游标,统一管理 GUI 绘制时的行列定位
|
||||
/// Header 区域纵向排列 Row,Row 内横向排列 Widget
|
||||
/// 玩家区域每玩家一行,行内横向排列 Widget
|
||||
/// </summary>
|
||||
public class LayoutCursor
|
||||
{
|
||||
/// <summary>
|
||||
/// 左侧偏移量
|
||||
/// </summary>
|
||||
public float OffsetX { get; set; } = 10f;
|
||||
|
||||
/// <summary>
|
||||
/// 顶部偏移量
|
||||
/// </summary>
|
||||
public float OffsetY { get; set; } = 20f;
|
||||
|
||||
/// <summary>
|
||||
/// 行高
|
||||
/// </summary>
|
||||
public float LineHeight { get; set; } = 50f;
|
||||
|
||||
private float _currentX;
|
||||
private int _currentLine;
|
||||
|
||||
/// <summary>
|
||||
/// 当前行的 Y 坐标
|
||||
/// </summary>
|
||||
public float CurrentY => OffsetY + LineHeight * _currentLine;
|
||||
|
||||
/// <summary>
|
||||
/// 重置游标到初始状态
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
_currentX = OffsetX;
|
||||
_currentLine = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开始新的一行,推进到下一行并重置列位置到 OffsetX
|
||||
/// 对齐原来的 GetNewYPosition() 行为
|
||||
/// </summary>
|
||||
public void BeginRow()
|
||||
{
|
||||
_currentLine++;
|
||||
_currentX = OffsetX;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在当前行获取下一列的 Rect,X 坐标自动推进
|
||||
/// </summary>
|
||||
/// <param name="width">列宽</param>
|
||||
/// <param name="height">行高</param>
|
||||
public Rect NextColumn(float width, float height)
|
||||
{
|
||||
var rect = new Rect(_currentX, CurrentY, width, height);
|
||||
_currentX += width;
|
||||
return rect;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 结束当前行,仅重置列位置(不行进,下一行由 BeginRow 推进)
|
||||
/// </summary>
|
||||
public void EndRow()
|
||||
{
|
||||
_currentX = OffsetX;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 换行并返回一个从 OffsetX 开始的整行 Rect
|
||||
/// 适用于独占整行的标题等
|
||||
/// </summary>
|
||||
/// <param name="width">宽度</param>
|
||||
/// <param name="height">高度</param>
|
||||
public Rect NextLine(float width, float height)
|
||||
{
|
||||
_currentLine++;
|
||||
_currentX = OffsetX;
|
||||
return new Rect(OffsetX, CurrentY, width, height);
|
||||
}
|
||||
}
|
||||
241
Gui/PlayerManagerGUI.cs
Normal file
241
Gui/PlayerManagerGUI.cs
Normal file
@@ -0,0 +1,241 @@
|
||||
using System.Linq;
|
||||
using MultiplayerBasicExample;
|
||||
using Steamworks;
|
||||
using UnityEngine;
|
||||
using stick.plugins.playermanager.Recorder;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace stick.plugins.playermanager.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// 玩家管理器GUI类
|
||||
/// 负责纯绘制:迭代注册的 HeaderRow / PlayerRowWidget,使用 LayoutCursor 定位
|
||||
/// 不包含任何业务逻辑或硬编码按钮
|
||||
/// </summary>
|
||||
public static class PlayerManagerGUI
|
||||
{
|
||||
#region GUI状态
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示大厅信息界面
|
||||
/// </summary>
|
||||
public static bool ShowLobbyInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 滚动位置
|
||||
/// </summary>
|
||||
private static Vector2 ScrollPosition;
|
||||
|
||||
/// <summary>
|
||||
/// 布局游标
|
||||
/// </summary>
|
||||
private static readonly LayoutCursor Cursor = new();
|
||||
|
||||
/// <summary>
|
||||
/// 记录该行widget是否该绘制
|
||||
/// </summary>
|
||||
private static readonly HashSet<IPlayerRowWidget> IgnoreWidgets = [];
|
||||
|
||||
#endregion
|
||||
|
||||
#region 主绘制方法
|
||||
|
||||
/// <summary>
|
||||
/// 绘制大厅信息GUI
|
||||
/// </summary>
|
||||
public static void DrawLobbyInfoGUI()
|
||||
{
|
||||
GUIStyles.Initialize();
|
||||
Cursor.Reset();
|
||||
|
||||
if (PlayerManager.MatchmakingHandler.IsInsideLobby)
|
||||
{
|
||||
DrawCurrentLobbyInfo();
|
||||
DrawLobbySearchInfo();
|
||||
}
|
||||
else
|
||||
{
|
||||
GUI.Label(
|
||||
Cursor.NextLine(500f, 40f),
|
||||
"当前不在大厅中",
|
||||
GUIStyles.Label
|
||||
);
|
||||
DrawLobbySearchInfo();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 大厅信息绘制
|
||||
|
||||
/// <summary>
|
||||
/// 绘制当前大厅信息
|
||||
/// 结构:标题行 → HeaderRow 区域(纵向) → 玩家行区域
|
||||
/// </summary>
|
||||
private static void DrawCurrentLobbyInfo()
|
||||
{
|
||||
// 固定:大厅标题(独占一行)
|
||||
GUI.Label(
|
||||
Cursor.NextLine(2000f, 40f),
|
||||
$"当前大厅: {PlayerManager.CurrentLobbyID} 房主: {PlayerManager.LobbyOwner.CurrentName}",
|
||||
GUIStyles.Label
|
||||
);
|
||||
|
||||
// HeaderRow 区域:纵向迭代每一行,行内横向迭代 Children
|
||||
foreach (var row in RegisterManager.HeaderRows)
|
||||
{
|
||||
if (!row.ShouldDraw()) continue;
|
||||
|
||||
Cursor.BeginRow();
|
||||
foreach (var child in row.Children)
|
||||
{
|
||||
if (child.ShouldDraw())
|
||||
child.Draw(Cursor.NextColumn(child.Width, 40f));
|
||||
}
|
||||
Cursor.EndRow();
|
||||
}
|
||||
|
||||
|
||||
IgnoreWidgets.Clear();
|
||||
// 如果一个widget对所有玩家都不需要绘制,则忽略
|
||||
foreach (var widget in RegisterManager.PlayerRowWidgets)
|
||||
{
|
||||
bool shouldDraw = false;
|
||||
foreach (var member in PlayerManager.lobbyMemberList)
|
||||
{
|
||||
if (!member.SteamId.IsValid()) continue;
|
||||
if (widget.ShouldDraw(member))
|
||||
{
|
||||
shouldDraw = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldDraw)
|
||||
IgnoreWidgets.Add(widget);
|
||||
}
|
||||
|
||||
// 玩家行区域:每玩家一行,行内横向迭代注册的 PlayerRowWidget
|
||||
foreach (var member in PlayerManager.lobbyMemberList)
|
||||
{
|
||||
if (!member.SteamId.IsValid()) continue;
|
||||
|
||||
Cursor.BeginRow();
|
||||
foreach (var widget in RegisterManager.PlayerRowWidgets)
|
||||
{
|
||||
if (widget.ShouldDraw(member))
|
||||
widget.Draw(Cursor.NextColumn(widget.Width, 40f), member);
|
||||
else
|
||||
if (!IgnoreWidgets.Contains(widget))
|
||||
Cursor.NextColumn(widget.Width, 40f); // 占位推进列位置对齐
|
||||
}
|
||||
Cursor.EndRow();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 大厅搜索绘制
|
||||
|
||||
/// <summary>
|
||||
/// 绘制大厅搜索信息(不在大厅时显示)
|
||||
/// </summary>
|
||||
private static void DrawLobbySearchInfo()
|
||||
{
|
||||
// 搜索范围按钮
|
||||
string areaText = GetAreaFilterText();
|
||||
if (GUI.Button(
|
||||
Cursor.NextLine(400f, 40f),
|
||||
$"搜索范围 {areaText}",
|
||||
GUIStyles.Button
|
||||
))
|
||||
{
|
||||
CycleLobbyDistanceFilter();
|
||||
PlayerManager.RequestLobbyList();
|
||||
}
|
||||
|
||||
// 刷新按钮(同一行)
|
||||
if (GUI.Button(
|
||||
new Rect(Cursor.OffsetX + 420f, Cursor.CurrentY, 100f, 40f),
|
||||
"刷新",
|
||||
GUIStyles.Button
|
||||
))
|
||||
{
|
||||
PlayerManager.RequestLobbyList();
|
||||
}
|
||||
|
||||
// 搜索结果数量
|
||||
GUI.Label(
|
||||
Cursor.NextLine(500f, 40f),
|
||||
$"搜索到大厅数量: {PlayerManager.lobbyDataList.Count}",
|
||||
GUIStyles.Label
|
||||
);
|
||||
|
||||
// 大厅列表滚动视图
|
||||
DrawLobbyListScrollView();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取区域过滤文本
|
||||
/// </summary>
|
||||
private static string GetAreaFilterText()
|
||||
{
|
||||
return PlayerManager.lobbyDistanceFilter switch
|
||||
{
|
||||
ELobbyDistanceFilter.k_ELobbyDistanceFilterClose => "当前地区",
|
||||
ELobbyDistanceFilter.k_ELobbyDistanceFilterDefault => "当前及附近地区",
|
||||
ELobbyDistanceFilter.k_ELobbyDistanceFilterFar => "半球范围内",
|
||||
ELobbyDistanceFilter.k_ELobbyDistanceFilterWorldwide => "世界",
|
||||
_ => "未知"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 循环切换大厅距离过滤
|
||||
/// </summary>
|
||||
private static void CycleLobbyDistanceFilter()
|
||||
{
|
||||
PlayerManager.lobbyDistanceFilter = PlayerManager.lobbyDistanceFilter switch
|
||||
{
|
||||
ELobbyDistanceFilter.k_ELobbyDistanceFilterWorldwide => ELobbyDistanceFilter.k_ELobbyDistanceFilterClose,
|
||||
_ => PlayerManager.lobbyDistanceFilter + 1
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绘制大厅列表滚动视图
|
||||
/// </summary>
|
||||
private static void DrawLobbyListScrollView()
|
||||
{
|
||||
ScrollPosition = GUI.BeginScrollView(
|
||||
new Rect(Cursor.OffsetX, Cursor.CurrentY + 40f, 525f, 405f),
|
||||
ScrollPosition,
|
||||
new Rect(0f, 0f, 505f, PlayerManager.lobbyDataList.Count * 50f)
|
||||
);
|
||||
|
||||
for (int i = 0; i < PlayerManager.lobbyDataList.Count; i++)
|
||||
{
|
||||
CSteamID lobbyId = PlayerManager.lobbyDataList[i];
|
||||
var style = PlayerManager.enteredLobbyList.Contains(lobbyId) ? GUIStyles.ButtonPressed : GUIStyles.Button;
|
||||
|
||||
if (GUI.Button(
|
||||
new Rect(Cursor.OffsetX, Cursor.LineHeight * i, 500f, 40f),
|
||||
$"{lobbyId} 人数{SteamMatchmaking.GetNumLobbyMembers(lobbyId)}",
|
||||
style
|
||||
))
|
||||
{
|
||||
// 在大厅中则不加入
|
||||
if (!PlayerManager.MatchmakingHandler.IsInsideLobby)
|
||||
{
|
||||
// 记录一下加入过的状态
|
||||
PlayerManager.enteredLobbyList.Add(lobbyId);
|
||||
PlayerManager.JoinSpecificServer(lobbyId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GUI.EndScrollView();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
101
Gui/RegisterManager.cs
Normal file
101
Gui/RegisterManager.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace stick.plugins.playermanager.Gui;
|
||||
|
||||
/// <summary>
|
||||
/// 组件注册管理器
|
||||
/// 提供 Header 行和玩家行组件的注册/反注册接口
|
||||
/// </summary>
|
||||
public static class RegisterManager
|
||||
{
|
||||
#region 注册表
|
||||
|
||||
/// <summary>
|
||||
/// Header 行注册表(纵向排列)
|
||||
/// </summary>
|
||||
public static List<IHeaderRow> HeaderRows { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 玩家行组件注册表(横向排列)
|
||||
/// </summary>
|
||||
public static List<IPlayerRowWidget> PlayerRowWidgets { get; } = new();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 注册方法
|
||||
|
||||
/// <summary>
|
||||
/// 注册一个 Header 行,按注册顺序在 Header 区域纵向排列
|
||||
/// </summary>
|
||||
/// <param name="row">Header 行实例</param>
|
||||
public static void RegisterHeaderRow(IHeaderRow row)
|
||||
{
|
||||
HeaderRows.Add(row);
|
||||
Debug.Log($"[PlayerManager] 注册 Header 行: {row.Name}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册一个玩家行组件,按注册顺序在每个玩家行中横向排列
|
||||
/// </summary>
|
||||
/// <param name="widget">玩家行组件实例</param>
|
||||
public static void RegisterPlayerRow(IPlayerRowWidget widget)
|
||||
{
|
||||
PlayerRowWidgets.Add(widget);
|
||||
Debug.Log($"[PlayerManager] 注册玩家行组件: {widget.Name}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 反注册方法
|
||||
|
||||
/// <summary>
|
||||
/// 按名称移除 Header 行
|
||||
/// </summary>
|
||||
/// <param name="name">行名称</param>
|
||||
/// <returns>是否成功移除</returns>
|
||||
public static bool UnregisterHeaderRow(string name)
|
||||
{
|
||||
int removed = HeaderRows.RemoveAll(r => r.Name == name);
|
||||
if (removed > 0)
|
||||
Debug.Log($"[PlayerManager] 移除 Header 行: {name}");
|
||||
return removed > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按名称移除玩家行组件
|
||||
/// </summary>
|
||||
/// <param name="name">组件名称</param>
|
||||
/// <returns>是否成功移除</returns>
|
||||
public static bool UnregisterPlayerRow(string name)
|
||||
{
|
||||
int removed = PlayerRowWidgets.RemoveAll(w => w.Name == name);
|
||||
if (removed > 0)
|
||||
Debug.Log($"[PlayerManager] 移除玩家行组件: {name}");
|
||||
return removed > 0;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 清空方法
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有注册的 Header 行
|
||||
/// </summary>
|
||||
public static void ClearHeaderRows()
|
||||
{
|
||||
HeaderRows.Clear();
|
||||
Debug.Log("[PlayerManager] 已清空所有 Header 行");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有注册的玩家行组件
|
||||
/// </summary>
|
||||
public static void ClearPlayerRowWidgets()
|
||||
{
|
||||
PlayerRowWidgets.Clear();
|
||||
Debug.Log("[PlayerManager] 已清空所有玩家行组件");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
207
Gui/Widget.cs
Normal file
207
Gui/Widget.cs
Normal file
@@ -0,0 +1,207 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using stick.plugins.playermanager.Recorder;
|
||||
using UnityEngine;
|
||||
|
||||
namespace stick.plugins.playermanager.Gui;
|
||||
|
||||
#region Header 组件接口
|
||||
|
||||
/// <summary>
|
||||
/// 单个 Header 组件接口(Label 或 Button)
|
||||
/// 不直接注册到 RegisterManager,而是放在 IHeaderRow.Children 中
|
||||
/// </summary>
|
||||
public interface IHeaderWidget
|
||||
{
|
||||
/// <summary>
|
||||
/// 组件名称,用于日志和反注册
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否应该绘制此组件
|
||||
/// </summary>
|
||||
bool ShouldDraw();
|
||||
|
||||
/// <summary>
|
||||
/// 组件宽度
|
||||
/// </summary>
|
||||
float Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 在指定 Rect 内绘制组件
|
||||
/// </summary>
|
||||
void Draw(Rect rect);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Header 行接口
|
||||
/// 一个 HeaderRow 占 Header 区域的一整行,其 Children 在行内横向排列
|
||||
/// </summary>
|
||||
public interface IHeaderRow
|
||||
{
|
||||
/// <summary>
|
||||
/// 行名称,用于日志和反注册
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否应该绘制此行
|
||||
/// </summary>
|
||||
bool ShouldDraw();
|
||||
|
||||
/// <summary>
|
||||
/// 行内的子组件列表(Label / Button),按顺序横向排列
|
||||
/// </summary>
|
||||
IList<IHeaderWidget> Children { get; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 玩家行组件接口
|
||||
|
||||
/// <summary>
|
||||
/// 玩家行组件接口(Label 或 Button)
|
||||
/// 注册到 RegisterManager 后,在每个玩家行中横向排列
|
||||
/// </summary>
|
||||
public interface IPlayerRowWidget
|
||||
{
|
||||
/// <summary>
|
||||
/// 组件名称,用于日志和反注册
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否应该为指定玩家绘制此组件
|
||||
/// </summary>
|
||||
bool ShouldDraw(PlayerInfo player);
|
||||
|
||||
/// <summary>
|
||||
/// 组件宽度
|
||||
/// </summary>
|
||||
float Width { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 在指定 Rect 内为指定玩家绘制组件
|
||||
/// </summary>
|
||||
void Draw(Rect rect, PlayerInfo player);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Header 组件具体实现
|
||||
|
||||
/// <summary>
|
||||
/// Header 标签组件
|
||||
/// </summary>
|
||||
public class HeaderLabelWidget : IHeaderWidget
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public Func<bool> Condition { get; set; }
|
||||
public Func<string> Content { get; set; }
|
||||
public float Width { get; set; }
|
||||
public GUIStyle Style { get; set; }
|
||||
|
||||
public bool ShouldDraw() => Condition == null || Condition();
|
||||
|
||||
public void Draw(Rect rect)
|
||||
{
|
||||
GUI.Label(rect, Content?.Invoke() ?? string.Empty, Style ?? GUIStyles.Label);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Header 按钮组件
|
||||
/// </summary>
|
||||
public class HeaderButtonWidget : IHeaderWidget
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public Func<bool> Condition { get; set; }
|
||||
public string Text { get; set; }
|
||||
public Action Action { get; set; }
|
||||
public float Width { get; set; }
|
||||
public GUIStyle Style { get; set; }
|
||||
|
||||
public bool ShouldDraw() => Condition == null || Condition();
|
||||
|
||||
public void Draw(Rect rect)
|
||||
{
|
||||
if (GUI.Button(rect, Text ?? string.Empty, Style ?? GUIStyles.Button))
|
||||
Action?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Header 行具体实现
|
||||
/// 包含一组 IHeaderWidget,在 Header 区域占一整行
|
||||
/// </summary>
|
||||
public class HeaderRow : IHeaderRow
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public Func<bool> Condition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 行内的子组件列表,按添加顺序横向排列
|
||||
/// </summary>
|
||||
public List<IHeaderWidget> Children { get; set; } = new();
|
||||
|
||||
IList<IHeaderWidget> IHeaderRow.Children => Children;
|
||||
|
||||
public bool ShouldDraw() => Condition == null || Condition();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 玩家行组件具体实现
|
||||
|
||||
/// <summary>
|
||||
/// 玩家行按钮组件
|
||||
/// </summary>
|
||||
public class PlayerRowButtonWidget : IPlayerRowWidget
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public Func<PlayerInfo, bool> Condition { get; set; }
|
||||
public Func<PlayerInfo, string> GetText { get; set; }
|
||||
public Action<PlayerInfo> OnClick { get; set; }
|
||||
public float Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 静态样式(当 GetStyle 为 null 或返回 null 时使用)
|
||||
/// </summary>
|
||||
public GUIStyle Style { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 动态样式获取(可选)。优先于 Style,用于根据玩家状态切换按钮颜色
|
||||
/// </summary>
|
||||
public Func<PlayerInfo, GUIStyle> GetStyle { get; set; }
|
||||
|
||||
public bool ShouldDraw(PlayerInfo player) => Condition == null || Condition(player);
|
||||
|
||||
public void Draw(Rect rect, PlayerInfo player)
|
||||
{
|
||||
GUIStyle style = GetStyle?.Invoke(player) ?? Style ?? GUIStyles.Button;
|
||||
if (GUI.Button(rect, GetText?.Invoke(player) ?? string.Empty, style))
|
||||
OnClick?.Invoke(player);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 玩家行标签组件
|
||||
/// </summary>
|
||||
public class PlayerRowLabelWidget : IPlayerRowWidget
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public Func<PlayerInfo, bool> Condition { get; set; }
|
||||
public Func<PlayerInfo, string> GetText { get; set; }
|
||||
public float Width { get; set; }
|
||||
public GUIStyle Style { get; set; }
|
||||
|
||||
public bool ShouldDraw(PlayerInfo player) => Condition == null || Condition(player);
|
||||
|
||||
public void Draw(Rect rect, PlayerInfo player)
|
||||
{
|
||||
GUI.Label(rect, GetText?.Invoke(player) ?? string.Empty, Style ?? GUIStyles.Label);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
22
Patches/DistanceFilterPatch.cs
Normal file
22
Patches/DistanceFilterPatch.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using System.Reflection.Emit;
|
||||
using System.Collections.Generic;
|
||||
using HarmonyLib;
|
||||
using stick.plugins.playermanager;
|
||||
|
||||
[HarmonyPatch(typeof(MatchmakingHandler), "OnRandomServerJoinFailed")]
|
||||
class DistanceFilterPatch
|
||||
{
|
||||
public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
|
||||
{
|
||||
return new CodeMatcher(instructions)
|
||||
.MatchForward(false,
|
||||
new CodeMatch(OpCodes.Ldc_I4_1),
|
||||
new CodeMatch(OpCodes.Stloc_0)
|
||||
)
|
||||
.SetInstruction(
|
||||
new CodeInstruction(OpCodes.Ldsfld,
|
||||
AccessTools.Field(typeof(PlayerManager), nameof(PlayerManager.lobbyDistanceFilter))))
|
||||
.InstructionEnumeration();
|
||||
}
|
||||
}
|
||||
|
||||
15
Patches/MutePatch.cs
Normal file
15
Patches/MutePatch.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using HarmonyLib;
|
||||
using stick.plugins.playermanager;
|
||||
|
||||
[HarmonyPatch(typeof(NetworkPlayer), "SyncClientChat")]
|
||||
class MutePatch
|
||||
{
|
||||
public static bool Prefix(NetworkPlayer __instance)
|
||||
{
|
||||
if (PlayerManager.MutedPlayers.Contains(__instance.NetworkSpawnID))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
137
PlayerInfo.cs
137
PlayerInfo.cs
@@ -1,137 +0,0 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using Steamworks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace stick.plugins.playermanager;
|
||||
|
||||
public class PlayerInfo : IEquatable<PlayerInfo>
|
||||
{
|
||||
// 即时获取的玩家名字,只读
|
||||
public string PlayerName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (SteamManager.Initialized)
|
||||
{
|
||||
return SteamFriends.GetFriendPersonaName(steamId);
|
||||
}
|
||||
else
|
||||
{
|
||||
return playerNameRecorded ?? "";
|
||||
}
|
||||
}
|
||||
}
|
||||
// 玩家名字,一般来说是曾用名
|
||||
public string playerNameRecorded;
|
||||
public bool displayNameRecorded = false;
|
||||
public bool HasRecordedName => !string.IsNullOrEmpty(playerNameRecorded) && playerNameRecorded != PlayerName;
|
||||
public CSteamID steamId;
|
||||
public bool IsFriend => SteamFriends.HasFriend(steamId, EFriendFlags.k_EFriendFlagImmediate);
|
||||
public PlayerRecordStatus RecordStatus = PlayerRecordStatus.NotRecorded;
|
||||
public PlayerInfo(CSteamID steamId, string playerName)
|
||||
{
|
||||
this.steamId = steamId;
|
||||
this.playerNameRecorded = playerName;
|
||||
}
|
||||
public PlayerInfo(CSteamID steamId)
|
||||
{
|
||||
this.steamId = steamId;
|
||||
playerNameRecorded = SteamFriends.GetFriendPersonaName(steamId);
|
||||
}
|
||||
public PlayerInfo(string playerInfo)
|
||||
{
|
||||
if (string.IsNullOrEmpty(playerInfo))
|
||||
{
|
||||
steamId = CSteamID.Nil;
|
||||
playerNameRecorded = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
Parse(playerInfo, out ulong steamIdValue, out string playerNameValue);
|
||||
steamId = new CSteamID(steamIdValue);
|
||||
playerNameRecorded = playerNameValue;
|
||||
|
||||
// 会因为初始化过早而导致插件初始化失败
|
||||
// playerName = SteamFriends.GetFriendPersonaName(steamId);
|
||||
}
|
||||
public static void Parse(string input, out ulong steamID, out string playerName)
|
||||
{
|
||||
// 查找第一个空格作为分隔符
|
||||
int spaceIndex = input.IndexOf(' ');
|
||||
|
||||
if (spaceIndex == -1)
|
||||
{
|
||||
// 没有空格的情况:整个字符串作为SteamID
|
||||
steamID = ulong.Parse(input);
|
||||
playerName = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
// 分割SteamID和玩家名字
|
||||
string steamIdPart = input.Substring(0, spaceIndex);
|
||||
string namePart = input.Substring(spaceIndex + 1);
|
||||
|
||||
steamID = ulong.Parse(steamIdPart);
|
||||
playerName = namePart; // 保留名字原始格式(含标签和空格)
|
||||
}
|
||||
|
||||
public static bool TryParse(string input, out ulong steamID, out string playerName)
|
||||
{
|
||||
steamID = 0;
|
||||
playerName = string.Empty;
|
||||
try
|
||||
{
|
||||
Parse(input, out steamID, out playerName);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public void
|
||||
MergeWith(PlayerInfo other)
|
||||
{
|
||||
if (other is null) return;
|
||||
|
||||
if (steamId != other.steamId)
|
||||
{
|
||||
Debug.LogWarning($"Attempting to merge PlayerInfo with different steam IDs: {steamId} and {other.steamId}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(other.playerNameRecorded))
|
||||
{
|
||||
playerNameRecorded = other.playerNameRecorded;
|
||||
}
|
||||
if (other.RecordStatus > RecordStatus)
|
||||
{
|
||||
RecordStatus = other.RecordStatus;
|
||||
}
|
||||
}
|
||||
|
||||
// 生成记录进文件用的字符串
|
||||
public string ToInfoString()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(playerNameRecorded))
|
||||
{
|
||||
return $"{steamId} {playerNameRecorded}";
|
||||
}
|
||||
return $"{steamId} {PlayerName}";
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (displayNameRecorded && !string.IsNullOrEmpty(playerNameRecorded))
|
||||
{
|
||||
return $"{steamId} {playerNameRecorded}";
|
||||
}
|
||||
return $"{steamId} {PlayerName}";
|
||||
}
|
||||
|
||||
public bool Equals(PlayerInfo toCompare)
|
||||
=> toCompare is not null && steamId == toCompare.steamId;
|
||||
public override int GetHashCode()
|
||||
=> steamId.GetHashCode();
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
using System.Linq;
|
||||
using MultiplayerBasicExample;
|
||||
using Steamworks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace stick.plugins.playermanager;
|
||||
|
||||
static public class PlayerManagerGUI
|
||||
{
|
||||
static public bool showLobbyInfo = false;
|
||||
static Vector2 scrollPosition = Vector2.zero;
|
||||
static float offsetX;
|
||||
static float offsetY;
|
||||
static float lineHeight;
|
||||
static float lineIndex;
|
||||
static GUIStyle TitleGuiStyle => new(GUI.skin.label) { fontSize = 22, };
|
||||
static GUIStyle CommonButtonGuiStyle => new(GUI.skin.button) { fontSize = 22, richText = false };
|
||||
static GUIStyle YellowButtonGuiStyle => new(GUI.skin.button) { fontSize = 22, normal = { textColor = Color.yellow, }, richText = false };
|
||||
static GUIStyle RedButtonGuiStyle => new(GUI.skin.button) { fontSize = 22, normal = { textColor = Color.red, }, richText = false };
|
||||
static GUIStyle GreenButtonGuiStyle => new(GUI.skin.button) { fontSize = 22, normal = { textColor = Color.green, }, richText = false };
|
||||
static void InitOffset()
|
||||
{
|
||||
offsetX = 10f;
|
||||
offsetY = 20f;
|
||||
lineHeight = 50f;
|
||||
lineIndex = 0f;
|
||||
}
|
||||
static float GetNewYPos()
|
||||
{
|
||||
return offsetY + lineHeight * ++lineIndex;
|
||||
}
|
||||
static float GetCurYPos()
|
||||
{
|
||||
return offsetY + lineHeight * lineIndex;
|
||||
}
|
||||
static GUIStyle GetButtonStyle(PlayerInfo playerInfo)
|
||||
{
|
||||
if (playerInfo.RecordStatus == PlayerRecordStatus.Blacklisted)
|
||||
{
|
||||
return RedButtonGuiStyle;
|
||||
}
|
||||
else if (playerInfo.RecordStatus == PlayerRecordStatus.Recorded)
|
||||
{
|
||||
return YellowButtonGuiStyle;
|
||||
}
|
||||
else if (playerInfo.IsFriend)
|
||||
{
|
||||
return GreenButtonGuiStyle;
|
||||
}
|
||||
else
|
||||
{
|
||||
return CommonButtonGuiStyle;
|
||||
}
|
||||
}
|
||||
static string GetRecordedNameButtonText(PlayerInfo playerInfo)
|
||||
=> playerInfo.displayNameRecorded switch
|
||||
{
|
||||
true => "显示现用名",
|
||||
false => "显示曾用名",
|
||||
};
|
||||
static string GetRecordButtonText(PlayerInfo playerInfo)
|
||||
=> playerInfo.RecordStatus switch
|
||||
{
|
||||
PlayerRecordStatus.NotRecorded => "记录",
|
||||
PlayerRecordStatus.Recorded => "拉黑",
|
||||
PlayerRecordStatus.Blacklisted => "取消",
|
||||
_ => "未知状态",
|
||||
};
|
||||
static void OnRecordButtonClick(PlayerInfo playerInfo)
|
||||
{
|
||||
|
||||
//Debug.Log($"RecordStatus {playerInfo.RecordStatus}");
|
||||
switch (playerInfo.RecordStatus)
|
||||
{
|
||||
case PlayerRecordStatus.NotRecorded:
|
||||
PlayerManager.playerRecoder.RecordPlayer(playerInfo);
|
||||
break;
|
||||
case PlayerRecordStatus.Recorded:
|
||||
PlayerManager.blackListRecorder.RecordPlayer(playerInfo);
|
||||
break;
|
||||
case PlayerRecordStatus.Blacklisted:
|
||||
PlayerManager.playerRecoder.CancelRecordPlayer(playerInfo);
|
||||
PlayerManager.blackListRecorder.CancelRecordPlayer(playerInfo);
|
||||
break;
|
||||
default:
|
||||
Debug.LogError($"未知的记录状态: {playerInfo.RecordStatus}");
|
||||
return;
|
||||
}
|
||||
|
||||
//Debug.Log($"RecordStatus {playerInfo.RecordStatus}");
|
||||
PlayerManager.OnLobbyMembersChanged();
|
||||
}
|
||||
|
||||
|
||||
static void DrawCurrentLobbyInfo()
|
||||
{
|
||||
// 当前Lobby信息
|
||||
GUI.Label
|
||||
(
|
||||
new Rect(offsetX, GetNewYPos(), 2000f, 40f),
|
||||
$"当前Lobby:{PlayerManager.CurrentLobbyID} 房主:{PlayerManager.LobbyOwner.PlayerName}",
|
||||
TitleGuiStyle
|
||||
);
|
||||
for (int i = 0; i < PlayerManager.lobbyMemberList.Count; i++)
|
||||
{
|
||||
PlayerInfo lobbyMember = PlayerManager.lobbyMemberList[i];
|
||||
if (lobbyMember.steamId.IsValid())
|
||||
{
|
||||
// 玩家信息
|
||||
if (GUI.Button
|
||||
(
|
||||
new Rect(offsetX, GetNewYPos(), 500f, 40f),
|
||||
lobbyMember.ToString(),
|
||||
GetButtonStyle(lobbyMember)
|
||||
)
|
||||
)
|
||||
{
|
||||
GUIUtility.systemCopyBuffer = lobbyMember.ToString();
|
||||
SteamFriends.ActivateGameOverlayToUser("steamid", lobbyMember.steamId);
|
||||
}
|
||||
// 记录按钮
|
||||
if (GUI.Button
|
||||
(
|
||||
new Rect(520f, GetCurYPos(), 100f, 40f),
|
||||
GetRecordButtonText(lobbyMember),
|
||||
CommonButtonGuiStyle
|
||||
)
|
||||
)
|
||||
{
|
||||
OnRecordButtonClick(lobbyMember);
|
||||
}
|
||||
// 踢人按钮
|
||||
if (!PlayerManager.IsHost(lobbyMember) && !PlayerManager.IsLocalPlayer(lobbyMember))
|
||||
{
|
||||
if (GUI.Button
|
||||
(
|
||||
new Rect(630f, GetCurYPos(), 100f, 40f),
|
||||
"Kick!",
|
||||
CommonButtonGuiStyle
|
||||
)
|
||||
)
|
||||
{
|
||||
PlayerManager.KickPlayer(lobbyMember.steamId, MultiplayerManager.KickResponse.DidNotRecievePackages);
|
||||
}
|
||||
}
|
||||
// 曾用名按钮
|
||||
if (lobbyMember.HasRecordedName)
|
||||
{
|
||||
if (GUI.Button
|
||||
(
|
||||
new Rect(740f, GetCurYPos(), 120f, 40f),
|
||||
GetRecordedNameButtonText(lobbyMember),
|
||||
CommonButtonGuiStyle
|
||||
)
|
||||
)
|
||||
{
|
||||
lobbyMember.displayNameRecorded = !lobbyMember.displayNameRecorded;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
static void DrawLobbySearchInfo()
|
||||
{
|
||||
string areaStr = "";
|
||||
switch (PlayerManager.elobbyDistanceFilter)
|
||||
{
|
||||
case ELobbyDistanceFilter.k_ELobbyDistanceFilterClose:
|
||||
areaStr = "当前地区";
|
||||
break;
|
||||
case ELobbyDistanceFilter.k_ELobbyDistanceFilterDefault:
|
||||
areaStr = "当前及附近地区";
|
||||
break;
|
||||
case ELobbyDistanceFilter.k_ELobbyDistanceFilterFar:
|
||||
areaStr = "半球范围内";
|
||||
break;
|
||||
case ELobbyDistanceFilter.k_ELobbyDistanceFilterWorldwide:
|
||||
areaStr = "世界";
|
||||
break;
|
||||
}
|
||||
if (GUI.Button
|
||||
(
|
||||
new Rect(offsetX, GetNewYPos(), 400f, 40f),
|
||||
$"搜索范围 {areaStr}",
|
||||
CommonButtonGuiStyle
|
||||
)
|
||||
)
|
||||
{
|
||||
if (PlayerManager.elobbyDistanceFilter == ELobbyDistanceFilter.k_ELobbyDistanceFilterWorldwide)
|
||||
{
|
||||
PlayerManager.elobbyDistanceFilter = ELobbyDistanceFilter.k_ELobbyDistanceFilterClose;
|
||||
}
|
||||
else
|
||||
{
|
||||
PlayerManager.elobbyDistanceFilter++;
|
||||
}
|
||||
}
|
||||
if (GUI.Button
|
||||
(
|
||||
new Rect(offsetX + 420f, GetCurYPos(), 100f, 40f),
|
||||
"刷新",
|
||||
CommonButtonGuiStyle
|
||||
)
|
||||
)
|
||||
{
|
||||
PlayerManager.RequestLobbyList();
|
||||
}
|
||||
GUI.Label(
|
||||
new Rect(offsetX, GetNewYPos(), 500f, 40f),
|
||||
$"搜索到Lobby数量:{PlayerManager.lobbyDataList.Count}",
|
||||
TitleGuiStyle
|
||||
);
|
||||
scrollPosition = GUI.BeginScrollView(
|
||||
new Rect(offsetX, GetNewYPos(), 525f, 405f),
|
||||
scrollPosition,
|
||||
new Rect(0f, 0f, 505f, PlayerManager.lobbyDataList.Count * 50)
|
||||
);
|
||||
for (int j = 0; j < PlayerManager.lobbyDataList.Count; j++)
|
||||
{
|
||||
CSteamID lobbyData = PlayerManager.lobbyDataList[j];
|
||||
if (GUI.Button
|
||||
(
|
||||
new Rect(offsetX, lineHeight * j, 500f, 40f),
|
||||
$"{lobbyData} 人数{SteamMatchmaking.GetNumLobbyMembers(lobbyData)}",
|
||||
CommonButtonGuiStyle
|
||||
)
|
||||
)
|
||||
{
|
||||
PlayerManager.JoinSpecificServer(lobbyData);
|
||||
}
|
||||
}
|
||||
GUI.EndScrollView();
|
||||
}
|
||||
static public void DrawLobbyInfoGUI()
|
||||
{
|
||||
InitOffset();
|
||||
|
||||
if (PlayerManager.IMatchmakingHandler.IsInsideLobby)
|
||||
{
|
||||
DrawCurrentLobbyInfo();
|
||||
}
|
||||
else
|
||||
{
|
||||
GUI.Label(
|
||||
new Rect(offsetX, GetNewYPos(), 500f, 40f),
|
||||
"当前不在Lobby中",
|
||||
TitleGuiStyle
|
||||
);
|
||||
}
|
||||
DrawLobbySearchInfo();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace stick.plugins.playermanager;
|
||||
|
||||
public class PlayerRecoder : RecoderBase
|
||||
{
|
||||
protected override string FileName => "CustomPlayerRecord.txt";
|
||||
protected override PlayerRecordStatus RecordStatus => PlayerRecordStatus.Recorded;
|
||||
}
|
||||
523
Plugin.cs
523
Plugin.cs
@@ -6,124 +6,539 @@ using UnityEngine;
|
||||
using Steamworks;
|
||||
using HarmonyLib;
|
||||
using System.Data;
|
||||
using System.Reflection;
|
||||
using stick.plugins.playermanager.Recorder;
|
||||
using stick.plugins.playermanager.Gui;
|
||||
using System.Collections;
|
||||
using MultiplayerBasicExample;
|
||||
|
||||
namespace stick.plugins.playermanager;
|
||||
|
||||
/// <summary>
|
||||
/// 玩家管理器插件主类
|
||||
/// 提供大厅信息显示、玩家记录、黑名单管理等功能
|
||||
/// </summary>
|
||||
[BepInPlugin(MyPluginInfo.PLUGIN_GUID, MyPluginInfo.PLUGIN_NAME, MyPluginInfo.PLUGIN_VERSION)]
|
||||
[BepInProcess("StickFight.exe")]
|
||||
public class PlayerManager : BaseUnityPlugin
|
||||
{
|
||||
static ConfigEntry<KeyCode> lobbyInfoToggleKeyCode;
|
||||
static ConfigEntry<bool> blockKicks;
|
||||
static CallResult<LobbyMatchList_t> onLobbyMatchListCallResult = CallResult<LobbyMatchList_t>.Create(null);
|
||||
public static List<CSteamID> lobbyDataList = [];
|
||||
public static ELobbyDistanceFilter elobbyDistanceFilter = ELobbyDistanceFilter.k_ELobbyDistanceFilterDefault;
|
||||
public static PlayerRecoder playerRecoder = new();
|
||||
#region 配置项
|
||||
|
||||
/// <summary>
|
||||
/// 是否屏蔽来自其他玩家的踢出请求
|
||||
/// </summary>
|
||||
internal static ConfigEntry<bool> _blockKicks;
|
||||
|
||||
/// <summary>
|
||||
/// 大厅信息开关快捷键
|
||||
/// </summary>
|
||||
internal static ConfigEntry<KeyCode> _lobbyInfoToggleKeyCode;
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示踢人按钮
|
||||
/// </summary>
|
||||
internal static ConfigEntry<bool> ShowKickButton;
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示转让按钮
|
||||
/// </summary>
|
||||
internal static ConfigEntry<bool> ShowTransButton;
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示记录/拉黑按钮
|
||||
/// </summary>
|
||||
internal static ConfigEntry<bool> ShowRecordButton;
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示曾用名切换按钮
|
||||
/// </summary>
|
||||
internal static ConfigEntry<bool> ShowNameToggleButton;
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示房间类型标签
|
||||
/// </summary>
|
||||
internal static ConfigEntry<bool> ShowRoomPublicityLabel;
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示切换公开性按钮
|
||||
/// </summary>
|
||||
internal static ConfigEntry<bool> ShowRoomPublicityButton;
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示禁音按钮
|
||||
/// </summary>
|
||||
internal static ConfigEntry<bool> ShowMuteButton;
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示加好友按钮
|
||||
/// </summary>
|
||||
internal static ConfigEntry<bool> ShowAddFriendButton;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Steam回调
|
||||
|
||||
/// <summary>
|
||||
/// 大厅列表查询回调
|
||||
/// </summary>
|
||||
private static readonly CallResult<LobbyMatchList_t> LobbyMatchListCallResult = CallResult<LobbyMatchList_t>.Create(null);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 公共属性
|
||||
|
||||
/// <summary>
|
||||
/// 玩家记录器实例
|
||||
/// </summary>
|
||||
public static PlayerRecorder playerRecorder = new();
|
||||
|
||||
/// <summary>
|
||||
/// 黑名单记录器实例
|
||||
/// </summary>
|
||||
public static BlackListRecorder blackListRecorder = new();
|
||||
|
||||
/// <summary>
|
||||
/// 当前大厅成员列表
|
||||
/// </summary>
|
||||
public static List<PlayerInfo> lobbyMemberList = [];
|
||||
|
||||
public static MatchmakingHandler IMatchmakingHandler => MatchmakingHandler.Instance;
|
||||
public static CSteamID CurrentLobbyID => IMatchmakingHandler.CurrentLobby;
|
||||
public static PlayerInfo LobbyOwner => new(SteamMatchmaking.GetLobbyOwner(CurrentLobbyID));// PlayerManager.cs 新增代码
|
||||
public static PlayerInfo LocalPlayer => new(SteamUser.GetSteamID());
|
||||
public static object lobbyMemberLock = new(); // 线程同步锁
|
||||
/// <summary>
|
||||
/// 搜索到的大厅列表
|
||||
/// </summary>
|
||||
public static List<CSteamID> lobbyDataList = [];
|
||||
|
||||
void Awake()
|
||||
/// <summary>
|
||||
/// 进入过的大厅列表
|
||||
/// </summary>
|
||||
public static HashSet<CSteamID> enteredLobbyList = [];
|
||||
|
||||
/// <summary>
|
||||
/// 大厅距离过滤设置
|
||||
/// </summary>
|
||||
public static ELobbyDistanceFilter lobbyDistanceFilter = ELobbyDistanceFilter.k_ELobbyDistanceFilterDefault;
|
||||
|
||||
/// <summary>
|
||||
/// 线程同步锁,保护大厅成员列表操作
|
||||
/// </summary>
|
||||
private static readonly object LobbyMemberLock = new();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 快捷访问属性
|
||||
|
||||
/// <summary>
|
||||
/// 多人管理器实例
|
||||
/// </summary>
|
||||
public static MultiplayerManager MultiplayerManager => FindObjectOfType<MultiplayerManager>();
|
||||
|
||||
/// <summary>
|
||||
/// 大厅管理器实例
|
||||
/// </summary>
|
||||
public static MatchmakingHandler MatchmakingHandler => MatchmakingHandler.Instance;
|
||||
|
||||
/// <summary>
|
||||
/// 原始的设置lobby类型方法
|
||||
/// </summary>
|
||||
public static MethodInfo RawChangeLobbyTypeMI => AccessTools.Method(
|
||||
typeof(MatchmakingHandler),
|
||||
"ChangeLobbyType"
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// 房间公开性
|
||||
/// </summary>
|
||||
public static bool IsPublic => MatchmakingHandler.LobbyType == ELobbyType.k_ELobbyTypePublic;
|
||||
|
||||
/// <summary>
|
||||
/// 当前大厅ID
|
||||
/// </summary>
|
||||
public static CSteamID CurrentLobbyID => MatchmakingHandler.CurrentLobby;
|
||||
|
||||
/// <summary>
|
||||
/// 大厅房主信息
|
||||
/// </summary>
|
||||
public static PlayerInfo LobbyOwner => new(SteamMatchmaking.GetLobbyOwner(CurrentLobbyID));
|
||||
|
||||
/// <summary>
|
||||
/// 本地玩家信息
|
||||
/// </summary>
|
||||
public static PlayerInfo LocalPlayer;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化localPlayer和只需要初始化一次的steam相关的协程
|
||||
/// </summary>
|
||||
IEnumerator InitAfterSteam()
|
||||
{
|
||||
// 绑定配置
|
||||
blockKicks = Config.Bind<bool>("General", "BlockKicks", true, "是否屏蔽来自其他玩家的踢出请求");
|
||||
lobbyInfoToggleKeyCode = Config.Bind<KeyCode>("LobbyInfo", "ToggleKeyCode", KeyCode.F2, "Lobby信息开关快捷键");
|
||||
// patch
|
||||
if (blockKicks.Value)
|
||||
// 等待 Steam 初始化(给游戏一点初始化时间)
|
||||
yield return new WaitForSeconds(2f);
|
||||
|
||||
// 或者轮询检查 Steam 是否就绪
|
||||
while (!SteamManager.Initialized) // 如果游戏有 SteamManager
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
|
||||
LocalPlayer = new(SteamUser.GetSteamID());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 禁音玩家列表
|
||||
/// </summary>
|
||||
public static HashSet<ushort> MutedPlayers = [];
|
||||
|
||||
#endregion
|
||||
|
||||
#region 生命周期方法
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
InitializeConfiguration();
|
||||
ApplyPatches();
|
||||
DefaultWidgets.RegisterAll();
|
||||
}
|
||||
|
||||
void Start()
|
||||
{
|
||||
StartCoroutine(InitAfterSteam());
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
HandleInput();
|
||||
if (SteamManager.Initialized && LocalPlayer is null)
|
||||
{
|
||||
LocalPlayer = new(SteamUser.GetSteamID());
|
||||
}
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
if (PlayerManagerGUI.ShowLobbyInfo)
|
||||
{
|
||||
PlayerManagerGUI.DrawLobbyInfoGUI();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 私有方法
|
||||
|
||||
/// <summary>
|
||||
/// 初始化配置项
|
||||
/// </summary>
|
||||
private void InitializeConfiguration()
|
||||
{
|
||||
_blockKicks = Config.Bind(
|
||||
"General",
|
||||
"BlockKicks",
|
||||
true,
|
||||
"是否屏蔽来自其他玩家的踢出请求"
|
||||
);
|
||||
|
||||
_lobbyInfoToggleKeyCode = Config.Bind(
|
||||
"LobbyInfo",
|
||||
"ToggleKeyCode",
|
||||
KeyCode.F2,
|
||||
"Lobby信息开关快捷键"
|
||||
);
|
||||
|
||||
ShowKickButton = Config.Bind(
|
||||
"GUI",
|
||||
"ShowKickButton",
|
||||
true,
|
||||
"是否显示踢人按钮"
|
||||
);
|
||||
|
||||
ShowTransButton = Config.Bind(
|
||||
"GUI",
|
||||
"ShowTransButton",
|
||||
true,
|
||||
"是否显示转让按钮"
|
||||
);
|
||||
|
||||
ShowRecordButton = Config.Bind(
|
||||
"GUI",
|
||||
"ShowRecordButton",
|
||||
true,
|
||||
"是否显示记录/拉黑按钮"
|
||||
);
|
||||
|
||||
ShowNameToggleButton = Config.Bind(
|
||||
"GUI",
|
||||
"ShowNameToggleButton",
|
||||
true,
|
||||
"是否显示曾用名切换按钮"
|
||||
);
|
||||
|
||||
ShowMuteButton = Config.Bind(
|
||||
"GUI",
|
||||
"ShowMuteButton",
|
||||
true,
|
||||
"是否显示禁音按钮"
|
||||
);
|
||||
|
||||
ShowRoomPublicityLabel = Config.Bind(
|
||||
"GUI",
|
||||
"ShowRoomPublicityLabel",
|
||||
true,
|
||||
"是否显示房间类型标签"
|
||||
);
|
||||
|
||||
ShowRoomPublicityButton = Config.Bind(
|
||||
"GUI",
|
||||
"ShowRoomPublicityButton",
|
||||
true,
|
||||
"是否显示切换公开性按钮"
|
||||
);
|
||||
|
||||
ShowAddFriendButton = Config.Bind(
|
||||
"GUI",
|
||||
"ShowAddFriendButton",
|
||||
true,
|
||||
"是否显示加好友按钮"
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用Harmony补丁
|
||||
/// </summary>
|
||||
private void ApplyPatches()
|
||||
{
|
||||
if (_blockKicks.Value)
|
||||
{
|
||||
Harmony.CreateAndPatchAll(typeof(IgnoreKick));
|
||||
}
|
||||
Harmony.CreateAndPatchAll(typeof(ApplyBlacklist));
|
||||
Harmony.CreateAndPatchAll(typeof(ListenForChange));
|
||||
Harmony.CreateAndPatchAll(typeof(MutePatch));
|
||||
Harmony.CreateAndPatchAll(typeof(DistanceFilterPatch));
|
||||
}
|
||||
void Update()
|
||||
|
||||
/// <summary>
|
||||
/// 处理输入
|
||||
/// </summary>
|
||||
private void HandleInput()
|
||||
{
|
||||
if (Input.GetKeyDown(lobbyInfoToggleKeyCode.Value))
|
||||
if (Input.GetKeyDown(_lobbyInfoToggleKeyCode.Value))
|
||||
{
|
||||
if (PlayerManagerGUI.showLobbyInfo)
|
||||
ToggleLobbyInfoGUI();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换大厅信息GUI显示状态
|
||||
/// </summary>
|
||||
private void ToggleLobbyInfoGUI()
|
||||
{
|
||||
PlayerManagerGUI.showLobbyInfo = false;
|
||||
Debug.Log("The GUI is closed");
|
||||
if (PlayerManagerGUI.ShowLobbyInfo)
|
||||
{
|
||||
PlayerManagerGUI.ShowLobbyInfo = false;
|
||||
Debug.Log("大厅信息界面已关闭");
|
||||
}
|
||||
else
|
||||
{
|
||||
RequestLobbyList();
|
||||
PlayerManagerGUI.showLobbyInfo = true;
|
||||
Debug.Log("The GUI is opened");
|
||||
OnLobbyMembersChanged();
|
||||
PlayerManagerGUI.ShowLobbyInfo = true;
|
||||
Debug.Log("大厅信息界面已打开");
|
||||
}
|
||||
}
|
||||
}
|
||||
void OnGUI()
|
||||
#endregion
|
||||
|
||||
#region 公共方法
|
||||
|
||||
/// <summary>
|
||||
/// 转让房主
|
||||
/// </summary>
|
||||
/// <param name="targetPlayer">目标玩家</param>
|
||||
/// <returns></returns>
|
||||
public static void TransferLobbyOwnership(PlayerInfo targetPlayer)
|
||||
{
|
||||
if (PlayerManagerGUI.showLobbyInfo)
|
||||
if (targetPlayer != null && targetPlayer != LocalPlayer)
|
||||
{
|
||||
PlayerManagerGUI.DrawLobbyInfoGUI();
|
||||
SteamMatchmaking.SetLobbyOwner(CurrentLobbyID, targetPlayer.SteamId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过SteamID获取NetworkSpawnID
|
||||
/// </summary>
|
||||
/// <param name="targetSteamID"></param>
|
||||
/// <returns></returns>
|
||||
public static ushort GetNetworkSpawnIDBySteamID(CSteamID targetSteamID)
|
||||
{
|
||||
ConnectedClientData[] clients = MultiplayerManager.ConnectedClients;
|
||||
for (ushort i = 0; i < clients.Length; i++)
|
||||
{
|
||||
if (clients[i] != null && clients[i].ClientID == targetSteamID)
|
||||
{
|
||||
return i; // i 就是 NetworkSpawnID
|
||||
}
|
||||
}
|
||||
return 65535; // ushort.MaxValue,表示未找到(与默认值一致)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断指定玩家是否被静音
|
||||
/// </summary>
|
||||
/// <param name="player">要检查的玩家</param>
|
||||
/// <returns>如果被静音返回true</returns>
|
||||
public static bool IsMuted(PlayerInfo player)
|
||||
{
|
||||
return player != null && MutedPlayers.Contains(player.NetworkSpawnID);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 静音指定玩家
|
||||
/// </summary>
|
||||
/// <param name="player">被静音的玩家</param>
|
||||
public static void MutePlayer(PlayerInfo player)
|
||||
{
|
||||
if (player != null)
|
||||
{
|
||||
MutedPlayers.Add(player.NetworkSpawnID);
|
||||
Debug.Log($"玩家 {player} 已被静音");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消静音指定玩家
|
||||
/// </summary>
|
||||
/// <param name="player">要取消静音的玩家</param>
|
||||
public static void UnmutePlayer(PlayerInfo player)
|
||||
{
|
||||
if (player != null && MutedPlayers.Contains(player.NetworkSpawnID))
|
||||
{
|
||||
MutedPlayers.Remove(player.NetworkSpawnID);
|
||||
Debug.Log($"玩家 {player} 已被取消静音");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断指定玩家是否为房主
|
||||
/// </summary>
|
||||
/// <param name="player">要检查的玩家</param>
|
||||
/// <returns>如果是房主返回true</returns>
|
||||
public static bool IsHost(PlayerInfo player)
|
||||
{
|
||||
return player.Equals(LobbyOwner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断指定玩家是否为本地玩家
|
||||
/// </summary>
|
||||
/// <param name="player">要检查的玩家</param>
|
||||
/// <returns>如果是本地玩家返回true</returns>
|
||||
public static bool IsLocalPlayer(PlayerInfo player)
|
||||
{
|
||||
return player.Equals(LocalPlayer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 修改大厅公开性
|
||||
/// </summary>
|
||||
public static void ToggleLobbyPublicity()
|
||||
{
|
||||
if (MultiplayerManager.IsServer)
|
||||
{
|
||||
var targetType = IsPublic ? ELobbyType.k_ELobbyTypeFriendsOnly : ELobbyType.k_ELobbyTypePublic;
|
||||
MatchmakingHandler.SetNewLobbyType(targetType);
|
||||
RawChangeLobbyTypeMI.Invoke(MatchmakingHandler, [targetType]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 请求大厅列表
|
||||
/// </summary>
|
||||
public static void RequestLobbyList()
|
||||
{
|
||||
SteamMatchmaking.AddRequestLobbyListDistanceFilter(elobbyDistanceFilter);
|
||||
SteamAPICall_t hAPICall2 = SteamMatchmaking.RequestLobbyList();
|
||||
onLobbyMatchListCallResult.Set(hAPICall2, new CallResult<LobbyMatchList_t>.APIDispatchDelegate(OnRequestLobbyList));
|
||||
Debug.Log("正在请求大厅列表...");
|
||||
SteamMatchmaking.AddRequestLobbyListDistanceFilter(lobbyDistanceFilter);
|
||||
SteamAPICall_t apiCall = SteamMatchmaking.RequestLobbyList();
|
||||
LobbyMatchListCallResult.Set(apiCall, new CallResult<LobbyMatchList_t>.APIDispatchDelegate(OnRequestLobbyList));
|
||||
}
|
||||
static void OnRequestLobbyList(LobbyMatchList_t param, bool bIOFailure)
|
||||
|
||||
/// <summary>
|
||||
/// 处理大厅列表查询结果
|
||||
/// </summary>
|
||||
private static void OnRequestLobbyList(LobbyMatchList_t result, bool ioFailure)
|
||||
{
|
||||
Debug.Log("大厅列表请求完成: " + result.m_nLobbiesMatching + " 个大厅");
|
||||
lobbyDataList = [];
|
||||
uint nLobbiesMatching = param.m_nLobbiesMatching;
|
||||
for (int i = 0; i < nLobbiesMatching; i++)
|
||||
enteredLobbyList = [];
|
||||
uint lobbyCount = result.m_nLobbiesMatching;
|
||||
|
||||
for (uint i = 0; i < lobbyCount; i++)
|
||||
{
|
||||
CSteamID lobbyByIndex = SteamMatchmaking.GetLobbyByIndex(i);
|
||||
if (SteamMatchmaking.GetLobbyData(lobbyByIndex, StickFightConstants.VERSION_KEY) == StickFightConstants.VERSION_VALUE)
|
||||
CSteamID lobbyId = SteamMatchmaking.GetLobbyByIndex((int)i);
|
||||
if (SteamMatchmaking.GetLobbyData(lobbyId, StickFightConstants.VERSION_KEY) == StickFightConstants.VERSION_VALUE)
|
||||
{
|
||||
lobbyDataList.Add(lobbyByIndex);
|
||||
lobbyDataList.Add(lobbyId);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("过滤掉不兼容版本的大厅: " + lobbyId);
|
||||
Debug.Log("大厅 " + lobbyId + " 版本: " + SteamMatchmaking.GetLobbyData(lobbyId, StickFightConstants.VERSION_KEY));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加入指定大厅
|
||||
/// </summary>
|
||||
/// <param name="lobby">要加入的大厅ID</param>
|
||||
public static void JoinSpecificServer(CSteamID lobby)
|
||||
{
|
||||
Traverse.Create(MatchmakingHandler.Instance).Field("mCurrentConnectionType").SetValue(ConnectionType.Specific);
|
||||
Traverse.Create(MatchmakingHandler.Instance)
|
||||
.Field("mCurrentConnectionType")
|
||||
.SetValue(ConnectionType.Specific);
|
||||
|
||||
MatchmakingHandler.Instance.JoinServer(lobby, GameManager.Instance.mMultiplayerManager.OnServerJoined);
|
||||
OnLobbyMembersChanged();
|
||||
}
|
||||
public static void KickPlayer(CSteamID clientID, MultiplayerManager.KickResponse response)
|
||||
|
||||
/// <summary>
|
||||
/// 踢出指定玩家
|
||||
/// </summary>
|
||||
/// <param name="clientId">被踢玩家的Steam ID</param>
|
||||
/// <param name="response">踢出原因响应</param>
|
||||
public static void KickPlayer(CSteamID clientId, MultiplayerManager.KickResponse response)
|
||||
{
|
||||
byte[] data = [(byte)response];
|
||||
P2PPackageHandler.Instance.SendP2PPacketToUser(clientID, data, P2PPackageHandler.MsgType.KickPlayer, EP2PSend.k_EP2PSendReliable, 0);
|
||||
P2PPackageHandler.Instance.SendP2PPacketToUser(
|
||||
clientId,
|
||||
data,
|
||||
P2PPackageHandler.MsgType.KickPlayer,
|
||||
EP2PSend.k_EP2PSendReliable,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当大厅成员变化时更新成员列表
|
||||
/// </summary>
|
||||
public static void OnLobbyMembersChanged()
|
||||
{
|
||||
Debug.Log("OnLobbyMembersChanged被触发");
|
||||
// 使用锁保护集合操作
|
||||
lock (lobbyMemberLock)
|
||||
Debug.Log("大厅成员列表已更新");
|
||||
|
||||
lock (LobbyMemberLock)
|
||||
{
|
||||
// 清空当前成员列表
|
||||
lobbyMemberList.Clear();
|
||||
|
||||
// 重新构建当前成员列表
|
||||
int lobbyMemberLimit = SteamMatchmaking.GetLobbyMemberLimit(CurrentLobbyID);
|
||||
for (int i = 0; i < lobbyMemberLimit; i++)
|
||||
int memberLimit = SteamMatchmaking.GetLobbyMemberLimit(CurrentLobbyID);
|
||||
for (int i = 0; i < memberLimit; i++)
|
||||
{
|
||||
PlayerInfo lobbyMember = new(SteamMatchmaking.GetLobbyMemberByIndex(CurrentLobbyID, i));
|
||||
// 合并记录状态
|
||||
// 注意这里先合并普通记录再合并黑名单的顺序是必要的
|
||||
// 因为黑名单记录的曾用名必然比普通记录的曾用名更新
|
||||
playerRecoder.CheckAndMerge(lobbyMember);
|
||||
blackListRecorder.CheckAndMerge(lobbyMember);
|
||||
lobbyMemberList.Add(lobbyMember);
|
||||
PlayerInfo member = new(SteamMatchmaking.GetLobbyMemberByIndex(CurrentLobbyID, i));
|
||||
|
||||
// 合并记录状态:先合并普通记录,再合并黑名单
|
||||
// 黑名单的曾用名比普通记录的曾用名更新
|
||||
playerRecorder.CheckAndMerge(member);
|
||||
blackListRecorder.CheckAndMerge(member);
|
||||
|
||||
lobbyMemberList.Add(member);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
10
Recorder/BlackListRecorder.cs
Normal file
10
Recorder/BlackListRecorder.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace stick.plugins.playermanager.Recorder;
|
||||
|
||||
/// <summary>
|
||||
/// 黑名单记录器,用于记录需要屏蔽的玩家
|
||||
/// </summary>
|
||||
public class BlackListRecorder : RecorderBase
|
||||
{
|
||||
protected override string FileName => "BlackList.txt";
|
||||
protected override PlayerRecordStatus RecordStatus => PlayerRecordStatus.Blacklisted;
|
||||
}
|
||||
227
Recorder/PlayerInfo.cs
Normal file
227
Recorder/PlayerInfo.cs
Normal file
@@ -0,0 +1,227 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using Steamworks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace stick.plugins.playermanager.Recorder;
|
||||
|
||||
/// <summary>
|
||||
/// 玩家信息类
|
||||
/// 封装玩家的Steam ID、名称和记录状态
|
||||
/// </summary>
|
||||
public class PlayerInfo : IEquatable<PlayerInfo>
|
||||
{
|
||||
#region 属性
|
||||
|
||||
/// <summary>
|
||||
/// 玩家的Steam ID
|
||||
/// </summary>
|
||||
public CSteamID SteamId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 记录的玩家名字(通常是曾用名)
|
||||
/// </summary>
|
||||
public string RecordedName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示记录的名称
|
||||
/// </summary>
|
||||
public bool DisplayNameRecorded { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否有记录的名称(与当前名称不同)
|
||||
/// </summary>
|
||||
public bool HasRecordedName => !string.IsNullOrEmpty(RecordedName) && RecordedName != CurrentName;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为好友
|
||||
/// </summary>
|
||||
public bool IsFriend => SteamFriends.HasFriend(SteamId, EFriendFlags.k_EFriendFlagImmediate);
|
||||
|
||||
/// <summary>
|
||||
/// 记录状态
|
||||
/// </summary>
|
||||
public PlayerRecordStatus RecordStatus { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 柴自己的NetworkSpawnID
|
||||
/// </summary>
|
||||
public ushort NetworkSpawnID => PlayerManager.GetNetworkSpawnIDBySteamID(SteamId);
|
||||
|
||||
/// <summary>
|
||||
/// 当前玩家名称(实时获取)
|
||||
/// </summary>
|
||||
public string CurrentName
|
||||
{
|
||||
get
|
||||
{
|
||||
if (SteamManager.Initialized)
|
||||
{
|
||||
return SteamFriends.GetFriendPersonaName(SteamId);
|
||||
}
|
||||
return RecordedName ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 构造函数
|
||||
|
||||
/// <summary>
|
||||
/// 使用Steam ID和名称构造
|
||||
/// </summary>
|
||||
public PlayerInfo(CSteamID steamId, string playerName)
|
||||
{
|
||||
SteamId = steamId;
|
||||
RecordedName = playerName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用Steam ID构造,自动获取名称
|
||||
/// </summary>
|
||||
public PlayerInfo(CSteamID steamId)
|
||||
{
|
||||
SteamId = steamId;
|
||||
RecordedName = SteamFriends.GetFriendPersonaName(steamId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用字符串构造(格式: "SteamID 玩家名称")
|
||||
/// </summary>
|
||||
public PlayerInfo(string playerInfo)
|
||||
{
|
||||
if (string.IsNullOrEmpty(playerInfo))
|
||||
{
|
||||
SteamId = CSteamID.Nil;
|
||||
RecordedName = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
Parse(playerInfo, out ulong steamIdValue, out string playerNameValue);
|
||||
SteamId = new CSteamID(steamIdValue);
|
||||
RecordedName = playerNameValue;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 解析方法
|
||||
|
||||
/// <summary>
|
||||
/// 解析玩家信息字符串
|
||||
/// </summary>
|
||||
public static void Parse(string input, out ulong steamId, out string playerName)
|
||||
{
|
||||
int spaceIndex = input.IndexOf(' ');
|
||||
|
||||
if (spaceIndex == -1)
|
||||
{
|
||||
steamId = ulong.Parse(input);
|
||||
playerName = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
string steamIdPart = input.Substring(0, spaceIndex);
|
||||
string namePart = input.Substring(spaceIndex + 1);
|
||||
|
||||
steamId = ulong.Parse(steamIdPart);
|
||||
playerName = namePart;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试解析玩家信息字符串
|
||||
/// </summary>
|
||||
public static bool TryParse(string input, out ulong steamId, out string playerName)
|
||||
{
|
||||
steamId = 0;
|
||||
playerName = string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
Parse(input, out steamId, out playerName);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 合并方法
|
||||
|
||||
/// <summary>
|
||||
/// 合并另一个玩家信息
|
||||
/// </summary>
|
||||
public void MergeWith(PlayerInfo other)
|
||||
{
|
||||
if (other is null) return;
|
||||
|
||||
if (SteamId != other.SteamId)
|
||||
{
|
||||
Debug.LogWarning($"尝试合并不同Steam ID的玩家信息: {SteamId} 和 {other.SteamId}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(other.RecordedName))
|
||||
{
|
||||
RecordedName = other.RecordedName;
|
||||
}
|
||||
|
||||
if (other.RecordStatus > RecordStatus)
|
||||
{
|
||||
RecordStatus = other.RecordStatus;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 字符串转换
|
||||
|
||||
/// <summary>
|
||||
/// 转换为文件存储格式
|
||||
/// </summary>
|
||||
public string ToInfoString()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(RecordedName))
|
||||
{
|
||||
return $"{SteamId} {RecordedName}";
|
||||
}
|
||||
return $"{SteamId} {CurrentName}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转换为显示字符串
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
if (DisplayNameRecorded && !string.IsNullOrEmpty(RecordedName))
|
||||
{
|
||||
return $"{SteamId} {RecordedName}";
|
||||
}
|
||||
return $"{SteamId} {CurrentName}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 相等性比较
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否与另一个玩家信息相等
|
||||
/// </summary>
|
||||
public bool Equals(PlayerInfo other)
|
||||
{
|
||||
return other is not null && SteamId == other.SteamId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取哈希码
|
||||
/// </summary>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return SteamId.GetHashCode();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
10
Recorder/PlayerRecorder.cs
Normal file
10
Recorder/PlayerRecorder.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace stick.plugins.playermanager.Recorder;
|
||||
|
||||
/// <summary>
|
||||
/// 玩家记录器,用于记录普通玩家信息
|
||||
/// </summary>
|
||||
public class PlayerRecorder : RecorderBase
|
||||
{
|
||||
protected override string FileName => "CustomPlayerRecord.txt";
|
||||
protected override PlayerRecordStatus RecordStatus => PlayerRecordStatus.Recorded;
|
||||
}
|
||||
187
Recorder/RecorderBase.cs
Normal file
187
Recorder/RecorderBase.cs
Normal file
@@ -0,0 +1,187 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using MultiplayerBasicExample;
|
||||
using Steamworks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace stick.plugins.playermanager.Recorder;
|
||||
|
||||
/// <summary>
|
||||
/// 记录器基类
|
||||
/// 提供玩家记录和黑名单功能的通用实现
|
||||
/// 使用 Dictionary<CSteamID, PlayerInfo> 实现 O(1) 查找
|
||||
/// </summary>
|
||||
public abstract class RecorderBase
|
||||
{
|
||||
#region 字段
|
||||
|
||||
/// <summary>
|
||||
/// 玩家信息字典(以 SteamID 为键,O(1) 查找)
|
||||
/// </summary>
|
||||
protected Dictionary<CSteamID, PlayerInfo> playerDict = new Dictionary<CSteamID, PlayerInfo>();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 抽象属性
|
||||
|
||||
/// <summary>
|
||||
/// 记录文件名
|
||||
/// </summary>
|
||||
protected abstract string FileName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 记录状态类型
|
||||
/// </summary>
|
||||
protected abstract PlayerRecordStatus RecordStatus { get; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region 属性
|
||||
|
||||
/// <summary>
|
||||
/// 记录文件完整路径
|
||||
/// </summary>
|
||||
protected string FilePath => Path.Combine(Application.dataPath, FileName);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 构造函数
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数,初始化时加载已有记录
|
||||
/// </summary>
|
||||
protected RecorderBase()
|
||||
{
|
||||
RefreshPlayerRecordList();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 公共方法
|
||||
|
||||
/// <summary>
|
||||
/// 检查玩家是否已被记录
|
||||
/// </summary>
|
||||
public bool IsPlayerRecorded(PlayerInfo playerInfo)
|
||||
{
|
||||
return playerInfo != null && playerDict.ContainsKey(playerInfo.SteamId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查玩家是否已被记录(通过Steam ID)
|
||||
/// </summary>
|
||||
public bool IsPlayerRecorded(CSteamID playerId)
|
||||
{
|
||||
return playerDict.ContainsKey(playerId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录玩家
|
||||
/// </summary>
|
||||
public void RecordPlayer(PlayerInfo playerInfo)
|
||||
{
|
||||
if (playerInfo == null) return;
|
||||
|
||||
playerInfo.RecordStatus = RecordStatus;
|
||||
|
||||
if (!playerDict.ContainsKey(playerInfo.SteamId))
|
||||
{
|
||||
playerDict.Add(playerInfo.SteamId, playerInfo);
|
||||
}
|
||||
|
||||
AppendToFile(playerInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消记录玩家
|
||||
/// </summary>
|
||||
public void CancelRecordPlayer(PlayerInfo playerInfo)
|
||||
{
|
||||
if (playerInfo == null) return;
|
||||
|
||||
if (playerDict.Remove(playerInfo.SteamId))
|
||||
{
|
||||
WriteBackToFile();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查并合并玩家记录信息(O(1) 字典查找)
|
||||
/// </summary>
|
||||
public void CheckAndMerge(PlayerInfo playerInfo)
|
||||
{
|
||||
if (playerInfo == null) return;
|
||||
|
||||
if (playerDict.TryGetValue(playerInfo.SteamId, out PlayerInfo existingInfo))
|
||||
{
|
||||
playerInfo.MergeWith(existingInfo);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 保护方法
|
||||
|
||||
/// <summary>
|
||||
/// 刷新玩家记录列表(从文件加载)
|
||||
/// </summary>
|
||||
protected void RefreshPlayerRecordList()
|
||||
{
|
||||
if (!File.Exists(FilePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
playerDict.Clear();
|
||||
|
||||
using (StreamReader reader = new StreamReader(FilePath))
|
||||
{
|
||||
string line;
|
||||
while ((line = reader.ReadLine()) != null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(line)) continue;
|
||||
|
||||
PlayerInfo playerInfo = new PlayerInfo(line)
|
||||
{
|
||||
RecordStatus = RecordStatus
|
||||
};
|
||||
|
||||
if (!playerDict.ContainsKey(playerInfo.SteamId))
|
||||
{
|
||||
playerDict.Add(playerInfo.SteamId, playerInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将玩家信息写入文件
|
||||
/// </summary>
|
||||
protected void WriteBackToFile()
|
||||
{
|
||||
using (StreamWriter writer = new StreamWriter(FilePath, false))
|
||||
{
|
||||
foreach (PlayerInfo info in playerDict.Values)
|
||||
{
|
||||
writer.WriteLine(info.ToInfoString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 私有方法
|
||||
|
||||
/// <summary>
|
||||
/// 追加玩家信息到文件
|
||||
/// </summary>
|
||||
private void AppendToFile(PlayerInfo playerInfo)
|
||||
{
|
||||
using (StreamWriter writer = new StreamWriter(FilePath, true))
|
||||
{
|
||||
writer.WriteLine(playerInfo);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using MultiplayerBasicExample;
|
||||
using Steamworks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace stick.plugins.playermanager;
|
||||
|
||||
abstract public class RecoderBase
|
||||
{
|
||||
protected List<PlayerInfo> playerList = [];
|
||||
protected abstract string FileName { get; }
|
||||
protected abstract PlayerRecordStatus RecordStatus { get; }
|
||||
protected string FilePath => Path.Combine(Application.dataPath, FileName);
|
||||
public bool IsPlayerRecorded(PlayerInfo playerInfo)
|
||||
{
|
||||
foreach (PlayerInfo info in playerList)
|
||||
{
|
||||
if (info.Equals(playerInfo)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public bool IsPlayerRecorded(CSteamID playerId)
|
||||
{
|
||||
PlayerInfo playerInfo = new(playerId);
|
||||
foreach (PlayerInfo info in playerList)
|
||||
{
|
||||
if (info.Equals(playerInfo)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
public void RecordPlayer(PlayerInfo playerInfo)
|
||||
{
|
||||
playerInfo.RecordStatus = RecordStatus;
|
||||
playerList.Add(playerInfo);
|
||||
using StreamWriter streamWriter = new(FilePath, true);
|
||||
streamWriter.WriteLine(playerInfo);
|
||||
}
|
||||
protected void RefreshPlayerRecordList()
|
||||
{
|
||||
if (!File.Exists(FilePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
playerList.Clear();
|
||||
using StreamReader streamReader = new(FilePath);
|
||||
string item;
|
||||
while ((item = streamReader.ReadLine()) != null)
|
||||
{
|
||||
PlayerInfo playerInfo = new(item)
|
||||
{
|
||||
RecordStatus = RecordStatus
|
||||
};
|
||||
playerList.Add(playerInfo);
|
||||
}
|
||||
}
|
||||
protected void WriteBackToFile()
|
||||
{
|
||||
using StreamWriter streamWriter = new(FilePath, false);
|
||||
foreach (PlayerInfo info in playerList)
|
||||
{
|
||||
streamWriter.WriteLine(info.ToInfoString());
|
||||
}
|
||||
}
|
||||
public void CancelRecordPlayer(PlayerInfo playerInfo)
|
||||
{
|
||||
playerList.Remove(playerInfo);
|
||||
WriteBackToFile();
|
||||
}
|
||||
protected RecoderBase()
|
||||
{
|
||||
RefreshPlayerRecordList();
|
||||
}
|
||||
public void CheckAndMerge(PlayerInfo playerInfo)
|
||||
{
|
||||
if (playerInfo is null) return;
|
||||
for (int i = 0; i < playerList.Count; i++)
|
||||
{
|
||||
if (playerList[i].Equals(playerInfo))
|
||||
{
|
||||
playerInfo.MergeWith(playerList[i]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
189
readme.md
189
readme.md
@@ -5,8 +5,8 @@
|
||||
## 功能
|
||||
|
||||
- 房间查找
|
||||
- 转让房主
|
||||
- 玩家记录
|
||||
|
||||
- 被记录的玩家显示为黄名
|
||||
- 是steam好友的玩家显示为绿名(优先级更高)
|
||||
- 被拉黑的玩家显示为红名(优先级最高)
|
||||
@@ -14,9 +14,56 @@
|
||||
- 拉黑玩家,当你是房主的时候生效
|
||||
- 可选择开启,默认开启的防踢
|
||||
- 可配置的界面开启按键,默认为F2
|
||||
- 可以随时切换房间的公开性
|
||||
- 静音单个玩家
|
||||
- 一键加好友
|
||||
|
||||
## 作者
|
||||
|
||||
老狼老狼几点钟、Moncak、z7572
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v4.0.8
|
||||
|
||||
1. 添加了转让房主功能
|
||||
2. 现在如果某个按钮(行组件)对所有人都不显示,则不留空
|
||||
|
||||
### v4.0.7
|
||||
|
||||
1. 现在点击大厅列表中的按钮进入大厅会在列表中将该大厅用灰色样式标识,标记已进入过
|
||||
2. 现在不会在每帧都初始化一遍LocalPlayer *难道他真的是猪??*
|
||||
|
||||
### v4.0.6
|
||||
|
||||
1. 大厅搜索范围按钮现在可同步修改快速匹配的搜索范围
|
||||
2. 修复了禁音功能 *难道他真的是猪??*
|
||||
|
||||
### v4.0.5
|
||||
|
||||
1. 修复了加好友按钮始终不显示的问题 *难道他真的是猪??*
|
||||
|
||||
### v4.0.4
|
||||
|
||||
1. 房间列表现在在房间内也显示,但是点击无效
|
||||
2. 添加了加好友按钮
|
||||
3. 修复了禁音功能 *难道他真的是猪??*
|
||||
|
||||
### v4.0.3
|
||||
|
||||
1. 静音单个玩家
|
||||
2. 性能优化
|
||||
3. 提供了更多可选是否开启功能显示的配置项
|
||||
4. 提供按钮组件注册入口
|
||||
5. 更好的房间公开性切换
|
||||
|
||||
### v3.1.1
|
||||
|
||||
1. 添加了房间公开性切换功能
|
||||
2. 更改房间筛选条件现在会触发刷新列表
|
||||
3. 现在房间列表不会在房间内显示,防止误触导致的游戏状态不一致
|
||||
4. 一些重构和优化
|
||||
|
||||
### v3.0.3
|
||||
|
||||
1. 修复了kick按钮无论如何都会显示的问题(难道他真的是猪???!!?)
|
||||
@@ -64,3 +111,143 @@
|
||||
感谢老狼的原版插件
|
||||
|
||||
旧版插件用户可以通过把 `...\StickFightTheGame\StickFight_Data`下的 `CustomPlayerRecord.txt`文件移动到 `...\StickFightTheGame\BepInEx\config\`目录来保留以前的记录
|
||||
|
||||
## 接口文档
|
||||
|
||||
### 组件注册
|
||||
|
||||
v4.0.3 起开放 GUI 组件注册,允许其他插件向界面的 Header 区或玩家行插入自定义控件。
|
||||
|
||||
命名空间:`stick.plugins.playermanager.Gui`
|
||||
|
||||
---
|
||||
|
||||
#### `RegisterManager` — 注册/反注册 API
|
||||
|
||||
```csharp
|
||||
// 注册一个 Header 行(纵向排列,行内子组件横向排列)
|
||||
static void RegisterHeaderRow(IHeaderRow row);
|
||||
|
||||
// 注册一个玩家行组件(每个玩家行中横向排列)
|
||||
static void RegisterPlayerRow(IPlayerRowWidget widget);
|
||||
|
||||
// 按 Name 反注册
|
||||
static bool UnregisterHeaderRow(string name);
|
||||
static bool UnregisterPlayerRow(string name);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 玩家行组件
|
||||
|
||||
每个大厅成员对应一行,注册的组件在行内从左到右排列。两种组件:
|
||||
|
||||
**`PlayerRowButtonWidget`** — 可点击按钮
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ---------------------------- | ---- | ------------------------------- |
|
||||
| `Name` | `string` | ✓ | 唯一标识,用于反注册 |
|
||||
| `Width` | `float` | ✓ | 组件宽度(像素) |
|
||||
| `GetText` | `Func<PlayerInfo, string>` | ✓ | 按钮文字,`player` 为当前行玩家 |
|
||||
| `OnClick` | `Action<PlayerInfo>` | ✓ | 点击回调 |
|
||||
| `Condition` | `Func<PlayerInfo, bool>` | | 返回 `false` 时对该玩家隐藏 |
|
||||
| `Style` | `GUIStyle` | | 静态样式 |
|
||||
| `GetStyle` | `Func<PlayerInfo, GUIStyle>` | | 动态样式,优先于 `Style` |
|
||||
|
||||
**`PlayerRowLabelWidget`** — 只读文本
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------- | -------------------------- | ---- | ------------------- |
|
||||
| `Name` | `string` | ✓ | 唯一标识 |
|
||||
| `Width` | `float` | ✓ | 组件宽度(像素) |
|
||||
| `GetText` | `Func<PlayerInfo, string>` | ✓ | 显示文本 |
|
||||
| `Condition` | `Func<PlayerInfo, bool>` | | 返回 `false` 时隐藏 |
|
||||
| `Style` | `GUIStyle` | | 自定义样式 |
|
||||
|
||||
---
|
||||
|
||||
#### Header 组件
|
||||
|
||||
Header 区位于标题下方,每行可含多个子组件。两种子组件:
|
||||
|
||||
**`HeaderRow`** — 一个 Header 行
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------- | --------------------- | ---- | ----------------------- |
|
||||
| `Name` | `string` | ✓ | 唯一标识 |
|
||||
| `Children` | `List<IHeaderWidget>` | ✓ | 行内子组件列表 |
|
||||
| `Condition` | `Func<bool>` | | 返回 `false` 时整行隐藏 |
|
||||
|
||||
**`HeaderLabelWidget`** — 行内文本
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------- | -------------- | ---- | ------------------- |
|
||||
| `Name` | `string` | ✓ | 唯一标识 |
|
||||
| `Width` | `float` | ✓ | 组件宽度(像素) |
|
||||
| `Content` | `Func<string>` | ✓ | 显示文本 |
|
||||
| `Condition` | `Func<bool>` | | 返回 `false` 时隐藏 |
|
||||
| `Style` | `GUIStyle` | | 自定义样式 |
|
||||
|
||||
**`HeaderButtonWidget`** — 行内按钮
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
| ----------- | ------------ | ---- | ------------------- |
|
||||
| `Name` | `string` | ✓ | 唯一标识 |
|
||||
| `Width` | `float` | ✓ | 组件宽度(像素) |
|
||||
| `Text` | `string` | ✓ | 按钮文字 |
|
||||
| `Action` | `Action` | ✓ | 点击回调 |
|
||||
| `Condition` | `Func<bool>` | | 返回 `false` 时隐藏 |
|
||||
| `Style` | `GUIStyle` | | 自定义样式 |
|
||||
|
||||
---
|
||||
|
||||
#### 示例
|
||||
|
||||
```csharp
|
||||
using stick.plugins.playermanager.Gui;
|
||||
using stick.plugins.playermanager.Recorder;
|
||||
|
||||
// 玩家行按钮
|
||||
RegisterManager.RegisterPlayerRow(new PlayerRowButtonWidget
|
||||
{
|
||||
Name = "MyButton",
|
||||
GetText = player => "打招呼",
|
||||
OnClick = player => Debug.Log($"你好 {player}"),
|
||||
Width = 100f,
|
||||
Condition = player => !PlayerManager.IsLocalPlayer(player),
|
||||
});
|
||||
|
||||
// 玩家行标签
|
||||
RegisterManager.RegisterPlayerRow(new PlayerRowLabelWidget
|
||||
{
|
||||
Name = "MyLabel",
|
||||
GetText = player => $"#{player.SteamId}",
|
||||
Width = 150f,
|
||||
Style = GUIStyles.Label,
|
||||
});
|
||||
|
||||
// Header 行(含标签 + 按钮)
|
||||
RegisterManager.RegisterHeaderRow(new HeaderRow
|
||||
{
|
||||
Name = "MyHeader",
|
||||
Condition = () => PlayerManager.IsHost(PlayerManager.LocalPlayer),
|
||||
Children = new List<IHeaderWidget>
|
||||
{
|
||||
new HeaderLabelWidget
|
||||
{
|
||||
Name = "Status",
|
||||
Content = () => "状态: 运行中",
|
||||
Width = 150f,
|
||||
},
|
||||
new HeaderButtonWidget
|
||||
{
|
||||
Name = "Action",
|
||||
Text = "执行",
|
||||
Action = () => Debug.Log("点击"),
|
||||
Width = 100f,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// 反注册
|
||||
RegisterManager.UnregisterPlayerRow("MyButton");
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<TargetFramework>net35</TargetFramework>
|
||||
<AssemblyName>stick.plugins.playermanager</AssemblyName>
|
||||
<Product>PlayerManager</Product>
|
||||
<Version>3.0.4</Version>
|
||||
<Version>4.0.8</Version>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<RestoreAdditionalProjectSources>
|
||||
|
||||
Reference in New Issue
Block a user