29 Commits

Author SHA1 Message Date
Yue-bin
3adcdc46ba Merge branch 'master' of 172.16.29.13:Stick_Mods/stick.plugins.playermanager
All checks were successful
Publish Release / build (push) Successful in 31s
2026-06-11 02:13:25 +08:00
Yue-bin
b6edc70594 chore(ci): 为.NET构建流程添加代理配置
在Gitea工作流中添加HTTP/HTTPS代理环境变量配置,
以解决网络访问问题并提高依赖包恢复速度。
2026-06-11 02:13:09 +08:00
9e7fb4d443 doc: 大厅搜索范围按钮现在可同步修改快速匹配的搜索范围
Some checks failed
Publish Release / build (push) Failing after 4m9s
2026-06-10 19:09:02 +08:00
6ab8c38109 feat(matchmaking): 大厅搜索范围按钮现在可同步修改快速匹配的搜索范围 2026-06-10 18:55:01 +08:00
9a5d149241 fix(playermanager): 应用静音Patch 2026-06-10 18:54:41 +08:00
Yue-bin
f193d8a805 fix(playermanager): 修复加好友按钮显示问题并更新版本号至4.0.5
Some checks failed
Publish Release / build (push) Failing after 3s
- 在readme.md中添加v4.0.5更新日志,记录修复了加好友按钮始终不显示的问题
- 更新stick.plugins.playermanager.csproj中的版本号从4.0.4到4.0.5
2026-05-31 01:59:19 +08:00
Yue-bin
00960eb3f7 fix(Gui): 修复添加好友按钮的本地玩家检查逻辑
- 将 PlayerManager.IsLocalPlayer(PlayerManager.LocalPlayer)
  修改为 PlayerManager.IsLocalPlayer(player)
- 确保正确检查目标玩家是否为本地玩家,而非始终检查本地玩家自己
- 避免好友列表中出现本地玩家的错误情况
2026-05-31 01:58:30 +08:00
Yue-bin
bc2d44f5cd docs(readme): 更新文档添加新功能说明
Some checks failed
Publish Release / build (push) Failing after 4m0s
- 移除了重复的玩家名称颜色显示说明
- 添加了一键加好友功能描述
- 更新了更新日志,包含v4.0.4版本的新功能:
  1. 房间列表现在在房间内也显示,但是点击无效
  2. 添加了加好友按钮
  3. 修复了禁音功能
- 调整了v4.0.3版本更新内容的序号格式
2026-05-28 17:02:17 +08:00
Yue-bin
c065b14f9d chore(PlayerManager): 更新版本号到4.0.4
- 将项目版本从4.0.3更新至4.0.4
2026-05-28 16:58:42 +08:00
Yue-bin
46f05b5489 🐞 fix(mute): 修复了禁音无效的问题
难道他真的是猪?
2026-05-28 16:58:06 +08:00
Yue-bin
3a9e903044 feat(gui): 添加加好友按钮功能
新增了加好友按钮组件,允许玩家通过Steam界面添加其他玩家为好友。
该功能包含一个可配置的按钮,点击后会复制玩家信息到剪贴板并打开Steam好友添加界面。
同时增加了相应的配置选项来控制按钮的显示。

需要配置项ShowAddFriendButton来控制加好友按钮的显示状态,
默认设置为显示(true)。
2026-05-28 16:24:23 +08:00
Yue-bin
ca331d4ac8 feat(Gui): 添加大厅搜索信息显示功能
添加了大厅搜索信息的绘制功能,并优化了服务器加入逻辑,
避免在已有大厅的情况下重复加入服务器。
2026-05-28 16:09:47 +08:00
Yue-bin
692fc25d42 fix(Gui): 修改静音按钮逻辑
- 移除主机玩家的条件限制,仅保留本地玩家检查
- 将"禁音"/"取消禁音"文本改为更标准的"静音"/"取消静音"
2026-05-08 17:42:15 +08:00
Yue-bin
bec55e4ecc fix(PlayerManagerGUI): 修复玩家管理界面中空元素导致的布局错乱问题
当widget不应该绘制时,之前会跳过列位置更新导致后续元素对齐错误,
现在添加了占位推进列位置以确保布局的一致性。
2026-05-08 17:23:16 +08:00
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
Yue-bin
610cca5ef6 chore(PlayerManager): 更新插件版本号从4.0.1到4.0.2
Some checks failed
Publish Release / build (push) Failing after 5m16s
- 将stick.plugins.playermanager.csproj中的版本号从4.0.1升级到4.0.2
2026-05-07 04:54:19 +08:00
Yue-bin
d9e01bbf0f fix(PlayerManager): 修复房间公开性判断逻辑
- 移除HostLeverHandler依赖,直接通过MatchmakingHandler.LobbyType判断房间是否公开
- 修复LobbyType切换时的目标类型逻辑错误,现在正确地将公开房间设为ELobbyType.k_ELobbyTypePublic,
  私有房间设为ELobbyType.k_ELobbyTypeFriendsOnly
- 移除PlayerManagerGUI.InitializeStyles()调用,该方法已被默认组件注册替代
2026-05-07 04:53:36 +08:00
Yue-bin
31cf778524 refactor(Gui): 移除废弃样式引用并优化玩家信息按钮样式处理
移除了多个内部组件中的废弃XML文档注释和未使用的CommonButtonStyle引用,
为PlayerInfoButton添加了动态样式管理机制以替代静态样式引用。
2026-05-07 04:53:22 +08:00
Yue-bin
07d2320c71 refactor(gui): 将公开的GUI样式字段改为私有并优化样式管理
将PlayerManagerGUI中的公开静态样式字段改为私有访问修饰符,
同时为Widget组件添加默认样式属性,统一管理UI控件的外观,
提高代码封装性和样式的一致性
2026-05-07 04:53:09 +08:00
Yue-bin
ab0df1ee0d chore(playermanager): 更新版本号
All checks were successful
Publish Release / build (push) Successful in 2m3s
更新 stick.plugins.playermanager.csproj 中的版本号从 4.0.0 到 4.0.1,
以反映最新的发布版本。
2026-05-07 04:32:01 +08:00
Yue-bin
74f9e1cf1c fix(Gui): 修正LayoutCursor行进逻辑
BeginRow方法现在正确地推进到下一行,并重置列位置到OffsetX,
对齐原来的GetNewYPosition()行为。EndRow方法仅重置列位置而不行进,
下一行由BeginRow方法推进,使行进逻辑更加清晰明确。
2026-05-07 04:31:25 +08:00
Yue-bin
007c093df4 fix(Gui): 房间公开性组件不显示在同一行
- 创建List集合管理房间公开性相关的widgets
- 将房间公开性标签和按钮合并到同一个HeaderRow中
- 添加条件判断只在有相关widgets时才注册HeaderRow
- 使label和button可以在同一行独立配置显示
2026-05-07 04:31:17 +08:00
Yue-bin
fd208de2db fix(PlayerManager): 样式未被正确初始化
将PlayerManagerGUI.InitializeStyles()方法的访问修饰符从private改为internal,
并在插件启动时调用此方法来初始化GUI样式。
2026-05-07 04:30:39 +08:00
22 changed files with 512 additions and 189 deletions

View File

@@ -21,6 +21,11 @@ jobs:
- 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

View File

@@ -1,3 +1,5 @@
using System.Collections.Generic;
namespace stick.plugins.playermanager.Gui;
/// <summary>
@@ -9,12 +11,22 @@ public static class DefaultWidgets
{
public static void RegisterAll()
{
// Header 组件
// Header房间公开性行label + button 在同一行,各自独立可配置)
var publicityWidgets = new List<IHeaderWidget>();
if (PlayerManager.ShowRoomPublicityLabel.Value)
RegisterManager.RegisterHeaderRow(InternalWidgets.RoomPublicityLabel.Create());
publicityWidgets.Add(InternalWidgets.RoomPublicityLabel.Create());
if (PlayerManager.ShowRoomPublicityButton.Value)
RegisterManager.RegisterHeaderRow(InternalWidgets.RoomPublicityButton.Create());
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());
@@ -28,5 +40,10 @@ public static class DefaultWidgets
if (PlayerManager.ShowNameToggleButton.Value)
RegisterManager.RegisterPlayerRow(InternalWidgets.NameToggleButton.Create());
if (PlayerManager.ShowMuteButton.Value)
RegisterManager.RegisterPlayerRow(InternalWidgets.MuteButton.Create());
if (PlayerManager.ShowAddFriendButton.Value)
RegisterManager.RegisterPlayerRow(InternalWidgets.AddFriendButton.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,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,
};
}
}

View File

@@ -2,9 +2,6 @@ using MultiplayerBasicExample;
namespace stick.plugins.playermanager.Gui.InternalWidgets;
/// <summary>
/// 踢人按钮的纯工厂
/// </summary>
public static class KickButton
{
public static IPlayerRowWidget Create()
@@ -19,7 +16,6 @@ public static class KickButton
OnClick = player =>
PlayerManager.KickPlayer(player.SteamId, MultiplayerManager.KickResponse.DidNotRecievePackages),
Width = 100f,
Style = PlayerManagerGUI.CommonButtonStyle,
};
}
}

View 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,
};
}
}

View File

@@ -1,8 +1,5 @@
namespace stick.plugins.playermanager.Gui.InternalWidgets;
/// <summary>
/// 曾用名切换按钮的纯工厂
/// </summary>
public static class NameToggleButton
{
public static IPlayerRowWidget Create()
@@ -14,7 +11,6 @@ public static class NameToggleButton
GetText = player => player.DisplayNameRecorded ? "显示现用名" : "显示曾用名",
OnClick = player => player.DisplayNameRecorded = !player.DisplayNameRecorded,
Width = 120f,
Style = PlayerManagerGUI.CommonButtonStyle,
};
}
}

View File

@@ -1,13 +1,9 @@
using Steamworks;
using UnityEngine;
using stick.plugins.playermanager.Recorder;
using static stick.plugins.playermanager.Gui.PlayerManagerGUI;
namespace stick.plugins.playermanager.Gui.InternalWidgets;
/// <summary>
/// 玩家信息按钮的纯工厂
/// </summary>
public static class PlayerInfoButton
{
public static IPlayerRowWidget Create()
@@ -22,11 +18,14 @@ public static class PlayerInfoButton
SteamFriends.ActivateGameOverlayToUser("steamid", player.SteamId);
},
Width = 500f,
GetStyle = player => player.RecordStatus switch
GetStyle = player =>
{
PlayerRecordStatus.Blacklisted => RedButtonStyle,
PlayerRecordStatus.Recorded => YellowButtonStyle,
_ => player.IsFriend ? GreenButtonStyle : CommonButtonStyle,
return player.RecordStatus switch
{
PlayerRecordStatus.Blacklisted => GUIStyles.RedButton,
PlayerRecordStatus.Recorded => GUIStyles.YellowButton,
_ => player.IsFriend ? GUIStyles.GreenButton : GUIStyles.Button,
};
},
};
}

View File

@@ -3,9 +3,6 @@ using stick.plugins.playermanager.Recorder;
namespace stick.plugins.playermanager.Gui.InternalWidgets;
/// <summary>
/// 记录/拉黑按钮的纯工厂
/// </summary>
public static class RecordButton
{
public static IPlayerRowWidget Create()
@@ -41,7 +38,6 @@ public static class RecordButton
PlayerManager.OnLobbyMembersChanged();
},
Width = 100f,
Style = PlayerManagerGUI.CommonButtonStyle,
};
}
}

View File

@@ -1,29 +1,15 @@
using static stick.plugins.playermanager.Gui.PlayerManagerGUI;
namespace stick.plugins.playermanager.Gui.InternalWidgets;
/// <summary>
/// 切换公开性按钮的纯工厂
/// </summary>
public static class RoomPublicityButton
{
public static IHeaderRow Create()
public static IHeaderWidget Create()
{
return new HeaderRow
return new HeaderButtonWidget
{
Name = "RoomPublicityButton",
Condition = () => PlayerManager.IsHost(PlayerManager.LocalPlayer),
Children =
{
new HeaderButtonWidget
{
Name = "RoomPublicityButton.Content",
Text = "切换公开性",
Action = () => PlayerManager.ToggleLobbyPublicity(),
Width = 150f,
Style = CommonButtonStyle,
},
},
Text = "切换公开性",
Action = () => PlayerManager.ToggleLobbyPublicity(),
Width = 150f,
};
}
}

View File

@@ -1,28 +1,14 @@
using static stick.plugins.playermanager.Gui.PlayerManagerGUI;
namespace stick.plugins.playermanager.Gui.InternalWidgets;
/// <summary>
/// 房间类型标签的纯工厂
/// </summary>
public static class RoomPublicityLabel
{
public static IHeaderRow Create()
public static IHeaderWidget Create()
{
return new HeaderRow
return new HeaderLabelWidget
{
Name = "RoomPublicityLabel",
Condition = () => PlayerManager.IsHost(PlayerManager.LocalPlayer),
Children =
{
new HeaderLabelWidget
{
Name = "RoomPublicityLabel.Content",
Content = () => $"房间类型: {(PlayerManager.IsPublic ? "" : "")}",
Width = 200f,
Style = TitleStyle,
},
},
Content = () => $"房间类型: {(PlayerManager.IsPublic ? "" : "")}",
Width = 200f,
};
}
}

View File

@@ -42,10 +42,12 @@ public class LayoutCursor
}
/// <summary>
/// 开始新的一行,重置列位置到 OffsetX
/// 开始新的一行,推进到下一行并重置列位置到 OffsetX
/// 对齐原来的 GetNewYPosition() 行为
/// </summary>
public void BeginRow()
{
_currentLine++;
_currentX = OffsetX;
}
@@ -62,11 +64,10 @@ public class LayoutCursor
}
/// <summary>
/// 结束当前行,移动到下一行
/// 结束当前行,仅重置列位置(不行进,下一行由 BeginRow 推进)
/// </summary>
public void EndRow()
{
_currentLine++;
_currentX = OffsetX;
}

View File

@@ -32,63 +32,6 @@ public static class PlayerManagerGUI
#endregion
#region GUIStyle缓存internal DefaultWidgets
/// <summary>
/// 标题样式
/// </summary>
internal static GUIStyle TitleStyle;
/// <summary>
/// 普通按钮样式
/// </summary>
internal static GUIStyle CommonButtonStyle;
/// <summary>
/// 黄色按钮样式(已记录玩家)
/// </summary>
internal static GUIStyle YellowButtonStyle;
/// <summary>
/// 红色按钮样式(黑名单玩家)
/// </summary>
internal static GUIStyle RedButtonStyle;
/// <summary>
/// 绿色按钮样式(好友)
/// </summary>
internal 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
/// <summary>
@@ -96,19 +39,20 @@ public static class PlayerManagerGUI
/// </summary>
public static void DrawLobbyInfoGUI()
{
InitializeStyles();
GUIStyles.Initialize();
Cursor.Reset();
if (PlayerManager.MatchmakingHandler.IsInsideLobby)
{
DrawCurrentLobbyInfo();
DrawLobbySearchInfo();
}
else
{
GUI.Label(
Cursor.NextLine(500f, 40f),
"当前不在大厅中",
TitleStyle
GUIStyles.Label
);
DrawLobbySearchInfo();
}
@@ -128,7 +72,7 @@ public static class PlayerManagerGUI
GUI.Label(
Cursor.NextLine(2000f, 40f),
$"当前大厅: {PlayerManager.CurrentLobbyID} 房主: {PlayerManager.LobbyOwner.CurrentName}",
TitleStyle
GUIStyles.Label
);
// HeaderRow 区域:纵向迭代每一行,行内横向迭代 Children
@@ -156,6 +100,8 @@ public static class PlayerManagerGUI
{
if (widget.ShouldDraw(member))
widget.Draw(Cursor.NextColumn(widget.Width, 40f), member);
else
Cursor.NextColumn(widget.Width, 40f); // 占位推进列位置
}
Cursor.EndRow();
}
@@ -175,7 +121,7 @@ public static class PlayerManagerGUI
if (GUI.Button(
Cursor.NextLine(400f, 40f),
$"搜索范围 {areaText}",
CommonButtonStyle
GUIStyles.Button
))
{
CycleLobbyDistanceFilter();
@@ -186,7 +132,7 @@ public static class PlayerManagerGUI
if (GUI.Button(
new Rect(Cursor.OffsetX + 420f, Cursor.CurrentY, 100f, 40f),
"刷新",
CommonButtonStyle
GUIStyles.Button
))
{
PlayerManager.RequestLobbyList();
@@ -196,7 +142,7 @@ public static class PlayerManagerGUI
GUI.Label(
Cursor.NextLine(500f, 40f),
$"搜索到大厅数量: {PlayerManager.lobbyDataList.Count}",
TitleStyle
GUIStyles.Label
);
// 大厅列表滚动视图
@@ -247,10 +193,12 @@ public static class PlayerManagerGUI
if (GUI.Button(
new Rect(Cursor.OffsetX, Cursor.LineHeight * i, 500f, 40f),
$"{lobbyId} 人数{SteamMatchmaking.GetNumLobbyMembers(lobbyId)}",
CommonButtonStyle
GUIStyles.Button
))
{
PlayerManager.JoinSpecificServer(lobbyId);
// 在大厅中则不加入
if (!PlayerManager.MatchmakingHandler.IsInsideLobby)
PlayerManager.JoinSpecificServer(lobbyId);
}
}

View File

@@ -106,7 +106,7 @@ public class HeaderLabelWidget : IHeaderWidget
public void Draw(Rect rect)
{
GUI.Label(rect, Content?.Invoke() ?? string.Empty, Style ?? GUI.skin.label);
GUI.Label(rect, Content?.Invoke() ?? string.Empty, Style ?? GUIStyles.Label);
}
}
@@ -126,7 +126,7 @@ public class HeaderButtonWidget : IHeaderWidget
public void Draw(Rect rect)
{
if (GUI.Button(rect, Text ?? string.Empty, Style ?? GUI.skin.button))
if (GUI.Button(rect, Text ?? string.Empty, Style ?? GUIStyles.Button))
Action?.Invoke();
}
}
@@ -179,7 +179,7 @@ public class PlayerRowButtonWidget : IPlayerRowWidget
public void Draw(Rect rect, PlayerInfo player)
{
GUIStyle style = GetStyle?.Invoke(player) ?? Style ?? GUI.skin.button;
GUIStyle style = GetStyle?.Invoke(player) ?? Style ?? GUIStyles.Button;
if (GUI.Button(rect, GetText?.Invoke(player) ?? string.Empty, style))
OnClick?.Invoke(player);
}
@@ -200,7 +200,7 @@ public class PlayerRowLabelWidget : IPlayerRowWidget
public void Draw(Rect rect, PlayerInfo player)
{
GUI.Label(rect, GetText?.Invoke(player) ?? string.Empty, Style ?? GUI.skin.label);
GUI.Label(rect, GetText?.Invoke(player) ?? string.Empty, Style ?? GUIStyles.Label);
}
}

View 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();
}
}

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;
}
}
}

15
Patches/MutePatch.cs Normal file
View 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;
}
}

129
Plugin.cs
View File

@@ -9,6 +9,8 @@ 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;
@@ -57,6 +59,16 @@ public class PlayerManager : BaseUnityPlugin
/// </summary>
internal static ConfigEntry<bool> ShowRoomPublicityButton;
/// <summary>
/// 是否显示禁音按钮
/// </summary>
internal static ConfigEntry<bool> ShowMuteButton;
/// <summary>
/// 是否显示加好友按钮
/// </summary>
internal static ConfigEntry<bool> ShowAddFriendButton;
#endregion
#region Steam回调
@@ -104,6 +116,11 @@ public class PlayerManager : BaseUnityPlugin
#region 访
/// <summary>
/// 多人管理器实例
/// </summary>
public static MultiplayerManager MultiplayerManager => FindObjectOfType<MultiplayerManager>();
/// <summary>
/// 大厅管理器实例
/// </summary>
@@ -117,15 +134,10 @@ public class PlayerManager : BaseUnityPlugin
"ChangeLobbyType"
);
/// <summary>
/// leverhandler实例
/// </summary>
public static HostLeverHandler HostLeverHandler => UnityEngine.Object.FindObjectOfType<HostLeverHandler>();
/// <summary>
/// 房间公开性
/// </summary>
public static bool IsPublic => HostLeverHandler?.isOn ?? MatchmakingHandler.LobbyType == ELobbyType.k_ELobbyTypePublic;
public static bool IsPublic => MatchmakingHandler.LobbyType == ELobbyType.k_ELobbyTypePublic;
/// <summary>
/// 当前大厅ID
@@ -140,7 +152,29 @@ public class PlayerManager : BaseUnityPlugin
/// <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<ushort> MutedPlayers = [];
#endregion
@@ -153,9 +187,18 @@ public class PlayerManager : BaseUnityPlugin
DefaultWidgets.RegisterAll();
}
void Start()
{
StartCoroutine(InitAfterSteam());
}
private void Update()
{
HandleInput();
if (SteamManager.Initialized)
{
LocalPlayer = new(SteamUser.GetSteamID());
}
}
private void OnGUI()
@@ -210,6 +253,13 @@ public class PlayerManager : BaseUnityPlugin
"是否显示曾用名切换按钮"
);
ShowMuteButton = Config.Bind(
"GUI",
"ShowMuteButton",
true,
"是否显示禁音按钮"
);
ShowRoomPublicityLabel = Config.Bind(
"GUI",
"ShowRoomPublicityLabel",
@@ -223,6 +273,13 @@ public class PlayerManager : BaseUnityPlugin
true,
"是否显示切换公开性按钮"
);
ShowAddFriendButton = Config.Bind(
"GUI",
"ShowAddFriendButton",
true,
"是否显示加好友按钮"
);
}
/// <summary>
@@ -236,6 +293,8 @@ public class PlayerManager : BaseUnityPlugin
}
Harmony.CreateAndPatchAll(typeof(ApplyBlacklist));
Harmony.CreateAndPatchAll(typeof(ListenForChange));
Harmony.CreateAndPatchAll(typeof(MutePatch));
Harmony.CreateAndPatchAll(typeof(DistanceFilterPatch));
}
/// <summary>
@@ -270,6 +329,60 @@ public class PlayerManager : BaseUnityPlugin
#region
/// <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>
@@ -297,7 +410,7 @@ public class PlayerManager : BaseUnityPlugin
{
if (MultiplayerManager.IsServer)
{
var targetType = IsPublic ? ELobbyType.k_ELobbyTypePublic : ELobbyType.k_ELobbyTypeFriendsOnly;
var targetType = IsPublic ? ELobbyType.k_ELobbyTypeFriendsOnly : ELobbyType.k_ELobbyTypePublic;
MatchmakingHandler.SetNewLobbyType(targetType);
RawChangeLobbyTypeMI.Invoke(MatchmakingHandler, [targetType]);
}

View File

@@ -43,6 +43,12 @@ public class PlayerInfo : IEquatable<PlayerInfo>
/// </summary>
public PlayerRecordStatus RecordStatus { get; set; }
/// <summary>
/// 柴自己的NetworkSpawnID
/// </summary>
public ushort NetworkSpawnID => PlayerManager.GetNetworkSpawnIDBySteamID(SteamId);
/// <summary>
/// 当前玩家名称(实时获取)
/// </summary>

View File

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

165
readme.md
View File

@@ -6,7 +6,6 @@
- 房间查找
- 玩家记录
- 被记录的玩家显示为黄名
- 是steam好友的玩家显示为绿名优先级更高
- 被拉黑的玩家显示为红名(优先级最高)
@@ -15,9 +14,33 @@
- 可选择开启,默认开启的防踢
- 可配置的界面开启按键默认为F2
- 可以随时切换房间的公开性
- 静音单个玩家
- 一键加好友
## 更新日志
### 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. 添加了房间公开性切换功能
@@ -72,3 +95,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");

View File

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