mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
feat: add Teams packet support (parsing, state tracking, /teams command, bot API)
Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/72ea891e-ba62-4dfc-bbba-f197cd1d0404 Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com>
This commit is contained in:
parent
cf65c6f241
commit
fdbffcc5bb
8 changed files with 397 additions and 1 deletions
77
MinecraftClient/Commands/Teams.cs
Normal file
77
MinecraftClient/Commands/Teams.cs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Brigadier.NET;
|
||||
using Brigadier.NET.Builder;
|
||||
using MinecraftClient.CommandHandler;
|
||||
using MinecraftClient.Mapping;
|
||||
|
||||
namespace MinecraftClient.Commands
|
||||
{
|
||||
public class Teams : Command
|
||||
{
|
||||
public override string CmdName => "teams";
|
||||
public override string CmdUsage => "teams";
|
||||
public override string CmdDesc => Translations.cmd_teams_desc;
|
||||
|
||||
public override void RegisterCommand(CommandDispatcher<CmdResult> dispatcher)
|
||||
{
|
||||
dispatcher.Register(l => l.Literal("help")
|
||||
.Then(l => l.Literal(CmdName)
|
||||
.Executes(r => GetUsage(r.Source, string.Empty))
|
||||
)
|
||||
);
|
||||
|
||||
dispatcher.Register(l => l.Literal(CmdName)
|
||||
.Executes(r => DoListTeams(r.Source))
|
||||
.Then(l => l.Literal("_help")
|
||||
.Executes(r => GetUsage(r.Source, string.Empty))
|
||||
.Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName)))
|
||||
);
|
||||
}
|
||||
|
||||
private int GetUsage(CmdResult r, string? cmd)
|
||||
{
|
||||
return r.SetAndReturn(cmd switch
|
||||
{
|
||||
#pragma warning disable format // @formatter:off
|
||||
_ => GetCmdDescTranslated(),
|
||||
#pragma warning restore format // @formatter:on
|
||||
});
|
||||
}
|
||||
|
||||
private static int DoListTeams(CmdResult r)
|
||||
{
|
||||
McClient handler = CmdResult.currentHandler!;
|
||||
Dictionary<string, PlayerTeam> snapshot = handler.GetTeams();
|
||||
|
||||
if (snapshot.Count == 0)
|
||||
return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_teams_no_teams);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
foreach (var team in snapshot.Values.OrderBy(static t => t.Name, StringComparer.Ordinal))
|
||||
{
|
||||
sb.AppendLine(string.Format(Translations.cmd_teams_team_header,
|
||||
team.Name,
|
||||
team.DisplayName,
|
||||
team.Color,
|
||||
team.Prefix,
|
||||
team.Suffix,
|
||||
team.NameTagVisibility,
|
||||
team.CollisionRule,
|
||||
team.AllowFriendlyFire,
|
||||
team.SeeFriendlyInvisibles));
|
||||
|
||||
if (team.Members.Count == 0)
|
||||
sb.AppendLine(Translations.cmd_teams_team_no_members);
|
||||
else
|
||||
sb.AppendLine(string.Format(Translations.cmd_teams_team_members,
|
||||
team.Members.Count,
|
||||
string.Join(", ", team.Members.OrderBy(static m => m, StringComparer.OrdinalIgnoreCase))));
|
||||
}
|
||||
|
||||
return r.SetAndReturn(CmdResult.Status.Done, sb.ToString().TrimEnd());
|
||||
}
|
||||
}
|
||||
}
|
||||
49
MinecraftClient/Mapping/PlayerTeam.cs
Normal file
49
MinecraftClient/Mapping/PlayerTeam.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace MinecraftClient.Mapping
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a Minecraft scoreboard team and its current state.
|
||||
/// </summary>
|
||||
public class PlayerTeam
|
||||
{
|
||||
/// <summary>Team internal name (up to 16 chars)</summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Display name component (formatted text)</summary>
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Friendly fire is allowed between team members</summary>
|
||||
public bool AllowFriendlyFire { get; set; }
|
||||
|
||||
/// <summary>Team members can see invisible teammates</summary>
|
||||
public bool SeeFriendlyInvisibles { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Nametag visibility rule.
|
||||
/// Values: "always", "hideForOtherTeams", "hideForOwnTeam", "never"
|
||||
/// </summary>
|
||||
public string NameTagVisibility { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Collision rule.
|
||||
/// Values: "always", "pushOtherTeams", "pushOwnTeam", "never"
|
||||
/// </summary>
|
||||
public string CollisionRule { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Team color as ChatFormatting enum ordinal (-1 = RESET/none,
|
||||
/// 0–15 = BLACK … WHITE).
|
||||
/// </summary>
|
||||
public int Color { get; set; } = -1;
|
||||
|
||||
/// <summary>Prefix displayed before member names (formatted text)</summary>
|
||||
public string Prefix { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Suffix displayed after member names (formatted text)</summary>
|
||||
public string Suffix { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Current set of player / entity names on this team</summary>
|
||||
public HashSet<string> Members { get; } = new(System.StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
|
@ -113,6 +113,9 @@ namespace MinecraftClient
|
|||
|
||||
// player attributes (e.g., block_break_speed, mining_efficiency, submerged_mining_speed)
|
||||
private readonly Dictionary<string, double> playerAttributes = new();
|
||||
|
||||
// scoreboard teams (key = team name)
|
||||
private readonly Dictionary<string, PlayerTeam> teams = new(StringComparer.Ordinal);
|
||||
|
||||
// Sneaking
|
||||
public bool IsSneaking { get; set; } = false;
|
||||
|
|
@ -162,6 +165,30 @@ namespace MinecraftClient
|
|||
return new Dictionary<Effects, EffectData>(playerEffects);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a snapshot of all known scoreboard teams.
|
||||
/// </summary>
|
||||
/// <returns>Dictionary mapping team name to <see cref="PlayerTeam"/></returns>
|
||||
public Dictionary<string, PlayerTeam> GetTeams()
|
||||
{
|
||||
lock (teams)
|
||||
return new Dictionary<string, PlayerTeam>(teams, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the team that contains the given player/entity name, or <c>null</c> if not found.
|
||||
/// </summary>
|
||||
public PlayerTeam? GetPlayerTeam(string playerName)
|
||||
{
|
||||
lock (teams)
|
||||
{
|
||||
foreach (var team in teams.Values)
|
||||
if (team.Members.Contains(playerName))
|
||||
return team;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public int GetLevel() { return playerLevel; }
|
||||
public int GetTotalExperience() { return playerTotalExperience; }
|
||||
public byte GetCurrentSlot() { return CurrentSlot; }
|
||||
|
|
@ -4053,7 +4080,78 @@ namespace MinecraftClient
|
|||
{
|
||||
DispatchBotEvent(bot => bot.OnUpdateScore(entityName, action, objectiveName, objectiveDisplayName, objectiveValue, numberFormat));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Called when a Teams packet is received. Updates the internal team state and notifies bots.
|
||||
/// </summary>
|
||||
public void OnTeam(string teamName, byte method, string displayName, byte friendlyFlags,
|
||||
string nameTagVisibility, string collisionRule, int color,
|
||||
string prefix, string suffix, List<string> players)
|
||||
{
|
||||
lock (teams)
|
||||
{
|
||||
switch (method)
|
||||
{
|
||||
case 0: // create
|
||||
var newTeam = new PlayerTeam
|
||||
{
|
||||
Name = teamName,
|
||||
DisplayName = displayName,
|
||||
AllowFriendlyFire = (friendlyFlags & 0x01) != 0,
|
||||
SeeFriendlyInvisibles = (friendlyFlags & 0x02) != 0,
|
||||
NameTagVisibility = nameTagVisibility,
|
||||
CollisionRule = collisionRule,
|
||||
Color = color,
|
||||
Prefix = prefix,
|
||||
Suffix = suffix
|
||||
};
|
||||
foreach (var p in players)
|
||||
newTeam.Members.Add(p);
|
||||
teams[teamName] = newTeam;
|
||||
break;
|
||||
|
||||
case 1: // remove
|
||||
teams.Remove(teamName);
|
||||
break;
|
||||
|
||||
case 2: // update parameters
|
||||
if (!teams.TryGetValue(teamName, out var updateTeam))
|
||||
{
|
||||
updateTeam = new PlayerTeam { Name = teamName };
|
||||
teams[teamName] = updateTeam;
|
||||
}
|
||||
updateTeam.DisplayName = displayName;
|
||||
updateTeam.AllowFriendlyFire = (friendlyFlags & 0x01) != 0;
|
||||
updateTeam.SeeFriendlyInvisibles = (friendlyFlags & 0x02) != 0;
|
||||
updateTeam.NameTagVisibility = nameTagVisibility;
|
||||
updateTeam.CollisionRule = collisionRule;
|
||||
updateTeam.Color = color;
|
||||
updateTeam.Prefix = prefix;
|
||||
updateTeam.Suffix = suffix;
|
||||
break;
|
||||
|
||||
case 3: // add players
|
||||
if (!teams.TryGetValue(teamName, out var addTeam))
|
||||
{
|
||||
addTeam = new PlayerTeam { Name = teamName };
|
||||
teams[teamName] = addTeam;
|
||||
}
|
||||
foreach (var p in players)
|
||||
addTeam.Members.Add(p);
|
||||
break;
|
||||
|
||||
case 4: // remove players
|
||||
if (teams.TryGetValue(teamName, out var removeTeam))
|
||||
foreach (var p in players)
|
||||
removeTeam.Members.Remove(p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
DispatchBotEvent(bot => bot.OnTeam(teamName, method, displayName, friendlyFlags,
|
||||
nameTagVisibility, collisionRule, color, prefix, suffix, players));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Called when the client received the Tab Header and Footer
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -3035,6 +3035,84 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
handler.OnUpdateScore(entityName, action3, objectiveName3, objectiveDisplayName3, objectiveValue2,
|
||||
numberFormat2);
|
||||
break;
|
||||
case PacketTypesIn.Teams:
|
||||
// Wire format per version:
|
||||
// All versions: name (string), method (byte)
|
||||
// method 0/2: displayName (component), options (byte),
|
||||
// nameTagVisibility, collisionRule, color (VarInt),
|
||||
// prefix (component), suffix (component)
|
||||
// method 0/3/4: players list (VarInt count + strings)
|
||||
// 1.21.9+ (protocol 773): nameTagVisibility and collisionRule are
|
||||
// VarInt-encoded enum IDs instead of UTF strings.
|
||||
var teamName = dataTypes.ReadNextString(packetData);
|
||||
var teamMethod = dataTypes.ReadNextByte(packetData);
|
||||
|
||||
var teamDisplayName = string.Empty;
|
||||
byte teamFriendlyFlags = 0;
|
||||
var teamNameTagVisibility = string.Empty;
|
||||
var teamCollisionRule = string.Empty;
|
||||
var teamColor = -1;
|
||||
var teamPrefix = string.Empty;
|
||||
var teamSuffix = string.Empty;
|
||||
|
||||
if (teamMethod is 0 or 2)
|
||||
{
|
||||
teamDisplayName = dataTypes.ReadNextChat(packetData);
|
||||
teamFriendlyFlags = dataTypes.ReadNextByte(packetData);
|
||||
|
||||
// nameTagVisibility
|
||||
if (protocolVersion >= MC_1_21_9_Version)
|
||||
{
|
||||
// STREAM_CODEC: 0=always, 1=never, 2=hideForOtherTeams, 3=hideForOwnTeam
|
||||
teamNameTagVisibility = dataTypes.ReadNextVarInt(packetData) switch
|
||||
{
|
||||
0 => "always",
|
||||
1 => "never",
|
||||
2 => "hideForOtherTeams",
|
||||
3 => "hideForOwnTeam",
|
||||
_ => "always"
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
teamNameTagVisibility = dataTypes.ReadNextString(packetData);
|
||||
}
|
||||
|
||||
// collisionRule
|
||||
if (protocolVersion >= MC_1_21_9_Version)
|
||||
{
|
||||
// STREAM_CODEC: 0=always, 1=never, 2=pushOtherTeams, 3=pushOwnTeam
|
||||
teamCollisionRule = dataTypes.ReadNextVarInt(packetData) switch
|
||||
{
|
||||
0 => "always",
|
||||
1 => "never",
|
||||
2 => "pushOtherTeams",
|
||||
3 => "pushOwnTeam",
|
||||
_ => "always"
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
teamCollisionRule = dataTypes.ReadNextString(packetData);
|
||||
}
|
||||
|
||||
teamColor = dataTypes.ReadNextVarInt(packetData);
|
||||
teamPrefix = dataTypes.ReadNextChat(packetData);
|
||||
teamSuffix = dataTypes.ReadNextChat(packetData);
|
||||
}
|
||||
|
||||
var teamPlayers = new List<string>();
|
||||
if (teamMethod is 0 or 3 or 4)
|
||||
{
|
||||
int playerCount = dataTypes.ReadNextVarInt(packetData);
|
||||
for (int i = 0; i < playerCount; i++)
|
||||
teamPlayers.Add(dataTypes.ReadNextString(packetData));
|
||||
}
|
||||
|
||||
handler.OnTeam(teamName, teamMethod, teamDisplayName, teamFriendlyFlags,
|
||||
teamNameTagVisibility, teamCollisionRule, teamColor,
|
||||
teamPrefix, teamSuffix, teamPlayers);
|
||||
break;
|
||||
case PacketTypesIn.BlockChangedAck:
|
||||
handler.OnBlockChangeAck(dataTypes.ReadNextVarInt(packetData));
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -489,6 +489,23 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="numberFormat">Number format: 0 - blank, 1 - styled, 2 - fixed</param>
|
||||
void OnUpdateScore(string entityName, int action, string objectiveName, string objectiveDisplayName, int objectiveValue, int numberFormat);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a Teams packet is received from the server.
|
||||
/// </summary>
|
||||
/// <param name="teamName">Internal team name (up to 16 chars)</param>
|
||||
/// <param name="method">0=create, 1=remove, 2=update, 3=add players, 4=remove players</param>
|
||||
/// <param name="displayName">Display name (formatted). Present when method is 0 or 2.</param>
|
||||
/// <param name="friendlyFlags">Bit 0=allowFriendlyFire, bit 1=seeFriendlyInvisibles. Present when method is 0 or 2.</param>
|
||||
/// <param name="nameTagVisibility">Nametag visibility rule string. Present when method is 0 or 2.</param>
|
||||
/// <param name="collisionRule">Collision rule string. Present when method is 0 or 2.</param>
|
||||
/// <param name="color">ChatFormatting color value (-1=none). Present when method is 0 or 2.</param>
|
||||
/// <param name="prefix">Member name prefix (formatted). Present when method is 0 or 2.</param>
|
||||
/// <param name="suffix">Member name suffix (formatted). Present when method is 0 or 2.</param>
|
||||
/// <param name="players">Player/entity names. Present when method is 0, 3, or 4.</param>
|
||||
void OnTeam(string teamName, byte method, string displayName, byte friendlyFlags,
|
||||
string nameTagVisibility, string collisionRule, int color,
|
||||
string prefix, string suffix, List<string> players);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the client received the Tab Header and Footer
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -4660,6 +4660,51 @@ namespace MinecraftClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to List all scoreboard teams and their members.
|
||||
/// </summary>
|
||||
internal static string cmd_teams_desc {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.teams.desc", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to No teams are currently tracked.
|
||||
/// </summary>
|
||||
internal static string cmd_teams_no_teams {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.teams.no_teams", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Team '{0}' (display: {1}, ...).
|
||||
/// </summary>
|
||||
internal static string cmd_teams_team_header {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.teams.team_header", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Members ({0}): {1}.
|
||||
/// </summary>
|
||||
internal static string cmd_teams_team_members {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.teams.team_members", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to No members.
|
||||
/// </summary>
|
||||
internal static string cmd_teams_team_no_members {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.teams.team_no_members", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Place a block or open chest.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1562,6 +1562,21 @@ You can use "/chunk status {0:0.0} {1:0.0} {2:0.0}" to check the chunk loading s
|
|||
<data name="cmd.tps.desc" xml:space="preserve">
|
||||
<value>Display server current tps (tick per second). May not be accurate</value>
|
||||
</data>
|
||||
<data name="cmd.teams.desc" xml:space="preserve">
|
||||
<value>List all scoreboard teams and their members.</value>
|
||||
</data>
|
||||
<data name="cmd.teams.no_teams" xml:space="preserve">
|
||||
<value>No teams are currently tracked.</value>
|
||||
</data>
|
||||
<data name="cmd.teams.team_header" xml:space="preserve">
|
||||
<value>Team '{0}' (display: {1}, color: {2}, prefix: '{3}', suffix: '{4}', nameTagVisibility: {5}, collisionRule: {6}, friendlyFire: {7}, seeInvisibles: {8})</value>
|
||||
</data>
|
||||
<data name="cmd.teams.team_members" xml:space="preserve">
|
||||
<value> Members ({0}): {1}</value>
|
||||
</data>
|
||||
<data name="cmd.teams.team_no_members" xml:space="preserve">
|
||||
<value> No members.</value>
|
||||
</data>
|
||||
<data name="cmd.useblock.desc" xml:space="preserve">
|
||||
<value>Place a block or open chest</value>
|
||||
</data>
|
||||
|
|
|
|||
|
|
@ -384,6 +384,23 @@ namespace MinecraftClient.Scripting
|
|||
/// <param name="numberFormat">Number format: 0 - blank, 1 - styled, 2 - fixed</param>
|
||||
public virtual void OnUpdateScore(string entityName, int action, string objectiveName, string objectiveDisplayName, int value, int numberFormat) { }
|
||||
|
||||
/// <summary>
|
||||
/// Called when a Teams packet is received from the server.
|
||||
/// </summary>
|
||||
/// <param name="teamName">Internal team name (up to 16 chars)</param>
|
||||
/// <param name="method">0=create, 1=remove, 2=update, 3=add players, 4=remove players</param>
|
||||
/// <param name="displayName">Display name (formatted). Present when method is 0 or 2.</param>
|
||||
/// <param name="friendlyFlags">Bit 0=allowFriendlyFire, bit 1=seeFriendlyInvisibles. Present when method is 0 or 2.</param>
|
||||
/// <param name="nameTagVisibility">Nametag visibility rule. Present when method is 0 or 2.</param>
|
||||
/// <param name="collisionRule">Collision rule. Present when method is 0 or 2.</param>
|
||||
/// <param name="color">ChatFormatting color value (-1=none). Present when method is 0 or 2.</param>
|
||||
/// <param name="prefix">Member name prefix (formatted). Present when method is 0 or 2.</param>
|
||||
/// <param name="suffix">Member name suffix (formatted). Present when method is 0 or 2.</param>
|
||||
/// <param name="players">Player/entity names. Present when method is 0, 3, or 4.</param>
|
||||
public virtual void OnTeam(string teamName, byte method, string displayName, byte friendlyFlags,
|
||||
string nameTagVisibility, string collisionRule, int color,
|
||||
string prefix, string suffix, List<string> players) { }
|
||||
|
||||
/// <summary>
|
||||
/// Called when the client received the Tab Header and Footer
|
||||
/// </summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue