7 Commits

Author SHA1 Message Date
Yue-bin
8a36e74c4b chore(PlayerManager): 更新插件版本号从4.0.2到4.0.3
All checks were successful
Publish Release / build (push) Successful in 30s
- 将stick.plugins.playermanager.csproj中的版本号从4.0.2升级至4.0.3
- 为新版本发布做准备
2026-05-07 05:49:24 +08:00
Yue-bin
4b9c831c73 📃 docs(Readme): v4.0.3 更新日志以及接口文档 2026-05-07 05:49:16 +08:00
Yue-bin
910c407e3d feat(gui): 添加禁音按钮功能
添加了禁音按钮组件,允许用户对非主机和非本地玩家进行静音操作,
包括静音状态显示、点击事件处理等功能,并在配置中添加了是否显示
该按钮的选项。

Co-authored-by: Copilot <copilot@github.com>
2026-05-07 05:39:48 +08:00
Yue-bin
56caddcb65 refactor(Gui): 提取GUI样式管理类并重构样式引用
- 创建新的GUIStyles静态类统一管理所有GUIStyle实例
- 将原来分散在各个组件中的样式定义移到GUIStyles类中
- 移除PlayerInfoButton、PlayerManagerGUI和Widget中的重复样式代码
- 更新所有组件使用GUIStyles类的静态样式属性
- 优化样式初始化逻辑,避免重复创建GUIStyle实例
2026-05-07 05:29:07 +08:00
Yue-bin
c8abc7c265 feat(Recorder): 将玩家信息存储从HashSet改为Dictionary以实现O(1)查找
将RecorderBase中的数据结构从HashSet和List改为Dictionary<CSteamID, PlayerInfo>,
实现了O(1)时间复杂度的玩家信息查找操作,提高了IsPlayerRecorded、AddPlayerRecord等
核心方法的性能。同时优化了CheckAndMerge方法的实现,避免了遍历操作。
2026-05-07 05:17:04 +08:00
Yue-bin
1dc30bdabd perf(playermanager): 延迟初始化本地玩家信息以等待Steam准备就绪
- 添加System.Collections引用以支持协程操作
- 将LocalPlayer从立即初始化改为延迟初始化
- 实现InitAfterSteam协程来等待SteamManager初始化完成
- 在Start方法中启动初始化协程
- 在Update方法中持续更新LocalPlayer当Steam已就绪时

Co-authored-by: Copilot <copilot@github.com>
2026-05-07 05:12:35 +08:00
Yue-bin
14db933880 refactor(playermanager): 移除HostLeverHandler静态属性和日志补丁
移除了PlayerManager中的HostLeverHandler静态属性,该属性用于查找HostLeverHandler实例,
同时删除了相关的日志记录补丁LogLeverAngle,清理了不必要的调试代码。
2026-05-07 05:01:01 +08:00
11 changed files with 345 additions and 186 deletions

View File

@@ -40,5 +40,8 @@ public static class DefaultWidgets
if (PlayerManager.ShowNameToggleButton.Value) if (PlayerManager.ShowNameToggleButton.Value)
RegisterManager.RegisterPlayerRow(InternalWidgets.NameToggleButton.Create()); RegisterManager.RegisterPlayerRow(InternalWidgets.NameToggleButton.Create());
if (PlayerManager.ShowMuteButton.Value)
RegisterManager.RegisterPlayerRow(InternalWidgets.MuteButton.Create());
} }
} }

50
Gui/GUIStyles.cs Normal file
View File

@@ -0,0 +1,50 @@
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>黄色按钮样式(已记录玩家)</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 };
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;
}
}

View File

@@ -0,0 +1,31 @@
using MultiplayerBasicExample;
namespace stick.plugins.playermanager.Gui.InternalWidgets;
public static class MuteButton
{
public static IPlayerRowWidget Create()
{
return new PlayerRowButtonWidget
{
Name = "mute",
Condition = player =>
!PlayerManager.IsHost(player)
&& !PlayerManager.IsLocalPlayer(player),
GetText = player =>
PlayerManager.IsMuted(player) ? "取消禁音" : "禁音",
OnClick = player =>
{
if (PlayerManager.IsMuted(player))
{
PlayerManager.UnmutePlayer(player);
}
else
{
PlayerManager.MutePlayer(player);
}
},
Width = 100f,
};
}
}

View File

@@ -6,23 +6,6 @@ namespace stick.plugins.playermanager.Gui.InternalWidgets;
public static class PlayerInfoButton public static class PlayerInfoButton
{ {
private static GUIStyle _redStyle;
private static GUIStyle _yellowStyle;
private static GUIStyle _greenStyle;
private static GUIStyle _normalStyle;
private static void EnsureStyles()
{
if (_normalStyle != null) return;
_normalStyle = new GUIStyle(GUI.skin.button) { fontSize = 22, richText = false };
_yellowStyle = new GUIStyle(GUI.skin.button) { fontSize = 22, richText = false };
_yellowStyle.normal.textColor = Color.yellow;
_redStyle = new GUIStyle(GUI.skin.button) { fontSize = 22, richText = false };
_redStyle.normal.textColor = Color.red;
_greenStyle = new GUIStyle(GUI.skin.button) { fontSize = 22, richText = false };
_greenStyle.normal.textColor = Color.green;
}
public static IPlayerRowWidget Create() public static IPlayerRowWidget Create()
{ {
return new PlayerRowButtonWidget return new PlayerRowButtonWidget
@@ -37,12 +20,11 @@ public static class PlayerInfoButton
Width = 500f, Width = 500f,
GetStyle = player => GetStyle = player =>
{ {
EnsureStyles();
return player.RecordStatus switch return player.RecordStatus switch
{ {
PlayerRecordStatus.Blacklisted => _redStyle, PlayerRecordStatus.Blacklisted => GUIStyles.RedButton,
PlayerRecordStatus.Recorded => _yellowStyle, PlayerRecordStatus.Recorded => GUIStyles.YellowButton,
_ => player.IsFriend ? _greenStyle : _normalStyle, _ => player.IsFriend ? GUIStyles.GreenButton : GUIStyles.Button,
}; };
}, },
}; };

View File

@@ -32,63 +32,6 @@ public static class PlayerManagerGUI
#endregion #endregion
#region GUIStyle缓存internal DefaultWidgets
/// <summary>
/// 标题样式
/// </summary>
private static GUIStyle TitleStyle;
/// <summary>
/// 普通按钮样式
/// </summary>
private static GUIStyle CommonButtonStyle;
/// <summary>
/// 黄色按钮样式(已记录玩家)
/// </summary>
private static GUIStyle YellowButtonStyle;
/// <summary>
/// 红色按钮样式(黑名单玩家)
/// </summary>
private static GUIStyle RedButtonStyle;
/// <summary>
/// 绿色按钮样式(好友)
/// </summary>
private static GUIStyle GreenButtonStyle;
/// <summary>
/// 样式是否已初始化
/// </summary>
private static bool StylesInitialized;
#endregion
#region
private static void InitializeStyles()
{
if (StylesInitialized) return;
TitleStyle = new GUIStyle(GUI.skin.label) { fontSize = 22 };
CommonButtonStyle = new GUIStyle(GUI.skin.button) { fontSize = 22, richText = false };
YellowButtonStyle = new GUIStyle(GUI.skin.button) { fontSize = 22, richText = false };
YellowButtonStyle.normal.textColor = Color.yellow;
RedButtonStyle = new GUIStyle(GUI.skin.button) { fontSize = 22, richText = false };
RedButtonStyle.normal.textColor = Color.red;
GreenButtonStyle = new GUIStyle(GUI.skin.button) { fontSize = 22, richText = false };
GreenButtonStyle.normal.textColor = Color.green;
StylesInitialized = true;
}
#endregion
#region #region
/// <summary> /// <summary>
@@ -96,7 +39,7 @@ public static class PlayerManagerGUI
/// </summary> /// </summary>
public static void DrawLobbyInfoGUI() public static void DrawLobbyInfoGUI()
{ {
InitializeStyles(); GUIStyles.Initialize();
Cursor.Reset(); Cursor.Reset();
if (PlayerManager.MatchmakingHandler.IsInsideLobby) if (PlayerManager.MatchmakingHandler.IsInsideLobby)
@@ -108,7 +51,7 @@ public static class PlayerManagerGUI
GUI.Label( GUI.Label(
Cursor.NextLine(500f, 40f), Cursor.NextLine(500f, 40f),
"当前不在大厅中", "当前不在大厅中",
TitleStyle GUIStyles.Label
); );
DrawLobbySearchInfo(); DrawLobbySearchInfo();
} }
@@ -128,7 +71,7 @@ public static class PlayerManagerGUI
GUI.Label( GUI.Label(
Cursor.NextLine(2000f, 40f), Cursor.NextLine(2000f, 40f),
$"当前大厅: {PlayerManager.CurrentLobbyID} 房主: {PlayerManager.LobbyOwner.CurrentName}", $"当前大厅: {PlayerManager.CurrentLobbyID} 房主: {PlayerManager.LobbyOwner.CurrentName}",
TitleStyle GUIStyles.Label
); );
// HeaderRow 区域:纵向迭代每一行,行内横向迭代 Children // HeaderRow 区域:纵向迭代每一行,行内横向迭代 Children
@@ -175,7 +118,7 @@ public static class PlayerManagerGUI
if (GUI.Button( if (GUI.Button(
Cursor.NextLine(400f, 40f), Cursor.NextLine(400f, 40f),
$"搜索范围 {areaText}", $"搜索范围 {areaText}",
CommonButtonStyle GUIStyles.Button
)) ))
{ {
CycleLobbyDistanceFilter(); CycleLobbyDistanceFilter();
@@ -186,7 +129,7 @@ public static class PlayerManagerGUI
if (GUI.Button( if (GUI.Button(
new Rect(Cursor.OffsetX + 420f, Cursor.CurrentY, 100f, 40f), new Rect(Cursor.OffsetX + 420f, Cursor.CurrentY, 100f, 40f),
"刷新", "刷新",
CommonButtonStyle GUIStyles.Button
)) ))
{ {
PlayerManager.RequestLobbyList(); PlayerManager.RequestLobbyList();
@@ -196,7 +139,7 @@ public static class PlayerManagerGUI
GUI.Label( GUI.Label(
Cursor.NextLine(500f, 40f), Cursor.NextLine(500f, 40f),
$"搜索到大厅数量: {PlayerManager.lobbyDataList.Count}", $"搜索到大厅数量: {PlayerManager.lobbyDataList.Count}",
TitleStyle GUIStyles.Label
); );
// 大厅列表滚动视图 // 大厅列表滚动视图
@@ -247,7 +190,7 @@ public static class PlayerManagerGUI
if (GUI.Button( if (GUI.Button(
new Rect(Cursor.OffsetX, Cursor.LineHeight * i, 500f, 40f), new Rect(Cursor.OffsetX, Cursor.LineHeight * i, 500f, 40f),
$"{lobbyId} 人数{SteamMatchmaking.GetNumLobbyMembers(lobbyId)}", $"{lobbyId} 人数{SteamMatchmaking.GetNumLobbyMembers(lobbyId)}",
CommonButtonStyle GUIStyles.Button
)) ))
{ {
PlayerManager.JoinSpecificServer(lobbyId); PlayerManager.JoinSpecificServer(lobbyId);

View File

@@ -96,17 +96,6 @@ public interface IPlayerRowWidget
/// </summary> /// </summary>
public class HeaderLabelWidget : IHeaderWidget public class HeaderLabelWidget : IHeaderWidget
{ {
private static GUIStyle _defaultStyle;
private static GUIStyle DefaultStyle
{
get
{
if (_defaultStyle == null)
_defaultStyle = new GUIStyle(GUI.skin.label) { fontSize = 22 };
return _defaultStyle;
}
}
public string Name { get; set; } public string Name { get; set; }
public Func<bool> Condition { get; set; } public Func<bool> Condition { get; set; }
public Func<string> Content { get; set; } public Func<string> Content { get; set; }
@@ -117,7 +106,7 @@ public class HeaderLabelWidget : IHeaderWidget
public void Draw(Rect rect) public void Draw(Rect rect)
{ {
GUI.Label(rect, Content?.Invoke() ?? string.Empty, Style ?? DefaultStyle); GUI.Label(rect, Content?.Invoke() ?? string.Empty, Style ?? GUIStyles.Label);
} }
} }
@@ -126,17 +115,6 @@ public class HeaderLabelWidget : IHeaderWidget
/// </summary> /// </summary>
public class HeaderButtonWidget : IHeaderWidget public class HeaderButtonWidget : IHeaderWidget
{ {
private static GUIStyle _defaultStyle;
private static GUIStyle DefaultStyle
{
get
{
if (_defaultStyle == null)
_defaultStyle = new GUIStyle(GUI.skin.button) { fontSize = 22, richText = false };
return _defaultStyle;
}
}
public string Name { get; set; } public string Name { get; set; }
public Func<bool> Condition { get; set; } public Func<bool> Condition { get; set; }
public string Text { get; set; } public string Text { get; set; }
@@ -148,7 +126,7 @@ public class HeaderButtonWidget : IHeaderWidget
public void Draw(Rect rect) public void Draw(Rect rect)
{ {
if (GUI.Button(rect, Text ?? string.Empty, Style ?? DefaultStyle)) if (GUI.Button(rect, Text ?? string.Empty, Style ?? GUIStyles.Button))
Action?.Invoke(); Action?.Invoke();
} }
} }
@@ -181,17 +159,6 @@ public class HeaderRow : IHeaderRow
/// </summary> /// </summary>
public class PlayerRowButtonWidget : IPlayerRowWidget public class PlayerRowButtonWidget : IPlayerRowWidget
{ {
private static GUIStyle _defaultStyle;
private static GUIStyle DefaultStyle
{
get
{
if (_defaultStyle == null)
_defaultStyle = new GUIStyle(GUI.skin.button) { fontSize = 22, richText = false };
return _defaultStyle;
}
}
public string Name { get; set; } public string Name { get; set; }
public Func<PlayerInfo, bool> Condition { get; set; } public Func<PlayerInfo, bool> Condition { get; set; }
public Func<PlayerInfo, string> GetText { get; set; } public Func<PlayerInfo, string> GetText { get; set; }
@@ -212,7 +179,7 @@ public class PlayerRowButtonWidget : IPlayerRowWidget
public void Draw(Rect rect, PlayerInfo player) public void Draw(Rect rect, PlayerInfo player)
{ {
GUIStyle style = GetStyle?.Invoke(player) ?? Style ?? DefaultStyle; GUIStyle style = GetStyle?.Invoke(player) ?? Style ?? GUIStyles.Button;
if (GUI.Button(rect, GetText?.Invoke(player) ?? string.Empty, style)) if (GUI.Button(rect, GetText?.Invoke(player) ?? string.Empty, style))
OnClick?.Invoke(player); OnClick?.Invoke(player);
} }
@@ -223,17 +190,6 @@ public class PlayerRowButtonWidget : IPlayerRowWidget
/// </summary> /// </summary>
public class PlayerRowLabelWidget : IPlayerRowWidget public class PlayerRowLabelWidget : IPlayerRowWidget
{ {
private static GUIStyle _defaultStyle;
private static GUIStyle DefaultStyle
{
get
{
if (_defaultStyle == null)
_defaultStyle = new GUIStyle(GUI.skin.label) { fontSize = 22 };
return _defaultStyle;
}
}
public string Name { get; set; } public string Name { get; set; }
public Func<PlayerInfo, bool> Condition { get; set; } public Func<PlayerInfo, bool> Condition { get; set; }
public Func<PlayerInfo, string> GetText { get; set; } public Func<PlayerInfo, string> GetText { get; set; }
@@ -244,7 +200,7 @@ public class PlayerRowLabelWidget : IPlayerRowWidget
public void Draw(Rect rect, PlayerInfo player) public void Draw(Rect rect, PlayerInfo player)
{ {
GUI.Label(rect, GetText?.Invoke(player) ?? string.Empty, Style ?? DefaultStyle); GUI.Label(rect, GetText?.Invoke(player) ?? string.Empty, Style ?? GUIStyles.Label);
} }
} }

View File

@@ -1,20 +0,0 @@
using HarmonyLib;
using stick.plugins.playermanager;
using Steamworks;
using UnityEngine;
[HarmonyPatch(typeof(HostLeverHandler), "Update")]
class LogLeverAngle
{
static Vector3 lastForward = Vector3.zero;
public static void PostFix()
{
Vector3 currentForward = PlayerManager.HostLeverHandler.lever.transform.forward;
if (currentForward != lastForward)
{
Debug.Log("Current Lever Angle: " + currentForward);
lastForward = currentForward;
}
}
}

View File

@@ -9,6 +9,8 @@ using System.Data;
using System.Reflection; using System.Reflection;
using stick.plugins.playermanager.Recorder; using stick.plugins.playermanager.Recorder;
using stick.plugins.playermanager.Gui; using stick.plugins.playermanager.Gui;
using System.Collections;
using MultiplayerBasicExample;
namespace stick.plugins.playermanager; namespace stick.plugins.playermanager;
@@ -57,6 +59,11 @@ public class PlayerManager : BaseUnityPlugin
/// </summary> /// </summary>
internal static ConfigEntry<bool> ShowRoomPublicityButton; internal static ConfigEntry<bool> ShowRoomPublicityButton;
/// <summary>
/// 是否显示禁音按钮
/// </summary>
internal static ConfigEntry<bool> ShowMuteButton;
#endregion #endregion
#region Steam回调 #region Steam回调
@@ -117,11 +124,6 @@ public class PlayerManager : BaseUnityPlugin
"ChangeLobbyType" "ChangeLobbyType"
); );
/// <summary>
/// leverhandler实例
/// </summary>
public static HostLeverHandler HostLeverHandler => UnityEngine.Object.FindObjectOfType<HostLeverHandler>();
/// <summary> /// <summary>
/// 房间公开性 /// 房间公开性
/// </summary> /// </summary>
@@ -140,7 +142,29 @@ public class PlayerManager : BaseUnityPlugin
/// <summary> /// <summary>
/// 本地玩家信息 /// 本地玩家信息
/// </summary> /// </summary>
public static PlayerInfo LocalPlayer => new(SteamUser.GetSteamID()); public static PlayerInfo LocalPlayer;
/// <summary>
/// 初始化localPlayer和只需要初始化一次的steam相关的协程
/// </summary>
IEnumerator InitAfterSteam()
{
// 等待 Steam 初始化(给游戏一点初始化时间)
yield return new WaitForSeconds(2f);
// 或者轮询检查 Steam 是否就绪
while (!SteamManager.Initialized) // 如果游戏有 SteamManager
{
yield return null;
}
LocalPlayer = new(SteamUser.GetSteamID());
}
/// <summary>
/// 禁音玩家列表
/// </summary>
public static HashSet<PlayerInfo> MutedPlayers = [];
#endregion #endregion
@@ -153,9 +177,18 @@ public class PlayerManager : BaseUnityPlugin
DefaultWidgets.RegisterAll(); DefaultWidgets.RegisterAll();
} }
void Start()
{
StartCoroutine(InitAfterSteam());
}
private void Update() private void Update()
{ {
HandleInput(); HandleInput();
if (SteamManager.Initialized)
{
LocalPlayer = new(SteamUser.GetSteamID());
}
} }
private void OnGUI() private void OnGUI()
@@ -210,6 +243,13 @@ public class PlayerManager : BaseUnityPlugin
"是否显示曾用名切换按钮" "是否显示曾用名切换按钮"
); );
ShowMuteButton = Config.Bind(
"GUI",
"ShowMuteButton",
true,
"是否显示禁音按钮"
);
ShowRoomPublicityLabel = Config.Bind( ShowRoomPublicityLabel = Config.Bind(
"GUI", "GUI",
"ShowRoomPublicityLabel", "ShowRoomPublicityLabel",
@@ -270,6 +310,42 @@ public class PlayerManager : BaseUnityPlugin
#region #region
/// <summary>
/// 判断指定玩家是否被静音
/// </summary>
/// <param name="player">要检查的玩家</param>
/// <returns>如果被静音返回true</returns>
public static bool IsMuted(PlayerInfo player)
{
return player != null && MutedPlayers.Contains(player);
}
/// <summary>
/// 静音指定玩家
/// </summary>
/// <param name="player">被静音的玩家</param>
public static void MutePlayer(PlayerInfo player)
{
if (player != null)
{
MutedPlayers.Add(player);
Debug.Log($"玩家 {player} 已被静音");
}
}
/// <summary>
/// 取消静音指定玩家
/// </summary>
/// <param name="player">要取消静音的玩家</param>
public static void UnmutePlayer(PlayerInfo player)
{
if (player != null && MutedPlayers.Contains(player))
{
MutedPlayers.Remove(player);
Debug.Log($"玩家 {player} 已被取消静音");
}
}
/// <summary> /// <summary>
/// 判断指定玩家是否为房主 /// 判断指定玩家是否为房主
/// </summary> /// </summary>

View File

@@ -9,20 +9,16 @@ namespace stick.plugins.playermanager.Recorder;
/// <summary> /// <summary>
/// 记录器基类 /// 记录器基类
/// 提供玩家记录和黑名单功能的通用实现 /// 提供玩家记录和黑名单功能的通用实现
/// 使用 Dictionary<CSteamID, PlayerInfo> 实现 O(1) 查找
/// </summary> /// </summary>
public abstract class RecorderBase public abstract class RecorderBase
{ {
#region #region
/// <summary> /// <summary>
/// 玩家信息集合(用于快速查找) /// 玩家信息字典(以 SteamID 为键O(1) 查找)
/// </summary> /// </summary>
protected HashSet<PlayerInfo> playerSet = new HashSet<PlayerInfo>(); protected Dictionary<CSteamID, PlayerInfo> playerDict = new Dictionary<CSteamID, PlayerInfo>();
/// <summary>
/// 玩家信息列表(用于有序遍历和文件写入)
/// </summary>
protected List<PlayerInfo> playerList = new List<PlayerInfo>();
#endregion #endregion
@@ -68,7 +64,7 @@ public abstract class RecorderBase
/// </summary> /// </summary>
public bool IsPlayerRecorded(PlayerInfo playerInfo) public bool IsPlayerRecorded(PlayerInfo playerInfo)
{ {
return playerInfo != null && playerSet.Contains(playerInfo); return playerInfo != null && playerDict.ContainsKey(playerInfo.SteamId);
} }
/// <summary> /// <summary>
@@ -76,8 +72,7 @@ public abstract class RecorderBase
/// </summary> /// </summary>
public bool IsPlayerRecorded(CSteamID playerId) public bool IsPlayerRecorded(CSteamID playerId)
{ {
PlayerInfo playerInfo = new PlayerInfo(playerId); return playerDict.ContainsKey(playerId);
return playerSet.Contains(playerInfo);
} }
/// <summary> /// <summary>
@@ -89,9 +84,9 @@ public abstract class RecorderBase
playerInfo.RecordStatus = RecordStatus; playerInfo.RecordStatus = RecordStatus;
if (playerSet.Add(playerInfo)) if (!playerDict.ContainsKey(playerInfo.SteamId))
{ {
playerList.Add(playerInfo); playerDict.Add(playerInfo.SteamId, playerInfo);
} }
AppendToFile(playerInfo); AppendToFile(playerInfo);
@@ -104,27 +99,22 @@ public abstract class RecorderBase
{ {
if (playerInfo == null) return; if (playerInfo == null) return;
if (playerSet.Remove(playerInfo)) if (playerDict.Remove(playerInfo.SteamId))
{ {
playerList.Remove(playerInfo);
WriteBackToFile(); WriteBackToFile();
} }
} }
/// <summary> /// <summary>
/// 检查并合并玩家记录信息 /// 检查并合并玩家记录信息O(1) 字典查找)
/// </summary> /// </summary>
public void CheckAndMerge(PlayerInfo playerInfo) public void CheckAndMerge(PlayerInfo playerInfo)
{ {
if (playerInfo == null) return; if (playerInfo == null) return;
foreach (PlayerInfo existingInfo in playerSet) if (playerDict.TryGetValue(playerInfo.SteamId, out PlayerInfo existingInfo))
{
if (existingInfo.Equals(playerInfo))
{ {
playerInfo.MergeWith(existingInfo); playerInfo.MergeWith(existingInfo);
return;
}
} }
} }
@@ -142,8 +132,7 @@ public abstract class RecorderBase
return; return;
} }
playerSet.Clear(); playerDict.Clear();
playerList.Clear();
using (StreamReader reader = new StreamReader(FilePath)) using (StreamReader reader = new StreamReader(FilePath))
{ {
@@ -157,9 +146,9 @@ public abstract class RecorderBase
RecordStatus = RecordStatus RecordStatus = RecordStatus
}; };
if (playerSet.Add(playerInfo)) if (!playerDict.ContainsKey(playerInfo.SteamId))
{ {
playerList.Add(playerInfo); playerDict.Add(playerInfo.SteamId, playerInfo);
} }
} }
} }
@@ -172,7 +161,7 @@ public abstract class RecorderBase
{ {
using (StreamWriter writer = new StreamWriter(FilePath, false)) using (StreamWriter writer = new StreamWriter(FilePath, false))
{ {
foreach (PlayerInfo info in playerList) foreach (PlayerInfo info in playerDict.Values)
{ {
writer.WriteLine(info.ToInfoString()); writer.WriteLine(info.ToInfoString());
} }

149
readme.md
View File

@@ -15,9 +15,18 @@
- 可选择开启,默认开启的防踢 - 可选择开启,默认开启的防踢
- 可配置的界面开启按键默认为F2 - 可配置的界面开启按键默认为F2
- 可以随时切换房间的公开性 - 可以随时切换房间的公开性
- 静音单个玩家
## 更新日志 ## 更新日志
### v4.0.3
1. 静音单个玩家
1. 性能优化
1. 提供了更多可选是否开启功能显示的配置项
1. 提供按钮组件注册入口
1. 更好的房间公开性切换
### v3.1.1 ### v3.1.1
1. 添加了房间公开性切换功能 1. 添加了房间公开性切换功能
@@ -72,3 +81,143 @@
感谢老狼的原版插件 感谢老狼的原版插件
旧版插件用户可以通过把 `...\StickFightTheGame\StickFight_Data`下的 `CustomPlayerRecord.txt`文件移动到 `...\StickFightTheGame\BepInEx\config\`目录来保留以前的记录 旧版插件用户可以通过把 `...\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");

View File

@@ -4,7 +4,7 @@
<TargetFramework>net35</TargetFramework> <TargetFramework>net35</TargetFramework>
<AssemblyName>stick.plugins.playermanager</AssemblyName> <AssemblyName>stick.plugins.playermanager</AssemblyName>
<Product>PlayerManager</Product> <Product>PlayerManager</Product>
<Version>4.0.2</Version> <Version>4.0.3</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<RestoreAdditionalProjectSources> <RestoreAdditionalProjectSources>