32 Commits

Author SHA1 Message Date
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
Yue-bin
40795a2f49 chore(playermanager): 升级版本到4.0.0
All checks were successful
Publish Release / build (push) Successful in 2m5s
升级PlayerManager插件版本从3.1.1到4.0.0,
以支持新的功能特性和改进。
2026-05-07 04:18:52 +08:00
Yue-bin
e157e24236 feat(gui): 添加默认组件注册器和可配置的UI组件
- 创建DefaultWidgets类作为默认组件注册入口,在Plugin.Awake()中调用RegisterAll()
- 实现多个内部组件工厂:KickButton、NameToggleButton、PlayerInfoButton、
  RecordButton、RoomPublicityButton、RoomPublicityLabel
- 将配置项访问权限从private改为internal以便外部访问
- 添加新的配置选项用于控制各个UI组件的显示开关
- 按配置条件动态注册相应的头部和玩家行组件
2026-05-07 04:16:40 +08:00
Yue-bin
dfb535e4b7 refactor(Gui): 重构PlayerManagerGUI实现纯绘制架构
- 将GUI类从包含业务逻辑改为纯绘制职责,使用LayoutCursor进行定位
- 移除硬编码的布局参数和按钮事件处理方法
- 改为迭代注册的HeaderRow/PlayerRowWidget进行绘制
- 调整GUIStyle访问修饰符为internal以供DefaultWidgets引用
2026-05-07 04:16:26 +08:00
Yue-bin
b98ae261d5 refactor(PlayerManager): 重构文件结构并更新命名空间
将GUI相关文件移动到Gui目录下,记录器相关文件移动到Recorder目录下,
并相应更新命名空间以匹配新的目录结构。

- 将PlayerManagerGUI.cs移至Gui/PlayerManagerGUI.cs
- 将BlackListRecorder.cs等记录器文件移至Recorder目录
- 更新命名空间从stick.plugins.playermanager到对应的子命名空间

Co-authored-by: Copilot <copilot@github.com>
2026-05-07 01:58:33 +08:00
Yue-bin
204fda4f59 refactor(PlayerManager): 移除未使用的lever方向控制方法并简化大厅类型切换逻辑
移除了SetHostLeverDirection方法,该方法用于修改lever朝向但不再需要。
同时重构了ChangeLobbyType方法中的大厅类型切换逻辑,使用更简洁的方式处理
公共和私有大厅类型的切换,移除了不必要的条件判断和lever旋转操作。

thank z7572

Co-authored-by: Copilot <copilot@github.com>
2026-05-07 01:51:49 +08:00
Yue-bin
023602e364 📃 docs(readme): 补充功能 2026-02-27 02:26:57 +08:00
Yue-bin
dde47af2d8 📃 docs(readme): v3.1.1 2026-02-27 02:25:43 +08:00
Yue-bin
8d7d08b99e ci: 正确的仓库
All checks were successful
Publish Release / build (push) Successful in 1m3s
2026-02-19 18:46:31 +08:00
Yue-bin
8262b70292 ci(gitea): 固定 workflow action 版本以提高安全性
Some checks failed
Publish Release / build (push) Failing after 3s
将 actions/checkout 和 actions/setup-dotnet 从浮动版本标签
更新为固定的提交哈希和特定版本号,防止潜在的安全风险并
确保 CI/CD 构建的可重复性。
2026-02-19 18:39:06 +08:00
Yue-bin
652693883b ci: 添加 Gitea Actions 自动发布工作流
Some checks failed
Publish Release / build (push) Failing after 4s
配置 Gitea Actions 工作流实现 .NET 项目自动化发布:
- 监听 v 开头标签触发构建任务
- 使用 .NET 8.0 构建 Release 版本
- 自动创建 Gitea Release 并上传 stick.plugins DLL 文件
- 智能识别 alpha/beta/rc 标签并标记为预发布版本
2026-02-19 18:30:49 +08:00
Yue-bin
8261d243e6 chore(playermanager): 更新版本号至 3.1.1 2026-02-19 17:35:59 +08:00
Yue-bin
4de0778dc8 fix(playermanager):修复了获取不到lobbylist的问题 2026-02-19 17:34:08 +08:00
Yue-bin
eb27008e4d fix(playermanager): 修复大厅搜索信息显示逻辑并添加筛选自动刷新
将 DrawLobbySearchInfo 移至未加入大厅的条件分支内,避免在大厅中错误显示搜索信息。
在切换距离筛选器时立即请求大厅列表,实现筛选条件变更后自动刷新结果。
2026-02-19 17:33:39 +08:00
Yue-bin
0b3a12f64d fix(playermanager): 修复拉杆旋转逻辑并添加大厅列表调试信息
将拉杆角度控制从直接修改 transform.forward 改为使用 Rigidbody.MoveRotation,
确保物理引擎正确处理旋转并避免抖动。

在大厅列表请求流程中添加详细调试日志,包括请求开始、
完成状态以及版本过滤详情,便于排查联机兼容性问题。
2026-02-19 17:32:53 +08:00
Yue-bin
f44815c600 feat(playermanager): 优化房间公开性切换的拉杆控制逻辑
将原有的 DisableLever()/EnableLever() 调用替换为更精确的 SetHostLeverDirection(angle) 方法,支持根据房间公开状态设置拉杆的不同朝向角度(公开时-30°,私密时30°)。新增回退机制,当拉杆组件不存在时直接调用原始方法修改房间类型,确保功能在各种环境下均可正常工作。
2026-02-01 22:18:13 +08:00
Yue-bin
ca1f7c0637 feat(playermanager): 重构大厅公开性切换并集成拉杆控制功能
提取 GUI 中的大厅公开性切换逻辑到 PlayerManager 统一管理,并增加与 HostLeverHandler 的集成。当切换为公开大厅时启用拉杆,切换为私人大厅时禁用拉杆。
2026-02-01 21:05:36 +08:00
Yue-bin
be50401453 feat(gui): 限制房间公开性设置仅房主可见
为房间公开性相关UI添加房主权限检查,确保只有房主才能查看和操作房间公开性设置,避免普通玩家看到不相关的UI元素。
2026-02-01 20:50:04 +08:00
Yue-bin
c6574c53c8 chore(playermanager): 将版本更新至 3.1.0
本次提交将 PlayerManager 插件的版本号从 3.0.4 升级至 3.1.0,以反映最近的功能更新和代码重构。主要变更包括房间公开性切换功能的添加以及玩家管理器代码结构的重构优化。
2026-02-01 20:45:26 +08:00
Yue-bin
9a0b40d3ee feat(gui): 添加房间公开性切换功能
在玩家管理界面中新增房间公开性显示和切换功能。
服务器管理员可以通过界面按钮切换房间的公开状态,
支持在"公开"和"好友可见"两种模式之间切换。
2026-02-01 20:44:32 +08:00
Yue-bin
530036cc30 refactor(gui): 重构玩家管理GUI按钮点击处理逻辑
将踢人、曾用名切换、大厅距离过滤和刷新列表等按钮的点击处理逻辑提取为独立的私有方法。提高代码可读性和可维护性,使GUI绘制方法更加简洁。
2026-02-01 19:18:14 +08:00
Yue-bin
3a2062d808 refactor(player-manager): 重构玩家管理器代码结构
为所有类和方法添加 XML 文档注释,完善代码组织结构,修复 RecoderBase/Recoder 类名拼写错误,统一使用 PascalCase 命名规范,重构 PlayerManagerGUI 和 Plugin 类以提升可读性和可维护性。
2026-02-01 19:11:25 +08:00
Yue-bin
45884570e6 refactor(player-manager): optimize performance with cached styles and O(1) lookups
- Cache GUIStyle objects instead of creating per-frame to reduce GC pressure
- Use HashSet for O(1) player lookup instead of O(n) list iteration
- Add null checks and improve error handling
2026-02-01 18:01:55 +08:00
Yue-bin
413fc0b930 🐞 fix: 进入房间时刷新列表 2025-12-24 23:28:26 +08:00
Yue-bin
66558e8b96 🎈 perf: lock it 2025-12-20 17:17:05 +08:00
26 changed files with 1747 additions and 550 deletions

View File

@@ -0,0 +1,46 @@
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
- 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') }}

View File

@@ -1,7 +0,0 @@
namespace stick.plugins.playermanager;
public class BlackListRecorder : RecoderBase
{
protected override string FileName => "BlackList.txt";
protected override PlayerRecordStatus RecordStatus => PlayerRecordStatus.Blacklisted;
}

44
Gui/DefaultWidgets.cs Normal file
View File

@@ -0,0 +1,44 @@
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());
}
}

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

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

View File

@@ -0,0 +1,50 @@
using Steamworks;
using UnityEngine;
using stick.plugins.playermanager.Recorder;
namespace stick.plugins.playermanager.Gui.InternalWidgets;
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()
{
return new PlayerRowButtonWidget
{
Name = "PlayerInfo",
GetText = player => player.ToString(),
OnClick = player =>
{
GUIUtility.systemCopyBuffer = player.ToString();
SteamFriends.ActivateGameOverlayToUser("steamid", player.SteamId);
},
Width = 500f,
GetStyle = player =>
{
EnsureStyles();
return player.RecordStatus switch
{
PlayerRecordStatus.Blacklisted => _redStyle,
PlayerRecordStatus.Recorded => _yellowStyle,
_ => player.IsFriend ? _greenStyle : _normalStyle,
};
},
};
}
}

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

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

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

86
Gui/LayoutCursor.cs Normal file
View File

@@ -0,0 +1,86 @@
using UnityEngine;
namespace stick.plugins.playermanager.Gui;
/// <summary>
/// 布局游标,统一管理 GUI 绘制时的行列定位
/// Header 区域纵向排列 RowRow 内横向排列 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>
/// 在当前行获取下一列的 RectX 坐标自动推进
/// </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);
}
}

261
Gui/PlayerManagerGUI.cs Normal file
View File

@@ -0,0 +1,261 @@
using System.Linq;
using MultiplayerBasicExample;
using Steamworks;
using UnityEngine;
using stick.plugins.playermanager.Recorder;
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();
#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
/// <summary>
/// 绘制大厅信息GUI
/// </summary>
public static void DrawLobbyInfoGUI()
{
InitializeStyles();
Cursor.Reset();
if (PlayerManager.MatchmakingHandler.IsInsideLobby)
{
DrawCurrentLobbyInfo();
}
else
{
GUI.Label(
Cursor.NextLine(500f, 40f),
"当前不在大厅中",
TitleStyle
);
DrawLobbySearchInfo();
}
}
#endregion
#region
/// <summary>
/// 绘制当前大厅信息
/// 结构:标题行 → HeaderRow 区域(纵向) → 玩家行区域
/// </summary>
private static void DrawCurrentLobbyInfo()
{
// 固定:大厅标题(独占一行)
GUI.Label(
Cursor.NextLine(2000f, 40f),
$"当前大厅: {PlayerManager.CurrentLobbyID} 房主: {PlayerManager.LobbyOwner.CurrentName}",
TitleStyle
);
// 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();
}
// 玩家行区域:每玩家一行,行内横向迭代注册的 PlayerRowWidget
for (int i = 0; i < PlayerManager.lobbyMemberList.Count; i++)
{
PlayerInfo member = PlayerManager.lobbyMemberList[i];
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);
}
Cursor.EndRow();
}
}
#endregion
#region
/// <summary>
/// 绘制大厅搜索信息(不在大厅时显示)
/// </summary>
private static void DrawLobbySearchInfo()
{
// 搜索范围按钮
string areaText = GetAreaFilterText();
if (GUI.Button(
Cursor.NextLine(400f, 40f),
$"搜索范围 {areaText}",
CommonButtonStyle
))
{
CycleLobbyDistanceFilter();
PlayerManager.RequestLobbyList();
}
// 刷新按钮(同一行)
if (GUI.Button(
new Rect(Cursor.OffsetX + 420f, Cursor.CurrentY, 100f, 40f),
"刷新",
CommonButtonStyle
))
{
PlayerManager.RequestLobbyList();
}
// 搜索结果数量
GUI.Label(
Cursor.NextLine(500f, 40f),
$"搜索到大厅数量: {PlayerManager.lobbyDataList.Count}",
TitleStyle
);
// 大厅列表滚动视图
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];
if (GUI.Button(
new Rect(Cursor.OffsetX, Cursor.LineHeight * i, 500f, 40f),
$"{lobbyId} 人数{SteamMatchmaking.GetNumLobbyMembers(lobbyId)}",
CommonButtonStyle
))
{
PlayerManager.JoinSpecificServer(lobbyId);
}
}
GUI.EndScrollView();
}
#endregion
}

101
Gui/RegisterManager.cs Normal file
View 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
}

251
Gui/Widget.cs Normal file
View File

@@ -0,0 +1,251 @@
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
{
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 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 ?? DefaultStyle);
}
}
/// <summary>
/// Header 按钮组件
/// </summary>
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 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 ?? DefaultStyle))
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
{
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 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 ?? DefaultStyle;
if (GUI.Button(rect, GetText?.Invoke(player) ?? string.Empty, style))
OnClick?.Invoke(player);
}
}
/// <summary>
/// 玩家行标签组件
/// </summary>
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 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 ?? DefaultStyle);
}
}
#endregion

20
Patches/LogLeverAngle.cs Normal file
View File

@@ -0,0 +1,20 @@
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

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

View File

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

View File

@@ -1,7 +0,0 @@
namespace stick.plugins.playermanager;
public class PlayerRecoder : RecoderBase
{
protected override string FileName => "CustomPlayerRecord.txt";
protected override PlayerRecordStatus RecordStatus => PlayerRecordStatus.Recorded;
}

391
Plugin.cs
View File

@@ -6,123 +6,394 @@ using UnityEngine;
using Steamworks;
using HarmonyLib;
using System.Data;
using System.Reflection;
using stick.plugins.playermanager.Recorder;
using stick.plugins.playermanager.Gui;
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> ShowRecordButton;
/// <summary>
/// 是否显示曾用名切换按钮
/// </summary>
internal static ConfigEntry<bool> ShowNameToggleButton;
/// <summary>
/// 是否显示房间类型标签
/// </summary>
internal static ConfigEntry<bool> ShowRoomPublicityLabel;
/// <summary>
/// 是否显示切换公开性按钮
/// </summary>
internal static ConfigEntry<bool> ShowRoomPublicityButton;
#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 ELobbyDistanceFilter lobbyDistanceFilter = ELobbyDistanceFilter.k_ELobbyDistanceFilterDefault;
/// <summary>
/// 线程同步锁,保护大厅成员列表操作
/// </summary>
private static readonly object LobbyMemberLock = new();
#endregion
#region 访
/// <summary>
/// 大厅管理器实例
/// </summary>
public static MatchmakingHandler MatchmakingHandler => MatchmakingHandler.Instance;
/// <summary>
/// 原始的设置lobby类型方法
/// </summary>
public static MethodInfo RawChangeLobbyTypeMI => AccessTools.Method(
typeof(MatchmakingHandler),
"ChangeLobbyType"
);
/// <summary>
/// leverhandler实例
/// </summary>
public static HostLeverHandler HostLeverHandler => UnityEngine.Object.FindObjectOfType<HostLeverHandler>();
/// <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 => new(SteamUser.GetSteamID());
#endregion
#region
private void Awake()
{
// 绑定配置
blockKicks = Config.Bind<bool>("General", "BlockKicks", true, "是否屏蔽来自其他玩家的踢出请求");
lobbyInfoToggleKeyCode = Config.Bind<KeyCode>("LobbyInfo", "ToggleKeyCode", KeyCode.F2, "Lobby信息开关快捷键");
// patch
if (blockKicks.Value)
InitializeConfiguration();
ApplyPatches();
DefaultWidgets.RegisterAll();
}
private void Update()
{
HandleInput();
}
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,
"是否显示踢人按钮"
);
ShowRecordButton = Config.Bind(
"GUI",
"ShowRecordButton",
true,
"是否显示记录/拉黑按钮"
);
ShowNameToggleButton = Config.Bind(
"GUI",
"ShowNameToggleButton",
true,
"是否显示曾用名切换按钮"
);
ShowRoomPublicityLabel = Config.Bind(
"GUI",
"ShowRoomPublicityLabel",
true,
"是否显示房间类型标签"
);
ShowRoomPublicityButton = Config.Bind(
"GUI",
"ShowRoomPublicityButton",
true,
"是否显示切换公开性按钮"
);
}
/// <summary>
/// 应用Harmony补丁
/// </summary>
private void ApplyPatches()
{
if (_blockKicks.Value)
{
Harmony.CreateAndPatchAll(typeof(IgnoreKick));
}
Harmony.CreateAndPatchAll(typeof(ApplyBlacklist));
Harmony.CreateAndPatchAll(typeof(ListenForChange));
}
void Update()
/// <summary>
/// 处理输入
/// </summary>
private void HandleInput()
{
if (Input.GetKeyDown(lobbyInfoToggleKeyCode.Value))
if (Input.GetKeyDown(_lobbyInfoToggleKeyCode.Value))
{
if (PlayerManagerGUI.showLobbyInfo)
{
PlayerManagerGUI.showLobbyInfo = false;
Debug.Log("The GUI is closed");
}
else
{
RequestLobbyList();
PlayerManagerGUI.showLobbyInfo = true;
Debug.Log("The GUI is opened");
}
ToggleLobbyInfoGUI();
}
}
void OnGUI()
/// <summary>
/// 切换大厅信息GUI显示状态
/// </summary>
private void ToggleLobbyInfoGUI()
{
if (PlayerManagerGUI.showLobbyInfo)
if (PlayerManagerGUI.ShowLobbyInfo)
{
PlayerManagerGUI.DrawLobbyInfoGUI();
PlayerManagerGUI.ShowLobbyInfo = false;
Debug.Log("大厅信息界面已关闭");
}
else
{
RequestLobbyList();
PlayerManagerGUI.ShowLobbyInfo = true;
Debug.Log("大厅信息界面已打开");
}
}
#endregion
#region
/// <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++)
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
}

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

221
Recorder/PlayerInfo.cs Normal file
View File

@@ -0,0 +1,221 @@
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>
/// 当前玩家名称(实时获取)
/// </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
}

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

198
Recorder/RecorderBase.cs Normal file
View File

@@ -0,0 +1,198 @@
using System.Collections.Generic;
using System.IO;
using MultiplayerBasicExample;
using Steamworks;
using UnityEngine;
namespace stick.plugins.playermanager.Recorder;
/// <summary>
/// 记录器基类
/// 提供玩家记录和黑名单功能的通用实现
/// </summary>
public abstract class RecorderBase
{
#region
/// <summary>
/// 玩家信息集合(用于快速查找)
/// </summary>
protected HashSet<PlayerInfo> playerSet = new HashSet<PlayerInfo>();
/// <summary>
/// 玩家信息列表(用于有序遍历和文件写入)
/// </summary>
protected List<PlayerInfo> playerList = new List<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 && playerSet.Contains(playerInfo);
}
/// <summary>
/// 检查玩家是否已被记录通过Steam ID
/// </summary>
public bool IsPlayerRecorded(CSteamID playerId)
{
PlayerInfo playerInfo = new PlayerInfo(playerId);
return playerSet.Contains(playerInfo);
}
/// <summary>
/// 记录玩家
/// </summary>
public void RecordPlayer(PlayerInfo playerInfo)
{
if (playerInfo == null) return;
playerInfo.RecordStatus = RecordStatus;
if (playerSet.Add(playerInfo))
{
playerList.Add(playerInfo);
}
AppendToFile(playerInfo);
}
/// <summary>
/// 取消记录玩家
/// </summary>
public void CancelRecordPlayer(PlayerInfo playerInfo)
{
if (playerInfo == null) return;
if (playerSet.Remove(playerInfo))
{
playerList.Remove(playerInfo);
WriteBackToFile();
}
}
/// <summary>
/// 检查并合并玩家记录信息
/// </summary>
public void CheckAndMerge(PlayerInfo playerInfo)
{
if (playerInfo == null) return;
foreach (PlayerInfo existingInfo in playerSet)
{
if (existingInfo.Equals(playerInfo))
{
playerInfo.MergeWith(existingInfo);
return;
}
}
}
#endregion
#region
/// <summary>
/// 刷新玩家记录列表(从文件加载)
/// </summary>
protected void RefreshPlayerRecordList()
{
if (!File.Exists(FilePath))
{
return;
}
playerSet.Clear();
playerList.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 (playerSet.Add(playerInfo))
{
playerList.Add(playerInfo);
}
}
}
}
/// <summary>
/// 将玩家信息写入文件
/// </summary>
protected void WriteBackToFile()
{
using (StreamWriter writer = new StreamWriter(FilePath, false))
{
foreach (PlayerInfo info in playerList)
{
writer.WriteLine(info.ToInfoString());
}
}
}
#endregion
#region
/// <summary>
/// 追加玩家信息到文件
/// </summary>
private void AppendToFile(PlayerInfo playerInfo)
{
using (StreamWriter writer = new StreamWriter(FilePath, true))
{
writer.WriteLine(playerInfo);
}
}
#endregion
}

View File

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

View File

@@ -14,9 +14,17 @@
- 拉黑玩家,当你是房主的时候生效
- 可选择开启,默认开启的防踢
- 可配置的界面开启按键默认为F2
- 可以随时切换房间的公开性
## 更新日志
### v3.1.1
1. 添加了房间公开性切换功能
2. 更改房间筛选条件现在会触发刷新列表
3. 现在房间列表不会在房间内显示,防止误触导致的游戏状态不一致
4. 一些重构和优化
### v3.0.3
1. 修复了kick按钮无论如何都会显示的问题难道他真的是猪

View File

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