diff --git a/Components/MineSharp.Auth/MineSharp.Auth.csproj b/Components/MineSharp.Auth/MineSharp.Auth.csproj index cb28959b..49b5e93d 100644 --- a/Components/MineSharp.Auth/MineSharp.Auth.csproj +++ b/Components/MineSharp.Auth/MineSharp.Auth.csproj @@ -3,7 +3,7 @@ enable enable - net7.0;net8.0 + net8.0 12 true README.md diff --git a/Components/MineSharp.ChatComponent/Chat.cs b/Components/MineSharp.ChatComponent/Chat.cs index e7d223ee..ea2d7143 100644 --- a/Components/MineSharp.ChatComponent/Chat.cs +++ b/Components/MineSharp.ChatComponent/Chat.cs @@ -238,7 +238,7 @@ public static Chat Parse(NbtTag tag) return KeybindComponent.Parse(tag); } - var empty = obj.Tags.FirstOrDefault(x => string.IsNullOrEmpty(x.Name)); + var empty = obj[""]; if (empty is not null) { return Parse(empty); diff --git a/Components/MineSharp.ChatComponent/Components/TranslatableComponent.cs b/Components/MineSharp.ChatComponent/Components/TranslatableComponent.cs index 640135c9..3e94a1ba 100644 --- a/Components/MineSharp.ChatComponent/Components/TranslatableComponent.cs +++ b/Components/MineSharp.ChatComponent/Components/TranslatableComponent.cs @@ -101,11 +101,16 @@ protected override string GetRawMessage(MinecraftData? data) var with = With.Select(x => x.GetMessage(data)).ToArray(); if (data == null) { - Logger.Warn("Cannot translate message because no minecraft data was provided!"); + Logger.Warn("Cannot translate message because no minecraft data was provided! For: {TranslationKey}", Translation); return string.Join(' ', with); } var rule = data.Language.GetTranslation(Translation)!; + if (rule == null) + { + Logger.Warn("Cannot translate message because no translation string was found! For: {TranslationKey}", Translation); + return string.Join(' ', with); + } return TranslateString(rule, with); } @@ -151,9 +156,50 @@ private string TranslateString(string rule, string[] usings) var with = Array.Empty(); if (obj.TryGet("with", out var withToken)) { - with = (withToken as NbtList)!.Select(Chat.Parse).ToArray(); + with = ParseArguments(withToken!); } return new(translate, with, Style.Parse(tag), ParseChildren(tag)); } + + private static Chat[] ParseArguments(NbtTag tag) + { + // as of 1.20.3 and the introduction of chat components via nbt, + // TranslatableComponents allow arguments to be the primitive types + // of java.lang.Number (byte, double, float, integer, ...), boolean and strings + + // because NbtLists only allow elements of the same tag, these primitives types + // are sometimes wrapped in a TAG_Compound + + return tag switch + { + NbtLongArray longArray => longArray.Value.Select(Chat (x) => new TextComponent(x.ToString())).ToArray(), + + NbtIntArray intArray => intArray.Value.Select(Chat (x) => new TextComponent(x.ToString())).ToArray(), + + NbtByteArray byteArray => byteArray.Value.Select(Chat (x) => new TextComponent(x.ToString())).ToArray(), + + NbtList list => list.Select(ParseArgument).ToArray(), + + _ => throw new ArgumentException($"Unexpected tag type: {tag.TagType}") + }; + } + + private static Chat ParseArgument(NbtTag tag) + { + if (tag.TagType != NbtTagType.Compound) + { + return new TextComponent(tag.StringValue); + } + + var compound = (tag as NbtCompound)!; + if (compound.Count == 1 && compound.TryGet("", out var primitive)) + { + // it's a primitive value wrapped in a compound + return new TextComponent(primitive.StringValue); + } + + // it's a normal chat component + return Chat.Parse(compound); + } } diff --git a/Components/MineSharp.ChatComponent/MineSharp.ChatComponent.csproj b/Components/MineSharp.ChatComponent/MineSharp.ChatComponent.csproj index c2f6cf9b..3f0ce003 100644 --- a/Components/MineSharp.ChatComponent/MineSharp.ChatComponent.csproj +++ b/Components/MineSharp.ChatComponent/MineSharp.ChatComponent.csproj @@ -1,9 +1,9 @@ - + enable enable - net8.0;net7.0 + net8.0 true README.md diff --git a/Components/MineSharp.Commands/CommandTree.cs b/Components/MineSharp.Commands/CommandTree.cs index 844cbe92..89dbb742 100644 --- a/Components/MineSharp.Commands/CommandTree.cs +++ b/Components/MineSharp.Commands/CommandTree.cs @@ -1,6 +1,7 @@ using MineSharp.Commands.Parser; using MineSharp.Core; using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; namespace MineSharp.Commands; @@ -74,12 +75,12 @@ private static CommandNode ReadNode(PacketBuffer buffer, MinecraftData data) return null; } - string name; + Identifier name; if (data.Version.Protocol < ProtocolVersion.V_1_19) { // in 1.18.x, the parser was specified by its name. - name = buffer.ReadString(); + name = buffer.ReadIdentifier(); } else { diff --git a/Components/MineSharp.Commands/MineSharp.Commands.csproj b/Components/MineSharp.Commands/MineSharp.Commands.csproj index 3c0a4fb7..cdbb1daf 100644 --- a/Components/MineSharp.Commands/MineSharp.Commands.csproj +++ b/Components/MineSharp.Commands/MineSharp.Commands.csproj @@ -5,7 +5,7 @@ enable MineSharp.Commands MineSharp.Commands - net7.0;net8.0 + net8.0 12 true README.md diff --git a/Components/MineSharp.Commands/Parser/BlockPositionParser.cs b/Components/MineSharp.Commands/Parser/BlockPositionParser.cs index e93cd789..d744a82a 100644 --- a/Components/MineSharp.Commands/Parser/BlockPositionParser.cs +++ b/Components/MineSharp.Commands/Parser/BlockPositionParser.cs @@ -1,13 +1,16 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; namespace MineSharp.Commands.Parser; public class BlockPositionParser : IParser { - public string GetName() + public static readonly Identifier BlockPosIdentifier = Identifier.Parse("minecraft:block_pos"); + + public Identifier GetName() { - return "minecraft:block_pos"; + return BlockPosIdentifier; } public int GetArgumentCount() diff --git a/Components/MineSharp.Commands/Parser/ColumnPosParser.cs b/Components/MineSharp.Commands/Parser/ColumnPosParser.cs index 38e2b1a1..b46891cb 100644 --- a/Components/MineSharp.Commands/Parser/ColumnPosParser.cs +++ b/Components/MineSharp.Commands/Parser/ColumnPosParser.cs @@ -1,13 +1,16 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; namespace MineSharp.Commands.Parser; public class ColumnPosParser : IParser { - public string GetName() + public static readonly Identifier ColumnPosIdentifier = Identifier.Parse("minecraft:column_pos"); + + public Identifier GetName() { - return "minecraft:column_pos"; + return ColumnPosIdentifier; } public int GetArgumentCount() diff --git a/Components/MineSharp.Commands/Parser/DoubleParser.cs b/Components/MineSharp.Commands/Parser/DoubleParser.cs index dbd221a5..4725cf96 100644 --- a/Components/MineSharp.Commands/Parser/DoubleParser.cs +++ b/Components/MineSharp.Commands/Parser/DoubleParser.cs @@ -1,16 +1,19 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; namespace MineSharp.Commands.Parser; public class DoubleParser : IParser { + public static readonly Identifier BrigadierDoubleIdentifier = Identifier.Parse("brigadier:double"); + public double Min { get; private set; } public double Max { get; private set; } - public string GetName() + public Identifier GetName() { - return "brigadier:double"; + return BrigadierDoubleIdentifier; } public int GetArgumentCount() diff --git a/Components/MineSharp.Commands/Parser/EmptyParser.cs b/Components/MineSharp.Commands/Parser/EmptyParser.cs index 958e6fcb..1da7fc8a 100644 --- a/Components/MineSharp.Commands/Parser/EmptyParser.cs +++ b/Components/MineSharp.Commands/Parser/EmptyParser.cs @@ -1,13 +1,14 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; namespace MineSharp.Commands.Parser; public class EmptyParser : IParser { - public string GetName() + public Identifier GetName() { - return string.Empty; + return Identifier.Empty; } public int GetArgumentCount() diff --git a/Components/MineSharp.Commands/Parser/EntityParser.cs b/Components/MineSharp.Commands/Parser/EntityParser.cs index 27447449..6a52fb74 100644 --- a/Components/MineSharp.Commands/Parser/EntityParser.cs +++ b/Components/MineSharp.Commands/Parser/EntityParser.cs @@ -1,15 +1,18 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; namespace MineSharp.Commands.Parser; public class EntityParser : IParser { + public static readonly Identifier EntityIdentifier = Identifier.Parse("minecraft:entity"); + public byte Flags { get; private set; } - public string GetName() + public Identifier GetName() { - return "minecraft:entity"; + return EntityIdentifier; } public int GetArgumentCount() diff --git a/Components/MineSharp.Commands/Parser/FloatParser.cs b/Components/MineSharp.Commands/Parser/FloatParser.cs index 6d1dae21..55e8f340 100644 --- a/Components/MineSharp.Commands/Parser/FloatParser.cs +++ b/Components/MineSharp.Commands/Parser/FloatParser.cs @@ -1,16 +1,19 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; namespace MineSharp.Commands.Parser; public class FloatParser : IParser { + public static readonly Identifier BrigadierFloatIdentifier = Identifier.Parse("brigadier:float"); + public float Min { get; private set; } public float Max { get; private set; } - public string GetName() + public Identifier GetName() { - return "brigadier:float"; + return BrigadierFloatIdentifier; } public int GetArgumentCount() diff --git a/Components/MineSharp.Commands/Parser/IParser.cs b/Components/MineSharp.Commands/Parser/IParser.cs index fb71df25..1a92080f 100644 --- a/Components/MineSharp.Commands/Parser/IParser.cs +++ b/Components/MineSharp.Commands/Parser/IParser.cs @@ -1,11 +1,12 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; namespace MineSharp.Commands.Parser; public interface IParser { - public string GetName(); + public Identifier GetName(); public int GetArgumentCount(); public void ReadProperties(PacketBuffer buffer, MinecraftData data); diff --git a/Components/MineSharp.Commands/Parser/IntegerParser.cs b/Components/MineSharp.Commands/Parser/IntegerParser.cs index 5524ddad..fa7dbf40 100644 --- a/Components/MineSharp.Commands/Parser/IntegerParser.cs +++ b/Components/MineSharp.Commands/Parser/IntegerParser.cs @@ -1,16 +1,19 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; namespace MineSharp.Commands.Parser; public class IntegerParser : IParser { + public static readonly Identifier BrigadierIntegerIdentifier = Identifier.Parse("brigadier:integer"); + public int Min { get; private set; } public int Max { get; private set; } - public string GetName() + public Identifier GetName() { - return "brigadier:integer"; + return BrigadierIntegerIdentifier; } public int GetArgumentCount() diff --git a/Components/MineSharp.Commands/Parser/LongParser.cs b/Components/MineSharp.Commands/Parser/LongParser.cs index 77cdcf8b..0c5189b8 100644 --- a/Components/MineSharp.Commands/Parser/LongParser.cs +++ b/Components/MineSharp.Commands/Parser/LongParser.cs @@ -1,16 +1,19 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; namespace MineSharp.Commands.Parser; public class LongParser : IParser { + public static readonly Identifier BrigadierLongIdentifier = Identifier.Parse("brigadier:long"); + public long Min { get; private set; } public long Max { get; private set; } - public string GetName() + public Identifier GetName() { - return "brigadier:long"; + return BrigadierLongIdentifier; } public int GetArgumentCount() diff --git a/Components/MineSharp.Commands/Parser/MessageParser.cs b/Components/MineSharp.Commands/Parser/MessageParser.cs index fce41a89..3e5067de 100644 --- a/Components/MineSharp.Commands/Parser/MessageParser.cs +++ b/Components/MineSharp.Commands/Parser/MessageParser.cs @@ -1,13 +1,16 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; namespace MineSharp.Commands.Parser; public class MessageParser : IParser { - public string GetName() + public static readonly Identifier MessageIdentifier = Identifier.Parse("minecraft:message"); + + public Identifier GetName() { - return "minecraft:message"; + return MessageIdentifier; } public int GetArgumentCount() diff --git a/Components/MineSharp.Commands/Parser/ParserRegistry.cs b/Components/MineSharp.Commands/Parser/ParserRegistry.cs index ec14f9e5..eb5c9b5a 100644 --- a/Components/MineSharp.Commands/Parser/ParserRegistry.cs +++ b/Components/MineSharp.Commands/Parser/ParserRegistry.cs @@ -1,222 +1,223 @@ using MineSharp.Core; +using MineSharp.Core.Common; using MineSharp.Data; namespace MineSharp.Commands.Parser; internal static class ParserRegistry { - private static readonly IDictionary Mapping119 = new Dictionary + private static readonly Dictionary Mapping119 = new() { - { 0, "brigadier:bool" }, - { 1, "brigadier:float" }, - { 2, "brigadier:double" }, - { 3, "brigadier:integer" }, - { 4, "brigadier:long" }, - { 5, "brigadier:string" }, - { 6, "minecraft:entity" }, - { 7, "minecraft:game_profile" }, - { 8, "minecraft:block_pos" }, - { 9, "minecraft:column_pos" }, - { 10, "minecraft:vec3" }, - { 11, "minecraft:vec2" }, - { 12, "minecraft:block_state" }, - { 13, "minecraft:block_predicate" }, - { 14, "minecraft:item_stack" }, - { 15, "minecraft:item_predicate" }, - { 16, "minecraft:color" }, - { 17, "minecraft:component" }, - { 18, "minecraft:message" }, - { 19, "minecraft:nbt" }, - { 20, "minecraft:nbt_tag" }, - { 21, "minecraft:nbt_path" }, - { 22, "minecraft:objective" }, - { 23, "minecraft:objective_criteria" }, - { 24, "minecraft:operation" }, - { 25, "minecraft:particle" }, - { 26, "minecraft:angle" }, - { 27, "minecraft:rotation" }, - { 28, "minecraft:scoreboard_slot" }, - { 29, "minecraft:score_holder" }, - { 30, "minecraft:swizzle" }, - { 31, "minecraft:team" }, - { 32, "minecraft:item_slot" }, - { 33, "minecraft:resource_location" }, - { 34, "minecraft:mob_effect" }, - { 35, "minecraft:function" }, - { 36, "minecraft:entity_anchor" }, - { 37, "minecraft:int_range" }, - { 38, "minecraft:float_range" }, - { 39, "minecraft:item_enchantment" }, - { 40, "minecraft:entity_summon" }, - { 41, "minecraft:dimension" }, - { 42, "minecraft:time" }, - { 43, "minecraft:resource_or_tag" }, - { 44, "minecraft:resource" }, - { 45, "minecraft:template_mirror" }, - { 46, "minecraft:template_rotation" }, - { 47, "minecraft:uuid" } + { 0, Identifier.Parse("brigadier:bool") }, + { 1, Identifier.Parse("brigadier:float") }, + { 2, Identifier.Parse("brigadier:double") }, + { 3, Identifier.Parse("brigadier:integer") }, + { 4, Identifier.Parse("brigadier:long") }, + { 5, Identifier.Parse("brigadier:string") }, + { 6, Identifier.Parse("minecraft:entity") }, + { 7, Identifier.Parse("minecraft:game_profile") }, + { 8, Identifier.Parse("minecraft:block_pos") }, + { 9, Identifier.Parse("minecraft:column_pos") }, + { 10, Identifier.Parse("minecraft:vec3") }, + { 11, Identifier.Parse("minecraft:vec2") }, + { 12, Identifier.Parse("minecraft:block_state") }, + { 13, Identifier.Parse("minecraft:block_predicate") }, + { 14, Identifier.Parse("minecraft:item_stack") }, + { 15, Identifier.Parse("minecraft:item_predicate") }, + { 16, Identifier.Parse("minecraft:color") }, + { 17, Identifier.Parse("minecraft:component") }, + { 18, Identifier.Parse("minecraft:message") }, + { 19, Identifier.Parse("minecraft:nbt") }, + { 20, Identifier.Parse("minecraft:nbt_tag") }, + { 21, Identifier.Parse("minecraft:nbt_path") }, + { 22, Identifier.Parse("minecraft:objective") }, + { 23, Identifier.Parse("minecraft:objective_criteria") }, + { 24, Identifier.Parse("minecraft:operation") }, + { 25, Identifier.Parse("minecraft:particle") }, + { 26, Identifier.Parse("minecraft:angle") }, + { 27, Identifier.Parse("minecraft:rotation") }, + { 28, Identifier.Parse("minecraft:scoreboard_slot") }, + { 29, Identifier.Parse("minecraft:score_holder") }, + { 30, Identifier.Parse("minecraft:swizzle") }, + { 31, Identifier.Parse("minecraft:team") }, + { 32, Identifier.Parse("minecraft:item_slot") }, + { 33, Identifier.Parse("minecraft:resource_location") }, + { 34, Identifier.Parse("minecraft:mob_effect") }, + { 35, Identifier.Parse("minecraft:function") }, + { 36, Identifier.Parse("minecraft:entity_anchor") }, + { 37, Identifier.Parse("minecraft:int_range") }, + { 38, Identifier.Parse("minecraft:float_range") }, + { 39, Identifier.Parse("minecraft:item_enchantment") }, + { 40, Identifier.Parse("minecraft:entity_summon") }, + { 41, Identifier.Parse("minecraft:dimension") }, + { 42, Identifier.Parse("minecraft:time") }, + { 43, Identifier.Parse("minecraft:resource_or_tag") }, + { 44, Identifier.Parse("minecraft:resource") }, + { 45, Identifier.Parse("minecraft:template_mirror") }, + { 46, Identifier.Parse("minecraft:template_rotation") }, + { 47, Identifier.Parse("minecraft:uuid") } }; - private static readonly IDictionary Mapping1193 = new Dictionary + private static readonly Dictionary Mapping1193 = new() { - { 0, "brigadier:bool" }, - { 1, "brigadier:float" }, - { 2, "brigadier:double" }, - { 3, "brigadier:integer" }, - { 4, "brigadier:long" }, - { 5, "brigadier:string" }, - { 6, "minecraft:entity" }, - { 7, "minecraft:game_profile" }, - { 8, "minecraft:block_pos" }, - { 9, "minecraft:column_pos" }, - { 10, "minecraft:vec3" }, - { 11, "minecraft:vec2" }, - { 12, "minecraft:block_state" }, - { 13, "minecraft:block_predicate" }, - { 14, "minecraft:item_stack" }, - { 15, "minecraft:item_predicate" }, - { 16, "minecraft:color" }, - { 17, "minecraft:component" }, - { 18, "minecraft:message" }, - { 19, "minecraft:nbt" }, - { 20, "minecraft:nbt_tag" }, - { 21, "minecraft:nbt_path" }, - { 22, "minecraft:objective" }, - { 23, "minecraft:objective_criteria" }, - { 24, "minecraft:operation" }, - { 25, "minecraft:particle" }, - { 26, "minecraft:angle" }, - { 27, "minecraft:rotation" }, - { 28, "minecraft:scoreboard_slot" }, - { 29, "minecraft:score_holder" }, - { 30, "minecraft:swizzle" }, - { 31, "minecraft:team" }, - { 32, "minecraft:item_slot" }, - { 33, "minecraft:resource_location" }, - { 34, "minecraft:function" }, - { 35, "minecraft:entity_anchor" }, - { 36, "minecraft:int_range" }, - { 37, "minecraft:float_range" }, - { 38, "minecraft:dimension" }, - { 39, "minecraft:gamemode" }, - { 40, "minecraft:time" }, - { 41, "minecraft:resource_or_tag" }, - { 42, "minecraft:resource_or_tag_key" }, - { 43, "minecraft:resource" }, - { 44, "minecraft:resource_key" }, - { 45, "minecraft:template_mirror" }, - { 46, "minecraft:template_rotation" }, - { 47, "minecraft:uuid" } + { 0, Identifier.Parse("brigadier:bool") }, + { 1, Identifier.Parse("brigadier:float") }, + { 2, Identifier.Parse("brigadier:double") }, + { 3, Identifier.Parse("brigadier:integer") }, + { 4, Identifier.Parse("brigadier:long") }, + { 5, Identifier.Parse("brigadier:string") }, + { 6, Identifier.Parse("minecraft:entity") }, + { 7, Identifier.Parse("minecraft:game_profile") }, + { 8, Identifier.Parse("minecraft:block_pos") }, + { 9, Identifier.Parse("minecraft:column_pos") }, + { 10, Identifier.Parse("minecraft:vec3") }, + { 11, Identifier.Parse("minecraft:vec2") }, + { 12, Identifier.Parse("minecraft:block_state") }, + { 13, Identifier.Parse("minecraft:block_predicate") }, + { 14, Identifier.Parse("minecraft:item_stack") }, + { 15, Identifier.Parse("minecraft:item_predicate") }, + { 16, Identifier.Parse("minecraft:color") }, + { 17, Identifier.Parse("minecraft:component") }, + { 18, Identifier.Parse("minecraft:message") }, + { 19, Identifier.Parse("minecraft:nbt") }, + { 20, Identifier.Parse("minecraft:nbt_tag") }, + { 21, Identifier.Parse("minecraft:nbt_path") }, + { 22, Identifier.Parse("minecraft:objective") }, + { 23, Identifier.Parse("minecraft:objective_criteria") }, + { 24, Identifier.Parse("minecraft:operation") }, + { 25, Identifier.Parse("minecraft:particle") }, + { 26, Identifier.Parse("minecraft:angle") }, + { 27, Identifier.Parse("minecraft:rotation") }, + { 28, Identifier.Parse("minecraft:scoreboard_slot") }, + { 29, Identifier.Parse("minecraft:score_holder") }, + { 30, Identifier.Parse("minecraft:swizzle") }, + { 31, Identifier.Parse("minecraft:team") }, + { 32, Identifier.Parse("minecraft:item_slot") }, + { 33, Identifier.Parse("minecraft:resource_location") }, + { 34, Identifier.Parse("minecraft:function") }, + { 35, Identifier.Parse("minecraft:entity_anchor") }, + { 36, Identifier.Parse("minecraft:int_range") }, + { 37, Identifier.Parse("minecraft:float_range") }, + { 38, Identifier.Parse("minecraft:dimension") }, + { 39, Identifier.Parse("minecraft:gamemode") }, + { 40, Identifier.Parse("minecraft:time") }, + { 41, Identifier.Parse("minecraft:resource_or_tag") }, + { 42, Identifier.Parse("minecraft:resource_or_tag_key") }, + { 43, Identifier.Parse("minecraft:resource") }, + { 44, Identifier.Parse("minecraft:resource_key") }, + { 45, Identifier.Parse("minecraft:template_mirror") }, + { 46, Identifier.Parse("minecraft:template_rotation") }, + { 47, Identifier.Parse("minecraft:uuid") } }; - private static readonly IDictionary Mapping1194 = new Dictionary + private static readonly Dictionary Mapping1194 = new() { - { 0, "brigadier:bool" }, - { 1, "brigadier:float" }, - { 2, "brigadier:double" }, - { 3, "brigadier:integer" }, - { 4, "brigadier:long" }, - { 5, "brigadier:string" }, - { 6, "minecraft:entity" }, - { 7, "minecraft:game_profile" }, - { 8, "minecraft:block_pos" }, - { 9, "minecraft:column_pos" }, - { 10, "minecraft:vec3" }, - { 11, "minecraft:vec2" }, - { 12, "minecraft:block_state" }, - { 13, "minecraft:block_predicate" }, - { 14, "minecraft:item_stack" }, - { 15, "minecraft:item_predicate" }, - { 16, "minecraft:color" }, - { 17, "minecraft:component" }, - { 18, "minecraft:message" }, - { 19, "minecraft:nbt" }, - { 20, "minecraft:nbt_tag" }, - { 21, "minecraft:nbt_path" }, - { 22, "minecraft:objective" }, - { 23, "minecraft:objective_criteria" }, - { 24, "minecraft:operation" }, - { 25, "minecraft:particle" }, - { 26, "minecraft:angle" }, - { 27, "minecraft:rotation" }, - { 28, "minecraft:scoreboard_slot" }, - { 29, "minecraft:score_holder" }, - { 30, "minecraft:swizzle" }, - { 31, "minecraft:team" }, - { 32, "minecraft:item_slot" }, - { 33, "minecraft:resource_location" }, - { 34, "minecraft:function" }, - { 35, "minecraft:entity_anchor" }, - { 36, "minecraft:int_range" }, - { 37, "minecraft:float_range" }, - { 38, "minecraft:dimension" }, - { 39, "minecraft:gamemode" }, - { 40, "minecraft:time" }, - { 41, "minecraft:resource_or_tag" }, - { 42, "minecraft:resource_or_tag_key" }, - { 43, "minecraft:resource" }, - { 44, "minecraft:resource_key" }, - { 45, "minecraft:template_mirror" }, - { 46, "minecraft:template_rotation" }, - { 47, "minecraft:heightmap" }, - { 48, "minecraft:uuid" } + { 0, Identifier.Parse("brigadier:bool") }, + { 1, Identifier.Parse("brigadier:float") }, + { 2, Identifier.Parse("brigadier:double") }, + { 3, Identifier.Parse("brigadier:integer") }, + { 4, Identifier.Parse("brigadier:long") }, + { 5, Identifier.Parse("brigadier:string") }, + { 6, Identifier.Parse("minecraft:entity") }, + { 7, Identifier.Parse("minecraft:game_profile") }, + { 8, Identifier.Parse("minecraft:block_pos") }, + { 9, Identifier.Parse("minecraft:column_pos") }, + { 10, Identifier.Parse("minecraft:vec3") }, + { 11, Identifier.Parse("minecraft:vec2") }, + { 12, Identifier.Parse("minecraft:block_state") }, + { 13, Identifier.Parse("minecraft:block_predicate") }, + { 14, Identifier.Parse("minecraft:item_stack") }, + { 15, Identifier.Parse("minecraft:item_predicate") }, + { 16, Identifier.Parse("minecraft:color") }, + { 17, Identifier.Parse("minecraft:component") }, + { 18, Identifier.Parse("minecraft:message") }, + { 19, Identifier.Parse("minecraft:nbt") }, + { 20, Identifier.Parse("minecraft:nbt_tag") }, + { 21, Identifier.Parse("minecraft:nbt_path") }, + { 22, Identifier.Parse("minecraft:objective") }, + { 23, Identifier.Parse("minecraft:objective_criteria") }, + { 24, Identifier.Parse("minecraft:operation") }, + { 25, Identifier.Parse("minecraft:particle") }, + { 26, Identifier.Parse("minecraft:angle") }, + { 27, Identifier.Parse("minecraft:rotation") }, + { 28, Identifier.Parse("minecraft:scoreboard_slot") }, + { 29, Identifier.Parse("minecraft:score_holder") }, + { 30, Identifier.Parse("minecraft:swizzle") }, + { 31, Identifier.Parse("minecraft:team") }, + { 32, Identifier.Parse("minecraft:item_slot") }, + { 33, Identifier.Parse("minecraft:resource_location") }, + { 34, Identifier.Parse("minecraft:function") }, + { 35, Identifier.Parse("minecraft:entity_anchor") }, + { 36, Identifier.Parse("minecraft:int_range") }, + { 37, Identifier.Parse("minecraft:float_range") }, + { 38, Identifier.Parse("minecraft:dimension") }, + { 39, Identifier.Parse("minecraft:gamemode") }, + { 40, Identifier.Parse("minecraft:time") }, + { 41, Identifier.Parse("minecraft:resource_or_tag") }, + { 42, Identifier.Parse("minecraft:resource_or_tag_key") }, + { 43, Identifier.Parse("minecraft:resource") }, + { 44, Identifier.Parse("minecraft:resource_key") }, + { 45, Identifier.Parse("minecraft:template_mirror") }, + { 46, Identifier.Parse("minecraft:template_rotation") }, + { 47, Identifier.Parse("minecraft:heightmap") }, + { 48, Identifier.Parse("minecraft:uuid") } }; - private static readonly IDictionary Mapping1203 = new Dictionary + private static readonly Dictionary Mapping1203 = new() { - { 0, "brigadier:bool" }, - { 1, "brigadier:float" }, - { 2, "brigadier:double" }, - { 3, "brigadier:integer" }, - { 4, "brigadier:long" }, - { 5, "brigadier:string" }, - { 6, "minecraft:entity" }, - { 7, "minecraft:game_profile" }, - { 8, "minecraft:block_pos" }, - { 9, "minecraft:column_pos" }, - { 10, "minecraft:vec3" }, - { 11, "minecraft:vec2" }, - { 12, "minecraft:block_state" }, - { 13, "minecraft:block_predicate" }, - { 14, "minecraft:item_stack" }, - { 15, "minecraft:item_predicate" }, - { 16, "minecraft:color" }, - { 17, "minecraft:component" }, - { 18, "minecraft:style" }, - { 19, "minecraft:message" }, - { 20, "minecraft:nbt" }, - { 21, "minecraft:nbt_tag" }, - { 22, "minecraft:nbt_path" }, - { 23, "minecraft:objective" }, - { 24, "minecraft:objective_criteria" }, - { 25, "minecraft:operation" }, - { 26, "minecraft:particle" }, - { 27, "minecraft:angle" }, - { 28, "minecraft:rotation" }, - { 29, "minecraft:scoreboard_slot" }, - { 30, "minecraft:score_holder" }, - { 31, "minecraft:swizzle" }, - { 32, "minecraft:team" }, - { 33, "minecraft:item_slot" }, - { 34, "minecraft:resource_location" }, - { 35, "minecraft:function" }, - { 36, "minecraft:entity_anchor" }, - { 37, "minecraft:int_range" }, - { 38, "minecraft:float_range" }, - { 39, "minecraft:dimension" }, - { 40, "minecraft:gamemode" }, - { 41, "minecraft:time" }, - { 42, "minecraft:resource_or_tag" }, - { 43, "minecraft:resource_or_tag_key" }, - { 44, "minecraft:resource" }, - { 45, "minecraft:resource_key" }, - { 46, "minecraft:template_mirror" }, - { 47, "minecraft:template_rotation" }, - { 48, "minecraft:heightmap" }, - { 49, "minecraft:uuid" } + { 0, Identifier.Parse("brigadier:bool") }, + { 1, Identifier.Parse("brigadier:float") }, + { 2, Identifier.Parse("brigadier:double") }, + { 3, Identifier.Parse("brigadier:integer") }, + { 4, Identifier.Parse("brigadier:long") }, + { 5, Identifier.Parse("brigadier:string") }, + { 6, Identifier.Parse("minecraft:entity") }, + { 7, Identifier.Parse("minecraft:game_profile") }, + { 8, Identifier.Parse("minecraft:block_pos") }, + { 9, Identifier.Parse("minecraft:column_pos") }, + { 10, Identifier.Parse("minecraft:vec3") }, + { 11, Identifier.Parse("minecraft:vec2") }, + { 12, Identifier.Parse("minecraft:block_state") }, + { 13, Identifier.Parse("minecraft:block_predicate") }, + { 14, Identifier.Parse("minecraft:item_stack") }, + { 15, Identifier.Parse("minecraft:item_predicate") }, + { 16, Identifier.Parse("minecraft:color") }, + { 17, Identifier.Parse("minecraft:component") }, + { 18, Identifier.Parse("minecraft:style") }, + { 19, Identifier.Parse("minecraft:message") }, + { 20, Identifier.Parse("minecraft:nbt") }, + { 21, Identifier.Parse("minecraft:nbt_tag") }, + { 22, Identifier.Parse("minecraft:nbt_path") }, + { 23, Identifier.Parse("minecraft:objective") }, + { 24, Identifier.Parse("minecraft:objective_criteria") }, + { 25, Identifier.Parse("minecraft:operation") }, + { 26, Identifier.Parse("minecraft:particle") }, + { 27, Identifier.Parse("minecraft:angle") }, + { 28, Identifier.Parse("minecraft:rotation") }, + { 29, Identifier.Parse("minecraft:scoreboard_slot") }, + { 30, Identifier.Parse("minecraft:score_holder") }, + { 31, Identifier.Parse("minecraft:swizzle") }, + { 32, Identifier.Parse("minecraft:team") }, + { 33, Identifier.Parse("minecraft:item_slot") }, + { 34, Identifier.Parse("minecraft:resource_location") }, + { 35, Identifier.Parse("minecraft:function") }, + { 36, Identifier.Parse("minecraft:entity_anchor") }, + { 37, Identifier.Parse("minecraft:int_range") }, + { 38, Identifier.Parse("minecraft:float_range") }, + { 39, Identifier.Parse("minecraft:dimension") }, + { 40, Identifier.Parse("minecraft:gamemode") }, + { 41, Identifier.Parse("minecraft:time") }, + { 42, Identifier.Parse("minecraft:resource_or_tag") }, + { 43, Identifier.Parse("minecraft:resource_or_tag_key") }, + { 44, Identifier.Parse("minecraft:resource") }, + { 45, Identifier.Parse("minecraft:resource_key") }, + { 46, Identifier.Parse("minecraft:template_mirror") }, + { 47, Identifier.Parse("minecraft:template_rotation") }, + { 48, Identifier.Parse("minecraft:heightmap") }, + { 49, Identifier.Parse("minecraft:uuid") } }; - public static string GetParserNameById(int parserId, MinecraftData data) + public static Identifier GetParserNameById(int parserId, MinecraftData data) { var mapping = data.Version.Protocol switch { @@ -231,9 +232,9 @@ public static string GetParserNameById(int parserId, MinecraftData data) return mapping[parserId]; } - public static IParser GetParserByName(string name) + public static IParser GetParserByName(Identifier name) { - return name switch + return name.ToString() switch { "brigadier:bool" => new EmptyParser(), "brigadier:float" => new FloatParser(), diff --git a/Components/MineSharp.Commands/Parser/RangeParser.cs b/Components/MineSharp.Commands/Parser/RangeParser.cs index 7afeb1ed..f03cfa59 100644 --- a/Components/MineSharp.Commands/Parser/RangeParser.cs +++ b/Components/MineSharp.Commands/Parser/RangeParser.cs @@ -1,15 +1,18 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; namespace MineSharp.Commands.Parser; public class RangeParser : IParser { + public static readonly Identifier RangeIdentifier = Identifier.Parse("minecraft:range"); + public bool Decimals { get; private set; } - public string GetName() + public Identifier GetName() { - return "minecraft:range"; + return RangeIdentifier; } public int GetArgumentCount() diff --git a/Components/MineSharp.Commands/Parser/ResourceOrTagParser.cs b/Components/MineSharp.Commands/Parser/ResourceOrTagParser.cs index ffbed038..9211dce1 100644 --- a/Components/MineSharp.Commands/Parser/ResourceOrTagParser.cs +++ b/Components/MineSharp.Commands/Parser/ResourceOrTagParser.cs @@ -1,15 +1,18 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; namespace MineSharp.Commands.Parser; public class ResourceOrTagParser : IParser { + public static readonly Identifier ResourceOrTagIdentifier = Identifier.Parse("minecraft:resource_or_tag"); + public string Registry { get; private set; } = string.Empty; - public string GetName() + public Identifier GetName() { - return "minecraft:resource_or_tag"; + return ResourceOrTagIdentifier; } public int GetArgumentCount() diff --git a/Components/MineSharp.Commands/Parser/ResourceParser.cs b/Components/MineSharp.Commands/Parser/ResourceParser.cs index a57ce7a2..b9806f67 100644 --- a/Components/MineSharp.Commands/Parser/ResourceParser.cs +++ b/Components/MineSharp.Commands/Parser/ResourceParser.cs @@ -1,16 +1,18 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; namespace MineSharp.Commands.Parser; public class ResourceParser : IParser { - public string? Registry { get; private set; } + public static readonly Identifier ResourceIdentifier = Identifier.Parse("minecraft:resource"); + public string? Registry { get; private set; } - public string GetName() + public Identifier GetName() { - return "minecraft:resource"; + return ResourceIdentifier; } public int GetArgumentCount() diff --git a/Components/MineSharp.Commands/Parser/RotationParser.cs b/Components/MineSharp.Commands/Parser/RotationParser.cs index 52495a08..949aaebf 100644 --- a/Components/MineSharp.Commands/Parser/RotationParser.cs +++ b/Components/MineSharp.Commands/Parser/RotationParser.cs @@ -1,13 +1,16 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; namespace MineSharp.Commands.Parser; public class RotationParser : IParser { - public string GetName() + public static readonly Identifier RotationIdentifier = Identifier.Parse("minecraft:rotation"); + + public Identifier GetName() { - return "minecraft:rotation"; + return RotationIdentifier; } public int GetArgumentCount() diff --git a/Components/MineSharp.Commands/Parser/ScoreHolderParser.cs b/Components/MineSharp.Commands/Parser/ScoreHolderParser.cs index 07140320..269c7147 100644 --- a/Components/MineSharp.Commands/Parser/ScoreHolderParser.cs +++ b/Components/MineSharp.Commands/Parser/ScoreHolderParser.cs @@ -1,15 +1,18 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; namespace MineSharp.Commands.Parser; public class ScoreHolderParser : IParser { + public static readonly Identifier ScoreHolderIdentifier = Identifier.Parse("minecraft:score_holder"); + public byte Flags { get; private set; } - public string GetName() + public Identifier GetName() { - return "minecraft:score_holder"; + return ScoreHolderIdentifier; } public int GetArgumentCount() diff --git a/Components/MineSharp.Commands/Parser/StringParser.cs b/Components/MineSharp.Commands/Parser/StringParser.cs index bed0ebd5..2bb73dcd 100644 --- a/Components/MineSharp.Commands/Parser/StringParser.cs +++ b/Components/MineSharp.Commands/Parser/StringParser.cs @@ -1,15 +1,18 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; namespace MineSharp.Commands.Parser; public class StringParser : IParser { + public static readonly Identifier BrigadierStringIdentifier = Identifier.Parse("brigadier:string"); + public StringType Type { get; private set; } - public string GetName() + public Identifier GetName() { - return "brigadier:string"; + return BrigadierStringIdentifier; } public int GetArgumentCount() diff --git a/Components/MineSharp.Commands/Parser/TimeParser.cs b/Components/MineSharp.Commands/Parser/TimeParser.cs index ef562845..bda62512 100644 --- a/Components/MineSharp.Commands/Parser/TimeParser.cs +++ b/Components/MineSharp.Commands/Parser/TimeParser.cs @@ -1,16 +1,19 @@ using MineSharp.Core; using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; namespace MineSharp.Commands.Parser; public class TimeParser : IParser { + public static readonly Identifier TimeIdentifier = Identifier.Parse("minecraft:time"); + public int? Min { get; private set; } - public string GetName() + public Identifier GetName() { - return "minecraft:time"; + return TimeIdentifier; } public int GetArgumentCount() diff --git a/Components/MineSharp.Commands/Parser/Vec2Parser.cs b/Components/MineSharp.Commands/Parser/Vec2Parser.cs index 4323acc9..a1683a10 100644 --- a/Components/MineSharp.Commands/Parser/Vec2Parser.cs +++ b/Components/MineSharp.Commands/Parser/Vec2Parser.cs @@ -1,13 +1,16 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; namespace MineSharp.Commands.Parser; public class Vec2Parser : IParser { - public string GetName() + public static readonly Identifier Vec2Identifier = Identifier.Parse("minecraft:vec2"); + + public Identifier GetName() { - return "minecraft:vec2"; + return Vec2Identifier; } public int GetArgumentCount() diff --git a/Components/MineSharp.Commands/Parser/Vec3Parser.cs b/Components/MineSharp.Commands/Parser/Vec3Parser.cs index d3e451bd..556fa4ea 100644 --- a/Components/MineSharp.Commands/Parser/Vec3Parser.cs +++ b/Components/MineSharp.Commands/Parser/Vec3Parser.cs @@ -1,13 +1,16 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; namespace MineSharp.Commands.Parser; public class Vec3Parser : IParser { - public string GetName() + public static readonly Identifier Vec3Identifier = Identifier.Parse("minecraft:vec3"); + + public Identifier GetName() { - return "minecraft:vec3"; + return Vec3Identifier; } public int GetArgumentCount() diff --git a/Components/MineSharp.Physics/Components/FluidPhysicsComponent.cs b/Components/MineSharp.Physics/Components/FluidPhysicsComponent.cs index e61e0c12..100f8939 100644 --- a/Components/MineSharp.Physics/Components/FluidPhysicsComponent.cs +++ b/Components/MineSharp.Physics/Components/FluidPhysicsComponent.cs @@ -62,14 +62,14 @@ private bool DoFluidPushing(BlockType type, double factor, out double height) var vel = Vector3.Zero.Clone(); var k1 = 0; - var pos = new MutablePosition(0, 0, 0); + var pos = Position.Zero; for (var x = fromX; x < toX; ++x) // TODO: Implement world iterators, that would be nicer { for (var y = fromY; y < toY; ++y) { for (var z = fromZ; z < toZ; ++z) { - pos.Set(x, y, z); + pos = new(x, y, z); var block = World.GetBlockAt(pos); if (block.Info.Type != type) @@ -143,12 +143,12 @@ private MutableVector3 GetFlow(IWorld world, Block fluid) { var dX = 0.0d; var dZ = 0.0d; - var pos = new MutablePosition(0, 0, 0); + var pos = Position.Zero; var height = GetFluidHeight(world, fluid); foreach (var direction in XzPlane) { - pos.Set( + pos = new( fluid.Position.X + (int)direction.X, fluid.Position.Y + (int)direction.Y, fluid.Position.Z + (int)direction.Z); diff --git a/Components/MineSharp.Physics/MineSharp.Physics.csproj b/Components/MineSharp.Physics/MineSharp.Physics.csproj index e1047677..900caf14 100644 --- a/Components/MineSharp.Physics/MineSharp.Physics.csproj +++ b/Components/MineSharp.Physics/MineSharp.Physics.csproj @@ -3,7 +3,7 @@ enable enable - net7.0;net8.0 + net8.0 12 true README.md diff --git a/Components/MineSharp.Physics/PhysicsConst.cs b/Components/MineSharp.Physics/PhysicsConst.cs index 2192e014..79b4c67b 100644 --- a/Components/MineSharp.Physics/PhysicsConst.cs +++ b/Components/MineSharp.Physics/PhysicsConst.cs @@ -1,4 +1,5 @@ -using MineSharp.Core.Common.Blocks; +using MineSharp.Core.Common; +using MineSharp.Core.Common.Blocks; using MineSharp.Core.Geometry; namespace MineSharp.Physics; @@ -20,7 +21,7 @@ internal static class PhysicsConst public const double FluidJumpFactor = 0.04d; public const int JumpDelay = 10; - public const string AttrMovementSpeed = "generic.movement_speed"; + public static readonly Identifier AttrMovementSpeed = Identifier.Parse("generic.movement_speed"); public const string SprintingUuid = "662a6b8d-da3e-4c1c-8813-96ea6097278d"; public const double PlayerSprintSpeed = 0.3d; public const double DefaultPlayerSpeed = 0.1d; diff --git a/Components/MineSharp.Protocol/ClientSettings.cs b/Components/MineSharp.Protocol/ClientSettings.cs index 25f4a041..1f6ef572 100644 --- a/Components/MineSharp.Protocol/ClientSettings.cs +++ b/Components/MineSharp.Protocol/ClientSettings.cs @@ -1,5 +1,4 @@ using MineSharp.Core.Common; -using NLog; namespace MineSharp.Protocol; @@ -16,7 +15,7 @@ public record ClientSettings 24, ChatMode.Enabled, true, - 0x7F, + SkinPart.All, PlayerHand.MainHand, false, true); @@ -25,7 +24,7 @@ public record ClientSettings /// Constructor /// public ClientSettings(string locale, byte viewDistance, ChatMode chatMode, bool coloredChat, - byte displayedSkinParts, + SkinPart displayedSkinParts, PlayerHand mainHand, bool enableTextFiltering, bool allowServerListings) { Locale = locale; @@ -61,7 +60,7 @@ public ClientSettings(string locale, byte viewDistance, ChatMode chatMode, bool /// /// Bitmask of skin parts displayed by the client (not used) /// - public byte DisplayedSkinParts { get; } + public SkinPart DisplayedSkinParts { get; } /// /// The clients main hand @@ -79,6 +78,26 @@ public ClientSettings(string locale, byte viewDistance, ChatMode chatMode, bool public bool AllowServerListings { get; } } +/// +/// Specifies the parts of the skin that are enabled +/// +[Flags] +public enum SkinPart : byte +{ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + None = 0x00, + Cape = 0x01, + Jacket = 0x02, + LeftSleeve = 0x04, + RightSleeve = 0x08, + LeftPants = 0x10, + RightPants = 0x20, + Hat = 0x40, + // 0x80 is unused + All = Cape | Jacket | LeftSleeve | RightSleeve | LeftPants | RightPants | Hat +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} + /// /// Specifies the chat mode /// diff --git a/Components/MineSharp.Protocol/Connection/IConnectionFactory.cs b/Components/MineSharp.Protocol/Connection/IConnectionFactory.cs index 8396f8f0..46c8e28e 100644 --- a/Components/MineSharp.Protocol/Connection/IConnectionFactory.cs +++ b/Components/MineSharp.Protocol/Connection/IConnectionFactory.cs @@ -16,5 +16,9 @@ public interface IConnectionFactory /// public Task CreateOpenConnection(IPAddress address, ushort port); + /// + /// Create an HTTP client + /// + /// public HttpClient CreateHttpClient(); } diff --git a/Components/MineSharp.Protocol/Cryptography/AesStream.cs b/Components/MineSharp.Protocol/Cryptography/AesStream.cs index ad64c514..f601f066 100644 --- a/Components/MineSharp.Protocol/Cryptography/AesStream.cs +++ b/Components/MineSharp.Protocol/Cryptography/AesStream.cs @@ -22,11 +22,12 @@ public class AesStream : Stream /// public AesStream(Stream stream, byte[] key) { + var cipherParameters = new ParametersWithIV(new KeyParameter(key), key, 0, 16); encryptCipher = new(new CfbBlockCipher(new AesEngine(), 8)); - encryptCipher.Init(true, new ParametersWithIV(new KeyParameter(key), key, 0, 16)); + encryptCipher.Init(true, cipherParameters); decryptCipher = new(new CfbBlockCipher(new AesEngine(), 8)); - decryptCipher.Init(false, new ParametersWithIV(new KeyParameter(key), key, 0, 16)); + decryptCipher.Init(false, cipherParameters); baseStream = new CipherStream(stream, decryptCipher, encryptCipher); } diff --git a/Components/MineSharp.Protocol/Cryptography/EncryptionHelper.cs b/Components/MineSharp.Protocol/Cryptography/EncryptionHelper.cs index ff31c87f..732dc691 100644 --- a/Components/MineSharp.Protocol/Cryptography/EncryptionHelper.cs +++ b/Components/MineSharp.Protocol/Cryptography/EncryptionHelper.cs @@ -7,15 +7,16 @@ namespace MineSharp.Protocol.Cryptography; -internal class EncryptionHelper +internal partial class EncryptionHelper { private static readonly ILogger Logger = LogManager.GetCurrentClassLogger(); + private static readonly ReadOnlyMemory SeqOid = new([0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00]); + public static RSA? DecodePublicKey(byte[] publicKeyBytes) { var ms = new MemoryStream(publicKeyBytes); var rd = new BinaryReader(ms); - byte[] seqOid = { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 }; var seq = new byte[15]; try @@ -45,7 +46,7 @@ internal class EncryptionHelper seq = rd.ReadBytes(15); - if (!CompareBytearrays(seq, seqOid)) + if (!seq.AsSpan().SequenceEqual(SeqOid.Span)) { return null; } @@ -96,16 +97,16 @@ internal class EncryptionHelper var rsa = RSA.Create(); //RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(parms); - var rsAparams = new RSAParameters(); + var rsaParams = new RSAParameters(); - rsAparams.Modulus = rd.ReadBytes(DecodeIntegerSize(rd)); + rsaParams.Modulus = rd.ReadBytes(DecodeIntegerSize(rd)); - GetTraits(rsAparams.Modulus.Length * 8, out var sizeMod, out var sizeExp); + GetTraits(rsaParams.Modulus.Length * 8, out var sizeMod, out var sizeExp); - rsAparams.Modulus = AlignBytes(rsAparams.Modulus, sizeMod); - rsAparams.Exponent = AlignBytes(rd.ReadBytes(DecodeIntegerSize(rd)), sizeExp); + rsaParams.Modulus = AlignBytes(rsaParams.Modulus, sizeMod); + rsaParams.Exponent = AlignBytes(rd.ReadBytes(DecodeIntegerSize(rd)), sizeExp); - rsa.ImportParameters(rsAparams); + rsa.ImportParameters(rsaParams); return rsa; } @@ -121,28 +122,6 @@ internal class EncryptionHelper } } - private static bool CompareBytearrays(byte[] a, byte[] b) - { - if (a.Length != b.Length) - { - return false; - } - - var i = 0; - - foreach (var c in a) - { - if (c != b[i]) - { - return false; - } - - i++; - } - - return true; - } - private static byte[] AlignBytes(byte[] inputBytes, int alignSize) { var inputBytesSize = inputBytes.Length; @@ -151,10 +130,7 @@ private static byte[] AlignBytes(byte[] inputBytes, int alignSize) { var buf = new byte[alignSize]; - for (var i = 0; i < inputBytesSize; ++i) - { - buf[i + (alignSize - inputBytesSize)] = inputBytes[i]; - } + inputBytes.CopyTo(buf.AsSpan().Slice(alignSize - inputBytesSize)); return buf; } @@ -254,7 +230,7 @@ private static void GetTraits(int modulusLengthInBits, out int sizeMod, out int public static string PemKeyToDer(string pem) { - var rx = new Regex("-+[^-]+-+"); + var rx = PemKeyHeaderFooterRegex(); var der = rx.Replace(pem, "") .Replace("\r", "") .Replace("\n", ""); @@ -279,4 +255,7 @@ public static string ComputeHash(string serverId, byte[] key, byte[] publicKey) return hex.TrimStart('0'); } + + [GeneratedRegex("-+[^-]+-+")] + private static partial Regex PemKeyHeaderFooterRegex(); } diff --git a/Components/MineSharp.Protocol/MineSharp.Protocol.csproj b/Components/MineSharp.Protocol/MineSharp.Protocol.csproj index 7114deb9..1e46f5a6 100644 --- a/Components/MineSharp.Protocol/MineSharp.Protocol.csproj +++ b/Components/MineSharp.Protocol/MineSharp.Protocol.csproj @@ -3,7 +3,7 @@ enable enable - net7.0;net8.0 + net8.0 12 true README.md diff --git a/Components/MineSharp.Protocol/MinecraftClient.cs b/Components/MineSharp.Protocol/MinecraftClient.cs index 8c826dd6..5a58635c 100644 --- a/Components/MineSharp.Protocol/MinecraftClient.cs +++ b/Components/MineSharp.Protocol/MinecraftClient.cs @@ -2,13 +2,15 @@ using System.Diagnostics; using System.Net; using System.Net.Sockets; +using System.Threading.Tasks.Dataflow; +using ConcurrentCollections; using MineSharp.Auth; using MineSharp.ChatComponent; using MineSharp.ChatComponent.Components; -using MineSharp.Core; -using MineSharp.Core.Common; using MineSharp.Core.Common.Protocol; +using MineSharp.Core.Concurrency; using MineSharp.Core.Events; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; using MineSharp.Protocol.Connection; @@ -16,18 +18,20 @@ using MineSharp.Protocol.Packets; using MineSharp.Protocol.Packets.Clientbound.Status; using MineSharp.Protocol.Packets.Handlers; -using MineSharp.Protocol.Packets.Serverbound.Configuration; using MineSharp.Protocol.Packets.Serverbound.Status; +using MineSharp.Protocol.Registrations; using Newtonsoft.Json.Linq; using NLog; +using ConfigurationClientInformationPacket = MineSharp.Protocol.Packets.Serverbound.Configuration.ClientInformationPacket; +using PlayClientInformationPacket = MineSharp.Protocol.Packets.Serverbound.Play.ClientInformationPacket; namespace MineSharp.Protocol; /// /// A Minecraft client. -/// Connect to a minecraft server. +/// Connect to a Minecraft server. /// -public sealed class MinecraftClient : IDisposable +public sealed class MinecraftClient : IAsyncDisposable, IDisposable { /// /// Delegate for handling packets async @@ -54,22 +58,21 @@ public sealed class MinecraftClient : IDisposable /// public readonly MinecraftData Data; - private readonly TaskCompletionSource gameJoinedTsc; - /// - /// The Hostname of the minecraft server provided in the constructor + /// The Hostname of the Minecraft server provided in the constructor /// public readonly string Hostname; - private readonly IPAddress ip; - private readonly IDictionary> packetHandlers; - private readonly ConcurrentQueue packetQueue; - private readonly ConcurrentQueue<(PacketType, PacketBuffer)> bundledPackets; - private readonly IDictionary> packetWaiters; - private readonly CancellationTokenSource cancellationTokenSource; + private readonly IPAddress ip; + + /// + /// Is cancelled once the client needs to stop. Usually because the connection was lost. + /// + // This variable exists (and is not a property) to prevent the possible problem when getting the token from the source when it's already disposed. + public readonly CancellationToken CancellationToken; /// - /// The Port of the minecraft server + /// The Port of the Minecraft server /// public readonly ushort Port; @@ -82,21 +85,56 @@ public sealed class MinecraftClient : IDisposable /// The clients settings /// public readonly ClientSettings Settings; - + /// /// Fires when the client disconnected from the server /// public AsyncEvent OnDisconnected = new(); + /// + /// Fired when the TCP connection to the server is lost. + /// This is called after but also in cases where is not called before. + /// + /// If you want to be notified when the client is no longer functional, register to instead. + /// is also cancelled when the connection is lost. + /// + public AsyncEvent OnConnectionLost = new(); + private readonly IConnectionFactory tcpTcpFactory; - private TcpClient? client; + private TcpClient? client; private MinecraftStream? stream; - private Task? streamLoop; - private GameState gameState; - private IPacketHandler internalPacketHandler; - private object streamLock = new(); - private bool bundlePackets; + private Task? streamLoop; + private int onConnectionLostFired; + + private readonly ConcurrentDictionary> packetHandlers; + private readonly ConcurrentDictionary> packetWaiters; + private readonly ConcurrentHashSet packetReceivers; + private GameStatePacketHandler gameStatePacketHandler; + private readonly BufferBlock packetQueue; + /// + /// Contains the packets that are bundled together. + /// + /// If this field is null, packets are not bundled. + /// If this field is not null, packets are bundled. + /// This way we can avoid all race conditions that would occur if we would use a boolean flag. + /// + private ConcurrentQueue<(PacketType Type, PacketBuffer Buffer)>? bundledPackets; + + private readonly CancellationTokenSource cancellationTokenSource; + + /// + /// Will be completed once the client has entered the state. + /// + internal readonly TaskCompletionSource GameJoinedTcs; + + + /// + /// The current of the client. + /// + /// Internal note: This property should not be used to determine the next because that is not thread safe. + /// + public GameState GameState => gameStatePacketHandler.GameState; /// /// Create a new MinecraftClient @@ -111,13 +149,18 @@ public MinecraftClient( ClientSettings settings) { Data = data; - packetQueue = new(); cancellationTokenSource = new(); - internalPacketHandler = new HandshakePacketHandler(this); - packetHandlers = new Dictionary>(); - packetWaiters = new Dictionary>(); - gameJoinedTsc = new(); - bundledPackets = new(); + CancellationToken = cancellationTokenSource.Token; + packetQueue = new(new DataflowBlockOptions() + { + CancellationToken = CancellationToken + }); + gameStatePacketHandler = new NoStatePacketHandler(this); + packetHandlers = new(); + packetWaiters = new(); + packetReceivers = new(); + GameJoinedTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); + bundledPackets = null; tcpTcpFactory = tcpFactory; ip = IpHelper.ResolveHostname(hostnameOrIp, ref port); @@ -125,18 +168,59 @@ public MinecraftClient( Session = session; Port = port; Hostname = hostnameOrIp; - gameState = GameState.Handshaking; Settings = settings; } - /// - public void Dispose() + /// + /// Must only be called after the was completed (this happens when was cancelled). + /// Otherwise race conditions can occur where there are still uncancelled tasks in the queue. + /// + private void CancelAllSendPendingPacketTasks() + { + // we need to cancel all tasks in the queue otherwise we might get a deadlock + // when some task is waiting for the packet task + if (packetQueue.TryReceiveAll(out var queuedPackets)) + { + foreach (var task in queuedPackets) + { + task.Task.TrySetCanceled(CancellationToken); + } + } + } + + private async Task DisposeInternal(bool calledFromStreamLoop) { cancellationTokenSource.Cancel(); - streamLoop?.Wait(); + // wait for the packetQueue to complete + await packetQueue.Completion; + CancelAllSendPendingPacketTasks(); + + // prevent waiting on ourselves (when called from the streamLoop task) + if (!calledFromStreamLoop) + { + await (streamLoop ?? Task.CompletedTask); + } client?.Dispose(); stream?.Close(); + + if (Interlocked.Exchange(ref onConnectionLostFired, 1) == 0) + { + await OnConnectionLost.Dispatch(this); + } + } + + /// + public async ValueTask DisposeAsync() + { + await DisposeInternal(false); + } + + + /// + public void Dispose() + { + DisposeAsync().AsTask().Wait(); } /// @@ -152,7 +236,7 @@ public async Task Connect(GameState nextState) return true; } - Logger.Debug($"Connecting to {ip}:{Port}."); + Logger.Debug($"Connecting to {ip}:{Port} with PVN={Data.Version.Protocol}"); try { @@ -178,40 +262,90 @@ public async Task Connect(GameState nextState) /// The packet to send. /// Optional cancellation token. /// A task that resolves once the packet was actually sent. - public Task SendPacket(IPacket packet, CancellationToken cancellation = default) + public async Task SendPacket(IPacket packet, CancellationToken cancellation = default) { - var sendingTask = new PacketSendTask(packet, cancellation, new()); - packetQueue.Enqueue(sendingTask); + var sendingTask = new PacketSendTask(packet, cancellation, new(TaskCreationOptions.RunContinuationsAsynchronously)); + try + { + if (!await packetQueue.SendAsync(sendingTask, cancellation)) + { + // if the packetQueue is completed we can not send any more packets + // so we need to cancel the task here + // this must have happened because the CancellationToken was cancelled + Logger.Warn("Packet {PacketType} could not be added send queue. Queue closed.", packet.Type); + sendingTask.Task.TrySetCanceled(CancellationToken); + } + else + { + Logger.Trace("Packet {PacketType} was added to send queue", packet.Type); + } + } + catch (OperationCanceledException e) + { + Logger.Warn("Packet {PacketType} could not be added send queue. Sending Packet CancellationToken was cancelled.", packet.Type); + sendingTask.Task.TrySetCanceled(e.CancellationToken); + throw; + } - return sendingTask.Task.Task; + await sendingTask.Task.Task; } + private Task? disconnectTask; + /// /// Disconnects the client from the server. /// /// The reason the client disconnected. Only used for the event. /// - public async Task Disconnect(Chat? reason = null) + public EnsureOnlyRunOnceAsyncResult Disconnect(Chat? reason = null) + { + return ConcurrencyHelper.EnsureOnlyRunOnceAsync(() => DisconnectInternal(reason), ref disconnectTask); + } + + private async Task DisconnectInternal(Chat? reason = null) { reason ??= new TranslatableComponent("disconnect.quitting"); - - Logger.Info($"Disconnecting: {reason.GetMessage(this.Data)}"); - if (!gameJoinedTsc.Task.IsCompleted) - { - gameJoinedTsc.SetException(new DisconnectedException("Client has been disconnected", reason.GetMessage(this.Data))); - } + var message = reason.GetMessage(Data); + Logger.Info($"Disconnecting: {message}"); + + GameJoinedTcs.TrySetException(new DisconnectedException("Client has been disconnected", message)); if (client is null || !client.Connected) { Logger.Warn("Disconnect() was called but client is not connected"); } - cancellationTokenSource.Cancel(); + await cancellationTokenSource.CancelAsync(); await (streamLoop ?? Task.CompletedTask); client?.Close(); await OnDisconnected.Dispatch(this, reason); + await OnConnectionLost.Dispatch(this); + } + + /// + /// Represents a registration for a packet handler that will be called whenever a packet of type is received. + /// This registration can be used to unregister the handler. + /// + /// The type of the packet. + public sealed class OnPacketRegistration : AbstractPacketReceiveRegistration + where T : IPacket + { + internal OnPacketRegistration(MinecraftClient client, AsyncPacketHandler handler) + : base(client, handler) + { + } + + /// + protected override void Unregister() + { + var key = T.StaticType; + if (Client.packetHandlers.TryGetValue(key, out var handlers)) + { + handlers.TryRemove(Handler); + } + } } /// @@ -220,17 +354,79 @@ public async Task Disconnect(Chat? reason = null) /// /// A delegate that will be called when a packet of type T is received /// The type of the packet - public void On(AsyncPacketHandler handler) where T : IPacket + /// A registration object that can be used to unregister the handler. + public OnPacketRegistration? On(AsyncPacketHandler handler) where T : IPacket { - var key = PacketPalette.GetPacketType(); + var key = T.StaticType; + AsyncPacketHandler rawHandler = packet => handler((T)packet); + var added = packetHandlers.GetOrAdd(key, _ => new ConcurrentHashSet()) + .Add(rawHandler); + return added ? new(this, rawHandler) : null; + } - if (!packetHandlers.TryGetValue(key, out var handlers)) + /// + /// Waits until a packet of the specified type is received and matches the given condition. + /// + /// The type of the packet. + /// A function that evaluates the packet and returns true if the condition is met. + /// A token to cancel the wait for the matching packet. + /// A task that completes once a packet matching the condition is received. + public Task WaitForPacketWhere(Func> condition, CancellationToken cancellationToken = default) + where TPacket : IPacket + { + // linked token is required to cancel the task when the client is disconnected + var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, CancellationToken); + var token = cts.Token; + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + async Task PacketHandler(TPacket packet) { - handlers = new List(); - packetHandlers.Add(key, handlers); + try + { + if (tcs.Task.IsCompleted) + { + return; + } + if (await condition(packet).WaitAsync(token)) + { + tcs.TrySetResult(packet); + } + } + catch (OperationCanceledException e) + { + tcs.TrySetCanceled(e.CancellationToken); + } + catch (Exception e) + { + tcs.TrySetException(e); + } + } + var packetRegistration = On(PacketHandler); + if (packetRegistration == null) + { + // TODO: Can this occur? + cts.Dispose(); + throw new InvalidOperationException("Could not register packet handler"); } + // this registration is required because otherwise the task will only get cancelled when the next packet of that ype is received + var cancellationRegistration = token.Register(() => + { + // cancelling the tcs will later dispose the other stuff + tcs.TrySetCanceled(token); + }); + tcs.Task.ContinueWith(_ => + { + cancellationRegistration.Dispose(); + packetRegistration.Dispose(); + cts.Dispose(); + }, TaskContinuationOptions.ExecuteSynchronously); + return tcs.Task; + } - handlers.Add(p => handler((T)p)); + /// + public Task WaitForPacketWhere(Func condition, CancellationToken cancellationToken = default) + where TPacket : IPacket + { + return WaitForPacketWhere(packet => Task.FromResult(condition(packet)), cancellationToken); } /// @@ -240,195 +436,265 @@ public void On(AsyncPacketHandler handler) where T : IPacket /// A task that completes once the packet is received public Task WaitForPacket() where T : IPacket { - var packetType = PacketPalette.GetPacketType(); - if (!packetWaiters.TryGetValue(packetType, out var task)) + var packetType = T.StaticType; + var tcs = packetWaiters.GetOrAdd(packetType, _ => new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously)); + return tcs.Task.ContinueWith(prev => (T)prev.Result); + } + + /// + /// Represents a registration for a packet handler that will be called whenever any packet is received. + /// This registration can be used to unregister the handler. + /// + public sealed class OnPacketReceivedRegistration : AbstractPacketReceiveRegistration + { + internal OnPacketReceivedRegistration(MinecraftClient client, AsyncPacketHandler handler) + : base(client, handler) { - var tsc = new TaskCompletionSource(); - packetWaiters.Add(packetType, tsc); + } - return tsc.Task.ContinueWith(prev => (T)prev.Result); + /// + protected override void Unregister() + { + Client.packetReceivers.TryRemove(Handler); } + } - return task.Task.ContinueWith(prev => (T)prev.Result); + /// + /// Registers a handler that will be called whenever a packet is received. + /// CAUTION: This will parse all packets, even if they are not awaited by any other handler. + /// This can lead to performance issues if the server sends a lot of packets. + /// Use this for debugging purposes only. + /// + /// A delegate that will be called when a packet is received. + /// A registration object that can be used to unregister the handler. + public OnPacketReceivedRegistration? OnPacketReceived(AsyncPacketHandler handler) + { + var added = packetReceivers.Add(handler); + return added ? new(this, handler) : null; } /// - /// Waits until the client jumps into the Play + /// Waits until the client jumps into the state. /// /// public Task WaitForGame() { - return gameJoinedTsc.Task; + return GameJoinedTcs.Task; } - internal void UpdateGameState(GameState next) + internal Task SendClientInformationPacket(GameState gameState) { - lock (streamLock) - { - gameState = next; - - internalPacketHandler = next switch - { - GameState.Handshaking => new HandshakePacketHandler(this), - GameState.Login => new LoginPacketHandler(this, Data), - GameState.Status => new StatusPacketHandler(this), - GameState.Configuration => new ConfigurationPacketHandler(this, Data), - GameState.Play => new PlayPacketHandler(this, Data), - _ => throw new UnreachableException() - }; - } - - if (next == GameState.Play && !gameJoinedTsc.Task.IsCompleted) + IPacket packet = gameState switch { - if (Data.Version.Protocol <= ProtocolVersion.V_1_20) - { - Task.Delay(10) - .ContinueWith(x => - SendPacket(new Packets.Serverbound.Play.ClientInformationPacket( - Settings.Locale, - Settings.ViewDistance, - (int)Settings.ChatMode, - Settings.ColoredChat, - Settings.DisplayedSkinParts, - (int)Settings.MainHand, - Settings.EnableTextFiltering, - Settings.AllowServerListings))); - } - gameJoinedTsc.TrySetResult(); - } + GameState.Configuration => new ConfigurationClientInformationPacket( + Settings.Locale, + Settings.ViewDistance, + Settings.ChatMode, + Settings.ColoredChat, + Settings.DisplayedSkinParts, + Settings.MainHand, + Settings.EnableTextFiltering, + Settings.AllowServerListings), + GameState.Play => new PlayClientInformationPacket( + Settings.Locale, + Settings.ViewDistance, + Settings.ChatMode, + Settings.ColoredChat, + Settings.DisplayedSkinParts, + Settings.MainHand, + Settings.EnableTextFiltering, + Settings.AllowServerListings), + _ => throw new NotImplementedException(), + }; + return SendPacket(packet); + } - if (next == GameState.Configuration) + internal async Task ChangeGameState(GameState next) + { + GameStatePacketHandler newGameStatePacketHandler = next switch { - SendPacket(new ClientInformationPacket( - Settings.Locale, - Settings.ViewDistance, - (int)Settings.ChatMode, - Settings.ColoredChat, - Settings.DisplayedSkinParts, - (int)Settings.MainHand, - Settings.EnableTextFiltering, - Settings.AllowServerListings)); - } + GameState.Handshaking => new HandshakePacketHandler(this), + GameState.Login => new LoginPacketHandler(this, Data), + GameState.Status => new StatusPacketHandler(this), + GameState.Configuration => new ConfigurationPacketHandler(this, Data), + GameState.Play => new PlayPacketHandler(this, Data), + _ => throw new UnreachableException() + }; + gameStatePacketHandler = newGameStatePacketHandler; + await newGameStatePacketHandler.StateEntered(); } internal void EnableEncryption(byte[] key) { - lock (streamLock) - { - stream!.EnableEncryption(key); - } + stream!.EnableEncryption(key); } internal void SetCompression(int threshold) { - lock (streamLock) + stream!.SetCompression(threshold); + } + + internal void HandleBundleDelimiter() + { + var bundledPackets = Interlocked.Exchange(ref this.bundledPackets, null); + if (bundledPackets != null) { - stream!.SetCompression(threshold); + _ = Task.Run(() => ProcessBundledPackets(bundledPackets), CancellationToken); + } + else + { + if (Interlocked.CompareExchange(ref this.bundledPackets, new(), null) != null) + { + Logger.Warn("Bundling could not be enabled because it was already enabled. This is a race condition."); + } } } - internal void HandleBundleDelimiter() + private async Task ProcessBundledPackets(ConcurrentQueue<(PacketType, PacketBuffer)> packets) { - bundlePackets = !bundlePackets; - if (!bundlePackets) + Logger.Trace($"Processing {packets.Count} bundled packets"); + try { - Logger.Debug("Processing bundled packets"); - var tasks = bundledPackets.Select( - p => HandleIncomingPacket(p.Item1, p.Item2)) - .ToArray(); + // wiki.vg: the client is guaranteed to process every packet in the bundle on the same tick + // we don't guarantee that. + // TODO: process bundled packets within a single tick + var tasks = packets.Select( + p => HandleIncomingPacket(p.Item1, p.Item2)) + .ToArray(); - Task.WaitAll(tasks); + // no clearing required the queue will no longer be used and will get GCed + // bundledPackets.Clear(); - var errors = tasks.Where(x => x.Exception != null); - foreach (var error in errors) + await Task.WhenAll(tasks); + + foreach (var faultedTask in tasks.Where(task => task.Status == TaskStatus.Faulted)) { - Logger.Error("Error handling bundled packet: {e}", error); + Logger.Error(faultedTask.Exception, "Error handling bundled packet."); } - - bundledPackets.Clear(); } - else + catch (Exception e) { - Logger.Debug("Bundling packets!"); + Logger.Error(e, "Error handling bundled packets."); } } - + private async Task StreamLoop() { - while (!cancellationTokenSource.Token.IsCancellationRequested) + try { - try - { - await ReceivePackets(); - await SendPackets(); - - await Task.Delay(1); - } - catch (Exception ex) + // run both tasks in parallel + // because the task factory does not unwrap the tasks (like Task.Run) we need to do it manually + var receiveTask = Task.Factory.StartNew(ReceivePackets, TaskCreationOptions.LongRunning).Unwrap(); + var sendTask = Task.Factory.StartNew(SendPackets, TaskCreationOptions.LongRunning).Unwrap(); + + // extract the exception from the task that finished first + await await Task.WhenAny(receiveTask, sendTask); + // DisposeInternal in the catch block will then stop the other task + } + catch (Exception e) + { + // EndOfStreamException is expected when the connection is closed + if (e is not EndOfStreamException) { - Logger.Error(ex, "Encountered error in stream loop"); + Logger.Error(e, "Encountered exception in outer stream loop. Connection will be terminated."); } + await DisposeInternal(true); } } private async Task ReceivePackets() { - while (client!.Available > 0 && !cancellationTokenSource.IsCancellationRequested) + try { - PacketBuffer buffer; - lock (streamLock) + while (true) { - buffer = stream!.ReadPacket(); - } - - var packetId = buffer.ReadVarInt(); - var packetType = Data.Protocol.GetPacketType(PacketFlow.Clientbound, gameState, packetId); + CancellationToken.ThrowIfCancellationRequested(); - if (gameState == GameState.Login) - { - await HandleIncomingPacket(packetType, buffer); - } - else - { - if (bundlePackets) + var buffer = stream!.ReadPacket(); + + var packetId = buffer.ReadVarInt(); + var gameState = gameStatePacketHandler.GameState; + var packetType = Data.Protocol.GetPacketType(PacketFlow.Clientbound, gameState, packetId); + + Logger.Trace("Received packet {PacketType}. GameState = {GameState}, PacketId = {PacketId}", packetType, gameState, packetId); + + // handle BundleDelimiter packet here, because there is a race condition where some + // packets may be read before HandleBundleDelimiter is invoked through a handler + if (packetType == PacketType.CB_Play_BundleDelimiter) { - bundledPackets.Enqueue((packetType, buffer)); + HandleBundleDelimiter(); + continue; + } + + if (gameState != GameState.Play) + { + await HandleIncomingPacket(packetType, buffer); } else { - _ = Task.Run(() => HandleIncomingPacket(packetType, buffer)); + var bundledPackets = this.bundledPackets; + if (bundledPackets != null) + { + bundledPackets.Enqueue((packetType, buffer)); + } + else + { + // handle the packet in a new task to prevent blocking the stream loop + _ = Task.Run(() => HandleIncomingPacket(packetType, buffer)); + } } } - - if (gameState != GameState.Play) - { - await Task.Delay(1); - } } + catch (Exception e) + { + Logger.Debug(e, "ReceivePackets loop ended with exception."); + throw; + } + // can never exit without exception because infinite loop without break } - private Task SendPackets() + private async Task SendPackets() { - if (!packetQueue.TryDequeue(out var task)) + try { - return Task.CompletedTask; - } + await foreach (var task in packetQueue.ReceiveAllAsync()) + { + if (task.Token.IsCancellationRequested) + { + task.Task.TrySetCanceled(); + continue; + } - if (task.Token is { IsCancellationRequested: true }) - { - return Task.CompletedTask; + try + { + DispatchPacket(task.Packet); + task.Task.TrySetResult(); + } + catch (OperationCanceledException e) + { + task.Task.TrySetCanceled(e.CancellationToken); + // we should stop. So we do by rethrowing the exception + throw; + } + catch (Exception e) + { + Logger.Error(e, "Encountered exception while dispatching packet {PacketType}", task.Packet.Type); + task.Task.TrySetException(e); + if (e is SocketException) + { + // break the loop to prevent further packets from being sent + // because the connection is probably dead + throw; + } + } + } } - - DispatchPacket(task.Packet); - - - _ = Task.Run(async () => + catch (Exception e) { - task.Task.TrySetResult(); - await HandleOutgoingPacket(task.Packet); - }); - - return Task.CompletedTask; + Logger.Debug(e, "SendPackets loop ended with exception."); + throw; + } + // can never exit without exception because infinite loop without break (because we never complete the BufferBlock we only cancel it) } private void DispatchPacket(IPacket packet) @@ -439,11 +705,43 @@ private void DispatchPacket(IPacket packet) buffer.WriteVarInt(packetId); packet.Write(buffer, Data); - lock (streamLock) + try { - Logger.Trace("Sending packet {packetType}", packet.Type); + Logger.Trace("Sending packet {PacketType}", packet.Type); stream!.WritePacket(buffer); } + catch (SocketException e) + { + Logger.Error(e, "Encountered exception while dispatching packet {PacketType}", packet.Type); + throw; + } + } + + // TODO: object is bad but IPacket is not allowed as generic type + private async Task ParsePacket(PacketPalette.PacketFactory packetFactory, PacketType packetType, PacketBuffer buffer) + { + var size = buffer.ReadableBytes; + try + { + var packet = packetFactory(buffer, Data); + + var unreadBytes = buffer.ReadableBytes; + if (unreadBytes != 0) + { + Logger.Warn("After reading the packet {PacketType}, the buffer still contains {unreadBytes}/{Size} bytes.", packetType, unreadBytes, size); + } + + return packet; + } + catch (Exception e) + { + Logger.Error(e, "Could not read packet {PacketType}, it was {Size} bytes.", packetType, size); + } + finally + { + await buffer.DisposeAsync(); + } + return null; } private async Task HandleIncomingPacket(PacketType packetType, PacketBuffer buffer) @@ -454,8 +752,8 @@ private async Task HandleIncomingPacket(PacketType packetType, PacketBuffer buff // - MinecraftClient.WaitForPacket() // - MinecraftClient.OnPacketReceived <-- Forces all packets to be parsed // - The internal IPacketHandler - - Logger.Trace("Received packet {packetType}", packetType); + + Logger.Trace("Handling packet {PacketType}", packetType); var factory = PacketPalette.GetFactory(packetType); if (factory == null) { @@ -465,10 +763,10 @@ private async Task HandleIncomingPacket(PacketType packetType, PacketBuffer buff var handlers = new List(); - // Internal packet handler - if (internalPacketHandler.HandlesIncoming(packetType)) + // GameState packet handler + if (gameStatePacketHandler.HandlesIncoming(packetType)) { - handlers.Add(internalPacketHandler.HandleIncoming); + handlers.Add(gameStatePacketHandler.HandleIncoming); } // Custom packet handlers @@ -477,64 +775,80 @@ private async Task HandleIncomingPacket(PacketType packetType, PacketBuffer buff handlers.AddRange(customHandlers); } - packetWaiters.TryGetValue(packetType, out var tsc); + packetWaiters.TryGetValue(packetType, out var tcs); - if (handlers.Count == 0 && tsc == null) + if (handlers.Count == 0 && tcs == null && packetReceivers.IsEmpty) { await buffer.DisposeAsync(); return; } - var size = buffer.ReadableBytes; - try + var packet = (IPacket?)await ParsePacket(factory, packetType, buffer); + + if (packet == null) { - var packet = factory(buffer, Data); - await buffer.DisposeAsync(); + // The packet could not be parsed + return; + } - tsc?.TrySetResult(packet); - var tasks = handlers - .Select(task => task(packet)) - .ToArray(); + tcs?.TrySetResult(packet); - try + var packetHandlersTasks = new List(); + try + { + // Run all handlers in parallel: + foreach (var packetReceiver in packetReceivers) { - await Task.WhenAll(tasks); + // The synchronous part of the handlers might throw an exception + // So we also do this in a try-catch block + try + { + packetHandlersTasks.Add(packetReceiver(packet)); + } + catch (Exception e) + { + Logger.Warn(e, "An packet receiver threw an exception when receiving a packet of type {PacketType}.", packetType); + } } - catch (Exception) + + foreach (var handler in handlers) { - foreach (var exception in tasks.Where(x => x.Exception != null)) + // The synchronous part of the handlers might throw an exception + // So we also do this in a try-catch block + try + { + packetHandlersTasks.Add(handler(packet)); + } + catch (Exception e) { - Logger.Warn($"Error in custom packet handling: {exception.Exception}"); + Logger.Warn(e, "An packet handler for packet of type {PacketType} threw an exception.", packetType); } } - } - catch (EndOfStreamException e) - { - Logger.Error(e, "Could not read packet {PacketType}, it was {Size} bytes.", packetType, size); - } - } - private async Task HandleOutgoingPacket(IPacket packet) - { - try - { - await internalPacketHandler.HandleOutgoing(packet); + await Task.WhenAll(packetHandlersTasks); } - catch (Exception e) + catch (Exception) { - Logger.Error(e, $"error in outgoing packet handler. Packet={packet.Type}"); + foreach (var faultedTask in packetHandlersTasks.Where(task => task.Status == TaskStatus.Faulted)) + { + Logger.Warn(faultedTask.Exception, "An packet handler for packet of type {PacketType} threw an exception.", packetType); + } } } /// /// Requests the server status and closes the connection. - /// Works only when is . + /// Works only when is . /// + /// The hostname or IP of the server. + /// The port of the server. + /// The time in milliseconds to wait for a response. + /// The factory to create the TCP connection. /// public static async Task RequestServerStatus( string hostnameOrIp, ushort port = 25565, - int timeout = 10000, + int responseTimeout = 10000, IConnectionFactory? tcpFactory = null) { var latest = await MinecraftData.FromVersion(LATEST_SUPPORTED_VERSION); @@ -552,30 +866,34 @@ public static async Task RequestServerStatus( throw new MineSharpHostException("failed to connect to server"); } - var timeoutCancellation = new CancellationTokenSource(); - var taskCompletionSource = new TaskCompletionSource(); + using var responseTimeoutCts = new CancellationTokenSource(); + var responseTimeoutCancellationToken = responseTimeoutCts.Token; - client.On(async packet => - { - var json = packet.Response; - var response = ServerStatus.FromJToken(JToken.Parse(json), client.Data); - taskCompletionSource.TrySetResult(response); + var statusResponsePacketTask = client.WaitForPacket(); - // the server closes the connection - // after sending the StatusResponsePacket - await client.Disconnect(); - client.Dispose(); - }); + await client.SendPacket(new StatusRequestPacket(), responseTimeoutCancellationToken); + await client.SendPacket(new PingRequestPacket(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()), responseTimeoutCancellationToken); - await client.SendPacket(new StatusRequestPacket(), timeoutCancellation.Token); - await client.SendPacket(new PingRequestPacket(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()), timeoutCancellation.Token); + responseTimeoutCts.CancelAfter(responseTimeout); - timeoutCancellation.Token.Register( - () => taskCompletionSource.TrySetCanceled(timeoutCancellation.Token)); + var statusResponsePacket = await statusResponsePacketTask.WaitAsync(responseTimeoutCancellationToken); + var json = statusResponsePacket.Response; + var response = ServerStatus.FromJToken(JToken.Parse(json), client.Data); - timeoutCancellation.CancelAfter(timeout); + // the server closes the connection + // after sending the StatusResponsePacket and PingResponsePacket + // so just dispose the client (no point in disconnecting) + try + { + await client.DisposeAsync(); + } + catch (Exception) + { + // ignore all errors + // in most cases the exception is an OperationCanceledException because the connection was terminated + } - return await taskCompletionSource.Task; + return response; } /// @@ -592,5 +910,5 @@ public static async Task AutodetectServerVersion(string hostname, return await MinecraftData.FromVersion(status.Version); } - private record PacketSendTask(IPacket Packet, CancellationToken? Token, TaskCompletionSource Task); + private record PacketSendTask(IPacket Packet, CancellationToken Token, TaskCompletionSource Task); } diff --git a/Components/MineSharp.Protocol/MinecraftStream.cs b/Components/MineSharp.Protocol/MinecraftStream.cs index 26e1910f..0cc7ff36 100644 --- a/Components/MineSharp.Protocol/MinecraftStream.cs +++ b/Components/MineSharp.Protocol/MinecraftStream.cs @@ -1,6 +1,6 @@ using System.Net.Sockets; using ICSharpCode.SharpZipLib.Zip.Compression; -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Protocol.Cryptography; using NLog; @@ -9,19 +9,25 @@ namespace MineSharp.Protocol; /// /// Handles reading and writing packets. /// Also handles encryption and compression. -/// This class is not thread-safe. +/// This class is thread-safe. /// internal class MinecraftStream { private const int CompressionDisabled = -1; private static readonly ILogger Logger = LogManager.GetCurrentClassLogger(); - private readonly Deflater deflater = new(); - private readonly Inflater inflater = new(); private readonly NetworkStream networkStream; private readonly int protocolVersion; + + // Always acquire the readLock before the writeLock if both are needed + private readonly object readLock = new(); + private readonly object writeLock = new(); + + private readonly ThreadLocal inflater = new(() => new()); + private readonly ThreadLocal deflater = new(() => new()); + private int compressionThreshold; private AesStream? encryptionStream; @@ -32,7 +38,7 @@ public MinecraftStream(NetworkStream networkStream, int protocolVersion) this.protocolVersion = protocolVersion; this.networkStream = networkStream; stream = this.networkStream; - + compressionThreshold = CompressionDisabled; } @@ -40,7 +46,7 @@ public void EnableEncryption(byte[] sharedSecret) { Logger.Debug("Enabling encryption."); encryptionStream = new(networkStream, sharedSecret); - stream = encryptionStream; + stream = encryptionStream; } public void SetCompression(int threshold) @@ -50,27 +56,40 @@ public void SetCompression(int threshold) public PacketBuffer ReadPacket() { - var uncompressedLength = 0; - var length = ReadVarInt(out _); + var localCompressionThreshold = compressionThreshold; - if (compressionThreshold != CompressionDisabled) + var uncompressedLength = 0; + byte[] data = Array.Empty(); + lock (readLock) { - uncompressedLength = ReadVarInt(out var r); - length -= r; - } + var length = PacketBuffer.ReadVarInt(stream, out _); - var data = new byte[length]; - - var read = 0; - while (read < length) - { - read += stream.Read(data, read, length - read); + if (localCompressionThreshold != CompressionDisabled) + { + uncompressedLength = PacketBuffer.ReadVarInt(stream, out var r); + length -= r; + } + + data = new byte[length]; + + var readRemaining = length; + var readStart = 0; + while (readRemaining > 0) + { + var read = stream.Read(data, readStart, readRemaining); + if (read == 0) + { + throw new EndOfStreamException(); + } + readStart += read; + readRemaining -= read; + } } - + var packetBuffer = uncompressedLength switch { > 0 => DecompressBuffer(data, uncompressedLength), - _ => new(data, protocolVersion) + _ => new(data, protocolVersion) }; return packetBuffer; @@ -78,13 +97,17 @@ public PacketBuffer ReadPacket() public void WritePacket(PacketBuffer buffer) { - if (compressionThreshold > 0) + var localCompressionThreshold = compressionThreshold; + if (localCompressionThreshold > 0) { - buffer = CompressBuffer(buffer); + buffer = CompressBuffer(buffer, localCompressionThreshold); + } + + lock (writeLock) + { + PacketBuffer.WriteVarInt(stream, (int)buffer.Size); + stream.Write(buffer.GetBuffer()); } - - WriteVarInt((int)buffer.Size); - stream.Write(buffer.GetBuffer().AsSpan()); } private PacketBuffer DecompressBuffer(byte[] buffer, int length) @@ -95,86 +118,73 @@ private PacketBuffer DecompressBuffer(byte[] buffer, int length) } var buffer2 = new byte[length]; - inflater.SetInput(buffer); - inflater.Inflate(buffer2); - inflater.Reset(); + var localInflater = inflater.Value!; + localInflater.SetInput(buffer); + localInflater.Inflate(buffer2); + localInflater.Reset(); return new(buffer2, protocolVersion); } - private PacketBuffer CompressBuffer(PacketBuffer input) + // compressionThreshold is given as a parameter to make it thread-safe + private PacketBuffer CompressBuffer(PacketBuffer input, int compressionThreshold) { var output = new PacketBuffer(protocolVersion); + var buffer = input.GetBuffer(); + if (input.Size < compressionThreshold) { output.WriteVarInt(0); - output.WriteBytes(input.GetBuffer().AsSpan()); + output.WriteBytes(buffer); return output; } - var buffer = input.GetBuffer(); output.WriteVarInt(buffer.Length); - deflater.SetInput(buffer); - deflater.Finish(); + var localDeflater = deflater.Value!; + localDeflater.SetInput(buffer); + localDeflater.Finish(); var deflateBuf = new byte[8192]; - while (!deflater.IsFinished) + while (!localDeflater.IsFinished) { - var j = deflater.Deflate(deflateBuf); + var j = localDeflater.Deflate(deflateBuf); output.WriteBytes(deflateBuf.AsSpan(0, j)); } - deflater.Reset(); + localDeflater.Reset(); return output; } - private int ReadVarInt(out int read) + /// + /// This method checks if the stream is still connected. + /// This is method can be useful when you want to determine whether the stream is dead without reading or writing to it. + /// This is because the stream does not throw an exception when it is dead until you read or write to it. + /// + public bool CheckStreamUseable() { - var value = 0; - var length = 0; - byte currentByte; + var r = networkStream.Socket.Poll(0, SelectMode.SelectRead); - while (true) + if (r && !networkStream.DataAvailable) { - currentByte = (byte)stream.ReadByte(); - value |= (currentByte & 0x7F) << (length * 7); - - length++; - if (length > 5) - { - throw new("VarInt too big"); - } - - if ((currentByte & 0x80) != 0x80) - { - break; - } + // the socket got closed + // we would only get the exception next time we read or write to it + // so we do that now to get the exception + // we can not do this always because doing so would change the stream or block + var buffer = Array.Empty(); + networkStream.Socket.Receive(buffer, SocketFlags.Peek); // this will throw an exception + return false; } - - read = length; - return value; + return networkStream.Socket.Connected; } - private void WriteVarInt(int value) + public void Close() { - while (true) - { - if ((value & ~0x7F) == 0) + lock (readLock) + lock (writeLock) { - stream.WriteByte((byte)value); - return; + networkStream.Close(); + encryptionStream?.Close(); } - - stream.WriteByte((byte)((value & 0x7F) | 0x80)); - value >>= 7; - } - } - - - public void Close() - { - networkStream.Close(); - encryptionStream?.Close(); } } diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/AddResourcePackPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/AddResourcePackPacket.cs new file mode 100644 index 00000000..214df5d2 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/AddResourcePackPacket.cs @@ -0,0 +1,57 @@ +using MineSharp.ChatComponent; +using MineSharp.Core.Common; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Configuration; + +/// +/// Add Resource Pack packet (configuration) +/// +/// The unique identifier of the resource pack. +/// The URL to the resource pack. +/// A 40 character hexadecimal, case-insensitive SHA-1 hash of the resource pack file. +/// Whether the client is forced to use the resource pack. +/// Whether a custom message should be used on the resource pack prompt. +/// The custom message shown in the prompt, if present. +public sealed record AddResourcePackPacket(Uuid Uuid, string Url, string Hash, bool Forced, bool HasPromptMessage, Chat? PromptMessage) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Configuration_AddResourcePack; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteUuid(Uuid); + buffer.WriteString(Url); + buffer.WriteString(Hash); + buffer.WriteBool(Forced); + buffer.WriteBool(HasPromptMessage); + if (HasPromptMessage) + { + buffer.WriteChatComponent(PromptMessage!); + } + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var uuid = buffer.ReadUuid(); + var url = buffer.ReadString(); + var hash = buffer.ReadString(); + var forced = buffer.ReadBool(); + var hasPromptMessage = buffer.ReadBool(); + var promptMessage = hasPromptMessage ? buffer.ReadChatComponent() : null; + + return new AddResourcePackPacket( + uuid, + url, + hash, + forced, + hasPromptMessage, + promptMessage); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/DisconnectPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/DisconnectPacket.cs index b4208fe8..1cf4255d 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/DisconnectPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/DisconnectPacket.cs @@ -1,5 +1,5 @@ using MineSharp.ChatComponent; -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; @@ -9,15 +9,13 @@ namespace MineSharp.Protocol.Packets.Clientbound.Configuration; /// Configuration Disconnect packet /// See https://wiki.vg/Protocol#Disconnect_.28configuration.29 /// -public class DisconnectPacket : IPacket +/// Reason for disconnect +public sealed record DisconnectPacket(Chat Reason) : IPacket { /// - public PacketType Type => PacketType.CB_Configuration_Disconnect; - - /// - /// Reason for disconnect - /// - public required Chat Reason { get; init; } + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Configuration_Disconnect; /// public void Write(PacketBuffer buffer, MinecraftData version) @@ -28,6 +26,6 @@ public void Write(PacketBuffer buffer, MinecraftData version) /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { - return new DisconnectPacket { Reason = buffer.ReadChatComponent() }; + return new DisconnectPacket(buffer.ReadChatComponent()); } } diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/FeatureFlagsPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/FeatureFlagsPacket.cs index aa3e8914..e706cae6 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/FeatureFlagsPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/FeatureFlagsPacket.cs @@ -1,4 +1,5 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; @@ -8,26 +9,23 @@ namespace MineSharp.Protocol.Packets.Clientbound.Configuration; /// Feature flags packet /// See https://wiki.vg/Protocol#Feature_Flags /// -public class FeatureFlagsPacket : IPacket +/// The enabled feature flags +public sealed record FeatureFlagsPacket(Identifier[] FeatureFlags) : IPacket { /// - public PacketType Type => PacketType.CB_Configuration_FeatureFlags; - - /// - /// The enabled feature flags - /// - public required string[] FeatureFlags { get; init; } + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Configuration_FeatureFlags; /// public void Write(PacketBuffer buffer, MinecraftData version) { - buffer.WriteVarIntArray(FeatureFlags, (buff, str) => buff.WriteString(str)); + buffer.WriteVarIntArray(FeatureFlags, (buff, str) => buff.WriteIdentifier(str)); } /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { - return new FeatureFlagsPacket { FeatureFlags = buffer.ReadVarIntArray(buff => buff.ReadString()) }; + return new FeatureFlagsPacket(buffer.ReadVarIntArray(buff => buff.ReadIdentifier())); } } -#pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/FinishConfigurationPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/FinishConfigurationPacket.cs index fb2fe8df..feb6472f 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/FinishConfigurationPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/FinishConfigurationPacket.cs @@ -1,4 +1,4 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; @@ -8,10 +8,12 @@ namespace MineSharp.Protocol.Packets.Clientbound.Configuration; /// Finish configuration packet /// See https://wiki.vg/Protocol#Finish_Configuration /// -public class FinishConfigurationPacket : IPacket +public sealed record FinishConfigurationPacket : IPacket { /// - public PacketType Type => PacketType.CB_Configuration_FinishConfiguration; + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Configuration_FinishConfiguration; /// public void Write(PacketBuffer buffer, MinecraftData version) diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/KeepAlivePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/KeepAlivePacket.cs index 6acf9b59..2604ab43 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/KeepAlivePacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/KeepAlivePacket.cs @@ -1,4 +1,4 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; @@ -8,15 +8,13 @@ namespace MineSharp.Protocol.Packets.Clientbound.Configuration; /// Keep alive packet in Configuration /// See https://wiki.vg/Protocol#Clientbound_Keep_Alive_.28configuration.29 /// -public class KeepAlivePacket : IPacket +/// The keep alive id +public sealed record KeepAlivePacket(long KeepAliveId) : IPacket { /// - public PacketType Type => PacketType.CB_Configuration_KeepAlive; - - /// - /// The keep alive id - /// - public required long KeepAliveId { get; init; } + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Configuration_KeepAlive; /// public void Write(PacketBuffer buffer, MinecraftData version) @@ -27,6 +25,6 @@ public void Write(PacketBuffer buffer, MinecraftData version) /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { - return new KeepAlivePacket() { KeepAliveId = buffer.ReadLong() }; + return new KeepAlivePacket(buffer.ReadLong()); } } diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/PingPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/PingPacket.cs index 20afca38..aca1d853 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/PingPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/PingPacket.cs @@ -1,4 +1,4 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; @@ -8,15 +8,13 @@ namespace MineSharp.Protocol.Packets.Clientbound.Configuration; /// Ping packet /// See https://wiki.vg/Protocol#Ping_.28configuration.29 /// -public class PingPacket : IPacket +/// The id of the ping +public sealed record PingPacket(int Id) : IPacket { /// - public PacketType Type => PacketType.CB_Configuration_Ping; - - /// - /// The id of the ping - /// - public required int Id { get; init; } + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Configuration_Ping; /// public void Write(PacketBuffer buffer, MinecraftData version) @@ -27,6 +25,6 @@ public void Write(PacketBuffer buffer, MinecraftData version) /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { - return new PingPacket() { Id = buffer.ReadInt() }; + return new PingPacket(buffer.ReadInt()); } } diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/PluginMessagePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/PluginMessagePacket.cs index 1eaf402a..b2cb67e5 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/PluginMessagePacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/PluginMessagePacket.cs @@ -1,51 +1,35 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Configuration; -#pragma warning disable CS1591 + /// /// Plugin message packet /// -public class PluginMessagePacket : IPacket +/// The name of the channel the data was sent +/// The message data +public sealed record PluginMessagePacket(Identifier Channel, byte[] Data) : IPacket { - /// - /// Create a new instance - /// - /// - /// - public PluginMessagePacket(string channelName, PacketBuffer data) - { - ChannelName = channelName; - Data = data; - } - - /// - /// The name of the channel the data was sent - /// - public string ChannelName { get; set; } - - /// - /// The message data - /// - public PacketBuffer Data { get; set; } - /// - public PacketType Type => PacketType.CB_Configuration_CustomPayload; + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Configuration_CustomPayload; /// public void Write(PacketBuffer buffer, MinecraftData version) { - buffer.WriteString(ChannelName); - buffer.WriteBytes(Data.GetBuffer()); + buffer.WriteIdentifier(Channel); + buffer.WriteBytes(Data); } /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { - var channelName = buffer.ReadString(); - var clone = new PacketBuffer(buffer.ReadBytes((int)buffer.ReadableBytes), version.Version.Protocol); - return new PluginMessagePacket(channelName, clone); + var channel = buffer.ReadIdentifier(); + var data = buffer.RestBuffer(); + return new PluginMessagePacket(channel, data); } } -#pragma warning restore CS1591 + diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/RegistryDataPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/RegistryDataPacket.cs index 920c84ea..65fb189d 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/RegistryDataPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/RegistryDataPacket.cs @@ -1,5 +1,5 @@ using fNbt; -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; @@ -9,25 +9,26 @@ namespace MineSharp.Protocol.Packets.Clientbound.Configuration; /// Registry data packet /// See https://wiki.vg/Protocol#Registry_Data /// -public class RegistryDataPacket : IPacket +/// The registry data +public sealed record RegistryDataPacket(NbtCompound RegistryData) : IPacket { /// - public PacketType Type => PacketType.CB_Configuration_RegistryData; - - /// - /// The registry data - /// - public required NbtCompound RegistryData { get; init; } - + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Configuration_RegistryData; + /// public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteNbt(RegistryData); } - + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { - return new RegistryDataPacket() { RegistryData = buffer.ReadNbtCompound() }; + var registryData = buffer.ReadNbtCompound(); + registryData = registryData.NormalizeRegistryDataTopLevelIdentifiers(); + return new RegistryDataPacket(registryData); } } + diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/RemoveResourcePackPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/RemoveResourcePackPacket.cs new file mode 100644 index 00000000..5e7b9954 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/RemoveResourcePackPacket.cs @@ -0,0 +1,38 @@ +using MineSharp.Core.Common; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Configuration; + +/// +/// Packet sent by the server to remove a resource pack (or all of them). +/// +/// The UUID of the resource pack to be removed. Or all of the resource packs if this value is null. +public sealed record RemoveResourcePackPacket(Uuid? Uuid) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Configuration_RemoveResourcePack; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + var hasUuid = Uuid != null; + buffer.WriteBool(hasUuid); + if (hasUuid) + { + buffer.WriteUuid(Uuid!.Value); + } + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var hasUuid = buffer.ReadBool(); + Uuid? uuid = hasUuid ? buffer.ReadUuid() : null; + + return new RemoveResourcePackPacket(uuid); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/UpdateTagsPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/UpdateTagsPacket.cs new file mode 100644 index 00000000..eb6f00da --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Configuration/UpdateTagsPacket.cs @@ -0,0 +1,32 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using MineSharp.Protocol.Packets.NetworkTypes; + +namespace MineSharp.Protocol.Packets.Clientbound.Configuration; + +/// +/// Update Tags (configuration) packet +/// +/// Array of registries with their tags +public sealed record UpdateTagsPacket(Registry[] Registries) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Configuration_Tags; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarIntArray(Registries, (buffer, registry) => registry.Write(buffer)); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var registries = buffer.ReadVarIntArray(Registry.Read); + + return new UpdateTagsPacket(registries); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Login/DisconnectPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Login/DisconnectPacket.cs index 183f7b8b..4e6ac2a1 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Login/DisconnectPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Login/DisconnectPacket.cs @@ -1,5 +1,7 @@ using MineSharp.ChatComponent; +using MineSharp.ChatComponent.Components; using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; @@ -9,27 +11,27 @@ namespace MineSharp.Protocol.Packets.Clientbound.Login; /// Disconnect packet for login /// See https://wiki.vg/Protocol#Disconnect_.28login.29 /// -public class DisconnectPacket : IPacket +/// The reason for being disconnected +public sealed record DisconnectPacket(Chat Reason) : IPacket { /// - public PacketType Type => PacketType.CB_Login_Disconnect; - - /// - /// The reason for being disconnected - /// - public required Chat Reason { get; init; } - + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Login_Disconnect; + /// public void Write(PacketBuffer buffer, MinecraftData version) { // Disconnect (login) packet is always sent as JSON text component according to wiki.vg buffer.WriteString(Reason.ToJson().ToString()); } - + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { var reason = buffer.ReadString(); - return new DisconnectPacket() { Reason = Chat.Parse(reason) }; + // Disconnect (login) packet is always sent as JSON text component according to wiki.vg + var chat = Chat.Parse(reason); + return new DisconnectPacket(chat); } } diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Login/EncryptionRequestPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Login/EncryptionRequestPacket.cs index 9da13d7b..1ebc4477 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Login/EncryptionRequestPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Login/EncryptionRequestPacket.cs @@ -1,4 +1,4 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; @@ -8,25 +8,15 @@ namespace MineSharp.Protocol.Packets.Clientbound.Login; /// Encryption request packet /// See https://wiki.vg/Protocol#Encryption_Request /// -public class EncryptionRequestPacket : IPacket +/// The hashed server id +/// The public key of the server +/// Verify token +public sealed record EncryptionRequestPacket(string ServerId, byte[] PublicKey, byte[] VerifyToken) : IPacket { /// - public PacketType Type => PacketType.CB_Login_EncryptionBegin; - - /// - /// The hashed server id - /// - public required string ServerId { get; init; } - - /// - /// The public key of the server - /// - public required byte[] PublicKey { get; init; } - - /// - /// Verify token - /// - public required byte[] VerifyToken { get; init; } + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Login_EncryptionBegin; /// public void Write(PacketBuffer buffer, MinecraftData version) @@ -47,11 +37,6 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) var verifyToken = new byte[buffer.ReadVarInt()]; buffer.ReadBytes(verifyToken); - return new EncryptionRequestPacket() - { - ServerId = serverId, - PublicKey = publicKey, - VerifyToken = verifyToken - }; + return new EncryptionRequestPacket(serverId, publicKey, verifyToken); } } diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Login/LoginPluginRequestPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Login/LoginPluginRequestPacket.cs index 5ff2ab1a..f653f89c 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Login/LoginPluginRequestPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Login/LoginPluginRequestPacket.cs @@ -1,50 +1,28 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Login; -#pragma warning disable CS1591 + /// /// Login plugin request packet /// -public class LoginPluginRequestPacket : IPacket +/// The message id +/// The channel identifier +/// The raw message data +public sealed record LoginPluginRequestPacket(int MessageId, Identifier Channel, byte[] Data) : IPacket { - /// - /// Create a new instance - /// - /// - /// - /// - public LoginPluginRequestPacket(int messageId, string channel, byte[] data) - { - MessageId = messageId; - Channel = channel; - Data = data; - } - - /// - /// The message id - /// - public int MessageId { get; set; } - - /// - /// The channel identifier - /// - public string Channel { get; set; } // TODO: Identifier - - /// - /// The raw message data - /// - public byte[] Data { get; set; } - /// - public PacketType Type => PacketType.CB_Login_LoginPluginRequest; + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Login_LoginPluginRequest; /// public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteVarInt(MessageId); - buffer.WriteString(Channel); + buffer.WriteIdentifier(Channel); buffer.WriteBytes(Data.AsSpan()); } @@ -52,10 +30,9 @@ public void Write(PacketBuffer buffer, MinecraftData version) public static IPacket Read(PacketBuffer buffer, MinecraftData version) { var messageId = buffer.ReadVarInt(); - var channel = buffer.ReadString(); + var channel = buffer.ReadIdentifier(); var data = buffer.RestBuffer(); return new LoginPluginRequestPacket(messageId, channel, data); } } -#pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Login/LoginSuccessPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Login/LoginSuccessPacket.cs index 02a64ca2..9cc40318 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Login/LoginSuccessPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Login/LoginSuccessPacket.cs @@ -1,33 +1,25 @@ using MineSharp.Core; using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; using MineSharp.Protocol.Exceptions; +using static MineSharp.Protocol.Packets.Clientbound.Login.LoginSuccessPacket; namespace MineSharp.Protocol.Packets.Clientbound.Login; /// /// Login success packet /// -public class LoginSuccessPacket : IPacket +/// Uuid +/// Username of the client +/// A list of properties sent for versions >= 1.19 +public sealed record LoginSuccessPacket(Uuid Uuid, string Username, Property[]? Properties = null) : IPacket { /// - public PacketType Type => PacketType.CB_Login_Success; - - /// - /// Uuid - /// - public required Uuid Uuid { get; init; } - - /// - /// Username of the client - /// - public required string Username { get; init; } - - /// - /// A list of properties sent for versions >= 1.19 - /// - public Property[]? Properties { get; init; } = null; + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Login_Success; /// public void Write(PacketBuffer buffer, MinecraftData version) @@ -60,34 +52,14 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) properties = buffer.ReadVarIntArray(Property.Read); } - return new LoginSuccessPacket() - { - Uuid = uuid, - Username = username, - Properties = properties - }; + return new LoginSuccessPacket(uuid, username, properties); } /// /// A player property /// - public class Property : ISerializable + public sealed record Property(string Name, string Value, string? Signature) : ISerializable { - /// - /// Name of this property - /// - public required string Name { get; init; } - - /// - /// Value of this property - /// - public required string Value { get; init; } - - /// - /// Signature - /// - public required string? Signature { get; init; } - /// public void Write(PacketBuffer buffer) { @@ -113,7 +85,7 @@ public static Property Read(PacketBuffer buffer) signature = buffer.ReadString(); } - return new() { Name = name, Value = value, Signature = signature }; + return new Property(name, value, signature); } } } diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Login/SetCompressionPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Login/SetCompressionPacket.cs index b1eef2db..13d4310c 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Login/SetCompressionPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Login/SetCompressionPacket.cs @@ -1,4 +1,4 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; @@ -7,16 +7,14 @@ namespace MineSharp.Protocol.Packets.Clientbound.Login; /// /// Set Compression packet /// -public class SetCompressionPacket : IPacket +/// Threshold for when to use compression +public sealed record SetCompressionPacket(int Threshold) : IPacket { /// - public PacketType Type => PacketType.CB_Login_Compress; - - /// - /// Threshold for when to use compression - /// - public required int Threshold { get; init; } - + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Login_Compress; + /// public void Write(PacketBuffer buffer, MinecraftData version) { @@ -27,6 +25,6 @@ public void Write(PacketBuffer buffer, MinecraftData version) public static IPacket Read(PacketBuffer buffer, MinecraftData version) { var threshold = buffer.ReadVarInt(); - return new SetCompressionPacket() { Threshold = threshold }; + return new SetCompressionPacket(threshold); } } diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/AcknowledgeBlockChangePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/AcknowledgeBlockChangePacket.cs index 85104bd5..a3d51fe8 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/AcknowledgeBlockChangePacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/AcknowledgeBlockChangePacket.cs @@ -1,6 +1,6 @@ using MineSharp.Core; -using MineSharp.Core.Common; using MineSharp.Core.Geometry; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; @@ -9,8 +9,21 @@ namespace MineSharp.Protocol.Packets.Clientbound.Play; /// /// Acknowledge block change packet /// -public class AcknowledgeBlockChangePacket : IPacket +public sealed record AcknowledgeBlockChangePacket : IPacket { + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_AcknowledgePlayerDigging; + + // Here is no non-argument constructor allowed + // Do not use +#pragma warning disable CS8618 + private AcknowledgeBlockChangePacket() +#pragma warning restore CS8618 + { + } + /// /// Constructor for version >= 1.19 /// @@ -39,13 +52,10 @@ internal AcknowledgeBlockChangePacket(IPacketBody body) /// /// The body of this packet. - /// Different minecraft versions use different packet bodies. + /// Different Minecraft versions use different packet bodies. /// public IPacketBody Body { get; set; } - /// - public PacketType Type => PacketType.CB_Play_AcknowledgePlayerDigging; - /// public void Write(PacketBuffer buffer, MinecraftData version) { @@ -63,7 +73,6 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) return new AcknowledgeBlockChangePacket(PacketBody119.Read(buffer)); } - /// /// Packet body of /// @@ -86,47 +95,16 @@ public interface IPacketBody /// /// Acknowledge block change packet for < 1.19 /// - public class PacketBody118 : IPacketBody + /// The Position of the block + /// Block state + /// Status of the block change + /// Whether the block change was successful + public sealed record PacketBody118(Position Location, int Block, int Status, bool Successful) : IPacketBody { - /// - /// Create a new instance - /// - /// - /// - /// - /// - public PacketBody118(Position location, int block, int status, bool successful) - { - Location = location; - Block = block; - Status = status; - Successful = successful; - } - - /// - /// The Position of the block - /// - public Position Location { get; set; } - - /// - /// Block state - /// - public int Block { get; set; } - - /// - /// Status of the block change - /// - public int Status { get; set; } - - /// - /// Whether the block change was successful - /// - public bool Successful { get; set; } - /// public void Write(PacketBuffer buffer) { - buffer.WriteULong(Location.ToULong()); + buffer.WritePosition(Location); buffer.WriteVarInt(Block); buffer.WriteVarInt(Status); buffer.WriteBool(Successful); @@ -136,7 +114,7 @@ public void Write(PacketBuffer buffer) public static IPacketBody Read(PacketBuffer buffer) { return new PacketBody118( - new(buffer.ReadULong()), + buffer.ReadPosition(), buffer.ReadVarInt(), buffer.ReadVarInt(), buffer.ReadBool()); @@ -146,22 +124,9 @@ public static IPacketBody Read(PacketBuffer buffer) /// /// Acknowledge block change packet body for versions >= 1.19 /// - public class PacketBody119 : IPacketBody + /// Sequence id used for synchronization + public sealed record PacketBody119(int SequenceId) : IPacketBody { - /// - /// Create a new instance - /// - /// - public PacketBody119(int sequenceId) - { - SequenceId = sequenceId; - } - - /// - /// Sequence id used for synchronization - /// - public int SequenceId { get; set; } - /// public void Write(PacketBuffer buffer) { diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/AddResourcePackPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/AddResourcePackPacket.cs new file mode 100644 index 00000000..d46e930f --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/AddResourcePackPacket.cs @@ -0,0 +1,51 @@ +using MineSharp.ChatComponent; +using MineSharp.Core.Common; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Packet sent by the server to add a resource pack. +/// +/// The unique identifier of the resource pack. +/// The URL to the resource pack. +/// A 40 character hexadecimal, case-insensitive SHA-1 hash of the resource pack file. +/// Whether the client is forced to use the resource pack. +/// The custom message shown in the prompt, if present. +public sealed record AddResourcePackPacket(Uuid Uuid, string Url, string Hash, bool Forced, Chat? PromptMessage) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_AddResourcePack; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteUuid(Uuid); + buffer.WriteString(Url); + buffer.WriteString(Hash); + buffer.WriteBool(Forced); + var hasPromptMessage = PromptMessage is not null; + buffer.WriteBool(hasPromptMessage); + if (hasPromptMessage && PromptMessage is not null) + { + buffer.WriteChatComponent(PromptMessage); + } + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var uuid = buffer.ReadUuid(); + var url = buffer.ReadString(); + var hash = buffer.ReadString(); + var forced = buffer.ReadBool(); + var hasPromptMessage = buffer.ReadBool(); + Chat? promptMessage = hasPromptMessage ? buffer.ReadChatComponent() : null; + + return new AddResourcePackPacket(uuid, url, hash, forced, promptMessage); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/AwardStatisticsPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/AwardStatisticsPacket.cs new file mode 100644 index 00000000..763c0e2b --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/AwardStatisticsPacket.cs @@ -0,0 +1,258 @@ +using System.Collections.Frozen; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Award statistics packet sent as a response to Client Command. +/// +/// Number of elements in the statistics array. +/// Array of statistics. +public sealed record AwardStatisticsPacket(int Count, AwardStatisticsPacket.Statistic[] Statistics) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_Statistics; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(Count); + foreach (var statistic in Statistics) + { + buffer.WriteVarInt((int)statistic.CategoryId); + buffer.WriteVarInt((int)statistic.StatisticId); + buffer.WriteVarInt(statistic.Value); + } + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var count = buffer.ReadVarInt(); + var statistics = new Statistic[count]; + for (int i = 0; i < count; i++) + { + var categoryId = (CategoryId)buffer.ReadVarInt(); + var statisticId = (StatisticId)buffer.ReadVarInt(); + var value = buffer.ReadVarInt(); + statistics[i] = new Statistic(categoryId, statisticId, value); + } + + return new AwardStatisticsPacket(count, statistics); + } + + /// + /// Represents a single statistic entry. + /// + /// The category ID of the statistic. + /// The statistic ID. + /// The value of the statistic. + public sealed record Statistic(CategoryId CategoryId, StatisticId StatisticId, int Value); + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + /// + /// Enum representing different types of statistic IDs. + /// + public enum StatisticId + { + LeaveGame = 0, + PlayOneMinute = 1, + TimeSinceDeath = 2, + TimeSinceRest = 3, + SneakTime = 4, + WalkOneCm = 5, + CrouchOneCm = 6, + SprintOneCm = 7, + WalkOnWaterOneCm = 8, + FallOneCm = 9, + ClimbOneCm = 10, + FlyOneCm = 11, + WalkUnderWaterOneCm = 12, + MinecartOneCm = 13, + BoatOneCm = 14, + PigOneCm = 15, + HorseOneCm = 16, + AviateOneCm = 17, + SwimOneCm = 18, + StriderOneCm = 19, + Jump = 20, + Drop = 21, + DamageDealt = 22, + DamageDealtAbsorbed = 23, + DamageDealtResisted = 24, + DamageTaken = 25, + DamageBlockedByShield = 26, + DamageAbsorbed = 27, + DamageResisted = 28, + Deaths = 29, + MobKills = 30, + AnimalsBred = 31, + PlayerKills = 32, + FishCaught = 33, + TalkedToVillager = 34, + TradedWithVillager = 35, + EatCakeSlice = 36, + FillCauldron = 37, + UseCauldron = 38, + CleanArmor = 39, + CleanBanner = 40, + CleanShulkerBox = 41, + InteractWithBrewingStand = 42, + InteractWithBeacon = 43, + InspectDropper = 44, + InspectHopper = 45, + InspectDispenser = 46, + PlayNoteblock = 47, + TuneNoteblock = 48, + PotFlower = 49, + TriggerTrappedChest = 50, + OpenEnderchest = 51, + EnchantItem = 52, + PlayRecord = 53, + InteractWithFurnace = 54, + InteractWithCraftingTable = 55, + OpenChest = 56, + SleepInBed = 57, + OpenShulkerBox = 58, + OpenBarrel = 59, + InteractWithBlastFurnace = 60, + InteractWithSmoker = 61, + InteractWithLectern = 62, + InteractWithCampfire = 63, + InteractWithCartographyTable = 64, + InteractWithLoom = 65, + InteractWithStonecutter = 66, + BellRing = 67, + RaidTrigger = 68, + RaidWin = 69, + InteractWithAnvil = 70, + InteractWithGrindstone = 71, + TargetHit = 72, + InteractWithSmithingTable = 73 + } + + /// + /// Enum representing different categories of statistics. + /// + public enum CategoryId + { + General = 0, + Blocks = 1, + Items = 2, + Mobs = 3 + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + + /// + /// Enum representing different units for statistics. + /// + public enum Unit + { + /// + /// No unit. + /// Just a normal number (formatted with 0 decimal places). + /// + None, + /// + /// Unit representing damage. + /// Value is 10 times the normal amount. + /// + Damage, + /// + /// Unit representing distance. + /// A distance in centimeters (hundredths of blocks). + /// + Distance, + /// + /// Unit representing time. + /// A time span in ticks + /// + Time + } + + // TODO: Is this data from a registry? Hardcoded for now. + /// + /// Lookup table for statistic ID to name and unit. + /// + public static readonly FrozenDictionary StatisticLookup = new Dictionary() + { + { StatisticId.LeaveGame, ("minecraft.leave_game", Unit.None) }, + { StatisticId.PlayOneMinute, ("minecraft.play_one_minute", Unit.Time) }, + { StatisticId.TimeSinceDeath, ("minecraft.time_since_death", Unit.Time) }, + { StatisticId.TimeSinceRest, ("minecraft.time_since_rest", Unit.Time) }, + { StatisticId.SneakTime, ("minecraft.sneak_time", Unit.Time) }, + { StatisticId.WalkOneCm, ("minecraft.walk_one_cm", Unit.Distance) }, + { StatisticId.CrouchOneCm, ("minecraft.crouch_one_cm", Unit.Distance) }, + { StatisticId.SprintOneCm, ("minecraft.sprint_one_cm", Unit.Distance) }, + { StatisticId.WalkOnWaterOneCm, ("minecraft.walk_on_water_one_cm", Unit.Distance) }, + { StatisticId.FallOneCm, ("minecraft.fall_one_cm", Unit.Distance) }, + { StatisticId.ClimbOneCm, ("minecraft.climb_one_cm", Unit.Distance) }, + { StatisticId.FlyOneCm, ("minecraft.fly_one_cm", Unit.Distance) }, + { StatisticId.WalkUnderWaterOneCm, ("minecraft.walk_under_water_one_cm", Unit.Distance) }, + { StatisticId.MinecartOneCm, ("minecraft.minecart_one_cm", Unit.Distance) }, + { StatisticId.BoatOneCm, ("minecraft.boat_one_cm", Unit.Distance) }, + { StatisticId.PigOneCm, ("minecraft.pig_one_cm", Unit.Distance) }, + { StatisticId.HorseOneCm, ("minecraft.horse_one_cm", Unit.Distance) }, + { StatisticId.AviateOneCm, ("minecraft.aviate_one_cm", Unit.Distance) }, + { StatisticId.SwimOneCm, ("minecraft.swim_one_cm", Unit.Distance) }, + { StatisticId.StriderOneCm, ("minecraft.strider_one_cm", Unit.Distance) }, + { StatisticId.Jump, ("minecraft.jump", Unit.None) }, + { StatisticId.Drop, ("minecraft.drop", Unit.None) }, + { StatisticId.DamageDealt, ("minecraft.damage_dealt", Unit.Damage) }, + { StatisticId.DamageDealtAbsorbed, ("minecraft.damage_dealt_absorbed", Unit.Damage) }, + { StatisticId.DamageDealtResisted, ("minecraft.damage_dealt_resisted", Unit.Damage) }, + { StatisticId.DamageTaken, ("minecraft.damage_taken", Unit.Damage) }, + { StatisticId.DamageBlockedByShield, ("minecraft.damage_blocked_by_shield", Unit.Damage) }, + { StatisticId.DamageAbsorbed, ("minecraft.damage_absorbed", Unit.Damage) }, + { StatisticId.DamageResisted, ("minecraft.damage_resisted", Unit.Damage) }, + { StatisticId.Deaths, ("minecraft.deaths", Unit.None) }, + { StatisticId.MobKills, ("minecraft.mob_kills", Unit.None) }, + { StatisticId.AnimalsBred, ("minecraft.animals_bred", Unit.None) }, + { StatisticId.PlayerKills, ("minecraft.player_kills", Unit.None) }, + { StatisticId.FishCaught, ("minecraft.fish_caught", Unit.None) }, + { StatisticId.TalkedToVillager, ("minecraft.talked_to_villager", Unit.None) }, + { StatisticId.TradedWithVillager, ("minecraft.traded_with_villager", Unit.None) }, + { StatisticId.EatCakeSlice, ("minecraft.eat_cake_slice", Unit.None) }, + { StatisticId.FillCauldron, ("minecraft.fill_cauldron", Unit.None) }, + { StatisticId.UseCauldron, ("minecraft.use_cauldron", Unit.None) }, + { StatisticId.CleanArmor, ("minecraft.clean_armor", Unit.None) }, + { StatisticId.CleanBanner, ("minecraft.clean_banner", Unit.None) }, + { StatisticId.CleanShulkerBox, ("minecraft.clean_shulker_box", Unit.None) }, + { StatisticId.InteractWithBrewingStand, ("minecraft.interact_with_brewingstand", Unit.None) }, + { StatisticId.InteractWithBeacon, ("minecraft.interact_with_beacon", Unit.None) }, + { StatisticId.InspectDropper, ("minecraft.inspect_dropper", Unit.None) }, + { StatisticId.InspectHopper, ("minecraft.inspect_hopper", Unit.None) }, + { StatisticId.InspectDispenser, ("minecraft.inspect_dispenser", Unit.None) }, + { StatisticId.PlayNoteblock, ("minecraft.play_noteblock", Unit.None) }, + { StatisticId.TuneNoteblock, ("minecraft.tune_noteblock", Unit.None) }, + { StatisticId.PotFlower, ("minecraft.pot_flower", Unit.None) }, + { StatisticId.TriggerTrappedChest, ("minecraft.trigger_trapped_chest", Unit.None) }, + { StatisticId.OpenEnderchest, ("minecraft.open_enderchest", Unit.None) }, + { StatisticId.EnchantItem, ("minecraft.enchant_item", Unit.None) }, + { StatisticId.PlayRecord, ("minecraft.play_record", Unit.None) }, + { StatisticId.InteractWithFurnace, ("minecraft.interact_with_furnace", Unit.None) }, + { StatisticId.InteractWithCraftingTable, ("minecraft.interact_with_crafting_table", Unit.None) }, + { StatisticId.OpenChest, ("minecraft.open_chest", Unit.None) }, + { StatisticId.SleepInBed, ("minecraft.sleep_in_bed", Unit.None) }, + { StatisticId.OpenShulkerBox, ("minecraft.open_shulker_box", Unit.None) }, + { StatisticId.OpenBarrel, ("minecraft.open_barrel", Unit.None) }, + { StatisticId.InteractWithBlastFurnace, ("minecraft.interact_with_blast_furnace", Unit.None) }, + { StatisticId.InteractWithSmoker, ("minecraft.interact_with_smoker", Unit.None) }, + { StatisticId.InteractWithLectern, ("minecraft.interact_with_lectern", Unit.None) }, + { StatisticId.InteractWithCampfire, ("minecraft.interact_with_campfire", Unit.None) }, + { StatisticId.InteractWithCartographyTable, ("minecraft.interact_with_cartography_table", Unit.None) }, + { StatisticId.InteractWithLoom, ("minecraft.interact_with_loom", Unit.None) }, + { StatisticId.InteractWithStonecutter, ("minecraft.interact_with_stonecutter", Unit.None) }, + { StatisticId.BellRing, ("minecraft.bell_ring", Unit.None) }, + { StatisticId.RaidTrigger, ("minecraft.raid_trigger", Unit.None) }, + { StatisticId.RaidWin, ("minecraft.raid_win", Unit.None) }, + { StatisticId.InteractWithAnvil, ("minecraft.interact_with_anvil", Unit.None) }, + { StatisticId.InteractWithGrindstone, ("minecraft.interact_with_grindstone", Unit.None) }, + { StatisticId.TargetHit, ("minecraft.target_hit", Unit.None) }, + { StatisticId.InteractWithSmithingTable, ("minecraft.interact_with_smithing_table", Unit.None) } + }.ToFrozenDictionary(); +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/BlockActionPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/BlockActionPacket.cs new file mode 100644 index 00000000..63f0e7a7 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/BlockActionPacket.cs @@ -0,0 +1,42 @@ +using MineSharp.Core.Geometry; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// This packet is used for a number of actions and animations performed by blocks, usually non-persistent. +/// The client ignores the provided block type and instead uses the block state in their world. +/// +/// Block coordinates. +/// Varies depending on block — see Block Actions. +/// Varies depending on block — see Block Actions. +/// The block type ID for the block. This value is unused by the Notchian client, as it will infer the type of block based on the given position. +public sealed record BlockActionPacket(Position Location, byte ActionId, byte ActionParameter, int BlockType) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_BlockAction; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WritePosition(Location); + buffer.WriteByte(ActionId); + buffer.WriteByte(ActionParameter); + buffer.WriteVarInt(BlockType); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var location = buffer.ReadPosition(); + var actionId = buffer.ReadByte(); + var actionParameter = buffer.ReadByte(); + var blockType = buffer.ReadVarInt(); + + return new BlockActionPacket(location, actionId, actionParameter, blockType); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/BlockEntityDataPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/BlockEntityDataPacket.cs new file mode 100644 index 00000000..4998ec82 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/BlockEntityDataPacket.cs @@ -0,0 +1,39 @@ +using fNbt; +using MineSharp.Core.Geometry; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Sets the block entity associated with the block at the given location. +/// +/// The location of the block entity +/// The type of the block entity +/// The NBT data to set +public sealed record BlockEntityDataPacket(Position Location, int BlockEntityType, NbtTag? NbtData) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_TileEntityData; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WritePosition(Location); + buffer.WriteVarInt(BlockEntityType); + buffer.WriteOptionalNbt(NbtData); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var location = buffer.ReadPosition(); + var type = buffer.ReadVarInt(); + var nbtData = buffer.ReadOptionalNbt(); + + return new BlockEntityDataPacket(location, type, nbtData); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/BlockUpdatePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/BlockUpdatePacket.cs index 865fea27..e74a6d31 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/BlockUpdatePacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/BlockUpdatePacket.cs @@ -1,52 +1,34 @@ -using MineSharp.Core.Common; -using MineSharp.Core.Geometry; +using MineSharp.Core.Geometry; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Play; -#pragma warning disable CS1591 + /// /// Block update packet /// -public class BlockUpdatePacket : IPacket +/// The location of the block update +/// The new state id +public sealed record BlockUpdatePacket(Position Location, int StateId) : IPacket { - /// - /// Create a new instance - /// - /// - /// - public BlockUpdatePacket(Position location, int stateId) - { - Location = location; - StateId = stateId; - } - - /// - /// The location of the block update - /// - public Position Location { get; set; } - - /// - /// The new state id - /// - public int StateId { get; set; } - /// - public PacketType Type => PacketType.CB_Play_BlockChange; + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_BlockChange; /// public void Write(PacketBuffer buffer, MinecraftData version) { - buffer.WriteULong(Location.ToULong()); + buffer.WritePosition(Location); buffer.WriteVarInt(StateId); } /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { - var location = new Position(buffer.ReadULong()); + var location = buffer.ReadPosition(); var stateId = buffer.ReadVarInt(); return new BlockUpdatePacket(location, stateId); } } -#pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/BossBarPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/BossBarPacket.cs new file mode 100644 index 00000000..5715115e --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/BossBarPacket.cs @@ -0,0 +1,226 @@ +using System.Collections.Frozen; +using MineSharp.ChatComponent; +using MineSharp.Core.Common; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using static MineSharp.Protocol.Packets.Clientbound.Play.BossBarPacket; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Boss bar packet +/// +/// Unique ID for this bar +/// Determines the layout of the remaining packet +public sealed record BossBarPacket(Uuid Uuid, BossBarAction Action) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_BossBar; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteUuid(Uuid); + buffer.WriteVarInt((int)Action.Type); + Action.Write(buffer); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var uuid = buffer.ReadUuid(); + var actionType = (BossBarActionType)buffer.ReadVarInt(); + var action = BossBarAction.Read(buffer, actionType); + return new BossBarPacket(uuid, action); + } + + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + /// + /// Represents the action of a boss bar packet + /// + public interface IBossBarAction where T : BossBarAction + { + static abstract BossBarActionType StaticType { get; } + void Write(PacketBuffer buffer); + static abstract T Read(PacketBuffer buffer); + } + + public abstract record BossBarAction + { + public abstract BossBarActionType Type { get; } + public abstract void Write(PacketBuffer buffer); + + public static BossBarAction Read(PacketBuffer buffer, BossBarActionType type) + { + if (BossActionReaders.TryGetValue(type, out var reader)) + { + return reader(buffer); + } + throw new ArgumentOutOfRangeException(); + } + + public static readonly FrozenDictionary> BossActionReaders = CreateBossActionReadersLookup(); + + private static FrozenDictionary> CreateBossActionReadersLookup() + { + Dictionary> lookup = new(); + + void Register() where T : BossBarAction, IBossBarAction + { + lookup.Add(T.StaticType, buffer => T.Read(buffer)); + } + + Register(); + Register(); + Register(); + Register(); + Register(); + Register(); + + return lookup.ToFrozenDictionary(); + } + } + + public enum BossBarActionType + { + Add = 0, + Remove = 1, + UpdateHealth = 2, + UpdateTitle = 3, + UpdateStyle = 4, + UpdateFlags = 5 + } + + public enum BossBarColor + { + Pink = 0, + Blue = 1, + Red = 2, + Green = 3, + Yellow = 4, + Purple = 5, + White = 6 + } + + public enum BossBarDivision + { + NoDivision = 0, + SixNotches = 1, + TenNotches = 2, + TwelveNotches = 3, + TwentyNotches = 4 + } + + public sealed record AddBossBarAction(Chat Title, float Health, BossBarColor Color, BossBarDivision Division, byte Flags) : BossBarAction, IBossBarAction + { + public override BossBarActionType Type => StaticType; + public static BossBarActionType StaticType => BossBarActionType.Add; + + public override void Write(PacketBuffer buffer) + { + buffer.WriteChatComponent(Title); + buffer.WriteFloat(Health); + buffer.WriteVarInt((int)Color); + buffer.WriteVarInt((int)Division); + buffer.WriteByte(Flags); + } + + public static AddBossBarAction Read(PacketBuffer buffer) + { + var title = buffer.ReadChatComponent(); + var health = buffer.ReadFloat(); + var color = (BossBarColor)buffer.ReadVarInt(); + var division = (BossBarDivision)buffer.ReadVarInt(); + var flags = buffer.ReadByte(); + return new AddBossBarAction(title, health, color, division, flags); + } + } + + public sealed record RemoveBossBarAction() : BossBarAction(), IBossBarAction + { + public override BossBarActionType Type => StaticType; + public static BossBarActionType StaticType => BossBarActionType.Remove; + + public override void Write(PacketBuffer buffer) { } + + public static RemoveBossBarAction Read(PacketBuffer buffer) + { + return new RemoveBossBarAction(); + } + } + + public sealed record UpdateHealthBossBarAction(float Health) : BossBarAction, IBossBarAction + { + public override BossBarActionType Type => StaticType; + public static BossBarActionType StaticType => BossBarActionType.UpdateHealth; + + public override void Write(PacketBuffer buffer) + { + buffer.WriteFloat(Health); + } + + public static UpdateHealthBossBarAction Read(PacketBuffer buffer) + { + var health = buffer.ReadFloat(); + return new UpdateHealthBossBarAction(health); + } + } + + public sealed record UpdateTitleBossBarAction(Chat Title) : BossBarAction, IBossBarAction + { + public override BossBarActionType Type => StaticType; + public static BossBarActionType StaticType => BossBarActionType.UpdateTitle; + + public override void Write(PacketBuffer buffer) + { + buffer.WriteChatComponent(Title); + } + + public static UpdateTitleBossBarAction Read(PacketBuffer buffer) + { + var title = buffer.ReadChatComponent(); + return new UpdateTitleBossBarAction(title); + } + } + + public sealed record UpdateStyleBossBarAction(BossBarColor Color, BossBarDivision Division) : BossBarAction, IBossBarAction + { + public override BossBarActionType Type => StaticType; + public static BossBarActionType StaticType => BossBarActionType.UpdateStyle; + + public override void Write(PacketBuffer buffer) + { + buffer.WriteVarInt((int)Color); + buffer.WriteVarInt((int)Division); + } + + public static UpdateStyleBossBarAction Read(PacketBuffer buffer) + { + var color = (BossBarColor)buffer.ReadVarInt(); + var division = (BossBarDivision)buffer.ReadVarInt(); + return new UpdateStyleBossBarAction(color, division); + } + } + + public sealed record UpdateFlagsBossBarAction(byte Flags) : BossBarAction, IBossBarAction + { + public override BossBarActionType Type => StaticType; + public static BossBarActionType StaticType => BossBarActionType.UpdateFlags; + + public override void Write(PacketBuffer buffer) + { + buffer.WriteByte(Flags); + } + + public static UpdateFlagsBossBarAction Read(PacketBuffer buffer) + { + var flags = buffer.ReadByte(); + return new UpdateFlagsBossBarAction(flags); + } + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/BundleDelimiterPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/BundleDelimiterPacket.cs index 6a8294ec..9ae9af88 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/BundleDelimiterPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/BundleDelimiterPacket.cs @@ -1,16 +1,18 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Play; -#pragma warning disable CS1591 + /// /// Bundle delimiter packet /// -public class BundleDelimiterPacket : IPacket +public sealed record BundleDelimiterPacket : IPacket { /// - public PacketType Type => PacketType.CB_Play_BundleDelimiter; + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_BundleDelimiter; /// public void Write(PacketBuffer buffer, MinecraftData version) @@ -22,4 +24,3 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) return new BundleDelimiterPacket(); } } -#pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/ChangeDifficultyPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/ChangeDifficultyPacket.cs new file mode 100644 index 00000000..f4c1c491 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/ChangeDifficultyPacket.cs @@ -0,0 +1,35 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using MineSharp.Protocol.Packets.NetworkTypes; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Change difficulty packet +/// +/// The difficulty setting +/// Whether the difficulty is locked +public sealed record ChangeDifficultyPacket(DifficultyLevel Difficulty, bool DifficultyLocked) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_Difficulty; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.Write((byte)Difficulty); + buffer.WriteBool(DifficultyLocked); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var difficulty = (DifficultyLevel)buffer.ReadByte(); + var difficultyLocked = buffer.ReadBool(); + + return new ChangeDifficultyPacket(difficulty, difficultyLocked); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/ChatPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/ChatPacket.cs index 9c99b714..89a41da4 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/ChatPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/ChatPacket.cs @@ -1,45 +1,23 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Play; -#pragma warning disable CS1591 + /// /// ChatPacket only used for versions <= 1.18.2. /// It was replaced by multiple packets in 1.19 /// -public class ChatPacket : IPacket +/// The chat message +/// The position of the chat message +/// The UUID of the message sender +public sealed record ChatPacket(string Message, byte Position, Uuid Sender) : IPacket { - /// - /// Create a new instance - /// - /// - /// - /// - public ChatPacket(string message, byte position, Uuid sender) - { - Message = message; - Position = position; - Sender = sender; - } - - /// - /// The chat message - /// - public string Message { get; set; } - - /// - /// The position of the chat message - /// - public byte Position { get; set; } - - /// - /// The UUID of the message sender - /// - public Uuid Sender { get; set; } - /// - public PacketType Type => PacketType.CB_Play_Chat; + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_Chat; /// public void Write(PacketBuffer buffer, MinecraftData version) @@ -58,4 +36,4 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) buffer.ReadUuid()); } } -#pragma warning restore CS1591 + diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/ChatSuggestionsPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/ChatSuggestionsPacket.cs new file mode 100644 index 00000000..b7cf5930 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/ChatSuggestionsPacket.cs @@ -0,0 +1,50 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using static MineSharp.Protocol.Packets.Clientbound.Play.ChatSuggestionsPacket; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Chat suggestions packet +/// +/// The action to perform +/// Number of elements in the following array +/// Array of chat suggestions +public sealed record ChatSuggestionsPacket(ChatSuggestionAction Action, int Count, string[] Entries) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_ChatSuggestions; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt((int)Action); + buffer.WriteVarInt(Count); + buffer.WriteVarIntArray(Entries, (buf, entry) => buf.WriteString(entry)); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var action = (ChatSuggestionAction)buffer.ReadVarInt(); + var count = buffer.ReadVarInt(); + var entries = buffer.ReadVarIntArray(buf => buf.ReadString()); + + return new ChatSuggestionsPacket(action, count, entries); + } + + /// + /// Enum representing the action for chat suggestions + /// + public enum ChatSuggestionAction + { +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + Add = 0, + Remove = 1, + Set = 2 +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/ChunkBatchFinishedPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/ChunkBatchFinishedPacket.cs index 98fae0ce..1d50d2f2 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/ChunkBatchFinishedPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/ChunkBatchFinishedPacket.cs @@ -1,4 +1,4 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; @@ -8,24 +8,13 @@ namespace MineSharp.Protocol.Packets.Clientbound.Play; /// The ChunkBatchFinished packet, used since 1.20.2. /// https://wiki.vg/Protocol#Chunk_Batch_Finished /// -public class ChunkBatchFinishedPacket : IPacket +/// The batch size +public sealed record ChunkBatchFinishedPacket(int BatchSize) : IPacket { - /// - /// Create a new ChunkBatchFinishedPacket instance - /// - /// - public ChunkBatchFinishedPacket(int batchSize) - { - BatchSize = batchSize; - } - - /// - /// The batch size - /// - public int BatchSize { get; set; } - /// - public PacketType Type => PacketType.CB_Play_ChunkBatchFinished; + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_ChunkBatchFinished; /// public void Write(PacketBuffer buffer, MinecraftData version) @@ -40,3 +29,4 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) buffer.ReadVarInt()); } } + diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/ChunkBatchStartPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/ChunkBatchStartPacket.cs index 020ae573..476dbcbf 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/ChunkBatchStartPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/ChunkBatchStartPacket.cs @@ -1,4 +1,4 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; @@ -8,10 +8,12 @@ namespace MineSharp.Protocol.Packets.Clientbound.Play; /// The ChunkBatchStart Packet, used since 1.20.2 /// https://wiki.vg/Protocol#Chunk_Batch_Start /// -public class ChunkBatchStartPacket : IPacket +public sealed record ChunkBatchStartPacket : IPacket { /// - public PacketType Type => PacketType.CB_Play_ChunkBatchStart; + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_ChunkBatchStart; /// public void Write(PacketBuffer buffer, MinecraftData version) diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/ChunkBiomesPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/ChunkBiomesPacket.cs new file mode 100644 index 00000000..04dc460b --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/ChunkBiomesPacket.cs @@ -0,0 +1,58 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using static MineSharp.Protocol.Packets.Clientbound.Play.ChunkBiomesPacket; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Chunk Biomes packet +/// +/// Number of chunks +/// Array of chunk biome data +public sealed record ChunkBiomesPacket(int NumberOfChunks, ChunkBiomeData[] ChunkBiomes) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_ChunkBiomes; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(NumberOfChunks); + foreach (var chunk in ChunkBiomes) + { + buffer.WriteInt(chunk.ChunkZ); + buffer.WriteInt(chunk.ChunkX); + buffer.WriteVarInt(chunk.Size); + buffer.WriteBytes(chunk.Data); + } + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var numberOfChunks = buffer.ReadVarInt(); + var chunkBiomeData = new ChunkBiomeData[numberOfChunks]; + for (int i = 0; i < numberOfChunks; i++) + { + var chunkZ = buffer.ReadInt(); + var chunkX = buffer.ReadInt(); + var size = buffer.ReadVarInt(); + var data = buffer.ReadBytes(size); + chunkBiomeData[i] = new ChunkBiomeData(chunkZ, chunkX, size, data); + } + + return new ChunkBiomesPacket(numberOfChunks, chunkBiomeData); + } + + /// + /// Represents the chunk biome data + /// + /// Chunk Z coordinate + /// Chunk X coordinate + /// Size of data in bytes + /// Chunk data structure + public sealed record ChunkBiomeData(int ChunkZ, int ChunkX, int Size, byte[] Data); +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/ChunkDataAndUpdateLightPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/ChunkDataAndUpdateLightPacket.cs index 315a7b75..caf195e2 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/ChunkDataAndUpdateLightPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/ChunkDataAndUpdateLightPacket.cs @@ -1,117 +1,46 @@ using fNbt; using MineSharp.Core; -using MineSharp.Core.Common; using MineSharp.Core.Common.Blocks; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; using MineSharp.Protocol.Exceptions; namespace MineSharp.Protocol.Packets.Clientbound.Play; -#pragma warning disable CS1591 + /// /// Chunk data and update light packet /// -public class ChunkDataAndUpdateLightPacket : IPacket +/// X coordinate of the chunk +/// Y coordinate of the chunk +/// Heightmaps +/// Raw chunk data +/// Array of BlockEntities +/// Whether to trust edges (only sent before 1.20) +/// +/// +/// +/// +/// +/// +public sealed record ChunkDataAndUpdateLightPacket( + int X, + int Z, + NbtCompound Heightmaps, + byte[] ChunkData, + BlockEntity[] BlockEntities, + bool? TrustEdges, + long[] SkyLightMask, + long[] BlockLightMask, + long[] EmptySkyLightMask, + long[] EmptyBlockLightMask, + byte[][] SkyLight, + byte[][] BlockLight) : IPacket { - /// - /// Create a new instance - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public ChunkDataAndUpdateLightPacket( - int x, - int z, - NbtCompound heightmaps, - byte[] chunkData, - BlockEntity[] blockEntities, - bool? trustEdges, - long[] skyLightMask, - long[] blockLightMask, - long[] emptyBlockLightMask, - long[] emptySkyLightMask, - byte[][] skyLight, - byte[][] blockLight) - { - X = x; - Z = z; - Heightmaps = heightmaps; - ChunkData = chunkData; - BlockEntities = blockEntities; - TrustEdges = trustEdges; - SkyLightMask = skyLightMask; - BlockLightMask = blockLightMask; - EmptyBlockLightMask = emptyBlockLightMask; - EmptySkyLightMask = emptySkyLightMask; - SkyLight = skyLight; - BlockLight = blockLight; - } - - /// - /// X coordinate of the chunk - /// - public int X { get; set; } - - /// - /// Y coordinate of the chunk - /// - public int Z { get; set; } - - /// - /// Heightmaps - /// - public NbtCompound Heightmaps { get; set; } - - /// - /// Raw chunk data - /// - public byte[] ChunkData { get; set; } - - /// - /// Array of BlockEntities - /// - public BlockEntity[] BlockEntities { get; set; } - - /// - /// Whether to trust edges (only sent before 1.20) - /// - public bool? TrustEdges { get; set; } - - /// - /// - public long[] SkyLightMask { get; set; } - - /// - /// - public long[] BlockLightMask { get; set; } - - /// - /// - public long[] EmptySkyLightMask { get; set; } - - /// - /// - public long[] EmptyBlockLightMask { get; set; } - - /// - /// - public byte[][] SkyLight { get; set; } - - /// - /// - public byte[][] BlockLight { get; set; } - /// - public PacketType Type => PacketType.CB_Play_MapChunk; + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_MapChunk; /// public void Write(PacketBuffer buffer, MinecraftData version) @@ -138,10 +67,10 @@ public void Write(PacketBuffer buffer, MinecraftData version) buffer.WriteBool(TrustEdges!.Value); } - WriteLongArray(SkyLightMask, buffer); - WriteLongArray(BlockLightMask, buffer); - WriteLongArray(EmptySkyLightMask, buffer); - WriteLongArray(EmptyBlockLightMask, buffer); + buffer.WriteLongArray(SkyLightMask); + buffer.WriteLongArray(BlockLightMask); + buffer.WriteLongArray(EmptySkyLightMask); + buffer.WriteLongArray(EmptyBlockLightMask); buffer.WriteVarInt(SkyLight.Length); foreach (var array in SkyLight) @@ -150,7 +79,6 @@ public void Write(PacketBuffer buffer, MinecraftData version) buffer.WriteBytes(array); } - buffer.WriteVarInt(BlockLight.Length); foreach (var array in BlockLight) { @@ -180,10 +108,10 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) trustEdges = buffer.ReadBool(); } - var skyLightMask = ReadLongArray(buffer); - var blockLightMask = ReadLongArray(buffer); - var emptySkyLightMask = ReadLongArray(buffer); - var emptyBlockLightMask = ReadLongArray(buffer); + var skyLightMask = buffer.ReadLongArray(); + var blockLightMask = buffer.ReadLongArray(); + var emptySkyLightMask = buffer.ReadLongArray(); + var emptyBlockLightMask = buffer.ReadLongArray(); var skyLight = new byte[buffer.ReadVarInt()][]; for (var i = 0; i < skyLight.Length; i++) { @@ -212,27 +140,4 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) skyLight, blockLight); } - - private static void WriteLongArray(long[] array, PacketBuffer buffer) - { - buffer.WriteVarInt(array.Length); - foreach (var l in array) - { - buffer.WriteLong(l); - } - } - - private static long[] ReadLongArray(PacketBuffer buffer) - { - var length = buffer.ReadVarInt(); - var array = new long[length]; - - for (var i = 0; i < array.Length; i++) - { - array[i] = buffer.ReadLong(); - } - - return array; - } } -#pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/ClearTitlesPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/ClearTitlesPacket.cs new file mode 100644 index 00000000..3bdaac48 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/ClearTitlesPacket.cs @@ -0,0 +1,30 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Clear Titles packet +/// +/// Whether to reset the client's current title information +public sealed record ClearTitlesPacket(bool Reset) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_ClearTitles; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteBool(Reset); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var reset = buffer.ReadBool(); + return new ClearTitlesPacket(reset); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/ClickContainerButtonPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/ClickContainerButtonPacket.cs new file mode 100644 index 00000000..7a6206f4 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/ClickContainerButtonPacket.cs @@ -0,0 +1,39 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Used when clicking on window buttons. Until 1.14, this was only used by enchantment tables. +/// +/// The ID of the window sent by Open Screen. +/// Meaning depends on window type; see below. +public sealed record ClickContainerButtonPacket(byte WindowId, byte ButtonId) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_EnchantItem; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteByte(WindowId); + buffer.WriteByte(ButtonId); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var windowId = buffer.ReadByte(); + var buttonId = buffer.ReadByte(); + + return new ClickContainerButtonPacket(windowId, buttonId); + } + + // TODO: Add all the meanings of the properties + // depends on the type of container but we only get the window ID here + // so we need static methods that have the container type as a parameter + // https://wiki.vg/index.php?title=Protocol&oldid=19208#Click_Container_Button +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/CloseWindowPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/CloseWindowPacket.cs index 038c1ac5..427c4234 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/CloseWindowPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/CloseWindowPacket.cs @@ -1,30 +1,19 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Play; -#pragma warning disable CS1591 + /// /// Close window packet +/// The id of the window to close /// -public class CloseWindowPacket : IPacket +public sealed record CloseWindowPacket(byte WindowId) : IPacket { - /// - /// Create a new instance - /// - /// - public CloseWindowPacket(byte windowId) - { - WindowId = windowId; - } - - /// - /// The id of the window to close - /// - public byte WindowId { get; set; } - /// - public PacketType Type => PacketType.CB_Play_CloseWindow; + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_CloseWindow; /// public void Write(PacketBuffer buffer, MinecraftData version) @@ -35,8 +24,6 @@ public void Write(PacketBuffer buffer, MinecraftData version) /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { - return new CloseWindowPacket( - buffer.ReadByte()); + return new CloseWindowPacket(buffer.ReadByte()); } } -#pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/CombatDeathPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/CombatDeathPacket.cs index 312b1bbe..b3b7bb04 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/CombatDeathPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/CombatDeathPacket.cs @@ -1,5 +1,6 @@ -using MineSharp.Core; -using MineSharp.Core.Common; +using MineSharp.ChatComponent; +using MineSharp.Core; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; @@ -8,15 +9,28 @@ namespace MineSharp.Protocol.Packets.Clientbound.Play; /// /// Combat death packet /// -public class CombatDeathPacket : IPacket +public sealed record CombatDeathPacket : IPacket { + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_DeathCombatEvent; + + // Here is no non-argument constructor allowed + // Do not use +#pragma warning disable CS8618 + private CombatDeathPacket() +#pragma warning restore CS8618 + { + } + /// /// Constructor before 1.20 /// /// /// /// - public CombatDeathPacket(int playerId, int entityId, string message) + public CombatDeathPacket(int playerId, int entityId, Chat message) { PlayerId = playerId; EntityId = entityId; @@ -28,13 +42,13 @@ public CombatDeathPacket(int playerId, int entityId, string message) /// /// /// - public CombatDeathPacket(int playerId, string message) + public CombatDeathPacket(int playerId, Chat message) { PlayerId = playerId; Message = message; } - private CombatDeathPacket(int playerId, int? entityId, string message) + private CombatDeathPacket(int playerId, int? entityId, Chat message) { PlayerId = playerId; EntityId = entityId; @@ -44,20 +58,17 @@ private CombatDeathPacket(int playerId, int? entityId, string message) /// /// Id of the player /// - public int PlayerId { get; set; } + public int PlayerId { get; init; } /// /// Id of the entity /// - public int? EntityId { get; set; } + public int? EntityId { get; init; } /// /// Death message /// - public string Message { get; set; } - - /// - public PacketType Type => PacketType.CB_Play_DeathCombatEvent; + public Chat Message { get; init; } /// public void Write(PacketBuffer buffer, MinecraftData version) @@ -68,7 +79,7 @@ public void Write(PacketBuffer buffer, MinecraftData version) buffer.WriteInt(EntityId!.Value); } - buffer.WriteString(Message); + buffer.WriteChatComponent(Message); } /// @@ -81,7 +92,7 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) entityId = buffer.ReadInt(); } - var message = buffer.ReadString(); + var message = buffer.ReadChatComponent(); return new CombatDeathPacket(playerId, entityId, message); } } diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/CommandSuggestionsResponsePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/CommandSuggestionsResponsePacket.cs new file mode 100644 index 00000000..b8ab2471 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/CommandSuggestionsResponsePacket.cs @@ -0,0 +1,85 @@ +using MineSharp.ChatComponent; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using static MineSharp.Protocol.Packets.Clientbound.Play.CommandSuggestionsResponsePacket; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Command Suggestions Response packet +/// +/// Transaction ID +/// Start of the text to replace +/// Length of the text to replace +/// Array of matches +public sealed record CommandSuggestionsResponsePacket(int Id, int Start, int Length, Match[] Matches) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_TabComplete; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(Id); + buffer.WriteVarInt(Start); + buffer.WriteVarInt(Length); + buffer.WriteVarInt(Matches.Length); + foreach (var match in Matches) + { + match.Write(buffer); + } + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var id = buffer.ReadVarInt(); + var start = buffer.ReadVarInt(); + var length = buffer.ReadVarInt(); + var count = buffer.ReadVarInt(); + var matches = new Match[count]; + for (int i = 0; i < count; i++) + { + matches[i] = Match.Read(buffer); + } + + return new CommandSuggestionsResponsePacket(id, start, length, matches); + } + + /// + /// Represents a match in the command suggestions response + /// + /// The match string + /// The tooltip text component or null + public sealed record Match(string Value, Chat? Tooltip) : ISerializable + { + /// + public void Write(PacketBuffer buffer) + { + buffer.WriteString(Value); + var hasTooltip = Tooltip != null; + buffer.WriteBool(hasTooltip); + if (hasTooltip) + { + buffer.WriteChatComponent(Tooltip!); + } + } + + /// + public static Match Read(PacketBuffer buffer) + { + var match = buffer.ReadString(); + var hasTooltip = buffer.ReadBool(); + Chat? tooltip = null; + if (hasTooltip) + { + tooltip = buffer.ReadChatComponent(); + } + + return new Match(match, tooltip); + } + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/DamageEventPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/DamageEventPacket.cs new file mode 100644 index 00000000..909b7ca9 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/DamageEventPacket.cs @@ -0,0 +1,78 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Damage event packet +/// +/// The ID of the entity taking damage +/// The type of damage in the minecraft:damage_type registry +/// The ID + 1 of the entity responsible for the damage, if present +/// The ID + 1 of the entity that directly dealt the damage, if present +/// Indicates the presence of the source position fields +/// The X coordinate of the source position, if present +/// The Y coordinate of the source position, if present +/// The Z coordinate of the source position, if present +public sealed record DamageEventPacket( + int EntityId, + int SourceTypeId, + int SourceCauseId, + int SourceDirectId, + bool HasSourcePosition, + double? SourcePositionX, + double? SourcePositionY, + double? SourcePositionZ) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_DamageEvent; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(EntityId); + buffer.WriteVarInt(SourceTypeId); + buffer.WriteVarInt(SourceCauseId); + buffer.WriteVarInt(SourceDirectId); + buffer.WriteBool(HasSourcePosition); + if (HasSourcePosition) + { + buffer.WriteDouble(SourcePositionX.GetValueOrDefault()); + buffer.WriteDouble(SourcePositionY.GetValueOrDefault()); + buffer.WriteDouble(SourcePositionZ.GetValueOrDefault()); + } + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var entityId = buffer.ReadVarInt(); + var sourceTypeId = buffer.ReadVarInt(); + var sourceCauseId = buffer.ReadVarInt(); + var sourceDirectId = buffer.ReadVarInt(); + var hasSourcePosition = buffer.ReadBool(); + double? sourcePositionX = null; + double? sourcePositionY = null; + double? sourcePositionZ = null; + + if (hasSourcePosition) + { + sourcePositionX = buffer.ReadDouble(); + sourcePositionY = buffer.ReadDouble(); + sourcePositionZ = buffer.ReadDouble(); + } + + return new DamageEventPacket( + entityId, + sourceTypeId, + sourceCauseId, + sourceDirectId, + hasSourcePosition, + sourcePositionX, + sourcePositionY, + sourcePositionZ); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/DeclareCommandsPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/DeclareCommandsPacket.cs index 1ce50bab..1053a2f2 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/DeclareCommandsPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/DeclareCommandsPacket.cs @@ -1,30 +1,21 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Play; -#pragma warning disable CS1591 + /// /// Declare commands packet /// -public class DeclareCommandsPacket : IPacket +/// +/// Raw buffer. The Command tree is not parsed here +/// +public sealed record DeclareCommandsPacket(PacketBuffer RawBuffer) : IPacket { - /// - /// Create a new instance - /// - /// - public DeclareCommandsPacket(PacketBuffer buffer) - { - RawBuffer = buffer; - } - - /// - /// Raw buffer. The Command tree is not parsed here - /// - public PacketBuffer RawBuffer { get; set; } - /// - public PacketType Type => PacketType.CB_Play_DeclareCommands; + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_DeclareCommands; /// public void Write(PacketBuffer buffer, MinecraftData version) @@ -39,4 +30,3 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) return new DeclareCommandsPacket(clone); } } -#pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/DeleteMessagePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/DeleteMessagePacket.cs new file mode 100644 index 00000000..3163eabf --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/DeleteMessagePacket.cs @@ -0,0 +1,42 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Packet sent by the server to remove a message from the client's chat. +/// This only works for messages with signatures; system messages cannot be deleted with this packet. +/// +/// The message ID + 1, used for validating message signature. +/// The previous message's signature. Always 256 bytes and not length-prefixed. +public sealed record DeleteMessagePacket(int MessageId, byte[]? Signature) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_HideMessage; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(MessageId); + if (MessageId == 0 && Signature != null) + { + buffer.WriteBytes(Signature); + } + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var messageId = buffer.ReadVarInt(); + byte[]? signature = null; + if (messageId == 0) + { + signature = buffer.ReadBytes(256); + } + + return new DeleteMessagePacket(messageId, signature); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/DisconnectPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/DisconnectPacket.cs index 6aafb95e..bc6cc656 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/DisconnectPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/DisconnectPacket.cs @@ -1,31 +1,22 @@ using MineSharp.ChatComponent; -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Play; -#pragma warning disable CS1591 + /// /// Disconnect packet for play /// -public class DisconnectPacket : IPacket +/// +/// The reason for being disconnected +/// +public sealed record DisconnectPacket(Chat Reason) : IPacket { - /// - /// Create a new instance - /// - /// - public DisconnectPacket(Chat reason) - { - Reason = reason; - } - - /// - /// The reason for being disconnected - /// - public Chat Reason { get; set; } - /// - public PacketType Type => PacketType.CB_Play_KickDisconnect; + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_KickDisconnect; /// public void Write(PacketBuffer buffer, MinecraftData version) @@ -39,4 +30,4 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) return new DisconnectPacket(buffer.ReadChatComponent()); } } -#pragma warning restore CS1591 + diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/DisguisedChatMessagePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/DisguisedChatMessagePacket.cs index 5c63cc98..e649c865 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/DisguisedChatMessagePacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/DisguisedChatMessagePacket.cs @@ -1,26 +1,25 @@ using MineSharp.ChatComponent; -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Play; -#pragma warning disable CS1591 -public class DisguisedChatMessagePacket : IPacket -{ - public DisguisedChatMessagePacket(Chat message, int chatType, Chat name, Chat? target) - { - Message = message; - ChatType = chatType; - Name = name; - Target = target; - } - public Chat Message { get; set; } - public int ChatType { get; set; } - public Chat Name { get; set; } - public Chat? Target { get; set; } - public PacketType Type => PacketType.CB_Play_ProfilelessChat; +/// +/// Disguised chat message packet +/// +/// The chat message +/// The type of chat +/// The name associated with the chat +/// The target of the chat message (optional) +public sealed record DisguisedChatMessagePacket(Chat Message, int ChatType, Chat Name, Chat? Target) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_ProfilelessChat; + /// public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteChatComponent(Message); @@ -33,6 +32,7 @@ public void Write(PacketBuffer buffer, MinecraftData version) } } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { return new DisguisedChatMessagePacket( @@ -42,4 +42,3 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) buffer.ReadBool() ? buffer.ReadChatComponent() : null); } } -#pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/DisplayObjectivePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/DisplayObjectivePacket.cs new file mode 100644 index 00000000..f0bb9da6 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/DisplayObjectivePacket.cs @@ -0,0 +1,34 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Display Objective packet sent to the client to display a scoreboard. +/// +/// The position of the scoreboard. +/// The unique name for the scoreboard to be displayed. +public sealed record DisplayObjectivePacket(int Position, string ScoreName) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_ScoreboardDisplayObjective; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(Position); + buffer.WriteString(ScoreName); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var position = buffer.ReadVarInt(); + var scoreName = buffer.ReadString(); + + return new DisplayObjectivePacket(position, scoreName); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/EndCombatPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/EndCombatPacket.cs new file mode 100644 index 00000000..4b0a2e36 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/EndCombatPacket.cs @@ -0,0 +1,30 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// End Combat packet +/// +/// Length of the combat in ticks +public sealed record EndCombatPacket(int Duration) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_EndCombatEvent; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(Duration); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var duration = buffer.ReadVarInt(); + return new EndCombatPacket(duration); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/EnterCombatPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/EnterCombatPacket.cs new file mode 100644 index 00000000..c8412768 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/EnterCombatPacket.cs @@ -0,0 +1,29 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Enter Combat packet +/// +public sealed record EnterCombatPacket() : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_EnterCombatEvent; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + // No fields to write + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + // No fields to read + return new EnterCombatPacket(); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/EntityAnimationPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/EntityAnimationPacket.cs new file mode 100644 index 00000000..69f42cc5 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/EntityAnimationPacket.cs @@ -0,0 +1,49 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using static MineSharp.Protocol.Packets.Clientbound.Play.EntityAnimationPacket; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Entity animation packet +/// +/// The entity ID +/// The animation ID +public sealed record EntityAnimationPacket(int EntityId, EntityAnimation Animation) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_Animation; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(EntityId); + buffer.WriteByte((byte)Animation); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var entityId = buffer.ReadVarInt(); + var animation = (EntityAnimation)buffer.ReadByte(); + + return new EntityAnimationPacket(entityId, animation); + } + + /// + /// Enum representing different types of entity animations. + /// + public enum EntityAnimation + { +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + SwingMainArm = 0, + LeaveBed = 2, + SwingOffhand = 3, + CriticalEffect = 4, + MagicCriticalEffect = 5 +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/EntityEffectPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/EntityEffectPacket.cs new file mode 100644 index 00000000..306860e1 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/EntityEffectPacket.cs @@ -0,0 +1,101 @@ +using fNbt; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play +{ + /// + /// Represents the entity effect packet sent by the server to the client. + /// + /// The entity ID + /// The effect ID + /// The amplifier of the effect + /// The duration of the effect in ticks. -1 for infinity + /// The flags for the effect + /// Indicates if the effect has factor data + /// The factor codec data + public sealed record EntityEffectPacket( + int EntityId, + int EffectId, + byte Amplifier, + int Duration, + EffectFlags Flags, + bool HasFactorData, + NbtTag? FactorCodec) : IPacket + { + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_EntityEffect; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(EntityId); + buffer.WriteVarInt(EffectId); + buffer.WriteByte(Amplifier); + buffer.WriteVarInt(Duration); + buffer.WriteByte((byte)Flags); + buffer.WriteBool(HasFactorData); + if (HasFactorData) + { + buffer.WriteNbt(FactorCodec!); + } + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var entityId = buffer.ReadVarInt(); + var effectId = buffer.ReadVarInt(); + var amplifier = buffer.ReadByte(); + var duration = buffer.ReadVarInt(); + var flags = (EffectFlags)buffer.ReadByte(); + var hasFactorData = buffer.ReadBool(); + NbtTag? factorCodec = null; + if (hasFactorData) + { + // TODO: Check if this is correct + factorCodec = buffer.ReadNbt(); + } + + return new EntityEffectPacket( + entityId, + effectId, + amplifier, + duration, + flags, + hasFactorData, + factorCodec); + } + } + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + [Flags] + public enum EffectFlags + { + None = 0, + /// + /// Is ambient - was the effect spawned from a beacon? + /// All beacon-generated effects are ambient. + /// Ambient effects use a different icon in the HUD (blue border rather than gray). + /// If all effects on an entity are ambient, the "Is potion effect ambient" living metadata field should be set to true. + /// Usually should not be enabled. + /// + IsAmbient = 0x01, + /// + /// Show particles - should all particles from this effect be hidden? + /// Effects with particles hidden are not included in the calculation of the effect color, + /// and are not rendered on the HUD (but are still rendered within the inventory). + /// Usually should be enabled. + /// + ShowParticles = 0x02, + /// + /// Show icon - should the icon be displayed on the client? + /// Usually should be enabled. + /// + ShowIcon = 0x04 + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/EntityPositionAndRotationPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/EntityPositionAndRotationPacket.cs index 6cdd9dc4..0e70a4ad 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/EntityPositionAndRotationPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/EntityPositionAndRotationPacket.cs @@ -1,32 +1,27 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Play; -#pragma warning disable CS1591 -public class EntityPositionAndRotationPacket : IPacket -{ - public EntityPositionAndRotationPacket(int entityId, short deltaX, short deltaY, short deltaZ, sbyte yaw, - sbyte pitch, bool onGround) - { - EntityId = entityId; - DeltaX = deltaX; - DeltaY = deltaY; - DeltaZ = deltaZ; - Yaw = yaw; - Pitch = pitch; - OnGround = onGround; - } - public int EntityId { get; set; } - public short DeltaX { get; set; } - public short DeltaY { get; set; } - public short DeltaZ { get; set; } - public sbyte Yaw { get; set; } - public sbyte Pitch { get; set; } - public bool OnGround { get; set; } - public PacketType Type => PacketType.CB_Play_EntityMoveLook; +/// +/// Entity position and rotation packet +/// +/// The entity ID +/// The change in X position +/// The change in Y position +/// The change in Z position +/// The yaw rotation +/// The pitch rotation +/// Whether the entity is on the ground +public sealed record EntityPositionAndRotationPacket(int EntityId, short DeltaX, short DeltaY, short DeltaZ, sbyte Yaw, sbyte Pitch, bool OnGround) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_EntityMoveLook; + /// public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteVarInt(EntityId); @@ -38,6 +33,7 @@ public void Write(PacketBuffer buffer, MinecraftData version) buffer.WriteBool(OnGround); } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { var entityId = buffer.ReadVarInt(); @@ -55,4 +51,3 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) onGround); } } -#pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/EntityPositionPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/EntityPositionPacket.cs index ee03130c..288e2445 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/EntityPositionPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/EntityPositionPacket.cs @@ -1,27 +1,25 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Play; -#pragma warning disable CS1591 -public class EntityPositionPacket : IPacket -{ - public EntityPositionPacket(int entityId, short deltaX, short deltaY, short deltaZ, bool onGround) - { - EntityId = entityId; - DeltaX = deltaX; - DeltaY = deltaY; - DeltaZ = deltaZ; - OnGround = onGround; - } - public int EntityId { get; set; } - public short DeltaX { get; set; } - public short DeltaY { get; set; } - public short DeltaZ { get; set; } - public bool OnGround { get; set; } - public PacketType Type => PacketType.CB_Play_RelEntityMove; +/// +/// Entity position packet +/// +/// The entity ID +/// The change in X position +/// The change in Y position +/// The change in Z position +/// Whether the entity is on the ground +public sealed record EntityPositionPacket(int EntityId, short DeltaX, short DeltaY, short DeltaZ, bool OnGround) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_RelEntityMove; + /// public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteVarInt(EntityId); @@ -31,6 +29,7 @@ public void Write(PacketBuffer buffer, MinecraftData version) buffer.WriteBool(OnGround); } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { var entityId = buffer.ReadVarInt(); @@ -45,4 +44,4 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) onGround); } } -#pragma warning restore CS1591 + diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/EntityRotationPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/EntityRotationPacket.cs index f691dd37..4303e478 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/EntityRotationPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/EntityRotationPacket.cs @@ -1,25 +1,24 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Play; -#pragma warning disable CS1591 -public class EntityRotationPacket : IPacket -{ - public EntityRotationPacket(int entityId, sbyte yaw, sbyte pitch, bool onGround) - { - EntityId = entityId; - Yaw = yaw; - Pitch = pitch; - OnGround = onGround; - } - public int EntityId { get; set; } - public sbyte Yaw { get; set; } - public sbyte Pitch { get; set; } - public bool OnGround { get; set; } - public PacketType Type => PacketType.CB_Play_EntityLook; +/// +/// Entity rotation packet +/// +/// The entity ID +/// The yaw rotation +/// The pitch rotation +/// Whether the entity is on the ground +public sealed record EntityRotationPacket(int EntityId, sbyte Yaw, sbyte Pitch, bool OnGround) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_EntityLook; + /// public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteVarInt(EntityId); @@ -28,6 +27,7 @@ public void Write(PacketBuffer buffer, MinecraftData version) buffer.WriteBool(OnGround); } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { var entityId = buffer.ReadVarInt(); @@ -38,4 +38,4 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) return new EntityRotationPacket(entityId, yaw, pitch, onGround); } } -#pragma warning restore CS1591 + diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/EntitySoundEffectPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/EntitySoundEffectPacket.cs new file mode 100644 index 00000000..c2678cc3 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/EntitySoundEffectPacket.cs @@ -0,0 +1,70 @@ +using MineSharp.Core.Common; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; +#pragma warning disable CS1591 +public sealed record EntitySoundEffectPacket( + int SoundId, + Identifier? SoundName, + bool? HasFixedRange, + float? Range, + int SoundCategory, + int EntityId, + float Volume, + float Pitch, + long Seed +) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_EntitySoundEffect; + + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(SoundId); + if (SoundId == 0) + { + buffer.WriteIdentifier(SoundName!); + buffer.WriteBool(HasFixedRange!.Value); + if (HasFixedRange.Value) + { + buffer.WriteFloat(Range!.Value); + } + } + buffer.WriteVarInt(SoundCategory); + buffer.WriteVarInt(EntityId); + buffer.WriteFloat(Volume); + buffer.WriteFloat(Pitch); + buffer.WriteLong(Seed); + } + + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var soundId = buffer.ReadVarInt(); + Identifier? soundName = null; + bool? hasFixedRange = null; + float? range = null; + + if (soundId == 0) + { + soundName = buffer.ReadIdentifier(); + hasFixedRange = buffer.ReadBool(); + if (hasFixedRange.Value) + { + range = buffer.ReadFloat(); + } + } + + var soundCategory = buffer.ReadVarInt(); + var entityId = buffer.ReadVarInt(); + var volume = buffer.ReadFloat(); + var pitch = buffer.ReadFloat(); + var seed = buffer.ReadLong(); + + return new EntitySoundEffectPacket(soundId, soundName, hasFixedRange, range, soundCategory, entityId, volume, pitch, seed); + } +} +#pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/EntityStatusPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/EntityStatusPacket.cs index 9612c849..92069a7a 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/EntityStatusPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/EntityStatusPacket.cs @@ -1,32 +1,38 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Play; -#pragma warning disable CS1591 -public class EntityStatusPacket : IPacket -{ - public EntityStatusPacket(int entityId, byte status) - { - EntityId = entityId; - Status = status; - } - public int EntityId { get; set; } - public byte Status { get; set; } - public PacketType Type => PacketType.CB_Play_EntityStatus; +/// +/// Entity status packet +/// +/// The entity ID +/// The status byte +public sealed record EntityStatusPacket(int EntityId, sbyte Status) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_EntityStatus; + /// public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteVarInt(EntityId); - buffer.WriteByte(Status); + buffer.WriteSByte(Status); } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { - return new EntityStatusPacket( - buffer.ReadInt(), - buffer.ReadByte()); + var entityId = buffer.ReadVarInt(); + var status = buffer.ReadSByte(); + + return new EntityStatusPacket(entityId, status); } + + // TODO: Add all the meanings of the status + // not all statuses are valid for all entities + // https://wiki.vg/Entity_statuses } -#pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/ExplosionPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/ExplosionPacket.cs new file mode 100644 index 00000000..b26e2652 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/ExplosionPacket.cs @@ -0,0 +1,149 @@ +using MineSharp.Core.Common; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using MineSharp.Protocol.Packets.NetworkTypes; +using static MineSharp.Protocol.Packets.Clientbound.Play.ExplosionPacket; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Explosion packet sent when an explosion occurs (creepers, TNT, and ghast fireballs). +/// +/// The X coordinate of the explosion. +/// The Y coordinate of the explosion. +/// The Z coordinate of the explosion. +/// The strength of the explosion. +/// The affected block records. +/// The X velocity of the player being pushed by the explosion. +/// The Y velocity of the player being pushed by the explosion. +/// The Z velocity of the player being pushed by the explosion. +/// The block interaction type. +/// The particle ID for small explosion particles. +/// The particle data for small explosion particles. +/// The particle ID for large explosion particles. +/// The particle data for large explosion particles. +/// The sound played during the explosion. +public sealed record ExplosionPacket( + double X, + double Y, + double Z, + float Strength, + (byte X, byte Y, byte Z)[] Records, + float PlayerMotionX, + float PlayerMotionY, + float PlayerMotionZ, + BlockInteractionType BlockInteraction, + int SmallExplosionParticleID, + IParticleData? SmallExplosionParticleData, + int LargeExplosionParticleID, + IParticleData? LargeExplosionParticleData, + ExplosionSound Sound +) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_Explosion; + + /// + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteDouble(X); + buffer.WriteDouble(Y); + buffer.WriteDouble(Z); + buffer.WriteFloat(Strength); + buffer.WriteVarInt(Records.Length); + foreach (var record in Records) + { + buffer.WriteByte(record.X); + buffer.WriteByte(record.Y); + buffer.WriteByte(record.Z); + } + buffer.WriteFloat(PlayerMotionX); + buffer.WriteFloat(PlayerMotionY); + buffer.WriteFloat(PlayerMotionZ); + buffer.WriteVarInt((int)BlockInteraction); + buffer.WriteVarInt(SmallExplosionParticleID); + SmallExplosionParticleData?.Write(buffer, data); + buffer.WriteVarInt(LargeExplosionParticleID); + LargeExplosionParticleData?.Write(buffer, data); + Sound.Write(buffer); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData data) + { + var x = buffer.ReadDouble(); + var y = buffer.ReadDouble(); + var z = buffer.ReadDouble(); + var strength = buffer.ReadFloat(); + var recordCount = buffer.ReadVarInt(); + var records = new (byte X, byte Y, byte Z)[recordCount]; + for (int i = 0; i < recordCount; i++) + { + records[i] = (buffer.ReadByte(), buffer.ReadByte(), buffer.ReadByte()); + } + var playerMotionX = buffer.ReadFloat(); + var playerMotionY = buffer.ReadFloat(); + var playerMotionZ = buffer.ReadFloat(); + var blockInteraction = (BlockInteractionType)buffer.ReadVarInt(); + var smallExplosionParticleID = buffer.ReadVarInt(); + var smallExplosionParticleData = ParticleDataRegistry.Read(buffer, data, smallExplosionParticleID); + var largeExplosionParticleID = buffer.ReadVarInt(); + var largeExplosionParticleData = ParticleDataRegistry.Read(buffer, data, largeExplosionParticleID); + var sound = ExplosionSound.Read(buffer); + + return new ExplosionPacket( + x, y, z, strength, records, playerMotionX, playerMotionY, playerMotionZ, + blockInteraction, smallExplosionParticleID, smallExplosionParticleData, + largeExplosionParticleID, largeExplosionParticleData, sound + ); + } + + /// + /// Represents the sound played during an explosion. + /// + /// The name of the sound played. + /// The fixed range of the sound. + public sealed record ExplosionSound( + Identifier SoundName, + float? Range + ) : ISerializable + { + /// + public void Write(PacketBuffer buffer) + { + buffer.WriteIdentifier(SoundName); + var hasFixedRange = Range.HasValue; + buffer.WriteBool(hasFixedRange); + if (hasFixedRange) + { + buffer.WriteFloat(Range!.Value); + } + } + + /// + public static ExplosionSound Read(PacketBuffer buffer) + { + var soundName = buffer.ReadIdentifier(); + var hasFixedRange = buffer.ReadBool(); + float? range = hasFixedRange ? buffer.ReadFloat() : null; + + return new ExplosionSound(soundName, range); + } + } + + /// + /// Enum representing the block interaction type during an explosion. + /// + public enum BlockInteractionType + { +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + Keep = 0, + Destroy = 1, + DestroyWithDecay = 2, + TriggerBlock = 3 +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/GameEventPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/GameEventPacket.cs index c1941e0b..c04cdff1 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/GameEventPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/GameEventPacket.cs @@ -1,32 +1,57 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; +using static MineSharp.Protocol.Packets.Clientbound.Play.GameEventPacket; namespace MineSharp.Protocol.Packets.Clientbound.Play; #pragma warning disable CS1591 -public class GameEventPacket : IPacket +/// +/// Game event packet +/// +/// The game event +/// The value associated with the event +public sealed record GameEventPacket(GameEvent Event, float Value) : IPacket { - public GameEventPacket(byte @event, float value) - { - Event = @event; - Value = value; - } - - public byte Event { get; set; } - public float Value { get; set; } - public PacketType Type => PacketType.CB_Play_GameStateChange; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_GameStateChange; + /// public void Write(PacketBuffer buffer, MinecraftData version) { - buffer.WriteByte(Event); + buffer.WriteByte((byte)Event); buffer.WriteFloat(Value); } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { var @event = buffer.ReadByte(); var value = buffer.ReadFloat(); - return new GameEventPacket(@event, value); + return new GameEventPacket((GameEvent)@event, value); + } + + /// + /// All the possible game events + /// + public enum GameEvent : byte + { + NoRespawnBlockAvailable, + EndRaining, + BeginRaining, + ChangeGameMode, + WinGame, + DemoEvent, + ArrowHitPlayer, + RainLevelChange, + ThunderLevelChange, + PlayPufferfishStingSound, + PlayElderGuardianMobAppearance, + EnableRespawnScreen, + LimitedCrafting, + StartWaitingForLevelChunks } } + #pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/HurtAnimationPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/HurtAnimationPacket.cs new file mode 100644 index 00000000..2be5c4e5 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/HurtAnimationPacket.cs @@ -0,0 +1,34 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Hurt Animation packet +/// +/// The ID of the entity taking damage +/// The direction the damage is coming from in relation to the entity +public sealed record HurtAnimationPacket(int EntityId, float Yaw) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_HurtAnimation; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(EntityId); + buffer.WriteFloat(Yaw); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var entityId = buffer.ReadVarInt(); + var yaw = buffer.ReadFloat(); + + return new HurtAnimationPacket(entityId, yaw); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/InitializeWorldBorderPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/InitializeWorldBorderPacket.cs new file mode 100644 index 00000000..6f086d2e --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/InitializeWorldBorderPacket.cs @@ -0,0 +1,68 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Initialize World Border packet +/// +/// The X coordinate of the world border center +/// The Z coordinate of the world border center +/// Current length of a single side of the world border, in meters +/// Target length of a single side of the world border, in meters +/// Number of real-time milliseconds until New Diameter is reached +/// Resulting coordinates from a portal teleport are limited to ±value +/// Warning distance in meters +/// Warning time in seconds +public sealed record InitializeWorldBorderPacket( + double X, + double Z, + double OldDiameter, + double NewDiameter, + long Speed, + int PortalTeleportBoundary, + int WarningBlocks, + int WarningTime) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_InitializeWorldBorder; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteDouble(X); + buffer.WriteDouble(Z); + buffer.WriteDouble(OldDiameter); + buffer.WriteDouble(NewDiameter); + buffer.WriteVarLong(Speed); + buffer.WriteVarInt(PortalTeleportBoundary); + buffer.WriteVarInt(WarningBlocks); + buffer.WriteVarInt(WarningTime); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var x = buffer.ReadDouble(); + var z = buffer.ReadDouble(); + var oldDiameter = buffer.ReadDouble(); + var newDiameter = buffer.ReadDouble(); + var speed = buffer.ReadVarLong(); + var portalTeleportBoundary = buffer.ReadVarInt(); + var warningBlocks = buffer.ReadVarInt(); + var warningTime = buffer.ReadVarInt(); + + return new InitializeWorldBorderPacket( + x, + z, + oldDiameter, + newDiameter, + speed, + portalTeleportBoundary, + warningBlocks, + warningTime); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/KeepAlivePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/KeepAlivePacket.cs index bf396b26..80e0f6bf 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/KeepAlivePacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/KeepAlivePacket.cs @@ -1,28 +1,29 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Play; -#pragma warning disable CS1591 -public class KeepAlivePacket : IPacket +/// +/// Keep alive packet +/// +/// The keep-alive ID +public sealed record KeepAlivePacket(long KeepAliveId) : IPacket { - public KeepAlivePacket(long id) - { - KeepAliveId = id; - } - - public long KeepAliveId { get; set; } - public PacketType Type => PacketType.CB_Play_KeepAlive; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_KeepAlive; + /// public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteLong(KeepAliveId); } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { var id = buffer.ReadLong(); return new KeepAlivePacket(id); } } -#pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/LinkEntitiesPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/LinkEntitiesPacket.cs new file mode 100644 index 00000000..3aa0df95 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/LinkEntitiesPacket.cs @@ -0,0 +1,34 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Packet sent when an entity has been leashed to another entity. +/// +/// The attached entity's ID +/// The ID of the entity holding the lead. Set to -1 to detach. +public sealed record LinkEntitiesPacket(int AttachedEntityId, int HoldingEntityId) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_AttachEntity; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteInt(AttachedEntityId); + buffer.WriteInt(HoldingEntityId); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var attachedEntityId = buffer.ReadInt(); + var holdingEntityId = buffer.ReadInt(); + + return new LinkEntitiesPacket(attachedEntityId, holdingEntityId); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/LoginPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/LoginPacket.cs index 92fa1f9d..6f8ecf7a 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/LoginPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/LoginPacket.cs @@ -2,82 +2,66 @@ using MineSharp.Core; using MineSharp.Core.Common; using MineSharp.Core.Geometry; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; using MineSharp.Protocol.Exceptions; namespace MineSharp.Protocol.Packets.Clientbound.Play; -#pragma warning disable CS1591 -public class LoginPacket : IPacket -{ - public LoginPacket(int entityId, - bool isHardcore, - byte gameMode, - sbyte previousGameMode, - string[] dimensionNames, - NbtCompound? registryCodec, - string dimensionType, - string dimensionName, - long hashedSeed, - int maxPlayers, - int viewDistance, - int simulationDistance, - bool reducedDebugInfo, - bool enableRespawnScreen, - bool isDebug, - bool isFlat, - bool hasDeathLocation, - string? deathDimensionName, - Position? deathLocation, - int? portalCooldown, - bool? doLimitedCrafting) - { - EntityId = entityId; - IsHardcore = isHardcore; - GameMode = gameMode; - PreviousGameMode = previousGameMode; - DimensionNames = dimensionNames; - RegistryCodec = registryCodec; - DimensionType = dimensionType; - DimensionName = dimensionName; - HashedSeed = hashedSeed; - MaxPlayers = maxPlayers; - ViewDistance = viewDistance; - SimulationDistance = simulationDistance; - ReducedDebugInfo = reducedDebugInfo; - EnableRespawnScreen = enableRespawnScreen; - IsDebug = isDebug; - IsFlat = isFlat; - HasDeathLocation = hasDeathLocation; - DeathDimensionName = deathDimensionName; - DeathLocation = deathLocation; - PortalCooldown = portalCooldown; - DoLimitedCrafting = doLimitedCrafting; - } - public int EntityId { get; set; } - public bool IsHardcore { get; set; } - public byte GameMode { get; set; } - public sbyte PreviousGameMode { get; set; } - public string[] DimensionNames { get; set; } - public NbtCompound? RegistryCodec { get; set; } - public string DimensionType { get; set; } - public string DimensionName { get; set; } - public long HashedSeed { get; set; } - public int MaxPlayers { get; set; } - public int ViewDistance { get; set; } - public int SimulationDistance { get; set; } - public bool ReducedDebugInfo { get; set; } - public bool EnableRespawnScreen { get; set; } - public bool IsDebug { get; set; } - public bool IsFlat { get; set; } - public bool HasDeathLocation { get; set; } - public string? DeathDimensionName { get; set; } - public Position? DeathLocation { get; set; } - public int? PortalCooldown { get; set; } - public bool? DoLimitedCrafting { get; set; } // since 1.20.2 - public PacketType Type => PacketType.CB_Play_Login; +/// +/// Represents a login packet. +/// +/// The entity ID. +/// Indicates if the game is in hardcore mode. +/// The current game mode. +/// The previous game mode. +/// The names of the dimensions. +/// The registry codec. +/// The type of the dimension. +/// The name of the dimension. +/// The hashed seed. +/// The maximum number of players. +/// The view distance. +/// The simulation distance. +/// Indicates if reduced debug info is enabled. +/// Indicates if the respawn screen is enabled. +/// Indicates if the game is in debug mode. +/// Indicates if the world is flat. +/// Indicates if there is a death location. +/// The name of the death dimension. +/// The death location. +/// The portal cooldown. +/// Indicates if limited crafting is enabled. +public sealed record LoginPacket( + int EntityId, + bool IsHardcore, + byte GameMode, + sbyte PreviousGameMode, + Identifier[] DimensionNames, + NbtCompound? RegistryCodec, + Identifier DimensionType, + Identifier DimensionName, + long HashedSeed, + int MaxPlayers, + int ViewDistance, + int SimulationDistance, + bool ReducedDebugInfo, + bool EnableRespawnScreen, + bool IsDebug, + bool IsFlat, + bool HasDeathLocation, + Identifier? DeathDimensionName, + Position? DeathLocation, + int? PortalCooldown, + bool? DoLimitedCrafting) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_Login; + /// public void Write(PacketBuffer buffer, MinecraftData version) { if (version.Version.Protocol < ProtocolVersion.V_1_19) @@ -94,12 +78,12 @@ public void Write(PacketBuffer buffer, MinecraftData version) buffer.WriteSByte(PreviousGameMode); } - buffer.WriteVarIntArray(DimensionNames, (buf, val) => buf.WriteString(val)); + buffer.WriteVarIntArray(DimensionNames, (buf, val) => buf.WriteIdentifier(val)); if (version.Version.Protocol < ProtocolVersion.V_1_20_2) { - buffer.WriteNbt(RegistryCodec); - buffer.WriteString(DimensionType); - buffer.WriteString(DimensionName); + buffer.WriteOptionalNbt(RegistryCodec); + buffer.WriteIdentifier(DimensionType); + buffer.WriteIdentifier(DimensionName); buffer.WriteLong(HashedSeed); } @@ -111,8 +95,8 @@ public void Write(PacketBuffer buffer, MinecraftData version) if (version.Version.Protocol >= ProtocolVersion.V_1_20_2) { buffer.WriteBool(DoLimitedCrafting!.Value); - buffer.WriteString(DimensionType); - buffer.WriteString(DimensionName); + buffer.WriteIdentifier(DimensionType); + buffer.WriteIdentifier(DimensionName); buffer.WriteLong(HashedSeed); buffer.WriteByte(GameMode); buffer.WriteSByte(PreviousGameMode); @@ -124,8 +108,8 @@ public void Write(PacketBuffer buffer, MinecraftData version) buffer.WriteBool(HasDeathLocation); if (HasDeathLocation) { - buffer.WriteString(DeathDimensionName!); - buffer.WriteULong(DeathLocation!.ToULong()); + buffer.WriteIdentifier(DeathDimensionName ?? throw new InvalidOperationException($"{nameof(DeathDimensionName)} must not be null if {nameof(HasDeathLocation)} is true.")); + buffer.WritePosition(DeathLocation ?? throw new InvalidOperationException($"{nameof(DeathLocation)} must not be null if {nameof(HasDeathLocation)} is true.")); } if (version.Version.Protocol >= ProtocolVersion.V_1_20) @@ -139,6 +123,7 @@ public void Write(PacketBuffer buffer, MinecraftData version) } } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { if (version.Version.Protocol >= ProtocolVersion.V_1_20_2) @@ -150,21 +135,22 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) var isHardcore = buffer.ReadBool(); var gameMode = buffer.ReadByte(); var previousGameMode = buffer.ReadSByte(); - var dimensionNames = buffer.ReadVarIntArray(buf => buf.ReadString()); + var dimensionNames = buffer.ReadVarIntArray(buf => buf.ReadIdentifier()); var registryCodec = buffer.ReadOptionalNbtCompound(); + registryCodec = registryCodec?.NormalizeRegistryDataTopLevelIdentifiers(); - string dimensionType; + Identifier dimensionType; if (version.Version.Protocol < ProtocolVersion.V_1_19) { var dimensionTypeNbt = buffer.ReadNbtCompound(); - dimensionType = dimensionTypeNbt.Get("effects")!.Value; + dimensionType = Identifier.Parse(dimensionTypeNbt.Get("effects")!.Value); } else { - dimensionType = buffer.ReadString(); + dimensionType = buffer.ReadIdentifier(); } - var dimensionName = buffer.ReadString(); + var dimensionName = buffer.ReadIdentifier(); var hashedSeed = buffer.ReadLong(); var maxPlayers = buffer.ReadVarInt(); var viewDistance = buffer.ReadVarInt(); @@ -174,7 +160,7 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) var isDebug = buffer.ReadBool(); var isFlat = buffer.ReadBool(); bool? hasDeathLocation = null; - string? deathDimensionName = null; + Identifier? deathDimensionName = null; Position? deathLocation = null; if (version.Version.Protocol >= ProtocolVersion.V_1_19) @@ -182,8 +168,8 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) hasDeathLocation = buffer.ReadBool(); if (hasDeathLocation.Value) { - deathDimensionName = buffer.ReadString(); - deathLocation = new(buffer.ReadULong()); + deathDimensionName = buffer.ReadIdentifier(); + deathLocation = buffer.ReadPosition(); } } @@ -221,27 +207,27 @@ private static LoginPacket ReadV1_20_2(PacketBuffer buffer, MinecraftData data) { var entityId = buffer.ReadInt(); var isHardcode = buffer.ReadBool(); - var dimensionNames = buffer.ReadVarIntArray(buf => buf.ReadString()); + var dimensionNames = buffer.ReadVarIntArray(buf => buf.ReadIdentifier()); var maxPlayer = buffer.ReadVarInt(); var viewDistance = buffer.ReadVarInt(); var simulationDistance = buffer.ReadVarInt(); var reducedDebugInfo = buffer.ReadBool(); var enableRespawnScreen = buffer.ReadBool(); var doLimitedCrafting = buffer.ReadBool(); - var dimensionType = buffer.ReadString(); - var dimensionName = buffer.ReadString(); + var dimensionType = buffer.ReadIdentifier(); + var dimensionName = buffer.ReadIdentifier(); var hashedSeed = buffer.ReadLong(); var gameMode = buffer.ReadByte(); var previousGameMode = buffer.ReadSByte(); var isDebug = buffer.ReadBool(); var isFlat = buffer.ReadBool(); var hasDeathLocation = buffer.ReadBool(); - string? deathDimensionName = null; + Identifier? deathDimensionName = null; Position? deathLocation = null; if (hasDeathLocation) { - deathDimensionName = buffer.ReadString(); - deathLocation = new(buffer.ReadULong()); + deathDimensionName = buffer.ReadIdentifier(); + deathLocation = buffer.ReadPosition(); } var portalCooldown = buffer.ReadVarInt(); @@ -270,4 +256,3 @@ private static LoginPacket ReadV1_20_2(PacketBuffer buffer, MinecraftData data) doLimitedCrafting); } } -#pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/LookAtPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/LookAtPacket.cs new file mode 100644 index 00000000..23965fb0 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/LookAtPacket.cs @@ -0,0 +1,77 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using static MineSharp.Protocol.Packets.Clientbound.Play.LookAtPacket; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Used to rotate the client player to face the given location or entity. +/// +/// Values are feet=0, eyes=1. If set to eyes, aims using the head position; otherwise aims using the feet position. +/// X coordinate of the point to face towards. +/// Y coordinate of the point to face towards. +/// Z coordinate of the point to face towards. +/// If true, additional information about an entity is provided. +/// The entity to face towards. Only if IsEntity is true. +/// Whether to look at the entity's eyes or feet. Same values and meanings as FeetOrEyes, just for the entity's head/feet. Only if IsEntity is true. +public sealed record LookAtPacket(LookAtPosition FeetOrEyes, double TargetX, double TargetY, double TargetZ, bool IsEntity, int? EntityId, LookAtPosition? EntityFeetOrEyes) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_FacePlayer; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt((int)FeetOrEyes); + buffer.WriteDouble(TargetX); + buffer.WriteDouble(TargetY); + buffer.WriteDouble(TargetZ); + buffer.WriteBool(IsEntity); + + if (IsEntity) + { + buffer.WriteVarInt(EntityId.GetValueOrDefault()); + buffer.WriteVarInt((int)EntityFeetOrEyes.GetValueOrDefault()); + } + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var feetOrEyes = (LookAtPosition)buffer.ReadVarInt(); + var targetX = buffer.ReadDouble(); + var targetY = buffer.ReadDouble(); + var targetZ = buffer.ReadDouble(); + var isEntity = buffer.ReadBool(); + + int? entityId = null; + LookAtPosition? entityFeetOrEyes = null; + + if (isEntity) + { + entityId = buffer.ReadVarInt(); + entityFeetOrEyes = (LookAtPosition)buffer.ReadVarInt(); + } + + return new LookAtPacket( + feetOrEyes, + targetX, targetY, targetZ, + isEntity, + entityId, entityFeetOrEyes); + } + + /// + /// Enum representing the position to look at (feet or eyes). + /// + public enum LookAtPosition + { +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + Feet = 0, + Eyes = 1 +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + } +} + diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/MapDataPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/MapDataPacket.cs new file mode 100644 index 00000000..5a0857bd --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/MapDataPacket.cs @@ -0,0 +1,211 @@ +using MineSharp.ChatComponent; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using static MineSharp.Protocol.Packets.Clientbound.Play.MapDataPacket; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Updates a rectangular area on a map item. +/// +/// Map ID of the map being modified +/// From 0 for a fully zoomed-in map (1 block per pixel) to 4 for a fully zoomed-out map (16 blocks per pixel) +/// True if the map has been locked in a cartography table +/// Indicates if the map has icons +/// Array of icons on the map +/// Details of the color patch update +public sealed record MapDataPacket(int MapId, byte Scale, bool Locked, bool HasIcons, Icon[]? Icons, ColorPatchInfo ColorPatch) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_Map; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(MapId); + buffer.WriteByte(Scale); + buffer.WriteBool(Locked); + buffer.WriteBool(HasIcons); + + if (HasIcons) + { + buffer.WriteVarInt(Icons?.Length ?? 0); + if (Icons != null) + { + foreach (var icon in Icons) + { + icon.Write(buffer); + } + } + } + + ColorPatch.Write(buffer); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var mapId = buffer.ReadVarInt(); + var scale = buffer.ReadByte(); + var locked = buffer.ReadBool(); + var hasIcons = buffer.ReadBool(); + + Icon[]? icons = null; + if (hasIcons) + { + var iconCount = buffer.ReadVarInt(); + icons = new Icon[iconCount]; + for (int i = 0; i < iconCount; i++) + { + icons[i] = Icon.Read(buffer); + } + } + + var colorPatch = ColorPatchInfo.Read(buffer); + + return new MapDataPacket(mapId, scale, locked, hasIcons, icons, colorPatch); + } + + /// + /// Represents an icon on the map. + /// + /// Type of the icon + /// X coordinate of the icon + /// Z coordinate of the icon + /// Direction of the icon + /// Indicates if the icon has a display name + /// Display name of the icon + public sealed record Icon(MapIconType Type, byte X, byte Z, byte Direction, bool HasDisplayName, Chat? DisplayName) : ISerializable + { + /// + public void Write(PacketBuffer buffer) + { + buffer.WriteVarInt((int)Type); + buffer.WriteByte(X); + buffer.WriteByte(Z); + buffer.WriteByte(Direction); + buffer.WriteBool(HasDisplayName); + if (HasDisplayName) + { + buffer.WriteChatComponent(DisplayName!); + } + } + + /// + public static Icon Read(PacketBuffer buffer) + { + var type = (MapIconType)buffer.ReadVarInt(); + var x = buffer.ReadByte(); + var z = buffer.ReadByte(); + var direction = buffer.ReadByte(); + var hasDisplayName = buffer.ReadBool(); + Chat? displayName = null; + if (hasDisplayName) + { + displayName = buffer.ReadChatComponent(); + } + + return new Icon(type, x, z, direction, hasDisplayName, displayName); + } + } + + /// + /// Represents the color patch update on the map. + /// + /// Number of columns updated + /// Number of rows updated + /// X offset of the westernmost column + /// Z offset of the northernmost row + /// Length of the following array + /// Array of updated data + public sealed record ColorPatchInfo(byte Columns, byte? Rows, byte? X, byte? Z, int? Length, byte[]? Data) : ISerializable + { + /// + public void Write(PacketBuffer buffer) + { + buffer.WriteByte(Columns); + if (Columns > 0) + { + buffer.WriteByte(Rows ?? 0); + buffer.WriteByte(X ?? 0); + buffer.WriteByte(Z ?? 0); + buffer.WriteVarInt(Length ?? 0); + if (Data != null) + { + buffer.WriteBytes(Data); + } + } + } + + /// + public static ColorPatchInfo Read(PacketBuffer buffer) + { + var columns = buffer.ReadByte(); + byte? rows = null, x = null, z = null; + int? length = null; + byte[]? data = null; + + if (columns > 0) + { + rows = buffer.ReadByte(); + x = buffer.ReadByte(); + z = buffer.ReadByte(); + length = buffer.ReadVarInt(); + data = buffer.ReadBytes(length.Value); + } + + return new ColorPatchInfo(columns, rows, x, z, length, data); + } + } + + /// + /// Represents the types of icons on a map. + /// + public enum MapIconType + { +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + /// + /// Alternative meaning: players + /// + WhiteArrow = 0, + /// + /// Alternative meaning: item frames + /// + GreenArrow = 1, + RedArrow = 2, + BlueArrow = 3, + WhiteCross = 4, + RedPointer = 5, + /// + /// Alternative meaning: off-map players + /// + WhiteCircle = 6, + /// + /// Alternative meaning: far-off-map players + /// + SmallWhiteCircle = 7, + Mansion = 8, + Monument = 9, + WhiteBanner = 10, + OrangeBanner = 11, + MagentaBanner = 12, + LightBlueBanner = 13, + YellowBanner = 14, + LimeBanner = 15, + PinkBanner = 16, + GrayBanner = 17, + LightGrayBanner = 18, + CyanBanner = 19, + PurpleBanner = 20, + BlueBanner = 21, + BrownBanner = 22, + GreenBanner = 23, + RedBanner = 24, + BlackBanner = 25, + TreasureMarker = 26 +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/MerchantOffersPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/MerchantOffersPacket.cs new file mode 100644 index 00000000..44c0211a --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/MerchantOffersPacket.cs @@ -0,0 +1,108 @@ +using MineSharp.Core.Common; +using MineSharp.Core.Common.Items; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using MineSharp.Protocol.Packets.NetworkTypes; +using static MineSharp.Protocol.Packets.Clientbound.Play.MerchantOffersPacket; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// The list of trades a villager NPC is offering. +/// +/// The ID of the window that is open; this is an int rather than a byte. +/// The number of trades in the following array. +/// The trades offered by the villager. +/// The level of the villager. +/// Total experience for this villager (always 0 for the wandering trader). +/// True if this is a regular villager; false for the wandering trader. +/// True for regular villagers and false for the wandering trader. +public sealed record MerchantOffersPacket(int WindowId, int Size, Trade[] Trades, int VillagerLevel, int Experience, bool IsRegularVillager, bool CanRestock) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_TradeList; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(WindowId); + buffer.WriteVarInt(Size); + foreach (var trade in Trades) + { + trade.Write(buffer, version); + } + buffer.WriteVarInt(VillagerLevel); + buffer.WriteVarInt(Experience); + buffer.WriteBool(IsRegularVillager); + buffer.WriteBool(CanRestock); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var windowId = buffer.ReadVarInt(); + var size = buffer.ReadVarInt(); + var trades = new Trade[size]; + for (int i = 0; i < size; i++) + { + trades[i] = Trade.Read(buffer, version); + } + var villagerLevel = buffer.ReadVarInt(); + var experience = buffer.ReadVarInt(); + var isRegularVillager = buffer.ReadBool(); + var canRestock = buffer.ReadBool(); + + return new MerchantOffersPacket(windowId, size, trades, villagerLevel, experience, isRegularVillager, canRestock); + } + + /// + /// Represents a trade offered by a villager. + /// + /// The first item the player has to supply for this villager trade. + /// The item the player will receive from this villager trade. + /// The second item the player has to supply for this villager trade. May be an empty slot. + /// True if the trade is disabled; false if the trade is enabled. + /// Number of times the trade has been used so far. + /// Number of times this trade can be used before it's exhausted. + /// Amount of XP the villager will earn each time the trade is used. + /// The number added to the price when an item is discounted due to player reputation or other effects. + /// Determines how much demand, player reputation, and temporary effects will adjust the price. + /// If positive, causes the price to increase. Negative values seem to be treated the same as zero. + public sealed record Trade(Item? InputItem1, Item? OutputItem, Item? InputItem2, bool TradeDisabled, int NumberOfTradeUses, int MaximumNumberOfTradeUses, int XP, int SpecialPrice, float PriceMultiplier, int Demand) : ISerializableWithMinecraftData + { + /// + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteOptionalItem(InputItem1); + buffer.WriteOptionalItem(OutputItem); + buffer.WriteOptionalItem(InputItem2); + buffer.WriteBool(TradeDisabled); + buffer.WriteInt(NumberOfTradeUses); + buffer.WriteInt(MaximumNumberOfTradeUses); + buffer.WriteInt(XP); + buffer.WriteInt(SpecialPrice); + buffer.WriteFloat(PriceMultiplier); + buffer.WriteInt(Demand); + } + + /// + public static Trade Read(PacketBuffer buffer, MinecraftData data) + { + var inputItem1 = buffer.ReadOptionalItem(data); + var outputItem = buffer.ReadOptionalItem(data); + var inputItem2 = buffer.ReadOptionalItem(data); + var tradeDisabled = buffer.ReadBool(); + var numberOfTradeUses = buffer.ReadInt(); + var maximumNumberOfTradeUses = buffer.ReadInt(); + var xp = buffer.ReadInt(); + var specialPrice = buffer.ReadInt(); + var priceMultiplier = buffer.ReadFloat(); + var demand = buffer.ReadInt(); + + return new Trade(inputItem1, outputItem, inputItem2, tradeDisabled, numberOfTradeUses, maximumNumberOfTradeUses, xp, specialPrice, priceMultiplier, demand); + } + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/MoveVehiclePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/MoveVehiclePacket.cs new file mode 100644 index 00000000..d05510d4 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/MoveVehiclePacket.cs @@ -0,0 +1,43 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Move Vehicle packet +/// +/// Absolute position (X coordinate) +/// Absolute position (Y coordinate) +/// Absolute position (Z coordinate) +/// Absolute rotation on the vertical axis, in degrees +/// Absolute rotation on the horizontal axis, in degrees +public sealed record MoveVehiclePacket(double X, double Y, double Z, float Yaw, float Pitch) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_VehicleMove; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteDouble(X); + buffer.WriteDouble(Y); + buffer.WriteDouble(Z); + buffer.WriteFloat(Yaw); + buffer.WriteFloat(Pitch); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var x = buffer.ReadDouble(); + var y = buffer.ReadDouble(); + var z = buffer.ReadDouble(); + var yaw = buffer.ReadFloat(); + var pitch = buffer.ReadFloat(); + + return new MoveVehiclePacket(x, y, z, yaw, pitch); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/MultiBlockUpdatePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/MultiBlockUpdatePacket.cs index 95da38fb..d3e9e3d5 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/MultiBlockUpdatePacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/MultiBlockUpdatePacket.cs @@ -1,13 +1,26 @@ using MineSharp.Core; -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; using MineSharp.Protocol.Exceptions; namespace MineSharp.Protocol.Packets.Clientbound.Play; #pragma warning disable CS1591 -public class MultiBlockUpdatePacket : IPacket +public sealed record MultiBlockUpdatePacket : IPacket { + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_MultiBlockChange; + + // Here is no non-argument constructor allowed + // Do not use +#pragma warning disable CS8618 + private MultiBlockUpdatePacket() +#pragma warning restore CS8618 + { + } + /// /// Constructor for before 1.20 /// @@ -33,10 +46,9 @@ public MultiBlockUpdatePacket(long chunkSection, long[] blocks) Blocks = blocks; } - public long ChunkSection { get; set; } - public bool? SuppressLightUpdates { get; set; } - public long[] Blocks { get; set; } - public PacketType Type => PacketType.CB_Play_MultiBlockChange; + public long ChunkSection { get; init; } + public bool? SuppressLightUpdates { get; init; } + public long[] Blocks { get; init; } public void Write(PacketBuffer buffer, MinecraftData version) { diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/OpenBookPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/OpenBookPacket.cs new file mode 100644 index 00000000..7896533a --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/OpenBookPacket.cs @@ -0,0 +1,31 @@ +using MineSharp.Core.Common; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Sent when a player right clicks with a signed book. This tells the client to open the book GUI. +/// +/// The hand used to open the book. +public sealed record OpenBookPacket(PlayerHand Hand) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_OpenBook; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt((int)Hand); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var hand = (PlayerHand)buffer.ReadVarInt(); + return new OpenBookPacket(hand); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/OpenHorseScreenPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/OpenHorseScreenPacket.cs new file mode 100644 index 00000000..b828788e --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/OpenHorseScreenPacket.cs @@ -0,0 +1,40 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Open Horse Screen packet +/// +/// The window ID +/// The number of slots in the horse inventory +/// The entity ID of the horse +public sealed record OpenHorseScreenPacket(byte WindowId, int SlotCount, int EntityId) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_OpenHorseWindow; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteByte(WindowId); + buffer.WriteVarInt(SlotCount); + buffer.WriteInt(EntityId); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var windowId = buffer.ReadByte(); + var slotCount = buffer.ReadVarInt(); + var entityId = buffer.ReadInt(); + + return new OpenHorseScreenPacket( + windowId, + slotCount, + entityId); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/OpenSignEditorPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/OpenSignEditorPacket.cs new file mode 100644 index 00000000..04543ad4 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/OpenSignEditorPacket.cs @@ -0,0 +1,36 @@ +using MineSharp.Core.Geometry; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Sent when the client has placed a sign and is allowed to send Update Sign. +/// There must already be a sign at the given location. +/// +/// The position of the sign +/// Whether the opened editor is for the front or on the back of the sign +public sealed record OpenSignEditorPacket(Position Location, bool IsFrontText) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_OpenSignEntity; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WritePosition(Location); + buffer.WriteBool(IsFrontText); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var location = buffer.ReadPosition(); + var isFrontText = buffer.ReadBool(); + + return new OpenSignEditorPacket(location, isFrontText); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/OpenWindowPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/OpenWindowPacket.cs index 0c36bd7f..270bf6be 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/OpenWindowPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/OpenWindowPacket.cs @@ -1,35 +1,31 @@ using MineSharp.ChatComponent; using MineSharp.Core; -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Play; -#pragma warning disable CS1591 -public class OpenWindowPacket : IPacket + +/// +/// Represents a packet to open a window in the client. +/// +/// The ID of the window. +/// The type of the inventory. +/// The title of the window. +/// The chat component of the window title. +public sealed record OpenWindowPacket(int WindowId, int InventoryType, string WindowTitle, Chat? WindowTitleChat = null) : IPacket { - public OpenWindowPacket(int windowId, int inventoryType, string windowTitle) - { - WindowId = windowId; - InventoryType = inventoryType; - WindowTitle = windowTitle; - } + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_OpenWindow; public OpenWindowPacket(int windowId, int inventoryType, Chat windowTitle) + : this(windowId, inventoryType, windowTitle.GetMessage(null), windowTitle) { - WindowId = windowId; - InventoryType = inventoryType; - WindowTitleChat = windowTitle; - WindowTitle = windowTitle.GetMessage(null); } - public int WindowId { get; set; } - public int InventoryType { get; set; } - public string WindowTitle { get; set; } - - public Chat? WindowTitleChat { get; set; } - public PacketType Type => PacketType.CB_Play_OpenWindow; - + /// public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteVarInt(WindowId); @@ -37,6 +33,7 @@ public void Write(PacketBuffer buffer, MinecraftData version) buffer.WriteString(WindowTitle); } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { if (version.Version.Protocol == ProtocolVersion.V_1_20_3) @@ -54,4 +51,3 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) buffer.ReadString()); } } -#pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/ParticlePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/ParticlePacket.cs new file mode 100644 index 00000000..fdb2b210 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/ParticlePacket.cs @@ -0,0 +1,77 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using MineSharp.Protocol.Packets.NetworkTypes; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Represents a packet for particle effects in the game. +/// +/// The ID of the particle. +/// Indicates if the particle is long distance. +/// The X coordinate of the particle. +/// The Y coordinate of the particle. +/// The Z coordinate of the particle. +/// The X offset of the particle. +/// The Y offset of the particle. +/// The Z offset of the particle. +/// The maximum speed of the particle. +/// The number of particles. +/// +/// Data depends on the particle id. +/// Will be an empty buffer for most particles. +/// +public sealed record ParticlePacket( + int ParticleId, + bool LongDistance, + double X, + double Y, + double Z, + float OffsetX, + float OffsetY, + float OffsetZ, + float MaxSpeed, + int ParticleCount, + IParticleData? Data +) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_WorldParticles; + + /// + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteVarInt(ParticleId); + buffer.WriteBool(LongDistance); + buffer.WriteDouble(X); + buffer.WriteDouble(Y); + buffer.WriteDouble(Z); + buffer.WriteFloat(OffsetX); + buffer.WriteFloat(OffsetY); + buffer.WriteFloat(OffsetZ); + buffer.WriteFloat(MaxSpeed); + buffer.WriteVarInt(ParticleCount); + Data?.Write(buffer, data); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData data) + { + var particleId = buffer.ReadVarInt(); + var longDistance = buffer.ReadBool(); + var x = buffer.ReadDouble(); + var y = buffer.ReadDouble(); + var z = buffer.ReadDouble(); + var offsetX = buffer.ReadFloat(); + var offsetY = buffer.ReadFloat(); + var offsetZ = buffer.ReadFloat(); + var maxSpeed = buffer.ReadFloat(); + var particleCount = buffer.ReadVarInt(); + var particleData = ParticleDataRegistry.Read(buffer, data, particleId); + + return new ParticlePacket(particleId, longDistance, x, y, z, offsetX, offsetY, offsetZ, maxSpeed, particleCount, particleData); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/PickupItemPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/PickupItemPacket.cs new file mode 100644 index 00000000..602531ab --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/PickupItemPacket.cs @@ -0,0 +1,41 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Sent by the server when someone picks up an item lying on the ground. +/// Its sole purpose appears to be the animation of the item flying towards you. +/// +/// The ID of the collected entity +/// The ID of the collector entity +/// The number of items picked up. Seems to be 1 for XP orbs, otherwise the number of items in the stack. +public sealed record PickupItemPacket(int CollectedEntityId, int CollectorEntityId, int PickupItemCount) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_Collect; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(CollectedEntityId); + buffer.WriteVarInt(CollectorEntityId); + buffer.WriteVarInt(PickupItemCount); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var collectedEntityId = buffer.ReadVarInt(); + var collectorEntityId = buffer.ReadVarInt(); + var pickupItemCount = buffer.ReadVarInt(); + + return new PickupItemPacket( + collectedEntityId, + collectorEntityId, + pickupItemCount); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/PingPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/PingPacket.cs index 77d6bc6a..a173e792 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/PingPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/PingPacket.cs @@ -1,4 +1,4 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; @@ -7,24 +7,13 @@ namespace MineSharp.Protocol.Packets.Clientbound.Play; /// /// Ping Packet https://wiki.vg/Protocol#Ping_.28play.29 /// -public class PingPacket : IPacket +/// The id of the ping +public sealed record PingPacket(int Id) : IPacket { - /// - /// Create a new instance - /// - /// - public PingPacket(int id) - { - Id = id; - } - - /// - /// The id of the ping - /// - public int Id { get; set; } - /// - public PacketType Type => PacketType.CB_Play_Ping; + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_Ping; /// public void Write(PacketBuffer buffer, MinecraftData version) @@ -35,7 +24,6 @@ public void Write(PacketBuffer buffer, MinecraftData version) /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { - return new PingPacket( - buffer.ReadInt()); + return new PingPacket(buffer.ReadInt()); } } diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/PingResponsePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/PingResponsePacket.cs new file mode 100644 index 00000000..f337cac6 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/PingResponsePacket.cs @@ -0,0 +1,31 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Ping response packet +/// +/// The payload, should be the same as sent by the client +public sealed record PingResponsePacket(long Payload) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_PingResponse; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteLong(Payload); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var payload = buffer.ReadLong(); + + return new PingResponsePacket(payload); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/PlaceGhostRecipePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/PlaceGhostRecipePacket.cs new file mode 100644 index 00000000..36cb9279 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/PlaceGhostRecipePacket.cs @@ -0,0 +1,35 @@ +using MineSharp.Core.Common; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Response to the serverbound packet (Place Recipe), with the same recipe ID. Appears to be used to notify the UI. +/// +/// The window ID +/// A recipe ID +public sealed record PlaceGhostRecipePacket(byte WindowId, Identifier Recipe) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_CraftRecipeResponse; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteByte(WindowId); + buffer.WriteIdentifier(Recipe); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var windowId = buffer.ReadByte(); + var recipe = buffer.ReadIdentifier(); + + return new PlaceGhostRecipePacket(windowId, recipe); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/PlayerAbilitiesPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/PlayerAbilitiesPacket.cs new file mode 100644 index 00000000..02f2dfdc --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/PlayerAbilitiesPacket.cs @@ -0,0 +1,38 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using MineSharp.Protocol.Packets.NetworkTypes; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Player abilities packet sent by the server to update the player's abilities. +/// +/// Bit field indicating various abilities. +/// The flying speed of the player. +/// Modifies the field of view, like a speed potion. +public sealed record PlayerAbilitiesPacket(PlayerAbilitiesFlags Flags, float FlyingSpeed, float FieldOfViewModifier) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_Abilities; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteSByte((sbyte)Flags); + buffer.WriteFloat(FlyingSpeed); + buffer.WriteFloat(FieldOfViewModifier); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var flags = (PlayerAbilitiesFlags)buffer.ReadSByte(); + var flyingSpeed = buffer.ReadFloat(); + var fieldOfViewModifier = buffer.ReadFloat(); + + return new PlayerAbilitiesPacket(flags, flyingSpeed, fieldOfViewModifier); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/PlayerChatPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/PlayerChatPacket.cs index 2cb50ad8..60d14ab1 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/PlayerChatPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/PlayerChatPacket.cs @@ -1,21 +1,20 @@ using MineSharp.ChatComponent; using MineSharp.Core; using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; using MineSharp.Protocol.Packets.NetworkTypes; +using static MineSharp.Protocol.Packets.Clientbound.Play.PlayerChatPacket; namespace MineSharp.Protocol.Packets.Clientbound.Play; #pragma warning disable CS1591 -public class PlayerChatPacket : IPacket +public sealed record PlayerChatPacket(IChatMessageBody Body) : IPacket { - public PlayerChatPacket(IChatMessageBody body) - { - Body = body; - } - - public IChatMessageBody Body { get; set; } - public PacketType Type => PacketType.CB_Play_PlayerChat; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_PlayerChat; public void Write(PacketBuffer buffer, MinecraftData version) { @@ -42,33 +41,31 @@ public interface IChatMessageBody void Write(PacketBuffer buffer, MinecraftData version); } - public class V119Body : IChatMessageBody + /// + /// Represents the body of a chat message in version 1.19. + /// + /// The signed chat message. + /// The unsigned chat message, if available. + /// The type of the message. + /// The UUID of the sender. + /// The name of the sender. + /// The name of the sender's team, if available. + /// The timestamp of the message. + /// The salt value for the message. + /// The signature of the message. + public sealed record V119Body( + Chat SignedChat, + Chat? UnsignedChat, + int MessageType, + Uuid Sender, + Chat SenderName, + Chat? SenderTeamName, + long Timestamp, + long Salt, + byte[] Signature + ) : IChatMessageBody { - public V119Body(Chat signedChat, Chat? unsignedChat, int messageType, Uuid sender, Chat senderName, - Chat? senderTeamName, - long timestamp, long salt, byte[] signature) - { - SignedChat = signedChat; - UnsignedChat = unsignedChat; - MessageType = messageType; - Sender = sender; - SenderName = senderName; - SenderTeamName = senderTeamName; - Timestamp = timestamp; - Salt = salt; - Signature = signature; - } - - public Chat SignedChat { get; set; } - public Chat? UnsignedChat { get; set; } - public int MessageType { get; set; } - public Uuid Sender { get; set; } - public Chat SenderName { get; set; } - public Chat? SenderTeamName { get; set; } - public long Timestamp { get; set; } - public long Salt { get; set; } - public byte[] Signature { get; set; } - + /// public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteChatComponent(SignedChat); @@ -84,6 +81,7 @@ public void Write(PacketBuffer buffer, MinecraftData version) buffer.WriteUuid(Sender); var hasTeamName = SenderTeamName != null; + buffer.WriteBool(hasTeamName); if (hasTeamName) { buffer.WriteChatComponent(SenderTeamName!); @@ -95,6 +93,11 @@ public void Write(PacketBuffer buffer, MinecraftData version) buffer.WriteBytes(Signature); } + /// + /// Reads a from the given . + /// + /// The buffer to read from. + /// The read . public static V119Body Read(PacketBuffer buffer) { var signedChat = buffer.ReadChatComponent(); @@ -122,13 +125,20 @@ public static V119Body Read(PacketBuffer buffer) var signature = new byte[buffer.ReadVarInt()]; buffer.ReadBytes(signature); - return new(signedChat, unsignedChat, messageType, sender, senderName, senderTeamName, timestamp, salt, - signature); + return new V119Body(signedChat, unsignedChat, messageType, sender, senderName, senderTeamName, timestamp, salt, signature); } } - public class V11923Body : IChatMessageBody + public sealed record V11923Body : IChatMessageBody { + // Here is no non-argument constructor allowed + // Do not use +#pragma warning disable CS8618 + private V11923Body() +#pragma warning restore CS8618 + { + } + private V11923Body(byte[]? previousSignature, Uuid sender, int? index, byte[]? signature, string plainMessage, Chat? formattedMessage, long timestamp, long salt, ChatMessageItem[] previousMessages, Chat? unsignedContent, int filterType, long[]? filterTypeMask, int type, Chat networkName, @@ -236,21 +246,21 @@ public V11923Body( NetworkTargetName = networkTargetName; } - public byte[]? PreviousSignature { get; set; } - public Uuid Sender { get; set; } - public int? Index { get; set; } - public byte[]? Signature { get; set; } - public string PlainMessage { get; set; } - public Chat? FormattedMessage { get; set; } - public long Timestamp { get; set; } - public long Salt { get; set; } - public ChatMessageItem[] PreviousMessages { get; set; } - public Chat? UnsignedContent { get; set; } - public int FilterType { get; set; } - public long[]? FilterTypeMask { get; set; } - public int Type { get; set; } - public Chat NetworkName { get; set; } - public Chat? NetworkTargetName { get; set; } + public byte[]? PreviousSignature { get; init; } + public Uuid Sender { get; init; } + public int? Index { get; init; } + public byte[]? Signature { get; init; } + public string PlainMessage { get; init; } + public Chat? FormattedMessage { get; init; } + public long Timestamp { get; init; } + public long Salt { get; init; } + public ChatMessageItem[] PreviousMessages { get; init; } + public Chat? UnsignedContent { get; init; } + public int FilterType { get; init; } + public long[]? FilterTypeMask { get; init; } + public int Type { get; init; } + public Chat NetworkName { get; init; } + public Chat? NetworkTargetName { get; init; } public void Write(PacketBuffer buffer, MinecraftData version) { @@ -296,7 +306,7 @@ public void Write(PacketBuffer buffer, MinecraftData version) buffer.WriteLong(Timestamp); buffer.WriteLong(Salt); - buffer.WriteVarIntArray(PreviousMessages, (buf, val) => val.Write(buf, version)); + buffer.WriteVarIntArray(PreviousMessages, (buf, val) => val.Write(buf)); var hasUnsignedContent = UnsignedContent != null; buffer.WriteBool(hasUnsignedContent); @@ -383,7 +393,7 @@ public static V11923Body Read(PacketBuffer buffer, MinecraftData version) timestamp = buffer.ReadLong(); salt = buffer.ReadLong(); - previousMessages = buffer.ReadVarIntArray(buff => ChatMessageItem.Read(buff, version)); + previousMessages = buffer.ReadVarIntArray(ChatMessageItem.Read); var hasUnsignedContent = buffer.ReadBool(); unsignedContent = null; diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/PlayerInfoRemovePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/PlayerInfoRemovePacket.cs index 33b973e0..76dba485 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/PlayerInfoRemovePacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/PlayerInfoRemovePacket.cs @@ -1,18 +1,16 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Play; #pragma warning disable CS1591 -public class PlayerInfoRemovePacket : IPacket +public sealed record PlayerInfoRemovePacket(Uuid[] Players) : IPacket { - public PlayerInfoRemovePacket(Uuid[] players) - { - Players = players; - } - - public Uuid[] Players { get; set; } - public PacketType Type => PacketType.CB_Play_PlayerRemove; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_PlayerRemove; public void Write(PacketBuffer buffer, MinecraftData version) { diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/PlayerInfoUpdatePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/PlayerInfoUpdatePacket.cs index f360725e..627e425b 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/PlayerInfoUpdatePacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/PlayerInfoUpdatePacket.cs @@ -1,23 +1,22 @@ -using MineSharp.Core; +using System.Collections.Frozen; +using MineSharp.Core; using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; +using static MineSharp.Protocol.Packets.Clientbound.Play.PlayerInfoUpdatePacket; +using static MineSharp.Protocol.Packets.Clientbound.Play.PlayerInfoUpdatePacket.AddPlayerAction; +using static MineSharp.Protocol.Packets.Clientbound.Play.PlayerInfoUpdatePacket.InitializeChatAction; namespace MineSharp.Protocol.Packets.Clientbound.Play; #pragma warning disable CS1591 -public class PlayerInfoUpdatePacket : IPacket +public sealed record PlayerInfoUpdatePacket(int Action, ActionEntry[] Data) : IPacket { - public PlayerInfoUpdatePacket(int action, ActionEntry[] data) - { - Action = action; - Data = data; - } - - public int Action { get; set; } - - public ActionEntry[] Data { get; set; } - public PacketType Type => PacketType.CB_Play_PlayerInfo; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_PlayerInfo; public void Write(PacketBuffer buffer, MinecraftData version) { @@ -53,17 +52,8 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) return new PlayerInfoUpdatePacket(action, data); } - public class ActionEntry + public sealed record ActionEntry(Uuid Player, IPlayerInfoAction[] Actions) { - public ActionEntry(Uuid player, IPlayerInfoAction[] actions) - { - Player = player; - Actions = actions; - } - - public Uuid Player { get; set; } - public IPlayerInfoAction[] Actions { get; set; } - public void Write(PacketBuffer buffer) { buffer.WriteUuid(Player); @@ -86,13 +76,6 @@ public static ActionEntry Read(PacketBuffer buffer, MinecraftData version, int a actions.Add(UpdateGameModeAction.Read(buffer)); actions.Add(UpdateLatencyAction.Read(buffer)); actions.Add(UpdateDisplayName.Read(buffer)); - - if (ProtocolVersion.IsBetween(version.Version.Protocol, ProtocolVersion.V_1_19, - ProtocolVersion.V_1_19_2)) - { - actions.Add(CryptoActionV19.Read(buffer)); - } - break; case 1: actions.Add(UpdateGameModeAction.Read(buffer)); @@ -110,57 +93,69 @@ public static ActionEntry Read(PacketBuffer buffer, MinecraftData version, int a } else { - if ((action & 0x01) != 0) + foreach (var (mask, actionFactory) in ActionRegistry.ActionFactories) { - actions.Add(AddPlayerAction.Read(buffer)); + if ((action & mask) != 0) + { + actions.Add(actionFactory(buffer)); + } } + } - if ((action & 0x02) != 0) - { - actions.Add(InitializeChatAction.Read(buffer)); - } + return new(uuid, actions.ToArray()); + } + } - if ((action & 0x04) != 0) - { - actions.Add(UpdateGameModeAction.Read(buffer)); - } + public static class ActionRegistry + { + public static readonly FrozenDictionary> ActionFactories; - if ((action & 0x08) != 0) - { - actions.Add(UpdateListedAction.Read(buffer)); - } + static ActionRegistry() + { + ActionFactories = InitializeActions(); + } - if ((action & 0x10) != 0) - { - actions.Add(UpdateLatencyAction.Read(buffer)); - } + private static FrozenDictionary> InitializeActions() + { + var dict = new Dictionary>(); - if ((action & 0x20) != 0) - { - actions.Add(UpdateDisplayName.Read(buffer)); - } + void Register() + where T : IPlayerInfoAction, IPlayerInfoActionStatic + { + var mask = T.StaticMask; + var factory = T.Read; + dict.Add(mask, factory); } - return new(uuid, actions.ToArray()); + Register(); + Register(); + Register(); + Register(); + Register(); + Register(); + + return dict.ToFrozenDictionary(); } } - public interface IPlayerInfoAction { + public int Mask { get; } + public void Write(PacketBuffer buffer); } - public class AddPlayerAction : IPlayerInfoAction + public interface IPlayerInfoActionStatic { - public AddPlayerAction(string name, Property[] properties) - { - Name = name; - Properties = properties; - } + public static abstract int StaticMask { get; } + + public static abstract IPlayerInfoAction Read(PacketBuffer buffer); + } - public string Name { get; set; } - public Property[] Properties { get; set; } + public sealed record AddPlayerAction(string Name, Property[] Properties) : IPlayerInfoAction, IPlayerInfoActionStatic + { + public static int StaticMask => 0x01; + public int Mask => StaticMask; public void Write(PacketBuffer buffer) { @@ -175,19 +170,13 @@ public static AddPlayerAction Read(PacketBuffer buffer) return new(name, properties); } - public class Property + static IPlayerInfoAction IPlayerInfoActionStatic.Read(PacketBuffer buffer) { - public Property(string name, string value, string? signature) - { - Name = name; - Value = value; - Signature = signature; - } - - public string Name { get; set; } - public string Value { get; set; } - public string? Signature { get; set; } + return Read(buffer); + } + public sealed record Property(string Name, string Value, string? Signature) + { public void Write(PacketBuffer buffer) { buffer.WriteString(Name); @@ -217,14 +206,10 @@ public static Property Read(PacketBuffer buffer) } } - public class UpdateGameModeAction : IPlayerInfoAction + public sealed record UpdateGameModeAction(GameMode GameMode) : IPlayerInfoAction, IPlayerInfoActionStatic { - public UpdateGameModeAction(GameMode gameMode) - { - GameMode = gameMode; - } - - public GameMode GameMode { get; set; } + public static int StaticMask => 0x04; + public int Mask => StaticMask; public void Write(PacketBuffer buffer) { @@ -236,16 +221,17 @@ public static UpdateGameModeAction Read(PacketBuffer buffer) var gameMode = (GameMode)buffer.ReadVarInt(); return new(gameMode); } - } - public class UpdateListedAction : IPlayerInfoAction - { - public UpdateListedAction(bool listed) + static IPlayerInfoAction IPlayerInfoActionStatic.Read(PacketBuffer buffer) { - Listed = listed; + return Read(buffer); } + } - public bool Listed { get; set; } + public sealed record UpdateListedAction(bool Listed) : IPlayerInfoAction, IPlayerInfoActionStatic + { + public static int StaticMask => 0x08; + public int Mask => StaticMask; public void Write(PacketBuffer buffer) { @@ -257,16 +243,17 @@ public static UpdateListedAction Read(PacketBuffer buffer) var listed = buffer.ReadBool(); return new(listed); } - } - public class UpdateLatencyAction : IPlayerInfoAction - { - public UpdateLatencyAction(int ping) + static IPlayerInfoAction IPlayerInfoActionStatic.Read(PacketBuffer buffer) { - Ping = ping; + return Read(buffer); } + } - public int Ping { get; set; } + public sealed record UpdateLatencyAction(int Ping) : IPlayerInfoAction, IPlayerInfoActionStatic + { + public static int StaticMask => 0x10; + public int Mask => StaticMask; public void Write(PacketBuffer buffer) { @@ -278,16 +265,17 @@ public static UpdateLatencyAction Read(PacketBuffer buffer) var ping = buffer.ReadVarInt(); return new(ping); } - } - public class UpdateDisplayName : IPlayerInfoAction - { - public UpdateDisplayName(string? displayName) + static IPlayerInfoAction IPlayerInfoActionStatic.Read(PacketBuffer buffer) { - DisplayName = displayName; + return Read(buffer); } + } - public string? DisplayName { get; set; } + public sealed record UpdateDisplayName(string? DisplayName) : IPlayerInfoAction, IPlayerInfoActionStatic + { + public static int StaticMask => 0x20; + public int Mask => StaticMask; public void Write(PacketBuffer buffer) { @@ -310,46 +298,41 @@ public static UpdateDisplayName Read(PacketBuffer buffer) return new(displayName); } - } - public class InitializeChatAction : IPlayerInfoAction - { - public InitializeChatAction() + static IPlayerInfoAction IPlayerInfoActionStatic.Read(PacketBuffer buffer) { - Present = false; + return Read(buffer); } + } - public InitializeChatAction(Uuid chatSessionId, long publicKeyExpiryTime, byte[] encodedPublicKey, - byte[] publicKeySignature) - { - Present = true; - ChatSessionId = chatSessionId; - PublicKeyExpiryTime = publicKeyExpiryTime; - EncodedPublicKey = encodedPublicKey; - PublicKeySignature = publicKeySignature; - } + public sealed record InitializeChatAction(InitializeChatActionData? Data) : IPlayerInfoAction, IPlayerInfoActionStatic + { + public static int StaticMask => 0x02; + public int Mask => StaticMask; - public bool Present { get; set; } - public Uuid? ChatSessionId { get; set; } - public long? PublicKeyExpiryTime { get; set; } - public byte[]? EncodedPublicKey { get; set; } - public byte[]? PublicKeySignature { get; set; } + public sealed record InitializeChatActionData( + Uuid ChatSessionId, + long PublicKeyExpiryTime, + byte[] EncodedPublicKey, + byte[] PublicKeySignature + ); public void Write(PacketBuffer buffer) { - buffer.WriteBool(Present); + var present = Data != null; + buffer.WriteBool(present); - if (!Present) + if (!present) { return; } - buffer.WriteUuid(ChatSessionId!.Value); - buffer.WriteLong(PublicKeyExpiryTime!.Value); - buffer.WriteVarInt(EncodedPublicKey!.Length); - buffer.WriteBytes(EncodedPublicKey); - buffer.WriteVarInt(PublicKeySignature!.Length); - buffer.WriteBytes(PublicKeySignature); + buffer.WriteUuid(Data!.ChatSessionId); + buffer.WriteLong(Data!.PublicKeyExpiryTime); + buffer.WriteVarInt(Data!.EncodedPublicKey.Length); + buffer.WriteBytes(Data!.EncodedPublicKey); + buffer.WriteVarInt(Data!.PublicKeySignature.Length); + buffer.WriteBytes(Data!.PublicKeySignature); } public static InitializeChatAction Read(PacketBuffer buffer) @@ -357,7 +340,7 @@ public static InitializeChatAction Read(PacketBuffer buffer) var present = buffer.ReadBool(); if (!present) { - return new(); + return new InitializeChatAction((InitializeChatActionData?)null); } var chatSessionId = buffer.ReadUuid(); @@ -367,62 +350,12 @@ public static InitializeChatAction Read(PacketBuffer buffer) var publicKeySignature = new byte[buffer.ReadVarInt()]; buffer.ReadBytes(publicKeySignature); - return new(chatSessionId, publicKeyExpiryTime, encodedPublicKey, publicKeySignature); - } - } - - public class CryptoActionV19 : IPlayerInfoAction - { - public CryptoActionV19() - { - Present = false; - } - - public CryptoActionV19(Uuid chatSessionId, long timestamp, byte[] encodedPublicKey, byte[] publicKeySignature) - { - Present = true; - Timestamp = timestamp; - EncodedPublicKey = encodedPublicKey; - PublicKeySignature = publicKeySignature; - } - - public bool Present { get; set; } - public long? Timestamp { get; set; } - public byte[]? EncodedPublicKey { get; set; } - public byte[]? PublicKeySignature { get; set; } - - public void Write(PacketBuffer buffer) - { - buffer.WriteBool(Present); - - if (!Present) - { - return; - } - - buffer.WriteLong(Timestamp!.Value); - buffer.WriteVarInt(EncodedPublicKey!.Length); - buffer.WriteBytes(EncodedPublicKey); - buffer.WriteVarInt(PublicKeySignature!.Length); - buffer.WriteBytes(PublicKeySignature); + return new InitializeChatAction(new InitializeChatActionData(chatSessionId, publicKeyExpiryTime, encodedPublicKey, publicKeySignature)); } - public static InitializeChatAction Read(PacketBuffer buffer) + static IPlayerInfoAction IPlayerInfoActionStatic.Read(PacketBuffer buffer) { - var present = buffer.ReadBool(); - if (!present) - { - return new(); - } - - var chatSessionId = buffer.ReadUuid(); - var publicKeyExpiryTime = buffer.ReadLong(); - var encodedPublicKey = new byte[buffer.ReadVarInt()]; - buffer.ReadBytes(encodedPublicKey); - var publicKeySignature = new byte[buffer.ReadVarInt()]; - buffer.ReadBytes(publicKeySignature); - - return new(chatSessionId, publicKeyExpiryTime, encodedPublicKey, publicKeySignature); + return Read(buffer); } } } diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/PlayerPositionPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/PlayerPositionPacket.cs index 3aaaff76..8502b454 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/PlayerPositionPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/PlayerPositionPacket.cs @@ -1,13 +1,37 @@ using MineSharp.Core; -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; using MineSharp.Protocol.Exceptions; namespace MineSharp.Protocol.Packets.Clientbound.Play; #pragma warning disable CS1591 -public class PlayerPositionPacket : IPacket +public sealed record PlayerPositionPacket : IPacket { + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_Position; + + // Here is no non-argument constructor allowed + // Do not use + private PlayerPositionPacket() + { + } + + private PlayerPositionPacket(double x, double y, double z, float yaw, float pitch, PositionFlags flags, int teleportId, + bool? dismountVehicle) + { + X = x; + Y = y; + Z = z; + Yaw = yaw; + Pitch = pitch; + Flags = flags; + TeleportId = teleportId; + DismountVehicle = dismountVehicle; + } + /// /// Constructor for 1.18.x-1.19.3 /// @@ -19,17 +43,10 @@ public class PlayerPositionPacket : IPacket /// /// /// - public PlayerPositionPacket(double x, double y, double z, float yaw, float pitch, sbyte flags, int teleportId, + public PlayerPositionPacket(double x, double y, double z, float yaw, float pitch, PositionFlags flags, int teleportId, bool dismountVehicle) + : this(x, y, z, yaw, pitch, flags, teleportId, (bool?)dismountVehicle) { - X = x; - Y = y; - Z = z; - Yaw = yaw; - Pitch = pitch; - Flags = flags; - TeleportId = teleportId; - DismountVehicle = dismountVehicle; } /// @@ -42,27 +59,19 @@ public PlayerPositionPacket(double x, double y, double z, float yaw, float pitch /// /// /// - public PlayerPositionPacket(double x, double y, double z, float yaw, float pitch, sbyte flags, int teleportId) + public PlayerPositionPacket(double x, double y, double z, float yaw, float pitch, PositionFlags flags, int teleportId) + : this(x, y, z, yaw, pitch, flags, teleportId, null) { - X = x; - Y = y; - Z = z; - Yaw = yaw; - Pitch = pitch; - Flags = flags; - TeleportId = teleportId; - DismountVehicle = null; } - public double X { get; set; } - public double Y { get; set; } - public double Z { get; set; } - public float Yaw { get; set; } - public float Pitch { get; set; } - public sbyte Flags { get; set; } - public int TeleportId { get; set; } - public bool? DismountVehicle { get; set; } - public PacketType Type => PacketType.CB_Play_Position; + public double X { get; init; } + public double Y { get; init; } + public double Z { get; init; } + public float Yaw { get; init; } + public float Pitch { get; init; } + public PositionFlags Flags { get; init; } + public int TeleportId { get; init; } + public bool? DismountVehicle { get; init; } public void Write(PacketBuffer buffer, MinecraftData version) { @@ -71,7 +80,7 @@ public void Write(PacketBuffer buffer, MinecraftData version) buffer.WriteDouble(Z); buffer.WriteFloat(Yaw); buffer.WriteFloat(Pitch); - buffer.WriteSByte(Flags); + buffer.WriteSByte((sbyte)Flags); buffer.WriteVarInt(TeleportId); if (version.Version.Protocol >= ProtocolVersion.V_1_19_4) @@ -94,7 +103,7 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) var z = buffer.ReadDouble(); var yaw = buffer.ReadFloat(); var pitch = buffer.ReadFloat(); - var flags = buffer.ReadSByte(); + var flags = (PositionFlags)buffer.ReadSByte(); var teleportId = buffer.ReadVarInt(); if (version.Version.Protocol >= ProtocolVersion.V_1_19_4) @@ -105,5 +114,15 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) var dismountVehicle = buffer.ReadBool(); return new PlayerPositionPacket(x, y, z, yaw, pitch, flags, teleportId, dismountVehicle); } + + [Flags] + public enum PositionFlags : sbyte + { + X = 0x01, + Y = 0x02, + Z = 0x04, + YRot = 0x08, + XRot = 0x10 + } } #pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/PluginMessagePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/PluginMessagePacket.cs new file mode 100644 index 00000000..06099bf1 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/PluginMessagePacket.cs @@ -0,0 +1,35 @@ +using MineSharp.Core.Common; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Clientbound Plugin Message packet +/// +/// Name of the plugin channel used to send the data +/// Any data, depending on the channel +public sealed record PluginMessagePacket(Identifier Channel, byte[] Data) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_CustomPayload; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteIdentifier(Channel); + buffer.WriteBytes(Data); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var channel = buffer.ReadObject(); + var data = buffer.RestBuffer(); + + return new PluginMessagePacket(channel, data); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/RemoveEntitiesPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/RemoveEntitiesPacket.cs index 051d129d..196f01e4 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/RemoveEntitiesPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/RemoveEntitiesPacket.cs @@ -1,18 +1,15 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Play; #pragma warning disable CS1591 -public class RemoveEntitiesPacket : IPacket +public sealed record RemoveEntitiesPacket(int[] EntityIds) : IPacket { - public RemoveEntitiesPacket(int[] entityIds) - { - EntityIds = entityIds; - } - - public int[] EntityIds { get; set; } - public PacketType Type => PacketType.CB_Play_EntityDestroy; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_EntityDestroy; public void Write(PacketBuffer buffer, MinecraftData version) { diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/RemoveEntityEffectPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/RemoveEntityEffectPacket.cs new file mode 100644 index 00000000..1dab7f86 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/RemoveEntityEffectPacket.cs @@ -0,0 +1,34 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Remove Entity Effect packet +/// +/// The entity ID +/// The effect ID +public sealed record RemoveEntityEffectPacket(int EntityId, int EffectId) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_RemoveEntityEffect; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(EntityId); + buffer.WriteVarInt(EffectId); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var entityId = buffer.ReadVarInt(); + var effectId = buffer.ReadVarInt(); + + return new RemoveEntityEffectPacket(entityId, effectId); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/RemoveResourcePackPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/RemoveResourcePackPacket.cs new file mode 100644 index 00000000..8298275d --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/RemoveResourcePackPacket.cs @@ -0,0 +1,38 @@ +using MineSharp.Core.Common; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Packet sent by the server to remove a resource pack. +/// +/// The UUID of the resource pack to be removed. +public sealed record RemoveResourcePackPacket(Uuid? Uuid) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_RemoveResourcePack; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + var hasUuid = Uuid != null; + buffer.WriteBool(hasUuid); + if (hasUuid) + { + buffer.WriteUuid(Uuid!.Value); + } + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var hasUuid = buffer.ReadBool(); + Uuid? uuid = hasUuid ? buffer.ReadUuid() : null; + + return new RemoveResourcePackPacket(uuid); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/ResetScorePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/ResetScorePacket.cs new file mode 100644 index 00000000..09d308ec --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/ResetScorePacket.cs @@ -0,0 +1,44 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Reset Score packet +/// +/// The entity whose score this is. For players, this is their username; for other entities, it is their UUID. +/// Whether the score should be removed for the specified objective, or for all of them. +/// The name of the objective the score belongs to. Only present if the previous field is true. +public sealed record ResetScorePacket(string EntityName, bool HasObjectiveName, string? ObjectiveName) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_ResetScore; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteString(EntityName); + buffer.WriteBool(HasObjectiveName); + if (HasObjectiveName) + { + buffer.WriteString(ObjectiveName!); + } + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var entityName = buffer.ReadString(); + var hasObjectiveName = buffer.ReadBool(); + string? objectiveName = null; + if (hasObjectiveName) + { + objectiveName = buffer.ReadString(); + } + + return new ResetScorePacket(entityName, hasObjectiveName, objectiveName); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/RespawnPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/RespawnPacket.cs index b228be93..ef4f020b 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/RespawnPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/RespawnPacket.cs @@ -2,46 +2,31 @@ using MineSharp.Core; using MineSharp.Core.Common; using MineSharp.Core.Geometry; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Play; #pragma warning disable CS1591 -public class RespawnPacket : IPacket +public sealed record RespawnPacket( + Identifier DimensionType, + Identifier DimensionName, + long HashedSeed, + byte GameMode, + sbyte PreviousGameMode, + bool IsDebug, + bool IsFlat, + bool CopyMetadata, + bool? HasDeathLocation, + Identifier? DeathDimensionName, + Position? DeathLocation, + int? PortalCooldown +) : IPacket { - public RespawnPacket(string dimension, string dimensionName, long hashedSeed, sbyte gameMode, byte previousGameMode, - bool isDebug, - bool isFlat, bool copyMetadata, bool? hasDeathLocation, string? deathDimensionName, - Position? deathLocation, - int? portalCooldown) - { - Dimension = dimension; - DimensionName = dimensionName; - HashedSeed = hashedSeed; - GameMode = gameMode; - PreviousGameMode = previousGameMode; - IsDebug = isDebug; - IsFlat = isFlat; - CopyMetadata = copyMetadata; - HasDeathLocation = hasDeathLocation; - DeathDimensionName = deathDimensionName; - DeathLocation = deathLocation; - PortalCooldown = portalCooldown; - } - - public string Dimension { get; set; } - public string DimensionName { get; set; } - public long HashedSeed { get; set; } - public sbyte GameMode { get; set; } - public byte PreviousGameMode { get; set; } - public bool IsDebug { get; set; } - public bool IsFlat { get; set; } - public bool CopyMetadata { get; set; } - public bool? HasDeathLocation { get; set; } - public string? DeathDimensionName { get; set; } - public Position? DeathLocation { get; set; } - public int? PortalCooldown { get; set; } - public PacketType Type => PacketType.CB_Play_Respawn; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_Respawn; public void Write(PacketBuffer buffer, MinecraftData version) { @@ -51,11 +36,11 @@ public void Write(PacketBuffer buffer, MinecraftData version) $"{nameof(RespawnPacket)}.Write() is not supported for versions before 1.19."); } - buffer.WriteString(Dimension); - buffer.WriteString(DimensionName); + buffer.WriteIdentifier(DimensionType); + buffer.WriteIdentifier(DimensionName); buffer.WriteLong(HashedSeed); - buffer.WriteSByte(GameMode); - buffer.WriteByte(PreviousGameMode); + buffer.WriteByte(GameMode); + buffer.WriteSByte(PreviousGameMode); buffer.WriteBool(IsDebug); buffer.WriteBool(IsFlat); buffer.WriteBool(CopyMetadata); @@ -65,52 +50,50 @@ public void Write(PacketBuffer buffer, MinecraftData version) buffer.WriteBool(HasDeathLocation.Value); } - if (!HasDeathLocation ?? false) + if ((HasDeathLocation ?? false)) { - return; + buffer.WriteIdentifier(DeathDimensionName ?? throw new InvalidOperationException($"{nameof(DeathDimensionName)} must not be null if {nameof(HasDeathLocation)} is true.")); + buffer.WritePosition(DeathLocation ?? throw new InvalidOperationException($"{nameof(DeathLocation)} must not be null if {nameof(HasDeathLocation)} is true.")); } - buffer.WriteString(DeathDimensionName!); - buffer.WriteULong(DeathLocation!.ToULong()); - if (version.Version.Protocol >= ProtocolVersion.V_1_20) { - buffer.WriteVarInt(PortalCooldown!.Value); + buffer.WriteVarInt(PortalCooldown ?? 0); } } public static IPacket Read(PacketBuffer buffer, MinecraftData version) { - string dimension; + Identifier dimensionType; if (version.Version.Protocol <= ProtocolVersion.V_1_19) { var dimensionNbt = buffer.ReadNbtCompound(); - dimension = dimensionNbt.Get("effects")!.Value; + dimensionType = Identifier.Parse(dimensionNbt.Get("effects")!.Value); } else { - dimension = buffer.ReadString(); + dimensionType = buffer.ReadIdentifier(); } - var dimensionName = buffer.ReadString(); + var dimensionName = buffer.ReadIdentifier(); var hashedSeed = buffer.ReadLong(); - var gameMode = buffer.ReadSByte(); - var previousGameMode = buffer.ReadByte(); + var gameMode = buffer.ReadByte(); + var previousGameMode = buffer.ReadSByte(); var isDebug = buffer.ReadBool(); var isFlat = buffer.ReadBool(); var copyMetadata = buffer.ReadBool(); bool? hasDeathLocation = null; - string? deathDimensionName = null; + Identifier? deathDimensionName = null; Position? deathLocation = null; if (version.Version.Protocol >= ProtocolVersion.V_1_19) { hasDeathLocation = buffer.ReadBool(); - if (hasDeathLocation ?? false) + if (hasDeathLocation.Value) { - deathDimensionName = buffer.ReadString(); - deathLocation = new(buffer.ReadULong()); + deathDimensionName = buffer.ReadIdentifier(); + deathLocation = buffer.ReadPosition(); } } @@ -120,8 +103,11 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) portalCooldown = buffer.ReadVarInt(); } + // Here is still something left in the buffer, but I can not figure out what it is. + // wiki.vg says something different for every version. But it does not match for the exact version I am using (1.20.4). + return new RespawnPacket( - dimension, + dimensionType, dimensionName, hashedSeed, gameMode, diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SelectAdvancementTabPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SelectAdvancementTabPacket.cs new file mode 100644 index 00000000..fdf64eef --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SelectAdvancementTabPacket.cs @@ -0,0 +1,52 @@ +using System.Collections.Frozen; +using MineSharp.Core.Common; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Sent by the server to indicate that the client should switch advancement tab. +/// Sent either when the client switches tab in the GUI or when an advancement in another tab is made. +/// +/// The identifier of the advancement tab. If no or an invalid identifier is sent, the client will switch to the first tab in the GUI. +public sealed record SelectAdvancementTabPacket(Identifier? Identifier) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_SelectAdvancementTab; + + /// + /// Set of all possible identifiers. + /// + public static readonly FrozenSet PossibleIdentifiers = new HashSet() + { + Identifier.Parse("minecraft:story/root"), + Identifier.Parse("minecraft:nether/root"), + Identifier.Parse("minecraft:end/root"), + Identifier.Parse("minecraft:adventure/root"), + Identifier.Parse("minecraft:husbandry/root") + }.ToFrozenSet(); + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + var hasId = Identifier is not null; + buffer.WriteBool(hasId); + if (hasId) + { + buffer.WriteIdentifier(Identifier!); + } + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var hasId = buffer.ReadBool(); + Identifier? identifier = hasId ? buffer.ReadIdentifier() : null; + + return new SelectAdvancementTabPacket(identifier); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/ServerDataPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/ServerDataPacket.cs new file mode 100644 index 00000000..d5ff4f51 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/ServerDataPacket.cs @@ -0,0 +1,51 @@ +using MineSharp.ChatComponent; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Sent by the server to provide server data such as MOTD, icon, and secure chat enforcement. +/// +/// The message of the day (MOTD) as a chat component. +/// Indicates if the server has an icon. +/// Optional icon bytes in the PNG format. +/// Indicates if the server enforces secure chat. +public sealed record ServerDataPacket(Chat Motd, bool HasIcon, byte[]? Icon, bool EnforcesSecureChat) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_ServerData; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteChatComponent(Motd); + buffer.WriteBool(HasIcon); + if (HasIcon && Icon is not null) + { + buffer.WriteVarInt(Icon.Length); + buffer.WriteBytes(Icon); + } + buffer.WriteBool(EnforcesSecureChat); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var motd = buffer.ReadChatComponent(); + var hasIcon = buffer.ReadBool(); + byte[]? icon = null; + if (hasIcon) + { + // TODO: Is the Size of the icon byte array always present? + var size = buffer.ReadVarInt(); + icon = buffer.ReadBytes(size); + } + var enforcesSecureChat = buffer.ReadBool(); + + return new ServerDataPacket(motd, hasIcon, icon, enforcesSecureChat); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetActionBarTextPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetActionBarTextPacket.cs new file mode 100644 index 00000000..95218632 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetActionBarTextPacket.cs @@ -0,0 +1,32 @@ +using MineSharp.ChatComponent; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Displays a message above the hotbar. Equivalent to System Chat Message with Overlay set to true, +/// except that chat message blocking isn't performed. Used by the Notchian server only to implement the /title command. +/// +/// The text to display in the action bar +public sealed record SetActionBarTextPacket(Chat ActionBarText) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_ActionBar; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteChatComponent(ActionBarText); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var actionBarText = buffer.ReadChatComponent(); + return new SetActionBarTextPacket(actionBarText); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetBlockDestroyStagePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetBlockDestroyStagePacket.cs new file mode 100644 index 00000000..3c930b61 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetBlockDestroyStagePacket.cs @@ -0,0 +1,41 @@ +using MineSharp.Core.Geometry; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Set Block Destroy Stage packet +/// +/// The ID of the entity breaking the block +/// Block Position +/// 0–9 to set it, any other value to remove it +public sealed record SetBlockDestroyStagePacket(int EntityId, Position Location, byte DestroyStage) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_BlockBreakAnimation; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(EntityId); + buffer.WritePosition(Location); + buffer.WriteByte(DestroyStage); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var entityId = buffer.ReadVarInt(); + var location = buffer.ReadPosition(); + var destroyStage = buffer.ReadByte(); + + return new SetBlockDestroyStagePacket( + entityId, + location, + destroyStage); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetBorderCenterPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetBorderCenterPacket.cs new file mode 100644 index 00000000..439cafe9 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetBorderCenterPacket.cs @@ -0,0 +1,34 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Packet sent by the server to set the world border center. +/// +/// The X coordinate of the world border center. +/// The Z coordinate of the world border center. +public sealed record SetBorderCenterPacket(double X, double Z) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_WorldBorderCenter; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteDouble(X); + buffer.WriteDouble(Z); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var x = buffer.ReadDouble(); + var z = buffer.ReadDouble(); + + return new SetBorderCenterPacket(x, z); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetBorderLerpSizePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetBorderLerpSizePacket.cs new file mode 100644 index 00000000..cc153246 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetBorderLerpSizePacket.cs @@ -0,0 +1,37 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Packet sent by the server to set the world border lerp size. +/// +/// Current length of a single side of the world border, in meters. +/// Target length of a single side of the world border, in meters. +/// Number of real-time milliseconds until New Diameter is reached. +public sealed record SetBorderLerpSizePacket(double OldDiameter, double NewDiameter, long Speed) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_WorldBorderLerpSize; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteDouble(OldDiameter); + buffer.WriteDouble(NewDiameter); + buffer.WriteVarLong(Speed); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var oldDiameter = buffer.ReadDouble(); + var newDiameter = buffer.ReadDouble(); + var speed = buffer.ReadVarLong(); + + return new SetBorderLerpSizePacket(oldDiameter, newDiameter, speed); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetBorderSizePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetBorderSizePacket.cs new file mode 100644 index 00000000..dd068c1b --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetBorderSizePacket.cs @@ -0,0 +1,30 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Packet sent by the server to set the world border size. +/// +/// Length of a single side of the world border, in meters. +public sealed record SetBorderSizePacket(double Diameter) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_WorldBorderSize; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteDouble(Diameter); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var diameter = buffer.ReadDouble(); + return new SetBorderSizePacket(diameter); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetBorderWarningDelayPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetBorderWarningDelayPacket.cs new file mode 100644 index 00000000..49a6dc56 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetBorderWarningDelayPacket.cs @@ -0,0 +1,30 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Packet sent by the server to set the border warning delay. +/// +/// The warning time in seconds as set by /worldborder warning time. +public sealed record SetBorderWarningDelayPacket(int WarningTime) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_WorldBorderWarningDelay; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(WarningTime); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var warningTime = buffer.ReadVarInt(); + return new SetBorderWarningDelayPacket(warningTime); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetBorderWarningDistancePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetBorderWarningDistancePacket.cs new file mode 100644 index 00000000..e1c87f37 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetBorderWarningDistancePacket.cs @@ -0,0 +1,30 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Packet sent by the server to set the border warning distance. +/// +/// The warning distance in meters. +public sealed record SetBorderWarningDistancePacket(int WarningBlocks) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_WorldBorderWarningReach; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(WarningBlocks); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var warningBlocks = buffer.ReadVarInt(); + return new SetBorderWarningDistancePacket(warningBlocks); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetCameraPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetCameraPacket.cs new file mode 100644 index 00000000..6c914e6b --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetCameraPacket.cs @@ -0,0 +1,30 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Sets the entity that the player renders from. This is normally used when the player left-clicks an entity while in spectator mode. +/// +/// ID of the entity to set the client's camera to. +public sealed record SetCameraPacket(int CameraId) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_Camera; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(CameraId); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var cameraId = buffer.ReadVarInt(); + return new SetCameraPacket(cameraId); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetCenterChunkPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetCenterChunkPacket.cs new file mode 100644 index 00000000..21e1afdd --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetCenterChunkPacket.cs @@ -0,0 +1,34 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Sets the center position of the client's chunk loading area. +/// +/// Chunk X coordinate of the loading area center. +/// Chunk Z coordinate of the loading area center. +public sealed record SetCenterChunkPacket(int ChunkX, int ChunkZ) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_UpdateViewPosition; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(ChunkX); + buffer.WriteVarInt(ChunkZ); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var chunkX = buffer.ReadVarInt(); + var chunkZ = buffer.ReadVarInt(); + + return new SetCenterChunkPacket(chunkX, chunkZ); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetContainerPropertyPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetContainerPropertyPacket.cs new file mode 100644 index 00000000..f6493133 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetContainerPropertyPacket.cs @@ -0,0 +1,42 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Set Container Property packet +/// +/// The window ID +/// The property to be updated +/// The new value for the property +public sealed record SetContainerPropertyPacket(byte WindowId, short Property, short Value) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_CraftProgressBar; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteByte(WindowId); + buffer.WriteShort(Property); + buffer.WriteShort(Value); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var windowId = buffer.ReadByte(); + var property = buffer.ReadShort(); + var value = buffer.ReadShort(); + + return new SetContainerPropertyPacket(windowId, property, value); + } + + // TODO: Add all the meanings of the properties + // depends on the type of container but we only get the window ID here + // so we need static methods that have the container type as a parameter + // https://wiki.vg/index.php?title=Protocol&oldid=19208#Set_Container_Content +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetCooldownPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetCooldownPacket.cs new file mode 100644 index 00000000..4454c8dc --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetCooldownPacket.cs @@ -0,0 +1,34 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Applies a cooldown period to all items with the given type. +/// +/// Numeric ID of the item to apply a cooldown to. +/// Number of ticks to apply a cooldown for, or 0 to clear the cooldown. +public sealed record SetCooldownPacket(int ItemId, int CooldownTicks) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_SetCooldown; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(ItemId); + buffer.WriteVarInt(CooldownTicks); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var itemId = buffer.ReadVarInt(); + var cooldownTicks = buffer.ReadVarInt(); + + return new SetCooldownPacket(itemId, cooldownTicks); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetDefaultSpawnPositionPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetDefaultSpawnPositionPacket.cs new file mode 100644 index 00000000..769b8894 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetDefaultSpawnPositionPacket.cs @@ -0,0 +1,35 @@ +using MineSharp.Core.Geometry; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Set Default Spawn Position packet +/// +/// The spawn location +/// The angle at which to respawn at +public sealed record SetDefaultSpawnPositionPacket(Position Location, float Angle) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_SpawnPosition; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WritePosition(Location); + buffer.WriteFloat(Angle); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var location = buffer.ReadPosition(); + var angle = buffer.ReadFloat(); + + return new SetDefaultSpawnPositionPacket(location, angle); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetEntityMetadataPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetEntityMetadataPacket.cs new file mode 100644 index 00000000..c0dbdaaf --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetEntityMetadataPacket.cs @@ -0,0 +1,36 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using MineSharp.Protocol.Packets.NetworkTypes; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Updates one or more metadata properties for an existing entity. +/// Any properties not included in the Metadata field are left unchanged. +/// +/// The entity ID +/// The entity metadata +public sealed record SetEntityMetadataPacket(int EntityId, EntityMetadata Metadata) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_EntityMetadata; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(EntityId); + Metadata.Write(buffer, version); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var entityId = buffer.ReadVarInt(); + var metadata = EntityMetadata.Read(buffer, version); + + return new SetEntityMetadataPacket(entityId, metadata); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetEntityVelocityPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetEntityVelocityPacket.cs index 45a0cf98..845d6468 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetEntityVelocityPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetEntityVelocityPacket.cs @@ -1,24 +1,15 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Play; #pragma warning disable CS1591 -public class SetEntityVelocityPacket : IPacket +public sealed record SetEntityVelocityPacket(int EntityId, short VelocityX, short VelocityY, short VelocityZ) : IPacket { - public SetEntityVelocityPacket(int entityId, short velocityX, short velocityY, short velocityZ) - { - EntityId = entityId; - VelocityX = velocityX; - VelocityY = velocityY; - VelocityZ = velocityZ; - } - - public int EntityId { get; set; } - public short VelocityX { get; set; } - public short VelocityY { get; set; } - public short VelocityZ { get; set; } - public PacketType Type => PacketType.CB_Play_EntityVelocity; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_EntityVelocity; public void Write(PacketBuffer buffer, MinecraftData version) { diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetEquipmentPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetEquipmentPacket.cs new file mode 100644 index 00000000..f00ceaf4 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetEquipmentPacket.cs @@ -0,0 +1,111 @@ +using MineSharp.Core.Common; +using MineSharp.Core.Common.Items; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using MineSharp.Protocol.Packets.NetworkTypes; +using static MineSharp.Protocol.Packets.Clientbound.Play.SetEquipmentPacket; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Set Equipment packet +/// +/// The entity ID +/// The equipment list +public sealed record SetEquipmentPacket(int EntityId, EquipmentEntry[] Equipment) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_EntityEquipment; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(EntityId); + for (int i = 0; i < Equipment.Length; i++) + { + var entry = Equipment[i]; + var isLastEntry = i == Equipment.Length - 1; + entry.Write(buffer, version, isLastEntry); + } + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var entityId = buffer.ReadVarInt(); + var equipment = new List(); + while (true) + { + var slot = buffer.Peek(); + // wiki.vg says: "has the top bit set if another entry follows, and otherwise unset if this is the last item in the array" + var isLast = (slot & 0x80) == 0; + equipment.Add(EquipmentEntry.Read(buffer, version)); + if (isLast) + { + break; + } + } + + return new SetEquipmentPacket(entityId, equipment.ToArray()); + } + + /// + /// Equipment entry + /// + /// The equipment slot + /// The item in the slot + public sealed record EquipmentEntry(EquipmentSlot SlotId, Item? Item) : ISerializableWithMinecraftData + { + /// + [Obsolete("Use the Write method with the isLastEntry parameter instead")] + public void Write(PacketBuffer buffer, MinecraftData data) + { + Write(buffer, data, true); + } + + /// + /// Writes the equipment entry to the packet buffer. + /// This method is required because the slotId needs to be changed when its not the last entry + /// + /// The packet buffer to write to. + /// The Minecraft data. + /// Indicates if this is the last entry in the equipment list. + public void Write(PacketBuffer buffer, MinecraftData data, bool isLastEntry) + { + var slotId = (byte)SlotId; + if (!isLastEntry) + { + // wiki.vg says: "has the top bit set if another entry follows, and otherwise unset if this is the last item in the array" + slotId |= 0x80; // Set the top bit if another entry follows + } + buffer.WriteByte(slotId); + buffer.WriteOptionalItem(Item); + } + + /// + public static EquipmentEntry Read(PacketBuffer buffer, MinecraftData data) + { + var slotId = (EquipmentSlot)(buffer.ReadByte() & 0x7F); // Unset the top bit + var item = buffer.ReadOptionalItem(data); + return new EquipmentEntry(slotId, item); + } + } + + /// + /// Equipment slot enumeration + /// + public enum EquipmentSlot : byte + { +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + MainHand = 0, + OffHand = 1, + Boots = 2, + Leggings = 3, + Chestplate = 4, + Helmet = 5 +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetExperiencePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetExperiencePacket.cs new file mode 100644 index 00000000..d21ee6fa --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetExperiencePacket.cs @@ -0,0 +1,37 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Set Experience packet +/// +/// The experience bar value between 0 and 1 +/// The experience level +/// The total experience points +public sealed record SetExperiencePacket(float ExperienceBar, int Level, int TotalExperience) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_Experience; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteFloat(ExperienceBar); + buffer.WriteVarInt(Level); + buffer.WriteVarInt(TotalExperience); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var experienceBar = buffer.ReadFloat(); + var level = buffer.ReadVarInt(); + var totalExperience = buffer.ReadVarInt(); + + return new SetExperiencePacket(experienceBar, level, totalExperience); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetHeadRotationPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetHeadRotationPacket.cs new file mode 100644 index 00000000..9a37b78f --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetHeadRotationPacket.cs @@ -0,0 +1,34 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Changes the direction an entity's head is facing. +/// +/// The entity ID +/// New angle, not a delta +public sealed record SetHeadRotationPacket(int EntityId, byte HeadYaw) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_EntityHeadRotation; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(EntityId); + buffer.WriteByte(HeadYaw); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var entityId = buffer.ReadVarInt(); + var headYaw = buffer.ReadByte(); + + return new SetHeadRotationPacket(entityId, headYaw); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetHealthPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetHealthPacket.cs index 0b2b9cd1..528ac20a 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetHealthPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetHealthPacket.cs @@ -1,22 +1,15 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Play; #pragma warning disable CS1591 -public class SetHealthPacket : IPacket +public sealed record SetHealthPacket(float Health, int Food, float Saturation) : IPacket { - public SetHealthPacket(float health, int food, float saturation) - { - Health = health; - Food = food; - Saturation = saturation; - } - - public float Health { get; set; } - public int Food { get; set; } - public float Saturation { get; set; } - public PacketType Type => PacketType.CB_Play_UpdateHealth; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_UpdateHealth; public void Write(PacketBuffer buffer, MinecraftData version) { diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetHeldItemPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetHeldItemPacket.cs index 1b08e458..064a6120 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetHeldItemPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetHeldItemPacket.cs @@ -1,18 +1,15 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Play; #pragma warning disable CS1591 -public class SetHeldItemPacket : IPacket +public sealed record SetHeldItemPacket(sbyte Slot) : IPacket { - public SetHeldItemPacket(sbyte slot) - { - Slot = slot; - } - - public sbyte Slot { get; set; } - public PacketType Type => PacketType.CB_Play_HeldItemSlot; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_HeldItemSlot; public void Write(PacketBuffer buffer, MinecraftData version) { diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetPassengersPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetPassengersPacket.cs index 6be834cc..0337868e 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetPassengersPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetPassengersPacket.cs @@ -1,22 +1,16 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Play; #pragma warning disable CS1591 -public class SetPassengersPacket : IPacket +public sealed record SetPassengersPacket(int EntityId, int[] PassengersId) : IPacket { - public SetPassengersPacket(int entityId, int[] passengersId) - { - EntityId = entityId; - PassengersId = passengersId; - } - - public int EntityId { get; set; } - - public int[] PassengersId { get; set; } - public PacketType Type => PacketType.CB_Play_SetPassengers; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_SetPassengers; public void Write(PacketBuffer buffer, MinecraftData version) { diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetRenderDistancePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetRenderDistancePacket.cs new file mode 100644 index 00000000..34b6740b --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetRenderDistancePacket.cs @@ -0,0 +1,31 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Sent by the integrated singleplayer server when changing render distance. +/// This packet is sent by the server when the client reappears in the overworld after leaving the end. +/// +/// Render distance (2-32). +public sealed record SetRenderDistancePacket(int ViewDistance) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_UpdateViewDistance; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(ViewDistance); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var viewDistance = buffer.ReadVarInt(); + return new SetRenderDistancePacket(viewDistance); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetSimulationDistancePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetSimulationDistancePacket.cs new file mode 100644 index 00000000..4e2fd85e --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetSimulationDistancePacket.cs @@ -0,0 +1,30 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Set Simulation Distance packet +/// +/// The distance that the client will process specific things, such as entities. +public sealed record SetSimulationDistancePacket(int SimulationDistance) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_SimulationDistance; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(SimulationDistance); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var simulationDistance = buffer.ReadVarInt(); + return new SetSimulationDistancePacket(simulationDistance); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetSubtitleTextPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetSubtitleTextPacket.cs new file mode 100644 index 00000000..94d49e3f --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetSubtitleTextPacket.cs @@ -0,0 +1,31 @@ +using MineSharp.ChatComponent; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Packet sent by the server to set the subtitle text. +/// +/// The subtitle text to be displayed. +public sealed record SetSubtitleTextPacket(Chat SubtitleText) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_SetTitleSubtitle; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteChatComponent(SubtitleText); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var subtitleText = buffer.ReadChatComponent(); + return new SetSubtitleTextPacket(subtitleText); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetTabListHeaderFooterPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetTabListHeaderFooterPacket.cs new file mode 100644 index 00000000..af6c78af --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetTabListHeaderFooterPacket.cs @@ -0,0 +1,35 @@ +using MineSharp.ChatComponent; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Set Tab List Header And Footer packet +/// +/// The header text component +/// The footer text component +public sealed record SetTabListHeaderFooterPacket(Chat Header, Chat Footer) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_PlayerlistHeader; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteChatComponent(Header); + buffer.WriteChatComponent(Footer); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var header = buffer.ReadChatComponent(); + var footer = buffer.ReadChatComponent(); + + return new SetTabListHeaderFooterPacket(header, footer); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetTickingStatePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetTickingStatePacket.cs new file mode 100644 index 00000000..9230384c --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetTickingStatePacket.cs @@ -0,0 +1,34 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Packet used to adjust the ticking rate of the client, and whether it's frozen. +/// +/// The tick rate of the client +/// Whether the client is frozen +public sealed record SetTickingStatePacket(float TickRate, bool IsFrozen) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_SetTickingState; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteFloat(TickRate); + buffer.WriteBool(IsFrozen); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var tickRate = buffer.ReadFloat(); + var isFrozen = buffer.ReadBool(); + + return new SetTickingStatePacket(tickRate, isFrozen); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetTitleAnimationTimesPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetTitleAnimationTimesPacket.cs new file mode 100644 index 00000000..5d3ccaef --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetTitleAnimationTimesPacket.cs @@ -0,0 +1,37 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Packet sent by the server to set the title animation times. +/// +/// Ticks to spend fading in. +/// Ticks to keep the title displayed. +/// Ticks to spend fading out, not when to start fading out. +public sealed record SetTitleAnimationTimesPacket(int FadeIn, int Stay, int FadeOut) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_SetTitleTime; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteInt(FadeIn); + buffer.WriteInt(Stay); + buffer.WriteInt(FadeOut); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var fadeIn = buffer.ReadInt(); + var stay = buffer.ReadInt(); + var fadeOut = buffer.ReadInt(); + + return new SetTitleAnimationTimesPacket(fadeIn, stay, fadeOut); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetTitleTextPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetTitleTextPacket.cs new file mode 100644 index 00000000..281b3db9 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SetTitleTextPacket.cs @@ -0,0 +1,31 @@ +using MineSharp.ChatComponent; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Packet sent by the server to set the title text in the client. +/// +/// The title text to be displayed +public sealed record SetTitleTextPacket(Chat TitleText) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_SetTitleText; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteChatComponent(TitleText); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var titleText = buffer.ReadChatComponent(); + return new SetTitleTextPacket(titleText); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SoundEffectPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SoundEffectPacket.cs new file mode 100644 index 00000000..c3e20998 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SoundEffectPacket.cs @@ -0,0 +1,76 @@ +using MineSharp.Core.Common; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; +#pragma warning disable CS1591 +public sealed record SoundEffectPacket( + int SoundId, + Identifier? SoundName, + bool? HasFixedRange, + float? Range, + int SoundCategory, + int EffectPositionX, + int EffectPositionY, + int EffectPositionZ, + float Volume, + float Pitch, + long Seed +) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_SoundEffect; + + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(SoundId); + if (SoundId == 0) + { + buffer.WriteIdentifier(SoundName!); + buffer.WriteBool(HasFixedRange!.Value); + if (HasFixedRange.Value) + { + buffer.WriteFloat(Range!.Value); + } + } + buffer.WriteVarInt(SoundCategory); + buffer.WriteInt(EffectPositionX); + buffer.WriteInt(EffectPositionY); + buffer.WriteInt(EffectPositionZ); + buffer.WriteFloat(Volume); + buffer.WriteFloat(Pitch); + buffer.WriteLong(Seed); + } + + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var soundId = buffer.ReadVarInt(); + Identifier? soundName = null; + bool? hasFixedRange = null; + float? range = null; + + if (soundId == 0) + { + soundName = buffer.ReadIdentifier(); + hasFixedRange = buffer.ReadBool(); + if (hasFixedRange.Value) + { + range = buffer.ReadFloat(); + } + } + + var soundCategory = buffer.ReadVarInt(); + var effectPositionX = buffer.ReadInt(); + var effectPositionY = buffer.ReadInt(); + var effectPositionZ = buffer.ReadInt(); + var volume = buffer.ReadFloat(); + var pitch = buffer.ReadFloat(); + var seed = buffer.ReadLong(); + + return new SoundEffectPacket(soundId, soundName, hasFixedRange, range, soundCategory, effectPositionX, effectPositionY, effectPositionZ, volume, pitch, seed); + } +} +#pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SpawnEntityPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SpawnEntityPacket.cs index e0a1e05c..f3a5c0c9 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SpawnEntityPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SpawnEntityPacket.cs @@ -1,51 +1,69 @@ using MineSharp.Core; using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Play; #pragma warning disable CS1591 -public class SpawnEntityPacket : IPacket +public sealed record SpawnEntityPacket( + int EntityId, + Uuid ObjectUuid, + int EntityType, + double X, + double Y, + double Z, + sbyte Pitch, + sbyte Yaw, + sbyte HeadPitch, + int ObjectData, + short VelocityX, + short VelocityY, + short VelocityZ +) : IPacket { - public SpawnEntityPacket(int entityId, Uuid objectUuid, int entityType, double x, double y, double z, sbyte pitch, - sbyte yaw, - sbyte headPitch, int objectData, short velocityX, short velocityY, short velocityZ) - { - EntityId = entityId; - ObjectUuid = objectUuid; - EntityType = entityType; - X = x; - Y = y; - Z = z; - Pitch = pitch; - Yaw = yaw; - HeadPitch = headPitch; - ObjectData = objectData; - VelocityX = velocityX; - VelocityY = velocityY; - VelocityZ = velocityZ; - } - - public int EntityId { get; set; } - public Uuid ObjectUuid { get; set; } - public int EntityType { get; set; } - public double X { get; set; } - public double Y { get; set; } - public double Z { get; set; } - public sbyte Pitch { get; set; } - public sbyte Yaw { get; set; } - public sbyte HeadPitch { get; set; } - public int ObjectData { get; set; } - public short VelocityX { get; set; } - public short VelocityY { get; set; } - public short VelocityZ { get; set; } - public PacketType Type => PacketType.CB_Play_SpawnEntity; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_SpawnEntity; + /// + /// Writes the packet data to the buffer. + /// + /// The buffer to write to. + /// The Minecraft version. public void Write(PacketBuffer buffer, MinecraftData version) { - throw new NotImplementedException(); + buffer.WriteVarInt(EntityId); + buffer.WriteUuid(ObjectUuid); + buffer.WriteVarInt(EntityType); + buffer.WriteDouble(X); + buffer.WriteDouble(Y); + buffer.WriteDouble(Z); + buffer.WriteSByte(Pitch); + buffer.WriteSByte(Yaw); + + if (version.Version.Protocol >= ProtocolVersion.V_1_19) + { + buffer.WriteSByte(HeadPitch); + buffer.WriteVarInt(ObjectData); + } + else + { + buffer.WriteInt(ObjectData); + } + + buffer.WriteShort(VelocityX); + buffer.WriteShort(VelocityY); + buffer.WriteShort(VelocityZ); } + /// + /// Reads the packet data from the buffer. + /// + /// The buffer to read from. + /// The Minecraft version. + /// A new instance of . public static IPacket Read(PacketBuffer buffer, MinecraftData version) { var entityId = buffer.ReadVarInt(); @@ -72,9 +90,7 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) var velocityY = buffer.ReadShort(); var velocityZ = buffer.ReadShort(); - return new SpawnEntityPacket(entityId, objectUuid, type, x, y, z, pitch, yaw, headPitch, objectData, velocityX, - velocityY, - velocityZ); + return new SpawnEntityPacket(entityId, objectUuid, type, x, y, z, pitch, yaw, headPitch, objectData, velocityX, velocityY, velocityZ); } } #pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SpawnExperienceOrbPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SpawnExperienceOrbPacket.cs new file mode 100644 index 00000000..cb325080 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SpawnExperienceOrbPacket.cs @@ -0,0 +1,43 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Spawn Experience Orb packet +/// +/// The entity ID +/// The X coordinate +/// The Y coordinate +/// The Z coordinate +/// The amount of experience this orb will reward once collected +public sealed record SpawnExperienceOrbPacket(int EntityId, double X, double Y, double Z, short Count) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_SpawnEntityExperienceOrb; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(EntityId); + buffer.WriteDouble(X); + buffer.WriteDouble(Y); + buffer.WriteDouble(Z); + buffer.WriteShort(Count); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var entityId = buffer.ReadVarInt(); + var x = buffer.ReadDouble(); + var y = buffer.ReadDouble(); + var z = buffer.ReadDouble(); + var count = buffer.ReadShort(); + + return new SpawnExperienceOrbPacket(entityId, x, y, z, count); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SpawnLivingEntityPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SpawnLivingEntityPacket.cs index 9b830084..5160159d 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SpawnLivingEntityPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SpawnLivingEntityPacket.cs @@ -1,47 +1,46 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Play; -#pragma warning disable CS1591 + /// /// SpawnLivingEntityPacket used for versions <= 1.18.2 /// -public class SpawnLivingEntityPacket : IPacket +/// The ID of the entity. +/// The UUID of the entity. +/// The type of the entity. +/// The X coordinate of the entity. +/// The Y coordinate of the entity. +/// The Z coordinate of the entity. +/// The yaw of the entity. +/// The pitch of the entity. +/// The head pitch of the entity. +/// The X velocity of the entity. +/// The Y velocity of the entity. +/// The Z velocity of the entity. +public sealed record SpawnLivingEntityPacket( + int EntityId, + Uuid EntityUuid, + int EntityType, + double X, + double Y, + double Z, + byte Yaw, + byte Pitch, + byte HeadPitch, + short VelocityX, + short VelocityY, + short VelocityZ +) : IPacket { - public SpawnLivingEntityPacket(int entityId, Uuid entityUuid, int entityType, double x, double y, double z, - byte yaw, byte pitch, - byte headPitch, short velocityX, short velocityY, short velocityZ) - { - EntityId = entityId; - EntityUuid = entityUuid; - EntityType = entityType; - X = x; - Y = y; - Z = z; - Yaw = yaw; - Pitch = pitch; - HeadPitch = headPitch; - VelocityX = velocityX; - VelocityY = velocityY; - VelocityZ = velocityZ; - } - - - public int EntityId { get; set; } - public Uuid EntityUuid { get; set; } - public int EntityType { get; set; } - public double X { get; set; } - public double Y { get; set; } - public double Z { get; set; } - public byte Yaw { get; set; } - public byte Pitch { get; set; } - public byte HeadPitch { get; set; } - public short VelocityX { get; set; } - public short VelocityY { get; set; } - public short VelocityZ { get; set; } - public PacketType Type => PacketType.CB_Play_SpawnEntityLiving; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_SpawnEntityLiving; + /// public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteVarInt(EntityId); @@ -58,6 +57,7 @@ public void Write(PacketBuffer buffer, MinecraftData version) buffer.WriteShort(VelocityZ); } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { return new SpawnLivingEntityPacket( @@ -72,7 +72,7 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) buffer.ReadByte(), buffer.ReadShort(), buffer.ReadShort(), - buffer.ReadShort()); + buffer.ReadShort() + ); } } -#pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SpawnPaintingPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SpawnPaintingPacket.cs index b6562ac7..66f4faeb 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SpawnPaintingPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SpawnPaintingPacket.cs @@ -1,49 +1,50 @@ using MineSharp.Core.Common; using MineSharp.Core.Geometry; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Play; -#pragma warning disable CS1591 + /// /// SpawnPaintingPacket used for versions <= 1.18.2 /// -public class SpawnPaintingPacket : IPacket +/// The ID of the entity. +/// The UUID of the entity. +/// The title of the painting. +/// The location of the painting. +/// The direction the painting is facing. +public sealed record SpawnPaintingPacket( + int EntityId, + Uuid EntityUuid, + int Title, + Position Location, + sbyte Direction +) : IPacket { - public SpawnPaintingPacket(int entityId, Uuid entityUuid, int title, Position location, sbyte direction) - { - EntityId = entityId; - EntityUuid = entityUuid; - Title = title; - Location = location; - Direction = direction; - } - - - public int EntityId { get; set; } - public Uuid EntityUuid { get; set; } - public int Title { get; set; } - public Position Location { get; set; } - public sbyte Direction { get; set; } - public PacketType Type => PacketType.CB_Play_SpawnEntityPainting; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_SpawnEntityPainting; + /// public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteVarInt(EntityId); buffer.WriteUuid(EntityUuid); buffer.WriteVarInt(Title); - buffer.WriteULong(Location.ToULong()); + buffer.WritePosition(Location); buffer.WriteSByte(Direction); } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { var entityId = buffer.ReadVarInt(); var entityUuid = buffer.ReadUuid(); var title = buffer.ReadVarInt(); - var location = new Position(buffer.ReadULong()); + var location = buffer.ReadPosition(); var direction = buffer.ReadSByte(); return new SpawnPaintingPacket(entityId, entityUuid, title, location, direction); } } -#pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SpawnPlayerPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SpawnPlayerPacket.cs index 2b4aafea..f8ff8579 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SpawnPlayerPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SpawnPlayerPacket.cs @@ -1,35 +1,37 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Play; -#pragma warning disable CS1591 + /// /// SpawnPlayerPacket used for versions <= 1.20.1 /// Merged with SpawnEntityPacket in 1.20.2 /// -public class SpawnPlayerPacket : IPacket +/// The ID of the entity. +/// The UUID of the player. +/// The X coordinate of the player. +/// The Y coordinate of the player. +/// The Z coordinate of the player. +/// The yaw of the player. +/// The pitch of the player. +public sealed record SpawnPlayerPacket( + int EntityId, + Uuid PlayerUuid, + double X, + double Y, + double Z, + byte Yaw, + byte Pitch +) : IPacket { - public SpawnPlayerPacket(int entityId, Uuid playerUuid, double x, double y, double z, byte yaw, byte pitch) - { - EntityId = entityId; - PlayerUuid = playerUuid; - X = x; - Y = y; - Z = z; - Yaw = yaw; - Pitch = pitch; - } - - public int EntityId { get; set; } - public Uuid PlayerUuid { get; set; } - public double X { get; set; } - public double Y { get; set; } - public double Z { get; set; } - public byte Yaw { get; set; } - public byte Pitch { get; set; } - public PacketType Type => PacketType.CB_Play_NamedEntitySpawn; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_NamedEntitySpawn; + /// public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteVarInt(EntityId); @@ -41,6 +43,7 @@ public void Write(PacketBuffer buffer, MinecraftData version) buffer.WriteByte(Pitch); } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { var entityId = buffer.ReadVarInt(); @@ -53,4 +56,3 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) return new SpawnPlayerPacket(entityId, playerUuid, x, y, z, yaw, pitch); } } -#pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/StartConfigurationPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/StartConfigurationPacket.cs new file mode 100644 index 00000000..78394dbb --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/StartConfigurationPacket.cs @@ -0,0 +1,30 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Sent during gameplay in order to redo the configuration process. +/// The client must respond with Acknowledge Configuration for the process to start. +/// +public sealed record StartConfigurationPacket() : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_StartConfiguration; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + // No fields to write + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + // No fields to read + return new StartConfigurationPacket(); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/StepTickPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/StepTickPacket.cs new file mode 100644 index 00000000..1c4e7c03 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/StepTickPacket.cs @@ -0,0 +1,30 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Advances the client processing by the specified number of ticks. Has no effect unless client ticking is frozen. +/// +/// The number of tick steps to advance +public sealed record StepTickPacket(int TickSteps) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_StepTick; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(TickSteps); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var tickSteps = buffer.ReadVarInt(); + return new StepTickPacket(tickSteps); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/StopSoundPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/StopSoundPacket.cs new file mode 100644 index 00000000..5fdadda7 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/StopSoundPacket.cs @@ -0,0 +1,76 @@ +using MineSharp.Core.Common; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using static MineSharp.Protocol.Packets.Clientbound.Play.StopSoundPacket; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Stop Sound packet +/// +/// Optional category of the sound. If not present, then sounds from all sources are cleared. +/// Optional sound effect name. If not present, then all sounds are cleared. +public sealed record StopSoundPacket(SoundCategory? Category, Identifier? Sound) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_StopSound; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + byte flags = 0; + flags |= (byte)(Category.HasValue ? 0x1 : 0); + flags |= (byte)(Sound != null ? 0x2 : 0); + + buffer.WriteByte(flags); + if (Category.HasValue) + { + buffer.WriteVarInt((int)Category.GetValueOrDefault()); + } + if (Sound != null) + { + buffer.WriteIdentifier(Sound); + } + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var flags = buffer.ReadByte(); + SoundCategory? category = null; + Identifier? sound = null; + + if ((flags & 0x1) != 0) + { + category = (SoundCategory)buffer.ReadVarInt(); + } + if ((flags & 0x2) != 0) + { + sound = buffer.ReadIdentifier(); + } + + return new StopSoundPacket(category, sound); + } + + /// + /// Enum representing sound categories + /// + public enum SoundCategory + { +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + Master = 0, + Music = 1, + Record = 2, + Weather = 3, + Block = 4, + Hostile = 5, + Neutral = 6, + Player = 7, + Ambient = 8, + Voice = 9 +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SystemChatMessagePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SystemChatMessagePacket.cs index 63d5e6d6..d813307d 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/SystemChatMessagePacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/SystemChatMessagePacket.cs @@ -1,9 +1,8 @@ using MineSharp.ChatComponent; using MineSharp.Core; -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; -using MineSharp.Protocol.Exceptions; namespace MineSharp.Protocol.Packets.Clientbound.Play; @@ -11,19 +10,16 @@ namespace MineSharp.Protocol.Packets.Clientbound.Play; /// Packet for system messages displayed in chat or hotbar /// See https://wiki.vg/Protocol#System_Chat_Message /// -public abstract class SystemChatMessagePacket : IPacket +public abstract record SystemChatMessagePacket(Chat Message) : IPacket { /// - public PacketType Type => PacketType.CB_Play_SystemChat; - - /// - /// The message - /// - public required Chat Message { get; init; } + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_SystemChat; /// public abstract void Write(PacketBuffer buffer, MinecraftData version); - + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { @@ -39,14 +35,8 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) /// /// Used before Minecraft Java 1.19.2 /// - public class Before192 : SystemChatMessagePacket + public sealed record Before192(Chat Message, int ChatType) : SystemChatMessagePacket(Message) { - /// - /// Position of the message - /// 0: chat (chat box), 1: system message (chat box), 2: game info (above hotbar), 3: say command, 4: msg command, 5: team msg command, 6: emote command, 7: tellraw command - /// - public required int ChatType { get; init; } - /// public override void Write(PacketBuffer buffer, MinecraftData data) { @@ -56,7 +46,7 @@ public override void Write(PacketBuffer buffer, MinecraftData data) internal static Before192 _Read(PacketBuffer buffer, MinecraftData version) { - return new() { Message = buffer.ReadChatComponent(), ChatType = buffer.ReadInt() }; + return new(buffer.ReadChatComponent(), buffer.ReadInt()); } } @@ -65,13 +55,8 @@ internal static Before192 _Read(PacketBuffer buffer, MinecraftData version) /// /// Used since Minecraft Java 1.19.2 /// - public class Since192 : SystemChatMessagePacket + public sealed record Since192(Chat Message, bool IsOverlay) : SystemChatMessagePacket(Message) { - /// - /// If true, the message is displayed on the action bar - /// - public required bool IsOverlay { get; init; } - /// public override void Write(PacketBuffer buffer, MinecraftData version) { @@ -81,8 +66,7 @@ public override void Write(PacketBuffer buffer, MinecraftData version) internal static Since192 _Read(PacketBuffer buffer, MinecraftData version) { - return new() { Message = buffer.ReadChatComponent(), IsOverlay = buffer.ReadBool() }; + return new(buffer.ReadChatComponent(), buffer.ReadBool()); } } } - diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/TagQueryResponsePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/TagQueryResponsePacket.cs new file mode 100644 index 00000000..071b1221 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/TagQueryResponsePacket.cs @@ -0,0 +1,35 @@ +using fNbt; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Tag Query Response packet +/// +/// The transaction ID +/// The NBT of the block or entity +public sealed record TagQueryResponsePacket(int TransactionId, NbtTag? Nbt) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_NbtQueryResponse; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(TransactionId); + buffer.WriteOptionalNbt(Nbt); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var transactionId = buffer.ReadVarInt(); + var nbt = buffer.ReadOptionalNbt(); + + return new TagQueryResponsePacket(transactionId, nbt); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/TeleportEntityPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/TeleportEntityPacket.cs index 53c98c68..a26aafaa 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/TeleportEntityPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/TeleportEntityPacket.cs @@ -1,30 +1,23 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Play; #pragma warning disable CS1591 -public class TeleportEntityPacket : IPacket +public sealed record TeleportEntityPacket( + int EntityId, + double X, + double Y, + double Z, + sbyte Yaw, + sbyte Pitch, + bool OnGround +) : IPacket { - public TeleportEntityPacket(int entityId, double x, double y, double z, sbyte yaw, sbyte pitch, bool onGround) - { - EntityId = entityId; - X = x; - Y = y; - Z = z; - Yaw = yaw; - Pitch = pitch; - OnGround = onGround; - } - - public int EntityId { get; set; } - public double X { get; set; } - public double Y { get; set; } - public double Z { get; set; } - public sbyte Yaw { get; set; } - public sbyte Pitch { get; set; } - public bool OnGround { get; set; } - public PacketType Type => PacketType.CB_Play_EntityTeleport; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_EntityTeleport; public void Write(PacketBuffer buffer, MinecraftData version) { diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/UnloadChunkPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/UnloadChunkPacket.cs index 49bc1048..e1e3f8a8 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/UnloadChunkPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/UnloadChunkPacket.cs @@ -1,27 +1,29 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Play; -#pragma warning disable CS1591 -public class UnloadChunkPacket : IPacket -{ - public UnloadChunkPacket(int x, int z) - { - X = x; - Z = z; - } - public int X { get; set; } - public int Z { get; set; } - public PacketType Type => PacketType.CB_Play_UnloadChunk; +/// +/// Represents a packet to unload a chunk. +/// +/// The X coordinate of the chunk. +/// The Z coordinate of the chunk. +public sealed record UnloadChunkPacket(int X, int Z) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_UnloadChunk; + /// public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteInt(X); buffer.WriteInt(Z); } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { var x = buffer.ReadInt(); @@ -29,4 +31,4 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) return new UnloadChunkPacket(x, z); } } -#pragma warning restore CS1591 + diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateAdvancementsPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateAdvancementsPacket.cs new file mode 100644 index 00000000..bad8ef1e --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateAdvancementsPacket.cs @@ -0,0 +1,313 @@ +using MineSharp.ChatComponent; +using MineSharp.Core.Common; +using MineSharp.Core.Common.Items; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using MineSharp.Protocol.Packets.NetworkTypes; +using static MineSharp.Protocol.Packets.Clientbound.Play.UpdateAdvancementsPacket; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Update Advancements packet +/// +/// Whether to reset/clear the current advancements +/// The advancement mappings +/// The identifiers of the advancements that should be removed +/// The progress mappings +public sealed record UpdateAdvancementsPacket( + bool ResetClear, + KeyValuePair[] AdvancementMappings, + Identifier[] Identifiers, + KeyValuePair[] ProgressMappings) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_Advancements; + + /// + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteBool(ResetClear); + buffer.WriteVarInt(AdvancementMappings.Length); + foreach (var (key, value) in AdvancementMappings) + { + buffer.WriteIdentifier(key); + value.Write(buffer, data); + } + buffer.WriteVarInt(Identifiers.Length); + foreach (var identifier in Identifiers) + { + buffer.WriteIdentifier(identifier); + } + buffer.WriteVarInt(ProgressMappings.Length); + foreach (var (key, value) in ProgressMappings) + { + buffer.WriteIdentifier(key); + value.Write(buffer); + } + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData data) + { + var resetClear = buffer.ReadBool(); + var advancementMappingsCount = buffer.ReadVarInt(); + var advancementMappings = new KeyValuePair[advancementMappingsCount]; + for (int i = 0; i < advancementMappingsCount; i++) + { + var key = buffer.ReadIdentifier(); + var value = Advancement.Read(buffer, data); + advancementMappings[i] = new(key, value); + } + var identifiersCount = buffer.ReadVarInt(); + var identifiers = new Identifier[identifiersCount]; + for (int i = 0; i < identifiersCount; i++) + { + identifiers[i] = buffer.ReadIdentifier(); + } + var progressMappingsCount = buffer.ReadVarInt(); + var progressMappings = new KeyValuePair[progressMappingsCount]; + for (int i = 0; i < progressMappingsCount; i++) + { + var key = buffer.ReadIdentifier(); + var value = AdvancementProgress.Read(buffer); + progressMappings[i] = new(key, value); + } + + return new UpdateAdvancementsPacket( + resetClear, + advancementMappings, + identifiers, + progressMappings); + } + + /// + /// Represents an advancement + /// + /// The identifier of the parent advancement + /// The display data + /// The requirements + /// Whether the client should include this achievement in the telemetry data when it's completed + public sealed record Advancement( + Identifier? ParentId, + AdvancementDisplay? DisplayData, + string[][] Requirements, + bool SendsTelemetryData) : ISerializableWithMinecraftData + { + /// + public void Write(PacketBuffer buffer, MinecraftData data) + { + var hasParent = ParentId != null; + buffer.WriteBool(hasParent); + if (hasParent) + { + buffer.WriteIdentifier(ParentId!); + } + var hasDisplay = DisplayData != null; + buffer.WriteBool(hasDisplay); + if (hasDisplay) + { + DisplayData!.Write(buffer, data); + } + buffer.WriteVarInt(Requirements.Length); + foreach (var requirement in Requirements) + { + buffer.WriteVarInt(requirement.Length); + foreach (var req in requirement) + { + buffer.WriteString(req); + } + } + buffer.WriteBool(SendsTelemetryData); + } + + /// + public static Advancement Read(PacketBuffer buffer, MinecraftData data) + { + var hasParent = buffer.ReadBool(); + Identifier? parentId = null; + if (hasParent) + { + parentId = buffer.ReadIdentifier(); + } + var hasDisplay = buffer.ReadBool(); + AdvancementDisplay? displayData = null; + if (hasDisplay) + { + displayData = AdvancementDisplay.Read(buffer, data); + } + var requirementsCount = buffer.ReadVarInt(); + var requirements = new string[requirementsCount][]; + for (int i = 0; i < requirementsCount; i++) + { + var innerCount = buffer.ReadVarInt(); + var innerList = new string[innerCount]; + for (int j = 0; j < innerCount; j++) + { + innerList[j] = buffer.ReadString(); + } + requirements[i] = innerList; + } + var sendsTelemetryData = buffer.ReadBool(); + + return new Advancement( + parentId, + displayData, + requirements, + sendsTelemetryData); + } + } + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public enum FrameType + { + Task = 0, + Challenge = 1, + Goal = 2 + } + + [Flags] + public enum AdvancementFlags + { + None = 0, + HasBackgroundTexture = 0x01, + ShowToast = 0x02, + Hidden = 0x04 + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + + /// + /// Represents the display data of an advancement + /// + /// The title of the advancement + /// The description of the advancement + /// The icon of the advancement + /// The frame type of the advancement + /// The flags of the advancement + /// The background texture location + /// The X coordinate of the advancement + /// The Y coordinate of the advancement + public sealed record AdvancementDisplay( + Chat Title, + Chat Description, + Item Icon, + FrameType FrameType, + AdvancementFlags Flags, + Identifier? BackgroundTexture, + float XCoord, + float YCoord) : ISerializableWithMinecraftData + { + /// + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteChatComponent(Title); + buffer.WriteChatComponent(Description); + buffer.WriteOptionalItem(Icon); + buffer.WriteVarInt((int)FrameType); + buffer.WriteInt((int)Flags); + if (Flags.HasFlag(AdvancementFlags.HasBackgroundTexture)) + { + buffer.WriteIdentifier(BackgroundTexture ?? throw new InvalidOperationException($"{nameof(BackgroundTexture)} must not be null if {nameof(Flags)} indicates that the background texture is present.")); + } + buffer.WriteFloat(XCoord); + buffer.WriteFloat(YCoord); + } + + /// + public static AdvancementDisplay Read(PacketBuffer buffer, MinecraftData data) + { + var title = buffer.ReadChatComponent(); + var description = buffer.ReadChatComponent(); + var icon = buffer.ReadOptionalItem(data)!; + var frameType = (FrameType)buffer.ReadVarInt(); + var flags = (AdvancementFlags)buffer.ReadInt(); + Identifier? backgroundTexture = null; + if (flags.HasFlag(AdvancementFlags.HasBackgroundTexture)) + { + backgroundTexture = buffer.ReadIdentifier(); + } + var xCoord = buffer.ReadFloat(); + var yCoord = buffer.ReadFloat(); + + return new AdvancementDisplay( + title, + description, + icon, + frameType, + flags, + backgroundTexture, + xCoord, + yCoord); + } + } + + /// + /// Represents the progress of an advancement + /// + /// The criteria of the advancement progress + public sealed record AdvancementProgress( + KeyValuePair[] Criteria) : ISerializable + { + /// + public void Write(PacketBuffer buffer) + { + buffer.WriteVarInt(Criteria.Length); + foreach (var (key, value) in Criteria) + { + buffer.WriteIdentifier(key); + value.Write(buffer); + } + } + + /// + public static AdvancementProgress Read(PacketBuffer buffer) + { + var criteriaCount = buffer.ReadVarInt(); + var criteria = new KeyValuePair[criteriaCount]; + for (int i = 0; i < criteriaCount; i++) + { + var key = buffer.ReadIdentifier(); + var value = CriterionProgress.Read(buffer); + criteria[i] = new(key, value); + } + + return new AdvancementProgress(criteria); + } + } + + /// + /// Represents the progress of a criterion + /// + /// The date of achieving the criterion + public sealed record CriterionProgress( + long? DateOfAchieving) : ISerializable + { + /// + public void Write(PacketBuffer buffer) + { + var achieved = DateOfAchieving != null; + buffer.WriteBool(achieved); + if (achieved) + { + buffer.WriteLong(DateOfAchieving!.Value); + } + } + + /// + public static CriterionProgress Read(PacketBuffer buffer) + { + var achieved = buffer.ReadBool(); + long? dateOfAchieving = null; + if (achieved) + { + dateOfAchieving = buffer.ReadLong(); + } + + return new CriterionProgress( + dateOfAchieving); + } + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateAttributesPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateAttributesPacket.cs index 6180a12b..2ac7b818 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateAttributesPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateAttributesPacket.cs @@ -1,67 +1,32 @@ -using MineSharp.Core.Common; -using MineSharp.Core.Common.Entities.Attributes; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; using Attribute = MineSharp.Core.Common.Entities.Attributes.Attribute; namespace MineSharp.Protocol.Packets.Clientbound.Play; #pragma warning disable CS1591 -public class UpdateAttributesPacket : IPacket +public sealed record UpdateAttributesPacket( + int EntityId, + Attribute[] Attributes +) : IPacket { - public UpdateAttributesPacket(int entityId, Attribute[] attributes) - { - EntityId = entityId; - Attributes = attributes; - } - - public int EntityId { get; set; } - public Attribute[] Attributes { get; set; } - public PacketType Type => PacketType.CB_Play_EntityUpdateAttributes; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_EntityUpdateAttributes; public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteVarInt(EntityId); - buffer.WriteVarIntArray(Attributes, WriteAttribute); + buffer.WriteVarIntArray(Attributes, (buffer, attribute) => attribute.Write(buffer)); } public static IPacket Read(PacketBuffer buffer, MinecraftData version) { var entityId = buffer.ReadVarInt(); - var attributes = buffer.ReadVarIntArray(ReadAttribute); + var attributes = buffer.ReadVarIntArray(Attribute.Read); return new UpdateAttributesPacket(entityId, attributes); } - - private void WriteAttribute(PacketBuffer buffer, Attribute attribute) - { - buffer.WriteString(attribute.Key); - buffer.WriteDouble(attribute.Value); - buffer.WriteVarIntArray(attribute.Modifiers.Values, WriteModifier); - } - - private void WriteModifier(PacketBuffer buffer, Modifier modifier) - { - buffer.WriteUuid(modifier.Uuid); - buffer.WriteDouble(modifier.Amount); - buffer.WriteByte((byte)modifier.Operation); - } - - private static Attribute ReadAttribute(PacketBuffer buffer) - { - var key = buffer.ReadString(); - var value = buffer.ReadDouble(); - var modifiers = buffer.ReadVarIntArray(ReadModifier); - - return new(key, value, modifiers); - } - - private static Modifier ReadModifier(PacketBuffer buffer) - { - var uuid = buffer.ReadUuid(); - var amount = buffer.ReadDouble(); - var operation = buffer.ReadByte(); - - return new(uuid, amount, (ModifierOp)operation); - } } #pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateLightPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateLightPacket.cs new file mode 100644 index 00000000..034f5e6e --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateLightPacket.cs @@ -0,0 +1,113 @@ +using MineSharp.Core.Common; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using static MineSharp.Protocol.Packets.Clientbound.Play.UpdateLightPacket; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Updates light levels for a chunk. +/// +/// Chunk coordinate (block coordinate divided by 16, rounded down) +/// Chunk coordinate (block coordinate divided by 16, rounded down) +/// BitSet for sky light sections +/// BitSet for block light sections +/// BitSet for empty sky light sections +/// BitSet for empty block light sections +/// Array of sky light data +/// Array of block light data +public sealed record UpdateLightPacket( + int ChunkX, + int ChunkZ, + BitSet SkyLightMask, + BitSet BlockLightMask, + BitSet EmptySkyLightMask, + BitSet EmptyBlockLightMask, + LightArray[] SkyLightArrays, + LightArray[] BlockLightArrays +) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_UpdateLight; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(ChunkX); + buffer.WriteVarInt(ChunkZ); + buffer.WriteBitSet(SkyLightMask); + buffer.WriteBitSet(BlockLightMask); + buffer.WriteBitSet(EmptySkyLightMask); + buffer.WriteBitSet(EmptyBlockLightMask); + buffer.WriteVarInt(SkyLightArrays.Length); + foreach (var array in SkyLightArrays) + { + array.Write(buffer); + } + buffer.WriteVarInt(BlockLightArrays.Length); + foreach (var array in BlockLightArrays) + { + array.Write(buffer); + } + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var chunkX = buffer.ReadVarInt(); + var chunkZ = buffer.ReadVarInt(); + var skyLightMask = buffer.ReadBitSet(); + var blockLightMask = buffer.ReadBitSet(); + var emptySkyLightMask = buffer.ReadBitSet(); + var emptyBlockLightMask = buffer.ReadBitSet(); + var skyLightArrayCount = buffer.ReadVarInt(); + var skyLightArrays = new LightArray[skyLightArrayCount]; + for (int i = 0; i < skyLightArrayCount; i++) + { + skyLightArrays[i] = LightArray.Read(buffer); + } + var blockLightArrayCount = buffer.ReadVarInt(); + var blockLightArrays = new LightArray[blockLightArrayCount]; + for (int i = 0; i < blockLightArrayCount; i++) + { + blockLightArrays[i] = LightArray.Read(buffer); + } + + return new UpdateLightPacket( + chunkX, + chunkZ, + skyLightMask, + blockLightMask, + emptySkyLightMask, + emptyBlockLightMask, + skyLightArrays, + blockLightArrays + ); + } + + /// + /// Represents a light array. + /// + /// Length of the array in bytes (always 2048) + /// The light data + public sealed record LightArray(int Length, byte[] Data) : ISerializable + { + /// + public void Write(PacketBuffer buffer) + { + buffer.WriteVarInt(Length); + buffer.WriteBytes(Data); + } + + /// + public static LightArray Read(PacketBuffer buffer) + { + var length = buffer.ReadVarInt(); + var data = buffer.ReadBytes(length); + return new LightArray(length, data); + } + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateObjectivesPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateObjectivesPacket.cs new file mode 100644 index 00000000..b052d766 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateObjectivesPacket.cs @@ -0,0 +1,104 @@ +using MineSharp.ChatComponent; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using MineSharp.Protocol.Packets.NetworkTypes; +using static MineSharp.Protocol.Packets.Clientbound.Play.UpdateObjectivesPacket; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Update Objectives packet +/// +/// A unique name for the objective +/// The mode of the objective update +/// The text to be displayed for the score +/// The type of the objective +/// The number format for the score +public sealed record UpdateObjectivesPacket( + string ObjectiveName, + ObjectiveMode Mode, + Chat? ObjectiveValue, + // this field can not be named "Type" or "ObjectiveType" + ObjectiveType? ObjectiveTypeValue, + IScoreboardNumberFormat? NumberFormat +) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_ScoreboardObjective; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteString(ObjectiveName); + buffer.WriteByte((byte)Mode); + + if (Mode == ObjectiveMode.Create || Mode == ObjectiveMode.Update) + { + buffer.WriteChatComponent(ObjectiveValue ?? throw new InvalidOperationException($"{nameof(ObjectiveValue)} is required if Mode is Create or Update")); + buffer.WriteVarInt((int)(ObjectiveTypeValue ?? throw new InvalidOperationException($"{nameof(ObjectiveTypeValue)} is required if Mode is Create or Update"))); + var hasNumberFormat = NumberFormat != null; + buffer.WriteBool(hasNumberFormat); + + if (hasNumberFormat) + { + buffer.WriteVarInt((int)NumberFormat!.Type); + NumberFormat!.Write(buffer); + } + } + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var objectiveName = buffer.ReadString(); + var mode = (ObjectiveMode)buffer.ReadByte(); + Chat? objectiveValue = null; + ObjectiveType? objectiveType = null; + IScoreboardNumberFormat? numberFormat = null; + + if (mode == ObjectiveMode.Create || mode == ObjectiveMode.Update) + { + objectiveValue = buffer.ReadChatComponent(); + objectiveType = (ObjectiveType)buffer.ReadVarInt(); + var hasNumberFormat = buffer.ReadBool(); + + if (hasNumberFormat) + { + var type = (ScoreboardNumberFormatType)buffer.ReadVarInt(); + numberFormat = ScoreboardNumberFormatRegistry.Read(buffer, type); + } + } + + return new UpdateObjectivesPacket( + objectiveName, + mode, + objectiveValue, + objectiveType, + numberFormat + ); + } + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + /// + /// Objective mode enumeration + /// + public enum ObjectiveMode : byte + { + Create = 0, + Remove = 1, + Update = 2 + } + + /// + /// Objective type enumeration + /// + public enum ObjectiveType : int + { + Integer = 0, + Hearts = 1 + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateRecipeBookPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateRecipeBookPacket.cs new file mode 100644 index 00000000..3d93568a --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateRecipeBookPacket.cs @@ -0,0 +1,131 @@ +using MineSharp.Core.Common; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using static MineSharp.Protocol.Packets.Clientbound.Play.UpdateRecipeBookPacket; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Packet sent by the server to update the recipe book. +/// +/// The action to perform (init, add, remove). +/// If true, the crafting recipe book will be open when the player opens its inventory. +/// If true, the filtering option is active when the player opens its inventory. +/// If true, the smelting recipe book will be open when the player opens its inventory. +/// If true, the filtering option is active when the player opens its inventory. +/// If true, the blast furnace recipe book will be open when the player opens its inventory. +/// If true, the filtering option is active when the player opens its inventory. +/// If true, the smoker recipe book will be open when the player opens its inventory. +/// If true, the filtering option is active when the player opens its inventory. +/// List of recipe IDs. +/// Optional list of recipe IDs, only present if action is Init. +public sealed record UpdateRecipeBookPacket( + RecipeBookAction Action, + bool CraftingRecipeBookOpen, + bool CraftingRecipeBookFilterActive, + bool SmeltingRecipeBookOpen, + bool SmeltingRecipeBookFilterActive, + bool BlastFurnaceRecipeBookOpen, + bool BlastFurnaceRecipeBookFilterActive, + bool SmokerRecipeBookOpen, + bool SmokerRecipeBookFilterActive, + Identifier[] RecipeIds, + Identifier[]? OptionalRecipeIds) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_UnlockRecipes; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt((int)Action); + buffer.WriteBool(CraftingRecipeBookOpen); + buffer.WriteBool(CraftingRecipeBookFilterActive); + buffer.WriteBool(SmeltingRecipeBookOpen); + buffer.WriteBool(SmeltingRecipeBookFilterActive); + buffer.WriteBool(BlastFurnaceRecipeBookOpen); + buffer.WriteBool(BlastFurnaceRecipeBookFilterActive); + buffer.WriteBool(SmokerRecipeBookOpen); + buffer.WriteBool(SmokerRecipeBookFilterActive); + buffer.WriteVarInt(RecipeIds.Length); + foreach (var recipeId in RecipeIds) + { + buffer.WriteIdentifier(recipeId); + } + if (Action == RecipeBookAction.Init) + { + buffer.WriteVarInt(OptionalRecipeIds?.Length ?? 0); + if (OptionalRecipeIds != null) + { + foreach (var optionalRecipeId in OptionalRecipeIds) + { + buffer.WriteIdentifier(optionalRecipeId); + } + } + } + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var action = (RecipeBookAction)buffer.ReadVarInt(); + var craftingRecipeBookOpen = buffer.ReadBool(); + var craftingRecipeBookFilterActive = buffer.ReadBool(); + var smeltingRecipeBookOpen = buffer.ReadBool(); + var smeltingRecipeBookFilterActive = buffer.ReadBool(); + var blastFurnaceRecipeBookOpen = buffer.ReadBool(); + var blastFurnaceRecipeBookFilterActive = buffer.ReadBool(); + var smokerRecipeBookOpen = buffer.ReadBool(); + var smokerRecipeBookFilterActive = buffer.ReadBool(); + var recipeIdsLength = buffer.ReadVarInt(); + var recipeIds = new Identifier[recipeIdsLength]; + for (int i = 0; i < recipeIdsLength; i++) + { + recipeIds[i] = buffer.ReadIdentifier(); + } + Identifier[]? optionalRecipeIds = null; + if (action == RecipeBookAction.Init) + { + var optionalRecipeIdsLength = buffer.ReadVarInt(); + optionalRecipeIds = new Identifier[optionalRecipeIdsLength]; + for (int i = 0; i < optionalRecipeIdsLength; i++) + { + optionalRecipeIds[i] = buffer.ReadIdentifier(); + } + } + return new UpdateRecipeBookPacket( + action, + craftingRecipeBookOpen, + craftingRecipeBookFilterActive, + smeltingRecipeBookOpen, + smeltingRecipeBookFilterActive, + blastFurnaceRecipeBookOpen, + blastFurnaceRecipeBookFilterActive, + smokerRecipeBookOpen, + smokerRecipeBookFilterActive, + recipeIds, + optionalRecipeIds); + } + + /// + /// Enum representing the action to perform on the recipe book. + /// + public enum RecipeBookAction + { + /// + /// All the recipes in list 1 will be tagged as displayed, and all the recipes in list 2 will be added to the recipe book. Recipes that aren't tagged will be shown in the notification. + /// + Init = 0, + /// + /// All the recipes in the list are added to the recipe book and their icons will be shown in the notification. + /// + Add = 1, + /// + /// Remove all the recipes in the list. This allows them to be re-displayed when they are re-added. + /// + Remove = 2 + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateRecipesPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateRecipesPacket.cs new file mode 100644 index 00000000..2a194872 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateRecipesPacket.cs @@ -0,0 +1,427 @@ +using System.Collections.Frozen; +using MineSharp.Core.Common; +using MineSharp.Core.Common.Items; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using MineSharp.Protocol.Packets.NetworkTypes; +using static MineSharp.Protocol.Packets.Clientbound.Play.UpdateRecipesPacket; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Represents a packet sent by the server to update the list of recipes. +/// +public sealed record UpdateRecipesPacket( + Recipe[] Recipes +) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_DeclareRecipes; + + /// + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteVarInt(Recipes.Length); + foreach (var recipe in Recipes) + { + recipe.Write(buffer, data); + } + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData data) + { + var numRecipes = buffer.ReadVarInt(); + var recipes = new Recipe[numRecipes]; + for (int i = 0; i < numRecipes; i++) + { + recipes[i] = Recipe.Read(buffer, data); + } + return new UpdateRecipesPacket(recipes); + } + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public interface IUpdateRecipesData + { + public void Write(PacketBuffer buffer, MinecraftData data); + } + + public interface IUpdateRecipesDataStatic + { + public static abstract IUpdateRecipesData Read(PacketBuffer buffer, MinecraftData data); + } + + public static class UpdateRecipesDataRegistry + { + public static IUpdateRecipesData Read(PacketBuffer buffer, MinecraftData data, Identifier type) + { + if (!UpdateRecipesDataTypeFactories.TryGetValue(type, out var reader)) + { + throw new InvalidOperationException($"Unsupported data type: {type}"); + } + return reader(buffer, data); + } + + public static readonly FrozenDictionary> UpdateRecipesDataTypeFactories; + + static UpdateRecipesDataRegistry() + { + UpdateRecipesDataTypeFactories = InitializeUpdateRecipesDataTypes(); + } + + private static FrozenDictionary> InitializeUpdateRecipesDataTypes() + { + var dict = new Dictionary>(); + + void Register(params Identifier[] identifiers) + where T : IUpdateRecipesData, IUpdateRecipesDataStatic + { + var factory = T.Read; + foreach (var identifier in identifiers) + { + dict.Add(identifier, factory); + } + } + + // TODO: Does this data come from some registry? + Register( + Identifier.Parse("minecraft:crafting_shapeless") + ); + Register( + Identifier.Parse("minecraft:crafting_shaped") + ); + Register( + Identifier.Parse("minecraft:crafting_special_armordye"), + Identifier.Parse("minecraft:crafting_special_bookcloning"), + Identifier.Parse("minecraft:crafting_special_mapcloning"), + Identifier.Parse("minecraft:crafting_special_mapextending"), + Identifier.Parse("minecraft:crafting_special_firework_rocket"), + Identifier.Parse("minecraft:crafting_special_firework_star"), + Identifier.Parse("minecraft:crafting_special_firework_star_fade"), + Identifier.Parse("minecraft:crafting_special_repairitem"), + Identifier.Parse("minecraft:crafting_special_tippedarrow"), + Identifier.Parse("minecraft:crafting_special_bannerduplicate"), + Identifier.Parse("minecraft:crafting_special_shielddecoration"), + Identifier.Parse("minecraft:crafting_special_shulkerboxcoloring"), + Identifier.Parse("minecraft:crafting_special_suspiciousstew"), + Identifier.Parse("minecraft:crafting_decorated_pot") + ); + Register( + Identifier.Parse("minecraft:smelting"), + Identifier.Parse("minecraft:blasting"), + Identifier.Parse("minecraft:smoking"), + Identifier.Parse("minecraft:campfire_cooking") + ); + Register( + Identifier.Parse("minecraft:stonecutting") + ); + Register( + Identifier.Parse("minecraft:smithing_transform") + ); + Register( + Identifier.Parse("minecraft:smithing_trim") + ); + + return dict.ToFrozenDictionary(); + } + } + + /// + /// Represents a recipe in the update recipes packet. + /// + public sealed record Recipe( + Identifier Type, + Identifier RecipeId, + IUpdateRecipesData Data + ) : ISerializableWithMinecraftData + { + /// + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteIdentifier(Type); + buffer.WriteIdentifier(RecipeId); + Data.Write(buffer, data); + } + + /// + public static Recipe Read(PacketBuffer buffer, MinecraftData data) + { + var type = buffer.ReadIdentifier(); + var recipeId = buffer.ReadIdentifier(); + var recipeData = UpdateRecipesDataRegistry.Read(buffer, data, type); + return new Recipe(type, recipeId, recipeData); + } + } + + public enum CraftingCategory + { + Building, + Redstone, + Equipment, + Misc + } + + public sealed record ShapelessCraftingData( + string Group, + CraftingCategory Category, + Ingredient[] Ingredients, + Item Result + ) : IUpdateRecipesData, IUpdateRecipesDataStatic, ISerializableWithMinecraftData + { + /// + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteString(Group); + buffer.WriteVarInt((int)Category); + buffer.WriteVarInt(Ingredients.Length); + foreach (var ingredient in Ingredients) + { + ingredient.Write(buffer, data); + } + buffer.WriteOptionalItem(Result); + } + + /// + public static ShapelessCraftingData Read(PacketBuffer buffer, MinecraftData data) + { + var group = buffer.ReadString(); + var category = (CraftingCategory)buffer.ReadVarInt(); + var ingredientCount = buffer.ReadVarInt(); + var ingredients = new Ingredient[ingredientCount]; + for (int i = 0; i < ingredientCount; i++) + { + ingredients[i] = Ingredient.Read(buffer, data); + } + var result = buffer.ReadOptionalItem(data)!; + return new ShapelessCraftingData(group, category, ingredients, result); + } + + static IUpdateRecipesData IUpdateRecipesDataStatic.Read(PacketBuffer buffer, MinecraftData data) + { + return Read(buffer, data); + } + } + + public sealed record ShapedCraftingData( + string Group, + CraftingCategory Category, + int Width, + int Height, + Ingredient[] Ingredients, + Item Result, + bool ShowNotification + ) : IUpdateRecipesData, IUpdateRecipesDataStatic, ISerializableWithMinecraftData + { + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteString(Group); + buffer.WriteVarInt((int)Category); + buffer.WriteVarInt(Width); + buffer.WriteVarInt(Height); + foreach (var ingredient in Ingredients) + { + ingredient.Write(buffer, data); + } + buffer.WriteOptionalItem(Result); + buffer.WriteBool(ShowNotification); + } + + public static ShapedCraftingData Read(PacketBuffer buffer, MinecraftData data) + { + var group = buffer.ReadString(); + var category = (CraftingCategory)buffer.ReadVarInt(); + var width = buffer.ReadVarInt(); + var height = buffer.ReadVarInt(); + var ingredients = new Ingredient[width * height]; + for (int i = 0; i < ingredients.Length; i++) + { + ingredients[i] = Ingredient.Read(buffer, data); + } + var result = buffer.ReadOptionalItem(data)!; + var showNotification = buffer.ReadBool(); + return new ShapedCraftingData(group, category, width, height, ingredients, result, showNotification); + } + + static IUpdateRecipesData IUpdateRecipesDataStatic.Read(PacketBuffer buffer, MinecraftData data) + { + return Read(buffer, data); + } + } + + public sealed record SpecialCraftingData( + CraftingCategory Category + ) : IUpdateRecipesData, IUpdateRecipesDataStatic, ISerializableWithMinecraftData + { + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteVarInt((int)Category); + } + + public static SpecialCraftingData Read(PacketBuffer buffer, MinecraftData data) + { + var category = (CraftingCategory)buffer.ReadVarInt(); + return new SpecialCraftingData(category); + } + + static IUpdateRecipesData IUpdateRecipesDataStatic.Read(PacketBuffer buffer, MinecraftData data) + { + return Read(buffer, data); + } + } + + public enum SmeltingCraftingCategory + { + Food, + Blocks, + Misc + } + + public sealed record SmeltingData( + string Group, + SmeltingCraftingCategory Category, + Ingredient Ingredient, + Item Result, + float Experience, + int CookingTime + ) : IUpdateRecipesData, IUpdateRecipesDataStatic, ISerializableWithMinecraftData + { + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteString(Group); + buffer.WriteVarInt((int)Category); + Ingredient.Write(buffer, data); + buffer.WriteOptionalItem(Result); + buffer.WriteFloat(Experience); + buffer.WriteVarInt(CookingTime); + } + + public static SmeltingData Read(PacketBuffer buffer, MinecraftData data) + { + var group = buffer.ReadString(); + var category = (SmeltingCraftingCategory)buffer.ReadVarInt(); + var ingredient = Ingredient.Read(buffer, data); + var result = buffer.ReadOptionalItem(data)!; + var experience = buffer.ReadFloat(); + var cookingTime = buffer.ReadVarInt(); + return new SmeltingData(group, category, ingredient, result, experience, cookingTime); + } + + static IUpdateRecipesData IUpdateRecipesDataStatic.Read(PacketBuffer buffer, MinecraftData data) + { + return Read(buffer, data); + } + } + + public sealed record StonecuttingData( + string Group, + Ingredient Ingredient, + Item Result + ) : IUpdateRecipesData, IUpdateRecipesDataStatic, ISerializableWithMinecraftData + { + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteString(Group); + Ingredient.Write(buffer, data); + buffer.WriteOptionalItem(Result); + } + + public static StonecuttingData Read(PacketBuffer buffer, MinecraftData data) + { + var group = buffer.ReadString(); + var ingredient = Ingredient.Read(buffer, data); + var result = buffer.ReadOptionalItem(data)!; + return new StonecuttingData(group, ingredient, result); + } + + static IUpdateRecipesData IUpdateRecipesDataStatic.Read(PacketBuffer buffer, MinecraftData data) + { + return Read(buffer, data); + } + } + + public sealed record SmithingTransformData( + Ingredient Template, + Ingredient Base, + Ingredient Addition, + Item Result + ) : IUpdateRecipesData, IUpdateRecipesDataStatic, ISerializableWithMinecraftData + { + public void Write(PacketBuffer buffer, MinecraftData data) + { + Template.Write(buffer, data); + Base.Write(buffer, data); + Addition.Write(buffer, data); + buffer.WriteOptionalItem(Result); + } + + public static SmithingTransformData Read(PacketBuffer buffer, MinecraftData data) + { + var template = Ingredient.Read(buffer, data); + var baseItem = Ingredient.Read(buffer, data); + var addition = Ingredient.Read(buffer, data); + var result = buffer.ReadOptionalItem(data); + return new SmithingTransformData(template, baseItem, addition, result); + } + + static IUpdateRecipesData IUpdateRecipesDataStatic.Read(PacketBuffer buffer, MinecraftData data) + { + return Read(buffer, data); + } + } + + public sealed record SmithingTrimData( + Ingredient Template, + Ingredient Base, + Ingredient Addition + ) : IUpdateRecipesData, IUpdateRecipesDataStatic, ISerializableWithMinecraftData + { + public void Write(PacketBuffer buffer, MinecraftData data) + { + Template.Write(buffer, data); + Base.Write(buffer, data); + Addition.Write(buffer, data); + } + + public static SmithingTrimData Read(PacketBuffer buffer, MinecraftData data) + { + var template = Ingredient.Read(buffer, data); + var baseItem = Ingredient.Read(buffer, data); + var addition = Ingredient.Read(buffer, data); + return new SmithingTrimData(template, baseItem, addition); + } + + static IUpdateRecipesData IUpdateRecipesDataStatic.Read(PacketBuffer buffer, MinecraftData data) + { + return Read(buffer, data); + } + } + + public sealed record Ingredient( + Item[] Items + ) : ISerializableWithMinecraftData + { + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteVarInt(Items.Length); + foreach (var item in Items) + { + buffer.WriteOptionalItem(item); + } + } + + public static Ingredient Read(PacketBuffer buffer, MinecraftData data) + { + var count = buffer.ReadVarInt(); + var items = new Item[count]; + for (int i = 0; i < count; i++) + { + items[i] = buffer.ReadOptionalItem(data)!; + } + return new Ingredient(items); + } + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateScorePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateScorePacket.cs new file mode 100644 index 00000000..791290bd --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateScorePacket.cs @@ -0,0 +1,83 @@ +using MineSharp.ChatComponent; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using MineSharp.Protocol.Packets.NetworkTypes; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Update Score packet +/// +/// The entity whose score this is. For players, this is their username; for other entities, it is their UUID. +/// The name of the objective the score belongs to +/// The score to be displayed next to the entry +/// The custom display name +/// The number format for the score +public sealed record UpdateScorePacket( + string EntityName, + string ObjectiveName, + int Value, + Chat? DisplayName, + IScoreboardNumberFormat? NumberFormat +) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_ScoreboardScore; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteString(EntityName); + buffer.WriteString(ObjectiveName); + buffer.WriteVarInt(Value); + var hasDisplayName = DisplayName != null; + buffer.WriteBool(hasDisplayName); + + if (hasDisplayName) + { + buffer.WriteChatComponent(DisplayName!); + } + + var hasNumberFormat = NumberFormat != null; + buffer.WriteBool(hasNumberFormat); + + if (hasNumberFormat) + { + buffer.WriteVarInt((int)NumberFormat!.Type); + NumberFormat!.Write(buffer); + } + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var entityName = buffer.ReadString(); + var objectiveName = buffer.ReadString(); + var value = buffer.ReadVarInt(); + var hasDisplayName = buffer.ReadBool(); + Chat? displayName = null; + if (hasDisplayName) + { + displayName = buffer.ReadChatComponent(); + } + + var hasNumberFormat = buffer.ReadBool(); + IScoreboardNumberFormat? numberFormat = null; + if (hasNumberFormat) + { + var type = (ScoreboardNumberFormatType)buffer.ReadVarInt(); + numberFormat = ScoreboardNumberFormatRegistry.Read(buffer, type); + } + + return new UpdateScorePacket( + entityName, + objectiveName, + value, + displayName, + numberFormat + ); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateTagsPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateTagsPacket.cs new file mode 100644 index 00000000..984f3301 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateTagsPacket.cs @@ -0,0 +1,32 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using MineSharp.Protocol.Packets.NetworkTypes; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Update Tags packet +/// +/// Array of registries with their tags +public sealed record UpdateTagsPacket(Registry[] Registries) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_Tags; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarIntArray(Registries, (buffer, registry) => registry.Write(buffer)); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var registries = buffer.ReadVarIntArray(Registry.Read); + + return new UpdateTagsPacket(registries); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateTeamsPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateTeamsPacket.cs new file mode 100644 index 00000000..dc702a84 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateTeamsPacket.cs @@ -0,0 +1,361 @@ +using System.Collections.Frozen; +using MineSharp.ChatComponent; +using MineSharp.Core.Common; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using static MineSharp.Protocol.Packets.Clientbound.Play.UpdateTeamsPacket; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Update Teams packet +/// +/// A unique name for the team +/// The data for the method type of this packet +public sealed record UpdateTeamsPacket(string TeamName, IUpdateTeamsMethod MethodData) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_Teams; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteString(TeamName); + buffer.WriteByte((byte)MethodData.MethodType); + MethodData.Write(buffer); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var teamName = buffer.ReadString(); + var method = (UpdateTeamsMethodType)buffer.ReadByte(); + var methodData = UpdateTeamsMethodRegistry.Read(buffer, method); + return new UpdateTeamsPacket(teamName, methodData); + } + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public enum UpdateTeamsMethodType + { + CreateTeam = 0, + RemoveTeam = 1, + UpdateTeamInfo = 2, + AddEntitiesToTeam = 3, + RemoveEntitiesFromTeam = 4 + } + + public interface IUpdateTeamsMethod + { + public UpdateTeamsMethodType MethodType { get; } + + public void Write(PacketBuffer buffer); + } + + public interface IUpdateTeamsMethodStatic + { + public static abstract UpdateTeamsMethodType StaticMethodType { get; } + + public static abstract IUpdateTeamsMethod Read(PacketBuffer buffer); + } + + public static class UpdateTeamsMethodRegistry + { + public static IUpdateTeamsMethod Read(PacketBuffer buffer, UpdateTeamsMethodType method) + { + if (!UpdateTeamsMethodFactories.TryGetValue(method, out var reader)) + { + throw new InvalidOperationException($"Unsupported UpdateTeamsMethodType: {method}"); + } + return reader(buffer); + } + + public static readonly FrozenDictionary> UpdateTeamsMethodFactories; + + static UpdateTeamsMethodRegistry() + { + UpdateTeamsMethodFactories = InitializeUpdateTeamsMethod(); + } + + private static FrozenDictionary> InitializeUpdateTeamsMethod() + { + var dict = new Dictionary>(); + + void Register() + where T : IUpdateTeamsMethod, IUpdateTeamsMethodStatic + { + var mask = T.StaticMethodType; + var factory = T.Read; + dict.Add(mask, factory); + } + + Register(); + Register(); + Register(); + Register(); + Register(); + + return dict.ToFrozenDictionary(); + } + } + + #region Parts + + /// + /// Marker interface for the parts of the UpdateTeams packet. + /// + public interface IUpdateTeamsPart + { + } + + public sealed record UpdateTeamsTeamInfoPart( + Chat TeamDisplayName, + byte FriendlyFlags, + NameTagVisibility NameTagVisibility, + CollisionRule CollisionRule, + TextColor TeamColor, + Chat TeamPrefix, + Chat TeamSuffix + ) : IUpdateTeamsPart, ISerializable + { + public void Write(PacketBuffer buffer) + { + buffer.WriteChatComponent(TeamDisplayName); + buffer.WriteByte(FriendlyFlags); + buffer.WriteString(NameTagVisibilityExtensions.ToUpdateTeamPacketString(NameTagVisibility)); + buffer.WriteString(CollisionRuleExtensions.ToUpdateTeamPacketString(CollisionRule)); + buffer.WriteVarInt((int)TeamColor); + buffer.WriteChatComponent(TeamPrefix); + buffer.WriteChatComponent(TeamSuffix); + } + + public static UpdateTeamsTeamInfoPart Read(PacketBuffer buffer) + { + var teamDisplayName = buffer.ReadChatComponent(); + var friendlyFlags = buffer.ReadByte(); + var nameTagVisibility = NameTagVisibilityExtensions.FromUpdateTeamPacketString(buffer.ReadString()); + var collisionRule = CollisionRuleExtensions.FromUpdateTeamPacketString(buffer.ReadString()); + var teamColor = (TextColor)buffer.ReadVarInt(); + var teamPrefix = buffer.ReadChatComponent(); + var teamSuffix = buffer.ReadChatComponent(); + return new UpdateTeamsTeamInfoPart(teamDisplayName, friendlyFlags, nameTagVisibility, collisionRule, teamColor, teamPrefix, teamSuffix); + } + } + + /// Identifiers entities. For players, this is their username; for other entities, it is their UUID. + public sealed record UpdateTeamsEntityInfoPart( + string[] Entities + ) : IUpdateTeamsPart, ISerializable + { + public void Write(PacketBuffer buffer) + { + buffer.WriteVarIntArray(Entities, (buffer, entity) => buffer.WriteString(entity)); + } + + public static UpdateTeamsEntityInfoPart Read(PacketBuffer buffer) + { + var entities = buffer.ReadVarIntArray((buffer) => buffer.ReadString()); + return new UpdateTeamsEntityInfoPart(entities); + } + } + + /// + /// Enum representing the visibility options for name tags in Minecraft. + /// + public enum NameTagVisibility + { + Always, + HideForOtherTeams, + HideForOwnTeam, + Never + } + + public static class NameTagVisibilityExtensions + { + private static readonly FrozenDictionary EnumToString = new Dictionary() + { + { NameTagVisibility.Always, "always" }, + { NameTagVisibility.HideForOtherTeams, "hideForOtherTeams" }, + { NameTagVisibility.HideForOwnTeam, "hideForOwnTeam" }, + { NameTagVisibility.Never, "never" } + }.ToFrozenDictionary(); + + private static readonly FrozenDictionary StringToEnum = EnumToString + .ToDictionary(kvp => kvp.Value, kvp => kvp.Key).ToFrozenDictionary(); + + // can't be an extension method because this class is not top level + public static string ToUpdateTeamPacketString(NameTagVisibility visibility) + { + return EnumToString[visibility]; + } + + public static NameTagVisibility FromUpdateTeamPacketString(string visibility) + { + if (StringToEnum.TryGetValue(visibility, out var enumValue)) + { + return enumValue; + } + throw new ArgumentException($"Invalid visibility string: {visibility}", nameof(visibility)); + } + } + + /// + /// Enum representing the collision rules in Minecraft. + /// + public enum CollisionRule + { + Always, + PushOtherTeams, + PushOwnTeam, + Never + } + + public static class CollisionRuleExtensions + { + private static readonly FrozenDictionary EnumToString = new Dictionary() + { + { CollisionRule.Always, "always" }, + { CollisionRule.PushOtherTeams, "pushOtherTeams" }, + { CollisionRule.PushOwnTeam, "pushOwnTeam" }, + { CollisionRule.Never, "never" } + }.ToFrozenDictionary(); + + private static readonly FrozenDictionary StringToEnum = EnumToString + .ToDictionary(kvp => kvp.Value, kvp => kvp.Key).ToFrozenDictionary(); + + // can't be an extension method because this class is not top level + public static string ToUpdateTeamPacketString(CollisionRule rule) + { + return EnumToString[rule]; + } + + public static CollisionRule FromUpdateTeamPacketString(string rule) + { + if (StringToEnum.TryGetValue(rule, out var enumValue)) + { + return enumValue; + } + throw new ArgumentException($"Invalid collision rule string: {rule}", nameof(rule)); + } + } + + #endregion + + public sealed record CreateTeam( + UpdateTeamsTeamInfoPart TeamInfoPart, + UpdateTeamsEntityInfoPart EntityInfoPart + ) : IUpdateTeamsMethod, IUpdateTeamsMethodStatic, ISerializable + { + public UpdateTeamsMethodType MethodType => StaticMethodType; + public static UpdateTeamsMethodType StaticMethodType => UpdateTeamsMethodType.CreateTeam; + + public void Write(PacketBuffer buffer) + { + TeamInfoPart.Write(buffer); + EntityInfoPart.Write(buffer); + } + + public static CreateTeam Read(PacketBuffer buffer) + { + var teamInfoPart = UpdateTeamsTeamInfoPart.Read(buffer); + var entityInfoPart = UpdateTeamsEntityInfoPart.Read(buffer); + return new(teamInfoPart, entityInfoPart); + } + + static IUpdateTeamsMethod IUpdateTeamsMethodStatic.Read(PacketBuffer buffer) + { + return Read(buffer); + } + } + + public sealed record RemoveTeam() + : IUpdateTeamsMethod, IUpdateTeamsMethodStatic, ISerializable + { + public UpdateTeamsMethodType MethodType => StaticMethodType; + public static UpdateTeamsMethodType StaticMethodType => UpdateTeamsMethodType.RemoveTeam; + + public void Write(PacketBuffer buffer) { } + + public static RemoveTeam Read(PacketBuffer buffer) => new(); + + static IUpdateTeamsMethod IUpdateTeamsMethodStatic.Read(PacketBuffer buffer) + { + return Read(buffer); + } + } + + public sealed record UpdateTeamInfo( + UpdateTeamsTeamInfoPart TeamInfoPart + ) : IUpdateTeamsMethod, IUpdateTeamsMethodStatic, ISerializable + { + public UpdateTeamsMethodType MethodType => StaticMethodType; + public static UpdateTeamsMethodType StaticMethodType => UpdateTeamsMethodType.UpdateTeamInfo; + + public void Write(PacketBuffer buffer) + { + TeamInfoPart.Write(buffer); + } + + public static UpdateTeamInfo Read(PacketBuffer buffer) + { + var teamInfoPart = UpdateTeamsTeamInfoPart.Read(buffer); + return new(teamInfoPart); + } + + static IUpdateTeamsMethod IUpdateTeamsMethodStatic.Read(PacketBuffer buffer) + { + return Read(buffer); + } + } + + public sealed record AddEntitiesToTeam( + UpdateTeamsEntityInfoPart EntityInfoPart + ) : IUpdateTeamsMethod, IUpdateTeamsMethodStatic, ISerializable + { + public UpdateTeamsMethodType MethodType => StaticMethodType; + public static UpdateTeamsMethodType StaticMethodType => UpdateTeamsMethodType.AddEntitiesToTeam; + + public void Write(PacketBuffer buffer) + { + EntityInfoPart.Write(buffer); + } + + public static AddEntitiesToTeam Read(PacketBuffer buffer) + { + var entityInfoPart = UpdateTeamsEntityInfoPart.Read(buffer); + return new(entityInfoPart); + } + + static IUpdateTeamsMethod IUpdateTeamsMethodStatic.Read(PacketBuffer buffer) + { + return Read(buffer); + } + } + + public sealed record RemoveEntitiesFromTeam( + UpdateTeamsEntityInfoPart EntityInfoPart + ) : IUpdateTeamsMethod, IUpdateTeamsMethodStatic, ISerializable + { + public UpdateTeamsMethodType MethodType => StaticMethodType; + public static UpdateTeamsMethodType StaticMethodType => UpdateTeamsMethodType.RemoveEntitiesFromTeam; + + public void Write(PacketBuffer buffer) + { + EntityInfoPart.Write(buffer); + } + + public static RemoveEntitiesFromTeam Read(PacketBuffer buffer) + { + var entityInfoPart = UpdateTeamsEntityInfoPart.Read(buffer); + return new(entityInfoPart); + } + + static IUpdateTeamsMethod IUpdateTeamsMethodStatic.Read(PacketBuffer buffer) + { + return Read(buffer); + } + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateTimePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateTimePacket.cs new file mode 100644 index 00000000..30d253eb --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/UpdateTimePacket.cs @@ -0,0 +1,38 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Update Time packet +/// +/// Time is based on ticks, where 20 ticks happen every second. There are 24000 ticks in a day, making Minecraft days exactly 20 minutes long. +/// The time of day is based on the timestamp modulo 24000. 0 is sunrise, 6000 is noon, 12000 is sunset, and 18000 is midnight. +/// The default SMP server increments the time by 20 every second. +/// +/// The world age in ticks +/// The time of day in ticks +public sealed record UpdateTimePacket(long WorldAge, long TimeOfDay) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_UpdateTime; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteLong(WorldAge); + buffer.WriteLong(TimeOfDay); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var worldAge = buffer.ReadLong(); + var timeOfDay = buffer.ReadLong(); + + return new UpdateTimePacket(worldAge, timeOfDay); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/WindowItemsPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/WindowItemsPacket.cs index da20b99c..a4d0f81e 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/WindowItemsPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/WindowItemsPacket.cs @@ -1,27 +1,26 @@ -using MineSharp.Core.Common; -using MineSharp.Core.Common.Items; +using MineSharp.Core.Common.Items; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; using MineSharp.Protocol.Packets.NetworkTypes; namespace MineSharp.Protocol.Packets.Clientbound.Play; -#pragma warning disable CS1591 -public class WindowItemsPacket : IPacket -{ - public WindowItemsPacket(byte windowId, int stateId, Item?[] items, Item? selectedItem) - { - WindowId = windowId; - StateId = stateId; - Items = items; - SelectedItem = selectedItem; - } - public byte WindowId { get; set; } - public int StateId { get; set; } - public Item?[] Items { get; set; } - public Item? SelectedItem { get; set; } - public PacketType Type => PacketType.CB_Play_WindowItems; +/// +/// Represents a packet containing window items. +/// +/// The ID of the window. +/// The state ID of the window. +/// The items in the window. +/// The selected item in the window. +public sealed record WindowItemsPacket(byte WindowId, int StateId, Item?[] Items, Item? SelectedItem) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_WindowItems; + /// public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteByte(WindowId); @@ -30,6 +29,7 @@ public void Write(PacketBuffer buffer, MinecraftData version) buffer.WriteOptionalItem(SelectedItem); } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { return new WindowItemsPacket( @@ -39,4 +39,4 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) buffer.ReadOptionalItem(version)); } } -#pragma warning restore CS1591 + diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/WindowSetSlotPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/WindowSetSlotPacket.cs index 78358b3f..71dea1c8 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Play/WindowSetSlotPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/WindowSetSlotPacket.cs @@ -1,24 +1,25 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; using MineSharp.Protocol.Packets.NetworkTypes; namespace MineSharp.Protocol.Packets.Clientbound.Play; -#pragma warning disable CS1591 -public class WindowSetSlotPacket : IPacket -{ - public WindowSetSlotPacket(sbyte windowId, int stateId, Slot slot) - { - WindowId = windowId; - StateId = stateId; - Slot = slot; - } - public sbyte WindowId { get; set; } - public int StateId { get; set; } - public Slot Slot { get; set; } - public PacketType Type => PacketType.CB_Play_SetSlot; +/// +/// Represents a packet that sets a slot in a window. +/// +/// The ID of the window. +/// The state ID of the window. +/// The slot to be set. +public sealed record WindowSetSlotPacket(sbyte WindowId, int StateId, Slot Slot) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_SetSlot; + /// public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteSByte(WindowId); @@ -26,6 +27,7 @@ public void Write(PacketBuffer buffer, MinecraftData version) buffer.WriteSlot(Slot); } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { return new WindowSetSlotPacket( @@ -34,4 +36,4 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) buffer.ReadSlot(version)); } } -#pragma warning restore CS1591 + diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Play/WorldEventPacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Play/WorldEventPacket.cs new file mode 100644 index 00000000..5c125e0a --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Play/WorldEventPacket.cs @@ -0,0 +1,143 @@ +using MineSharp.Core.Common; +using MineSharp.Core.Geometry; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using static MineSharp.Protocol.Packets.Clientbound.Play.WorldEventPacket; + +namespace MineSharp.Protocol.Packets.Clientbound.Play; + +/// +/// Sent when a client is to play a sound or particle effect. +/// +/// The event, see below. +/// The location of the event. +/// Extra data for certain events, see below. +/// +/// If true, the effect is played from 2 blocks away in the correct direction, ignoring distance-based volume adjustment. +/// +public sealed record WorldEventPacket(EventType Event, Position Location, int Data, bool DisableRelativeVolume) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Play_WorldEvent; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteInt((int)Event); + buffer.WritePosition(Location); + buffer.WriteInt(Data); + buffer.WriteBool(DisableRelativeVolume); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var eventType = (EventType)buffer.ReadInt(); + var location = buffer.ReadPosition(); + var data = buffer.ReadInt(); + var disableRelativeVolume = buffer.ReadBool(); + + return new WorldEventPacket(eventType, location, data, disableRelativeVolume); + } + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + /// + /// Enum representing the possible events. + /// + public enum EventType + { + #region Sound Events + DispenserDispenses = 1000, + DispenserFailsToDispense = 1001, + DispenserShoots = 1002, + EnderEyeLaunched = 1003, + FireworkShot = 1004, + FireExtinguished = 1009, + /// + /// Meaning of Data field: + /// An ID in the minecraft:item registry, corresponding to a record item. + /// If the ID doesn't correspond to a record, the packet is ignored. + /// Any record already being played at the given location is overwritten. + /// See Data Generators for information on item IDs. + /// + PlayRecord = 1010, + StopRecord = 1011, + GhastWarns = 1015, + GhastShoots = 1016, + EnderdragonShoots = 1017, + BlazeShoots = 1018, + ZombieAttacksWoodDoor = 1019, + ZombieAttacksIronDoor = 1020, + ZombieBreaksWoodDoor = 1021, + WitherBreaksBlock = 1022, + WitherSpawned = 1023, + WitherShoots = 1024, + BatTakesOff = 1025, + ZombieInfects = 1026, + ZombieVillagerConverted = 1027, + EnderDragonDeath = 1028, + AnvilDestroyed = 1029, + AnvilUsed = 1030, + AnvilLanded = 1031, + PortalTravel = 1032, + ChorusFlowerGrown = 1033, + ChorusFlowerDied = 1034, + BrewingStandBrewed = 1035, + IronTrapdoorOpened = 1036, + IronTrapdoorClosed = 1037, + EndPortalCreatedInOverworld = 1038, + PhantomBites = 1039, + ZombieConvertsToDrowned = 1040, + HuskConvertsToZombieByDrowning = 1041, + GrindstoneUsed = 1042, + BookPageTurned = 1043, + #endregion + + #region Particle Events + ComposterComposts = 1500, + LavaConvertsBlock = 1501, + RedstoneTorchBurnsOut = 1502, + EnderEyePlaced = 1503, + /// + /// Meaning of Data field: + /// See + /// + SpawnsSmokeParticles = 2000, + /// + /// Meaning of Data field: + /// Block state ID (see Chunk Format#Block state registry). + /// + BlockBreak = 2001, + /// + /// Meaning of Data field: + /// RGB color as an integer (e.g. 8364543 for #7FA1FF). + /// + SplashPotion = 2002, + EyeOfEnderBreak = 2003, + MobSpawnParticleEffect = 2004, + /// + /// Meaning of Data field: + /// How many particles to spawn (if set to 0, 15 are spawned). + /// + BonemealParticles = 2005, + DragonBreath = 2006, + /// + /// Meaning of Data field: + /// RGB color as an integer (e.g. 8364543 for #7FA1FF). + /// + InstantSplashPotion = 2007, + EnderDragonDestroysBlock = 2008, + WetSpongeVaporizesInNether = 2009, + EndGatewaySpawn = 3000, + EnderdragonGrowl = 3001, + ElectricSpark = 3002, + CopperApplyWax = 3003, + CopperRemoveWax = 3004, + CopperScrapeOxidation = 3005 + #endregion + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Status/PingResponsePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Status/PingResponsePacket.cs new file mode 100644 index 00000000..1d9b5862 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Status/PingResponsePacket.cs @@ -0,0 +1,30 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Clientbound.Status; +#pragma warning disable CS1591 +/// +/// Packet for ping response +/// +/// The payload of the ping response +public sealed record PingResponsePacket(long Payload) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Status_Ping; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteLong(Payload); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + return new PingResponsePacket(buffer.ReadLong()); + } +} +#pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Status/PongResponsePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Status/PongResponsePacket.cs deleted file mode 100644 index 97eb6e7b..00000000 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Status/PongResponsePacket.cs +++ /dev/null @@ -1,27 +0,0 @@ -using MineSharp.Core.Common; -using MineSharp.Data; -using MineSharp.Data.Protocol; - -namespace MineSharp.Protocol.Packets.Clientbound.Status; -#pragma warning disable CS1591 -public class PongResponsePacket : IPacket -{ - public PongResponsePacket(long payload) - { - Payload = payload; - } - - public long Payload { get; set; } - public PacketType Type => PacketType.CB_Status_Ping; - - public void Write(PacketBuffer buffer, MinecraftData version) - { - buffer.WriteLong(Payload); - } - - public static IPacket Read(PacketBuffer buffer, MinecraftData version) - { - return new PongResponsePacket(buffer.ReadLong()); - } -} -#pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Clientbound/Status/StatusResponsePacket.cs b/Components/MineSharp.Protocol/Packets/Clientbound/Status/StatusResponsePacket.cs index a3073850..f8c7ca30 100644 --- a/Components/MineSharp.Protocol/Packets/Clientbound/Status/StatusResponsePacket.cs +++ b/Components/MineSharp.Protocol/Packets/Clientbound/Status/StatusResponsePacket.cs @@ -1,24 +1,27 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Clientbound.Status; #pragma warning disable CS1591 -public class StatusResponsePacket : IPacket +/// +/// Packet for server status response +/// +/// The server response +public sealed record StatusResponsePacket(string Response) : IPacket { - public StatusResponsePacket(string response) - { - Response = response; - } - - public string Response { get; set; } - public PacketType Type => PacketType.CB_Status_ServerInfo; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.CB_Status_ServerInfo; + /// public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteString(Response); } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { return new StatusResponsePacket(buffer.ReadString()); diff --git a/Components/MineSharp.Protocol/Packets/Handlers/ConfigurationPacketHandler.cs b/Components/MineSharp.Protocol/Packets/Handlers/ConfigurationPacketHandler.cs index 26bed969..ad030579 100644 --- a/Components/MineSharp.Protocol/Packets/Handlers/ConfigurationPacketHandler.cs +++ b/Components/MineSharp.Protocol/Packets/Handlers/ConfigurationPacketHandler.cs @@ -3,26 +3,29 @@ using MineSharp.Data.Protocol; using MineSharp.Protocol.Packets.Clientbound.Configuration; using MineSharp.Protocol.Packets.Serverbound.Configuration; -using NLog; using FinishConfigurationPacket = MineSharp.Protocol.Packets.Clientbound.Configuration.FinishConfigurationPacket; using KeepAlivePacket = MineSharp.Protocol.Packets.Clientbound.Configuration.KeepAlivePacket; namespace MineSharp.Protocol.Packets.Handlers; -internal class ConfigurationPacketHandler : IPacketHandler +internal sealed class ConfigurationPacketHandler : GameStatePacketHandler { - private static readonly ILogger Logger = LogManager.GetCurrentClassLogger(); - private readonly MinecraftClient client; private readonly MinecraftData data; public ConfigurationPacketHandler(MinecraftClient client, MinecraftData data) + : base(GameState.Configuration) { this.client = client; this.data = data; } - public Task HandleIncoming(IPacket packet) + public override Task StateEntered() + { + return client.SendClientInformationPacket(GameState); + } + + public override Task HandleIncoming(IPacket packet) { return packet switch { @@ -35,17 +38,7 @@ public Task HandleIncoming(IPacket packet) }; } - public Task HandleOutgoing(IPacket packet) - { - if (packet is Serverbound.Configuration.FinishConfigurationPacket) - { - client.UpdateGameState(GameState.Play); - } - - return Task.CompletedTask; - } - - public bool HandlesIncoming(PacketType type) + public override bool HandlesIncoming(PacketType type) { return type is PacketType.CB_Configuration_Disconnect or PacketType.CB_Configuration_FinishConfiguration @@ -60,21 +53,19 @@ private Task HandleDisconnect(DisconnectPacket packet) return Task.CompletedTask; } - private Task HandleFinishConfiguration(FinishConfigurationPacket packet) + private async Task HandleFinishConfiguration(FinishConfigurationPacket packet) { - _ = client.SendPacket(new Serverbound.Configuration.FinishConfigurationPacket()); - return Task.CompletedTask; + await client.SendPacket(new Serverbound.Configuration.FinishConfigurationPacket()); + await client.ChangeGameState(GameState.Play); } private Task HandleKeepAlive(KeepAlivePacket packet) { - client.SendPacket(new Serverbound.Configuration.KeepAlivePacket(packet.KeepAliveId)); - return Task.CompletedTask; + return client.SendPacket(new Serverbound.Configuration.KeepAlivePacket(packet.KeepAliveId)); } private Task HandlePing(PingPacket packet) { - client.SendPacket(new PongPacket(packet.Id)); - return Task.CompletedTask; + return client.SendPacket(new PongPacket(packet.Id)); } } diff --git a/Components/MineSharp.Protocol/Packets/Handlers/GameStatePacketHandler.cs b/Components/MineSharp.Protocol/Packets/Handlers/GameStatePacketHandler.cs new file mode 100644 index 00000000..d23c5d0c --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Handlers/GameStatePacketHandler.cs @@ -0,0 +1,21 @@ +using MineSharp.Core.Common.Protocol; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Handlers; + +internal abstract class GameStatePacketHandler +{ + public readonly GameState GameState; + + protected GameStatePacketHandler(GameState gameState) + { + GameState = gameState; + } + + public virtual Task StateEntered() + { + return Task.CompletedTask; + } + public abstract Task HandleIncoming(IPacket packet); + public abstract bool HandlesIncoming(PacketType type); +} diff --git a/Components/MineSharp.Protocol/Packets/Handlers/HandshakePacketHandler.cs b/Components/MineSharp.Protocol/Packets/Handlers/HandshakePacketHandler.cs index fe076364..b0dfc443 100644 --- a/Components/MineSharp.Protocol/Packets/Handlers/HandshakePacketHandler.cs +++ b/Components/MineSharp.Protocol/Packets/Handlers/HandshakePacketHandler.cs @@ -1,42 +1,25 @@ -using MineSharp.Data.Protocol; -using MineSharp.Protocol.Exceptions; -using MineSharp.Protocol.Packets.Serverbound.Handshaking; +using MineSharp.Core.Common.Protocol; +using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Handlers; -internal class HandshakePacketHandler : IPacketHandler +internal sealed class HandshakePacketHandler : GameStatePacketHandler { private readonly MinecraftClient client; public HandshakePacketHandler(MinecraftClient client) + : base(GameState.Handshaking) { this.client = client; } - public Task HandleIncoming(IPacket packet) + public override Task HandleIncoming(IPacket packet) { return Task.CompletedTask; } - public Task HandleOutgoing(IPacket packet) - { - return packet switch - { - HandshakePacket handshake => HandleHandshake(handshake), - _ => throw new UnexpectedPacketException( - $"unexpected outgoing packet during handshaking: {packet.GetType().FullName}") - }; - } - - public bool HandlesIncoming(PacketType type) + public override bool HandlesIncoming(PacketType type) { return false; } - - - private Task HandleHandshake(HandshakePacket packet) - { - client.UpdateGameState(packet.NextState); - return Task.CompletedTask; - } } diff --git a/Components/MineSharp.Protocol/Packets/Handlers/IPacketHandler.cs b/Components/MineSharp.Protocol/Packets/Handlers/IPacketHandler.cs deleted file mode 100644 index f099b29b..00000000 --- a/Components/MineSharp.Protocol/Packets/Handlers/IPacketHandler.cs +++ /dev/null @@ -1,11 +0,0 @@ -using MineSharp.Data.Protocol; - -namespace MineSharp.Protocol.Packets.Handlers; - -internal interface IPacketHandler -{ - public Task HandleIncoming(IPacket packet); - public Task HandleOutgoing(IPacket packet); - - public bool HandlesIncoming(PacketType type); -} diff --git a/Components/MineSharp.Protocol/Packets/Handlers/LoginPacketHandler.cs b/Components/MineSharp.Protocol/Packets/Handlers/LoginPacketHandler.cs index 7fbfa5c7..ad4af480 100644 --- a/Components/MineSharp.Protocol/Packets/Handlers/LoginPacketHandler.cs +++ b/Components/MineSharp.Protocol/Packets/Handlers/LoginPacketHandler.cs @@ -2,8 +2,8 @@ using System.Security.Cryptography; using MineSharp.Auth.Exceptions; using MineSharp.Core; -using MineSharp.Core.Common; using MineSharp.Core.Common.Protocol; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; using MineSharp.Protocol.Cryptography; @@ -13,7 +13,7 @@ namespace MineSharp.Protocol.Packets.Handlers; -internal class LoginPacketHandler : IPacketHandler +internal sealed class LoginPacketHandler : GameStatePacketHandler { private static readonly ILogger Logger = LogManager.GetCurrentClassLogger(); @@ -21,29 +21,32 @@ internal class LoginPacketHandler : IPacketHandler private readonly MinecraftData data; public LoginPacketHandler(MinecraftClient client, MinecraftData data) + : base(GameState.Login) { this.client = client; this.data = data; } - public Task HandleIncoming(IPacket packet) + public override Task StateEntered() + { + var login = HandshakeProtocol.GetLoginPacket(data, client.Session); + return client.SendPacket(login); + } + + public override Task HandleIncoming(IPacket packet) { return packet switch { DisconnectPacket disconnect => HandleDisconnect(disconnect), EncryptionRequestPacket encryption => HandleEncryptionRequest(encryption), SetCompressionPacket compression => HandleSetCompression(compression), + // TODO: handle LoginPluginRequestPacket LoginSuccessPacket success => HandleLoginSuccess(success), _ => throw new UnreachableException() }; } - public Task HandleOutgoing(IPacket packet) - { - return Task.CompletedTask; - } - - public bool HandlesIncoming(PacketType type) + public override bool HandlesIncoming(PacketType type) { return type is PacketType.CB_Login_Disconnect or PacketType.CB_Login_EncryptionBegin @@ -107,8 +110,8 @@ private async Task HandleEncryptionRequest(EncryptionRequestPacket packet) response = new(sharedSecret, encVerToken, null); } - _ = client.SendPacket(response) - .ContinueWith(_ => client.EnableEncryption(aes.Key)); + await client.SendPacket(response); + client.EnableEncryption(aes.Key); } private Task HandleSetCompression(SetCompressionPacket packet) @@ -118,16 +121,16 @@ private Task HandleSetCompression(SetCompressionPacket packet) return Task.CompletedTask; } - private Task HandleLoginSuccess(LoginSuccessPacket packet) + private async Task HandleLoginSuccess(LoginSuccessPacket packet) { if (data.Version.Protocol < ProtocolVersion.V_1_20_2) { - client.UpdateGameState(GameState.Play); - return Task.CompletedTask; + await client.ChangeGameState(GameState.Play); + } + else + { + await client.SendPacket(new LoginAcknowledgedPacket()); + await client.ChangeGameState(GameState.Configuration); } - - _ = client.SendPacket(new AcknowledgeLoginPacket()) - .ContinueWith(_ => client.UpdateGameState(GameState.Configuration)); - return Task.CompletedTask; } } diff --git a/Components/MineSharp.Protocol/Packets/Handlers/NoStatePacketHandler.cs b/Components/MineSharp.Protocol/Packets/Handlers/NoStatePacketHandler.cs new file mode 100644 index 00000000..d26c7074 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Handlers/NoStatePacketHandler.cs @@ -0,0 +1,25 @@ +using MineSharp.Core.Common.Protocol; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Handlers; + +internal sealed class NoStatePacketHandler : GameStatePacketHandler +{ + private readonly MinecraftClient client; + + public NoStatePacketHandler(MinecraftClient client) + : base(GameState.None) + { + this.client = client; + } + + public override Task HandleIncoming(IPacket packet) + { + return Task.CompletedTask; + } + + public override bool HandlesIncoming(PacketType type) + { + return false; + } +} diff --git a/Components/MineSharp.Protocol/Packets/Handlers/PlayPacketHandler.cs b/Components/MineSharp.Protocol/Packets/Handlers/PlayPacketHandler.cs index f09187e6..7765e370 100644 --- a/Components/MineSharp.Protocol/Packets/Handlers/PlayPacketHandler.cs +++ b/Components/MineSharp.Protocol/Packets/Handlers/PlayPacketHandler.cs @@ -1,4 +1,6 @@ -using MineSharp.Data; +using MineSharp.Core; +using MineSharp.Core.Common.Protocol; +using MineSharp.Data; using MineSharp.Data.Protocol; using MineSharp.Protocol.Packets.Clientbound.Play; using MineSharp.Protocol.Packets.Serverbound.Play; @@ -6,56 +8,50 @@ namespace MineSharp.Protocol.Packets.Handlers; -internal class PlayPacketHandler : IPacketHandler +internal sealed class PlayPacketHandler : GameStatePacketHandler { private readonly MinecraftClient client; private readonly MinecraftData data; public PlayPacketHandler(MinecraftClient client, MinecraftData data) + : base(GameState.Play) { this.client = client; this.data = data; } - public Task HandleIncoming(IPacket packet) + public override Task StateEntered() + { + client.GameJoinedTcs.SetResult(); + return Task.CompletedTask; + } + + public override Task HandleIncoming(IPacket packet) { return packet switch { KeepAlivePacket keepAlive => HandleKeepAlive(keepAlive), - BundleDelimiterPacket bundleDelimiter => HandleBundleDelimiter(bundleDelimiter), PingPacket ping => HandlePing(ping), DisconnectPacket disconnect => HandleDisconnect(disconnect), + LoginPacket login => HandleLogin(login), _ => Task.CompletedTask }; } - public Task HandleOutgoing(IPacket packet) - { - return Task.CompletedTask; - } - - public bool HandlesIncoming(PacketType type) + public override bool HandlesIncoming(PacketType type) { - return type is PacketType.CB_Play_KeepAlive or PacketType.CB_Play_BundleDelimiter or PacketType.CB_Play_Ping - or PacketType.CB_Play_KickDisconnect; + return type is PacketType.CB_Play_KeepAlive or PacketType.CB_Play_Ping + or PacketType.CB_Play_KickDisconnect or PacketType.CB_Play_Login; } private Task HandleKeepAlive(KeepAlivePacket packet) { - client.SendPacket(new Serverbound.Play.KeepAlivePacket(packet.KeepAliveId)); - return Task.CompletedTask; - } - - private Task HandleBundleDelimiter(BundleDelimiterPacket bundleDelimiter) - { - client.HandleBundleDelimiter(); - return Task.CompletedTask; + return client.SendPacket(new Serverbound.Play.KeepAlivePacket(packet.KeepAliveId)); } private Task HandlePing(PingPacket ping) { - client.SendPacket(new PongPacket(ping.Id)); - return Task.CompletedTask; + return client.SendPacket(new PongPacket(ping.Id)); } private Task HandleDisconnect(DisconnectPacket packet) @@ -63,4 +59,13 @@ private Task HandleDisconnect(DisconnectPacket packet) _ = Task.Run(() => client.Disconnect(packet.Reason)); return Task.CompletedTask; } + + private Task HandleLogin(LoginPacket packet) + { + return data.Version.Protocol switch + { + <= ProtocolVersion.V_1_20 => client.SendClientInformationPacket(GameState), + _ => Task.CompletedTask + }; + } } diff --git a/Components/MineSharp.Protocol/Packets/Handlers/StatusPacketHandler.cs b/Components/MineSharp.Protocol/Packets/Handlers/StatusPacketHandler.cs index 4956f985..a47cd10a 100644 --- a/Components/MineSharp.Protocol/Packets/Handlers/StatusPacketHandler.cs +++ b/Components/MineSharp.Protocol/Packets/Handlers/StatusPacketHandler.cs @@ -1,27 +1,24 @@ -using MineSharp.Data.Protocol; +using MineSharp.Core.Common.Protocol; +using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Handlers; -internal class StatusPacketHandler : IPacketHandler +internal sealed class StatusPacketHandler : GameStatePacketHandler { - private MinecraftClient client; + private readonly MinecraftClient client; public StatusPacketHandler(MinecraftClient client) + : base(GameState.Status) { this.client = client; } - public Task HandleIncoming(IPacket packet) + public override Task HandleIncoming(IPacket packet) { return Task.CompletedTask; } - public Task HandleOutgoing(IPacket packet) - { - return Task.CompletedTask; - } - - public bool HandlesIncoming(PacketType type) + public override bool HandlesIncoming(PacketType type) { return false; } diff --git a/Components/MineSharp.Protocol/Packets/HandshakeProtocol.cs b/Components/MineSharp.Protocol/Packets/HandshakeProtocol.cs index d56d188e..d7323ea0 100644 --- a/Components/MineSharp.Protocol/Packets/HandshakeProtocol.cs +++ b/Components/MineSharp.Protocol/Packets/HandshakeProtocol.cs @@ -12,24 +12,23 @@ internal static class HandshakeProtocol { public static async Task PerformHandshake(MinecraftClient client, GameState next, MinecraftData data) { - if (next is GameState.Play or GameState.Handshaking) + if (!(next is GameState.Status or GameState.Login)) { throw new ArgumentException($"{nameof(next)} must either be {GameState.Status} or {GameState.Login}"); } var handshake = new HandshakePacket(data.Version.Protocol, client.Hostname, client.Port, next); + await client.ChangeGameState(GameState.Handshaking); await client.SendPacket(handshake); + await client.ChangeGameState(next); if (next == GameState.Status) { return; } - - var login = GetLoginPacket(data, client.Session); - await client.SendPacket(login); } - private static LoginStartPacket GetLoginPacket(MinecraftData data, Session session) + internal static LoginStartPacket GetLoginPacket(MinecraftData data, Session session) { LoginStartPacket.SignatureContainer? signature = null; diff --git a/Components/MineSharp.Protocol/Packets/IPacket.cs b/Components/MineSharp.Protocol/Packets/IPacket.cs index 8a0b9797..709e534e 100644 --- a/Components/MineSharp.Protocol/Packets/IPacket.cs +++ b/Components/MineSharp.Protocol/Packets/IPacket.cs @@ -1,19 +1,27 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets; /// -/// Represents a minecraft packet +/// Represents a Minecraft packet /// public interface IPacket { /// /// The corresponding + /// + /// /// public PacketType Type { get; } + /// + /// The corresponding . + /// The same as but static. + /// + public static abstract PacketType StaticType { get; } + /// /// Serialize the packet data into the buffer. /// diff --git a/Components/MineSharp.Protocol/Packets/NetworkTypes/ChatMessageItem.cs b/Components/MineSharp.Protocol/Packets/NetworkTypes/ChatMessageItem.cs index 0ae63272..252cdd30 100644 --- a/Components/MineSharp.Protocol/Packets/NetworkTypes/ChatMessageItem.cs +++ b/Components/MineSharp.Protocol/Packets/NetworkTypes/ChatMessageItem.cs @@ -1,13 +1,13 @@ using MineSharp.Core; using MineSharp.Core.Common; -using MineSharp.Data; +using MineSharp.Core.Serialization; namespace MineSharp.Protocol.Packets.NetworkTypes; /// /// Represents a signed chat message /// -public class ChatMessageItem +public class ChatMessageItem : ISerializable { /// /// Creates a new instance @@ -50,9 +50,9 @@ private ChatMessageItem(Uuid? sender, byte[]? signature) /// /// /// - public void Write(PacketBuffer buffer, MinecraftData version) + public void Write(PacketBuffer buffer) { - if (version.Version.Protocol == ProtocolVersion.V_1_19_2) + if (buffer.ProtocolVersion == ProtocolVersion.V_1_19_2) { buffer.WriteUuid(Sender!.Value); buffer.WriteVarInt(Signature!.Length); @@ -74,12 +74,12 @@ public void Write(PacketBuffer buffer, MinecraftData version) /// /// /// - public static ChatMessageItem Read(PacketBuffer buffer, MinecraftData version) + public static ChatMessageItem Read(PacketBuffer buffer) { Uuid? uuid = null; byte[]? signature = null; - if (version.Version.Protocol == ProtocolVersion.V_1_19_2) + if (buffer.ProtocolVersion == ProtocolVersion.V_1_19_2) { uuid = buffer.ReadUuid(); signature = new byte[buffer.ReadVarInt()]; diff --git a/Components/MineSharp.Protocol/Packets/NetworkTypes/CoreTypeExtensions.cs b/Components/MineSharp.Protocol/Packets/NetworkTypes/CoreTypeExtensions.cs index ce0b2feb..58579a7b 100644 --- a/Components/MineSharp.Protocol/Packets/NetworkTypes/CoreTypeExtensions.cs +++ b/Components/MineSharp.Protocol/Packets/NetworkTypes/CoreTypeExtensions.cs @@ -1,5 +1,6 @@ using MineSharp.Core.Common; using MineSharp.Core.Common.Items; +using MineSharp.Core.Serialization; using MineSharp.Data; namespace MineSharp.Protocol.Packets.NetworkTypes; diff --git a/Components/MineSharp.Protocol/Packets/NetworkTypes/DifficultyLevel.cs b/Components/MineSharp.Protocol/Packets/NetworkTypes/DifficultyLevel.cs new file mode 100644 index 00000000..a462eee7 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/NetworkTypes/DifficultyLevel.cs @@ -0,0 +1,14 @@ +namespace MineSharp.Protocol.Packets.NetworkTypes; + +/// +/// Enum representing the difficulty levels +/// +public enum DifficultyLevel : byte +{ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + Peaceful = 0, + Easy = 1, + Normal = 2, + Hard = 3 +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} diff --git a/Components/MineSharp.Protocol/Packets/NetworkTypes/EntityMetadata.cs b/Components/MineSharp.Protocol/Packets/NetworkTypes/EntityMetadata.cs new file mode 100644 index 00000000..5d270a49 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/NetworkTypes/EntityMetadata.cs @@ -0,0 +1,447 @@ +using System.Collections.Frozen; +using System.Diagnostics.Contracts; +using fNbt; +using MineSharp.ChatComponent; +using MineSharp.Core.Common; +using MineSharp.Core.Common.Items; +using MineSharp.Core.Geometry; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using static MineSharp.Protocol.Packets.NetworkTypes.SnifferStateMetadata; + +namespace MineSharp.Protocol.Packets.NetworkTypes; + +/// +/// Represents the metadata of an entity. +/// +/// The metadata entries. +public sealed record EntityMetadata(EntityMetadataEntry[] Entries) : ISerializableWithMinecraftData +{ + /// + /// Represents the end of metadata index. + /// + public const byte EndOfMetadataIndex = 0xff; + + /// + public void Write(PacketBuffer buffer, MinecraftData data) + { + foreach (var entry in Entries) + { + entry.Write(buffer, data); + } + // Write the terminating entry + buffer.WriteByte(EndOfMetadataIndex); + } + + /// + public static EntityMetadata Read(PacketBuffer buffer, MinecraftData data) + { + var entries = new List(); + while (true) + { + // TODO: Some metadata is broken and causes exceptions + var index = buffer.Peek(); + if (index == EndOfMetadataIndex) + { + buffer.ReadByte(); // Consume the index + break; + } + + var entry = EntityMetadataEntry.Read(buffer, data); + entries.Add(entry); + } + return new EntityMetadata(entries.ToArray()); + } +} + +/// +/// Represents a entity metadata entry. +/// +public sealed record EntityMetadataEntry(byte Index, int Type, IMetadataValue Value) : ISerializableWithMinecraftData +{ + /// + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteByte(Index); + buffer.WriteVarInt(Type); + Value!.Write(buffer, data); + } + + /// + public static EntityMetadataEntry Read(PacketBuffer buffer, MinecraftData data) + { + var index = buffer.ReadByte(); + var type = buffer.ReadVarInt(); + var value = MetadataValueFactory.Create(type, buffer, data); + + return new(index, type, value); + } +} + +/// +/// Factory for creating metadata values based on type. +/// +public static class MetadataValueFactory +{ + private static readonly FrozenDictionary> TypeToReadDelegate = new Dictionary>() + { + { 0, ByteMetadata.Read }, + { 1, VarIntMetadata.Read }, + { 2, VarLongMetadata.Read }, + { 3, FloatMetadata.Read }, + { 4, StringMetadata.Read }, + { 5, TextComponentMetadata.Read }, + { 6, OptionalTextComponentMetadata.Read }, + { 7, SlotMetadata.Read }, + { 8, BooleanMetadata.Read }, + { 9, RotationsMetadata.Read }, + { 10, PositionMetadata.Read }, + { 11, OptionalPositionMetadata.Read }, + { 12, DirectionMetadata.Read }, + { 13, OptionalUuidMetadata.Read }, + { 14, BlockStateMetadata.Read }, + { 15, OptionalBlockStateMetadata.Read }, + { 16, NbtMetadata.Read }, + { 17, ParticleMetadata.Read }, + { 18, VillagerDataMetadata.Read }, + { 19, OptionalVarIntMetadata.Read }, + { 20, PoseMetadata.Read }, + { 21, CatVariantMetadata.Read }, + { 22, FrogVariantMetadata.Read }, + { 23, OptionalGlobalPositionMetadata.Read }, + { 24, PaintingVariantMetadata.Read }, + { 25, SnifferStateMetadata.Read }, + { 26, Vector3Metadata.Read }, + { 27, QuaternionMetadata.Read }, + }.ToFrozenDictionary(); + + /// + /// Creates an instance of based on the provided type. + /// + /// The type of the metadata value. + /// The packet buffer to read from. + /// The Minecraft data context. + /// An instance of . + /// Thrown when the type is unknown. + public static IMetadataValue Create(int type, PacketBuffer buffer, MinecraftData data) + { + if (TypeToReadDelegate.TryGetValue(type, out var readDelegate)) + { + return readDelegate(buffer, data); + } + + throw new ArgumentOutOfRangeException(nameof(type), $"Unknown metadata type: {type}"); + } +} + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +/// +/// Interface for metadata values. +/// +public interface IMetadataValue +{ + public abstract void Write(PacketBuffer buffer, MinecraftData data); +} + +public sealed record ByteMetadata(byte Value) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) => buffer.WriteByte(Value); + public static ByteMetadata Read(PacketBuffer buffer, MinecraftData data) => new(buffer.ReadByte()); +} + +public sealed record VarIntMetadata(int Value) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) => buffer.WriteVarInt(Value); + public static VarIntMetadata Read(PacketBuffer buffer, MinecraftData data) => new(buffer.ReadVarInt()); +} + +public sealed record VarLongMetadata(long Value) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) => buffer.WriteVarLong(Value); + public static VarLongMetadata Read(PacketBuffer buffer, MinecraftData data) => new(buffer.ReadVarLong()); +} + +public sealed record FloatMetadata(float Value) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) => buffer.WriteFloat(Value); + public static FloatMetadata Read(PacketBuffer buffer, MinecraftData data) => new(buffer.ReadFloat()); +} + +public sealed record StringMetadata(string Value) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) => buffer.WriteString(Value); + public static StringMetadata Read(PacketBuffer buffer, MinecraftData data) => new(buffer.ReadString()); +} + +public sealed record TextComponentMetadata(Chat Value) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) => buffer.WriteChatComponent(Value); + public static TextComponentMetadata Read(PacketBuffer buffer, MinecraftData data) => new(buffer.ReadChatComponent()); +} + +public sealed record OptionalTextComponentMetadata(Chat? Value) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) + { + var hasValue = Value != null; + buffer.WriteBool(hasValue); + if (hasValue) buffer.WriteChatComponent(Value!); + } + + public static OptionalTextComponentMetadata Read(PacketBuffer buffer, MinecraftData data) + { + var hasValue = buffer.ReadBool(); + var value = hasValue ? buffer.ReadChatComponent() : null; + return new(value); + } +} + +public sealed record SlotMetadata(Item? Value) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) => buffer.WriteOptionalItem(Value); + public static SlotMetadata Read(PacketBuffer buffer, MinecraftData data) => new(buffer.ReadOptionalItem(data)); +} + +public sealed record BooleanMetadata(bool Value) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) => buffer.WriteBool(Value); + public static BooleanMetadata Read(PacketBuffer buffer, MinecraftData data) => new(buffer.ReadBool()); +} + +public sealed record RotationsMetadata(float X, float Y, float Z) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteFloat(X); + buffer.WriteFloat(Y); + buffer.WriteFloat(Z); + } + + public static RotationsMetadata Read(PacketBuffer buffer, MinecraftData data) + { + var x = buffer.ReadFloat(); + var y = buffer.ReadFloat(); + var z = buffer.ReadFloat(); + return new(x, y, z); + } +} + +public sealed record PositionMetadata(Position Value) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) => buffer.WritePosition(Value); + public static PositionMetadata Read(PacketBuffer buffer, MinecraftData data) => new(buffer.ReadPosition()); +} + +/// Position is present if the Boolean is set to true. +public sealed record OptionalPositionMetadata(Position? Value) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) + { + var hasValue = Value.HasValue; + buffer.WriteBool(hasValue); + if (hasValue) buffer.WritePosition(Value!.Value); + } + + public static OptionalPositionMetadata Read(PacketBuffer buffer, MinecraftData data) + { + var hasValue = buffer.ReadBool(); + Position? value = hasValue ? buffer.ReadPosition() : null; + return new(value); + } +} + +public sealed record DirectionMetadata(Direction Value) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) => buffer.WriteVarInt((int)Value); + public static DirectionMetadata Read(PacketBuffer buffer, MinecraftData data) => new((Direction)buffer.ReadVarInt()); +} + +public sealed record OptionalUuidMetadata(Uuid? Value) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) + { + var hasValue = Value.HasValue; + buffer.WriteBool(hasValue); + if (hasValue) buffer.WriteUuid(Value!.Value); + } + + public static OptionalUuidMetadata Read(PacketBuffer buffer, MinecraftData data) + { + var hasValue = buffer.ReadBool(); + Uuid? value = hasValue ? buffer.ReadUuid() : null; + return new(value); + } +} + +/// An ID in the block state registry. +public sealed record BlockStateMetadata(int Value) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) => buffer.WriteVarInt(Value); + public static BlockStateMetadata Read(PacketBuffer buffer, MinecraftData data) => new(buffer.ReadVarInt()); +} + +/// 0 for absent (air is unrepresentable); otherwise, an ID in the block state registry. +public sealed record OptionalBlockStateMetadata(int Value) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) => buffer.WriteVarInt(Value); + public static OptionalBlockStateMetadata Read(PacketBuffer buffer, MinecraftData data) => new(buffer.ReadVarInt()); +} + +public sealed record NbtMetadata(NbtTag Value) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) => buffer.WriteNbt(Value); + public static NbtMetadata Read(PacketBuffer buffer, MinecraftData data) => new(buffer.ReadNbt()); +} + +public sealed record ParticleMetadata(int ParticleType, byte[]? ParticleData) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteVarInt(ParticleType); + // TODO: Write particle data + //buffer.WriteObject(ParticleData); + } + + public static ParticleMetadata Read(PacketBuffer buffer, MinecraftData data) + { + var particleType = buffer.ReadVarInt(); + //var particleData = buffer.ReadObject(); + return new(particleType, null); + } +} + +public sealed record VillagerDataMetadata(int VillagerType, int VillagerProfession, int Level) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteVarInt(VillagerType); + buffer.WriteVarInt(VillagerProfession); + buffer.WriteVarInt(Level); + } + + public static VillagerDataMetadata Read(PacketBuffer buffer, MinecraftData data) + { + var villagerType = buffer.ReadVarInt(); + var villagerProfession = buffer.ReadVarInt(); + var level = buffer.ReadVarInt(); + return new(villagerType, villagerProfession, level); + } +} + +public sealed record OptionalVarIntMetadata(int? Value) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteVarInt(Value.HasValue ? Value.Value + 1 : 0); + } + + public static OptionalVarIntMetadata Read(PacketBuffer buffer, MinecraftData data) + { + var value = buffer.ReadVarInt(); + return new(value == 0 ? (int?)null : value - 1); + } +} + +public sealed record PoseMetadata(EntityPose Value) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) => buffer.WriteVarInt((int)Value); + public static PoseMetadata Read(PacketBuffer buffer, MinecraftData data) => new((EntityPose)buffer.ReadVarInt()); +} + +public sealed record CatVariantMetadata(int Value) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) => buffer.WriteVarInt(Value); + public static CatVariantMetadata Read(PacketBuffer buffer, MinecraftData data) => new(buffer.ReadVarInt()); +} + +public sealed record FrogVariantMetadata(int Value) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) => buffer.WriteVarInt(Value); + public static FrogVariantMetadata Read(PacketBuffer buffer, MinecraftData data) => new(buffer.ReadVarInt()); +} + +public sealed record OptionalGlobalPositionMetadata(bool HasValue, Identifier? DimensionIdentifier, Position? Position) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteBool(HasValue); + if (HasValue) + { + buffer.WriteIdentifier(DimensionIdentifier ?? throw new InvalidOperationException($"{nameof(DimensionIdentifier)} must not be null if {nameof(HasValue)} is true.")); + buffer.WritePosition(Position ?? throw new InvalidOperationException($"{nameof(Position)} must not be null if {nameof(HasValue)} is true.")); + } + } + + public static OptionalGlobalPositionMetadata Read(PacketBuffer buffer, MinecraftData data) + { + var hasValue = buffer.ReadBool(); + var dimensionIdentifier = hasValue ? buffer.ReadIdentifier() : null; + Position? position = hasValue ? buffer.ReadPosition() : null; + return new(hasValue, dimensionIdentifier, position); + } +} + +public sealed record PaintingVariantMetadata(int Value) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) => buffer.WriteVarInt(Value); + public static PaintingVariantMetadata Read(PacketBuffer buffer, MinecraftData data) => new(buffer.ReadVarInt()); +} + +public sealed record SnifferStateMetadata(SnifferState Value) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) => buffer.WriteVarInt((int)Value); + public static SnifferStateMetadata Read(PacketBuffer buffer, MinecraftData data) => new((SnifferState)buffer.ReadVarInt()); + + public enum SnifferState + { + Idling = 0, + FeelingHappy = 1, + Scenting = 2, + Sniffing = 3, + Searching = 4, + Digging = 5, + Rising = 6 + } +} + +public sealed record Vector3Metadata(float X, float Y, float Z) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteFloat(X); + buffer.WriteFloat(Y); + buffer.WriteFloat(Z); + } + + public static Vector3Metadata Read(PacketBuffer buffer, MinecraftData data) + { + var x = buffer.ReadFloat(); + var y = buffer.ReadFloat(); + var z = buffer.ReadFloat(); + return new(x, y, z); + } + + [Pure] + public Vector3 ToVector3() => new(X, Y, Z); +} + +public sealed record QuaternionMetadata(float X, float Y, float Z, float W) : IMetadataValue, ISerializableWithMinecraftData +{ + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteFloat(X); + buffer.WriteFloat(Y); + buffer.WriteFloat(Z); + buffer.WriteFloat(W); + } + + public static QuaternionMetadata Read(PacketBuffer buffer, MinecraftData data) + { + var x = buffer.ReadFloat(); + var y = buffer.ReadFloat(); + var z = buffer.ReadFloat(); + var w = buffer.ReadFloat(); + return new(x, y, z, w); + } +} +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member diff --git a/Components/MineSharp.Protocol/Packets/NetworkTypes/ISerializableWithMinecraftData.cs b/Components/MineSharp.Protocol/Packets/NetworkTypes/ISerializableWithMinecraftData.cs new file mode 100644 index 00000000..f80eb65a --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/NetworkTypes/ISerializableWithMinecraftData.cs @@ -0,0 +1,26 @@ +using MineSharp.Data; + +namespace MineSharp.Core.Serialization; + +/// +/// Interface for serializing and deserializing objects from and to +/// while being aware of the Minecraft version +/// +/// +public interface ISerializableWithMinecraftData where T : ISerializableWithMinecraftData +{ + /// + /// Serialize the object into the buffer + /// + /// + /// + public void Write(PacketBuffer buffer, MinecraftData data); + + /// + /// Read the object from the buffer + /// + /// + /// + /// + public static abstract T Read(PacketBuffer buffer, MinecraftData data); +} diff --git a/Components/MineSharp.Protocol/Packets/NetworkTypes/ParticleData.cs b/Components/MineSharp.Protocol/Packets/NetworkTypes/ParticleData.cs new file mode 100644 index 00000000..2803eb46 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/NetworkTypes/ParticleData.cs @@ -0,0 +1,369 @@ +using System.Collections.Frozen; +using MineSharp.Core.Common; +using MineSharp.Core.Common.Items; +using MineSharp.Core.Geometry; +using MineSharp.Core.Serialization; +using MineSharp.Data; + +namespace MineSharp.Protocol.Packets.NetworkTypes; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +public interface IParticleData +{ + public void Write(PacketBuffer buffer, MinecraftData data); +} + +public interface IParticleDataStatic +{ + public static abstract IParticleData Read(PacketBuffer buffer, MinecraftData data); +} + +public static class ParticleDataRegistry +{ + public static IParticleData? Read(PacketBuffer buffer, MinecraftData data, int particleId) + { + var particleTypeIdentifier = data.Particles.GetName(particleId); + return Read(buffer, data, particleTypeIdentifier); + } + + public static IParticleData? Read(PacketBuffer buffer, MinecraftData data, Identifier typeIdentifier) + { + if (!ParticleDataFactories.TryGetValue(typeIdentifier, out var reader)) + { + return null; + } + return reader(buffer, data); + } + + public static readonly FrozenDictionary> ParticleDataFactories; + + static ParticleDataRegistry() + { + ParticleDataFactories = InitializeParticleData(); + } + + private static FrozenDictionary> InitializeParticleData() + { + var dict = new Dictionary>(); + + void Register(params Identifier[] identifiers) + where T : IParticleData, IParticleDataStatic + { + var factory = T.Read; + foreach (var identifier in identifiers) + { + dict.Add(identifier, factory); + } + } + + Register( + Identifier.Parse("minecraft:block"), + Identifier.Parse("minecraft:block_marker"), + Identifier.Parse("minecraft:falling_dust"), + Identifier.Parse("minecraft:dust_pillar") + ); + Register( + Identifier.Parse("minecraft:dust") + ); + Register( + Identifier.Parse("minecraft:dust_color_transition") + ); + Register( + Identifier.Parse("minecraft:entity_effect") + ); + Register( + Identifier.Parse("minecraft:sculk_charge") + ); + Register( + Identifier.Parse("minecraft:item") + ); + Register( + Identifier.Parse("minecraft:vibration") + ); + Register( + Identifier.Parse("minecraft:shriek") + ); + + return dict.ToFrozenDictionary(); + } +} +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + +/// +/// Represents particle data that includes a block state. +/// +/// The ID of the block state. +public sealed record BlockStateParticleData(int BlockState) : IParticleData, IParticleDataStatic, ISerializableWithMinecraftData +{ + /// + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteVarInt(BlockState); + } + + /// + public static BlockStateParticleData Read(PacketBuffer buffer, MinecraftData data) + { + var blockState = buffer.ReadVarInt(); + + return new(blockState); + } + + static IParticleData IParticleDataStatic.Read(PacketBuffer buffer, MinecraftData data) => Read(buffer, data); +} + +/// +/// Represents the data for the "dust" particle. +/// +/// The red RGB value, between 0 and 1. Divide actual RGB value by 255. +/// The green RGB value, between 0 and 1. Divide actual RGB value by 255. +/// The blue RGB value, between 0 and 1. Divide actual RGB value by 255. +/// The scale, will be clamped between 0.01 and 4. +public sealed record DustParticleData( + float Red, + float Green, + float Blue, + float Scale +) : IParticleData, IParticleDataStatic, ISerializableWithMinecraftData +{ + /// + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteFloat(Red); + buffer.WriteFloat(Green); + buffer.WriteFloat(Blue); + buffer.WriteFloat(Scale); + } + + /// + public static DustParticleData Read(PacketBuffer buffer, MinecraftData data) + { + var red = buffer.ReadFloat(); + var green = buffer.ReadFloat(); + var blue = buffer.ReadFloat(); + var scale = buffer.ReadFloat(); + + return new DustParticleData(red, green, blue, scale); + } + + static IParticleData IParticleDataStatic.Read(PacketBuffer buffer, MinecraftData data) => Read(buffer, data); +} + +/// +/// Represents the data for the "dust_color_transition" particle. +/// +/// The start red RGB value, between 0 and 1. Divide actual RGB value by 255. +/// The start green RGB value, between 0 and 1. Divide actual RGB value by 255. +/// The start blue RGB value, between 0 and 1. Divide actual RGB value by 255. +/// The end red RGB value, between 0 and 1. Divide actual RGB value by 255. +/// The end green RGB value, between 0 and 1. Divide actual RGB value by 255. +/// The end blue RGB value, between 0 and 1. Divide actual RGB value by 255. +/// The scale, will be clamped between 0.01 and 4. +public sealed record DustColorTransitionParticleData( + float FromRed, + float FromGreen, + float FromBlue, + float ToRed, + float ToGreen, + float ToBlue, + float Scale +) : IParticleData, IParticleDataStatic, ISerializableWithMinecraftData +{ + /// + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteFloat(FromRed); + buffer.WriteFloat(FromGreen); + buffer.WriteFloat(FromBlue); + buffer.WriteFloat(ToRed); + buffer.WriteFloat(ToGreen); + buffer.WriteFloat(ToBlue); + buffer.WriteFloat(Scale); + } + + /// + public static DustColorTransitionParticleData Read(PacketBuffer buffer, MinecraftData data) + { + var fromRed = buffer.ReadFloat(); + var fromGreen = buffer.ReadFloat(); + var fromBlue = buffer.ReadFloat(); + var toRed = buffer.ReadFloat(); + var toGreen = buffer.ReadFloat(); + var toBlue = buffer.ReadFloat(); + var scale = buffer.ReadFloat(); + + return new(fromRed, fromGreen, fromBlue, toRed, toGreen, toBlue, scale); + } + + static IParticleData IParticleDataStatic.Read(PacketBuffer buffer, MinecraftData data) => Read(buffer, data); +} + +/// +/// Represents the data for the "entity_effect" particle. +/// +/// The ARGB components of the color encoded as an Int. +public sealed record EntityEffectParticleData(int Color) : IParticleData, IParticleDataStatic, ISerializableWithMinecraftData +{ + /// + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteInt(Color); + } + + /// + public static EntityEffectParticleData Read(PacketBuffer buffer, MinecraftData data) + { + var color = buffer.ReadInt(); + + return new(color); + } + + static IParticleData IParticleDataStatic.Read(PacketBuffer buffer, MinecraftData data) => Read(buffer, data); +} + +/// +/// Represents the data for the "sculk_charge" particle. +/// +/// How much the particle will be rotated when displayed. +public sealed record SculkChargeParticleData(float Roll) : IParticleData, IParticleDataStatic, ISerializableWithMinecraftData +{ + /// + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteFloat(Roll); + } + + /// + public static SculkChargeParticleData Read(PacketBuffer buffer, MinecraftData data) + { + var roll = buffer.ReadFloat(); + + return new(roll); + } + + static IParticleData IParticleDataStatic.Read(PacketBuffer buffer, MinecraftData data) => Read(buffer, data); +} + +/// +/// Represents the data for the "item" particle. +/// +/// The item that will be used. +public sealed record ItemParticleData(Item Item) : IParticleData, IParticleDataStatic, ISerializableWithMinecraftData +{ + /// + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteOptionalItem(Item); + } + + /// + public static ItemParticleData Read(PacketBuffer buffer, MinecraftData data) + { + var item = buffer.ReadOptionalItem(data)!; + + return new(item); + } + + static IParticleData IParticleDataStatic.Read(PacketBuffer buffer, MinecraftData data) => Read(buffer, data); +} + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +public enum PositionSourceType +{ + Block = 0, + Entity = 1 +} +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + +/// +/// Represents the data for the "vibration" particle. +/// +/// The source type of the position (Block or Entity). +/// TThe position of the block the vibration originated from, if the source type is Block. +/// The ID of the entity the vibration originated from, if the source type is Entity. +/// The height of the entity's eye relative to the entity, if the source type is Entity. +/// The amount of ticks it takes for the vibration to travel from its source to its destination. +public sealed record VibrationParticleData( + PositionSourceType PositionSourceType, + Position? BlockPosition, + int? EntityId, + float? EntityEyeHeight, + int Ticks +) : IParticleData, IParticleDataStatic, ISerializableWithMinecraftData +{ + public static readonly Identifier BlockPositionSourceType = Identifier.Parse("minecraft:block"); + public static readonly Identifier EntityPositionSourceType = Identifier.Parse("minecraft:entity"); + + public static readonly FrozenDictionary PositionSourceTypes = new Dictionary + { + { PositionSourceType.Block, BlockPositionSourceType }, + { PositionSourceType.Entity, EntityPositionSourceType }, + }.ToFrozenDictionary(); + + /// + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteVarInt((int)PositionSourceType); + var positionSourceTypeIdentifier = PositionSourceTypes[PositionSourceType]; + + if (positionSourceTypeIdentifier == BlockPositionSourceType) + { + buffer.WritePosition(BlockPosition ?? throw new InvalidOperationException($"{nameof(BlockPosition)} must be set when {nameof(PositionSourceType)} is {PositionSourceType}")); + } + else if (positionSourceTypeIdentifier == EntityPositionSourceType) + { + buffer.WriteVarInt(EntityId ?? throw new InvalidOperationException($"{nameof(EntityId)} must be set when {nameof(PositionSourceType)} is {PositionSourceType}")); + buffer.WriteFloat(EntityEyeHeight ?? throw new InvalidOperationException($"{nameof(EntityEyeHeight)} must be set when {nameof(PositionSourceType)} is {PositionSourceType}")); + } + + buffer.WriteVarInt(Ticks); + } + + /// + public static VibrationParticleData Read(PacketBuffer buffer, MinecraftData data) + { + var positionSourceType = (PositionSourceType)buffer.ReadVarInt(); + var positionSourceTypeIdentifier = PositionSourceTypes[positionSourceType]; + + Position? blockPosition = null; + int? entityId = null; + float? entityEyeHeight = null; + if (positionSourceTypeIdentifier == BlockPositionSourceType) + { + blockPosition = buffer.ReadPosition(); + } + else if (positionSourceTypeIdentifier == EntityPositionSourceType) + { + entityId = buffer.ReadVarInt(); + entityEyeHeight = buffer.ReadFloat(); + } + var ticks = buffer.ReadVarInt(); + + return new(positionSourceType, blockPosition, entityId, entityEyeHeight, ticks); + } + + static IParticleData IParticleDataStatic.Read(PacketBuffer buffer, MinecraftData data) => Read(buffer, data); +} + +/// +/// Represents the data for the "shriek" particle. +/// +/// The time in ticks before the particle is displayed. +public sealed record ShriekParticleData(int Delay) : IParticleData, IParticleDataStatic, ISerializableWithMinecraftData +{ + /// + public void Write(PacketBuffer buffer, MinecraftData data) + { + buffer.WriteVarInt(Delay); + } + + /// + public static ShriekParticleData Read(PacketBuffer buffer, MinecraftData data) + { + var delay = buffer.ReadVarInt(); + + return new(delay); + } + + static IParticleData IParticleDataStatic.Read(PacketBuffer buffer, MinecraftData data) => Read(buffer, data); +} diff --git a/Components/MineSharp.Protocol/Packets/NetworkTypes/PlayerAbilitiesFlags.cs b/Components/MineSharp.Protocol/Packets/NetworkTypes/PlayerAbilitiesFlags.cs new file mode 100644 index 00000000..6a9f3e93 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/NetworkTypes/PlayerAbilitiesFlags.cs @@ -0,0 +1,26 @@ +namespace MineSharp.Protocol.Packets.NetworkTypes; + +/// +/// Flags representing various player abilities. +/// +[Flags] +public enum PlayerAbilitiesFlags : byte +{ + /// + /// Player is invulnerable. + /// + Invulnerable = 0x01, + /// + /// Player is flying. + /// + Flying = 0x02, + /// + /// Player is allowed to fly. + /// + AllowFlying = 0x04, + /// + /// Player is in creative mode. + /// And can instantly break blocks. + /// + CreativeMode = 0x08, +} diff --git a/Components/MineSharp.Protocol/Packets/NetworkTypes/PlayerActionStatus.cs b/Components/MineSharp.Protocol/Packets/NetworkTypes/PlayerActionStatus.cs new file mode 100644 index 00000000..162e0581 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/NetworkTypes/PlayerActionStatus.cs @@ -0,0 +1,52 @@ +namespace MineSharp.Protocol.Packets.NetworkTypes; + +/// +/// Specifies the status of a player action +/// +public enum PlayerActionStatus +{ + /// + /// Sent when the player starts digging a block. + /// If the block was instamined or the player is in creative mode, the client will not send Status = Finished digging, and will assume the server completed the destruction. + /// To detect this, it is necessary to calculate the block destruction speed server-side. + /// + StartDigging = 0, + /// + /// Sent when the player lets go of the Mine Block key (default: left click). + /// + /// Face is always set to -Y. + /// + CancelledDigging = 1, + /// + /// Sent when the client thinks it is finished. + /// + FinishedDigging = 2, + /// + /// Triggered by using the Drop Item key (default: Q) with the modifier to drop the entire selected stack (default: Control or Command, depending on OS). + /// + /// Location is always set to 0/0/0, Face is always set to -Y. + /// Sequence is always set to 0. + /// + DropItemStack = 3, + /// + /// Triggered by using the Drop Item key (default: Q). + /// + /// Location is always set to 0/0/0, Face is always set to -Y. + /// Sequence is always set to 0. + /// + DropItem = 4, + /// + /// Indicates that the currently held item should have its state updated such as eating food, pulling back bows, using buckets, etc. + /// + /// Location is always set to 0/0/0, Face is always set to -Y. + /// Sequence is always set to 0. + /// + ShootArrowOrFinishEating = 5, + /// + /// Used to swap or assign an item to the second hand. + /// + /// Location is always set to 0/0/0, Face is always set to -Y. + /// Sequence is always set to 0. + /// + SwapItemInHand = 6, +} diff --git a/Components/MineSharp.Protocol/Packets/NetworkTypes/ResourcePackResult.cs b/Components/MineSharp.Protocol/Packets/NetworkTypes/ResourcePackResult.cs new file mode 100644 index 00000000..ec4a46b4 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/NetworkTypes/ResourcePackResult.cs @@ -0,0 +1,18 @@ +namespace MineSharp.Protocol.Packets.NetworkTypes; + +/// +/// Enum representing the possible results of a resource pack response +/// +public enum ResourcePackResult +{ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + Success = 0, + Declined = 1, + FailedDownload = 2, + Accepted = 3, + Downloaded = 4, + InvalidUrl = 5, + FailedToReload = 6, + Discarded = 7 +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} diff --git a/Components/MineSharp.Protocol/Packets/NetworkTypes/ScoreboardNumberFormat.cs b/Components/MineSharp.Protocol/Packets/NetworkTypes/ScoreboardNumberFormat.cs new file mode 100644 index 00000000..ad8f8ed7 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/NetworkTypes/ScoreboardNumberFormat.cs @@ -0,0 +1,152 @@ +using System.Collections.Frozen; +using fNbt; +using MineSharp.ChatComponent; +using MineSharp.Core.Serialization; + +namespace MineSharp.Protocol.Packets.NetworkTypes; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member +/// +/// Scoreboard number format type enumeration +/// +public enum ScoreboardNumberFormatType +{ + Blank = 0, + Styled = 1, + Fixed = 2 +} + +public interface IScoreboardNumberFormat +{ + public ScoreboardNumberFormatType Type { get; } + + public void Write(PacketBuffer buffer); +} + +public interface IScoreboardNumberFormatStatic +{ + public static abstract ScoreboardNumberFormatType StaticType { get; } + + public static abstract IScoreboardNumberFormat Read(PacketBuffer buffer); +} + +public static class ScoreboardNumberFormatRegistry +{ + public static IScoreboardNumberFormat Read(PacketBuffer buffer, ScoreboardNumberFormatType type) + { + if (!ScoreboardNumberFormatFactories.TryGetValue(type, out var reader)) + { + throw new InvalidOperationException($"Unsupported UpdateTeamsMethodType: {type}"); + } + return reader(buffer); + } + + public static readonly FrozenDictionary> ScoreboardNumberFormatFactories; + + static ScoreboardNumberFormatRegistry() + { + ScoreboardNumberFormatFactories = InitializeUpdateTeamsMethod(); + } + + private static FrozenDictionary> InitializeUpdateTeamsMethod() + { + var dict = new Dictionary>(); + + void Register() + where T : IScoreboardNumberFormat, IScoreboardNumberFormatStatic + { + var mask = T.StaticType; + var factory = T.Read; + dict.Add(mask, factory); + } + + Register(); + Register(); + Register(); + + return dict.ToFrozenDictionary(); + } +} +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + +/// +/// Blank number format +/// +public sealed record BlankNumberFormat() : IScoreboardNumberFormat, IScoreboardNumberFormatStatic, ISerializable +{ + /// + public ScoreboardNumberFormatType Type => StaticType; + /// + public static ScoreboardNumberFormatType StaticType => ScoreboardNumberFormatType.Blank; + + /// + public void Write(PacketBuffer buffer) { } + + /// + public static BlankNumberFormat Read(PacketBuffer buffer) => new(); + + static IScoreboardNumberFormat IScoreboardNumberFormatStatic.Read(PacketBuffer buffer) + { + return Read(buffer); + } +} + +/// +/// Styled number format +/// +/// The styling to be used when formatting the score number +public sealed record StyledNumberFormat(NbtCompound Styling) : IScoreboardNumberFormat, IScoreboardNumberFormatStatic, ISerializable +{ + /// + public ScoreboardNumberFormatType Type => StaticType; + /// + public static ScoreboardNumberFormatType StaticType => ScoreboardNumberFormatType.Blank; + + /// + public void Write(PacketBuffer buffer) + { + buffer.WriteNbt(Styling); + } + + /// + public static StyledNumberFormat Read(PacketBuffer buffer) + { + var styling = buffer.ReadNbtCompound(); + return new(styling); + } + + static IScoreboardNumberFormat IScoreboardNumberFormatStatic.Read(PacketBuffer buffer) + { + return Read(buffer); + } +} + +/// +/// Fixed number format +/// +/// The text to be used as placeholder +public sealed record FixedNumberFormat(Chat Content) : IScoreboardNumberFormat, IScoreboardNumberFormatStatic, ISerializable +{ + /// + public ScoreboardNumberFormatType Type => StaticType; + /// + public static ScoreboardNumberFormatType StaticType => ScoreboardNumberFormatType.Blank; + + /// + public void Write(PacketBuffer buffer) + { + buffer.WriteChatComponent(Content); + } + + /// + public static FixedNumberFormat Read(PacketBuffer buffer) + { + var content = buffer.ReadChatComponent(); + return new(content); + } + + static IScoreboardNumberFormat IScoreboardNumberFormatStatic.Read(PacketBuffer buffer) + { + return Read(buffer); + } +} diff --git a/Components/MineSharp.Protocol/Packets/NetworkTypes/TagRegistry.cs b/Components/MineSharp.Protocol/Packets/NetworkTypes/TagRegistry.cs new file mode 100644 index 00000000..51202c9b --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/NetworkTypes/TagRegistry.cs @@ -0,0 +1,70 @@ +using MineSharp.Core.Common; +using MineSharp.Core.Serialization; + +namespace MineSharp.Protocol.Packets.NetworkTypes; + +/// +/// Registry record +/// +/// Registry identifier +/// Array of tags +public sealed record Registry(Identifier Identifier, Tag[] Tags) : ISerializable +{ + /// + public void Write(PacketBuffer buffer) + { + buffer.WriteIdentifier(Identifier); + buffer.WriteVarInt(Tags.Length); + foreach (var tag in Tags) + { + tag.Write(buffer); + } + } + + /// + public static Registry Read(PacketBuffer buffer) + { + var identifier = buffer.ReadIdentifier(); + var length = buffer.ReadVarInt(); + var tags = new Tag[length]; + for (var i = 0; i < length; i++) + { + tags[i] = Tag.Read(buffer); + } + + return new Registry(identifier, tags); + } +} + +/// +/// Tag record +/// +/// Tag name +/// Array of entries +public sealed record Tag(Identifier TagName, int[] Entries) : ISerializable +{ + /// + public void Write(PacketBuffer buffer) + { + buffer.WriteIdentifier(TagName); + buffer.WriteVarInt(Entries.Length); + foreach (var entry in Entries) + { + buffer.WriteVarInt(entry); + } + } + + /// + public static Tag Read(PacketBuffer buffer) + { + var tagName = buffer.ReadIdentifier(); + var length = buffer.ReadVarInt(); + var entries = new int[length]; + for (var i = 0; i < length; i++) + { + entries[i] = buffer.ReadVarInt(); + } + + return new Tag(tagName, entries); + } +} diff --git a/Components/MineSharp.Protocol/Packets/PacketBufferExtensions.cs b/Components/MineSharp.Protocol/Packets/PacketBufferExtensions.cs index ddb31baa..1244e9ae 100644 --- a/Components/MineSharp.Protocol/Packets/PacketBufferExtensions.cs +++ b/Components/MineSharp.Protocol/Packets/PacketBufferExtensions.cs @@ -1,6 +1,6 @@ using MineSharp.ChatComponent; using MineSharp.Core; -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; namespace MineSharp.Protocol.Packets; diff --git a/Components/MineSharp.Protocol/Packets/PacketPalette.cs b/Components/MineSharp.Protocol/Packets/PacketPalette.cs index ee89af3e..5729e6ec 100644 --- a/Components/MineSharp.Protocol/Packets/PacketPalette.cs +++ b/Components/MineSharp.Protocol/Packets/PacketPalette.cs @@ -1,11 +1,11 @@ -using MineSharp.Core.Common; +using System.Collections.Frozen; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; using MineSharp.Protocol.Packets.Clientbound.Configuration; using MineSharp.Protocol.Packets.Clientbound.Login; using MineSharp.Protocol.Packets.Clientbound.Play; using MineSharp.Protocol.Packets.Clientbound.Status; -using MineSharp.Protocol.Packets.Serverbound.Configuration; using MineSharp.Protocol.Packets.Serverbound.Handshaking; using MineSharp.Protocol.Packets.Serverbound.Login; using MineSharp.Protocol.Packets.Serverbound.Play; @@ -13,15 +13,29 @@ using NLog; using CBChatPacket = MineSharp.Protocol.Packets.Clientbound.Play.ChatPacket; using CBCloseWindowPacket = MineSharp.Protocol.Packets.Clientbound.Play.CloseWindowPacket; +using CBConfigurationAddResourcePackPacket = MineSharp.Protocol.Packets.Clientbound.Configuration.AddResourcePackPacket; using CBConfigurationKeepAlivePacket = MineSharp.Protocol.Packets.Clientbound.Configuration.KeepAlivePacket; +using CBConfigurationPluginMessagePacket = MineSharp.Protocol.Packets.Clientbound.Configuration.PluginMessagePacket; +using CBConfigurationRemoveResourcePackPacket = MineSharp.Protocol.Packets.Clientbound.Configuration.RemoveResourcePackPacket; +using CBConfigurationUpdateTagsPacket = MineSharp.Protocol.Packets.Clientbound.Configuration.UpdateTagsPacket; using CBFinishConfigurationPacket = MineSharp.Protocol.Packets.Clientbound.Configuration.FinishConfigurationPacket; using CBKeepAlivePacket = MineSharp.Protocol.Packets.Clientbound.Play.KeepAlivePacket; -using CBPluginMessagePacket = MineSharp.Protocol.Packets.Clientbound.Configuration.PluginMessagePacket; +using CBPlayAddResourcePackPacket = MineSharp.Protocol.Packets.Clientbound.Play.AddResourcePackPacket; +using CBPlayChangeDifficultyPacket = MineSharp.Protocol.Packets.Clientbound.Play.ChangeDifficultyPacket; +using CBPlayMoveVehiclePacket = MineSharp.Protocol.Packets.Clientbound.Play.MoveVehiclePacket; +using CBPlayPingResponsePacket = MineSharp.Protocol.Packets.Clientbound.Play.PingResponsePacket; +using CBPlayPlayerAbilitiesPacket = MineSharp.Protocol.Packets.Clientbound.Play.PlayerAbilitiesPacket; +using CBPlayPluginMessagePacket = MineSharp.Protocol.Packets.Clientbound.Play.PluginMessagePacket; +using CBPlayRemoveResourcePackPacket = MineSharp.Protocol.Packets.Clientbound.Play.RemoveResourcePackPacket; +using CBPlayUpdateTagsPacket = MineSharp.Protocol.Packets.Clientbound.Play.UpdateTagsPacket; using CBSetHeldItemPacket = MineSharp.Protocol.Packets.Clientbound.Play.SetHeldItemPacket; +using CBStatusPingResponsePacket = MineSharp.Protocol.Packets.Clientbound.Status.PingResponsePacket; +using ConfClientInformationPacket = MineSharp.Protocol.Packets.Serverbound.Configuration.ClientInformationPacket; using ConfigurationDisconnectPacket = MineSharp.Protocol.Packets.Clientbound.Configuration.DisconnectPacket; using ConfPingPacket = MineSharp.Protocol.Packets.Clientbound.Configuration.PingPacket; using ConfPongPacket = MineSharp.Protocol.Packets.Serverbound.Configuration.PongPacket; using LoginDisconnectPacket = MineSharp.Protocol.Packets.Clientbound.Login.DisconnectPacket; +using PlayClientInformationPacket = MineSharp.Protocol.Packets.Serverbound.Play.ClientInformationPacket; using PlayDisconnectPacket = MineSharp.Protocol.Packets.Clientbound.Play.DisconnectPacket; using PlayPingPacket = MineSharp.Protocol.Packets.Clientbound.Play.PingPacket; using PlayPongPacket = MineSharp.Protocol.Packets.Serverbound.Play.PongPacket; @@ -29,12 +43,17 @@ using SBChatPacket = MineSharp.Protocol.Packets.Serverbound.Play.ChatPacket; using SBCloseWindowPacket = MineSharp.Protocol.Packets.Serverbound.Play.CloseWindowPacket; using SBConfigurationKeepAlivePacket = MineSharp.Protocol.Packets.Serverbound.Configuration.KeepAlivePacket; +using SBConfigurationPluginMessagePacket = MineSharp.Protocol.Packets.Serverbound.Configuration.PluginMessagePacket; +using SBConfigurationResourcePackResponsePacket = MineSharp.Protocol.Packets.Serverbound.Configuration.ResourcePackResponsePacket; using SBFinishConfigurationPacket = MineSharp.Protocol.Packets.Serverbound.Configuration.FinishConfigurationPacket; using SBKeepAlivePacket = MineSharp.Protocol.Packets.Serverbound.Play.KeepAlivePacket; -using SBPluginMessagePacket = MineSharp.Protocol.Packets.Serverbound.Configuration.PluginMessagePacket; +using SBPlayChangeDifficultyPacket = MineSharp.Protocol.Packets.Serverbound.Play.ChangeDifficultyPacket; +using SBPlayMoveVehiclePacket = MineSharp.Protocol.Packets.Serverbound.Play.MoveVehiclePacket; +using SBPlayPingRequestPacket = MineSharp.Protocol.Packets.Serverbound.Play.PingRequestPacket; +using SBPlayPlayerAbilitiesPacket = MineSharp.Protocol.Packets.Serverbound.Play.PlayerAbilitiesPacket; +using SBPlayPluginMessagePacket = MineSharp.Protocol.Packets.Serverbound.Play.PluginMessagePacket; using SBSetHeldItemPacket = MineSharp.Protocol.Packets.Serverbound.Play.SetHeldItemPacket; -using ConfClientInformation = MineSharp.Protocol.Packets.Serverbound.Configuration.ClientInformationPacket; -using PlayClientInformation = MineSharp.Protocol.Packets.Serverbound.Play.ClientInformationPacket; +using StatusPingRequestPacket = MineSharp.Protocol.Packets.Serverbound.Status.PingRequestPacket; namespace MineSharp.Protocol.Packets; @@ -43,15 +62,11 @@ internal static class PacketPalette public delegate IPacket PacketFactory(PacketBuffer buffer, MinecraftData version); private static readonly ILogger Logger = LogManager.GetCurrentClassLogger(); - private static readonly IDictionary PacketFactories; - private static readonly IDictionary ClassToTypeMap; + private static readonly FrozenDictionary PacketFactories; static PacketPalette() { - PacketFactories = new Dictionary(); - ClassToTypeMap = new Dictionary(); - - InitializePackets(); + PacketFactories = InitializePackets(); } public static PacketFactory? GetFactory(PacketType packetType) @@ -65,124 +80,245 @@ static PacketPalette() return packet; } - public static PacketType GetPacketType() where T : IPacket + private static FrozenDictionary InitializePackets() { - var guid = typeof(T).GUID; - return ClassToTypeMap[guid]; - } + Dictionary packetFactories = new(); + + void RegisterPacket() + where TPacket : IPacket + { + packetFactories.Add(TPacket.StaticType, TPacket.Read); + } - private static void InitializePackets() - { // Handshaking - RegisterPacket(PacketType.SB_Handshake_SetProtocol); + RegisterPacket(); // Login - RegisterPacket(PacketType.CB_Login_Disconnect); - RegisterPacket(PacketType.CB_Login_EncryptionBegin); - RegisterPacket(PacketType.CB_Login_Success); - RegisterPacket(PacketType.CB_Login_Compress); - RegisterPacket(PacketType.CB_Login_LoginPluginRequest); + // CB + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); - RegisterPacket(PacketType.SB_Login_LoginStart); - RegisterPacket(PacketType.SB_Login_EncryptionBegin); - RegisterPacket(PacketType.SB_Login_LoginPluginResponse); - RegisterPacket(PacketType.SB_Login_LoginAcknowledged); + // SB + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); // Status - RegisterPacket(PacketType.CB_Status_ServerInfo); - RegisterPacket(PacketType.CB_Status_Ping); + // CB + RegisterPacket(); + RegisterPacket(); - RegisterPacket(PacketType.SB_Status_PingStart); - RegisterPacket(PacketType.SB_Status_Ping); + // SB + RegisterPacket(); + RegisterPacket(); // Configuration - RegisterPacket(PacketType.CB_Configuration_CustomPayload); - RegisterPacket(PacketType.CB_Configuration_Disconnect); - RegisterPacket(PacketType.CB_Configuration_FinishConfiguration); - RegisterPacket(PacketType.CB_Configuration_KeepAlive); - RegisterPacket(PacketType.CB_Configuration_Ping); - RegisterPacket(PacketType.CB_Configuration_RegistryData); - RegisterPacket(PacketType.CB_Configuration_FeatureFlags); - - RegisterPacket(PacketType.SB_Configuration_Settings); - RegisterPacket(PacketType.SB_Configuration_CustomPayload); - RegisterPacket(PacketType.SB_Configuration_FinishConfiguration); - RegisterPacket(PacketType.SB_Configuration_KeepAlive); - RegisterPacket(PacketType.SB_Configuration_Pong); - RegisterPacket(PacketType.SB_Configuration_ResourcePackReceive); + // CB + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + + // SB + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); // Play - RegisterPacket(PacketType.CB_Play_SpawnEntityPainting); - RegisterPacket(PacketType.CB_Play_SpawnEntityLiving); - RegisterPacket(PacketType.CB_Play_SpawnEntity); - RegisterPacket(PacketType.CB_Play_KeepAlive); - RegisterPacket(PacketType.CB_Play_MapChunk); - RegisterPacket(PacketType.CB_Play_UnloadChunk); - RegisterPacket(PacketType.CB_Play_BlockChange); - RegisterPacket(PacketType.CB_Play_MultiBlockChange); - RegisterPacket(PacketType.CB_Play_Login); - RegisterPacket(PacketType.CB_Play_Position); - RegisterPacket(PacketType.CB_Play_UpdateHealth); - RegisterPacket(PacketType.CB_Play_DeathCombatEvent); - RegisterPacket(PacketType.CB_Play_Respawn); - RegisterPacket(PacketType.CB_Play_EntityDestroy); - RegisterPacket(PacketType.CB_Play_EntityVelocity); - RegisterPacket(PacketType.CB_Play_RelEntityMove); - RegisterPacket(PacketType.CB_Play_EntityMoveLook); - RegisterPacket(PacketType.CB_Play_EntityLook); - RegisterPacket(PacketType.CB_Play_EntityTeleport); - RegisterPacket(PacketType.CB_Play_EntityUpdateAttributes); - RegisterPacket(PacketType.CB_Play_DeclareCommands); - RegisterPacket(PacketType.CB_Play_Chat); - RegisterPacket(PacketType.CB_Play_PlayerChat); - RegisterPacket(PacketType.CB_Play_NamedEntitySpawn); - RegisterPacket(PacketType.CB_Play_PlayerInfo); - RegisterPacket(PacketType.CB_Play_PlayerRemove); - RegisterPacket(PacketType.CB_Play_GameStateChange); - RegisterPacket(PacketType.CB_Play_AcknowledgePlayerDigging); - RegisterPacket(PacketType.CB_Play_WindowItems); - RegisterPacket(PacketType.CB_Play_SetSlot); - RegisterPacket(PacketType.CB_Play_OpenWindow); - RegisterPacket(PacketType.CB_Play_CloseWindow); - RegisterPacket(PacketType.CB_Play_HeldItemSlot); - RegisterPacket(PacketType.CB_Play_SystemChat); - RegisterPacket(PacketType.CB_Play_ProfilelessChat); - RegisterPacket(PacketType.CB_Play_EntityStatus); - RegisterPacket(PacketType.CB_Play_ChunkBatchStart); - RegisterPacket(PacketType.CB_Play_ChunkBatchFinished); - RegisterPacket(PacketType.CB_Play_Ping); - RegisterPacket(PacketType.CB_Play_KickDisconnect); - RegisterPacket(PacketType.CB_Play_SetPassengers); - - RegisterPacket(PacketType.SB_Play_KeepAlive); - RegisterPacket(PacketType.SB_Play_Position); - RegisterPacket(PacketType.SB_Play_PositionLook); - RegisterPacket(PacketType.SB_Play_ClientCommand); - RegisterPacket(PacketType.SB_Play_Chat); - RegisterPacket(PacketType.SB_Play_ChatMessage); - RegisterPacket(PacketType.SB_Play_ChatCommand); - RegisterPacket(PacketType.SB_Play_MessageAcknowledgement); - RegisterPacket(PacketType.SB_Play_ChatSessionUpdate); - RegisterPacket(PacketType.SB_Play_TeleportConfirm); - RegisterPacket(PacketType.SB_Play_UpdateCommandBlock); - RegisterPacket(PacketType.SB_Play_WindowClick); - RegisterPacket(PacketType.SB_Play_BlockPlace); - RegisterPacket(PacketType.SB_Play_BlockDig); - RegisterPacket(PacketType.SB_Play_ArmAnimation); - RegisterPacket(PacketType.SB_Play_UseEntity); - RegisterPacket(PacketType.SB_Play_CloseWindow); - RegisterPacket(PacketType.SB_Play_EntityAction); - RegisterPacket(PacketType.SB_Play_UseItem); - RegisterPacket(PacketType.SB_Play_HeldItemSlot); - RegisterPacket(PacketType.SB_Play_ChunkBatchReceived); - RegisterPacket(PacketType.SB_Play_SetCreativeSlot); - RegisterPacket(PacketType.SB_Play_Pong); - RegisterPacket(PacketType.SB_Play_Settings); - } + // CB + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); - private static void RegisterPacket(PacketType type) where TPacket : IPacket - { - PacketFactories.Add(type, TPacket.Read); - ClassToTypeMap.Add(typeof(TPacket).GUID, type); + // SB + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + RegisterPacket(); + + return packetFactories.ToFrozenDictionary(); } + } diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Configuration/ClientInformationPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Configuration/ClientInformationPacket.cs index 496262ed..b89c1ad2 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Configuration/ClientInformationPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Configuration/ClientInformationPacket.cs @@ -1,58 +1,63 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Serverbound.Configuration; -#pragma warning disable CS1591 -public class ClientInformationPacket : IPacket -{ - public ClientInformationPacket(string locale, byte viewDistance, int chatMode, bool chatColors, - byte displayedSkinParts, int mainHand, - bool enableTextFiltering, bool allowServerListings) - { - Locale = locale; - ViewDistance = viewDistance; - ChatMode = chatMode; - ChatColors = chatColors; - DisplayedSkinParts = displayedSkinParts; - MainHand = mainHand; - EnableTextFiltering = enableTextFiltering; - AllowServerListings = allowServerListings; - } - public string Locale { get; set; } - public byte ViewDistance { get; set; } - public int ChatMode { get; set; } - public bool ChatColors { get; set; } - public byte DisplayedSkinParts { get; set; } - public int MainHand { get; set; } - public bool EnableTextFiltering { get; set; } - public bool AllowServerListings { get; set; } - public PacketType Type => PacketType.SB_Configuration_Settings; +/// +/// Client information packet +/// +/// The locale of the client +/// The view distance setting +/// The chat mode setting +/// Whether chat colors are enabled +/// The displayed skin parts +/// The main hand setting +/// Whether text filtering is enabled +/// Whether server listings are allowed +public sealed record ClientInformationPacket( + string Locale, + byte ViewDistance, + ChatMode ChatMode, + bool ChatColors, + SkinPart DisplayedSkinParts, + PlayerHand MainHand, + bool EnableTextFiltering, + bool AllowServerListings +) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Configuration_Settings; + /// public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteString(Locale); buffer.WriteByte(ViewDistance); - buffer.WriteVarInt(ChatMode); + buffer.WriteVarInt((int)ChatMode); buffer.WriteBool(ChatColors); - buffer.WriteByte(DisplayedSkinParts); - buffer.WriteVarInt(MainHand); + buffer.WriteByte((byte)DisplayedSkinParts); + buffer.WriteVarInt((int)MainHand); buffer.WriteBool(EnableTextFiltering); buffer.WriteBool(AllowServerListings); } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { return new ClientInformationPacket( buffer.ReadString(), buffer.ReadByte(), - buffer.ReadVarInt(), + (ChatMode)buffer.ReadVarInt(), buffer.ReadBool(), - buffer.ReadByte(), - buffer.ReadVarInt(), + (SkinPart)buffer.ReadByte(), + (PlayerHand)buffer.ReadVarInt(), buffer.ReadBool(), - buffer.ReadBool()); + buffer.ReadBool() + ); } } -#pragma warning restore CS1591 + diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Configuration/FinishConfigurationPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Configuration/FinishConfigurationPacket.cs index 9c742462..47f11676 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Configuration/FinishConfigurationPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Configuration/FinishConfigurationPacket.cs @@ -1,16 +1,21 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Serverbound.Configuration; #pragma warning disable CS1591 -public class FinishConfigurationPacket : IPacket +public sealed record FinishConfigurationPacket : IPacket { - public PacketType Type => PacketType.SB_Configuration_FinishConfiguration; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Configuration_FinishConfiguration; + /// public void Write(PacketBuffer buffer, MinecraftData version) { } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { return new FinishConfigurationPacket(); diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Configuration/KeepAlivePacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Configuration/KeepAlivePacket.cs index dda74fd0..4ed55555 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Configuration/KeepAlivePacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Configuration/KeepAlivePacket.cs @@ -1,27 +1,30 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Serverbound.Configuration; -#pragma warning disable CS1591 -public class KeepAlivePacket : IPacket -{ - public KeepAlivePacket(long keepAliveId) - { - KeepAliveId = keepAliveId; - } - public long KeepAliveId { get; set; } - public PacketType Type => PacketType.SB_Configuration_KeepAlive; +/// +/// Keep alive packet +/// +/// The keep-alive ID +public sealed record KeepAlivePacket(long KeepAliveId) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Configuration_KeepAlive; + /// public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteLong(KeepAliveId); } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { return new KeepAlivePacket(buffer.ReadLong()); } } -#pragma warning restore CS1591 + diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Configuration/PluginMessagePacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Configuration/PluginMessagePacket.cs index 9140de57..fef43cb5 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Configuration/PluginMessagePacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Configuration/PluginMessagePacket.cs @@ -1,32 +1,34 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Serverbound.Configuration; -#pragma warning disable CS1591 -public class PluginMessagePacket : IPacket -{ - public PluginMessagePacket(string channelName, PacketBuffer data) - { - ChannelName = channelName; - Data = data; - } - public string ChannelName { get; set; } - public PacketBuffer Data { get; set; } - public PacketType Type => PacketType.SB_Configuration_CustomPayload; +/// +/// Plugin message packet +/// +/// The name of the channel +/// The data of the plugin message +public sealed record PluginMessagePacket(Identifier Channel, byte[] Data) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Configuration_CustomPayload; + /// public void Write(PacketBuffer buffer, MinecraftData version) { - buffer.WriteString(ChannelName); - buffer.WriteBytes(Data.GetBuffer()); + buffer.WriteIdentifier(Channel); + buffer.WriteBytes(Data); } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { - var channelName = buffer.ReadString(); - var clone = new PacketBuffer(buffer.ReadBytes((int)buffer.ReadableBytes), version.Version.Protocol); - return new PluginMessagePacket(channelName, clone); + var channel = buffer.ReadIdentifier(); + var data = buffer.RestBuffer(); + return new PluginMessagePacket(channel, data); } } -#pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Configuration/PongPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Configuration/PongPacket.cs index ce2f55f4..1f966fe8 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Configuration/PongPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Configuration/PongPacket.cs @@ -1,28 +1,30 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Serverbound.Configuration; -#pragma warning disable CS1591 -public class PongPacket : IPacket -{ - public PongPacket(int id) - { - Id = id; - } - public int Id { get; set; } - public PacketType Type => PacketType.SB_Configuration_Pong; +/// +/// Pong packet +/// +/// The ID of the pong packet +public sealed record PongPacket(int Id) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Configuration_Pong; + /// public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteInt(Id); } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { - return new PongPacket( - buffer.ReadInt()); + return new PongPacket(buffer.ReadInt()); } } -#pragma warning restore CS1591 + diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Configuration/ResourcePackResponsePacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Configuration/ResourcePackResponsePacket.cs index da6ab1b4..7c1d7f1a 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Configuration/ResourcePackResponsePacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Configuration/ResourcePackResponsePacket.cs @@ -1,28 +1,33 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; +using MineSharp.Protocol.Packets.NetworkTypes; + namespace MineSharp.Protocol.Packets.Serverbound.Configuration; -#pragma warning disable CS1591 -public class ResourcePackResponsePacket : IPacket +/// +/// Resource pack response packet +/// +/// The result of the resource pack response +public sealed record ResourcePackResponsePacket(ResourcePackResult Result) : IPacket { - public ResourcePackResponsePacket(int result) - { - Result = result; - } - - public int Result { get; set; } - public PacketType Type => PacketType.SB_Configuration_ResourcePackReceive; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Configuration_ResourcePackReceive; + /// public void Write(PacketBuffer buffer, MinecraftData version) { - buffer.WriteVarInt(Result); + buffer.WriteVarInt((int)Result); } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { - return new ResourcePackResponsePacket( - buffer.ReadVarInt()); + var result = (ResourcePackResult)buffer.ReadVarInt(); + + return new ResourcePackResponsePacket(result); } + } -#pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Handshaking/HandshakePacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Handshaking/HandshakePacket.cs index de167c7a..54130920 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Handshaking/HandshakePacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Handshaking/HandshakePacket.cs @@ -1,28 +1,26 @@ -using MineSharp.Core.Common; -using MineSharp.Core.Common.Protocol; +using MineSharp.Core.Common.Protocol; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Serverbound.Handshaking; #pragma warning disable CS1591 - -public class HandshakePacket : IPacket +/// +/// Packet for setting the protocol during handshake +/// +/// The protocol version +/// The host address +/// The port number +/// The next game state +public sealed record HandshakePacket(int ProtocolVersion, string Host, ushort Port, GameState NextState) : IPacket { - public HandshakePacket(int protocolVersion, string host, ushort port, GameState nextState) - { - ProtocolVersion = protocolVersion; - Host = host; - Port = port; - NextState = nextState; - } - - public int ProtocolVersion { get; set; } - public string Host { get; set; } - public ushort Port { get; set; } - public GameState NextState { get; set; } - public PacketType Type => PacketType.SB_Handshake_SetProtocol; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Handshake_SetProtocol; + /// public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteVarInt(ProtocolVersion); @@ -31,6 +29,7 @@ public void Write(PacketBuffer buffer, MinecraftData version) buffer.WriteVarInt((int)NextState); } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { var protocolVersion = buffer.ReadVarInt(); @@ -41,5 +40,4 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) return new HandshakePacket(protocolVersion, host, port, nextState); } } - #pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Login/AcknowledgeLoginPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Login/AcknowledgeLoginPacket.cs deleted file mode 100644 index ebd9c86b..00000000 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Login/AcknowledgeLoginPacket.cs +++ /dev/null @@ -1,19 +0,0 @@ -using MineSharp.Core.Common; -using MineSharp.Data; -using MineSharp.Data.Protocol; - -namespace MineSharp.Protocol.Packets.Serverbound.Login; -#pragma warning disable CS1591 -public class AcknowledgeLoginPacket : IPacket -{ - public PacketType Type => PacketType.SB_Login_LoginAcknowledged; - - public void Write(PacketBuffer buffer, MinecraftData version) - { } - - public static IPacket Read(PacketBuffer buffer, MinecraftData version) - { - return new AcknowledgeLoginPacket(); - } -} -#pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Login/EncryptionResponsePacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Login/EncryptionResponsePacket.cs index 7e064241..5fe94389 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Login/EncryptionResponsePacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Login/EncryptionResponsePacket.cs @@ -1,25 +1,26 @@ using MineSharp.Core; -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; using MineSharp.Protocol.Exceptions; +using static MineSharp.Protocol.Packets.Serverbound.Login.EncryptionResponsePacket; namespace MineSharp.Protocol.Packets.Serverbound.Login; -#pragma warning disable CS1591 -public class EncryptionResponsePacket : IPacket -{ - public EncryptionResponsePacket(byte[] sharedSecret, byte[]? verifyToken, CryptoContainer? crypto) - { - SharedSecret = sharedSecret; - VerifyToken = verifyToken; - Crypto = crypto; - } - public byte[] SharedSecret { get; set; } - public byte[]? VerifyToken { get; set; } - public CryptoContainer? Crypto { get; set; } - public PacketType Type => PacketType.SB_Login_EncryptionBegin; +/// +/// Encryption response packet +/// +/// The shared secret +/// The verify token +/// The crypto container +public sealed record EncryptionResponsePacket(byte[] SharedSecret, byte[]? VerifyToken, CryptoContainer? Crypto) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Login_EncryptionBegin; + /// public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteVarInt(SharedSecret.Length); @@ -51,6 +52,7 @@ public void Write(PacketBuffer buffer, MinecraftData version) buffer.WriteBytes(VerifyToken); } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { var sharedSecretLength = buffer.ReadVarInt(); @@ -76,18 +78,14 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) return new EncryptionResponsePacket(sharedSecret, verifyToken, crypto); } - - public class CryptoContainer : ISerializable + /// + /// Crypto container class + /// + /// The salt + /// The message signature + public sealed record CryptoContainer(long Salt, byte[] MessageSignature) : ISerializable { - public CryptoContainer(long salt, byte[] messageSignature) - { - Salt = salt; - MessageSignature = messageSignature; - } - - public long Salt { get; set; } - public byte[] MessageSignature { get; set; } - + /// public void Write(PacketBuffer buffer) { buffer.WriteLong(Salt); @@ -95,14 +93,14 @@ public void Write(PacketBuffer buffer) buffer.WriteBytes(MessageSignature.AsSpan()); } + /// public static CryptoContainer Read(PacketBuffer buffer) { var salt = buffer.ReadLong(); var length = buffer.ReadVarInt(); var verifyToken = buffer.ReadBytes(length); - return new(salt, verifyToken); + return new CryptoContainer(salt, verifyToken); } } } -#pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Login/LoginAcknowledgedPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Login/LoginAcknowledgedPacket.cs new file mode 100644 index 00000000..96915af1 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Login/LoginAcknowledgedPacket.cs @@ -0,0 +1,27 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Serverbound.Login; + +/// +/// Login acknowledged packet +/// +public sealed record LoginAcknowledgedPacket() : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Login_LoginAcknowledged; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + return new LoginAcknowledgedPacket(); + } +} + diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Login/LoginPluginResponsePacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Login/LoginPluginResponsePacket.cs index 4cb13f97..94f12880 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Login/LoginPluginResponsePacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Login/LoginPluginResponsePacket.cs @@ -1,21 +1,25 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Serverbound.Login; -#pragma warning disable CS1591 -public class LoginPluginResponsePacket : IPacket + +/// +/// Login plugin response packet +/// +/// The message ID +/// The data +public sealed record LoginPluginResponsePacket(int MessageId, byte[]? Data) : IPacket { - public LoginPluginResponsePacket(int messageId, byte[]? data) - { - MessageId = messageId; - Data = data; - } + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Login_LoginPluginResponse; - public int MessageId { get; set; } - public byte[]? Data { get; set; } - public PacketType Type => PacketType.SB_Login_LoginPluginResponse; + /// + public bool Successful => Data != null; + /// public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteVarInt(MessageId); @@ -27,6 +31,7 @@ public void Write(PacketBuffer buffer, MinecraftData version) } } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { var messageId = buffer.ReadVarInt(); @@ -40,4 +45,4 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) return new LoginPluginResponsePacket(messageId, data); } } -#pragma warning restore CS1591 + diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Login/LoginStartPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Login/LoginStartPacket.cs index 9938b601..846bb6ff 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Login/LoginStartPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Login/LoginStartPacket.cs @@ -1,13 +1,27 @@ using MineSharp.Core; using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; using MineSharp.Protocol.Exceptions; namespace MineSharp.Protocol.Packets.Serverbound.Login; #pragma warning disable CS1591 -public class LoginStartPacket : IPacket +public sealed record LoginStartPacket : IPacket { + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Login_LoginStart; + + // Here is no non-argument constructor allowed + // Do not use +#pragma warning disable CS8618 + private LoginStartPacket() +#pragma warning restore CS8618 + { + } + /// /// Constructor for versions before 1.19 /// @@ -28,7 +42,6 @@ public LoginStartPacket(string username, Uuid? playerUuid) PlayerUuid = playerUuid; } - /// /// Constructor for version 1.19-1.19.2 /// @@ -42,10 +55,9 @@ public LoginStartPacket(string username, SignatureContainer? signature, Uuid? pl PlayerUuid = playerUuid; } - public string Username { get; set; } - public SignatureContainer? Signature { get; set; } - public Uuid? PlayerUuid { get; set; } - public PacketType Type => PacketType.SB_Login_LoginStart; + public string Username { get; init; } + public SignatureContainer? Signature { get; init; } + public Uuid? PlayerUuid { get; init; } public void Write(PacketBuffer buffer, MinecraftData version) { @@ -101,19 +113,8 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) return new LoginStartPacket(username, signature, playerUuid); } - public class SignatureContainer : ISerializable + public sealed record SignatureContainer(long Timestamp, byte[] PublicKey, byte[] Signature) : ISerializable { - public SignatureContainer(long timestamp, byte[] publicKey, byte[] signature) - { - Timestamp = timestamp; - PublicKey = publicKey; - Signature = signature; - } - - public long Timestamp { get; set; } - public byte[] PublicKey { get; set; } - public byte[] Signature { get; set; } - public void Write(PacketBuffer buffer) { buffer.WriteLong(Timestamp); @@ -126,12 +127,12 @@ public void Write(PacketBuffer buffer) public static SignatureContainer Read(PacketBuffer buffer) { var timestamp = buffer.ReadLong(); - Span publicKey = new byte[buffer.ReadVarInt()]; + var publicKey = new byte[buffer.ReadVarInt()]; buffer.ReadBytes(publicKey); - Span signature = new byte[buffer.ReadVarInt()]; + var signature = new byte[buffer.ReadVarInt()]; buffer.ReadBytes(signature); - return new(timestamp, publicKey.ToArray(), signature.ToArray()); + return new(timestamp, publicKey, signature); } } } diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/AcknowledgeConfigurationPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/AcknowledgeConfigurationPacket.cs new file mode 100644 index 00000000..b18297a1 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/AcknowledgeConfigurationPacket.cs @@ -0,0 +1,29 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Acknowledge Configuration packet sent by the client upon receiving a Start Configuration packet from the server. +/// +public sealed record AcknowledgeConfigurationPacket() : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_ConfigurationAcknowledged; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + // No fields to write + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + // No fields to read + return new AcknowledgeConfigurationPacket(); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/ChangeContainerSlotStatePacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/ChangeContainerSlotStatePacket.cs new file mode 100644 index 00000000..81c6edc9 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/ChangeContainerSlotStatePacket.cs @@ -0,0 +1,38 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Change Container Slot State packet. +/// This packet is sent by the client when toggling the state of a Crafter. +/// +/// The ID of the slot that was changed +/// The ID of the window that was changed +/// The new state of the slot. True for enabled, false for disabled +public sealed record ChangeContainerSlotStatePacket(int SlotId, int WindowId, bool State) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_SetSlotState; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(SlotId); + buffer.WriteVarInt(WindowId); + buffer.WriteBool(State); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var slotId = buffer.ReadVarInt(); + var windowId = buffer.ReadVarInt(); + var state = buffer.ReadBool(); + + return new ChangeContainerSlotStatePacket(slotId, windowId, state); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/ChangeDifficultyPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/ChangeDifficultyPacket.cs new file mode 100644 index 00000000..25ee788a --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/ChangeDifficultyPacket.cs @@ -0,0 +1,32 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using MineSharp.Protocol.Packets.NetworkTypes; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Change difficulty packet +/// +/// The new difficulty level +public sealed record ChangeDifficultyPacket(DifficultyLevel NewDifficulty) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_SetDifficulty; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteByte((byte)NewDifficulty); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var newDifficulty = (DifficultyLevel)buffer.ReadByte(); + + return new ChangeDifficultyPacket(newDifficulty); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/ChangeRecipeBookSettingsPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/ChangeRecipeBookSettingsPacket.cs new file mode 100644 index 00000000..00d80e1f --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/ChangeRecipeBookSettingsPacket.cs @@ -0,0 +1,51 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using static MineSharp.Protocol.Packets.Serverbound.Play.ChangeRecipeBookSettingsPacket; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Packet sent by the client to change recipe book settings. +/// +/// The ID of the recipe book. +/// Whether the book is open. +/// Whether the filter is active. +public sealed record ChangeRecipeBookSettingsPacket(RecipeBookType BookId, bool BookOpen, bool FilterActive) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_RecipeBook; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt((int)BookId); + buffer.WriteBool(BookOpen); + buffer.WriteBool(FilterActive); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var bookId = (RecipeBookType)buffer.ReadVarInt(); + var bookOpen = buffer.ReadBool(); + var filterActive = buffer.ReadBool(); + + return new ChangeRecipeBookSettingsPacket(bookId, bookOpen, filterActive); + } + + /// + /// Enum representing the different types of recipe books. + /// + public enum RecipeBookType + { +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + Crafting = 0, + Furnace = 1, + BlastFurnace = 2, + Smoker = 3 +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/ChatCommandPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/ChatCommandPacket.cs index 8bc8f85d..b1dc971b 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Play/ChatCommandPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/ChatCommandPacket.cs @@ -1,5 +1,5 @@ using MineSharp.Core; -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; using MineSharp.Protocol.Exceptions; @@ -7,8 +7,21 @@ namespace MineSharp.Protocol.Packets.Serverbound.Play; #pragma warning disable CS1591 -public class ChatCommandPacket : IPacket +public sealed record ChatCommandPacket : IPacket { + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_ChatCommand; + + // Here is no non-argument constructor allowed + // Do not use +#pragma warning disable CS8618 + private ChatCommandPacket() +#pragma warning restore CS8618 + { + } + /// /// Constructor for 1.19 - 1.19.1 /// @@ -71,16 +84,15 @@ public ChatCommandPacket(string command, long timestamp, long salt, ArgumentSign Acknowledged = acknowledged; } - public string Command { get; set; } - public long Timestamp { get; set; } - public long Salt { get; set; } - public ArgumentSignature[] Signatures { get; set; } - public bool? SignedPreview { get; set; } - public ChatMessageItem[]? PreviousMessages { get; set; } - public ChatMessageItem? LastRejectedMessage { get; set; } - public int? MessageCount { get; set; } - public byte[]? Acknowledged { get; set; } - public PacketType Type => PacketType.SB_Play_ChatCommand; + public string Command { get; init; } + public long Timestamp { get; init; } + public long Salt { get; init; } + public ArgumentSignature[] Signatures { get; init; } + public bool? SignedPreview { get; init; } + public ChatMessageItem[]? PreviousMessages { get; init; } + public ChatMessageItem? LastRejectedMessage { get; init; } + public int? MessageCount { get; init; } + public byte[]? Acknowledged { get; init; } public void Write(PacketBuffer buffer, MinecraftData version) { @@ -126,7 +138,7 @@ public void Write(PacketBuffer buffer, MinecraftData version) throw new MineSharpPacketVersionException(nameof(PreviousMessages), version.Version.Protocol); } - buffer.WriteVarIntArray(PreviousMessages, (buf, val) => val.Write(buf, version)); + buffer.WriteVarIntArray(PreviousMessages, (buf, val) => val.Write(buf)); var hasLastRejectedMessage = LastRejectedMessage != null; buffer.WriteBool(hasLastRejectedMessage); @@ -136,26 +148,60 @@ public void Write(PacketBuffer buffer, MinecraftData version) return; } - LastRejectedMessage!.Write(buffer, version); + LastRejectedMessage!.Write(buffer); } + private const int AfterMc1192AcknowledgedLength = 20; + public static IPacket Read(PacketBuffer buffer, MinecraftData version) { - throw new NotImplementedException(); - } + var command = buffer.ReadString(); + var timestamp = buffer.ReadLong(); + var salt = buffer.ReadLong(); + var signatures = buffer.ReadVarIntArray((buf) => ArgumentSignature.Read(buf, version)); - public class ArgumentSignature - { - public byte[] Signature; + bool? signedPreview = null; + if (ProtocolVersion.IsBetween(version.Version.Protocol, ProtocolVersion.V_1_19, ProtocolVersion.V_1_19_2)) + { + signedPreview = buffer.ReadBool(); + } - public ArgumentSignature(string argumentName, byte[] signature) + byte[]? acknowledged = null; + int? messageCount = null; + if (version.Version.Protocol >= ProtocolVersion.V_1_19_3) { - ArgumentName = argumentName; - Signature = signature; + messageCount = buffer.ReadVarInt(); + acknowledged = buffer.ReadBytes(AfterMc1192AcknowledgedLength); + } + + ChatMessageItem[]? previousMessages = null; + ChatMessageItem? lastRejectedMessage = null; + if (version.Version.Protocol == ProtocolVersion.V_1_19_2) + { + previousMessages = buffer.ReadVarIntArray(ChatMessageItem.Read); + var hasLastRejectedMessage = buffer.ReadBool(); + if (hasLastRejectedMessage) + { + lastRejectedMessage = ChatMessageItem.Read(buffer); + } } - public string ArgumentName { get; set; } + return new ChatCommandPacket + { + Command = command, + Timestamp = timestamp, + Salt = salt, + Signatures = signatures, + SignedPreview = signedPreview, + PreviousMessages = previousMessages, + LastRejectedMessage = lastRejectedMessage, + MessageCount = messageCount, + Acknowledged = acknowledged + }; + } + public sealed record ArgumentSignature(string ArgumentName, byte[] Signature) + { public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteString(ArgumentName); @@ -170,6 +216,24 @@ public void Write(PacketBuffer buffer, MinecraftData version) buffer.WriteBytes(Signature); } } + + private const int AfterMc1192SignatureLength = 256; + + public static ArgumentSignature Read(PacketBuffer buffer, MinecraftData version) + { + var argumentName = buffer.ReadString(); + byte[] signature; + if (version.Version.Protocol <= ProtocolVersion.V_1_19_2) + { + var length = buffer.ReadVarInt(); + signature = buffer.ReadBytes(length); + } + else + { + signature = buffer.ReadBytes(AfterMc1192SignatureLength); + } + return new ArgumentSignature(argumentName, signature); + } } } #pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/ChatMessagePacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/ChatMessagePacket.cs index 57b88c1b..48cb27ce 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Play/ChatMessagePacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/ChatMessagePacket.cs @@ -1,5 +1,5 @@ using MineSharp.Core; -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; using MineSharp.Protocol.Exceptions; @@ -7,8 +7,13 @@ namespace MineSharp.Protocol.Packets.Serverbound.Play; #pragma warning disable CS1591 -public class ChatMessagePacket : IPacket +public sealed record ChatMessagePacket : IPacket { + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_ChatMessage; + public ChatMessagePacket( string message, long timestamp, @@ -50,16 +55,15 @@ public ChatMessagePacket(string message, long timestamp, long salt, byte[]? sign : this(message, timestamp, salt, signature, null, null, null, messageCount, acknowledged) { } - public string Message { get; set; } - public long Timestamp { get; set; } - public long Salt { get; set; } - public byte[]? Signature { get; set; } - public bool? SignedPreview { get; set; } - public ChatMessageItem[]? PreviousMessages { get; set; } - public ChatMessageItem? LastRejectedMessage { get; set; } - public int? MessageCount { get; set; } - public byte[]? Acknowledged { get; set; } - public PacketType Type => PacketType.SB_Play_ChatMessage; + public string Message { get; init; } + public long Timestamp { get; init; } + public long Salt { get; init; } + public byte[]? Signature { get; init; } + public bool? SignedPreview { get; init; } + public ChatMessageItem[]? PreviousMessages { get; init; } + public ChatMessageItem? LastRejectedMessage { get; init; } + public int? MessageCount { get; init; } + public byte[]? Acknowledged { get; init; } public void Write(PacketBuffer buffer, MinecraftData version) { @@ -118,7 +122,7 @@ public void Write(PacketBuffer buffer, MinecraftData version) throw new MineSharpPacketVersionException(nameof(PreviousMessages), version.Version.Protocol); } - buffer.WriteVarIntArray(PreviousMessages, (buf, val) => val.Write(buf, version)); + buffer.WriteVarIntArray(PreviousMessages, (buf, val) => val.Write(buf)); var hasLastRejectedMessage = LastRejectedMessage != null; buffer.WriteBool(hasLastRejectedMessage); @@ -128,7 +132,7 @@ public void Write(PacketBuffer buffer, MinecraftData version) return; } - LastRejectedMessage!.Write(buffer, version); + LastRejectedMessage!.Write(buffer); } public static IPacket Read(PacketBuffer buffer, MinecraftData version) @@ -176,12 +180,12 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) } - previousMessages = buffer.ReadVarIntArray(buff => ChatMessageItem.Read(buff, version)); + previousMessages = buffer.ReadVarIntArray(ChatMessageItem.Read); var hasLastRejectedMessage = buffer.ReadBool(); if (hasLastRejectedMessage) { - lastRejectedMessage = ChatMessageItem.Read(buffer, version); + lastRejectedMessage = ChatMessageItem.Read(buffer); } else { diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/ChatPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/ChatPacket.cs index c690dec9..03f3b8c3 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Play/ChatPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/ChatPacket.cs @@ -1,4 +1,4 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; @@ -7,15 +7,12 @@ namespace MineSharp.Protocol.Packets.Serverbound.Play; /// /// ChatPacket used before 1.19 to send a Chat message /// -public class ChatPacket : IPacket +public sealed record ChatPacket(string Message) : IPacket { - public ChatPacket(string message) - { - Message = message; - } - - public string Message { get; set; } - public PacketType Type => PacketType.SB_Play_Chat; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_Chat; public void Write(PacketBuffer buffer, MinecraftData version) { diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/ChunkBatchReceivedPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/ChunkBatchReceivedPacket.cs index 08faa345..873e86cb 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Play/ChunkBatchReceivedPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/ChunkBatchReceivedPacket.cs @@ -1,4 +1,4 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; @@ -8,24 +8,12 @@ namespace MineSharp.Protocol.Packets.Serverbound.Play; /// The ChunkBatchReceived packet, used since 1.20.2. /// https://wiki.vg/Protocol#Chunk_Batch_Received /// -public class ChunkBatchReceivedPacket : IPacket +public sealed record ChunkBatchReceivedPacket(float ChunksPerTick) : IPacket { - /// - /// Create a new ChunkBatchReceivedPacket instance - /// - /// - public ChunkBatchReceivedPacket(float chunksPerTick) - { - ChunksPerTick = chunksPerTick; - } - - /// - /// ChunksPerTick - /// - public float ChunksPerTick { get; set; } - /// - public PacketType Type => PacketType.SB_Play_ChunkBatchReceived; + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_ChunkBatchReceived; /// public void Write(PacketBuffer buffer, MinecraftData version) diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/ClientCommandPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/ClientCommandPacket.cs index 5f9f9f5b..b14a8670 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Play/ClientCommandPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/ClientCommandPacket.cs @@ -1,28 +1,47 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; +using static MineSharp.Protocol.Packets.Serverbound.Play.ClientCommandPacket; namespace MineSharp.Protocol.Packets.Serverbound.Play; -#pragma warning disable CS1591 -public class ClientCommandPacket : IPacket -{ - public ClientCommandPacket(int actionId) - { - ActionId = actionId; - } - public int ActionId { get; set; } - public PacketType Type => PacketType.SB_Play_ClientCommand; +/// +/// Represents a client command packet. +/// +/// The action ID of the client command. +public sealed record ClientCommandPacket(ClientCommandAction ActionId) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_ClientCommand; + /// public void Write(PacketBuffer buffer, MinecraftData version) { - buffer.WriteVarInt(ActionId); + buffer.WriteVarInt((int)ActionId); } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { - var actionId = buffer.ReadVarInt(); + var actionId = (ClientCommandAction)buffer.ReadVarInt(); + return new ClientCommandPacket(actionId); } + + /// + /// Represents the action ID of the client command. + /// + public enum ClientCommandAction + { + /// + /// Sent when the client is ready to complete login and when the client is ready to respawn after death. + /// + PerformRespawn = 0, + /// + /// Sent when the client opens the Statistics menu. + /// + RequestStats = 1, + } } -#pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/ClientInformationPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/ClientInformationPacket.cs index 1d2cf871..1c672da1 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Play/ClientInformationPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/ClientInformationPacket.cs @@ -1,43 +1,34 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Serverbound.Play; #pragma warning disable CS1591 -public class ClientInformationPacket : IPacket +public sealed record ClientInformationPacket( + string Locale, + byte ViewDistance, + ChatMode ChatMode, + bool ChatColors, + SkinPart DisplayedSkinParts, + PlayerHand MainHand, + bool EnableTextFiltering, + bool AllowServerListings +) : IPacket { - public ClientInformationPacket(string locale, byte viewDistance, int chatMode, bool chatColors, - byte displayedSkinParts, int mainHand, - bool enableTextFiltering, bool allowServerListings) - { - Locale = locale; - ViewDistance = viewDistance; - ChatMode = chatMode; - ChatColors = chatColors; - DisplayedSkinParts = displayedSkinParts; - MainHand = mainHand; - EnableTextFiltering = enableTextFiltering; - AllowServerListings = allowServerListings; - } - - public string Locale { get; set; } - public byte ViewDistance { get; set; } - public int ChatMode { get; set; } - public bool ChatColors { get; set; } - public byte DisplayedSkinParts { get; set; } - public int MainHand { get; set; } - public bool EnableTextFiltering { get; set; } - public bool AllowServerListings { get; set; } - public PacketType Type => PacketType.SB_Play_Settings; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_Settings; public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteString(Locale); buffer.WriteByte(ViewDistance); - buffer.WriteVarInt(ChatMode); + buffer.WriteVarInt((int)ChatMode); buffer.WriteBool(ChatColors); - buffer.WriteByte(DisplayedSkinParts); - buffer.WriteVarInt(MainHand); + buffer.WriteByte((byte)DisplayedSkinParts); + buffer.WriteVarInt((int)MainHand); buffer.WriteBool(EnableTextFiltering); buffer.WriteBool(AllowServerListings); } @@ -47,12 +38,13 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) return new ClientInformationPacket( buffer.ReadString(), buffer.ReadByte(), - buffer.ReadVarInt(), + (ChatMode)buffer.ReadVarInt(), buffer.ReadBool(), - buffer.ReadByte(), - buffer.ReadVarInt(), + (SkinPart)buffer.ReadByte(), + (PlayerHand)buffer.ReadVarInt(), buffer.ReadBool(), - buffer.ReadBool()); + buffer.ReadBool() + ); } } #pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/CloseWindowPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/CloseWindowPacket.cs index a70c0448..2731c572 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Play/CloseWindowPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/CloseWindowPacket.cs @@ -1,18 +1,15 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Serverbound.Play; #pragma warning disable CS1591 -public class CloseWindowPacket : IPacket +public sealed record CloseWindowPacket(byte WindowId) : IPacket { - public CloseWindowPacket(byte windowId) - { - WindowId = windowId; - } - - public byte WindowId { get; set; } - public PacketType Type => PacketType.SB_Play_CloseWindow; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_CloseWindow; public void Write(PacketBuffer buffer, MinecraftData version) { @@ -21,8 +18,7 @@ public void Write(PacketBuffer buffer, MinecraftData version) public static IPacket Read(PacketBuffer buffer, MinecraftData version) { - return new CloseWindowPacket( - buffer.ReadByte()); + return new CloseWindowPacket(buffer.ReadByte()); } } #pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/CommandBlockUpdatePacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/CommandBlockUpdatePacket.cs new file mode 100644 index 00000000..cef20fae --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/CommandBlockUpdatePacket.cs @@ -0,0 +1,62 @@ +using MineSharp.Core.Geometry; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using static MineSharp.Protocol.Packets.Serverbound.Play.CommandBlockUpdatePacket; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Sent by the client when the player updated a command block. +/// +/// The position of the command block. +/// The command to be executed by the command block. +/// The mode of the command block. +/// The flags for the command block. +public sealed record CommandBlockUpdatePacket(Position Location, string Command, CommandBlockMode Mode, CommandBlockFlags Flags) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_UpdateCommandBlock; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WritePosition(Location); + buffer.WriteString(Command); + buffer.WriteVarInt((int)Mode); + buffer.WriteSByte((sbyte)Flags); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var location = buffer.ReadPosition(); + var command = buffer.ReadString(); + var mode = (CommandBlockMode)buffer.ReadVarInt(); + var flags = (CommandBlockFlags)buffer.ReadSByte(); + + return new CommandBlockUpdatePacket(location, command, mode, flags); + } + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public enum CommandBlockMode + { + Sequence, + Auto, + Redstone + } + + [Flags] + public enum CommandBlockFlags : sbyte + { + /// + /// If not set, the output of the previous command will not be stored within the command block. + /// + TrackOutput = 0x01, + Conditional = 0x02, + Automatic = 0x04 + } +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/CommandSuggestionsRequestPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/CommandSuggestionsRequestPacket.cs new file mode 100644 index 00000000..c9f019eb --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/CommandSuggestionsRequestPacket.cs @@ -0,0 +1,34 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Command Suggestions Request packet +/// +/// The id of the transaction +/// All text behind the cursor without the '/' +public sealed record CommandSuggestionsRequestPacket(int TransactionId, string Text) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_TabComplete; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(TransactionId); + buffer.WriteString(Text); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var transactionId = buffer.ReadVarInt(); + var text = buffer.ReadString(); + + return new CommandSuggestionsRequestPacket(transactionId, text); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/ConfirmTeleportPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/ConfirmTeleportPacket.cs index 9de1e18f..746612b8 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Play/ConfirmTeleportPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/ConfirmTeleportPacket.cs @@ -1,18 +1,15 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Serverbound.Play; #pragma warning disable CS1591 -public class ConfirmTeleportPacket : IPacket +public sealed record ConfirmTeleportPacket(int TeleportId) : IPacket { - public ConfirmTeleportPacket(int teleportId) - { - TeleportId = teleportId; - } - - public int TeleportId { get; set; } - public PacketType Type => PacketType.SB_Play_TeleportConfirm; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_TeleportConfirm; public void Write(PacketBuffer buffer, MinecraftData version) { diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/EditBookPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/EditBookPacket.cs new file mode 100644 index 00000000..3a7bb33f --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/EditBookPacket.cs @@ -0,0 +1,43 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Edit Book packet sent by the client to edit or sign a book. +/// +/// The hotbar slot where the written book is located +/// Text from each page. Maximum array size is 200. Maximum string length is 8192 chars. +/// Title of the book. Only present if the book is being signed. +public sealed record EditBookPacket(int Slot, string[] Entries, string? Title) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_EditBook; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(Slot); + buffer.WriteVarIntArray(Entries, (buf, entry) => buf.WriteString(entry)); + var hasTitle = Title != null; + buffer.WriteBool(hasTitle); + if (hasTitle) + { + buffer.WriteString(Title!); + } + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var slot = buffer.ReadVarInt(); + var entries = buffer.ReadVarIntArray(buf => buf.ReadString()); + var hasTitle = buffer.ReadBool(); + var title = hasTitle ? buffer.ReadString() : null; + + return new EditBookPacket(slot, entries, title); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/EntityActionPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/EntityActionPacket.cs index f914fdfd..9dee9ea7 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Play/EntityActionPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/EntityActionPacket.cs @@ -1,36 +1,27 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Serverbound.Play; -#pragma warning disable CS1591 -public class EntityActionPacket : IPacket -{ - public enum EntityAction - { - StartSneaking = 0, - StopSneaking = 1, - LeaveBed = 2, - StartSprinting = 3, - StopSprinting = 4, - StartJumpWithHorse = 5, - StopJumpWithHorse = 6, - OpenVehicleInventory = 7, - StartFlyingWithElytra = 8 - } - - public EntityActionPacket(int entityId, EntityAction action, int jumpBoost) - { - EntityId = entityId; - Action = action; - JumpBoost = jumpBoost; - } - public int EntityId { get; set; } - public EntityAction Action { get; set; } - public int JumpBoost { get; set; } - public PacketType Type => PacketType.SB_Play_EntityAction; +/// +/// Sent by the client to indicate that it has performed certain actions: sneaking (crouching), sprinting, exiting a bed, jumping with a horse, and opening a horse's inventory while riding it. +/// +/// Leave bed is only sent when the "Leave Bed" button is clicked on the sleep GUI, not when waking up in the morning. +/// +/// Open vehicle inventory is only sent when pressing the inventory key (default: E) while on a horse or chest boat — all other methods of opening such an inventory (involving right-clicking or shift-right-clicking it) do not use this packet. +/// +/// Player ID +/// The ID of the action +/// Only used by the “start jump with horse” action, in which case it ranges from 0 to 100. In all other cases it is 0. +public sealed record EntityActionPacket(int EntityId, EntityActionPacket.EntityAction Action, int JumpBoost) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_EntityAction; + /// public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteVarInt(EntityId); @@ -38,6 +29,7 @@ public void Write(PacketBuffer buffer, MinecraftData version) buffer.WriteVarInt(JumpBoost); } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { return new EntityActionPacket( @@ -45,6 +37,20 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) (EntityAction)buffer.ReadVarInt(), buffer.ReadVarInt()); } -} + +#pragma warning disable CS1591 + public enum EntityAction + { + StartSneaking = 0, + StopSneaking = 1, + LeaveBed = 2, + StartSprinting = 3, + StopSprinting = 4, + StartJumpWithHorse = 5, + StopJumpWithHorse = 6, + OpenVehicleInventory = 7, + StartFlyingWithElytra = 8 + } #pragma warning restore CS1591 +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/InteractPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/InteractPacket.cs index 48d6b41e..61e5fae0 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Play/InteractPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/InteractPacket.cs @@ -1,59 +1,36 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; +using static MineSharp.Protocol.Packets.Serverbound.Play.InteractPacket; namespace MineSharp.Protocol.Packets.Serverbound.Play; #pragma warning disable CS1591 -public class InteractPacket : IPacket +public sealed record InteractPacket( + int EntityId, + InteractionType Interaction, + float? TargetX, + float? TargetY, + float? TargetZ, + PlayerHand? Hand, + bool Sneaking +) : IPacket { - public enum InteractionType - { - Interact = 0, - Attack = 1, - InteractAt = 2 - } - /// - /// Constructor + /// Constructor for all interaction types except . /// /// /// /// public InteractPacket(int entityId, InteractionType interaction, bool sneaking) + : this(entityId, interaction, null, null, null, null, sneaking) { - EntityId = entityId; - Interaction = interaction; - Sneaking = sneaking; } - /// - /// Constructor for - /// - /// - /// - /// - /// - /// - /// - public InteractPacket(int entityId, float targetX, float targetY, float targetZ, PlayerHand hand, bool sneaking) - { - EntityId = entityId; - Interaction = InteractionType.InteractAt; - TargetX = targetX; - TargetY = targetY; - TargetZ = targetZ; - Hand = hand; - Sneaking = sneaking; - } - - public int EntityId { get; set; } - public InteractionType Interaction { get; set; } - public float? TargetX { get; set; } - public float? TargetY { get; set; } - public float? TargetZ { get; set; } - public PlayerHand? Hand { get; set; } - public bool Sneaking { get; set; } - public PacketType Type => PacketType.SB_Play_UseEntity; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_UseEntity; public void Write(PacketBuffer buffer, MinecraftData version) { @@ -79,17 +56,31 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) { return new InteractPacket( entityId, + interaction, buffer.ReadFloat(), buffer.ReadFloat(), buffer.ReadFloat(), (PlayerHand)buffer.ReadVarInt(), - buffer.ReadBool()); + buffer.ReadBool() + ); } return new InteractPacket( entityId, interaction, - buffer.ReadBool()); + null, + null, + null, + null, + buffer.ReadBool() + ); + } + + public enum InteractionType + { + Interact = 0, + Attack = 1, + InteractAt = 2 } } #pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/JigsawGeneratePacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/JigsawGeneratePacket.cs new file mode 100644 index 00000000..82a51182 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/JigsawGeneratePacket.cs @@ -0,0 +1,38 @@ +using MineSharp.Core.Geometry; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Sent when Generate is pressed on the Jigsaw Block interface. +/// +/// Block entity location. +/// Value of the levels slider/max depth to generate. +/// Whether to keep jigsaws. +public sealed record JigsawGeneratePacket(Position Location, int Levels, bool KeepJigsaws) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_GenerateStructure; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WritePosition(Location); + buffer.WriteVarInt(Levels); + buffer.WriteBool(KeepJigsaws); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var location = buffer.ReadPosition(); + var levels = buffer.ReadVarInt(); + var keepJigsaws = buffer.ReadBool(); + + return new JigsawGeneratePacket(location, levels, keepJigsaws); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/KeepAlivePacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/KeepAlivePacket.cs index c3ae77fd..d0075138 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Play/KeepAlivePacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/KeepAlivePacket.cs @@ -1,18 +1,15 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Serverbound.Play; #pragma warning disable CS1591 -public class KeepAlivePacket : IPacket +public sealed record KeepAlivePacket(long KeepAliveId) : IPacket { - public KeepAlivePacket(long id) - { - KeepAliveId = id; - } - - public long KeepAliveId { get; set; } - public PacketType Type => PacketType.SB_Play_KeepAlive; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_KeepAlive; public void Write(PacketBuffer buffer, MinecraftData version) { diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/LockDifficultyPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/LockDifficultyPacket.cs new file mode 100644 index 00000000..8bd3cc5c --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/LockDifficultyPacket.cs @@ -0,0 +1,31 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Lock Difficulty packet +/// +/// Indicates if the difficulty is locked +public sealed record LockDifficultyPacket(bool Locked) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_LockDifficulty; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteBool(Locked); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var locked = buffer.ReadBool(); + + return new LockDifficultyPacket(locked); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/MessageAcknowledgementPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/MessageAcknowledgementPacket.cs index 97c5ff33..b7e4afb1 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Play/MessageAcknowledgementPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/MessageAcknowledgementPacket.cs @@ -1,5 +1,5 @@ using MineSharp.Core; -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; using MineSharp.Protocol.Exceptions; @@ -7,8 +7,19 @@ namespace MineSharp.Protocol.Packets.Serverbound.Play; #pragma warning disable CS1591 -public class MessageAcknowledgementPacket : IPacket +public sealed record MessageAcknowledgementPacket : IPacket { + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_MessageAcknowledgement; + + // Here is no non-argument constructor allowed + // Do not use + private MessageAcknowledgementPacket() + { + } + /** * Constructor for >= 1.19.3 */ @@ -26,10 +37,9 @@ public MessageAcknowledgementPacket(ChatMessageItem[]? previousMessages, ChatMes LastRejectedMessage = lastRejectedMessage; } - public int? Count { get; set; } - public ChatMessageItem[]? PreviousMessages { get; set; } - public ChatMessageItem? LastRejectedMessage { get; set; } - public PacketType Type => PacketType.SB_Play_MessageAcknowledgement; + public int? Count { get; init; } + public ChatMessageItem[]? PreviousMessages { get; init; } + public ChatMessageItem? LastRejectedMessage { get; init; } public void Write(PacketBuffer buffer, MinecraftData version) { @@ -49,7 +59,7 @@ public void Write(PacketBuffer buffer, MinecraftData version) throw new MineSharpPacketVersionException(nameof(PreviousMessages), version.Version.Protocol); } - buffer.WriteVarIntArray(PreviousMessages, (buf, val) => val.Write(buf, version)); + buffer.WriteVarIntArray(PreviousMessages, (buf, val) => val.Write(buf)); var hasLastRejectedMessage = LastRejectedMessage != null; buffer.WriteBool(hasLastRejectedMessage); @@ -58,7 +68,7 @@ public void Write(PacketBuffer buffer, MinecraftData version) return; } - LastRejectedMessage!.Write(buffer, version); + LastRejectedMessage!.Write(buffer); } public static IPacket Read(PacketBuffer buffer, MinecraftData version) diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/MoveVehiclePacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/MoveVehiclePacket.cs new file mode 100644 index 00000000..420d95c5 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/MoveVehiclePacket.cs @@ -0,0 +1,44 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Sent when a player moves in a vehicle. Fields are the same as in Set Player Position and Rotation. +/// Note that all fields use absolute positioning and do not allow for relative positioning. +/// +/// Absolute position (X coordinate). +/// Absolute position (Y coordinate). +/// Absolute position (Z coordinate). +/// Absolute rotation on the vertical axis, in degrees. +/// Absolute rotation on the horizontal axis, in degrees. +public sealed record MoveVehiclePacket(double X, double Y, double Z, float Yaw, float Pitch) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_VehicleMove; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteDouble(X); + buffer.WriteDouble(Y); + buffer.WriteDouble(Z); + buffer.WriteFloat(Yaw); + buffer.WriteFloat(Pitch); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var x = buffer.ReadDouble(); + var y = buffer.ReadDouble(); + var z = buffer.ReadDouble(); + var yaw = buffer.ReadFloat(); + var pitch = buffer.ReadFloat(); + + return new MoveVehiclePacket(x, y, z, yaw, pitch); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/PaddleBoatPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/PaddleBoatPacket.cs new file mode 100644 index 00000000..f7cc5948 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/PaddleBoatPacket.cs @@ -0,0 +1,34 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Used to visually update whether boat paddles are turning. +/// +/// Indicates if the left paddle is turning +/// Indicates if the right paddle is turning +public sealed record PaddleBoatPacket(bool LeftPaddleTurning, bool RightPaddleTurning) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_SteerBoat; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteBool(LeftPaddleTurning); + buffer.WriteBool(RightPaddleTurning); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var leftPaddleTurning = buffer.ReadBool(); + var rightPaddleTurning = buffer.ReadBool(); + + return new PaddleBoatPacket(leftPaddleTurning, rightPaddleTurning); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/PickItemPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/PickItemPacket.cs new file mode 100644 index 00000000..a93b4550 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/PickItemPacket.cs @@ -0,0 +1,42 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Used to swap out an empty space on the hotbar with the item in the given inventory slot. +/// The Notchian client uses this for pick block functionality (middle click) to retrieve items from the inventory. +/// +/// The server first searches the player's hotbar for an empty slot, starting from the current slot and looping around to the slot before it. +/// If there are no empty slots, it starts a second search from the current slot and finds the first slot that does not contain an enchanted item. +/// If there still are no slots that meet that criteria, then the server uses the currently selected slot. +/// +/// After finding the appropriate slot, the server swaps the items and sends 3 packets: +/// +/// Set Container Slot with window ID set to -2, updating the chosen hotbar slot. +/// Set Container Slot with window ID set to -2, updating the slot where the picked item used to be. +/// Set Held Item, switching to the newly chosen slot. +/// +/// The slot to use +public sealed record PickItemPacket(int SlotToUse) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_PickItem; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(SlotToUse); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var slotToUse = buffer.ReadVarInt(); + + return new PickItemPacket(slotToUse); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/PingRequestPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/PingRequestPacket.cs new file mode 100644 index 00000000..8a798b2b --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/PingRequestPacket.cs @@ -0,0 +1,31 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Ping request packet sent by the client to the server. +/// +/// The payload, which may be any number. Notchian clients use a system-dependent time value counted in milliseconds. +public sealed record PingRequestPacket(long Payload) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_PingRequest; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteLong(Payload); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var payload = buffer.ReadLong(); + + return new PingRequestPacket(payload); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/PlaceBlockPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/PlaceBlockPacket.cs index 7a1ed711..d0817170 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Play/PlaceBlockPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/PlaceBlockPacket.cs @@ -1,13 +1,27 @@ using MineSharp.Core; using MineSharp.Core.Common; using MineSharp.Core.Geometry; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Serverbound.Play; #pragma warning disable CS1591 -public class PlaceBlockPacket : IPacket +public sealed record PlaceBlockPacket : IPacket { + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_BlockPlace; + + // Here is no non-argument constructor allowed + // Do not use +#pragma warning disable CS8618 + private PlaceBlockPacket() +#pragma warning restore CS8618 + { + } + /// /// Constructor >= 1.19 /// @@ -19,7 +33,7 @@ public class PlaceBlockPacket : IPacket /// /// /// - public PlaceBlockPacket(int hand, Position location, BlockFace direction, float cursorX, float cursorY, + public PlaceBlockPacket(PlayerHand hand, Position location, BlockFace direction, float cursorX, float cursorY, float cursorZ, bool insideBlock, int sequenceId) { @@ -43,7 +57,7 @@ public PlaceBlockPacket(int hand, Position location, BlockFace direction, float /// /// /// - public PlaceBlockPacket(int hand, Position location, BlockFace direction, float cursorX, float cursorY, + public PlaceBlockPacket(PlayerHand hand, Position location, BlockFace direction, float cursorX, float cursorY, float cursorZ, bool insideBlock) { Hand = hand; @@ -55,20 +69,19 @@ public PlaceBlockPacket(int hand, Position location, BlockFace direction, float InsideBlock = insideBlock; } - public int Hand { get; set; } - public Position Location { get; set; } - public BlockFace Direction { get; set; } - public float CursorX { get; set; } - public float CursorY { get; set; } - public float CursorZ { get; set; } - public bool InsideBlock { get; set; } - public int? SequenceId { get; set; } - public PacketType Type => PacketType.SB_Play_BlockPlace; + public PlayerHand Hand { get; init; } + public Position Location { get; init; } + public BlockFace Direction { get; init; } + public float CursorX { get; init; } + public float CursorY { get; init; } + public float CursorZ { get; init; } + public bool InsideBlock { get; init; } + public int? SequenceId { get; init; } public void Write(PacketBuffer buffer, MinecraftData version) { - buffer.WriteVarInt(Hand); - buffer.WriteULong(Location.ToULong()); + buffer.WriteVarInt((int)Hand); + buffer.WritePosition(Location); buffer.WriteVarInt((int)Direction); buffer.WriteFloat(CursorX); buffer.WriteFloat(CursorY); @@ -83,8 +96,8 @@ public void Write(PacketBuffer buffer, MinecraftData version) public static IPacket Read(PacketBuffer buffer, MinecraftData version) { - var hand = buffer.ReadVarInt(); - var position = new Position(buffer.ReadULong()); + var hand = (PlayerHand)buffer.ReadVarInt(); + var position = buffer.ReadPosition(); var direction = buffer.ReadVarInt(); var cursorX = buffer.ReadFloat(); var cursorY = buffer.ReadFloat(); diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/PlaceRecipePacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/PlaceRecipePacket.cs new file mode 100644 index 00000000..fc27f6e9 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/PlaceRecipePacket.cs @@ -0,0 +1,38 @@ +using MineSharp.Core.Common; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Place Recipe packet sent when a player clicks a recipe in the crafting book that is craftable (white border). +/// +/// The window ID +/// The recipe ID +/// Whether to make all items (true if shift is down when clicked) +public sealed record PlaceRecipePacket(byte WindowId, Identifier Recipe, bool MakeAll) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_CraftRecipeRequest; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteByte(WindowId); + buffer.WriteIdentifier(Recipe); + buffer.WriteBool(MakeAll); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var windowId = buffer.ReadByte(); + var recipe = buffer.ReadIdentifier(); + var makeAll = buffer.ReadBool(); + + return new PlaceRecipePacket(windowId, recipe, makeAll); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/PlayerAbilitiesPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/PlayerAbilitiesPacket.cs new file mode 100644 index 00000000..9e426f49 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/PlayerAbilitiesPacket.cs @@ -0,0 +1,32 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using MineSharp.Protocol.Packets.NetworkTypes; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Player abilities packet sent by the client to update the player's abilities. +/// +/// Bit mask indicating the player's abilities. Client may only send the flag. +public sealed record PlayerAbilitiesPacket(PlayerAbilitiesFlags Flags) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_Abilities; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteByte((byte)Flags); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var flags = (PlayerAbilitiesFlags)buffer.ReadByte(); + + return new PlayerAbilitiesPacket(flags); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/PlayerActionPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/PlayerActionPacket.cs index c9a1acea..cb961471 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Play/PlayerActionPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/PlayerActionPacket.cs @@ -1,20 +1,34 @@ using MineSharp.Core; -using MineSharp.Core.Common; using MineSharp.Core.Geometry; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; +using MineSharp.Protocol.Packets.NetworkTypes; namespace MineSharp.Protocol.Packets.Serverbound.Play; #pragma warning disable CS1591 -public class PlayerActionPacket : IPacket +public sealed record PlayerActionPacket : IPacket { + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_BlockDig; + + // Here is no non-argument constructor allowed + // Do not use +#pragma warning disable CS8618 + private PlayerActionPacket() +#pragma warning restore CS8618 + { + } + /// /// Constructor for versions before 1.19 /// /// /// /// - public PlayerActionPacket(int status, Position location, BlockFace face) + public PlayerActionPacket(PlayerActionStatus status, Position location, BlockFace face) { Status = status; Location = location; @@ -29,7 +43,7 @@ public PlayerActionPacket(int status, Position location, BlockFace face) /// /// /// - public PlayerActionPacket(int status, Position location, BlockFace face, int? sequenceId) + public PlayerActionPacket(PlayerActionStatus status, Position location, BlockFace face, int? sequenceId) { Status = status; Location = location; @@ -37,16 +51,15 @@ public PlayerActionPacket(int status, Position location, BlockFace face, int? se SequenceId = sequenceId; } - public int Status { get; set; } - public Position Location { get; set; } - public BlockFace Face { get; set; } - public int? SequenceId { get; set; } - public PacketType Type => PacketType.SB_Play_BlockDig; + public PlayerActionStatus Status { get; init; } + public Position Location { get; init; } + public BlockFace Face { get; init; } + public int? SequenceId { get; init; } public void Write(PacketBuffer buffer, MinecraftData version) { - buffer.WriteVarInt(Status); - buffer.WriteULong(Location.ToULong()); + buffer.WriteVarInt((int)Status); + buffer.WritePosition(Location); buffer.WriteByte((byte)Face); if (version.Version.Protocol >= ProtocolVersion.V_1_19) @@ -60,15 +73,15 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) if (version.Version.Protocol >= ProtocolVersion.V_1_19) { return new PlayerActionPacket( - buffer.ReadVarInt(), - new(buffer.ReadULong()), + (PlayerActionStatus)buffer.ReadVarInt(), + buffer.ReadPosition(), (BlockFace)buffer.ReadByte(), buffer.ReadVarInt()); } return new PlayerActionPacket( - buffer.ReadVarInt(), - new(buffer.ReadULong()), + (PlayerActionStatus)buffer.ReadVarInt(), + buffer.ReadPosition(), (BlockFace)buffer.ReadByte()); } } diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/PlayerInputPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/PlayerInputPacket.cs new file mode 100644 index 00000000..8ef3b904 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/PlayerInputPacket.cs @@ -0,0 +1,51 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using static MineSharp.Protocol.Packets.Serverbound.Play.PlayerInputPacket; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Player input packet sent by the client to the server. +/// +/// Positive to the left of the player. +/// Positive forward. +/// Bit mask of flags. See . +public sealed record PlayerInputPacket(float Sideways, float Forward, PlayerInputFlags Flags) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_SteerVehicle; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteFloat(Sideways); + buffer.WriteFloat(Forward); + buffer.WriteByte((byte)Flags); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var sideways = buffer.ReadFloat(); + var forward = buffer.ReadFloat(); + var flags = (PlayerInputFlags)buffer.ReadByte(); + + return new PlayerInputPacket(sideways, forward, flags); + } + + /// + /// Flags indicating player actions. + /// + [Flags] + public enum PlayerInputFlags : byte + { +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + None = 0x0, + Jump = 0x1, + Unmount = 0x2 +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/PlayerSessionPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/PlayerSessionPacket.cs index 4368f3ee..90af38a4 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Play/PlayerSessionPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/PlayerSessionPacket.cs @@ -1,24 +1,16 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Serverbound.Play; #pragma warning disable CS1591 -public class PlayerSessionPacket : IPacket +public sealed record PlayerSessionPacket(Uuid SessionId, long ExpiresAt, byte[] PublicKey, byte[] KeySignature) : IPacket { - public PlayerSessionPacket(Uuid sessionId, long expiresAt, byte[] publicKey, byte[] keySignature) - { - SessionId = sessionId; - ExpiresAt = expiresAt; - PublicKey = publicKey; - KeySignature = keySignature; - } - - public Uuid SessionId { get; set; } - public long ExpiresAt { get; set; } - public byte[] PublicKey { get; set; } - public byte[] KeySignature { get; set; } - public PacketType Type => PacketType.SB_Play_ChatSessionUpdate; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_ChatSessionUpdate; public void Write(PacketBuffer buffer, MinecraftData version) { @@ -39,8 +31,7 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) var keySignature = new byte[buffer.ReadVarInt()]; buffer.ReadBytes(keySignature); - return new PlayerSessionPacket( - sessionId, expiresAt, publicKey, keySignature); + return new PlayerSessionPacket(sessionId, expiresAt, publicKey, keySignature); } } #pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/PluginMessagePacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/PluginMessagePacket.cs new file mode 100644 index 00000000..d7a658a8 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/PluginMessagePacket.cs @@ -0,0 +1,35 @@ +using MineSharp.Core.Common; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Serverbound Plugin Message packet +/// +/// Name of the plugin channel used to send the data +/// Any data, depending on the channel +public sealed record PluginMessagePacket(Identifier Channel, byte[] Data) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_CustomPayload; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteIdentifier(Channel); + buffer.WriteBytes(Data); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var channel = buffer.ReadIdentifier(); + var data = buffer.ReadBytes((int)buffer.ReadableBytes); + + return new PluginMessagePacket(channel, data); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/PongPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/PongPacket.cs index 5cba6626..e7a9bf2e 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Play/PongPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/PongPacket.cs @@ -1,4 +1,4 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; @@ -7,16 +7,13 @@ namespace MineSharp.Protocol.Packets.Serverbound.Play; /// /// Pong Packet https://wiki.vg/Protocol#Ping_Response_.28play.29 /// -/// -public class PongPacket(int id) : IPacket +/// +public sealed record PongPacket(int Id) : IPacket { - /// - /// Pong id - /// - public int Id { get; set; } = id; - /// - public PacketType Type => PacketType.SB_Play_Pong; + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_Pong; /// public void Write(PacketBuffer buffer, MinecraftData version) @@ -27,7 +24,8 @@ public void Write(PacketBuffer buffer, MinecraftData version) /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { - return new PongPacket( - buffer.ReadInt()); + var id = buffer.ReadInt(); + + return new PongPacket(id); } } diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/ProgramJigsawBlockPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/ProgramJigsawBlockPacket.cs new file mode 100644 index 00000000..5a91abfe --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/ProgramJigsawBlockPacket.cs @@ -0,0 +1,70 @@ +using MineSharp.Core.Common; +using MineSharp.Core.Geometry; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Sent when Done is pressed on the Jigsaw Block interface. +/// +/// Block entity location +/// Name identifier +/// Target identifier +/// Pool identifier +/// "Turns into" on the GUI, final_state in NBT +/// Joint type, rollable if the attached piece can be rotated, else aligned +/// Selection priority +/// Placement priority +public sealed record ProgramJigsawBlockPacket( + Position Location, + Identifier Name, + Identifier Target, + Identifier Pool, + string FinalState, + string JointType, + int SelectionPriority, + int PlacementPriority) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_UpdateJigsawBlock; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WritePosition(Location); + buffer.WriteIdentifier(Name); + buffer.WriteIdentifier(Target); + buffer.WriteIdentifier(Pool); + buffer.WriteString(FinalState); + buffer.WriteString(JointType); + buffer.WriteVarInt(SelectionPriority); + buffer.WriteVarInt(PlacementPriority); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var location = buffer.ReadPosition(); + var name = buffer.ReadIdentifier(); + var target = buffer.ReadIdentifier(); + var pool = buffer.ReadIdentifier(); + var finalState = buffer.ReadString(); + var jointType = buffer.ReadString(); + var selectionPriority = buffer.ReadVarInt(); + var placementPriority = buffer.ReadVarInt(); + + return new ProgramJigsawBlockPacket( + location, + name, + target, + pool, + finalState, + jointType, + selectionPriority, + placementPriority); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/ProgramStructureBlockPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/ProgramStructureBlockPacket.cs new file mode 100644 index 00000000..9a5c9da6 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/ProgramStructureBlockPacket.cs @@ -0,0 +1,164 @@ +using MineSharp.Core.Geometry; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using static MineSharp.Protocol.Packets.Serverbound.Play.ProgramStructureBlockPacket; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; +/// +/// Program Structure Block packet +/// +/// Block entity location +/// An additional action to perform beyond simply saving the given data. See +/// One of +/// Name of the structure +/// Offset X, between -48 and 48 +/// Offset Y, between -48 and 48 +/// Offset Z, between -48 and 48 +/// Size X, between 0 and 48 +/// Size Y, between 0 and 48 +/// Size Z, between 0 and 48 +/// One of +/// One of +/// Metadata of the structure +/// Integrity, between 0 and 1 +/// Seed for the structure +/// Flags. See +public sealed record ProgramStructureBlockPacket( + Position Location, + StructureBlockAction Action, + StructureBlockMode Mode, + string Name, + sbyte OffsetX, + sbyte OffsetY, + sbyte OffsetZ, + sbyte SizeX, + sbyte SizeY, + sbyte SizeZ, + StructureBlockMirror Mirror, + StructureBlockRotation Rotation, + string Metadata, + float Integrity, + long Seed, + StructureBlockFlags Flags) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_UpdateStructureBlock; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WritePosition(Location); + buffer.WriteVarInt((int)Action); + buffer.WriteVarInt((int)Mode); + buffer.WriteString(Name); + buffer.WriteSByte(OffsetX); + buffer.WriteSByte(OffsetY); + buffer.WriteSByte(OffsetZ); + buffer.WriteSByte(SizeX); + buffer.WriteSByte(SizeY); + buffer.WriteSByte(SizeZ); + buffer.WriteVarInt((int)Mirror); + buffer.WriteVarInt((int)Rotation); + buffer.WriteString(Metadata); + buffer.WriteFloat(Integrity); + buffer.WriteVarLong(Seed); + buffer.WriteSByte((sbyte)Flags); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var location = buffer.ReadPosition(); + var action = (StructureBlockAction)buffer.ReadVarInt(); + var mode = (StructureBlockMode)buffer.ReadVarInt(); + var name = buffer.ReadString(); + var offsetX = buffer.ReadSByte(); + var offsetY = buffer.ReadSByte(); + var offsetZ = buffer.ReadSByte(); + var sizeX = buffer.ReadSByte(); + var sizeY = buffer.ReadSByte(); + var sizeZ = buffer.ReadSByte(); + var mirror = (StructureBlockMirror)buffer.ReadVarInt(); + var rotation = (StructureBlockRotation)buffer.ReadVarInt(); + var metadata = buffer.ReadString(); + var integrity = buffer.ReadFloat(); + var seed = buffer.ReadVarLong(); + var flags = (StructureBlockFlags)buffer.ReadSByte(); + + return new ProgramStructureBlockPacket( + location, + action, + mode, + name, + offsetX, + offsetY, + offsetZ, + sizeX, + sizeY, + sizeZ, + mirror, + rotation, + metadata, + integrity, + seed, + flags); + } + + /// + /// Enum representing the action to perform on the structure block. + /// + public enum StructureBlockAction + { + UpdateData = 0, + SaveStructure = 1, + LoadStructure = 2, + DetectSize = 3 + } + + /// + /// Enum representing the mode of the structure block. + /// + public enum StructureBlockMode + { + Save = 0, + Load = 1, + Corner = 2, + Data = 3 + } + + /// + /// Enum representing the mirror type of the structure block. + /// + public enum StructureBlockMirror + { + None = 0, + LeftRight = 1, + FrontBack = 2 + } + + /// + /// Enum representing the rotation type of the structure block. + /// + public enum StructureBlockRotation + { + None = 0, + Clockwise90 = 1, + Clockwise180 = 2, + Counterclockwise90 = 3 + } + + /// + /// Enum representing the flags for the structure block. + /// + [Flags] + public enum StructureBlockFlags : sbyte + { + IgnoreEntities = 0x01, + ShowAir = 0x02, + ShowBoundingBox = 0x04 + } + +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/QueryBlockEntityTagPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/QueryBlockEntityTagPacket.cs new file mode 100644 index 00000000..4354b91d --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/QueryBlockEntityTagPacket.cs @@ -0,0 +1,36 @@ +using MineSharp.Core.Geometry; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Query Block Entity Tag packet +/// Used when F3+I is pressed while looking at a block. +/// +/// An incremental ID so that the client can verify that the response matches. +/// The location of the block to check. +public sealed record QueryBlockEntityTagPacket(int TransactionId, Position Location) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_QueryBlockNbt; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(TransactionId); + buffer.WritePosition(Location); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var transactionId = buffer.ReadVarInt(); + var location = buffer.ReadPosition(); + + return new QueryBlockEntityTagPacket(transactionId, location); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/QueryEntityTagPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/QueryEntityTagPacket.cs new file mode 100644 index 00000000..a631efdd --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/QueryEntityTagPacket.cs @@ -0,0 +1,35 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Query Entity Tag packet. +/// Used when F3+I is pressed while looking at an entity. +/// +/// An incremental ID so that the client can verify that the response matches. +/// The ID of the entity to query. +public sealed record QueryEntityTagPacket(int TransactionId, int EntityId) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_QueryEntityNbt; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(TransactionId); + buffer.WriteVarInt(EntityId); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var transactionId = buffer.ReadVarInt(); + var entityId = buffer.ReadVarInt(); + + return new QueryEntityTagPacket(transactionId, entityId); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/RenameItemPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/RenameItemPacket.cs new file mode 100644 index 00000000..f03081db --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/RenameItemPacket.cs @@ -0,0 +1,31 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Sent as a player is renaming an item in an anvil. +/// +/// The new name of the item. +public sealed record RenameItemPacket(string ItemName) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_NameItem; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteString(ItemName); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var itemName = buffer.ReadString(); + + return new RenameItemPacket(itemName); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/ResourcePackResponsePacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/ResourcePackResponsePacket.cs new file mode 100644 index 00000000..a4e152c5 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/ResourcePackResponsePacket.cs @@ -0,0 +1,36 @@ +using MineSharp.Core.Common; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using MineSharp.Protocol.Packets.NetworkTypes; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Resource Pack Response packet +/// +/// The unique identifier of the resource pack +/// The result of the resource pack response +public sealed record ResourcePackResponsePacket(Uuid Uuid, ResourcePackResult Result) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_ResourcePackReceive; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteUuid(Uuid); + buffer.WriteVarInt((int)Result); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var uuid = buffer.ReadUuid(); + var result = (ResourcePackResult)buffer.ReadVarInt(); + + return new ResourcePackResponsePacket(uuid, result); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/SeenAdvancementsPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/SeenAdvancementsPacket.cs new file mode 100644 index 00000000..47a8a2c5 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/SeenAdvancementsPacket.cs @@ -0,0 +1,56 @@ +using MineSharp.Core.Common; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; +using static MineSharp.Protocol.Packets.Serverbound.Play.SeenAdvancementsPacket; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Seen Advancements packet +/// +/// The action taken +/// The identifier of the tab, only present if action is +public sealed record SeenAdvancementsPacket(SeenAdvancementsAction Action, Identifier? TabId) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_AdvancementTab; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt((int)Action); + if (Action == SeenAdvancementsAction.OpenedTab) + { + buffer.WriteIdentifier(TabId!); + } + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var action = (SeenAdvancementsAction)buffer.ReadVarInt(); + Identifier? tabId = null; + if (action == SeenAdvancementsAction.OpenedTab) + { + tabId = buffer.ReadIdentifier(); + } + + return new SeenAdvancementsPacket(action, tabId); + } + + /// + /// Enum representing the actions for the Seen Advancements packet + /// + [Flags] + public enum SeenAdvancementsAction + { +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + None = 0, + OpenedTab = 1 << 0, + ClosedScreen = 1 << 1 +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/SelectTradePacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/SelectTradePacket.cs new file mode 100644 index 00000000..415d5f38 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/SelectTradePacket.cs @@ -0,0 +1,31 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Select Trade packet sent by the client when a player selects a specific trade offered by a villager NPC. +/// +/// The selected slot in the player's current (trading) inventory. +public sealed record SelectTradePacket(int SelectedSlot) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_SelectTrade; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(SelectedSlot); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var selectedSlot = buffer.ReadVarInt(); + + return new SelectTradePacket(selectedSlot); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetBeaconEffectPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetBeaconEffectPacket.cs new file mode 100644 index 00000000..8b646604 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetBeaconEffectPacket.cs @@ -0,0 +1,50 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Changes the effect of the current beacon. +/// +/// The primary effect ID +/// The secondary effect ID +public sealed record SetBeaconEffectPacket(int? PrimaryEffect, int? SecondaryEffect) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_SetBeaconEffect; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + var hasPrimaryEffect = PrimaryEffect.HasValue; + buffer.WriteBool(hasPrimaryEffect); + if (hasPrimaryEffect) + { + buffer.WriteVarInt(PrimaryEffect!.Value); + } + + var hasSecondaryEffect = SecondaryEffect.HasValue; + buffer.WriteBool(hasSecondaryEffect); + if (hasSecondaryEffect) + { + buffer.WriteVarInt(SecondaryEffect!.Value); + } + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var hasPrimaryEffect = buffer.ReadBool(); + int? primaryEffect = hasPrimaryEffect ? buffer.ReadVarInt() : null; + + var hasSecondaryEffect = buffer.ReadBool(); + int? secondaryEffect = hasSecondaryEffect ? buffer.ReadVarInt() : null; + + return new SetBeaconEffectPacket( + primaryEffect, + secondaryEffect); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetCreativeSlotPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetCreativeSlotPacket.cs index 8fb8649c..1220aa5f 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetCreativeSlotPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetCreativeSlotPacket.cs @@ -1,5 +1,5 @@ -using MineSharp.Core.Common; -using MineSharp.Core.Common.Items; +using MineSharp.Core.Common.Items; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; using MineSharp.Protocol.Packets.NetworkTypes; @@ -9,29 +9,14 @@ namespace MineSharp.Protocol.Packets.Serverbound.Play; /// /// Packet used to set slots in creative inventory (https://wiki.vg/Protocol#Set_Creative_Mode_Slot) /// -public class SetCreativeSlotPacket : IPacket +/// The slot index +/// The clicked Item +public sealed record SetCreativeSlotPacket(short SlotIndex, Item? Item) : IPacket { - /// - /// Constructor - /// - public SetCreativeSlotPacket(short slotIndex, Item? item) - { - SlotIndex = slotIndex; - Item = item; - } - - /// - /// The inventory slot index - /// - public short SlotIndex { get; set; } - - /// - /// The clicked item - /// - public Item? Item { get; set; } - /// - public PacketType Type => PacketType.SB_Play_SetCreativeSlot; + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_SetCreativeSlot; /// public void Write(PacketBuffer buffer, MinecraftData version) diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetHeldItemPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetHeldItemPacket.cs index 27d87f2a..aaff55fb 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetHeldItemPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetHeldItemPacket.cs @@ -1,4 +1,4 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; @@ -7,24 +7,12 @@ namespace MineSharp.Protocol.Packets.Serverbound.Play; /// /// Sent when the player changes the slot selection /// -public class SetHeldItemPacket : IPacket +public sealed record SetHeldItemPacket(short Slot) : IPacket { - /// - /// Constructor - /// - /// - public SetHeldItemPacket(short slot) - { - Slot = slot; - } - - /// - /// Index of the new selected hotbar slot (0-8) - /// - public short Slot { get; set; } - /// - public PacketType Type => PacketType.SB_Play_HeldItemSlot; + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_HeldItemSlot; /// public void Write(PacketBuffer buffer, MinecraftData version) diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetPlayerOnGroundPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetPlayerOnGroundPacket.cs new file mode 100644 index 00000000..62e3963d --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetPlayerOnGroundPacket.cs @@ -0,0 +1,31 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// This packet is used to indicate whether the player is on ground (walking/swimming), or airborne (jumping/falling). +/// +/// True if the client is on the ground, false otherwise. +public sealed record SetPlayerOnGroundPacket(bool OnGround) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_Flying; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteBool(OnGround); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var onGround = buffer.ReadBool(); + + return new SetPlayerOnGroundPacket(onGround); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetPlayerPositionAndRotationPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetPlayerPositionAndRotationPacket.cs index 3e9fbede..42ac2242 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetPlayerPositionAndRotationPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetPlayerPositionAndRotationPacket.cs @@ -1,28 +1,15 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Serverbound.Play; #pragma warning disable CS1591 -public class SetPlayerPositionAndRotationPacket : IPacket +public sealed record SetPlayerPositionAndRotationPacket(double X, double Y, double Z, float Yaw, float Pitch, bool IsOnGround) : IPacket { - public SetPlayerPositionAndRotationPacket(double x, double y, double z, float yaw, float pitch, bool isOnGround) - { - X = x; - Y = y; - Z = z; - Yaw = yaw; - Pitch = pitch; - IsOnGround = isOnGround; - } - - public double X { get; set; } - public double Y { get; set; } - public double Z { get; set; } - public float Yaw { get; set; } - public float Pitch { get; set; } - public bool IsOnGround { get; set; } - public PacketType Type => PacketType.SB_Play_PositionLook; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_PositionLook; public void Write(PacketBuffer buffer, MinecraftData version) { diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetPlayerPositionPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetPlayerPositionPacket.cs index 680d071c..d816dcc9 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetPlayerPositionPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetPlayerPositionPacket.cs @@ -1,24 +1,15 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Serverbound.Play; #pragma warning disable CS1591 -public class SetPlayerPositionPacket : IPacket +public sealed record SetPlayerPositionPacket(double X, double Y, double Z, bool IsOnGround) : IPacket { - public SetPlayerPositionPacket(double x, double y, double z, bool isOnGround) - { - X = x; - Y = y; - Z = z; - IsOnGround = isOnGround; - } - - public double X { get; set; } - public double Y { get; set; } - public double Z { get; set; } - public bool IsOnGround { get; set; } - public PacketType Type => PacketType.SB_Play_Position; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_Position; public void Write(PacketBuffer buffer, MinecraftData version) { diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetPlayerRotationPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetPlayerRotationPacket.cs new file mode 100644 index 00000000..87991ea0 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetPlayerRotationPacket.cs @@ -0,0 +1,37 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Updates the direction the player is looking in. +/// +/// Absolute rotation on the X Axis, in degrees. +/// Absolute rotation on the Y Axis, in degrees. +/// True if the client is on the ground, false otherwise. +public sealed record SetPlayerRotationPacket(float Yaw, float Pitch, bool OnGround) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_Look; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteFloat(Yaw); + buffer.WriteFloat(Pitch); + buffer.WriteBool(OnGround); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var yaw = buffer.ReadFloat(); + var pitch = buffer.ReadFloat(); + var onGround = buffer.ReadBool(); + + return new SetPlayerRotationPacket(yaw, pitch, onGround); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetSeenRecipePacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetSeenRecipePacket.cs new file mode 100644 index 00000000..2ef33893 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/SetSeenRecipePacket.cs @@ -0,0 +1,32 @@ +using MineSharp.Core.Common; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Packet sent by the client when a recipe is first seen in the recipe book. +/// +/// The ID of the recipe. +public sealed record SetSeenRecipePacket(Identifier RecipeId) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_DisplayedRecipe; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteIdentifier(RecipeId); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var recipeId = buffer.ReadIdentifier(); + + return new SetSeenRecipePacket(recipeId); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/SwingArmPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/SwingArmPacket.cs index 70f2b462..fa73ba93 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Play/SwingArmPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/SwingArmPacket.cs @@ -1,28 +1,32 @@ using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Serverbound.Play; -#pragma warning disable CS1591 -public class SwingArmPacket : IPacket -{ - public SwingArmPacket(PlayerHand hand) - { - Hand = hand; - } - public PlayerHand Hand { get; set; } - public PacketType Type => PacketType.SB_Play_ArmAnimation; +/// +/// Sent by the client when the player swings their arm. +/// +/// The hand used by the player. +public sealed record SwingArmPacket(PlayerHand Hand) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_ArmAnimation; + /// public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteVarInt((int)Hand); } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { - return new SwingArmPacket( - (PlayerHand)buffer.ReadVarInt()); + var hand = (PlayerHand)buffer.ReadVarInt(); + + return new SwingArmPacket(hand); } } -#pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/TeleportToEntityPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/TeleportToEntityPacket.cs new file mode 100644 index 00000000..e990c222 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/TeleportToEntityPacket.cs @@ -0,0 +1,37 @@ +using MineSharp.Core.Common; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Teleports the player to the given entity. The player must be in spectator mode. +/// +/// The Notchian client only uses this to teleport to players, but it appears to accept any type of entity. +/// The entity does not need to be in the same dimension as the player; if necessary, the player will be respawned in the right world. +/// If the given entity cannot be found (or isn't loaded), this packet will be ignored. +/// It will also be ignored if the player attempts to teleport to themselves. +/// +/// UUID of the player to teleport to (can also be an entity UUID). +public sealed record TeleportToEntityPacket(Uuid TargetPlayer) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_Spectate; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteUuid(TargetPlayer); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var targetPlayer = buffer.ReadUuid(); + + return new TeleportToEntityPacket(targetPlayer); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/UpdateCommandBlock.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/UpdateCommandBlock.cs deleted file mode 100644 index 2b7ec4a6..00000000 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Play/UpdateCommandBlock.cs +++ /dev/null @@ -1,42 +0,0 @@ -using MineSharp.Core.Common; -using MineSharp.Core.Geometry; -using MineSharp.Data; -using MineSharp.Data.Protocol; - -namespace MineSharp.Protocol.Packets.Serverbound.Play; -#pragma warning disable CS1591 -public class UpdateCommandBlock : IPacket -{ - public UpdateCommandBlock(Position location, string command, int mode, byte flags) - { - Location = location; - Command = command; - Mode = mode; - Flags = flags; - } - - public Position Location { get; set; } - public string Command { get; set; } - public int Mode { get; set; } - public byte Flags { get; set; } - public PacketType Type => PacketType.SB_Play_UpdateCommandBlock; - - public void Write(PacketBuffer buffer, MinecraftData version) - { - buffer.WriteULong(Location.ToULong()); - buffer.WriteString(Command); - buffer.WriteVarInt(Mode); - buffer.WriteByte(Flags); - } - - public static IPacket Read(PacketBuffer buffer, MinecraftData version) - { - return new UpdateCommandBlock( - new(buffer.ReadULong()), - buffer.ReadString(), - buffer.ReadVarInt(), - buffer.ReadByte()); - } -} - -#pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/UpdateCommandBlockMinecartPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/UpdateCommandBlockMinecartPacket.cs new file mode 100644 index 00000000..34086868 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/UpdateCommandBlockMinecartPacket.cs @@ -0,0 +1,40 @@ +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Program Command Block Minecart packet +/// +/// The entity ID +/// The command to be executed +/// Whether to track the output of the command +public sealed record UpdateCommandBlockMinecartPacket(int EntityId, string Command, bool TrackOutput) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_UpdateCommandBlockMinecart; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WriteVarInt(EntityId); + buffer.WriteString(Command); + buffer.WriteBool(TrackOutput); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var entityId = buffer.ReadVarInt(); + var command = buffer.ReadString(); + var trackOutput = buffer.ReadBool(); + + return new UpdateCommandBlockMinecartPacket( + entityId, + command, + trackOutput); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/UpdateSignPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/UpdateSignPacket.cs new file mode 100644 index 00000000..d8bfa710 --- /dev/null +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/UpdateSignPacket.cs @@ -0,0 +1,50 @@ +using MineSharp.Core.Geometry; +using MineSharp.Core.Serialization; +using MineSharp.Data; +using MineSharp.Data.Protocol; + +namespace MineSharp.Protocol.Packets.Serverbound.Play; + +/// +/// Update Sign packet sent from the client to the server when the "Done" button is pushed after placing a sign. +/// +/// Block Coordinates +/// Whether the updated text is in front or on the back of the sign +/// First line of text in the sign +/// Second line of text in the sign +/// Third line of text in the sign +/// Fourth line of text in the sign +public sealed record UpdateSignPacket(Position Location, bool IsFrontText, string Line1, string Line2, string Line3, string Line4) : IPacket +{ + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_UpdateSign; + + /// + public void Write(PacketBuffer buffer, MinecraftData version) + { + buffer.WritePosition(Location); + buffer.WriteBool(IsFrontText); + buffer.WriteString(Line1); + buffer.WriteString(Line2); + buffer.WriteString(Line3); + buffer.WriteString(Line4); + } + + /// + public static IPacket Read(PacketBuffer buffer, MinecraftData version) + { + var location = buffer.ReadPosition(); + var isFrontText = buffer.ReadBool(); + var line1 = buffer.ReadString(); + var line2 = buffer.ReadString(); + var line3 = buffer.ReadString(); + var line4 = buffer.ReadString(); + + return new UpdateSignPacket( + location, + isFrontText, + line1, line2, line3, line4); + } +} diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/UseItemPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/UseItemPacket.cs index be6b593b..5df34ff8 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Play/UseItemPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/UseItemPacket.cs @@ -1,5 +1,6 @@ using MineSharp.Core; using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; @@ -8,40 +9,17 @@ namespace MineSharp.Protocol.Packets.Serverbound.Play; /// /// Packet to use an item /// -public class UseItemPacket : IPacket +/// The Hand used +/// +/// Sequence id used to synchronize server and client. +/// Only used for versions >= 1.19 +/// +public sealed record UseItemPacket(PlayerHand Hand, int? SequenceId = null) : IPacket { - /// - /// Constructor for 1.18-1.18.2 - /// - /// - public UseItemPacket(PlayerHand hand) - { - Hand = hand; - } - - /// - /// Constructor for >= 1.19 - /// - /// - /// - public UseItemPacket(PlayerHand hand, int sequenceId) : this(hand) - { - SequenceId = sequenceId; - } - - /// - /// The Hand used - /// - public PlayerHand Hand { get; set; } - - /// - /// Sequence id used to synchronize server and client. - /// Only used for versions >= 1.19 - /// - public int? SequenceId { get; set; } - /// - public PacketType Type => PacketType.SB_Play_UseItem; + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_UseItem; /// public void Write(PacketBuffer buffer, MinecraftData version) diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Play/WindowClickPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Play/WindowClickPacket.cs index 173fbf72..4563aa42 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Play/WindowClickPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Play/WindowClickPacket.cs @@ -1,34 +1,26 @@ using MineSharp.Core.Common; using MineSharp.Core.Common.Items; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; using MineSharp.Protocol.Packets.NetworkTypes; namespace MineSharp.Protocol.Packets.Serverbound.Play; #pragma warning disable CS1591 -public class WindowClickPacket : IPacket +public sealed record WindowClickPacket( + byte WindowId, + int StateId, + short Slot, + sbyte MouseButton, + int Mode, + Slot[] ChangedSlots, + Item? SelectedItem +) : IPacket { - public WindowClickPacket(byte windowId, int stateId, short slot, sbyte mouseButton, int mode, Slot[] changedSlots, - Item? selectedItem) - { - WindowId = windowId; - StateId = stateId; - Slot = slot; - MouseButton = mouseButton; - Mode = mode; - ChangedSlots = changedSlots; - SelectedItem = selectedItem; - } - - public byte WindowId { get; set; } - public int StateId { get; set; } - public short Slot { get; set; } - public sbyte MouseButton { get; set; } - public int Mode { get; set; } - public Slot[] ChangedSlots { get; set; } - public Item? SelectedItem { get; set; } - public PacketType Type => PacketType.SB_Play_WindowClick; - + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Play_WindowClick; public void Write(PacketBuffer buffer, MinecraftData version) { @@ -50,7 +42,8 @@ public static IPacket Read(PacketBuffer buffer, MinecraftData version) buffer.ReadSByte(), buffer.ReadVarInt(), buffer.ReadVarIntArray(buff => buff.ReadSlot(version)), - buffer.ReadOptionalItem(version)); + buffer.ReadOptionalItem(version) + ); } } #pragma warning restore CS1591 diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Status/PingRequestPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Status/PingRequestPacket.cs index f532dd09..711b8af7 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Status/PingRequestPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Status/PingRequestPacket.cs @@ -1,24 +1,27 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Serverbound.Status; #pragma warning disable CS1591 -public class PingRequestPacket : IPacket +/// +/// Packet for ping request +/// +/// The payload of the ping request +public sealed record PingRequestPacket(long Payload) : IPacket { - public PingRequestPacket(long payload) - { - Payload = payload; - } - - public long Payload { get; set; } - public PacketType Type => PacketType.SB_Status_Ping; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Status_Ping; + /// public void Write(PacketBuffer buffer, MinecraftData version) { buffer.WriteLong(Payload); } + /// public static IPacket Read(PacketBuffer buffer, MinecraftData version) { return new PingRequestPacket(buffer.ReadLong()); diff --git a/Components/MineSharp.Protocol/Packets/Serverbound/Status/StatusRequestPacket.cs b/Components/MineSharp.Protocol/Packets/Serverbound/Status/StatusRequestPacket.cs index a7a4cae6..e8b87837 100644 --- a/Components/MineSharp.Protocol/Packets/Serverbound/Status/StatusRequestPacket.cs +++ b/Components/MineSharp.Protocol/Packets/Serverbound/Status/StatusRequestPacket.cs @@ -1,12 +1,15 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.Data.Protocol; namespace MineSharp.Protocol.Packets.Serverbound.Status; #pragma warning disable CS1591 -public class StatusRequestPacket : IPacket +public sealed record StatusRequestPacket : IPacket { - public PacketType Type => PacketType.SB_Status_PingStart; + /// + public PacketType Type => StaticType; + /// + public static PacketType StaticType => PacketType.SB_Status_PingStart; public void Write(PacketBuffer buffer, MinecraftData version) { } diff --git a/Components/MineSharp.Protocol/Registrations/AbstractDisposableRegistration.cs b/Components/MineSharp.Protocol/Registrations/AbstractDisposableRegistration.cs new file mode 100644 index 00000000..f6f5f946 --- /dev/null +++ b/Components/MineSharp.Protocol/Registrations/AbstractDisposableRegistration.cs @@ -0,0 +1,40 @@ +namespace MineSharp.Protocol.Registrations; + +/// +/// Abstract class a disposable registration. +/// +public abstract class AbstractDisposableRegistration : IDisposable +{ + private int disposedValue; + /// + /// Whether this registration is disposed and the handler is unregistered. + /// + public bool Disposed => disposedValue != 0; + + /// + /// Initializes a new instance of the class. + /// + protected AbstractDisposableRegistration() + { + disposedValue = 0; + } + + /// + /// This method unregisters the the registered object. + /// Must be overridden by subclasses. + /// + /// This method is called when the registration is disposed. + /// + protected abstract void Unregister(); + + /// + public void Dispose() + { + if (Interlocked.Exchange(ref disposedValue, 1) != 0) + { + // Already disposed + return; + } + Unregister(); + } +} diff --git a/Components/MineSharp.Protocol/Registrations/AbstractPacketReceiveRegistration.cs b/Components/MineSharp.Protocol/Registrations/AbstractPacketReceiveRegistration.cs new file mode 100644 index 00000000..4975549e --- /dev/null +++ b/Components/MineSharp.Protocol/Registrations/AbstractPacketReceiveRegistration.cs @@ -0,0 +1,18 @@ +using static MineSharp.Protocol.MinecraftClient; + +namespace MineSharp.Protocol.Registrations; + +/// +/// Abstract class for packet receive registration. +/// +public abstract class AbstractPacketReceiveRegistration : AbstractDisposableRegistration +{ + protected readonly MinecraftClient Client; + protected readonly AsyncPacketHandler Handler; + + protected AbstractPacketReceiveRegistration(MinecraftClient client, AsyncPacketHandler handler) + { + Client = client; + Handler = handler; + } +} diff --git a/Components/MineSharp.Windows/MineSharp.Windows.csproj b/Components/MineSharp.Windows/MineSharp.Windows.csproj index 9a835226..80443c9f 100644 --- a/Components/MineSharp.Windows/MineSharp.Windows.csproj +++ b/Components/MineSharp.Windows/MineSharp.Windows.csproj @@ -3,7 +3,7 @@ enable enable - net7.0;net8.0 + net8.0 12 true @@ -20,11 +20,12 @@ - + + - + - + True diff --git a/Components/MineSharp.Windows/Window.cs b/Components/MineSharp.Windows/Window.cs index 61fce142..707b6d46 100644 --- a/Components/MineSharp.Windows/Window.cs +++ b/Components/MineSharp.Windows/Window.cs @@ -1,6 +1,7 @@ using MineSharp.Core.Common; using MineSharp.Core.Common.Items; using MineSharp.Core.Events; +using MineSharp.Data.Windows; using MineSharp.Windows.Clicks; using NLog; @@ -202,6 +203,18 @@ public Slot[] GetInventorySlots() .ToArray(); } + /// + /// Returns only the hotbar slots of the inventory part of this window. + /// Only works for player inventories + /// + /// + public Slot[] GetHotbarSlots() + { + return GetInventorySlots() + .Where(x => x.SlotIndex >= (short)PlayerWindowSlots.HotbarStart && x.SlotIndex <= (short)PlayerWindowSlots.HotbarEnd) + .ToArray(); + } + /// /// Returns all slots in this window /// diff --git a/Components/MineSharp.World/AbstractWorld.cs b/Components/MineSharp.World/AbstractWorld.cs index ef15af0b..3c47db7a 100644 --- a/Components/MineSharp.World/AbstractWorld.cs +++ b/Components/MineSharp.World/AbstractWorld.cs @@ -17,7 +17,8 @@ namespace MineSharp.World; /// Base class for other implementations of the IWorld interface /// /// -public abstract class AbstractWorld(MinecraftData data) : IWorld +/// +public abstract class AbstractWorld(MinecraftData data, DimensionInfo dimensionInfo) : IWorld, IAsyncWorld { private static readonly ILogger Logger = LogManager.GetCurrentClassLogger(); @@ -26,6 +27,11 @@ public abstract class AbstractWorld(MinecraftData data) : IWorld /// public readonly MinecraftData Data = data; + /// + /// The Dimension that this world represents + /// + public readonly DimensionInfo DimensionInfo = dimensionInfo; + private readonly BlockInfo outOfMapBlock = data.Blocks.ByType(BlockType.Air)!; /// @@ -33,11 +39,21 @@ public abstract class AbstractWorld(MinecraftData data) : IWorld /// protected ConcurrentDictionary Chunks = new(); + /// + /// A dictionary that holds for chunks that are yet to be loaded + /// but are requested by some async method in this class. + /// + /// + /// Implementation Note: This could also be done using the event, but this way + /// has less overhead and is more robust. Since the user might remove the event listener. + /// + protected ConcurrentDictionary> ChunkLoadAwaiters = new(); + /// - public abstract int MaxY { get; } + public int MaxY => DimensionInfo.WorldMaxY; /// - public abstract int MinY { get; } + public int MinY => DimensionInfo.WorldMinY; /// @@ -80,9 +96,10 @@ public Position ToWorldPosition(ChunkCoordinates coordinates, Position position) } /// + [Pure] public IChunk GetChunkAt(ChunkCoordinates coordinates) { - if (!Chunks.TryGetValue(coordinates, out var chunk)) + if (!TryGetChunkAt(coordinates, out var chunk)) { throw new ChunkNotLoadedException($"The chunk at {coordinates} is not loaded."); } @@ -90,24 +107,43 @@ public IChunk GetChunkAt(ChunkCoordinates coordinates) return chunk; } + /// + [Pure] + public bool TryGetChunkAt(ChunkCoordinates coordinates, [NotNullWhen(true)] out IChunk? chunk) + { + return Chunks.TryGetValue(coordinates, out chunk); + } + /// [Pure] public bool IsChunkLoaded(ChunkCoordinates coordinates) { - return IsChunkLoaded(coordinates, out _); + return Chunks.ContainsKey(coordinates); } /// public void LoadChunk(IChunk chunk) { - if (IsChunkLoaded(chunk.Coordinates, out var oldChunk)) + IChunk? oldChunk = null; + // to be thread-safe, we need to use AddOrUpdate + if (Chunks.AddOrUpdate(chunk.Coordinates, chunk, (key, oldValue) => + { + oldChunk = oldValue; + return chunk; + }) != chunk) + { + throw new Exception($"Failed to update chunk at {chunk.Coordinates}. This should never happen."); + } + + if (oldChunk != null) { oldChunk.OnBlockUpdated -= OnChunkBlockUpdate; - Chunks[chunk.Coordinates] = chunk; } - else + + // we complete the chunk load awaiter before firing the events + if (ChunkLoadAwaiters.TryRemove(chunk.Coordinates, out var tcs)) { - Chunks.TryAdd(chunk.Coordinates, chunk); + tcs.SetResult(chunk); } chunk.OnBlockUpdated += OnChunkBlockUpdate; @@ -132,18 +168,41 @@ public void UnloadChunk(ChunkCoordinates coordinates) /// public abstract bool IsOutOfMap(Position position); + /// + /// Get a block that represents the out of map block at the given position. + /// If the position is not out of map, this method returns null. + /// + /// + /// + private Block? GetOutOfMapBlock(Position position) + { + if (IsOutOfMap(position)) + { + return new(outOfMapBlock, outOfMapBlock.DefaultState, position); + } + return null; + } + /// public bool IsBlockLoaded(Position position, [NotNullWhen(true)] out IChunk? chunk) { return Chunks.TryGetValue(ToChunkCoordinates(position), out chunk); } + private Block GetBlockFromChunk(Position position, IChunk chunk) + { + var relative = ToChunkPosition(position); + var blockState = chunk.GetBlockAt(relative); + return new Block(Data.Blocks.ByState(blockState)!, blockState, position); + } + /// public Block GetBlockAt(Position position) { - if (IsOutOfMap(position)) + var block = GetOutOfMapBlock(position); + if (block != null) { - return new(outOfMapBlock, outOfMapBlock.DefaultState, position); + return block; } if (!IsBlockLoaded(position, out var chunk)) @@ -151,18 +210,17 @@ public Block GetBlockAt(Position position) throw new ChunkNotLoadedException($"Block at {position} is not loaded."); } - var relative = ToChunkPosition(position); - var blockState = chunk.GetBlockAt(relative); - var block = new Block( - Data.Blocks.ByState(blockState)!, - blockState, - position); - return block; + return GetBlockFromChunk(position, chunk); } /// public void SetBlock(Block block) { + if (IsOutOfMap(block.Position)) + { + throw new InvalidOperationException("Cannot set block at out of map position."); + } + if (!IsBlockLoaded(block.Position, out var chunk)) { throw new ChunkNotLoadedException($"Block at {block.Position} is not loaded."); @@ -175,6 +233,11 @@ public void SetBlock(Block block) /// public Biome GetBiomeAt(Position position) { + if (IsOutOfMap(position)) + { + throw new InvalidOperationException("Cannot get biome at out of map position."); + } + if (!IsBlockLoaded(position, out var chunk)) { throw new ChunkNotLoadedException($"Position {position} is not loaded."); @@ -222,25 +285,6 @@ public IEnumerable FindBlocks(BlockType type, int? maxCount = null) } } - /// - /// Mutate to a world position - /// - /// - /// - protected void MutateToWorldPosition(ChunkCoordinates coordinates, MutablePosition position) - { - var dx = coordinates.X * IChunk.Size; - var dz = coordinates.Z * IChunk.Size; - position.Set(position.X + dx, position.Y, position.Z + dz); - } - - /// - [Pure] - protected bool IsChunkLoaded(ChunkCoordinates coordinates, [NotNullWhen(true)] out IChunk? chunk) - { - return Chunks.TryGetValue(coordinates, out chunk); - } - private void OnChunkBlockUpdate(IChunk chunk, int state, Position position) { var worldPosition = ToWorldPosition(chunk.Coordinates, position); @@ -258,4 +302,116 @@ private int NonNegativeMod(int x, int m) return v; } + + #region IAsyncWorld + + private Task RegisterChunkAwaiter(ChunkCoordinates coordinates) + { + var tcs = ChunkLoadAwaiters.GetOrAdd(coordinates, _ => new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously)); + // because there is a small chance that the chunk was loaded before we were able to add the awaiter + // we need to check again if the chunk is loaded + if (TryGetChunkAt(coordinates, out var chunk)) + { + tcs.SetResult(chunk); + } + return tcs.Task; + } + + /// + public Task GetChunkAtAsync(ChunkCoordinates coordinates) + { + if (Chunks.TryGetValue(coordinates, out var chunk)) + { + return Task.FromResult(chunk); + } + + // If the chunk is not loaded, we need to wait for it to be loaded + return RegisterChunkAwaiter(coordinates); + } + + private Task GetChunkForBlockPosAsync(Position position) + { + return GetChunkAtAsync(ToChunkCoordinates(position)); + } + + /// + public Task GetBlockAtAsync(Position position) + { + var block = GetOutOfMapBlock(position); + if (block != null) + { + return Task.FromResult(block); + } + + var blockTask = GetChunkForBlockPosAsync(position) + .ContinueWith(chunkTask => + { + var chunk = chunkTask.Result; + return GetBlockFromChunk(position, chunk); + }, TaskContinuationOptions.OnlyOnRanToCompletion); + return blockTask; + } + + /// + public Task SetBlockAsync(Block block) + { + if (IsOutOfMap(block.Position)) + { + throw new InvalidOperationException("Cannot set block at out of map position."); + } + + var blockTask = GetChunkForBlockPosAsync(block.Position) + .ContinueWith(chunkTask => + { + var chunk = chunkTask.Result; + var relative = ToChunkPosition(block.Position); + chunk.SetBlockAt(block.State, relative); + }, TaskContinuationOptions.OnlyOnRanToCompletion); + return blockTask; + } + + /// + public Task GetBiomeAtAsync(Position position) + { + if (IsOutOfMap(position)) + { + throw new InvalidOperationException("Cannot get biome at out of map position."); + } + + var biomeTask = GetChunkForBlockPosAsync(position) + .ContinueWith(chunkTask => + { + var chunk = chunkTask.Result; + var relative = ToChunkPosition(position); + return chunk.GetBiomeAt(relative); + }, TaskContinuationOptions.OnlyOnRanToCompletion); + return biomeTask; + } + + /// + public Task SetBiomeAtAsync(Position position, Biome biome) + { + if (IsOutOfMap(position)) + { + throw new InvalidOperationException("Cannot set biome at out of map position."); + } + + var biomeTask = GetChunkForBlockPosAsync(position) + .ContinueWith(chunkTask => + { + var chunk = chunkTask.Result; + var relative = ToChunkPosition(position); + chunk.SetBiomeAt(relative, biome); + }, TaskContinuationOptions.OnlyOnRanToCompletion); + return biomeTask; + } + + /// + public IAsyncEnumerable FindBlocksAsync(BlockType type, IWorldIterator iterator, int? maxCount = null) + { + // can be implemented once FindBlocks is implemented + throw new NotImplementedException(); + } + + #endregion } diff --git a/Components/MineSharp.World/Chunks/IChunkSection.cs b/Components/MineSharp.World/Chunks/IChunkSection.cs index f03bb443..d877d9df 100644 --- a/Components/MineSharp.World/Chunks/IChunkSection.cs +++ b/Components/MineSharp.World/Chunks/IChunkSection.cs @@ -13,7 +13,7 @@ public interface IChunkSection /// /// The number of solid blocks in this chunk section. /// - public short SolidBlockCount { get; protected set; } + public short SolidBlockCount { get; } /// /// Returns the Block at the given position. diff --git a/Components/MineSharp.World/Containers/BiomeContainer.cs b/Components/MineSharp.World/Containers/BiomeContainer.cs index efcbf041..3fc9a36c 100644 --- a/Components/MineSharp.World/Containers/BiomeContainer.cs +++ b/Components/MineSharp.World/Containers/BiomeContainer.cs @@ -1,4 +1,4 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.World.Containers.Palettes; diff --git a/Components/MineSharp.World/Containers/BlockContainer.cs b/Components/MineSharp.World/Containers/BlockContainer.cs index e3257963..2e10432d 100644 --- a/Components/MineSharp.World/Containers/BlockContainer.cs +++ b/Components/MineSharp.World/Containers/BlockContainer.cs @@ -1,4 +1,4 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.World.Containers.Palettes; diff --git a/Components/MineSharp.World/Containers/PaletteContainer.cs b/Components/MineSharp.World/Containers/PaletteContainer.cs index ebc8af44..2084ccbf 100644 --- a/Components/MineSharp.World/Containers/PaletteContainer.cs +++ b/Components/MineSharp.World/Containers/PaletteContainer.cs @@ -1,4 +1,4 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.World.Containers.Palettes; namespace MineSharp.World.Containers; @@ -71,11 +71,7 @@ protected static (IPalette palette, IntBitArray data) FromStream(PacketBuffer bu palette = DirectPalette.FromStream(buffer); } - var data = new long[buffer.ReadVarInt()]; - for (var i = 0; i < data.Length; i++) - { - data[i] = buffer.ReadLong(); - } + var data = buffer.ReadLongArray(); return (palette, new(data, bitsPerEntry)); } diff --git a/Components/MineSharp.World/Containers/Palettes/DirectPalette.cs b/Components/MineSharp.World/Containers/Palettes/DirectPalette.cs index 3fb747e3..c3458e12 100644 --- a/Components/MineSharp.World/Containers/Palettes/DirectPalette.cs +++ b/Components/MineSharp.World/Containers/Palettes/DirectPalette.cs @@ -1,4 +1,4 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; namespace MineSharp.World.Containers.Palettes; diff --git a/Components/MineSharp.World/Containers/Palettes/IndirectPalette.cs b/Components/MineSharp.World/Containers/Palettes/IndirectPalette.cs index 53600f33..bee4b0bb 100644 --- a/Components/MineSharp.World/Containers/Palettes/IndirectPalette.cs +++ b/Components/MineSharp.World/Containers/Palettes/IndirectPalette.cs @@ -1,4 +1,4 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; namespace MineSharp.World.Containers.Palettes; diff --git a/Components/MineSharp.World/Containers/Palettes/SingleValuePalette.cs b/Components/MineSharp.World/Containers/Palettes/SingleValuePalette.cs index 9ca03dba..31f496c7 100644 --- a/Components/MineSharp.World/Containers/Palettes/SingleValuePalette.cs +++ b/Components/MineSharp.World/Containers/Palettes/SingleValuePalette.cs @@ -1,4 +1,4 @@ -using MineSharp.Core.Common; +using MineSharp.Core.Serialization; namespace MineSharp.World.Containers.Palettes; @@ -32,8 +32,9 @@ public int Get(int index) var count = (int)Math.Ceiling(container.Capacity / (64.0F / container.MinBits)); var lData = new long[count]; - container.Data = new(lData, container.MaxBits); - container.Data.Set(index, 1); + var newData = new IntBitArray(lData, container.MinBits); + newData.Set(index, 1); + container.Data = newData; return new IndirectPalette(map); } diff --git a/Components/MineSharp.World/DimensionInfo.cs b/Components/MineSharp.World/DimensionInfo.cs new file mode 100644 index 00000000..948c062f --- /dev/null +++ b/Components/MineSharp.World/DimensionInfo.cs @@ -0,0 +1,49 @@ +using MineSharp.Core.Common; +using MineSharp.Data; + +namespace MineSharp.World; + +/// +/// Specifies the Dimension. +/// +public record DimensionInfo(Dimension Dimension, int WorldMinY, int WorldMaxY) +{ + public static readonly MinecraftVersion MinecraftVersionMajor118 = new("1.18", -1); + + // TODO: Select Version Specific Dimension Info + // since we currently only support 1.18 and above this is fine + + public static readonly DimensionInfo Overworld = new DimensionInfo(Dimension.Overworld, -64, 320); + public static readonly DimensionInfo Nether = new DimensionInfo(Dimension.Nether, 0, 256); + public static readonly DimensionInfo End = new DimensionInfo(Dimension.End, 0, 256); + + /// + /// Gets the height of the world. + /// + /// + /// The height of the world, calculated as the difference between WorldMaxY and WorldMinY. + /// + public int GetWorldHeight() + { + return WorldMaxY - WorldMinY; + } + + /// + /// Gets the DimensionInfo for the specified Dimension. + /// + /// The dimension to get the DimensionInfo for. + /// The DimensionInfo for the specified dimension. + /// + /// Thrown when the specified dimension is not recognized. + /// + public static DimensionInfo FromDimension(Dimension dimension) + { + return dimension switch + { + Dimension.Overworld => Overworld, + Dimension.Nether => Nether, + Dimension.End => End, + _ => throw new ArgumentOutOfRangeException(nameof(dimension), dimension, null) + }; + } +} diff --git a/Components/MineSharp.World/IAsyncWorld.cs b/Components/MineSharp.World/IAsyncWorld.cs new file mode 100644 index 00000000..4fed4174 --- /dev/null +++ b/Components/MineSharp.World/IAsyncWorld.cs @@ -0,0 +1,78 @@ +using MineSharp.Core.Common.Biomes; +using MineSharp.Core.Common.Blocks; +using MineSharp.Core.Geometry; +using MineSharp.World.Chunks; +using MineSharp.World.Iterators; + +namespace MineSharp.World; + +/// +/// Interface for creating a Minecraft world +/// +public interface IAsyncWorld : IWorld +{ + /// + /// Return the chunk at the given chunk position. + /// Waits for the chunk to be loaded if it's not loaded yet. + /// + /// + /// + public Task GetChunkAtAsync(ChunkCoordinates coordinates); + + /// + /// Return the block at the given position. + /// Waits for the chunk the block is in to be loaded if it's not loaded yet. + /// + /// + /// + public Task GetBlockAtAsync(Position position); + + /// + /// Set the block at the given position. + /// Waits for the chunk the block is in to be loaded if it's not loaded yet. + /// + /// + public Task SetBlockAsync(Block block); + + /// + /// Get the biome at the given position. + /// Waits for the chunk the block is in to be loaded if it's not loaded yet. + /// + /// + /// + public Task GetBiomeAtAsync(Position position); + + /// + /// Set the biome at the given position. + /// Waits for the chunk the block is in to be loaded if it's not loaded yet. + /// + /// + /// + public Task SetBiomeAtAsync(Position position, Biome biome); + + /// + /// Query the world for a block type. + /// Waits for the chunks the block are in to be loaded if they are not loaded yet. + /// + /// + /// + /// + /// + public IAsyncEnumerable FindBlocksAsync(BlockType type, IWorldIterator iterator, int? maxCount = null); + + /// + /// Find a block of type . + /// Waits for the chunks the block are in to be loaded if they are not loaded yet. + /// + /// + /// + /// + public async Task FindBlockAsync(BlockType type, IWorldIterator iterator) + { + await foreach (var block in FindBlocksAsync(type, iterator, 1)) + { + return block; + } + return null; + } +} diff --git a/Components/MineSharp.World/IWorld.cs b/Components/MineSharp.World/IWorld.cs index 2dc98a84..e58296e6 100644 --- a/Components/MineSharp.World/IWorld.cs +++ b/Components/MineSharp.World/IWorld.cs @@ -72,6 +72,16 @@ public interface IWorld /// public IChunk GetChunkAt(ChunkCoordinates coordinates); + /// + /// Try to get the chunk at the given chunk coordinates. + /// This method does the same as but does not throw an exception. + /// Instead it returns a boolean indicating the success of the operation. + /// + /// + /// + /// + public bool TryGetChunkAt(ChunkCoordinates coordinates, [NotNullWhen(true)] out IChunk? chunk); + /// /// Whether the chunk at the given coordinates is loaded /// diff --git a/Components/MineSharp.World/MineSharp.World.csproj b/Components/MineSharp.World/MineSharp.World.csproj index a182f96b..7ee29477 100644 --- a/Components/MineSharp.World/MineSharp.World.csproj +++ b/Components/MineSharp.World/MineSharp.World.csproj @@ -3,7 +3,7 @@ enable enable - net7.0;net8.0 + net8.0 12 true README.md diff --git a/Components/MineSharp.World/V1_18/ChunkSection_1_18.cs b/Components/MineSharp.World/V1_18/ChunkSection_1_18.cs index 2c243a9e..19763c8c 100644 --- a/Components/MineSharp.World/V1_18/ChunkSection_1_18.cs +++ b/Components/MineSharp.World/V1_18/ChunkSection_1_18.cs @@ -1,7 +1,7 @@ -using MineSharp.Core.Common; -using MineSharp.Core.Common.Biomes; +using MineSharp.Core.Common.Biomes; using MineSharp.Core.Common.Blocks; using MineSharp.Core.Geometry; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.World.Chunks; using MineSharp.World.Containers; @@ -19,12 +19,13 @@ internal class ChunkSection118 : IChunkSection public ChunkSection118(MinecraftData data, short blockCount, BlockContainer blocks, BiomeContainer biomes) { this.data = data; - SolidBlockCount = blockCount; + solidBlockCount = blockCount; blockContainer = blocks; biomeContainer = biomes; } - public short SolidBlockCount { get; set; } + private int solidBlockCount; + public short SolidBlockCount => (short)solidBlockCount; public int GetBlockAt(Position position) { @@ -44,11 +45,11 @@ public void SetBlockAt(int state, Position position) { if (isSolid) { - SolidBlockCount++; + Interlocked.Increment(ref solidBlockCount); } else { - SolidBlockCount--; + Interlocked.Decrement(ref solidBlockCount); } } diff --git a/Components/MineSharp.World/V1_18/Chunk_1_18.cs b/Components/MineSharp.World/V1_18/Chunk_1_18.cs index af1c3deb..02a32427 100644 --- a/Components/MineSharp.World/V1_18/Chunk_1_18.cs +++ b/Components/MineSharp.World/V1_18/Chunk_1_18.cs @@ -1,8 +1,8 @@ -using MineSharp.Core.Common; -using MineSharp.Core.Common.Biomes; +using MineSharp.Core.Common.Biomes; using MineSharp.Core.Common.Blocks; using MineSharp.Core.Events; using MineSharp.Core.Geometry; +using MineSharp.Core.Serialization; using MineSharp.Data; using MineSharp.World.Chunks; using MineSharp.World.Exceptions; @@ -14,24 +14,28 @@ namespace MineSharp.World.V1_18; /// public sealed class Chunk118 : IChunk { - private const int SectionCount = World118.WorldHeight / ChunkSection118.SectionSize; private readonly Dictionary blockEntities; private readonly MinecraftData data; + private readonly DimensionInfo dimensionInfo; private readonly IChunkSection[] sections; + private int CalculateSectionCount() => dimensionInfo.GetWorldHeight() / ChunkSection118.SectionSize; + /// /// Create a new instance /// /// + /// /// /// - public Chunk118(MinecraftData data, ChunkCoordinates coordinates, BlockEntity[] blockEntities) + public Chunk118(MinecraftData data, DimensionInfo dimensionInfo, ChunkCoordinates coordinates, BlockEntity[] blockEntities) { Coordinates = coordinates; this.data = data; + this.dimensionInfo = dimensionInfo; this.blockEntities = new(); - sections = new IChunkSection[SectionCount]; + sections = new IChunkSection[CalculateSectionCount()]; foreach (var entity in blockEntities) { @@ -49,7 +53,7 @@ public Chunk118(MinecraftData data, ChunkCoordinates coordinates, BlockEntity[] public void LoadData(byte[] buf) { var buffer = new PacketBuffer(buf, data.Version.Protocol); - for (var i = 0; i < SectionCount; i++) + for (var i = 0; i < sections.Length; i++) { sections[i] = ChunkSection118.FromStream(data, buffer); } @@ -107,7 +111,7 @@ public IEnumerable FindBlocks(BlockType type, int? maxCount = null) { block.Position = new( block.Position.X, - FromChunkSectionY(block.Position.Y, i), + FromChunkSectionY(block.Position.Y, i, dimensionInfo.WorldMinY), block.Position.Z); yield return block; @@ -121,7 +125,7 @@ public IEnumerable FindBlocks(BlockType type, int? maxCount = null) private (int y, IChunkSection section) GetChunkSectionAndNewYFromPosition(Position position) { - var sectionIndex = GetSectionIndex(position.Y); + var sectionIndex = GetSectionIndex(position.Y, dimensionInfo.WorldMinY); if (sectionIndex >= sections.Length) { throw new OutOfWorldException($"The Y coordinate {position.Y} is out of the world."); @@ -143,14 +147,14 @@ private int ToChunkSectionY(int y) return v; } - private static int FromChunkSectionY(int y, int sectionIndex) + private static int FromChunkSectionY(int y, int sectionIndex, int worldMinY) { - const int negative_section_count = -World118.MIN_Y / ChunkSection118.SectionSize; + int negative_section_count = -worldMinY / ChunkSection118.SectionSize; return ((sectionIndex - negative_section_count) * ChunkSection118.SectionSize) + y; } - private static int GetSectionIndex(int y) + private static int GetSectionIndex(int y, int worldMinY) { - return (y - World118.MIN_Y) / ChunkSection118.SectionSize; + return (y - worldMinY) / ChunkSection118.SectionSize; } } diff --git a/Components/MineSharp.World/V1_18/World_1_18.cs b/Components/MineSharp.World/V1_18/World_1_18.cs index 19b79cb4..bf869211 100644 --- a/Components/MineSharp.World/V1_18/World_1_18.cs +++ b/Components/MineSharp.World/V1_18/World_1_18.cs @@ -11,22 +11,14 @@ namespace MineSharp.World.V1_18; /// public class World118 : AbstractWorld { - internal const int WorldHeight = MAX_Y - MIN_Y; - internal const int MAX_Y = 320; - internal const int MIN_Y = -64; private static readonly ILogger Logger = LogManager.GetCurrentClassLogger(typeof(IWorld)); /// - public World118(MinecraftData data) : base(data) + public World118(MinecraftData data, DimensionInfo dimensionInfo) + : base(data, dimensionInfo) { } - /// - public override int MaxY => MAX_Y; - - /// - public override int MinY => MIN_Y; - /// public override bool IsOutOfMap(Position position) { @@ -51,6 +43,6 @@ public override bool IsOutOfMap(Position position) /// public override IChunk CreateChunk(ChunkCoordinates coordinates, BlockEntity[] entities) { - return new Chunk118(Data, coordinates, entities); + return new Chunk118(Data, DimensionInfo, coordinates, entities); } } diff --git a/Components/MineSharp.World/WorldVersion.cs b/Components/MineSharp.World/WorldVersion.cs index 5f30e0f4..d6392f29 100644 --- a/Components/MineSharp.World/WorldVersion.cs +++ b/Components/MineSharp.World/WorldVersion.cs @@ -1,4 +1,5 @@ -using MineSharp.Data; +using MineSharp.Core.Common; +using MineSharp.Data; using MineSharp.World.V1_18; namespace MineSharp.World; @@ -8,19 +9,18 @@ namespace MineSharp.World; /// public static class WorldVersion { - private static readonly MinecraftVersion Major118 = new("1.18", -1); - /// /// Create a new IWorld for the given version /// /// + /// /// /// - public static IWorld CreateWorld(MinecraftData version) + public static IAsyncWorld CreateWorld(MinecraftData version, Dimension dimension) { - if (version.Version >= Major118) + if (version.Version >= DimensionInfo.MinecraftVersionMajor118) { - return new World118(version); + return new World118(version, DimensionInfo.FromDimension(dimension)); } throw new NotSupportedException( diff --git a/Components/Tests/MineSharp.Windows.Tests/MineSharp.Windows.Tests.csproj b/Components/Tests/MineSharp.Windows.Tests/MineSharp.Windows.Tests.csproj index 3046408b..eb124473 100644 --- a/Components/Tests/MineSharp.Windows.Tests/MineSharp.Windows.Tests.csproj +++ b/Components/Tests/MineSharp.Windows.Tests/MineSharp.Windows.Tests.csproj @@ -6,7 +6,7 @@ false - net7.0;net8.0 + net8.0 12 diff --git a/Components/Tests/MineSharp.World.Tests/MineSharp.World.Tests.csproj b/Components/Tests/MineSharp.World.Tests/MineSharp.World.Tests.csproj index 38d4f5fe..e7da4cb5 100644 --- a/Components/Tests/MineSharp.World.Tests/MineSharp.World.Tests.csproj +++ b/Components/Tests/MineSharp.World.Tests/MineSharp.World.Tests.csproj @@ -6,7 +6,7 @@ false - net7.0;net8.0 + net8.0 12 diff --git a/Data/MineSharp.Data.Tests/MineSharp.Data.Tests.csproj b/Data/MineSharp.Data.Tests/MineSharp.Data.Tests.csproj index 56b183fd..29ef82e5 100644 --- a/Data/MineSharp.Data.Tests/MineSharp.Data.Tests.csproj +++ b/Data/MineSharp.Data.Tests/MineSharp.Data.Tests.csproj @@ -6,7 +6,7 @@ false true - net8.0;net7.0 + net8.0 diff --git a/Data/MineSharp.Data.Tests/TestVersions.cs b/Data/MineSharp.Data.Tests/TestVersions.cs index 7b6e0f3d..2f9d2f92 100644 --- a/Data/MineSharp.Data.Tests/TestVersions.cs +++ b/Data/MineSharp.Data.Tests/TestVersions.cs @@ -4,6 +4,7 @@ using MineSharp.Core.Common.Enchantments; using MineSharp.Core.Common.Entities; using MineSharp.Core.Common.Items; +using MineSharp.Core.Common.Particles; using MineSharp.Data.Protocol; namespace MineSharp.Data.Tests; @@ -46,6 +47,7 @@ public void TestLoadData() data.Materials.GetMultiplier(Material.Shovel, ItemType.StoneSword); data.Protocol.GetPacketId(PacketType.CB_Play_Login); data.Recipes.ByItem(ItemType.DiamondShovel); + data.Particles.GetProtocolId(ParticleType.Composter); data.Windows.ById(0); } } diff --git a/Data/MineSharp.Data/Blocks/BlockProvider.cs b/Data/MineSharp.Data/Blocks/BlockProvider.cs index bc60bb61..e963a69b 100644 --- a/Data/MineSharp.Data/Blocks/BlockProvider.cs +++ b/Data/MineSharp.Data/Blocks/BlockProvider.cs @@ -60,7 +60,7 @@ private static BlockInfo FromToken(JToken dataToken, IItemData items) return new( id, - BlockTypeLookup.FromName(NameUtils.GetBiomeName(name)), + BlockTypeLookup.FromName(NameUtils.GetBlockName(name)), name, displayName, hardness, diff --git a/Data/MineSharp.Data/Framework/DataInterfaces.cs b/Data/MineSharp.Data/Framework/DataInterfaces.cs index 8dd89647..dcbaeec1 100644 --- a/Data/MineSharp.Data/Framework/DataInterfaces.cs +++ b/Data/MineSharp.Data/Framework/DataInterfaces.cs @@ -1,9 +1,11 @@ -using MineSharp.Core.Common.Biomes; +using MineSharp.Core.Common; +using MineSharp.Core.Common.Biomes; using MineSharp.Core.Common.Blocks; using MineSharp.Core.Common.Effects; using MineSharp.Core.Common.Enchantments; using MineSharp.Core.Common.Entities; using MineSharp.Core.Common.Items; +using MineSharp.Core.Common.Particles; using MineSharp.Core.Common.Protocol; using MineSharp.Core.Common.Recipes; using MineSharp.Core.Geometry; @@ -182,5 +184,10 @@ public interface IWindowData /// /// /// - public WindowInfo ByName(string name); + public WindowInfo ByName(Identifier name); } + +/// +/// Interface for implementing particle data +/// +public interface IParticleData : INameAndProtocolNumberIndexedData; diff --git a/Data/MineSharp.Data/Framework/INameAndProtocolNumberIndexedData.cs b/Data/MineSharp.Data/Framework/INameAndProtocolNumberIndexedData.cs new file mode 100644 index 00000000..f73083bc --- /dev/null +++ b/Data/MineSharp.Data/Framework/INameAndProtocolNumberIndexedData.cs @@ -0,0 +1,48 @@ +using MineSharp.Core.Common; + +namespace MineSharp.Data.Framework; + +/// +/// Interface to implement indexed data, where a single entry does not +/// contain any other data besides a name and a corresponding protocol number. +/// +public interface INameAndProtocolNumberIndexedData +{ + /// + /// The number of data entries + /// + public int Count { get; } + + /// + /// Return the protocol number associated with the given identifier. + /// + /// + /// + public int GetProtocolId(Identifier name); + + /// + /// Return the associated with the given protocol number. + /// + /// + /// + public Identifier GetName(int id); +} + +/// +/// +public interface INameAndProtocolNumberIndexedData : INameAndProtocolNumberIndexedData +{ + /// + /// Return the protocol number associated with the given + /// + /// + /// + public int GetProtocolId(TEnum type); + + /// + /// Return associated with the given protocol number. + /// + /// + /// + public TEnum GetType(int id); +} diff --git a/Data/MineSharp.Data/Internal/NameAndProtocolNumberIndexedData.cs b/Data/MineSharp.Data/Internal/NameAndProtocolNumberIndexedData.cs new file mode 100644 index 00000000..06d40b76 --- /dev/null +++ b/Data/MineSharp.Data/Internal/NameAndProtocolNumberIndexedData.cs @@ -0,0 +1,82 @@ +using System.Collections.Frozen; +using MineSharp.Core.Common; +using MineSharp.Data.Framework; +using MineSharp.Data.Framework.Providers; + +namespace MineSharp.Data.Internal; + +internal class NameAndProtocolNumberIndexedData(IDataProvider> provider) + : IndexedData>(provider), INameAndProtocolNumberIndexedData +{ + public int Count { get; private set; } + + protected IReadOnlyDictionary? ProtocolNumberToName; + protected IReadOnlyDictionary? NameToProtocolNumber; + + protected override void InitializeData(IReadOnlyDictionary data) + { + Count = data.Count; + NameToProtocolNumber = data.ToFrozenDictionary(); + ProtocolNumberToName = NameToProtocolNumber.ToFrozenDictionary(x => x.Value, x => x.Key); + } + + public int GetProtocolId(Identifier name) + { + if (!Loaded) + { + Load(); + } + + return NameToProtocolNumber![name]; + } + + public Identifier GetName(int id) + { + if (!Loaded) + { + Load(); + } + + return ProtocolNumberToName![id]; + } +} + +internal class NameAndProtocolNumberIndexedData(IDataProvider> provider) + : NameAndProtocolNumberIndexedData(provider), INameAndProtocolNumberIndexedData + where TEnum : struct, Enum +{ + private readonly EnumNameLookup enumNameLookup = new(); + private IReadOnlyDictionary? protocolNumberToType; + private IReadOnlyDictionary? typeToProtocolNumber; + + protected override void InitializeData(IReadOnlyDictionary data) + { + base.InitializeData(data); + + protocolNumberToType = ProtocolNumberToName! + .ToFrozenDictionary( + x => x.Key, + x => enumNameLookup.FromName(NameUtils.GetParticleName(x.Value.Name))); + typeToProtocolNumber = protocolNumberToType.ToFrozenDictionary(x => x.Value, x => x.Key); + } + + public int GetProtocolId(TEnum type) + { + if (!Loaded) + { + Load(); + } + + return typeToProtocolNumber![type]; + } + + public TEnum GetType(int id) + { + if (!Loaded) + { + Load(); + } + + return protocolNumberToType![id]; + } +} diff --git a/Data/MineSharp.Data/Internal/NameUtils.cs b/Data/MineSharp.Data/Internal/NameUtils.cs index 1376ecec..c84adf7d 100644 --- a/Data/MineSharp.Data/Internal/NameUtils.cs +++ b/Data/MineSharp.Data/Internal/NameUtils.cs @@ -98,6 +98,11 @@ public static string GetGameState(string name) { return CommonGetName(name); } + + public static string GetParticleName(string name) + { + return CommonGetName(name); + } public static string GetPacketName(string name, string direction, string ns) { diff --git a/Data/MineSharp.Data/MineSharp.Data.csproj b/Data/MineSharp.Data/MineSharp.Data.csproj index b9296c17..b99f6b7f 100644 --- a/Data/MineSharp.Data/MineSharp.Data.csproj +++ b/Data/MineSharp.Data/MineSharp.Data.csproj @@ -3,7 +3,7 @@ enable enable - net7.0;net8.0 + net8.0 12 true README.md diff --git a/Data/MineSharp.Data/MinecraftData.cs b/Data/MineSharp.Data/MinecraftData.cs index a1557403..57abde55 100644 --- a/Data/MineSharp.Data/MinecraftData.cs +++ b/Data/MineSharp.Data/MinecraftData.cs @@ -1,4 +1,5 @@ -using MineSharp.Data.Biomes; +using MineSharp.Core.Common; +using MineSharp.Data.Biomes; using MineSharp.Data.BlockCollisionShapes; using MineSharp.Data.Blocks; using MineSharp.Data.Effects; @@ -9,6 +10,7 @@ using MineSharp.Data.Items; using MineSharp.Data.Language; using MineSharp.Data.Materials; +using MineSharp.Data.Particles; using MineSharp.Data.Protocol; using MineSharp.Data.Recipes; using MineSharp.Data.Windows; @@ -49,6 +51,7 @@ private MinecraftData( IRecipeData recipes, IWindowData windows, ILanguageData language, + IParticleData particles, MinecraftVersion version) { Biomes = biomes; @@ -63,6 +66,7 @@ private MinecraftData( Recipes = recipes; Windows = windows; Language = language; + Particles = particles; Version = version; } @@ -125,9 +129,14 @@ private MinecraftData( /// The language data provider for this version /// public ILanguageData Language { get; } + + /// + /// The particle data for this version + /// + public IParticleData Particles { get; } /// - /// The minecraft version of this instance + /// The Minecraft version of this instance /// public MinecraftVersion Version { get; } @@ -164,6 +173,7 @@ public static async Task FromVersion(string version) var materialsToken = await LoadAsset("materials", versionToken); var recipesToken = await LoadAsset("recipes", versionToken); var languageToken = await LoadAsset("language", versionToken); + var particleToken = await LoadAsset("particles", versionToken); var biomes = new BiomeData(new BiomeProvider(biomeToken)); var items = new ItemData(new ItemProvider(itemsToken)); @@ -176,6 +186,7 @@ public static async Task FromVersion(string version) var materials = new MaterialData(new MaterialsProvider(materialsToken, items)); var recipes = new RecipeData(new(recipesToken, items)); var language = new LanguageData(new LanguageProvider(languageToken)); + var particles = new ParticleData(new ParticleDataProvider(particleToken)); var windows = GetWindowData(minecraftVersion); var data = new MinecraftData( @@ -191,6 +202,7 @@ public static async Task FromVersion(string version) recipes, windows, language, + particles, minecraftVersion); LoadedData.Add(version, data); diff --git a/Data/MineSharp.Data/Particles/ParticleData.cs b/Data/MineSharp.Data/Particles/ParticleData.cs new file mode 100644 index 00000000..5b15523a --- /dev/null +++ b/Data/MineSharp.Data/Particles/ParticleData.cs @@ -0,0 +1,10 @@ +using MineSharp.Core.Common; +using MineSharp.Core.Common.Particles; +using MineSharp.Data.Framework; +using MineSharp.Data.Framework.Providers; +using MineSharp.Data.Internal; + +namespace MineSharp.Data.Particles; + +internal class ParticleData(IDataProvider> provider) + : NameAndProtocolNumberIndexedData(provider), IParticleData; diff --git a/Data/MineSharp.Data/Particles/ParticleDataProvider.cs b/Data/MineSharp.Data/Particles/ParticleDataProvider.cs new file mode 100644 index 00000000..ac1a89b9 --- /dev/null +++ b/Data/MineSharp.Data/Particles/ParticleDataProvider.cs @@ -0,0 +1,36 @@ +using System.Collections.Frozen; +using MineSharp.Core.Common; +using MineSharp.Data.Framework.Providers; +using Newtonsoft.Json.Linq; + +namespace MineSharp.Data.Particles; + +internal class ParticleDataProvider : IDataProvider> +{ + private readonly JArray token; + + public ParticleDataProvider(JToken token) + { + if (token.Type != JTokenType.Array) + { + throw new ArgumentException("Expected the token to be an array"); + } + + this.token = (token as JArray)!; + } + + public IReadOnlyDictionary GetData() + { + return token + .Select(FromToken) + .ToFrozenDictionary(x => x.Key, x => x.Value); + } + + private static KeyValuePair FromToken(JToken token) + { + var name = (string)token["name"]!; + var id = (int)token["id"]!; + + return new (Identifier.Parse(name), id); + } +} diff --git a/Data/MineSharp.Data/Protocol/ProtocolData.cs b/Data/MineSharp.Data/Protocol/ProtocolData.cs index efbadc24..d1de3aef 100644 --- a/Data/MineSharp.Data/Protocol/ProtocolData.cs +++ b/Data/MineSharp.Data/Protocol/ProtocolData.cs @@ -1,15 +1,19 @@ -using MineSharp.Core.Common.Protocol; +using System.Collections.Frozen; +using MineSharp.Core.Common.Protocol; using MineSharp.Data.Framework; using MineSharp.Data.Framework.Providers; using MineSharp.Data.Internal; +using NLog; namespace MineSharp.Data.Protocol; internal class ProtocolData(IDataProvider provider) : IndexedData(provider), IProtocolData { - private Dictionary>> idToType = new(); - private Dictionary typeToId = new(); + private static readonly ILogger Logger = LogManager.GetCurrentClassLogger(); + + private FrozenDictionary>> idToType = FrozenDictionary>>.Empty; + private FrozenDictionary typeToId = FrozenDictionary.Empty; public int GetPacketId(PacketType type) { @@ -28,7 +32,15 @@ public PacketType GetPacketType(PacketFlow flow, GameState state, int id) Load(); } - return idToType[flow][state][id]; + try + { + return idToType[flow][state][id]; + } + catch (Exception) + { + Logger.Error("Failed to get PacketType for: flow = {Flow}, state = {State}, id = {Id}", flow, state, id); + throw; + } } protected override void InitializeData(ProtocolDataBlob data) @@ -38,6 +50,6 @@ protected override void InitializeData(ProtocolDataBlob data) typeToId = idToType.Values .SelectMany(x => x.Values) .SelectMany(x => x.ToArray()) - .ToDictionary(x => x.Value, x => x.Key); + .ToFrozenDictionary(x => x.Value, x => x.Key); } } diff --git a/Data/MineSharp.Data/Protocol/ProtocolDataBlob.cs b/Data/MineSharp.Data/Protocol/ProtocolDataBlob.cs index 1bd95db3..432e1aa4 100644 --- a/Data/MineSharp.Data/Protocol/ProtocolDataBlob.cs +++ b/Data/MineSharp.Data/Protocol/ProtocolDataBlob.cs @@ -1,6 +1,7 @@ -using MineSharp.Core.Common.Protocol; +using System.Collections.Frozen; +using MineSharp.Core.Common.Protocol; namespace MineSharp.Data.Protocol; internal record ProtocolDataBlob( - Dictionary>> IdToTypeMap); + FrozenDictionary>> IdToTypeMap); diff --git a/Data/MineSharp.Data/Protocol/ProtocolProvider.cs b/Data/MineSharp.Data/Protocol/ProtocolProvider.cs index aaccdfb2..a0f16b90 100644 --- a/Data/MineSharp.Data/Protocol/ProtocolProvider.cs +++ b/Data/MineSharp.Data/Protocol/ProtocolProvider.cs @@ -1,4 +1,5 @@ -using MineSharp.Core.Common.Protocol; +using System.Collections.Frozen; +using MineSharp.Core.Common.Protocol; using MineSharp.Data.Framework.Providers; using MineSharp.Data.Internal; using Newtonsoft.Json.Linq; @@ -24,10 +25,10 @@ public ProtocolProvider(JToken token) public ProtocolDataBlob GetData() { - var byFlow = new Dictionary>> + var byFlow = new Dictionary>> { - { PacketFlow.Clientbound, new Dictionary>() }, - { PacketFlow.Serverbound, new Dictionary>() } + { PacketFlow.Clientbound, new Dictionary>() }, + { PacketFlow.Serverbound, new Dictionary>() } }; foreach (var ns in token.Properties()) @@ -46,15 +47,16 @@ public ProtocolDataBlob GetData() byFlow[PacketFlow.Serverbound].Add(state, sbPackets); } - return new(byFlow); + var frozenDict = byFlow.ToFrozenDictionary(x => x.Key, y => y.Value.ToFrozenDictionary()); + return new(frozenDict); } - private Dictionary CollectPackets(string ns, string direction) + private FrozenDictionary CollectPackets(string ns, string direction) { var obj = (JObject)token.SelectToken($"{ns}.{direction}.types.packet[1][0].type[1].mappings")!; return obj.Properties() - .ToDictionary( + .ToFrozenDictionary( x => Convert.ToInt32(x.Name, 16), x => TypeLookup.FromName( NameUtils.GetPacketName( diff --git a/Data/MineSharp.Data/Windows/WindowData.cs b/Data/MineSharp.Data/Windows/WindowData.cs index 782f377e..cd8900b9 100644 --- a/Data/MineSharp.Data/Windows/WindowData.cs +++ b/Data/MineSharp.Data/Windows/WindowData.cs @@ -1,4 +1,5 @@ -using MineSharp.Core.Common.Blocks; +using MineSharp.Core.Common; +using MineSharp.Core.Common.Blocks; using MineSharp.Data.Framework; using MineSharp.Data.Framework.Providers; using MineSharp.Data.Internal; @@ -46,7 +47,7 @@ internal class WindowData(IDataProvider provider) : IndexedData windowMap = new(); + private Dictionary windowMap = new(); private WindowInfo[]? Windows { get; set; } @@ -66,7 +67,7 @@ public WindowInfo ById(int id) return Windows![id]; } - public WindowInfo ByName(string name) + public WindowInfo ByName(Identifier name) { if (!Loaded) { diff --git a/Data/MineSharp.Data/Windows/WindowInfo.cs b/Data/MineSharp.Data/Windows/WindowInfo.cs index 542b935d..a9d9854d 100644 --- a/Data/MineSharp.Data/Windows/WindowInfo.cs +++ b/Data/MineSharp.Data/Windows/WindowInfo.cs @@ -1,4 +1,6 @@ -namespace MineSharp.Data.Windows; +using MineSharp.Core.Common; + +namespace MineSharp.Data.Windows; /// /// Window descriptor. @@ -9,11 +11,30 @@ /// Whether the player's inventory is excluded for this window /// Whether the window has an offhand public record WindowInfo( - string Name, + Identifier Name, string Title, int UniqueSlots, bool ExcludeInventory = false, - bool HasOffHandSlot = false); + bool HasOffHandSlot = false) +{ + /// + /// Constructor for convenience that handles parsing. + /// + /// + /// + /// + /// + /// + public WindowInfo( + string name, + string title, + int uniqueSlots, + bool excludeInventory = false, + bool hasOffHandSlot = false) + : this(Identifier.Parse(name), title, uniqueSlots, excludeInventory, hasOffHandSlot) + { + } +} #pragma warning disable CS1591 diff --git a/Data/MineSharp.SourceGenerator/Generators/ParticleGenerator.cs b/Data/MineSharp.SourceGenerator/Generators/ParticleGenerator.cs new file mode 100644 index 00000000..c3f32d02 --- /dev/null +++ b/Data/MineSharp.SourceGenerator/Generators/ParticleGenerator.cs @@ -0,0 +1,21 @@ +using MineSharp.SourceGenerator.Utils; +using Newtonsoft.Json.Linq; + +namespace MineSharp.SourceGenerator.Generators; + +public class ParticleGenerator +{ + private readonly Generator typeGenerator = new("particles", GetName, "ParticleType", "Particles"); + + public Task Run(MinecraftDataWrapper wrapper) + { + return Task.WhenAll( + typeGenerator.Generate(wrapper)); + } + + private static string GetName(JToken token) + { + var name = (string)token.SelectToken("name")!; + return NameUtils.GetParticleName(name); + } +} diff --git a/Data/MineSharp.SourceGenerator/MineSharp.SourceGenerator.csproj b/Data/MineSharp.SourceGenerator/MineSharp.SourceGenerator.csproj index 43fcb6fb..7238608e 100644 --- a/Data/MineSharp.SourceGenerator/MineSharp.SourceGenerator.csproj +++ b/Data/MineSharp.SourceGenerator/MineSharp.SourceGenerator.csproj @@ -4,9 +4,9 @@ Exe enable enable - net7.0;net8.0 12 false + net8.0 diff --git a/Data/MineSharp.SourceGenerator/Program.cs b/Data/MineSharp.SourceGenerator/Program.cs index 79f43b4f..40cc6ed1 100644 --- a/Data/MineSharp.SourceGenerator/Program.cs +++ b/Data/MineSharp.SourceGenerator/Program.cs @@ -10,7 +10,7 @@ { new BiomeGenerator().Run(data), new BlockGenerator().Run(data), new EffectGenerator().Run(data), new EnchantmentGenerator().Run(data), new EntityGenerator().Run(data), new ItemGenerator().Run(data), - new ProtocolGenerator().Run(data) + new ProtocolGenerator().Run(data), new ParticleGenerator().Run(data) }; if (Directory.Exists(DirectoryUtils.GetSourceDirectory())) diff --git a/Data/MineSharp.SourceGenerator/Utils/NameUtils.cs b/Data/MineSharp.SourceGenerator/Utils/NameUtils.cs index c9ad82aa..43fa1332 100644 --- a/Data/MineSharp.SourceGenerator/Utils/NameUtils.cs +++ b/Data/MineSharp.SourceGenerator/Utils/NameUtils.cs @@ -99,6 +99,11 @@ public static string GetGameState(string name) return CommonGetName(name); } + public static string GetParticleName(string name) + { + return CommonGetName(name); + } + public static string GetPacketName(string name, string direction, string ns) { direction = direction == "toClient" ? "CB" : "SB"; diff --git a/Data/MineSharp.SourceGenerator/minecraft-data b/Data/MineSharp.SourceGenerator/minecraft-data index 8a8c811c..27e91e96 160000 --- a/Data/MineSharp.SourceGenerator/minecraft-data +++ b/Data/MineSharp.SourceGenerator/minecraft-data @@ -1 +1 @@ -Subproject commit 8a8c811c71790d75ec405429e5b50343cb77f88b +Subproject commit 27e91e9679243982dfd8edf4e9a050ee6b544baa diff --git a/MineSharp.Bot.IntegrationTests/MineSharp.Bot.IntegrationTests.csproj b/MineSharp.Bot.IntegrationTests/MineSharp.Bot.IntegrationTests.csproj index 506d7e9a..95c99e49 100644 --- a/MineSharp.Bot.IntegrationTests/MineSharp.Bot.IntegrationTests.csproj +++ b/MineSharp.Bot.IntegrationTests/MineSharp.Bot.IntegrationTests.csproj @@ -4,7 +4,7 @@ Exe enable enable - net7.0;net8.0 + net8.0 12 false diff --git a/MineSharp.Bot/Blocks/PlayerActionStatus.cs b/MineSharp.Bot/Blocks/PlayerActionStatus.cs deleted file mode 100644 index fb587037..00000000 --- a/MineSharp.Bot/Blocks/PlayerActionStatus.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace MineSharp.Bot.Blocks; - -/// -/// Specifies the status of a player action -/// -public enum PlayerActionStatus -{ - /// - /// Digging has started - /// - StartedDigging = 0, - - /// - /// Digging has been cancelled - /// - CancelledDigging = 1, - - /// - /// Digging is complete - /// - FinishedDigging = 2, - - /// - /// Drop the item stack - /// - DropItemStack = 3, - - /// - /// Drop a single item - /// - DropItem = 4, - - /// - /// Finished eating or shot bow - /// - ShootArrowFinishedEating = 5, - - /// - /// Swap item to off hand - /// - SwapItemHand = 6 -} diff --git a/MineSharp.Bot/Chat/ChatSignature.cs b/MineSharp.Bot/Chat/ChatSignature.cs index 68805c9b..a38d5570 100644 --- a/MineSharp.Bot/Chat/ChatSignature.cs +++ b/MineSharp.Bot/Chat/ChatSignature.cs @@ -7,6 +7,7 @@ using System.Security.Cryptography; using System.Text; using MineSharp.Core.Common; +using MineSharp.Core.Serialization; using MineSharp.Data; using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/MineSharp.Bot/MineSharp.Bot.csproj b/MineSharp.Bot/MineSharp.Bot.csproj index 9031b47d..0ec3ab60 100644 --- a/MineSharp.Bot/MineSharp.Bot.csproj +++ b/MineSharp.Bot/MineSharp.Bot.csproj @@ -3,7 +3,7 @@ enable enable - net7.0;net8.0 + net8.0 12 true README.md @@ -43,4 +43,10 @@ + + + Always + + + diff --git a/MineSharp.Bot/MineSharpBot.cs b/MineSharp.Bot/MineSharpBot.cs index 7b82acdb..e6b3b156 100644 --- a/MineSharp.Bot/MineSharpBot.cs +++ b/MineSharp.Bot/MineSharpBot.cs @@ -2,9 +2,8 @@ using MineSharp.Auth; using MineSharp.Bot.Exceptions; using MineSharp.Bot.Plugins; -using MineSharp.ChatComponent.Components; using MineSharp.Core.Common.Protocol; -using MineSharp.Core.Events; +using MineSharp.Core.Concurrency; using MineSharp.Data; using MineSharp.Protocol; using MineSharp.Protocol.Packets.Clientbound.Configuration; @@ -17,16 +16,20 @@ namespace MineSharp.Bot; /// A Minecraft Bot. /// The Minecraft Bot uses Plugins that contain helper methods to handle and send minecraft packets. /// -public class MineSharpBot +public sealed class MineSharpBot : IAsyncDisposable, IDisposable { private static readonly ILogger Logger = LogManager.GetCurrentClassLogger(); - private readonly CancellationTokenSource cancellation; /// /// The underlying used by this bot /// public readonly MinecraftClient Client; + /// + /// Is cancelled once the client needs to stop. Usually because the connection was lost. + /// + public CancellationToken CancellationToken => Client!.CancellationToken; + /// /// The instance used by this bot /// @@ -38,17 +41,12 @@ public class MineSharpBot /// The object used by this bot /// public readonly Session Session; - + /// /// NBT Registry sent by the server /// public NbtCompound Registry { get; private set; } = []; - /// - /// Fired when the bot disconnects - /// - public AsyncEvent OnBotDisconnected = new(); - // This field is used for syncing block updates since 1.19. internal int SequenceId = 0; @@ -64,13 +62,21 @@ public MineSharpBot(MinecraftClient client) Data = Client.Data; Session = Client.Session; - cancellation = new(); plugins = new Dictionary(); - Client.OnDisconnected += OnClientDisconnected; - Client.On(packet => Task.FromResult(Registry = packet.RegistryData)); - Client.On( - packet => Task.FromResult(packet.RegistryCodec != null ? Registry = packet.RegistryCodec : null)); + Client.On(packet => + { + Registry = packet.RegistryData; + return Task.CompletedTask; + }); + Client.On(packet => + { + if (packet.RegistryCodec != null) + { + Registry = packet.RegistryCodec; + } + return Task.CompletedTask; + }); } /// @@ -123,7 +129,7 @@ await Task.WhenAll( plugins.Values .Select(pl => pl.Initialize())); - tickLoop = TickLoop(); + tickLoop = Task.Run(TickLoop); return true; } @@ -132,22 +138,17 @@ await Task.WhenAll( /// Disconnect from the Minecraft server /// /// The reason for disconnecting - public async Task Disconnect(ChatComponent.Chat? reason = null) + public EnsureOnlyRunOnceAsyncResult Disconnect(ChatComponent.Chat? reason = null) { - reason ??= new TranslatableComponent("disconnect.quitting"); - - if (tickLoop is { Status: TaskStatus.Running }) - { - cancellation.Cancel(); - await tickLoop!; - } + // there is no point in waiting for the tickLoop + // it wil be cancelled when the bot disconnects - await Client.Disconnect(reason); + return Client.Disconnect(reason); } private async Task TickLoop() { - while (!cancellation.Token.IsCancellationRequested) + while (!CancellationToken.IsCancellationRequested) { var start = DateTime.Now; @@ -176,27 +177,14 @@ private async Task TickLoop() } } - private Task OnClientDisconnected(MinecraftClient sender, ChatComponent.Chat reason) + public void Dispose() { - return OnBotDisconnected.Dispatch(this, reason); + // TODO: improve packet callback registrations to be able to remove them here } - /// - /// Enable debug logs of minesharp to be written to the console and a log file. - /// Trace logs are only written to the logfile. - /// - /// if true, also log trace messages. - public static void EnableDebugLogs(bool trace = false) + public ValueTask DisposeAsync() { - var configuration = new NLog.Config.LoggingConfiguration(); - - var logfile = new NLog.Targets.FileTarget("customfile") { FileName = $"{DateTime.Now:dd.MM.yyyy hh:mm:ss}.log" }; - var logconsole = new NLog.Targets.ConsoleTarget("logconsole"); - - var level = trace ? LogLevel.Trace : LogLevel.Debug; - configuration.AddRule(level, LogLevel.Fatal, logfile); - configuration.AddRule(LogLevel.Debug, LogLevel.Fatal, logconsole); - - LogManager.Configuration = configuration; + Dispose(); + return ValueTask.CompletedTask; } } diff --git a/MineSharp.Core/NLog.config b/MineSharp.Bot/NLog.config similarity index 83% rename from MineSharp.Core/NLog.config rename to MineSharp.Bot/NLog.config index e19029f8..d9c584ac 100644 --- a/MineSharp.Core/NLog.config +++ b/MineSharp.Bot/NLog.config @@ -4,7 +4,7 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> - + diff --git a/MineSharp.Bot/Plugins/ChatPlugin.cs b/MineSharp.Bot/Plugins/ChatPlugin.cs index adf32d58..969b00c4 100644 --- a/MineSharp.Bot/Plugins/ChatPlugin.cs +++ b/MineSharp.Bot/Plugins/ChatPlugin.cs @@ -45,10 +45,10 @@ public ChatPlugin(MineSharpBot bot) : base(bot) _ => null }; - Bot.Client.On(HandleChatMessagePacket); - Bot.Client.On(HandleChat); - Bot.Client.On(HandleSystemChatMessage); - Bot.Client.On(HandleDisguisedChatMessage); + OnPacketAfterInitialization(HandleChatMessagePacket); + OnPacketAfterInitialization(HandleChat); + OnPacketAfterInitialization(HandleSystemChatMessage); + OnPacketAfterInitialization(HandleDisguisedChatMessage); initDeclareCommandsPacket = Bot.Client.WaitForPacket(); } @@ -72,7 +72,8 @@ await Bot.Client.SendPacket( )); } - await HandleDeclareCommandsPacket(await initDeclareCommandsPacket); + var declareCommandsPacket = await initDeclareCommandsPacket.WaitAsync(Bot.CancellationToken); + await HandleDeclareCommandsPacket(declareCommandsPacket).WaitAsync(Bot.CancellationToken); } /// @@ -136,6 +137,7 @@ public Task SendCommand(string command) }; } + private static readonly Identifier MessageIdentifier = Identifier.Parse("minecraft:message"); /* * Thanks to Minecraft-Console-Client @@ -177,7 +179,7 @@ public Task SendCommand(string command) var arg = command.Split(' ', argCnt + 1); if ((node.Flags & 0x04) > 0) { - if (node.Parser != null && node.Parser.GetName() == "minecraft:message") + if (node.Parser != null && node.Parser.GetName() == MessageIdentifier) { arguments.Add((node.Name!, command)); } @@ -531,14 +533,14 @@ private Task HandleSystemChatMessage(SystemChatMessagePacket packet) { return packet switch { - SystemChatMessagePacket.Before192 before192 + SystemChatMessagePacket.Before192 before192 => HandleChatInternal(null, before192.Message, (ChatMessageType)before192.ChatType), - SystemChatMessagePacket.Since192 since192 + SystemChatMessagePacket.Since192 since192 => HandleChatInternal(null, since192.Message, since192.IsOverlay ? ChatMessageType.GameInfo : ChatMessageType.SystemMessage), - + _ => throw new UnreachableException() }; } @@ -582,7 +584,7 @@ private Task HandleChatInternal(Uuid? sender, ChatComponent.Chat message, ChatMe ChatComponent.Chat? target) { var entry = Bot.Registry["minecraft:chat_type"]["value"][index]; - var name = entry["name"]!.StringValue!; + var name = Identifier.Parse(entry["name"]!.StringValue!); var element = entry["element"]; var styleCompound = element["chat"]["style"]; var translation = element["chat"]["translation_key"].StringValue; @@ -620,9 +622,9 @@ private Task HandleChatInternal(Uuid? sender, ChatComponent.Chat message, ChatMe return (translatable, GetChatMessageType(name)); } - private static ChatMessageType GetChatMessageType(string message) + private static ChatMessageType GetChatMessageType(Identifier message) { - return message switch + return message.ToString() switch { "minecraft:chat" => ChatMessageType.Chat, "minecraft:emote_command" => ChatMessageType.Emote, @@ -633,7 +635,7 @@ private static ChatMessageType GetChatMessageType(string message) }; } - private static ChatMessageType UnknownChatMessage(string msg) + private static ChatMessageType UnknownChatMessage(Identifier msg) { Logger.Debug("Unknown chat message type {message}.", msg); return ChatMessageType.Raw; diff --git a/MineSharp.Bot/Plugins/CraftingPlugin.cs b/MineSharp.Bot/Plugins/CraftingPlugin.cs index ddf5360b..d21ee879 100644 --- a/MineSharp.Bot/Plugins/CraftingPlugin.cs +++ b/MineSharp.Bot/Plugins/CraftingPlugin.cs @@ -22,7 +22,7 @@ protected override async Task Init() { windowPlugin = Bot.GetPlugin(); - await windowPlugin.WaitForInventory(); + await windowPlugin.WaitForInventory().WaitAsync(Bot.CancellationToken); } /// diff --git a/MineSharp.Bot/Plugins/EntityPlugin.cs b/MineSharp.Bot/Plugins/EntityPlugin.cs index dbe376d9..c805bcf2 100644 --- a/MineSharp.Bot/Plugins/EntityPlugin.cs +++ b/MineSharp.Bot/Plugins/EntityPlugin.cs @@ -28,17 +28,17 @@ public EntityPlugin(MineSharpBot bot) : base(bot) { Entities = new ConcurrentDictionary(); - Bot.Client.On(HandleSpawnEntityPacket); - Bot.Client.On(HandleSpawnLivingEntityPacket); - Bot.Client.On(HandleRemoveEntitiesPacket); - Bot.Client.On(HandleSetEntityVelocityPacket); - Bot.Client.On(HandleUpdateEntityPositionPacket); - Bot.Client.On(HandleUpdateEntityPositionAndRotationPacket); - Bot.Client.On(HandleUpdateEntityRotationPacket); - Bot.Client.On(HandleTeleportEntityPacket); - Bot.Client.On(HandleUpdateAttributesPacket); - Bot.Client.On(HandleSynchronizePlayerPosition); - Bot.Client.On(HandleSetPassengersPacket); + OnPacketAfterInitialization(HandleSpawnEntityPacket); + OnPacketAfterInitialization(HandleSpawnLivingEntityPacket); + OnPacketAfterInitialization(HandleRemoveEntitiesPacket); + OnPacketAfterInitialization(HandleSetEntityVelocityPacket); + OnPacketAfterInitialization(HandleUpdateEntityPositionPacket); + OnPacketAfterInitialization(HandleUpdateEntityPositionAndRotationPacket); + OnPacketAfterInitialization(HandleUpdateEntityRotationPacket); + OnPacketAfterInitialization(HandleTeleportEntityPacket); + OnPacketAfterInitialization(HandleUpdateAttributesPacket); + OnPacketAfterInitialization(HandleSynchronizePlayerPosition); + OnPacketAfterInitialization(HandleSetPassengersPacket); } /// @@ -60,7 +60,7 @@ public EntityPlugin(MineSharpBot bot) : base(bot) protected override async Task Init() { playerPlugin = Bot.GetPlugin(); - await playerPlugin.WaitForInitialization(); + await playerPlugin.WaitForInitialization().WaitAsync(Bot.CancellationToken); } internal void AddEntity(Entity entity) @@ -183,7 +183,7 @@ private Task HandleSetEntityVelocityPacket(SetEntityVelocityPacket packet) return Task.CompletedTask; } - (entity.Position as MutableVector3)!.Add( + (entity.Velocity as MutableVector3)!.Set( NetUtils.ConvertToVelocity(packet.VelocityX), NetUtils.ConvertToVelocity(packet.VelocityY), NetUtils.ConvertToVelocity(packet.VelocityZ) @@ -204,7 +204,7 @@ private Task HandleUpdateEntityPositionPacket(EntityPositionPacket packet) return Task.CompletedTask; } - (entity.Position as MutableVector3)!.Set( + (entity.Position as MutableVector3)!.Add( NetUtils.ConvertDeltaPosition(packet.DeltaX), NetUtils.ConvertDeltaPosition(packet.DeltaY), NetUtils.ConvertDeltaPosition(packet.DeltaZ) @@ -212,7 +212,8 @@ private Task HandleUpdateEntityPositionPacket(EntityPositionPacket packet) entity.IsOnGround = packet.OnGround; - return OnEntityMoved.Dispatch(Bot, entity); + _ = OnEntityMoved.Dispatch(Bot, entity); + return Task.CompletedTask; } private Task HandleUpdateEntityPositionAndRotationPacket(EntityPositionAndRotationPacket packet) @@ -236,7 +237,8 @@ private Task HandleUpdateEntityPositionAndRotationPacket(EntityPositionAndRotati entity.Pitch = NetUtils.FromAngleByte(packet.Pitch); entity.IsOnGround = packet.OnGround; - return OnEntityMoved.Dispatch(Bot, entity); + _ = OnEntityMoved.Dispatch(Bot, entity); + return Task.CompletedTask; } private Task HandleUpdateEntityRotationPacket(EntityRotationPacket packet) @@ -255,7 +257,8 @@ private Task HandleUpdateEntityRotationPacket(EntityRotationPacket packet) entity.Pitch = NetUtils.FromAngleByte(packet.Pitch); entity.IsOnGround = packet.OnGround; - return OnEntityMoved.Dispatch(Bot, entity); + _ = OnEntityMoved.Dispatch(Bot, entity); + return Task.CompletedTask; } private Task HandleTeleportEntityPacket(TeleportEntityPacket packet) @@ -275,8 +278,9 @@ private Task HandleTeleportEntityPacket(TeleportEntityPacket packet) entity.Yaw = NetUtils.FromAngleByte(packet.Yaw); entity.Pitch = NetUtils.FromAngleByte(packet.Pitch); - - return OnEntityMoved.Dispatch(Bot, entity); + + _ = OnEntityMoved.Dispatch(Bot, entity); + return Task.CompletedTask; } private Task HandleUpdateAttributesPacket(UpdateAttributesPacket packet) @@ -293,10 +297,8 @@ private Task HandleUpdateAttributesPacket(UpdateAttributesPacket packet) foreach (var attribute in packet.Attributes) { - if (!entity.Attributes.TryAdd(attribute.Key, attribute)) - { - entity.Attributes[attribute.Key] = attribute; - } + // must not be Attributes[Key] = attribute because this might cause an exception + entity.Attributes.AddOrUpdate(attribute.Key, attribute, (key, oldvalue) => attribute); } return Task.CompletedTask; @@ -313,7 +315,7 @@ private async Task HandleSynchronizePlayerPosition(PlayerPositionPacket packet) var position = (playerPlugin!.Entity!.Position as MutableVector3)!; - if ((packet.Flags & 0x01) == 0x01) + if (packet.Flags.HasFlag(PlayerPositionPacket.PositionFlags.X)) { position.Add(packet.X, 0, 0); } @@ -322,7 +324,7 @@ private async Task HandleSynchronizePlayerPosition(PlayerPositionPacket packet) position.SetX(packet.X); } - if ((packet.Flags & 0x02) == 0x02) + if (packet.Flags.HasFlag(PlayerPositionPacket.PositionFlags.Y)) { position.Add(0, packet.Y, 0); } @@ -331,7 +333,7 @@ private async Task HandleSynchronizePlayerPosition(PlayerPositionPacket packet) position.SetY(packet.Y); } - if ((packet.Flags & 0x04) == 0x04) + if (packet.Flags.HasFlag(PlayerPositionPacket.PositionFlags.Z)) { position.Add(0, 0, packet.Z); } @@ -340,7 +342,7 @@ private async Task HandleSynchronizePlayerPosition(PlayerPositionPacket packet) position.SetZ(packet.Z); } - if ((packet.Flags & 0x08) == 0x08) + if (packet.Flags.HasFlag(PlayerPositionPacket.PositionFlags.YRot)) { playerPlugin!.Entity!.Pitch += packet.Pitch; } @@ -349,7 +351,7 @@ private async Task HandleSynchronizePlayerPosition(PlayerPositionPacket packet) playerPlugin!.Entity!.Pitch = packet.Pitch; } - if ((packet.Flags & 0x10) == 0x10) + if (packet.Flags.HasFlag(PlayerPositionPacket.PositionFlags.XRot)) { playerPlugin!.Entity!.Yaw += packet.Yaw; } diff --git a/MineSharp.Bot/Plugins/PhysicsPlugin.cs b/MineSharp.Bot/Plugins/PhysicsPlugin.cs index 177b2d43..8e02bfbe 100644 --- a/MineSharp.Bot/Plugins/PhysicsPlugin.cs +++ b/MineSharp.Bot/Plugins/PhysicsPlugin.cs @@ -67,12 +67,13 @@ protected override async Task Init() playerPlugin = Bot.GetPlugin(); worldPlugin = Bot.GetPlugin(); - await playerPlugin.WaitForInitialization(); + await playerPlugin.WaitForInitialization().WaitAsync(Bot.CancellationToken); + await worldPlugin.WaitForInitialization().WaitAsync(Bot.CancellationToken); self = playerPlugin.Self; - await UpdateServerPos(); + await UpdateServerPos().WaitAsync(Bot.CancellationToken); - Engine = new(Bot.Data, self!, worldPlugin.World, InputControls); + Engine = new(Bot.Data, self!, worldPlugin.World!, InputControls); Engine.OnCrouchingChanged += OnSneakingChanged; Engine.OnSprintingChanged += OnSprintingChanged; } @@ -187,11 +188,20 @@ public Task LookAt(Block block, float smoothness = RotationSmoothness) return LookAt(new Vector3(0.5, 0.5, 0.5).Plus(block.Position), smoothness); } + /// + /// Represents the result of a ray casting operation. + /// + /// The block that was hit by the ray. + /// The face of the block that was hit. + /// The index for the collision shape of the block that was hit. You can get the with: MineSharp.Bot.Data.BlockCollisionShapes.GetForBlock(block). + /// The distance from the ray origin to the hit point. + public record RaycastBlockResult(Block Block, BlockFace Face, int BlockCollisionShapeIndex, double Distance); + /// /// Casts a ray from the players eyes, and returns the first block that is hit. /// /// - public (Block Block, BlockFace Face)? Raycast(double distance = 64) + public RaycastBlockResult? Raycast(double distance = 64) { if (distance < 0) { @@ -207,7 +217,7 @@ public Task LookAt(Block block, float smoothness = RotationSmoothness) foreach (var pos in iterator.Iterate()) { - var block = worldPlugin!.World.GetBlockAt(pos); + var block = worldPlugin!.World!.GetBlockAt(pos); if (!block.IsSolid()) { continue; @@ -215,13 +225,15 @@ public Task LookAt(Block block, float smoothness = RotationSmoothness) var bbs = Bot.Data.BlockCollisionShapes.GetForBlock(block); - foreach (var bb in bbs) + for (int bbIndex = 0; bbIndex < bbs.Length; bbIndex++) { + var bb = bbs[bbIndex]; var blockBb = bb.Clone().Offset(block.Position.X, block.Position.Y, block.Position.Z); - if (blockBb.IntersectsLine(position, lookVector)) + var intersectionDistance = blockBb.IntersectsLine(position, lookVector); + if (intersectionDistance is not null) { - return (block, iterator.CurrentFace); + return new(block, iterator.CurrentFace, bbIndex, intersectionDistance.Value); } } } @@ -230,7 +242,7 @@ public Task LookAt(Block block, float smoothness = RotationSmoothness) } /// - public override Task OnTick() + protected internal override Task OnTick() { if (!IsLoaded) { @@ -299,7 +311,7 @@ private async Task UpdateServerPos() await Bot.Client.SendPacket(packet); - await BotMoved.Dispatch(Bot); + _ = BotMoved.Dispatch(Bot); } private void OnSneakingChanged(PlayerPhysics sender, bool isSneaking) @@ -311,7 +323,7 @@ private void OnSneakingChanged(PlayerPhysics sender, bool isSneaking) : EntityActionPacket.EntityAction.StopSneaking, 0); - Bot.Client.SendPacket(packet); + _ = Bot.Client.SendPacket(packet); } private void OnSprintingChanged(PlayerPhysics sender, bool isSprinting) @@ -323,7 +335,7 @@ private void OnSprintingChanged(PlayerPhysics sender, bool isSprinting) : EntityActionPacket.EntityAction.StopSprinting, 0); - Bot.Client.SendPacket(packet); + _ = Bot.Client.SendPacket(packet); } private record PlayerState @@ -384,7 +396,7 @@ public LerpRotation(MinecraftPlayer player, float toYaw, float toPitch, float sm var yawTicks = Math.Abs((int)(deltaYaw / yawPerTick)); var pitchTicks = Math.Abs((int)(deltaPitch / pitchPerTick)); - task = new(); + task = new(TaskCreationOptions.RunContinuationsAsynchronously); remainingYawTicks = yawTicks; remainingPitchTicks = pitchTicks; } diff --git a/MineSharp.Bot/Plugins/PlayerPlugin.cs b/MineSharp.Bot/Plugins/PlayerPlugin.cs index ed3bd307..8c480126 100644 --- a/MineSharp.Bot/Plugins/PlayerPlugin.cs +++ b/MineSharp.Bot/Plugins/PlayerPlugin.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Collections.Frozen; using System.Diagnostics; using MineSharp.Bot.Exceptions; using MineSharp.Bot.Utils; @@ -14,7 +15,7 @@ namespace MineSharp.Bot.Plugins; /// -/// This Plugins provides basic functionality for the minecraft player. +/// This Plugins provides basic functionality for the Minecraft player. /// It keeps track of things like Health and initializes the Bot entity, /// which is required for many other plugins. /// @@ -24,6 +25,7 @@ public class PlayerPlugin : Plugin private readonly Task initLoginPacket; private readonly Task initPositionPacket; + private readonly Task initHealthPacket; private EntityPlugin? entities; private PhysicsPlugin? physics; @@ -37,19 +39,20 @@ public PlayerPlugin(MineSharpBot bot) : base(bot) Players = new ConcurrentDictionary(); PlayerMap = new ConcurrentDictionary(); - Bot.Client.On(HandleSetHealthPacket); - Bot.Client.On(HandleCombatDeathPacket); - Bot.Client.On(HandleRespawnPacket); - Bot.Client.On(HandleSpawnPlayer); - Bot.Client.On(HandlePlayerInfoUpdate); - Bot.Client.On(HandlePlayerInfoRemove); - Bot.Client.On(HandleGameEvent); - Bot.Client.On(HandleAcknowledgeBlockChange); - Bot.Client.On(HandleEntityStatus); + OnPacketAfterInitialization(HandleSetHealthPacket); + OnPacketAfterInitialization(HandleCombatDeathPacket); + OnPacketAfterInitialization(HandleRespawnPacket); + OnPacketAfterInitialization(HandleSpawnPlayer); + OnPacketAfterInitialization(HandlePlayerInfoUpdate); + OnPacketAfterInitialization(HandlePlayerInfoRemove); + OnPacketAfterInitialization(HandleGameEvent); + OnPacketAfterInitialization(HandleAcknowledgeBlockChange); + OnPacketAfterInitialization(HandleEntityStatus); // already start listening to the packets here, as they sometimes get lost when calling in init() initLoginPacket = Bot.Client.WaitForPacket(); initPositionPacket = Bot.Client.WaitForPacket(); + initHealthPacket = Bot.Client.WaitForPacket(); } /// @@ -91,7 +94,7 @@ public PlayerPlugin(MineSharpBot bot) : base(bot) /// /// The Name of the dimension the bot is currently in. /// - public string? Dimension { get; private set; } + public Identifier? DimensionName { get; private set; } /// /// Whether the bot is alive or dead. @@ -156,7 +159,7 @@ protected override async Task Init() { entities = Bot.GetPlugin(); - await Task.WhenAll(initLoginPacket, initPositionPacket); + await Task.WhenAll(initLoginPacket, initPositionPacket).WaitAsync(Bot.CancellationToken); var loginPacket = await initLoginPacket; var positionPacket = await initPositionPacket; @@ -177,26 +180,32 @@ protected override async Task Init() 0, (GameMode)loginPacket.GameMode, entity, - ParseDimension(loginPacket.DimensionName)); + ParseDimension(loginPacket.DimensionType)); - PlayerMap.TryAdd(entity.ServerId, Self); + DimensionName = loginPacket.DimensionName; + + var healthPacket = await initHealthPacket.WaitAsync(Bot.CancellationToken); - Health = 20.0f; - Food = 20.0f; - Saturation = 20.0f; - Dimension = loginPacket.DimensionName; + Health = healthPacket.Health; + Food = healthPacket.Food; + Saturation = healthPacket.Saturation; Logger.Info( - $"Initialized Bot Entity: Position=({Entity!.Position}), GameMode={Self.GameMode}, Dimension={Dimension}."); + "Initialized Bot Entity: Position=({Position}), GameMode={GameMode}, Health={Health}, Dimension={DimensionName} ({Dimension}).", Entity!.Position, Self.GameMode, Health, DimensionName, Self!.Dimension); + + PlayerMap.TryAdd(entity.ServerId, Self); - await Bot.Client.SendPacket( - new SetPlayerPositionAndRotationPacket( - positionPacket.X, - positionPacket.Y, - positionPacket.Z, - positionPacket.Yaw, - positionPacket.Pitch, - Entity.IsOnGround)); + if (Health > 0) + { + await Bot.Client.SendPacket( + new SetPlayerPositionAndRotationPacket( + positionPacket.X, + positionPacket.Y, + positionPacket.Z, + positionPacket.Yaw, + positionPacket.Pitch, + Entity.IsOnGround)); + } try { @@ -211,7 +220,7 @@ await Bot.Client.SendPacket( /// public Task Respawn() { - return Bot.Client.SendPacket(new ClientCommandPacket(0)); + return Bot.Client.SendPacket(new ClientCommandPacket(ClientCommandPacket.ClientCommandAction.PerformRespawn)); } @@ -291,7 +300,7 @@ private Task HandleRespawnPacket(RespawnPacket packet) return Task.CompletedTask; } - Self!.Dimension = ParseDimension(packet.Dimension); + Self!.Dimension = ParseDimension(packet.DimensionType); OnRespawned.Dispatch(Bot); return Task.CompletedTask; @@ -428,27 +437,27 @@ private Task HandleGameEvent(GameEventPacket packet) { switch (packet.Event) { - case 1: // End Raining + case GameEventPacket.GameEvent.EndRaining: IsRaining = false; OnWeatherChanged.Dispatch(Bot); break; - case 2: // Begin Raining + case GameEventPacket.GameEvent.BeginRaining: IsRaining = true; OnWeatherChanged.Dispatch(Bot); break; - case 7: // Rain Level Changed + case GameEventPacket.GameEvent.RainLevelChange: RainLevel = packet.Value; OnWeatherChanged.Dispatch(Bot); break; - case 8: // Thunder Level Changed + case GameEventPacket.GameEvent.ThunderLevelChange: ThunderLevel = packet.Value; OnWeatherChanged.Dispatch(Bot); break; - case 3: // GameMode Change + case GameEventPacket.GameEvent.ChangeGameMode: var gameMode = (GameMode)packet.Value; Self!.GameMode = gameMode; break; @@ -490,15 +499,19 @@ private void HandlePlayerSetPermission(EntityStatusPacket packet) player.PermissionLevel = permissionLevel; } + private static readonly FrozenDictionary DimensionByIdentifier = new Dictionary + { + { Identifier.Parse("minecraft:overworld"), Dimension.Overworld }, + { Identifier.Parse("minecraft:the_nether"), Dimension.Nether }, + { Identifier.Parse("minecraft:the_end"), Dimension.End } + }.ToFrozenDictionary(); - private Dimension ParseDimension(string dimensionName) + private Dimension ParseDimension(Identifier dimensionName) { - return dimensionName switch + if (!DimensionByIdentifier.TryGetValue(dimensionName, out var dimension)) { - "minecraft:overworld" => Core.Common.Dimension.Overworld, - "minecraft:the_nether" => Core.Common.Dimension.Nether, - "minecraft:the_end" => Core.Common.Dimension.End, - _ => throw new UnreachableException() - }; + throw new UnreachableException($"{nameof(dimensionName)} was: {dimensionName}"); + } + return dimension; } } diff --git a/MineSharp.Bot/Plugins/Plugin.cs b/MineSharp.Bot/Plugins/Plugin.cs index ddaf0a54..897c80c6 100644 --- a/MineSharp.Bot/Plugins/Plugin.cs +++ b/MineSharp.Bot/Plugins/Plugin.cs @@ -1,16 +1,31 @@ -using NLog; +using ConcurrentCollections; +using MineSharp.Protocol.Packets; +using MineSharp.Protocol.Registrations; +using NLog; +using static MineSharp.Protocol.MinecraftClient; namespace MineSharp.Bot.Plugins; /// /// Plugin for . /// -public abstract class Plugin +public abstract class Plugin : IAsyncDisposable { private static readonly ILogger Logger = LogManager.GetCurrentClassLogger(); private readonly TaskCompletionSource initializationTask; + /// + /// The bot + /// + protected readonly MineSharpBot Bot; + + /// + /// All the registrations for packet handlers that this plugin has mode. + /// Used to unregister them when the plugin is disposed. + /// + protected readonly ConcurrentHashSet PacketReceiveRegistrations; + /// /// Create a new Plugin instance /// @@ -19,14 +34,10 @@ protected Plugin(MineSharpBot bot) { Bot = bot; IsEnabled = true; - initializationTask = new(); + initializationTask = new(TaskCreationOptions.RunContinuationsAsynchronously); + PacketReceiveRegistrations = new(); } - /// - /// The bot - /// - protected MineSharpBot Bot { get; } - /// /// Whether this plugin is currently enabled /// @@ -34,11 +45,14 @@ protected Plugin(MineSharpBot bot) /// /// Whether this plugin is loaded and functional + /// + /// /// - public bool IsLoaded { get; private set; } + public bool IsLoaded => initializationTask.Task.IsCompleted; /// /// This method is called once when the plugin starts. + /// It should stop (cancel) when the is cancelled. /// /// protected virtual Task Init() @@ -48,9 +62,10 @@ protected virtual Task Init() /// /// This method is called each minecraft tick. (About 20 times a second.) + /// It should stop (cancel) when the is cancelled. /// /// - public virtual Task OnTick() + protected internal virtual Task OnTick() { return Task.CompletedTask; } @@ -105,6 +120,42 @@ public Task WaitForInitialization() return initializationTask.Task; } + private AsyncPacketHandler CreateAfterInitializationPacketHandlerWrapper(AsyncPacketHandler packetHandler, bool queuePacketsSentBeforeInitializationCompleted = false) + where TPacket : IPacket + { + return async param => + { + if (queuePacketsSentBeforeInitializationCompleted) + { + await WaitForInitialization(); + } + else + { + if (!IsLoaded) + { + return; + } + } + await packetHandler(param); + }; + } + + /// + /// Registers a packet handler that is only invoked after the plugin has been initialized. + /// + /// The type of the packet. + /// The packet handler to be called. + /// Whether packets sent before the plugin has been initialized should be queued and processed after initialization. + public void OnPacketAfterInitialization(AsyncPacketHandler packetHandler, bool queuePacketsSentBeforeInitializationCompleted = true) + where TPacket : IPacket + { + var registration = Bot.Client.On(CreateAfterInitializationPacketHandlerWrapper(packetHandler, queuePacketsSentBeforeInitializationCompleted)); + if (registration != null) + { + PacketReceiveRegistrations.Add(registration); + } + } + internal async Task Initialize() { if (IsLoaded) @@ -118,13 +169,43 @@ internal async Task Initialize() initializationTask.TrySetResult(); - IsLoaded = true; Logger.Info("Plugin loaded: {PluginName}", GetType().Name); } + catch (OperationCanceledException e) + { + Logger.Error(e, "Plugin {PluginName} was cancelled during Init()", GetType().Name); + initializationTask.TrySetCanceled(e.CancellationToken); + throw; + } catch (Exception e) { Logger.Error(e, "Plugin {PluginName} threw an exception during Init(). Aborting", GetType().Name); + initializationTask.TrySetException(e); throw; } } + + /// + public ValueTask DisposeAsync() + { + GC.SuppressFinalize(this); + return DisposeAsyncInternal(); + } + + /// + /// Disposes the plugin asynchronously. + /// Can be overridden by plugins to add custom dispose logic. + /// IMPORTANT: Always call base.DisposeAsync() in the overridden method (at the end). + /// + /// A ValueTask representing the asynchronous dispose operation. + protected virtual ValueTask DisposeAsyncInternal() + { + var registrations = PacketReceiveRegistrations.ToArray(); + foreach (var registration in registrations) + { + PacketReceiveRegistrations.TryRemove(registration); + registration.Dispose(); + } + return ValueTask.CompletedTask; + } } diff --git a/MineSharp.Bot/Plugins/WindowPlugin.cs b/MineSharp.Bot/Plugins/WindowPlugin.cs index d441c71e..2c275604 100644 --- a/MineSharp.Bot/Plugins/WindowPlugin.cs +++ b/MineSharp.Bot/Plugins/WindowPlugin.cs @@ -4,6 +4,7 @@ using MineSharp.Core.Common; using MineSharp.Core.Common.Blocks; using MineSharp.Core.Common.Items; +using MineSharp.Core.Concurrency; using MineSharp.Core.Events; using MineSharp.Core.Geometry; using MineSharp.Data.Windows; @@ -20,7 +21,7 @@ namespace MineSharp.Bot.Plugins; /// -/// The Window plugin takes care of minecraft window's system. +/// The Window plugin takes care of Minecraft window's system. /// It handles the Bot's Inventory, window slot updates and provides /// methods to open blocks like chests, crafting tables, ... /// @@ -44,7 +45,7 @@ public class WindowPlugin : Plugin /// public WindowPlugin(MineSharpBot bot) : base(bot) { - inventoryLoadedTsc = new(); + inventoryLoadedTsc = new(TaskCreationOptions.RunContinuationsAsynchronously); openWindows = new ConcurrentDictionary(); windowLock = new(); @@ -58,10 +59,12 @@ public WindowPlugin(MineSharpBot bot) : base(bot) CreativeInventory = new(bot); - Bot.Client.On(HandleWindowItems); - Bot.Client.On(HandleSetSlot); - Bot.Client.On(HandleHeldItemChange); - Bot.Client.On(HandleOpenWindow); + // OnPacketAfterInitialization is required to ensure that the plugin is initialized + // before handling packets. Otherwise we have race conditions that might cause errors + OnPacketAfterInitialization(HandleWindowItems); + OnPacketAfterInitialization(HandleSetSlot); + OnPacketAfterInitialization(HandleHeldItemChange); + OnPacketAfterInitialization(HandleOpenWindow); } /// @@ -102,12 +105,12 @@ public WindowPlugin(MineSharpBot bot) : base(bot) public AsyncEvent OnHeldItemChanged = new(); /// - protected override Task Init() + protected override async Task Init() { playerPlugin = Bot.GetPlugin(); + await playerPlugin.WaitForInitialization().WaitAsync(Bot.CancellationToken); CreateInventory(); - return base.Init(); } /// @@ -133,7 +136,7 @@ public async Task OpenContainer(Block block, int timeoutMs = 10 * 1000) throw new ArgumentException("Cannot open block of type " + block.Info.Name); } - openContainerTsc = new(); + openContainerTsc = new(TaskCreationOptions.RunContinuationsAsynchronously); var packet = new PlaceBlockPacket( (int)PlayerHand.MainHand, @@ -207,8 +210,8 @@ public async Task SelectHotbarIndex(byte hotbarIndex) await Bot.Client.SendPacket(packet); SelectedHotbarIndex = hotbarIndex; - - await OnHeldItemChanged.Dispatch(Bot, HeldItem); + + _ = OnHeldItemChanged.Dispatch(Bot, HeldItem); } /// @@ -223,6 +226,199 @@ public Task UseItem(PlayerHand hand = PlayerHand.MainHand) return Bot.Client.SendPacket(packet); } + /// + /// Confirmation flags for eating a food item. + /// + [Flags] + public enum EatFoodItemConfirmation + { + /// + /// No confirmation. + /// + None = 0, + + /// + /// Confirm if food or saturation value increased. + /// + FoodOrSaturationValueIncreased = 1 << 0, + + /// + /// Confirm if food item count decreased. + /// + FoodItemCountDecreased = 1 << 1, + + /// + /// Confirm all conditions. + /// + All = FoodOrSaturationValueIncreased | FoodItemCountDecreased + } + + /// + /// Reasons why eating food was canceled. + /// + public enum EatFoodItemResult + { +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + /// + /// This value is internal and should never be returned. + /// + Unknown, + SuccessfullyEaten, + CancelledUserRequested, + CancelledBotDisconnected, + CancelledBotDied, + CancelledFoodItemSlotChanged, +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + } + + /// + /// Makes the bot eat a food item from the specified hand. + /// + /// The hand from which to eat the food item. + /// The confirmation flags determining what checks should be made to ensure the food was eaten (server side). + /// The cancellation token to cancel the task. + /// A task that completes once the food was eaten or eating the food was cancelled. + public async Task EatFoodItem(PlayerHand hand = PlayerHand.MainHand, EatFoodItemConfirmation eatFoodItemConfirmation = EatFoodItemConfirmation.All, CancellationToken cancellationToken = default) + { + var foodSlot = await GetSlotForPlayerHand(hand); + + // TODO: Also check whether the item is eatable + if (foodSlot is null) + { + throw new Exception("No food item found in hand"); + } + + var result = EatFoodItemResult.Unknown; + + var cts = CancellationTokenSource.CreateLinkedTokenSource(Bot.CancellationToken, cancellationToken); + var token = cts.Token; + var botDiedTask = Bot.Client.WaitForPacketWhere(packet => packet.Health <= 0, token) + .ContinueWith(_ => + { + InterlockedHelper.CompareExchangeIfNot(ref result, EatFoodItemResult.CancelledBotDied, EatFoodItemResult.SuccessfullyEaten); + Logger.Debug("Bot died while eating food."); + return cts.CancelAsync(); + }, TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously); + + var currentFood = playerPlugin!.Food; + var currentSaturation = playerPlugin!.Saturation; + var foodValueIncreasedTask = eatFoodItemConfirmation.HasFlag(EatFoodItemConfirmation.FoodOrSaturationValueIncreased) + ? Bot.Client.WaitForPacketWhere(packet => packet.Food > currentFood || packet.Saturation > currentSaturation, token) + // for debugging race conditions + //.ContinueWith(t => + //{ + // var packet = t.Result; + // Logger.Debug("Eat finish SetHealthPacket: {Health} {Food} {Saturation}", packet.Health, packet.Food, packet.Saturation); + //}, TaskContinuationOptions.OnlyOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously) + : Task.CompletedTask; + + TaskCompletionSource? foodItemCountDecreasedTcs = null; + CancellationTokenRegistration cancellationTokenRegistration = default; + Task foodSlotUpdatedTask = Task.CompletedTask; + if (eatFoodItemConfirmation.HasFlag(EatFoodItemConfirmation.FoodItemCountDecreased)) + { + foodItemCountDecreasedTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + cancellationTokenRegistration = token.Register(() => foodItemCountDecreasedTcs.TrySetCanceled()); + foodSlotUpdatedTask = foodItemCountDecreasedTcs.Task; + } + Inventory!.OnSlotChanged += HandleSlotUpdate; + Task HandleSlotUpdate(Window window, short index) + { + var slot = window.GetSlot(index); + if (slot.SlotIndex == foodSlot.SlotIndex) + { + var itemTypeMatches = slot.Item?.Info.Type == foodSlot.Item!.Info.Type; + var currentCount = slot.Item?.Count ?? 0; + if (currentCount == 0 || itemTypeMatches) + { + if (eatFoodItemConfirmation.HasFlag(EatFoodItemConfirmation.FoodItemCountDecreased)) + { + if (currentCount > foodSlot.Item!.Count) + { + // player picked up more food + // we need to update our expected food count + foodSlot.Item!.Count = currentCount; + } + else if (currentCount < foodSlot.Item!.Count) + { + // food item got removed from slot + foodItemCountDecreasedTcs!.TrySetResult(); + } + } + } + // not else if. Do both + if (!itemTypeMatches) + { + // food item got removed or replaced from slot + // the case that the food count reached zero can be both an cancel reason and a success reason + InterlockedHelper.CompareExchange(ref result, EatFoodItemResult.CancelledFoodItemSlotChanged, EatFoodItemResult.Unknown); + return cts.CancelAsync(); + } + } + return Task.CompletedTask; + } + await UseItem(hand); + + EatFoodItemResult finalResult = EatFoodItemResult.Unknown; + try + { + // wait for food to be eaten + // no WaitAsync here because both tasks will get canceled + await Task.WhenAll(foodValueIncreasedTask, foodSlotUpdatedTask); + // TODO: Maybe here is a race condition when the foodSlotUpdatedTask finishes (item == null) so that foodValueIncreasedTask gets canceled before the packet gets handled + // or that the player died but the died packet came in later + // ^^ only solution would be a packetReceiveIndex or synchronous packet handling + + // here we do not use interlocked because it commonly happens that the result is set to CancelledFoodItemSlotChanged (item == null) + // but that is a this a success case + result = EatFoodItemResult.SuccessfullyEaten; + Logger.Debug("Bot ate food of type {FoodType} at slot {SlotIndex}.", foodSlot.Item?.Info.Type, foodSlot.SlotIndex); + } + catch (OperationCanceledException) + { + if (cancellationToken.IsCancellationRequested) + { + InterlockedHelper.CompareExchange(ref result, EatFoodItemResult.CancelledUserRequested, EatFoodItemResult.Unknown); + } + else if (Bot.CancellationToken.IsCancellationRequested) + { + InterlockedHelper.CompareExchange(ref result, EatFoodItemResult.CancelledBotDisconnected, EatFoodItemResult.Unknown); + } + } + finally + { + Inventory!.OnSlotChanged -= HandleSlotUpdate; + cancellationTokenRegistration.Dispose(); + + // cancel the other WaitForPacket tasks (like botDiedTask) + await cts.CancelAsync(); + cts.Dispose(); + finalResult = result; + if (finalResult != EatFoodItemResult.SuccessfullyEaten) + { + Logger.Debug("Eating food of type {FoodType} at slot {SlotIndex} was cancelled with reason: {CancelReason}.", foodSlot.Item?.Info.Type, foodSlot.SlotIndex, finalResult); + } + } + // if it occurs that the cancelReason is not EatFoodCancelReason.None but the "Bot ate food" is logged than there is a race condition + return finalResult; + } + + /// + /// Gets the slot for the specified player hand. + /// + /// The hand to get the slot for. + /// The slot corresponding to the specified hand. + public async Task GetSlotForPlayerHand(PlayerHand hand) + { + await WaitForInventory(); + + var slot = hand == PlayerHand.MainHand + ? Inventory!.GetSlot((short)(PlayerWindowSlots.HotbarStart + SelectedHotbarIndex)) + : Inventory!.GetSlot(Inventory.Slot.OffHand); + + return slot; + } + /// /// Search inventory for item of type , and put it /// in the bots . @@ -232,11 +428,7 @@ public Task UseItem(PlayerHand hand = PlayerHand.MainHand) /// public async Task EquipItem(ItemType type, PlayerHand hand = PlayerHand.MainHand) { - await WaitForInventory(); - - var handSlot = hand == PlayerHand.MainHand - ? Inventory!.GetSlot((short)(PlayerWindowSlots.HotbarStart + SelectedHotbarIndex)) - : Inventory!.GetSlot(Inventory.Slot.OffHand); + var handSlot = await GetSlotForPlayerHand(hand); _EquipItem(type, handSlot); } @@ -302,11 +494,11 @@ private void CreateInventory() private Window OpenWindow(int id, WindowInfo windowInfo) { - Logger.Debug("Opening window with id=" + id); + Logger.Debug("Opening window with id={WindowId}", id); if (openWindows.ContainsKey(id)) { - throw new ArgumentException("Window with id " + id + " already opened"); + throw new ArgumentException($"Window with id {id} already opened"); } var window = new Window( @@ -320,7 +512,7 @@ private Window OpenWindow(int id, WindowInfo windowInfo) { if (!openWindows.TryAdd(id, window)) { - Logger.Warn($"Could not add window with id {id}, it already existed."); + Logger.Warn("Could not add window with id {WindowId}, it already existed.", id); } } @@ -333,7 +525,7 @@ private Window OpenWindow(int id, WindowInfo windowInfo) && DateTime.Now - cacheTimestamp! <= TimeSpan.FromSeconds(5)) { // use cache - Logger.Debug("Applying cached window items for window with id=" + id); + Logger.Debug("Applying cached window items for window with id={WindowId}", id); HandleWindowItems(cachedWindowItemsPacket); } @@ -385,18 +577,17 @@ private Task HandleSetSlot(WindowSetSlotPacket packet) } else if (!openWindows.TryGetValue(packet.WindowId, out window)) { - Logger.Warn($"Received {nameof(WindowSetSlotPacket)} for windowId={packet.WindowId}, but its not opened"); + Logger.Warn("Received {PacketType} for windowId={WindowId}, but it's not opened", nameof(WindowSetSlotPacket), packet.WindowId); return Task.CompletedTask; } if (window == null) { - Logger.Warn( - $"Received {nameof(WindowSetSlotPacket)} for windowId={packet.WindowId}, but its not opened, {CurrentlyOpenedWindow?.ToString() ?? "null"}, {Inventory?.ToString() ?? "null"}"); + Logger.Warn("Received {PacketType} for windowId={WindowId}, but it's not opened, {CurrentlyOpenedWindow}, {Inventory}", nameof(WindowSetSlotPacket), packet.WindowId, CurrentlyOpenedWindow?.ToString() ?? "null", Inventory?.ToString() ?? "null"); return Task.CompletedTask; } - Logger.Debug("Handle set slot: {Slot}", packet.Slot); + // Logger.Debug("Handle set slot: {Slot}", packet.Slot); window.StateId = packet.StateId; window.SetSlot(packet.Slot); @@ -411,7 +602,7 @@ private Task HandleWindowItems(WindowItemsPacket packet) { if (!openWindows.TryGetValue(packet.WindowId, out window)) { - Logger.Warn($"Received {packet.GetType().Name} for windowId={packet.WindowId}, but its not opened"); + Logger.Warn("Received {PacketType} for windowId={WindowId}, but it's not opened", packet.GetType().Name, packet.WindowId); // Cache items in case it gets opened in a bit cachedWindowItemsPacket = packet; @@ -420,7 +611,7 @@ private Task HandleWindowItems(WindowItemsPacket packet) return Task.CompletedTask; } - Logger.Debug($"HandleWindowItems for window {window.Title}"); + Logger.Debug("HandleWindowItems for window {WindowTitle}", window.Title); } var slots = packet.Items @@ -429,9 +620,9 @@ private Task HandleWindowItems(WindowItemsPacket packet) window.StateId = packet.StateId; window.SetSlots(slots); - if (window.WindowId == 0 && !inventoryLoadedTsc.Task.IsCompleted) + if (window.WindowId == 0) { - inventoryLoadedTsc.SetResult(); + inventoryLoadedTsc.TrySetResult(); } return Task.CompletedTask; @@ -449,7 +640,7 @@ private Task HandleOpenWindow(OpenWindowPacket packet) var windowInfo = Bot.Data.Windows.ById(packet.InventoryType); windowInfo = windowInfo with { Title = packet.WindowTitle }; - Logger.Debug($"Received Open Window Packet id={packet.WindowId}"); + Logger.Debug("Received Open Window Packet id={WindowId}", packet.WindowId); OpenWindow(packet.WindowId, windowInfo); return Task.CompletedTask; diff --git a/MineSharp.Bot/Plugins/WorldPlugin.cs b/MineSharp.Bot/Plugins/WorldPlugin.cs index b1c3ea06..a542bb19 100644 --- a/MineSharp.Bot/Plugins/WorldPlugin.cs +++ b/MineSharp.Bot/Plugins/WorldPlugin.cs @@ -4,15 +4,17 @@ using MineSharp.Core.Common.Effects; using MineSharp.Core.Geometry; using MineSharp.Protocol.Packets.Clientbound.Play; +using MineSharp.Protocol.Packets.NetworkTypes; using MineSharp.Protocol.Packets.Serverbound.Play; using MineSharp.World; using MineSharp.World.Chunks; using NLog; +using static MineSharp.Protocol.Packets.Serverbound.Play.CommandBlockUpdatePacket; namespace MineSharp.Bot.Plugins; /// -/// World plugin handles all kind of packets regarding the minecraft world, +/// World plugin handles all kind of packets regarding the Minecraft world, /// and provides methods to interact with it, like mining and digging. /// public class WorldPlugin : Plugin @@ -30,27 +32,29 @@ public class WorldPlugin : Plugin /// public WorldPlugin(MineSharpBot bot) : base(bot) { - World = WorldVersion.CreateWorld(Bot.Data); - - Bot.Client.On(HandleChunkDataAndLightUpdatePacket); - Bot.Client.On(HandleUnloadChunkPacket); - Bot.Client.On(HandleBlockUpdatePacket); - Bot.Client.On(HandleMultiBlockUpdatePacket); - Bot.Client.On(HandleChunkBatchStartPacket); - Bot.Client.On(HandleChunkBatchFinishedPacket); + // we want all the packets. Even those that are sent before the plugin is initialized. + OnPacketAfterInitialization(HandleChunkDataAndLightUpdatePacket); + OnPacketAfterInitialization(HandleUnloadChunkPacket); + OnPacketAfterInitialization(HandleBlockUpdatePacket); + OnPacketAfterInitialization(HandleMultiBlockUpdatePacket); + OnPacketAfterInitialization(HandleChunkBatchStartPacket); + OnPacketAfterInitialization(HandleChunkBatchFinishedPacket); } /// /// The world of the Minecraft server /// - public IWorld World { get; } + public IAsyncWorld? World { get; private set; } /// - protected override Task Init() + protected override async Task Init() { playerPlugin = Bot.GetPlugin(); windowPlugin = Bot.GetPlugin(); - return Task.CompletedTask; + + await playerPlugin.WaitForInitialization().WaitAsync(Bot.CancellationToken); + var dimension = playerPlugin.Self!.Dimension; + World = WorldVersion.CreateWorld(Bot.Data, dimension); } /// @@ -73,7 +77,7 @@ public async Task WaitForChunks(int radius = 5) for (var z = chunkCoords.Z - radius; z <= chunkCoords.Z + radius; z++) { var coords = new ChunkCoordinates(x, z); - while (!World.IsChunkLoaded(coords)) + while (!World!.IsChunkLoaded(coords)) { await Task.Delay(10); } @@ -90,14 +94,14 @@ public async Task WaitForChunks(int radius = 5) /// /// /// - public Task UpdateCommandBlock(Position location, string command, int mode, byte flags) + public Task UpdateCommandBlock(Position location, string command, CommandBlockMode mode, CommandBlockFlags flags) { if (playerPlugin?.Self?.GameMode != GameMode.Creative) { throw new("Player must be in creative mode."); } - var packet = new UpdateCommandBlock(location, command, mode, flags); + var packet = new CommandBlockUpdatePacket(location, command, mode, flags); return Bot.Client.SendPacket(packet); } @@ -139,7 +143,7 @@ public async Task MineBlock(Block block, BlockFace? face = null // first packet: Start digging var startPacket = new PlayerActionPacket( // TODO: PlayerActionPacket hardcoded values - (int)PlayerActionStatus.StartedDigging, + PlayerActionStatus.StartDigging, block.Position, face.Value, ++Bot.SequenceId); // Sequence Id is ignored when sending before 1.19 @@ -171,7 +175,7 @@ public async Task MineBlock(Block block, BlockFace? face = null await Task.Delay(time); var finishPacket = new PlayerActionPacket( - (int)PlayerActionStatus.FinishedDigging, + PlayerActionStatus.FinishedDigging, block.Position, face.Value, ++Bot.SequenceId); @@ -189,7 +193,7 @@ public async Task MineBlock(Block block, BlockFace? face = null if (ack.Body is AcknowledgeBlockChangePacket.PacketBody118 p118) { - if ((PlayerActionStatus)p118.Status != PlayerActionStatus.StartedDigging) + if ((PlayerActionStatus)p118.Status != PlayerActionStatus.StartDigging) { return MineBlockStatus.Failed; } @@ -204,7 +208,7 @@ public async Task MineBlock(Block block, BlockFace? face = null catch (TaskCanceledException) { var cancelPacket = new PlayerActionPacket( - (int)PlayerActionStatus.CancelledDigging, + PlayerActionStatus.CancelledDigging, block.Position, face.Value, ++Bot.SequenceId); @@ -227,7 +231,7 @@ public async Task PlaceBlock(Position position, PlayerHand hand = PlayerHand.Mai { // TODO: PlaceBlock: Hardcoded values var packet = new PlaceBlockPacket( - (int)hand, + hand, position, face, 0.5f, @@ -288,7 +292,7 @@ private Task HandleChunkDataAndLightUpdatePacket(ChunkDataAndUpdateLightPacket p } var coords = new ChunkCoordinates(packet.X, packet.Z); - var chunk = World.CreateChunk(coords, packet.BlockEntities); + var chunk = World!.CreateChunk(coords, packet.BlockEntities); chunk.LoadData(packet.ChunkData); World!.LoadChunk(chunk); @@ -319,38 +323,22 @@ private Task HandleBlockUpdatePacket(BlockUpdatePacket packet) var blockInfo = Bot.Data.Blocks.ByState(packet.StateId)!; var block = new Block(blockInfo, packet.StateId, packet.Location); - World!.SetBlock(block); - return Task.CompletedTask; + return World!.SetBlockAsync(block); } - private Task HandleMultiBlockUpdatePacket(MultiBlockUpdatePacket packet) + private async Task HandleMultiBlockUpdatePacket(MultiBlockUpdatePacket packet) { if (!IsEnabled) { - return Task.CompletedTask; - } - - var sectionX = packet.ChunkSection >> 42; - var sectionY = (packet.ChunkSection << 44) >> 44; - var sectionZ = (packet.ChunkSection << 22) >> 42; - - if (sectionX > Math.Pow(2, 21)) - { - sectionX -= (int)Math.Pow(2, 22); - } - - if (sectionY > Math.Pow(2, 19)) - { - sectionY -= (int)Math.Pow(2, 20); + return; } - if (sectionZ > Math.Pow(2, 21)) - { - sectionZ -= (int)Math.Pow(2, 22); - } + var sectionX = packet.ChunkSection >> (64 - 22); // first 22 bits + var sectionZ = (packet.ChunkSection << 22) >> (64 - 22); // next 22 bits + var sectionY = (packet.ChunkSection << (22 + 22)) >> (64 - 20); // last 20 bits var coords = new ChunkCoordinates((int)sectionX, (int)sectionZ); - var chunk = World!.GetChunkAt(coords); + var chunk = await World!.GetChunkAtAsync(coords); foreach (var l in packet.Blocks) { @@ -361,8 +349,6 @@ private Task HandleMultiBlockUpdatePacket(MultiBlockUpdatePacket packet) chunk.SetBlockAt(stateId, new(blockX, blockY, blockZ)); } - - return Task.CompletedTask; } private Task HandleChunkBatchStartPacket(ChunkBatchStartPacket packet) diff --git a/MineSharp.Bot/Utils/LoggingHelper.cs b/MineSharp.Bot/Utils/LoggingHelper.cs new file mode 100644 index 00000000..5b953a03 --- /dev/null +++ b/MineSharp.Bot/Utils/LoggingHelper.cs @@ -0,0 +1,27 @@ +using NLog; + +namespace MineSharp.Bot.Utils; + +public static class LoggingHelper +{ + /// + /// Enable debug logs of minesharp to be written to the console and a log file. + /// Trace logs are only written to the logfile. + /// + /// if true, also log trace messages. + public static void EnableDebugLogs(bool trace = false) + { + var configuration = new NLog.Config.LoggingConfiguration(); + + var logfile = new NLog.Targets.FileTarget("customfile") { FileName = $"logs/{DateTime.Now:dd.MM.yyyy hh:mm:ss}.log" }; + var latestlog = new NLog.Targets.FileTarget("latestfile") { FileName = "logs/latest.log", DeleteOldFileOnStartup = true }; + var logconsole = new NLog.Targets.ConsoleTarget("logconsole"); + + var level = trace ? LogLevel.Trace : LogLevel.Debug; + configuration.AddRule(level, LogLevel.Fatal, logfile); + configuration.AddRule(level, LogLevel.Fatal, latestlog); + configuration.AddRule(LogLevel.Debug, LogLevel.Fatal, logconsole); + + LogManager.Configuration = configuration; + } +} diff --git a/MineSharp.Core/Common/Biomes/BiomeCategory.cs b/MineSharp.Core/Common/Biomes/BiomeCategory.cs index 93303664..5d2c8a18 100644 --- a/MineSharp.Core/Common/Biomes/BiomeCategory.cs +++ b/MineSharp.Core/Common/Biomes/BiomeCategory.cs @@ -1,4 +1,4 @@ -/////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////// // This File is generated by MineSharp.SourceGenerator and should not be modified. // /////////////////////////////////////////////////////////////////////////////////////////// @@ -26,7 +26,7 @@ public enum BiomeCategory Mushroom = 15, Underground = 16, Nether = 17, - TheEnd = 18 + TheEnd = 18, } #pragma warning restore CS1591 diff --git a/MineSharp.Core/Common/Biomes/BiomeType.cs b/MineSharp.Core/Common/Biomes/BiomeType.cs index 2a14d0e0..68ea8bf4 100644 --- a/MineSharp.Core/Common/Biomes/BiomeType.cs +++ b/MineSharp.Core/Common/Biomes/BiomeType.cs @@ -1,4 +1,4 @@ -/////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////// // This File is generated by MineSharp.SourceGenerator and should not be modified. // /////////////////////////////////////////////////////////////////////////////////////////// @@ -71,7 +71,7 @@ public enum BiomeType EndBarrens = 60, MangroveSwamp = 61, DeepDark = 62, - CherryGrove = 63 + CherryGrove = 63, } #pragma warning restore CS1591 diff --git a/MineSharp.Core/Common/BitSet.cs b/MineSharp.Core/Common/BitSet.cs new file mode 100644 index 00000000..634cea42 --- /dev/null +++ b/MineSharp.Core/Common/BitSet.cs @@ -0,0 +1,1026 @@ +/* +This file is a port of the BitSet class from Java 7. +The original Java code is available at: https://github.com/openjdk-mirror/jdk7u-jdk/blob/f4d80957e89a19a29bb9f9807d2a28351ed7f7df/src/share/classes/java/util/BitSet.java +The port was made with assistance from GitHub Copilot Chat. +*/ + +using System.Numerics; +using System.Runtime.InteropServices; +using System.Text; + +namespace MineSharp.Core.Common; + +// TODO: Unit tests would be great to see whether the ported code works as expected. + +/// +/// This class implements a vector of bits that grows as needed. Each +/// component of the bit set has a value. The +/// bits of a are indexed by nonnegative integers. +/// Individual indexed bits can be examined, set, or cleared. One +/// may be used to modify the contents of another +/// through logical AND, logical inclusive OR, and +/// logical exclusive OR operations. +/// +/// +/// By default, all bits in the set initially have the value +/// false. +/// Every bit set has a current size, which is the number of bits +/// of space currently in use by the bit set. Note that the size is +/// related to the implementation of a bit set, so it may change with +/// implementation. The length of a bit set relates to logical length +/// of a bit set and is defined independently of implementation. +/// Unless otherwise noted, passing a null parameter to any of the +/// methods in a will result in an +/// ArgumentNullException. +/// +/// A is not safe for multithreaded use without +/// external synchronization. +/// +public sealed class BitSet : IEquatable, ICloneable +{ + /// + /// BitSets are packed into arrays of "words." Currently a word is + /// a long, which consists of 64 bits, requiring 6 address bits. + /// The choice of word size is determined purely by performance concerns. + /// + private const int ADDRESS_BITS_PER_WORD = 6; + private const int BITS_PER_WORD = 1 << ADDRESS_BITS_PER_WORD; + private const int BIT_INDEX_MASK = BITS_PER_WORD - 1; + /// + /// Used to shift left or right for a partial word mask. + /// + private const ulong WORD_MASK = 0xffffffffffffffffUL; + + /// + /// The internal field corresponding to the serialField "bits". + /// + private ulong[] words; + /// + /// The number of words in the logical size of this BitSet. + /// + private int wordsInUse = 0; + /// + /// Whether the size of "words" is user-specified. If so, we assume + /// the user knows what he's doing and try harder to preserve it. + /// + private bool sizeIsSticky = false; + + + /// + /// Given a bit index, return word index containing it. + /// + private static int WordIndex(int bitIndex) => bitIndex >> ADDRESS_BITS_PER_WORD; + + /// + /// Every public method must preserve these invariants. + /// + private void CheckInvariants() + { + // for performance reasons, we only check these in debug mode +#if DEBUG + if (wordsInUse > 0) + { + if (words[wordsInUse - 1] == 0) throw new InvalidOperationException(); + } + if (wordsInUse < 0 || wordsInUse > words.Length) throw new InvalidOperationException(); + if (wordsInUse != words.Length && words[wordsInUse] != 0) throw new InvalidOperationException(); +#endif + } + + /// + /// Sets the field wordsInUse to the logical size in words of the bit set. + /// WARNING: This method assumes that the number of words actually in use is + /// less than or equal to the current value of wordsInUse! + /// + private void RecalculateWordsInUse() + { + var lastUsedIndex = words.AsSpan(0, wordsInUse).LastIndexOfAnyExcept(0ul); + wordsInUse = lastUsedIndex + 1; + } + + private void InitWords(int nbits) + { + words = new ulong[WordIndex(nbits - 1) + 1]; + } + + #region Constructors + +#pragma warning disable CS8618 // words in initialized in InitWords + /// + /// Creates a new bit set. All bits are initially false. + /// + public BitSet() + { + InitWords(BITS_PER_WORD); + sizeIsSticky = false; + } + + /// + /// Creates a bit set whose initial size is large enough to explicitly + /// represent bits with indices in the range 0 through + /// nbits-1. All bits are initially false. + /// + /// The initial size of the bit set. + /// If the specified initial size is negative. + public BitSet(int nbits) + { + if (nbits < 0) throw new ArgumentOutOfRangeException(nameof(nbits), "nbits < 0"); + InitWords(nbits); + sizeIsSticky = true; + } + + /// + /// Creates a bit set using words as the internal representation. + /// The last word (if there is one) must be non-zero. + /// + private BitSet(ulong[] words) + { + this.words = words; + this.wordsInUse = words.Length; + CheckInvariants(); + } +#pragma warning restore CS8618 + + #endregion + + /// + /// Ensures that the BitSet can hold enough words. + /// + /// The minimum acceptable number of words. + private void EnsureCapacity(int wordsRequired) + { + if (words.Length < wordsRequired) + { + int request = Math.Max(2 * words.Length, wordsRequired); + Array.Resize(ref words, request); + sizeIsSticky = false; + } + } + + /// + /// Ensures that the BitSet can accommodate a given wordIndex, + /// temporarily violating the invariants. The caller must + /// restore the invariants before returning to the user, + /// possibly using . + /// + /// The index to be accommodated. + private void ExpandTo(int wordIndex) + { + int wordsRequired = wordIndex + 1; + if (wordsInUse < wordsRequired) + { + EnsureCapacity(wordsRequired); + wordsInUse = wordsRequired; + } + } + + /// + /// Checks that fromIndex ... toIndex is a valid range of bit indices. + /// + private static void CheckRange(int fromIndex, int toIndex) + { + if (fromIndex < 0) throw new ArgumentOutOfRangeException(nameof(fromIndex), "fromIndex < 0"); + if (toIndex < 0) throw new ArgumentOutOfRangeException(nameof(toIndex), "toIndex < 0"); + if (fromIndex > toIndex) throw new ArgumentOutOfRangeException(nameof(fromIndex), $"fromIndex: {fromIndex} > toIndex: {toIndex}"); + } + + /// + /// Sets all of the bits in this BitSet to false. + /// + public void Clear() + { + words.AsSpan(0, wordsInUse).Clear(); + } + + #region Single Bit Operations + + /// + /// Sets the bit at the specified index to the complement of its + /// current value. + /// + /// The index of the bit to flip. + /// If the specified index is negative. + public void Flip(int bitIndex) + { + if (bitIndex < 0) + throw new ArgumentOutOfRangeException(nameof(bitIndex), "bitIndex < 0"); + + int wordIndex = WordIndex(bitIndex); + ExpandTo(wordIndex); + + words[wordIndex] ^= (1UL << bitIndex); + + RecalculateWordsInUse(); + CheckInvariants(); + } + + /// + /// Sets the bit at the specified index to true. + /// + /// A bit index. + /// If the specified index is negative. + public void Set(int bitIndex) + { + if (bitIndex < 0) + throw new ArgumentOutOfRangeException(nameof(bitIndex), "bitIndex < 0"); + + int wordIndex = WordIndex(bitIndex); + ExpandTo(wordIndex); + + words[wordIndex] |= (1UL << bitIndex); // Restores invariants + + CheckInvariants(); + } + + /// + /// Sets the bit at the specified index to the specified value. + /// + /// A bit index. + /// A boolean value to set. + /// If the specified index is negative. + public void Set(int bitIndex, bool value) + { + if (value) + Set(bitIndex); + else + Clear(bitIndex); + } + + /// + /// Sets the bit specified by the index to false. + /// + /// The index of the bit to be cleared. + /// If the specified index is negative. + public void Clear(int bitIndex) + { + if (bitIndex < 0) + throw new ArgumentOutOfRangeException(nameof(bitIndex), "bitIndex < 0"); + + int wordIndex = WordIndex(bitIndex); + if (wordIndex >= wordsInUse) + return; + + words[wordIndex] &= ~(1UL << bitIndex); + + RecalculateWordsInUse(); + CheckInvariants(); + } + + /// + /// Returns the index of the first bit that is set to true + /// that occurs on or after the specified starting index. If no such + /// bit exists then -1 is returned. + /// + /// The index to start checking from (inclusive). + /// The index of the next set bit, or -1 if there is no such bit. + /// If the specified index is negative. + public int NextSetBit(int fromIndex) + { + if (fromIndex < 0) + throw new ArgumentOutOfRangeException(nameof(fromIndex), "fromIndex < 0"); + + CheckInvariants(); + + int u = WordIndex(fromIndex); + if (u >= wordsInUse) + return -1; + + ulong word = words[u] & (WORD_MASK << fromIndex); + + + // TODO: Use Span + while (true) + { + if (word != 0) + return (u * BITS_PER_WORD) + BitOperations.TrailingZeroCount(word); + if (++u == wordsInUse) + return -1; + word = words[u]; + } + } + + /// + /// Returns the index of the first bit that is set to false + /// that occurs on or after the specified starting index. + /// + /// The index to start checking from (inclusive). + /// The index of the next clear bit. + /// If the specified index is negative. + public int NextClearBit(int fromIndex) + { + if (fromIndex < 0) + throw new ArgumentOutOfRangeException(nameof(fromIndex), "fromIndex < 0"); + + CheckInvariants(); + + int u = WordIndex(fromIndex); + if (u >= wordsInUse) + return fromIndex; + + ulong word = ~words[u] & (WORD_MASK << fromIndex); + + while (true) + { + if (word != 0) + return (u * BITS_PER_WORD) + BitOperations.TrailingZeroCount(word); + if (++u == wordsInUse) + return wordsInUse * BITS_PER_WORD; + word = ~words[u]; + } + } + + /// + /// Returns the index of the nearest bit that is set to true + /// that occurs on or before the specified starting index. + /// If no such bit exists, or if -1 is given as the + /// starting index, then -1 is returned. + /// + /// The index to start checking from (inclusive). + /// The index of the previous set bit, or -1 if there is no such bit. + /// If the specified index is less than -1. + public int PreviousSetBit(int fromIndex) + { + if (fromIndex < 0) + { + if (fromIndex == -1) + return -1; + throw new ArgumentOutOfRangeException(nameof(fromIndex), "fromIndex < -1"); + } + + CheckInvariants(); + + int u = WordIndex(fromIndex); + if (u >= wordsInUse) + return Length() - 1; + + ulong word = words[u] & (WORD_MASK >> -(fromIndex + 1)); + + while (true) + { + if (word != 0) + return (u + 1) * BITS_PER_WORD - 1 - BitOperations.LeadingZeroCount(word); + if (u-- == 0) + return -1; + word = words[u]; + } + } + + /// + /// Returns the index of the nearest bit that is set to false + /// that occurs on or before the specified starting index. + /// If no such bit exists, or if -1 is given as the + /// starting index, then -1 is returned. + /// + /// The index to start checking from (inclusive). + /// The index of the previous clear bit, or -1 if there is no such bit. + /// If the specified index is less than -1. + public int PreviousClearBit(int fromIndex) + { + if (fromIndex < 0) + { + if (fromIndex == -1) + return -1; + throw new ArgumentOutOfRangeException(nameof(fromIndex), "fromIndex < -1"); + } + + CheckInvariants(); + + int u = WordIndex(fromIndex); + if (u >= wordsInUse) + return fromIndex; + + ulong word = ~words[u] & (WORD_MASK >> -(fromIndex + 1)); + + while (true) + { + if (word != 0) + return (u + 1) * BITS_PER_WORD - 1 - BitOperations.LeadingZeroCount(word); + if (u-- == 0) + return -1; + word = ~words[u]; + } + } + + /// + /// Returns the value of the bit with the specified index. The value + /// is true if the bit with the index bitIndex + /// is currently set in this BitSet; otherwise, the result + /// is false. + /// + /// The bit index. + /// The value of the bit with the specified index. + /// If the specified index is negative. + public bool Get(int bitIndex) + { + if (bitIndex < 0) throw new ArgumentOutOfRangeException(nameof(bitIndex), "bitIndex < 0"); + + CheckInvariants(); + + int wordIndex = WordIndex(bitIndex); + return (wordIndex < wordsInUse) && ((words[wordIndex] & (1UL << bitIndex)) != 0); + } + + #endregion + + #region BitSet Operations + + + /// + /// Returns a new composed of bits from this + /// from (inclusive) to (exclusive). + /// + /// Index of the first bit to include. + /// Index after the last bit to include. + /// A new from a range of this . + /// If is negative, + /// or is negative, or is + /// larger than . + public BitSet Get(int fromIndex, int toIndex) + { + CheckRange(fromIndex, toIndex); + CheckInvariants(); + + int len = Length(); + + // If no set bits in range return empty bitset + if (len <= fromIndex || fromIndex == toIndex) + return new BitSet(0); + + // An optimization + if (toIndex > len) + toIndex = len; + + BitSet result = new BitSet(toIndex - fromIndex); + int targetWords = WordIndex(toIndex - fromIndex - 1) + 1; + int sourceIndex = WordIndex(fromIndex); + bool wordAligned = ((fromIndex & BIT_INDEX_MASK) == 0); + + // Process all words but the last word + for (int i = 0; i < targetWords - 1; i++, sourceIndex++) + result.words[i] = wordAligned ? words[sourceIndex] : + (words[sourceIndex] >>> fromIndex) | + (words[sourceIndex + 1] << -fromIndex); + + // Process the last word + ulong lastWordMask = WORD_MASK >>> -toIndex; + result.words[targetWords - 1] = + ((toIndex - 1) & BIT_INDEX_MASK) < (fromIndex & BIT_INDEX_MASK) + ? /* straddles source words */ + ((words[sourceIndex] >>> fromIndex) | + (words[sourceIndex + 1] & lastWordMask) << -fromIndex) + : + ((words[sourceIndex] & lastWordMask) >>> fromIndex); + + // Set wordsInUse correctly + result.wordsInUse = targetWords; + result.RecalculateWordsInUse(); + result.CheckInvariants(); + + return result; + } + + /// + /// Sets the bits from the specified (inclusive) to the + /// specified (exclusive) to true. + /// + /// Index of the first bit to be set. + /// Index after the last bit to be set. + /// If is negative, + /// or is negative, or is + /// larger than . + public void Set(int fromIndex, int toIndex) + { + CheckRange(fromIndex, toIndex); + + if (fromIndex == toIndex) + return; + + // Increase capacity if necessary + int startWordIndex = WordIndex(fromIndex); + int endWordIndex = WordIndex(toIndex - 1); + ExpandTo(endWordIndex); + + ulong firstWordMask = WORD_MASK << fromIndex; + ulong lastWordMask = WORD_MASK >>> -toIndex; + if (startWordIndex == endWordIndex) + { + // Case 1: One word + words[startWordIndex] |= (firstWordMask & lastWordMask); + } + else + { + // Case 2: Multiple words + // Handle first word + words[startWordIndex] |= firstWordMask; + + // Handle intermediate words, if any + for (int i = startWordIndex + 1; i < endWordIndex; i++) + words[i] = WORD_MASK; + + // Handle last word (restores invariants) + words[endWordIndex] |= lastWordMask; + } + + CheckInvariants(); + } + + /// + /// Sets the bits from the specified (inclusive) to the + /// specified (exclusive) to the specified value. + /// + /// Index of the first bit to be set. + /// Index after the last bit to be set. + /// Value to set the selected bits to. + /// If is negative, + /// or is negative, or is + /// larger than . + public void Set(int fromIndex, int toIndex, bool value) + { + if (value) + Set(fromIndex, toIndex); + else + Clear(fromIndex, toIndex); + } + + /// + /// Sets the bits from the specified (inclusive) to the + /// specified (exclusive) to false. + /// + /// Index of the first bit to be cleared. + /// Index after the last bit to be cleared. + /// If is negative, + /// or is negative, or is + /// larger than . + public void Clear(int fromIndex, int toIndex) + { + CheckRange(fromIndex, toIndex); + + if (fromIndex == toIndex) + return; + + int startWordIndex = WordIndex(fromIndex); + if (startWordIndex >= wordsInUse) + return; + + int endWordIndex = WordIndex(toIndex - 1); + if (endWordIndex >= wordsInUse) + { + toIndex = Length(); + endWordIndex = wordsInUse - 1; + } + + ulong firstWordMask = WORD_MASK << fromIndex; + ulong lastWordMask = WORD_MASK >>> -toIndex; + if (startWordIndex == endWordIndex) + { + // Case 1: One word + words[startWordIndex] &= ~(firstWordMask & lastWordMask); + } + else + { + // Case 2: Multiple words + // Handle first word + words[startWordIndex] &= ~firstWordMask; + + // Handle intermediate words, if any + for (int i = startWordIndex + 1; i < endWordIndex; i++) + words[i] = 0; + + // Handle last word + words[endWordIndex] &= ~lastWordMask; + } + + RecalculateWordsInUse(); + CheckInvariants(); + } + + /// + /// Sets each bit from the specified (inclusive) to the + /// specified (exclusive) to the complement of its current + /// value. + /// + /// Index of the first bit to flip. + /// Index after the last bit to flip. + /// If is negative, + /// or is negative, or is + /// larger than . + public void Flip(int fromIndex, int toIndex) + { + CheckRange(fromIndex, toIndex); + + if (fromIndex == toIndex) + return; + + int startWordIndex = WordIndex(fromIndex); + int endWordIndex = WordIndex(toIndex - 1); + ExpandTo(endWordIndex); + + ulong firstWordMask = WORD_MASK << fromIndex; + ulong lastWordMask = WORD_MASK >>> -toIndex; + if (startWordIndex == endWordIndex) + { + // Case 1: One word + words[startWordIndex] ^= (firstWordMask & lastWordMask); + } + else + { + // Case 2: Multiple words + // Handle first word + words[startWordIndex] ^= firstWordMask; + + // Handle intermediate words, if any + for (int i = startWordIndex + 1; i < endWordIndex; i++) + words[i] ^= WORD_MASK; + + // Handle last word + words[endWordIndex] ^= lastWordMask; + } + + RecalculateWordsInUse(); + CheckInvariants(); + } + + #endregion + + #region Serialization + + /// + /// Returns a new bit set containing all the bits in the given ulong array. + /// + /// A ulong array containing a little-endian representation + /// of a sequence of bits to be used as the initial bits of the new bit set. + /// A new containing all the bits in the given ulong array. + public static BitSet Create(ReadOnlySpan longs) + { + if (longs.IsEmpty) + return new BitSet(); + + var lastUsedIndex = longs.LastIndexOfAnyExcept(0ul); + var newWords = longs.Slice(0, lastUsedIndex + 1).ToArray(); + return new BitSet(newWords); + } + + /// + /// Returns a new bit set containing all the bits in the given byte array. + /// + /// A byte array containing a little-endian representation + /// of a sequence of bits to be used as the initial bits of the new bit set. + /// A new containing all the bits in the given byte array. + public static BitSet Create(ReadOnlySpan bytes) + { + if (bytes.Length % 8 != 0) + throw new ArgumentException("byte array length must be a multiple of 8", nameof(bytes)); + return Create(MemoryMarshal.Cast(bytes)); + } + + /// + /// Returns a new byte array containing all the bits in this bit set. + /// + /// A byte array containing a little-endian representation + /// of all the bits in this bit set. + public byte[] ToByteArray() + { + var bytes = new byte[wordsInUse * 8]; + WriteTo(MemoryMarshal.Cast(bytes)); + return bytes; + } + + /// + /// Returns a new long array containing all the bits in this bit set. + /// + /// A long array containing a little-endian representation + /// of all the bits in this bit set. + public ulong[] ToLongArray() + { + var newWords = words.AsSpan(0, wordsInUse).ToArray(); + return newWords; + } + + /// + /// Writes the bits in this bit set to the specified span of ulongs. + /// + /// A span of ulongs to write the bits to. + /// Thrown if the length of the span is less than the number of words in use. + public void WriteTo(Span longs) + { + if (longs.Length < wordsInUse) + throw new ArgumentException("byte array length is too small", nameof(longs)); + + words.AsSpan(0, wordsInUse).CopyTo(longs); + } + + #endregion + + #region Logical BitSet Operations + + /// + /// Returns true if the specified has any bits set to + /// true that are also set to true in this . + /// + /// to intersect with. + /// Boolean indicating whether this intersects + /// the specified . + public bool Intersects(BitSet set) + { + // TODO: Optimize using Vector + for (int i = Math.Min(wordsInUse, set.wordsInUse) - 1; i >= 0; i--) + if ((words[i] & set.words[i]) != 0) + return true; + return false; + } + + /// + /// Returns the number of bits set to true in this . + /// + /// The number of bits set to true in this . + public int Cardinality() + { + // TODO: Optimize using Vector + int sum = 0; + for (int i = 0; i < wordsInUse; i++) + sum += BitOperations.PopCount(words[i]); + return sum; + } + + /// + /// Performs a logical AND of this target bit set with the + /// argument bit set. This bit set is modified so that each bit in it + /// has the value true if and only if it both initially + /// had the value true and the corresponding bit in the + /// bit set argument also had the value true. + /// + /// A bit set. + public void And(BitSet set) + { + if (this == set) + return; + + words.AsSpan(set.wordsInUse, wordsInUse - set.wordsInUse).Clear(); + + // TODO: Optimize using Vector + // Perform logical AND on words in common + for (int i = 0; i < wordsInUse; i++) + words[i] &= set.words[i]; + + RecalculateWordsInUse(); + CheckInvariants(); + } + + /// + /// Performs a logical OR of this bit set with the bit set + /// argument. This bit set is modified so that a bit in it has the + /// value true if and only if it either already had the + /// value true or the corresponding bit in the bit set + /// argument has the value true. + /// + /// A bit set. + public void Or(BitSet set) + { + if (this == set) + return; + + int wordsInCommon = Math.Min(wordsInUse, set.wordsInUse); + + if (wordsInUse < set.wordsInUse) + { + EnsureCapacity(set.wordsInUse); + wordsInUse = set.wordsInUse; + } + + // TODO: Optimize using Vector + // Perform logical OR on words in common + for (int i = 0; i < wordsInCommon; i++) + words[i] |= set.words[i]; + + // Copy any remaining words + if (wordsInCommon < set.wordsInUse) + Array.Copy(set.words, wordsInCommon, words, wordsInCommon, set.wordsInUse - wordsInCommon); + + // RecalculateWordsInUse() is unnecessary + CheckInvariants(); + } + + /// + /// Performs a logical XOR of this bit set with the bit set + /// argument. This bit set is modified so that a bit in it has the + /// value true if and only if one of the following + /// statements holds: + /// + /// The bit initially has the value true, and the + /// corresponding bit in the argument has the value false. + /// The bit initially has the value false, and the + /// corresponding bit in the argument has the value true. + /// + /// + /// A bit set. + public void Xor(BitSet set) + { + int wordsInCommon = Math.Min(wordsInUse, set.wordsInUse); + + if (wordsInUse < set.wordsInUse) + { + EnsureCapacity(set.wordsInUse); + wordsInUse = set.wordsInUse; + } + + // TODO: Optimize using Vector + // Perform logical XOR on words in common + for (int i = 0; i < wordsInCommon; i++) + words[i] ^= set.words[i]; + + // Copy any remaining words + if (wordsInCommon < set.wordsInUse) + Array.Copy(set.words, wordsInCommon, words, wordsInCommon, set.wordsInUse - wordsInCommon); + + RecalculateWordsInUse(); + CheckInvariants(); + } + + /// + /// Clears all of the bits in this whose corresponding + /// bit is set in the specified . + /// + /// The with which to mask this + /// . + public void AndNot(BitSet set) + { + // TODO: Optimize using Vector + // Perform logical (a & !b) on words in common + for (int i = Math.Min(wordsInUse, set.wordsInUse) - 1; i >= 0; i--) + words[i] &= ~set.words[i]; + + RecalculateWordsInUse(); + CheckInvariants(); + } + + #endregion + + #region HashCode, Equals, Clone and ToString + + /// + /// Returns the hash code value for this bit set. The hash code depends + /// only on which bits are set within this . + /// + /// The hash code value for this bit set. + public override int GetHashCode() + { + ulong h = 1234; + // TODO: Optimize using Vector + for (int i = wordsInUse; --i >= 0;) + h ^= words[i] * (ulong)(i + 1); + + return (int)((h >> 32) ^ h); + } + + /// + /// + /// + /// + /// + public bool Equals(BitSet? other) + { + if (other == null) + return false; + if (ReferenceEquals(this, other)) + return true; + + CheckInvariants(); + other.CheckInvariants(); + + if (wordsInUse != other.wordsInUse) + return false; + + // Check words in use by both BitSets + return words.AsSpan(0, wordsInUse).SequenceEqual(other.words.AsSpan(0, wordsInUse)); + } + + /// + /// Compares this object against the specified object. + /// The result is true if and only if the argument is + /// not null and is a object that has + /// exactly the same set of bits set to true as this bit + /// set. + /// + /// The object to compare with. + /// true if the objects are the same; false otherwise. + public override bool Equals(object? obj) + { + if (ReferenceEquals(this, obj)) + return true; + if (obj is not BitSet other) + return false; + + return Equals(other); + } + + /// + /// Cloning this produces a new + /// that is equal to it. + /// The clone of the bit set is another bit set that has exactly the + /// same bits set to true as this bit set. + /// + /// A clone of this bit set. + public BitSet CloneTyped() + { + if (!sizeIsSticky) + TrimToSize(); + + try + { + BitSet result = (BitSet)MemberwiseClone(); + result.words = (ulong[])words.Clone(); + result.CheckInvariants(); + return result; + } + catch (Exception e) + { + throw new InvalidOperationException("Clone not supported", e); + } + } + + /// + /// + /// + public object Clone() + { + return CloneTyped(); + } + + /// + /// Returns a string representation of this bit set. For every index + /// for which this contains a bit in the set + /// state, the decimal representation of that index is included in + /// the result. Such indices are listed in order from lowest to + /// highest, separated by ", " (a comma and a space) and + /// surrounded by braces, resulting in the usual mathematical + /// notation for a set of integers. + /// + /// A string representation of this bit set. + public override string ToString() + { + CheckInvariants(); + + int numBits = (wordsInUse > 128) ? Cardinality() : wordsInUse * BITS_PER_WORD; + StringBuilder b = new StringBuilder(6 * numBits + 2); + b.Append('{'); + + int i = NextSetBit(0); + if (i != -1) + { + b.Append(i); + for (i = NextSetBit(i + 1); i >= 0; i = NextSetBit(i + 1)) + { + int endOfRun = NextClearBit(i); + do { b.Append(", ").Append(i); } + while (++i < endOfRun); + } + } + + b.Append('}'); + return b.ToString(); + } + + #endregion + + /// + /// Returns the number of bits of space actually in use by this + /// BitSet to represent bit values. + /// The maximum element in the set is the size - 1st element. + /// + /// The number of bits currently in this bit set. + public int Size() => words.Length * BITS_PER_WORD; + + /// + /// Attempts to reduce internal storage used for the bits in this bit set. + /// Calling this method may, but is not required to, affect the value + /// returned by a subsequent call to the method. + /// + private void TrimToSize() + { + if (wordsInUse != words.Length) + { + Array.Resize(ref words, wordsInUse); + CheckInvariants(); + } + } + + /// + /// Returns the "logical size" of this BitSet: the index of + /// the highest set bit in the BitSet plus one. Returns zero + /// if the BitSet contains no set bits. + /// + /// The logical size of this BitSet. + public int Length() + { + if (wordsInUse == 0) return 0; + + return BITS_PER_WORD * (wordsInUse - 1) + (BITS_PER_WORD - BitOperations.LeadingZeroCount(words[wordsInUse - 1])); + } + + /// + /// Returns true if this BitSet contains no bits that are set + /// to true. + /// + /// Boolean indicating whether this BitSet is empty. + public bool IsEmpty() => wordsInUse == 0; +} diff --git a/MineSharp.Core/Common/Blocks/BlockType.cs b/MineSharp.Core/Common/Blocks/BlockType.cs index 489b6dd2..a2bc54b5 100644 --- a/MineSharp.Core/Common/Blocks/BlockType.cs +++ b/MineSharp.Core/Common/Blocks/BlockType.cs @@ -1,4 +1,4 @@ -/////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////// // This File is generated by MineSharp.SourceGenerator and should not be modified. // /////////////////////////////////////////////////////////////////////////////////////////// @@ -1068,7 +1068,7 @@ public enum BlockType Crafter = 1057, TrialSpawner = 1058, Vault = 1059, - HeavyCore = 1060 + HeavyCore = 1060, } #pragma warning restore CS1591 diff --git a/MineSharp.Core/Common/Direction.cs b/MineSharp.Core/Common/Direction.cs new file mode 100644 index 00000000..e4c9d6e7 --- /dev/null +++ b/MineSharp.Core/Common/Direction.cs @@ -0,0 +1,16 @@ +namespace MineSharp.Core.Common; + +/// +/// Enum representing the directions. +/// +public enum Direction +{ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + Down = 0, + Up = 1, + North = 2, + South = 3, + West = 4, + East = 5 +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} diff --git a/MineSharp.Core/Common/Effects/EffectType.cs b/MineSharp.Core/Common/Effects/EffectType.cs index 8f613ecd..b2cc051b 100644 --- a/MineSharp.Core/Common/Effects/EffectType.cs +++ b/MineSharp.Core/Common/Effects/EffectType.cs @@ -1,4 +1,4 @@ -/////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////// // This File is generated by MineSharp.SourceGenerator and should not be modified. // /////////////////////////////////////////////////////////////////////////////////////////// @@ -47,7 +47,7 @@ public enum EffectType WindCharged = 36, Weaving = 37, Oozing = 38, - Infested = 39 + Infested = 39, } #pragma warning restore CS1591 diff --git a/MineSharp.Core/Common/Enchantments/EnchantmentCategory.cs b/MineSharp.Core/Common/Enchantments/EnchantmentCategory.cs index 6a2cb6f6..6ca8343c 100644 --- a/MineSharp.Core/Common/Enchantments/EnchantmentCategory.cs +++ b/MineSharp.Core/Common/Enchantments/EnchantmentCategory.cs @@ -1,4 +1,4 @@ -/////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////// // This File is generated by MineSharp.SourceGenerator and should not be modified. // /////////////////////////////////////////////////////////////////////////////////////////// @@ -21,7 +21,7 @@ public enum EnchantmentCategory Trident = 10, Crossbow = 11, Vanishable = 12, - ArmorLegs = 13 + ArmorLegs = 13, } #pragma warning restore CS1591 diff --git a/MineSharp.Core/Common/Enchantments/EnchantmentType.cs b/MineSharp.Core/Common/Enchantments/EnchantmentType.cs index 790902fe..e53e1bb3 100644 --- a/MineSharp.Core/Common/Enchantments/EnchantmentType.cs +++ b/MineSharp.Core/Common/Enchantments/EnchantmentType.cs @@ -1,4 +1,4 @@ -/////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////// // This File is generated by MineSharp.SourceGenerator and should not be modified. // /////////////////////////////////////////////////////////////////////////////////////////// @@ -50,7 +50,7 @@ public enum EnchantmentType SweepingEdge = 39, Density = 40, Breach = 41, - WindBurst = 42 + WindBurst = 42, } #pragma warning restore CS1591 diff --git a/MineSharp.Core/Common/Entities/Attributes/Attribute.cs b/MineSharp.Core/Common/Entities/Attributes/Attribute.cs index ae1b0093..d9c02492 100644 --- a/MineSharp.Core/Common/Entities/Attributes/Attribute.cs +++ b/MineSharp.Core/Common/Entities/Attributes/Attribute.cs @@ -1,19 +1,20 @@ using System.Diagnostics; +using MineSharp.Core.Serialization; namespace MineSharp.Core.Common.Entities.Attributes; /// /// Entity Attribute /// -public class Attribute +public sealed record Attribute : ISerializable { /// /// Create a new Attribute /// - /// - /// - /// - public Attribute(string key, double @base, Modifier[] modifiers) + /// The name of this Attribute + /// The base value of this attribute + /// The modifiers active for this attribute. Indexed by their UUID + public Attribute(Identifier key, double @base, Modifier[] modifiers) { Key = key; Base = @base; @@ -23,17 +24,17 @@ public Attribute(string key, double @base, Modifier[] modifiers) /// /// The name of this Attribute /// - public string Key { get; } + public Identifier Key { get; init; } /// /// The base value of this attribute /// - public double Base { get; } + public double Base { get; init; } /// /// The modifiers active for this attribute. Indexed by their UUID /// - public Dictionary Modifiers { get; } + public Dictionary Modifiers { get; } // must never be set from outside because we do not want uncontrollable Dictionary changes /// /// Calculate the Multiplier of this attribute with all modifiers. @@ -74,4 +75,22 @@ public void RemoveModifier(Uuid uuid) { Modifiers.Remove(uuid); } + + /// + public void Write(PacketBuffer buffer) + { + buffer.WriteIdentifier(Key); + buffer.WriteDouble(Value); + buffer.WriteVarIntArray(Modifiers.Values, (buffer, modifier) => modifier.Write(buffer)); + } + + /// + public static Attribute Read(PacketBuffer buffer) + { + var key = buffer.ReadIdentifier(); + var value = buffer.ReadDouble(); + var modifiers = buffer.ReadVarIntArray(Modifier.Read); + + return new(key, value, modifiers); + } } diff --git a/MineSharp.Core/Common/Entities/Attributes/Modifier.cs b/MineSharp.Core/Common/Entities/Attributes/Modifier.cs index a052ccb0..438be1d6 100644 --- a/MineSharp.Core/Common/Entities/Attributes/Modifier.cs +++ b/MineSharp.Core/Common/Entities/Attributes/Modifier.cs @@ -1,9 +1,30 @@ -namespace MineSharp.Core.Common.Entities.Attributes; +using MineSharp.Core.Serialization; + +namespace MineSharp.Core.Common.Entities.Attributes; /// /// A modifier for an attribute /// -/// The uuid associated with this Modifier. This is a constant value from minecraft java. +/// The uuid associated with this Modifier. This is a constant value from Minecraft java. /// /// -public record Modifier(Uuid Uuid, double Amount, ModifierOp Operation); +public sealed record Modifier(Uuid Uuid, double Amount, ModifierOp Operation) : ISerializable +{ + /// + public void Write(PacketBuffer buffer) + { + buffer.WriteUuid(Uuid); + buffer.WriteDouble(Amount); + buffer.WriteByte((byte)Operation); + } + + /// + public static Modifier Read(PacketBuffer buffer) + { + var uuid = buffer.ReadUuid(); + var amount = buffer.ReadDouble(); + var operation = buffer.ReadByte(); + + return new(uuid, amount, (ModifierOp)operation); + } +} diff --git a/MineSharp.Core/Common/Entities/Entity.cs b/MineSharp.Core/Common/Entities/Entity.cs index 96b7b250..63d3bc5c 100644 --- a/MineSharp.Core/Common/Entities/Entity.cs +++ b/MineSharp.Core/Common/Entities/Entity.cs @@ -74,7 +74,7 @@ public class Entity( /// /// A list of attributes active on this entity /// - public IDictionary Attributes { get; } = new ConcurrentDictionary(); + public ConcurrentDictionary Attributes { get; } = new(); /// /// The entity this entity is riding (f.e. an boat or a minecart) @@ -87,7 +87,7 @@ public class Entity( /// /// /// - public Attribute? GetAttribute(string name) + public Attribute? GetAttribute(Identifier name) { Attributes.TryGetValue(name, out var attr); return attr; @@ -99,7 +99,7 @@ public class Entity( /// public void AddAttribute(Attribute attribute) { - Attributes.Add(attribute.Key, attribute); + Attributes.TryAdd(attribute.Key, attribute); } /// diff --git a/MineSharp.Core/Common/Entities/EntityCategory.cs b/MineSharp.Core/Common/Entities/EntityCategory.cs index 84277677..be461614 100644 --- a/MineSharp.Core/Common/Entities/EntityCategory.cs +++ b/MineSharp.Core/Common/Entities/EntityCategory.cs @@ -1,4 +1,4 @@ -/////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////// // This File is generated by MineSharp.SourceGenerator and should not be modified. // /////////////////////////////////////////////////////////////////////////////////////////// @@ -13,7 +13,7 @@ public enum EntityCategory Immobile = 2, Projectiles = 3, HostileMobs = 4, - Vehicles = 5 + Vehicles = 5, } #pragma warning restore CS1591 diff --git a/MineSharp.Core/Common/Entities/EntityType.cs b/MineSharp.Core/Common/Entities/EntityType.cs index d82bc8d4..553ffb32 100644 --- a/MineSharp.Core/Common/Entities/EntityType.cs +++ b/MineSharp.Core/Common/Entities/EntityType.cs @@ -1,4 +1,4 @@ -/////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////// // This File is generated by MineSharp.SourceGenerator and should not be modified. // /////////////////////////////////////////////////////////////////////////////////////////// @@ -137,7 +137,7 @@ public enum EntityType Armadillo = 126, Bogged = 127, BreezeWindCharge = 128, - OminousItemSpawner = 129 + OminousItemSpawner = 129, } #pragma warning restore CS1591 diff --git a/MineSharp.Core/Common/Entities/MinecraftPlayer.cs b/MineSharp.Core/Common/Entities/MinecraftPlayer.cs index f257af2c..18a44886 100644 --- a/MineSharp.Core/Common/Entities/MinecraftPlayer.cs +++ b/MineSharp.Core/Common/Entities/MinecraftPlayer.cs @@ -61,12 +61,17 @@ public class MinecraftPlayer( /// public EntityPose Pose { get; set; } = EntityPose.Standing; + /// + /// The offset for the player's eye height. + /// + public static readonly Vector3 PlayerEyeHeightOffset = new(0, 1.62, 0); + /// /// The position of this player's head. /// /// public Vector3 GetHeadPosition() { - return Entity!.Position.Plus(Vector3.Up); + return Entity!.Position.Plus(PlayerEyeHeightOffset); } } diff --git a/MineSharp.Core/Common/Identifier.cs b/MineSharp.Core/Common/Identifier.cs new file mode 100644 index 00000000..588d1762 --- /dev/null +++ b/MineSharp.Core/Common/Identifier.cs @@ -0,0 +1,251 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.RegularExpressions; +using MineSharp.Core.Serialization; + +namespace MineSharp.Core.Common; + +/// +/// Represents an identifier with a namespace and a name. +/// This class is immutable. +/// The and methods are overridden to compare two identifiers with the following rules: +/// The default namespace is used for the comparison if none is specified. +/// +public sealed partial record Identifier +{ + /// + /// The namespace that is assumed by default when none is specified. + /// + public const string DefaultNamespace = "minecraft"; + /// + /// The namespace that is used when no namespace is specified. + /// + public const string NoNamespace = ""; + + /// + /// Represents an empty identifier. + /// It has no namespace and an empty name. + /// + public static readonly Identifier Empty = new(""); + + /// + /// The namespace part of the identifier. + /// + public readonly string Namespace; + + /// + /// The name part of the identifier. + /// + public readonly string Name; + + // This constructor is private to prevent creating identifiers with invalid characters + private Identifier(string @namespace, string name) + { + Namespace = @namespace; + Name = name; + } + + // This constructor is private to prevent creating identifiers with invalid characters + private Identifier(string name) + : this(DefaultNamespace, name) + { + } + + /// + /// Gets a value indicating whether the identifier has a namespace. + /// + public bool HasNamespace => Namespace != NoNamespace; + + /// + /// Gets a value indicating whether the identifier has the default namespace. + /// + public bool HasDefaultNamespace => Namespace == DefaultNamespace; + + /// + /// Converts the identifier to a complete identifier with the default namespace if none is specified. + /// + /// A complete identifier with a namespace. + public Identifier ToCompleteIdentifier() + { + return HasNamespace ? this : new Identifier(DefaultNamespace, Name); + } + + /// + /// Determines whether the specified is equal to the current . + /// The comparison uses the default namespace if none is specified. + /// + /// The to compare with the current . + /// true if the specified is equal to the current ; otherwise, false. + /// + public bool Equals(Identifier? other) + { + if (other is null) + { + return false; + } + if (ReferenceEquals(this, other)) + { + return true; + } + + var @namespace = HasNamespace ? Namespace : DefaultNamespace; + var otherNamespace = other.HasNamespace ? other.Namespace : DefaultNamespace; + return @namespace == otherNamespace && Name == other.Name; + } + + /// + /// Determines whether the specified is equal to the current . + /// In contrast to this comparison is strict and does not use the default namespace if none is specified. + /// + /// The to compare with the current . + /// true if the specified is equal to the current ; otherwise, false. + /// + public bool EqualsStrict(Identifier? other) + { + if (other is null) + { + return false; + } + if (ReferenceEquals(this, other)) + { + return true; + } + + return Namespace == other.Namespace && Name == other.Name; + } + + /// + public override int GetHashCode() + { + var @namespace = HasNamespace ? Namespace : DefaultNamespace; + return HashCode.Combine(@namespace, Name); + } + + /// + /// Returns a string representation of the identifier. + /// + /// A string in the format "namespace:name" or "name" if no namespace is specified. + public override string ToString() + { + return HasNamespace ? $"{Namespace}:{Name}" : Name; + } + + /// + /// Parses a string into an object. + /// + /// The string to parse. + /// An object. + /// Thrown when the string format is invalid. + public static Identifier Parse(string identifierString) + { + var parseError = TryParseInternal(identifierString, out var identifier); + if (parseError != null) + { + throw new FormatException($"Invalid identifier format: {parseError}"); + } + return identifier!; + } + + private static string? TryParseInternal(string identifierString, [NotNullWhen(true)] out Identifier? identifier) + { + if (string.IsNullOrEmpty(identifierString)) + { + identifier = Empty; + return null; + } + + var colonIndex = identifierString.IndexOf(':'); + if (colonIndex == -1) + { + return TryCreateInternal(NoNamespace, identifierString, out identifier); + } + else if (colonIndex == 0 || colonIndex == identifierString.Length - 1) + { + identifier = null; + return "Colon must not be at the very start or end"; + } + return TryCreateInternal(identifierString.Substring(0, colonIndex), identifierString.Substring(colonIndex + 1), out identifier); + } + + /// + /// Tries to parse a string into an object. + /// + /// The string to parse. + /// The resulting object if parsing is successful. + /// true if parsing is successful; otherwise, false. + public static bool TryParse(string identifierString, [NotNullWhen(true)] out Identifier? identifier) + { + return TryParseInternal(identifierString, out identifier) == null; + } + + private static string? TryCreateInternal(string @namespace, string name, out Identifier? identifier) + { + if (!IsValidNamespace(@namespace)) + { + identifier = null; + return "Invalid namespace"; + } + if (!IsValidName(name)) + { + identifier = null; + return "Invalid name"; + } + identifier = new Identifier(@namespace, name); + return null; + } + + /// + /// Tries to create an object with the specified namespace and name. + /// + /// The namespace part of the identifier. + /// The name part of the identifier. + /// The resulting object if creation is successful. + /// true if creation is successful; otherwise, false. + public static bool TryCreate(string @namespace, string name, [NotNullWhen(true)] out Identifier? identifier) + { + var parseError = TryCreateInternal(@namespace, name, out identifier); + return parseError == null; + } + + /// + /// Creates an object with the specified namespace and name. + /// + /// The namespace part of the identifier. + /// The name part of the identifier. + /// An object. + /// Thrown when the namespace or name is invalid. + public static Identifier Create(string @namespace, string name) + { + var parseError = TryCreateInternal(@namespace, name, out var identifier); + if (parseError != null) + { + throw new FormatException(parseError); + } + return identifier!; + } + + [GeneratedRegex("[a-z0-9.-_]*")] + private static partial Regex NamespaceRegex(); + + /// + /// Determines whether the specified namespace is valid. + /// + /// The namespace to validate. + /// true if the namespace is valid; otherwise, false. + public static bool IsValidNamespace(string @namespace) + { + return NamespaceRegex().MatchEntireString(@namespace) != null; + } + + [GeneratedRegex("[a-z0-9.-_/]+")] + private static partial Regex NameRegex(); + + /// + /// Determines whether the specified name is valid. + /// + /// The name to validate. + /// true if the name is valid; otherwise, false. + public static bool IsValidName(string name) + { + return NameRegex().MatchEntireString(name) != null; + } +} diff --git a/MineSharp.Core/Common/Items/ItemType.cs b/MineSharp.Core/Common/Items/ItemType.cs index 6d850f5b..8aa85062 100644 --- a/MineSharp.Core/Common/Items/ItemType.cs +++ b/MineSharp.Core/Common/Items/ItemType.cs @@ -1,4 +1,4 @@ -/////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////// // This File is generated by MineSharp.SourceGenerator and should not be modified. // /////////////////////////////////////////////////////////////////////////////////////////// @@ -1339,7 +1339,7 @@ public enum ItemType OminousTrialKey = 1328, Vault = 1329, OminousBottle = 1330, - BreezeRod = 1331 + BreezeRod = 1331, } #pragma warning restore CS1591 diff --git a/Data/MineSharp.Data/MinecraftVersion.cs b/MineSharp.Core/Common/MinecraftVersion.cs similarity index 98% rename from Data/MineSharp.Data/MinecraftVersion.cs rename to MineSharp.Core/Common/MinecraftVersion.cs index dd848d21..e6f1dfb0 100644 --- a/Data/MineSharp.Data/MinecraftVersion.cs +++ b/MineSharp.Core/Common/MinecraftVersion.cs @@ -1,4 +1,4 @@ -namespace MineSharp.Data; +namespace MineSharp.Core.Common; /// /// A Minecraft Version diff --git a/MineSharp.Core/Common/Particles/ParticleType.cs b/MineSharp.Core/Common/Particles/ParticleType.cs new file mode 100644 index 00000000..eb13fdd7 --- /dev/null +++ b/MineSharp.Core/Common/Particles/ParticleType.cs @@ -0,0 +1,128 @@ +/////////////////////////////////////////////////////////////////////////////////////////// +// This File is generated by MineSharp.SourceGenerator and should not be modified. // +/////////////////////////////////////////////////////////////////////////////////////////// + +#pragma warning disable CS1591 + +namespace MineSharp.Core.Common.Particles; + +public enum ParticleType +{ + AmbientEntityEffect = 0, + AngryVillager = 1, + Block = 2, + BlockMarker = 3, + Bubble = 4, + Cloud = 5, + Crit = 6, + DamageIndicator = 7, + DragonBreath = 8, + DrippingLava = 9, + FallingLava = 10, + LandingLava = 11, + DrippingWater = 12, + FallingWater = 13, + Dust = 14, + DustColorTransition = 15, + Effect = 16, + ElderGuardian = 17, + EnchantedHit = 18, + Enchant = 19, + EndRod = 20, + EntityEffect = 21, + ExplosionEmitter = 22, + Explosion = 23, + FallingDust = 24, + Firework = 25, + Fishing = 26, + Flame = 27, + SoulFireFlame = 28, + Soul = 29, + Flash = 30, + HappyVillager = 31, + Composter = 32, + Heart = 33, + InstantEffect = 34, + Item = 35, + Vibration = 36, + ItemSlime = 37, + ItemSnowball = 38, + LargeSmoke = 39, + Lava = 40, + Mycelium = 41, + Note = 42, + Poof = 43, + Portal = 44, + Rain = 45, + Smoke = 46, + Sneeze = 47, + Spit = 48, + SquidInk = 49, + SweepAttack = 50, + TotemOfUndying = 51, + Underwater = 52, + Splash = 53, + Witch = 54, + BubblePop = 55, + CurrentDown = 56, + BubbleColumnUp = 57, + Nautilus = 58, + Dolphin = 59, + CampfireCosySmoke = 60, + CampfireSignalSmoke = 61, + DrippingHoney = 62, + FallingHoney = 63, + LandingHoney = 64, + FallingNectar = 65, + FallingSporeBlossom = 66, + Ash = 67, + CrimsonSpore = 68, + WarpedSpore = 69, + SporeBlossomAir = 70, + DrippingObsidianTear = 71, + FallingObsidianTear = 72, + LandingObsidianTear = 73, + ReversePortal = 74, + WhiteAsh = 75, + SmallFlame = 76, + Snowflake = 77, + DrippingDripstoneLava = 78, + FallingDripstoneLava = 79, + DrippingDripstoneWater = 80, + FallingDripstoneWater = 81, + GlowSquidInk = 82, + Glow = 83, + WaxOn = 84, + WaxOff = 85, + ElectricSpark = 86, + Scrape = 87, + SonicBoom = 88, + SculkSoul = 89, + SculkCharge = 90, + SculkChargePop = 91, + Shriek = 92, + DrippingCherryLeaves = 93, + FallingCherryLeaves = 94, + LandingCherryLeaves = 95, + CherryLeaves = 96, + EggCrack = 97, + Gust = 98, + GustEmitter = 99, + WhiteSmoke = 100, + DustPlume = 101, + GustDust = 102, + TrialSpawnerDetection = 103, + SmallGust = 104, + GustEmitterLarge = 105, + GustEmitterSmall = 106, + Infested = 107, + ItemCobweb = 108, + TrialSpawnerDetectionOminous = 109, + VaultConnection = 110, + DustPillar = 111, + OminousSpawning = 112, + RaidOmen = 113, + TrialOmen = 114, +} + +#pragma warning restore CS1591 diff --git a/MineSharp.Core/Common/Protocol/GameState.cs b/MineSharp.Core/Common/Protocol/GameState.cs index 5337ee46..eae5fc17 100644 --- a/MineSharp.Core/Common/Protocol/GameState.cs +++ b/MineSharp.Core/Common/Protocol/GameState.cs @@ -11,5 +11,7 @@ public enum GameState Login = 2, Play = 3, Configuration = 4, + + None = -1 #pragma warning restore CS1591 } diff --git a/MineSharp.Core/Common/TextColor.cs b/MineSharp.Core/Common/TextColor.cs new file mode 100644 index 00000000..6da6a1ba --- /dev/null +++ b/MineSharp.Core/Common/TextColor.cs @@ -0,0 +1,35 @@ +namespace MineSharp.Core.Common; + +// TODO: use this enum in more places like Chat +// and add extension methods that convert it to and from the §0-9a-fk-r format + +/// +/// Enum representing the various text colors (and stylings) available in Minecraft. +/// +public enum TextColor +{ +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + Black = 0, + DarkBlue = 1, + DarkGreen = 2, + DarkAqua = 3, + DarkRed = 4, + DarkPurple = 5, + Gold = 6, + Gray = 7, + DarkGray = 8, + Blue = 9, + Green = 10, + Aqua = 11, + Red = 12, + LightPurple = 13, + Yellow = 14, + White = 15, + Obfuscated = 16, + Bold = 17, + Strikethrough = 18, + Underlined = 19, + Italic = 20, + Reset = 21 +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member +} diff --git a/MineSharp.Core/Common/UUID.cs b/MineSharp.Core/Common/UUID.cs index 4ee6a0bf..53ee67f8 100644 --- a/MineSharp.Core/Common/UUID.cs +++ b/MineSharp.Core/Common/UUID.cs @@ -1,4 +1,7 @@ -namespace MineSharp.Core.Common; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace MineSharp.Core.Common; // Thanks to MrZoidberg // https://gist.github.com/MrZoidberg/9bac07cf3f5aa5896f75 @@ -7,17 +10,12 @@ /// Represents an immutable Java universally unique identifier (UUID). /// A UUID represents a 128-bit value. /// -public struct Uuid : IEquatable +public readonly struct Uuid : IEquatable { /// /// Empty UUID /// - public static readonly Uuid Empty; - - static Uuid() - { - Empty = new(); - } + public static readonly Uuid Empty = new(); /// /// Constructs a new UUID using the specified data. @@ -33,32 +31,41 @@ public Uuid(long mostSignificantBits, long leastSignificantBits) /// /// Constructs a new UUID using the specified data. /// - /// Bytes array that represents the UUID. - public Uuid(byte[] b) + /// Bytes array that represents the UUID. Must be 16 bytes in big-endian order. + public Uuid(ReadOnlySpan bytes) { - if (b == null) + if (bytes.Length != 16) { - throw new ArgumentNullException("b"); + throw new ArgumentException("Length of the UUID byte array should be 16"); } - if (b.Length != 16) + Span byteSpan = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref this, 1)); + if (BitConverter.IsLittleEndian) { - throw new ArgumentException("Length of the UUID byte array should be 16"); + bytes.CopyTo(byteSpan); + byteSpan.Reverse(); } - - MostSignificantBits = BitConverter.ToInt64(b, 0); - LeastSignificantBits = BitConverter.ToInt64(b, 8); + else + { + // architecure is big-endian but our files are little-endian ordered + // so we need to reverse the longs + var longSpan = MemoryMarshal.Cast(byteSpan); + longSpan.Reverse(); + } + // since we do these operations in-place, we are done here } + // The order of the most significant and least significant fields is important + // when serializing and deserializing the UUID as unmanaged data types (little-endian) /// /// The least significant 64 bits of this UUID's 128 bit value. /// - public long LeastSignificantBits { get; } + public readonly long LeastSignificantBits; /// /// The most significant 64 bits of this UUID's 128 bit value. /// - public long MostSignificantBits { get; } + public readonly long MostSignificantBits; /// /// Returns a value that indicates whether this instance is equal to a specified @@ -68,13 +75,11 @@ public Uuid(byte[] b) /// true if o is a that has the same value as this instance; otherwise, false. public override bool Equals(object? obj) { - if (!(obj is Uuid)) + if (!(obj is Uuid uuid)) { return false; } - var uuid = (Uuid)obj; - return Equals(uuid); } @@ -86,7 +91,7 @@ public override bool Equals(object? obj) /// true if is equal to this instance; otherwise, false. public bool Equals(Uuid uuid) { - return MostSignificantBits == uuid.MostSignificantBits && LeastSignificantBits == uuid.LeastSignificantBits; + return LeastSignificantBits == uuid.LeastSignificantBits && MostSignificantBits == uuid.MostSignificantBits; } /// @@ -95,7 +100,8 @@ public bool Equals(Uuid uuid) /// The hash code for this instance. public override int GetHashCode() { - return ((Guid)this).GetHashCode(); + // our hash code must not be compatible with Guid.GetHashCode so we do it ourselves + return HashCode.Combine(LeastSignificantBits, MostSignificantBits); } /// @@ -112,25 +118,47 @@ public override string ToString() GetDigits(LeastSignificantBits, 12); } + private ReadOnlySpan AsByteSpan() + { + return MemoryMarshal.AsBytes(MemoryMarshal.CreateReadOnlySpan(ref Unsafe.AsRef(in this), 1)); + } + /// - /// Returns a 16-element byte array that contains the value of this instance. + /// Writes the UUID to the specified destination span in big-endian order. + /// + /// The destination span to write the UUID to. + public void WriteTo(Span destination) + { + if (destination.Length != 16) + { + throw new ArgumentException("Destination span must be 16 bytes long"); + } + + var byteSpan = AsByteSpan(); + + if (BitConverter.IsLittleEndian) + { + byteSpan.CopyTo(destination); + destination.Reverse(); + } + else + { + // architecture is big-endian but our fields are little-endian ordered + // so we need to reverse the longs + var longSpan = MemoryMarshal.Cast(destination); + longSpan.Reverse(); + } + } + + /// + /// Returns a 16-element byte array that contains the value of this instance in big-endian. /// /// A 16-element byte array public byte[] ToByteArray() { - var uuidMostSignificantBytes = BitConverter.GetBytes(MostSignificantBits); - var uuidLeastSignificantBytes = BitConverter.GetBytes(LeastSignificantBits); - byte[] bytes = - { - uuidMostSignificantBytes[0], uuidMostSignificantBytes[1], uuidMostSignificantBytes[2], - uuidMostSignificantBytes[3], uuidMostSignificantBytes[4], uuidMostSignificantBytes[5], - uuidMostSignificantBytes[6], uuidMostSignificantBytes[7], uuidLeastSignificantBytes[0], - uuidLeastSignificantBytes[1], uuidLeastSignificantBytes[2], uuidLeastSignificantBytes[3], - uuidLeastSignificantBytes[4], uuidLeastSignificantBytes[5], uuidLeastSignificantBytes[6], - uuidLeastSignificantBytes[7] - }; - - return bytes; + var destinationBytes = new byte[16]; + WriteTo(destinationBytes); + return destinationBytes; } /// Indicates whether the values of two specified objects are equal. @@ -161,19 +189,8 @@ public static explicit operator Guid(Uuid uuid) return default; } - var uuidMostSignificantBytes = BitConverter.GetBytes(uuid.MostSignificantBits); - var uuidLeastSignificantBytes = BitConverter.GetBytes(uuid.LeastSignificantBits); - byte[] guidBytes = - { - uuidMostSignificantBytes[4], uuidMostSignificantBytes[5], uuidMostSignificantBytes[6], - uuidMostSignificantBytes[7], uuidMostSignificantBytes[2], uuidMostSignificantBytes[3], - uuidMostSignificantBytes[0], uuidMostSignificantBytes[1], uuidLeastSignificantBytes[7], - uuidLeastSignificantBytes[6], uuidLeastSignificantBytes[5], uuidLeastSignificantBytes[4], - uuidLeastSignificantBytes[3], uuidLeastSignificantBytes[2], uuidLeastSignificantBytes[1], - uuidLeastSignificantBytes[0] - }; - - return new(guidBytes); + var byteSpan = uuid.AsByteSpan(); + return new(byteSpan, true); } /// Converts a to an . @@ -186,16 +203,9 @@ public static implicit operator Uuid(Guid value) return default; } - var guidBytes = value.ToByteArray(); - byte[] uuidBytes = - { - guidBytes[6], guidBytes[7], guidBytes[4], guidBytes[5], guidBytes[0], guidBytes[1], guidBytes[2], - guidBytes[3], guidBytes[15], guidBytes[14], guidBytes[13], guidBytes[12], guidBytes[11], guidBytes[10], - guidBytes[9], guidBytes[8] - }; - - - return new(BitConverter.ToInt64(uuidBytes, 0), BitConverter.ToInt64(uuidBytes, 8)); + Span byteSpan = stackalloc byte[16]; + value.TryWriteBytes(byteSpan, true, out _); + return new(byteSpan); } /// diff --git a/MineSharp.Core/Concurrency/ConcurrencyHelper.cs b/MineSharp.Core/Concurrency/ConcurrencyHelper.cs new file mode 100644 index 00000000..e2e47c7b --- /dev/null +++ b/MineSharp.Core/Concurrency/ConcurrencyHelper.cs @@ -0,0 +1,79 @@ +using System.Runtime.CompilerServices; + +namespace MineSharp.Core.Concurrency; + +public record struct EnsureOnlyRunOnceAsyncResult(Task Task, bool FirstInvocation) +{ + public TaskAwaiter GetAwaiter() => Task.GetAwaiter(); +} +public record struct EnsureOnlyRunOnceAsyncResult(Task Task, bool FirstInvocation) +{ + public TaskAwaiter GetAwaiter() => Task.GetAwaiter(); +} + +public static class ConcurrencyHelper +{ + #region EnsureOnlyRunOnce + + /// + public static EnsureOnlyRunOnceAsyncResult EnsureOnlyRunOnceAsync(Func action, ref Task? taskStore) + { + var ret = EnsureOnlyRunOnceAsync(async () => + { + // we need to do the typical async/await because otherwise the exception would be lost and the task will not be faulted + await action(); + // return value does mean nothing. Is just used to call the other method. + return true; + }, + // this ref type conversion is safe because all Tasks with return type are also normal Tasks + ref Unsafe.As?>(ref taskStore)); + + return new(ret.Task, ret.FirstInvocation); + } + + /// + /// Ensures that the given async action is only run once while still returning the task of that first invocation for further calls. + /// + /// + /// The async action to execute once. + /// A reference to a variable holding the task of the first and only execution. + /// + public static EnsureOnlyRunOnceAsyncResult EnsureOnlyRunOnceAsync(Func> action, ref Task? taskStore) + { + var storedTask = taskStore; + if (storedTask is not null) + { + return new(storedTask, false); + } + + // we can use using here because this method is the only place where the cts is used + using var cts = new CancellationTokenSource(); + var newTaskWrapped = new Task>(action, cancellationToken: cts.Token); + // TODO: Check if Unwrap does forward OCE. If not use UnwrapSlow + var newTask = newTaskWrapped.Unwrap(); + //var newTask = UnwrapSlow(newTaskWrapped); + + var actualStoredTask = Interlocked.CompareExchange(ref taskStore, newTask, null); + if (ReferenceEquals(actualStoredTask, null)) + { + // we set the stored task to the new task + // and start the new task + + // important: start newTaskWrapped not newTask + newTaskWrapped.Start(); + return new(newTask, true); + } + + // some other task was faster and set the task + // cancel the new task + cts.Cancel(); + // and return the already started task + return new(actualStoredTask, false); + } + + private static async Task UnwrapSlow(Task> task) + { + return await await task; + } + #endregion +} diff --git a/MineSharp.Core/Concurrency/InterlockedHelper.cs b/MineSharp.Core/Concurrency/InterlockedHelper.cs new file mode 100644 index 00000000..99871573 --- /dev/null +++ b/MineSharp.Core/Concurrency/InterlockedHelper.cs @@ -0,0 +1,134 @@ +using System.Runtime.CompilerServices; + +namespace MineSharp.Core.Concurrency; + +public static class InterlockedHelper +{ + /// + /// Compares two 32-bit signed integers for equality and, if they are not equal to the specified value, replaces the first value. + /// + /// The variable to set to the specified value. + /// The value to which the is set if the comparison succeeds. + /// The value that is compared to the value at . + /// The original value of . + public static int CompareExchangeIfNot(ref int location1, int value, int notComparand) + { + var newCurrent = location1; + var oldCurrent = newCurrent; + do + { + oldCurrent = newCurrent; + if (oldCurrent == notComparand) + { + return oldCurrent; + } + } while ((newCurrent = Interlocked.CompareExchange(ref location1, value, oldCurrent)) != oldCurrent); + + return newCurrent; + } + + /// + /// Compares two 64-bit signed integers for equality and, if they are not equal to the specified value, replaces the first value. + /// + /// The variable to set to the specified value. + /// The value to which the is set if the comparison succeeds. + /// The value that is compared to the value at . + /// The original value of . + public static long CompareExchangeIfNot(ref long location1, long value, long notComparand) + { + var newCurrent = location1; + var oldCurrent = newCurrent; + do + { + oldCurrent = newCurrent; + if (oldCurrent == notComparand) + { + return oldCurrent; + } + } while ((newCurrent = Interlocked.CompareExchange(ref location1, value, oldCurrent)) != oldCurrent); + + return newCurrent; + } + + #region Enum + + /// + /// Exchanges the enum value of the specified location with the given value. + /// + /// Note: This is very slow and also a bit unsafe but it's the only way to do it before .NET 9 + /// + /// The type of the value. Must be an enum. + /// The variable to set to the specified value. + /// The value to which the is set. + /// The original value of . + public static T Exchange(ref T location1, T value) + where T : unmanaged, Enum + { + switch (Type.GetTypeCode(Enum.GetUnderlyingType(typeof(T)))) + { + case TypeCode.UInt32: + case TypeCode.Int32: + var resultInt = Interlocked.Exchange(ref Unsafe.As(ref location1), Unsafe.As(ref value)); + return Unsafe.As(ref resultInt); + case TypeCode.UInt64: + case TypeCode.Int64: + var resultLong = Interlocked.Exchange(ref Unsafe.As(ref location1), Unsafe.As(ref value)); + return Unsafe.As(ref resultLong); + default: + throw new NotSupportedException(); + } + } + + /// + /// Compares two enum values for equality and, if they are equal, replaces the first value. + /// + /// The type of the value. Must be an enum. + /// The variable to set to the specified value. + /// The value to which the is set if the comparison succeeds. + /// The value that is compared to the value at . + /// The original value of . + public static T CompareExchange(ref T location1, T value, T comparand) + where T : unmanaged, Enum + { + switch (Type.GetTypeCode(Enum.GetUnderlyingType(typeof(T)))) + { + case TypeCode.UInt32: + case TypeCode.Int32: + var resultInt = Interlocked.CompareExchange(ref Unsafe.As(ref location1), Unsafe.As(ref value), Unsafe.As(ref comparand)); + return Unsafe.As(ref resultInt); + case TypeCode.UInt64: + case TypeCode.Int64: + var resultLong = Interlocked.CompareExchange(ref Unsafe.As(ref location1), Unsafe.As(ref value), Unsafe.As(ref comparand)); + return Unsafe.As(ref resultLong); + default: + throw new NotSupportedException(); + } + } + + /// + /// Compares two enum values for equality and, if they are not equal to the specified value, replaces the first value. + /// + /// The type of the value. Must be an enum. + /// The variable to set to the specified value. + /// The value to which the is set if the comparison succeeds. + /// The value that is compared to the value at . + /// The original value of . + public static T CompareExchangeIfNot(ref T location1, T value, T notComparand) + where T : unmanaged, Enum + { + switch (Type.GetTypeCode(Enum.GetUnderlyingType(typeof(T)))) + { + case TypeCode.UInt32: + case TypeCode.Int32: + var resultInt = CompareExchangeIfNot(ref Unsafe.As(ref location1), Unsafe.As(ref value), Unsafe.As(ref notComparand)); + return Unsafe.As(ref resultInt); + case TypeCode.UInt64: + case TypeCode.Int64: + var resultLong = CompareExchangeIfNot(ref Unsafe.As(ref location1), Unsafe.As(ref value), Unsafe.As(ref notComparand)); + return Unsafe.As(ref resultLong); + default: + throw new NotSupportedException(); + } + } + #endregion +} diff --git a/MineSharp.Core/Events/AsyncEvent.cs b/MineSharp.Core/Events/AsyncEvent.cs index e81f8a2b..2bf7ceb1 100644 --- a/MineSharp.Core/Events/AsyncEvent.cs +++ b/MineSharp.Core/Events/AsyncEvent.cs @@ -1,4 +1,5 @@ -using NLog; +using ConcurrentCollections; +using NLog; namespace MineSharp.Core.Events; @@ -12,52 +13,51 @@ public abstract class AsyncEventBase where TSync : Delegate where /// /// List of synchronous handlers subscribed to this event /// - protected readonly HashSet Handlers = []; - + protected readonly ConcurrentHashSet Handlers = []; + /// /// List of asynchronous handlers subscribed to this event /// - protected readonly HashSet AsyncHandlers = []; + protected readonly ConcurrentHashSet AsyncHandlers = []; /// /// Subscribe to this event /// - public void Subscribe(TSync handler) + public bool Subscribe(TSync handler) { - Handlers.Add(handler); - } - + return Handlers.Add(handler); + } + /// /// Unsubscribe from this event /// - public void Unsubscribe(TSync handler) + public bool Unsubscribe(TSync handler) { - Handlers.Remove(handler); + return Handlers.TryRemove(handler); } - + /// /// Subscribe to this event /// - public void Subscribe(TAsync handler) + public bool Subscribe(TAsync handler) { - AsyncHandlers.Add(handler); + return AsyncHandlers.Add(handler); } /// /// Unsubscribe from this event /// - public void Unsubscribe(TAsync handler) + public bool Unsubscribe(TAsync handler) { - AsyncHandlers.Remove(handler); + return AsyncHandlers.TryRemove(handler); } - + /// /// Makes sure all handlers run to completion and catch exceptions in the handlers /// /// - /// /// - protected Task WaitForHandlers(Task[] tasks, bool block = false) + protected Task WaitForHandlers(Task[] tasks) { var task = Task.Run(async () => { @@ -74,9 +74,7 @@ protected Task WaitForHandlers(Task[] tasks, bool block = false) } }); - return block - ? task - : Task.CompletedTask; + return task; } /// @@ -99,27 +97,26 @@ public class AsyncEvent : AsyncEventBase public delegate void Handler(); - + /// /// An asynchronous event handler /// public delegate Task AsyncHandler(); - + /// /// Dispatch this event /// - /// Whether to block until all handlers are completed - /// - public Task Dispatch(bool block = false) + /// A task that completes once all handlers have completed. + public Task Dispatch() { var tasks = AsyncHandlers .Select(handler => Task.Run(async () => await handler())) .Concat(Handlers.Select(handler => Task.Run(() => handler()))) .ToArray(); - return WaitForHandlers(tasks, block); + return WaitForHandlers(tasks); } - + /// /// Subscribe to this event /// @@ -131,7 +128,7 @@ public Task Dispatch(bool block = false) eventBase.Subscribe(handler); return eventBase; } - + /// /// Subscribe to this event /// @@ -143,7 +140,7 @@ public Task Dispatch(bool block = false) eventBase.Subscribe(handler); return eventBase; } - + /// /// Unsubscribe from this event /// @@ -155,7 +152,7 @@ public Task Dispatch(bool block = false) eventBase.Unsubscribe(handler); return eventBase; } - + /// /// Unsubscribe from this event /// @@ -178,28 +175,27 @@ public class AsyncEvent : AsyncEventBase.Handler, AsyncEvent /// A synchronous event handler /// public delegate void Handler(T arg1); - + /// /// An asynchronous event handler /// public delegate Task AsyncHandler(T arg1); - + /// /// Dispatch this event /// - /// Whether to block until all handlers are completed /// First argument for the handlers - /// - public Task Dispatch(T arg1, bool block = false) + /// A task that completes once all handlers have completed. + public Task Dispatch(T arg1) { var tasks = AsyncHandlers .Select(handler => Task.Run(async () => await handler(arg1))) .Concat(Handlers.Select(handler => Task.Run(() => handler(arg1)))) .ToArray(); - return WaitForHandlers(tasks, block); + return WaitForHandlers(tasks); } - + /// /// Subscribe to this event /// @@ -211,7 +207,7 @@ public Task Dispatch(T arg1, bool block = false) eventBase.Subscribe(handler); return eventBase; } - + /// /// Subscribe to this event /// @@ -223,7 +219,7 @@ public Task Dispatch(T arg1, bool block = false) eventBase.Subscribe(handler); return eventBase; } - + /// /// Unsubscribe from this event /// @@ -235,7 +231,7 @@ public Task Dispatch(T arg1, bool block = false) eventBase.Unsubscribe(handler); return eventBase; } - + /// /// Unsubscribe from this event /// @@ -258,29 +254,28 @@ public class AsyncEvent : AsyncEventBase.Handler, Asy /// A synchronous event handler /// public delegate void Handler(T1 arg1, T2 arg2); - + /// /// An asynchronous event handler /// public delegate Task AsyncHandler(T1 arg1, T2 arg2); - + /// /// Dispatch this event /// - /// Whether to block until all handlers are completed /// First argument for the handlers /// Second argument for the handlers - /// - public Task Dispatch(T1 arg1, T2 arg2, bool block = false) + /// A task that completes once all handlers have completed. + public Task Dispatch(T1 arg1, T2 arg2) { var tasks = AsyncHandlers .Select(handler => Task.Run(async () => await handler(arg1, arg2))) .Concat(Handlers.Select(handler => Task.Run(() => handler(arg1, arg2)))) .ToArray(); - return WaitForHandlers(tasks, block); + return WaitForHandlers(tasks); } - + /// /// Subscribe to this event /// @@ -292,7 +287,7 @@ public Task Dispatch(T1 arg1, T2 arg2, bool block = false) eventBase.Subscribe(handler); return eventBase; } - + /// /// Subscribe to this event /// @@ -304,7 +299,7 @@ public Task Dispatch(T1 arg1, T2 arg2, bool block = false) eventBase.Subscribe(handler); return eventBase; } - + /// /// Unsubscribe from this event /// @@ -316,7 +311,7 @@ public Task Dispatch(T1 arg1, T2 arg2, bool block = false) eventBase.Unsubscribe(handler); return eventBase; } - + /// /// Unsubscribe from this event /// @@ -339,30 +334,29 @@ public class AsyncEvent : AsyncEventBase.Hand /// A synchronous event handler /// public delegate void Handler(T1 arg1, T2 arg2, T3 arg3); - + /// /// An asynchronous event handler /// public delegate Task AsyncHandler(T1 arg1, T2 arg2, T3 arg3); - + /// /// Dispatch this event /// - /// Whether to block until all handlers are completed /// First argument for the handlers /// Second argument for the handlers /// Third argument for the handlers - /// - public Task Dispatch(T1 arg1, T2 arg2, T3 arg3, bool block = false) + /// A task that completes once all handlers have completed. + public Task Dispatch(T1 arg1, T2 arg2, T3 arg3) { var tasks = AsyncHandlers .Select(handler => Task.Run(async () => await handler(arg1, arg2, arg3))) .Concat(Handlers.Select(handler => Task.Run(() => handler(arg1, arg2, arg3)))) .ToArray(); - - return WaitForHandlers(tasks, block); + + return WaitForHandlers(tasks); } - + /// /// Subscribe to this event /// @@ -374,7 +368,7 @@ public Task Dispatch(T1 arg1, T2 arg2, T3 arg3, bool block = false) eventBase.Subscribe(handler); return eventBase; } - + /// /// Subscribe to this event /// @@ -386,7 +380,7 @@ public Task Dispatch(T1 arg1, T2 arg2, T3 arg3, bool block = false) eventBase.Subscribe(handler); return eventBase; } - + /// /// Unsubscribe from this event /// @@ -398,7 +392,7 @@ public Task Dispatch(T1 arg1, T2 arg2, T3 arg3, bool block = false) eventBase.Unsubscribe(handler); return eventBase; } - + /// /// Unsubscribe from this event /// @@ -421,12 +415,12 @@ public class AsyncEvent : AsyncEventBase public delegate void Handler(T1 arg1, T2 arg2, T3 arg3, T4 arg4); - + /// /// An asynchronous event handler /// public delegate Task AsyncHandler(T1 arg1, T2 arg2, T3 arg3, T4 arg4); - + /// /// Dispatch this event /// @@ -435,17 +429,17 @@ public class AsyncEvent : AsyncEventBaseSecond argument for the handlers /// Third argument for the handlers /// Fourth argument for the handlers - /// - public Task Dispatch(T1 arg1, T2 arg2, T3 arg3, T4 arg4, bool block = false) + /// A task that completes once all handlers have completed. + public Task Dispatch(T1 arg1, T2 arg2, T3 arg3, T4 arg4) { var tasks = AsyncHandlers .Select(handler => Task.Run(async () => await handler(arg1, arg2, arg3, arg4))) .Concat(Handlers.Select(handler => Task.Run(() => handler(arg1, arg2, arg3, arg4)))) .ToArray(); - - return WaitForHandlers(tasks, block); + + return WaitForHandlers(tasks); } - + /// /// Subscribe to this event /// @@ -457,7 +451,7 @@ public Task Dispatch(T1 arg1, T2 arg2, T3 arg3, T4 arg4, bool block = false) eventBase.Subscribe(handler); return eventBase; } - + /// /// Subscribe to this event /// @@ -469,7 +463,7 @@ public Task Dispatch(T1 arg1, T2 arg2, T3 arg3, T4 arg4, bool block = false) eventBase.Subscribe(handler); return eventBase; } - + /// /// Unsubscribe from this event /// @@ -481,7 +475,7 @@ public Task Dispatch(T1 arg1, T2 arg2, T3 arg3, T4 arg4, bool block = false) eventBase.Unsubscribe(handler); return eventBase; } - + /// /// Unsubscribe from this event /// diff --git a/MineSharp.Core/Exceptions/MineSharpException.cs b/MineSharp.Core/Exceptions/MineSharpException.cs index fd2cae3c..da5b0e9e 100644 --- a/MineSharp.Core/Exceptions/MineSharpException.cs +++ b/MineSharp.Core/Exceptions/MineSharpException.cs @@ -1,7 +1,8 @@ namespace MineSharp.Core.Exceptions; /// -/// Exception thrown by MineSharp projects +/// Exception thrown by MineSharp projects. /// /// -public abstract class MineSharpException(string message) : Exception(message); +/// +public abstract class MineSharpException(string message, Exception? innerException = null) : Exception(message, innerException); diff --git a/MineSharp.Core/Exceptions/SerializationException.cs b/MineSharp.Core/Exceptions/SerializationException.cs new file mode 100644 index 00000000..8529ca3a --- /dev/null +++ b/MineSharp.Core/Exceptions/SerializationException.cs @@ -0,0 +1,8 @@ +namespace MineSharp.Core.Exceptions; + +/// +/// Thrown when de-/serialization of anything fails. +/// This might be a packet, from the root stream or a common element such as Attribute, Item or NbtTag, or anything else. +/// +/// +public class SerializationException(string message) : MineSharpException(message); diff --git a/MineSharp.Core/Geometry/AABB.cs b/MineSharp.Core/Geometry/AABB.cs index da296bf9..abdd3bf6 100644 --- a/MineSharp.Core/Geometry/AABB.cs +++ b/MineSharp.Core/Geometry/AABB.cs @@ -111,7 +111,7 @@ public bool Contains(Vector3 point) /// /// /// - public bool IntersectsLine(Vector3 origin, Vector3 direction) + public double? IntersectsLine(Vector3 origin, Vector3 direction) { direction = direction.Normalized(); @@ -125,7 +125,11 @@ public bool IntersectsLine(Vector3 origin, Vector3 direction) var tMin = Math.Max(Math.Max(Math.Min(tMinX, tMaxX), Math.Min(tMinY, tMaxY)), Math.Min(tMinZ, tMaxZ)); var tMax = Math.Min(Math.Min(Math.Max(tMinX, tMaxX), Math.Max(tMinY, tMaxY)), Math.Max(tMinZ, tMaxZ)); - return !(tMax < 0 || tMin > tMax); + if (tMax < 0 || tMin > tMax) + { + return null; + } + return tMin; } /// diff --git a/MineSharp.Core/Geometry/BlockFace.cs b/MineSharp.Core/Geometry/BlockFace.cs index 9a65d851..7e36cc6f 100644 --- a/MineSharp.Core/Geometry/BlockFace.cs +++ b/MineSharp.Core/Geometry/BlockFace.cs @@ -5,12 +5,28 @@ /// public enum BlockFace { -#pragma warning disable CS1591 + /// + /// Offset: -Y + /// Bottom = 0, + /// + /// Offset: +Y + /// Top = 1, + /// + /// Offset: -Z + /// North = 2, + /// + /// Offset: +Z + /// South = 3, + /// + /// Offset: -X + /// West = 4, + /// + /// Offset: +X + /// East = 5 -#pragma warning restore CS1591 } diff --git a/MineSharp.Core/Geometry/Position.cs b/MineSharp.Core/Geometry/Position.cs index fb39d0e7..a70d02ed 100644 --- a/MineSharp.Core/Geometry/Position.cs +++ b/MineSharp.Core/Geometry/Position.cs @@ -3,8 +3,28 @@ /// /// Represents a 3D-Position /// -public class Position +public readonly struct Position : IEquatable { + /// + /// A Position with all coordinates set to zero + /// + public static Position Zero => new(0, 0, 0); + + /// + /// The X coordinate + /// + public readonly int X; + + /// + /// The Y coordinate + /// + public readonly int Y; + + /// + /// The Z coordinate + /// + public readonly int Z; + /// /// Create a new Position from a packed ulong /// @@ -49,21 +69,6 @@ public Position(double x, double y, double z) Z = (int)Math.Floor(z); } - /// - /// The X coordinate - /// - public int X { get; protected set; } - - /// - /// The Y coordinate - /// - public int Y { get; protected set; } - - /// - /// The Z coordinate - /// - public int Z { get; protected set; } - /// /// Check if the two positions represent the same point /// @@ -72,7 +77,7 @@ public Position(double x, double y, double z) /// public static bool operator ==(Position a, Position b) { - return a.ToULong() == b.ToULong(); + return a.Equals(b); } /// @@ -83,7 +88,13 @@ public Position(double x, double y, double z) /// public static bool operator !=(Position a, Position b) { - return a.ToULong() != b.ToULong(); + return !(a == b); + } + + /// + public bool Equals(Position other) + { + return ToULong() == other.ToULong(); } /// @@ -91,7 +102,7 @@ public override bool Equals(object? obj) { if (obj is Position pos) { - return pos.ToULong() == ToULong(); + return Equals(pos); } return false; @@ -128,30 +139,3 @@ public static explicit operator Vector3(Position x) return new(x.X, x.Y, x.Z); } } - -/// -/// A whose coordinates are mutable. -/// -public class MutablePosition : Position -{ - /// - public MutablePosition(ulong value) : base(value) - { } - - /// - public MutablePosition(int x, int y, int z) : base(x, y, z) - { } - - /// - /// Update the X, Y, Z coordinates of this position - /// - /// - /// - /// - public void Set(int x, int y, int z) - { - X = x; - Y = y; - Z = z; - } -} diff --git a/MineSharp.Core/Geometry/Vector3.cs b/MineSharp.Core/Geometry/Vector3.cs index a3432063..a6e5ec0a 100644 --- a/MineSharp.Core/Geometry/Vector3.cs +++ b/MineSharp.Core/Geometry/Vector3.cs @@ -187,7 +187,7 @@ public double HorizontalDistanceToSquared(Vector3 other) { var dX = X - other.X; var dZ = Z - other.Z; - return (dX * dX) + dZ + dZ; + return (dX * dX) + (dZ * dZ); } /// diff --git a/MineSharp.Core/MineSharp.Core.csproj b/MineSharp.Core/MineSharp.Core.csproj index b42679d4..9afdb8d8 100644 --- a/MineSharp.Core/MineSharp.Core.csproj +++ b/MineSharp.Core/MineSharp.Core.csproj @@ -1,9 +1,9 @@ - + enable enable - net7.0;net8.0 + net8.0 12 true README.md @@ -24,21 +24,17 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - + + + + + + + True - - - Always - - - diff --git a/Components/MineSharp.Protocol/Packets/ISerializable.cs b/MineSharp.Core/Serialization/ISerializable.cs similarity index 79% rename from Components/MineSharp.Protocol/Packets/ISerializable.cs rename to MineSharp.Core/Serialization/ISerializable.cs index 3d1a45ba..eed07f80 100644 --- a/Components/MineSharp.Protocol/Packets/ISerializable.cs +++ b/MineSharp.Core/Serialization/ISerializable.cs @@ -1,12 +1,11 @@ -using MineSharp.Core.Common; - -namespace MineSharp.Protocol.Packets; +namespace MineSharp.Core.Serialization; /// /// Interface for serializing and deserializing objects from and to /// /// -public interface ISerializable where T : ISerializable +public interface ISerializable + where T : ISerializable { /// /// Serialize the object into the buffer diff --git a/MineSharp.Core/Serialization/NbtExtensions.cs b/MineSharp.Core/Serialization/NbtExtensions.cs new file mode 100644 index 00000000..46998c48 --- /dev/null +++ b/MineSharp.Core/Serialization/NbtExtensions.cs @@ -0,0 +1,31 @@ +using fNbt; +using MineSharp.Core.Common; + +namespace MineSharp.Core.Serialization; + +/// +/// Provides extension methods for NbtCompound. +/// +public static class NbtExtensions +{ + /// + /// Normalizes the top-level identifiers in the given NbtCompound. + /// This is required when using the registry of a 1.21 server via ViaProxy. + /// + /// The NbtCompound to normalize. + public static NbtCompound NormalizeRegistryDataTopLevelIdentifiers(this NbtCompound compound) + { + // we need to make copies of every tag because fNbt doesn't allow modifying the collection while iterating + // and there is no way to clear the parent of a tag + var newCompound = new NbtCompound(); + foreach (var tag in compound) + { + var identifier = Identifier.Parse(tag.Name); + var newTag = (NbtTag)tag.Clone(); + newTag.Name = identifier.ToCompleteIdentifier().ToString(); + newCompound.Add(newTag); + } + + return newCompound; + } +} diff --git a/MineSharp.Core/Common/PacketBuffer.cs b/MineSharp.Core/Serialization/PacketBuffer.cs similarity index 53% rename from MineSharp.Core/Common/PacketBuffer.cs rename to MineSharp.Core/Serialization/PacketBuffer.cs index d4c8e7b1..8002a8a9 100644 --- a/MineSharp.Core/Common/PacketBuffer.cs +++ b/MineSharp.Core/Serialization/PacketBuffer.cs @@ -1,9 +1,13 @@ -using System.Text; -using System.Text.RegularExpressions; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; using fNbt; +using MineSharp.Core.Common; using MineSharp.Core.Common.Blocks; +using MineSharp.Core.Exceptions; +using MineSharp.Core.Geometry; -namespace MineSharp.Core.Common; +namespace MineSharp.Core.Serialization; /// /// Read and write values from and to a byte buffer. @@ -30,7 +34,7 @@ public PacketBuffer(int protocolVersion) public PacketBuffer(byte[] bytes, int protocolVersion) { ProtocolVersion = protocolVersion; - buffer = new(bytes); + buffer = new(bytes, false); } /// @@ -76,7 +80,7 @@ public void Dispose() } /// - /// Return the buffer's byte array + /// Return a copy of the buffer's byte array /// /// public byte[] GetBuffer() @@ -91,8 +95,45 @@ public byte[] GetBuffer() /// public string HexDump(bool cutToPosition = false) { - var hex = Convert.ToHexString(GetBuffer().Skip(cutToPosition ? (int)Position : 0).ToArray()); - return Regex.Replace(hex, ".{2}", "$0 ").TrimEnd(); + byte[] dataToDump; + if (cutToPosition) + { + dataToDump = new byte[ReadableBytes]; + PeekBytes(dataToDump); + } + else + { + dataToDump = GetBuffer(); + } + + if (dataToDump.Length == 0) + { + return string.Empty; + } + + var hexStringBuffer = new char[dataToDump.Length * 3 - 1]; + var hexStringBufferSpan = new Span(hexStringBuffer); + for (var i = 0; i < dataToDump.Length; i++) + { + var b = dataToDump[i]; + b.TryFormat(hexStringBufferSpan.Slice(i * 3, 2), out _, "X2"); + if (i != dataToDump.Length - 1) + { + // Add a space between each byte + // but not at the very end + hexStringBufferSpan[i * 3 + 2] = ' '; + } + } + return new string(hexStringBuffer); + } + + private void PeekBytes(Span bytes) + { + EnsureEnoughReadableBytes(bytes.Length); + + var oldPosition = buffer.Position; + buffer.Read(bytes); + buffer.Position = oldPosition; } private void EnsureEnoughReadableBytes(int count) @@ -107,6 +148,31 @@ private void EnsureEnoughReadableBytes(int count) #region Reading + /// + /// Read an unmanaged value from the buffer. + /// This works great for signed and unsigned integer primitives. + /// For other value types or custom structs this will likely not work (on some systems). + /// Because this method just reverses the bytes if the system is little endian. + /// + /// The unmanaged type to read. + /// + public T Read() + where T : unmanaged + { + Unsafe.SkipInit(out T value); + var typedSpan = MemoryMarshal.CreateSpan(ref value, 1); + var byteSpan = MemoryMarshal.AsBytes(typedSpan); + + ReadBytes(byteSpan); + + if (BitConverter.IsLittleEndian) + { + byteSpan.Reverse(); + } + + return value; + } + public int ReadBytes(Span bytes) { EnsureEnoughReadableBytes(bytes.Length); @@ -144,125 +210,89 @@ public byte ReadByte() return (byte)buffer.ReadByte(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public sbyte ReadSByte() { return (sbyte)ReadByte(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool ReadBool() { return ReadByte() == 1; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public ushort ReadUShort() { - EnsureEnoughReadableBytes(2); - - var b0 = buffer.ReadByte(); - var b1 = buffer.ReadByte(); - - if (BitConverter.IsLittleEndian) - { - return (ushort)((b0 << 8) | b1); - } - - return (ushort)(b0 | (b1 << 8)); + return Read(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public short ReadShort() { - return (short)ReadUShort(); + return Read(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public uint ReadUInt() { - EnsureEnoughReadableBytes(4); - - var b0 = buffer.ReadByte(); - var b1 = buffer.ReadByte(); - var b2 = buffer.ReadByte(); - var b3 = buffer.ReadByte(); - - if (BitConverter.IsLittleEndian) - { - return (uint)((b0 << 24) | (b1 << 16) | (b2 << 8) | b3); - } - - return (uint)(b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)); + return Read(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public int ReadInt() { - return (int)ReadUInt(); + return Read(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public ulong ReadULong() { - EnsureEnoughReadableBytes(8); - - long b0 = buffer.ReadByte(); - long b1 = buffer.ReadByte(); - long b2 = buffer.ReadByte(); - long b3 = buffer.ReadByte(); - long b4 = buffer.ReadByte(); - long b5 = buffer.ReadByte(); - long b6 = buffer.ReadByte(); - long b7 = buffer.ReadByte(); - - if (BitConverter.IsLittleEndian) - { - return (ulong)((b0 << 56) | (b1 << 48) | (b2 << 40) | (b3 << 32) | (b4 << 24) | (b5 << 16) | (b6 << 8) | - b7); - } - - return (ulong)(b0 | (b1 << 8) | (b2 << 16) | (b3 << 24) | (b4 << 32) | (b5 << 40) | (b6 << 48) | (b7 << 56)); + return Read(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public long ReadLong() { - return (long)ReadULong(); + return Read(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public float ReadFloat() { - EnsureEnoughReadableBytes(4); - - Span bytes = stackalloc byte[sizeof(float)]; - ReadBytes(bytes); - - if (BitConverter.IsLittleEndian) - { - bytes.Reverse(); - } - - return BitConverter.ToSingle(bytes); + var bits = Read(); + return BitConverter.UInt32BitsToSingle(bits); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public double ReadDouble() { - EnsureEnoughReadableBytes(8); - - Span bytes = stackalloc byte[sizeof(double)]; - ReadBytes(bytes); - - if (BitConverter.IsLittleEndian) - { - bytes.Reverse(); - } - - return BitConverter.ToDouble(bytes); + var bits = Read(); + return BitConverter.UInt64BitsToDouble(bits); } - public int ReadVarInt() + private const int VarIntSegmentBits = 0x7F; + private const int VarIntContinueBit = 0x80; + + // This method is also used by MinecraftStream + public static int ReadVarInt(Stream stream, out int byteCount) { var value = 0; var shift = 0; + byteCount = 0; while (true) { - var b = ReadByte(); - value |= (b & 0x7f) << shift; - if ((b & 0x80) == 0x00) + var b = stream.ReadByte(); + if (b == -1) + { + throw new EndOfStreamException(); + } + + byteCount++; + value |= (b & VarIntSegmentBits) << shift; + if ((b & VarIntContinueBit) == 0) { break; } @@ -270,13 +300,19 @@ public int ReadVarInt() shift += 7; if (shift >= 32) { - throw new("varint is too big"); + throw new SerializationException("VarInt is too big."); } } return value; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int ReadVarInt() + { + return ReadVarInt(buffer, out _); + } + public long ReadVarLong() { long value = 0; @@ -285,8 +321,8 @@ public long ReadVarLong() while (true) { var b = ReadByte(); - value |= (b & (long)0x7f) << shift; - if ((b & 0x80) == 0x00) + value |= (long)(b & VarIntSegmentBits) << shift; + if ((b & VarIntContinueBit) == 0) { break; } @@ -294,7 +330,7 @@ public long ReadVarLong() shift += 7; if (shift >= 64) { - throw new("varlong is too big"); + throw new SerializationException("VarLong is too big."); } } @@ -312,11 +348,25 @@ public string ReadString(Encoding? encoding = null) return encoding.GetString(bytes); } + public Identifier ReadIdentifier() + { + var str = ReadString(); + // if the string is not a valid identifier, it will throw an exception + return Identifier.Parse(str); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] public Uuid ReadUuid() { - var l1 = ReadLong(); - var l2 = ReadLong(); - return new(l1, l2); + Span bytes = stackalloc byte[16]; + ReadBytes(bytes); + return new(bytes); + } + + public BitSet ReadBitSet() + { + var longs = ReadLongArray(); + return BitSet.Create(MemoryMarshal.Cast(longs)); } public T[] ReadVarIntArray(Func reader) @@ -331,6 +381,19 @@ public T[] ReadVarIntArray(Func reader) return array; } + public long[] ReadLongArray() + { + var length = ReadVarInt(); + var array = new long[length]; + + for (var i = 0; i < array.Length; i++) + { + array[i] = Read(); + } + + return array; + } + public byte[] RestBuffer() { var bytes = new byte[ReadableBytes]; @@ -372,7 +435,7 @@ public NbtCompound ReadNbtCompound() public BlockEntity ReadBlockEntity() { var packedXz = ReadByte(); - var x = (byte)((packedXz >> 4) & 0xF); + var x = (byte)(packedXz >> 4 & 0xF); var z = (byte)(packedXz & 0xF); var y = ReadShort(); var type = ReadVarInt(); @@ -381,7 +444,13 @@ public BlockEntity ReadBlockEntity() return new(x, y, z, type, nbt); } - public T? ReadOptional() where T : class + public Position ReadPosition() + { + return new Position(ReadULong()); + } + + public T? ReadOptional() + where T : class { var available = ReadBool(); if (!available) @@ -389,10 +458,11 @@ public BlockEntity ReadBlockEntity() return null; } - return Read(); + return ReadObject(); } - public T? ReadOptional(bool _ = false) where T : struct + public T? ReadOptional(bool _ = false) + where T : unmanaged { var available = ReadBool(); if (!available) @@ -403,27 +473,14 @@ public BlockEntity ReadBlockEntity() return Read(); } - - public T Read() + public T ReadObject() + where T : class { var type = Type.GetTypeCode(typeof(T)); object value = type switch { - TypeCode.Boolean => ReadBool(), - TypeCode.SByte => ReadSByte(), - TypeCode.Byte => ReadByte(), - TypeCode.Int16 => ReadShort(), - TypeCode.Int32 => ReadInt(), - TypeCode.Int64 => ReadLong(), - - TypeCode.UInt16 => ReadUShort(), - TypeCode.UInt32 => ReadUInt(), - TypeCode.UInt64 => ReadULong(), - TypeCode.String => ReadString(), - TypeCode.Single => ReadFloat(), - TypeCode.Double => ReadBool(), _ => throw new NotSupportedException() }; return (T)value; @@ -434,132 +491,136 @@ public T Read() #region Writing - public void WriteBytes(Span bytes) + /// + /// Writes an unmanaged value to the buffer. + /// This works great for signed and unsigned integer primitives. + /// For other value types or custom structs this will likely not work (on some systems). + /// Because this method just reverses the bytes if the system is little endian. + /// + /// The unmanaged type to write. + /// + public void Write(T value) + where T : unmanaged + { + var typedSpan = MemoryMarshal.CreateSpan(ref value, 1); + var byteSpan = MemoryMarshal.AsBytes(typedSpan); + + if (BitConverter.IsLittleEndian) + { + byteSpan.Reverse(); + } + + WriteBytes(byteSpan); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void WriteBytes(ReadOnlySpan bytes) { buffer.Write(bytes); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteBool(bool value) { WriteByte((byte)(value ? 1 : 0)); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteByte(byte b) { buffer.WriteByte(b); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteSByte(sbyte b) { WriteByte((byte)b); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteUShort(ushort value) { - if (BitConverter.IsLittleEndian) - { - WriteByte((byte)((value >> 8) & 0xFF)); - WriteByte((byte)(value & 0xFF)); - return; - } - - WriteByte((byte)(value & 0xFF)); - WriteByte((byte)((value >> 8) & 0xFF)); + Write(value); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteShort(short value) { - WriteUShort((ushort)value); + Write(value); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteUInt(uint value) { - if (BitConverter.IsLittleEndian) - { - WriteByte((byte)((value >> 24) & 0xFF)); - WriteByte((byte)((value >> 16) & 0xFF)); - WriteByte((byte)((value >> 8) & 0xFF)); - WriteByte((byte)(value & 0xFF)); - return; - } - - WriteByte((byte)(value & 0xFF)); - WriteByte((byte)((value >> 8) & 0xFF)); - WriteByte((byte)((value >> 16) & 0xFF)); - WriteByte((byte)((value >> 24) & 0xFF)); + Write(value); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteInt(int value) { - WriteUInt((uint)value); + Write(value); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteULong(ulong value) { - if (BitConverter.IsLittleEndian) - { - WriteByte((byte)((value >> 56) & 0xFF)); - WriteByte((byte)((value >> 48) & 0xFF)); - WriteByte((byte)((value >> 40) & 0xFF)); - WriteByte((byte)((value >> 32) & 0xFF)); - WriteByte((byte)((value >> 24) & 0xFF)); - WriteByte((byte)((value >> 16) & 0xFF)); - WriteByte((byte)((value >> 8) & 0xFF)); - WriteByte((byte)(value & 0xFF)); - return; - } - - WriteByte((byte)(value & 0xFF)); - WriteByte((byte)((value >> 8) & 0xFF)); - WriteByte((byte)((value >> 16) & 0xFF)); - WriteByte((byte)((value >> 24) & 0xFF)); - WriteByte((byte)((value >> 32) & 0xFF)); - WriteByte((byte)((value >> 40) & 0xFF)); - WriteByte((byte)((value >> 48) & 0xFF)); - WriteByte((byte)((value >> 56) & 0xFF)); + Write(value); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteLong(long value) { - WriteULong((ulong)value); + Write(value); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteFloat(float value) { var val = BitConverter.SingleToUInt32Bits(value); - WriteUInt(val); + Write(val); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public void WriteDouble(double value) { var val = BitConverter.DoubleToUInt64Bits(value); - WriteULong(val); + Write(val); } - public void WriteVarInt(int value) + // Is also used by MinecraftStream + public static void WriteVarInt(Stream stream, int value) { while (true) { - if ((value & ~0x7F) == 0) + if ((value & ~VarIntSegmentBits) == 0) { - buffer.WriteByte((byte)value); + stream.WriteByte((byte)value); return; } - buffer.WriteByte((byte)((value & 0x7F) | 0x80)); + stream.WriteByte((byte)(value & VarIntSegmentBits | VarIntContinueBit)); value >>>= 7; } } - public void WriteVarLong(int value) + public void WriteVarInt(int value) + { + WriteVarInt(buffer, value); + } + + public void WriteVarLong(long value) { - while ((value & ~0x7F) != 0x00) + while (true) { - buffer.WriteByte((byte)((value & 0xFF) | 0x80)); + if ((value & ~VarIntSegmentBits) == 0) + { + WriteByte((byte)value); + return; + } + + WriteByte((byte)(value & VarIntSegmentBits | VarIntContinueBit)); value >>>= 7; } - - buffer.WriteByte((byte)value); } public void WriteString(string value, Encoding? encoding = null) @@ -573,10 +634,23 @@ public void WriteString(string value, Encoding? encoding = null) WriteBytes(bytes); } + public void WriteIdentifier(Identifier identifier) + { + var str = identifier.ToString(); + WriteString(str); + } + public void WriteUuid(Uuid value) { - WriteLong(value.MostSignificantBits); - WriteLong(value.LeastSignificantBits); + Span bytes = stackalloc byte[16]; + value.WriteTo(bytes); + WriteBytes(bytes); + } + + public void WriteBitSet(BitSet bitSet) + { + var longs = bitSet.ToLongArray(); + WriteLongArray(MemoryMarshal.Cast(longs)); } public void WriteVarIntArray(ICollection collection, Action writer) @@ -589,6 +663,15 @@ public void WriteVarIntArray(ICollection collection, Action array) + { + WriteVarInt(array.Length); + foreach (var l in array) + { + WriteLong(l); + } + } + public void WriteNbt(NbtTag tag) { var f = new NbtFile(tag) { BigEndian = true, Anonymous = UseAnonymousNbt }; @@ -596,25 +679,30 @@ public void WriteNbt(NbtTag tag) } public void WriteOptionalNbt(NbtTag? tag) - { + { if (tag is null or NbtCompound { Count: 0 }) { buffer.WriteByte((byte)NbtTagType.End); return; } - + WriteNbt(tag); } public void WriteBlockEntity(BlockEntity entity) { - var packedXz = (byte)(((entity.X << 4) & 0xF) | (entity.Z & 0xF)); + var packedXz = (byte)(entity.X << 4 & 0xF | entity.Z & 0xF); WriteByte(packedXz); WriteShort(entity.Y); WriteVarInt(entity.Type); WriteOptionalNbt(entity.Data); } + public void WritePosition(Position position) + { + WriteULong(position.ToULong()); + } + #endregion #pragma warning restore CS1591 diff --git a/MineSharp.Core/Serialization/RegexHelper.cs b/MineSharp.Core/Serialization/RegexHelper.cs new file mode 100644 index 00000000..1835b23d --- /dev/null +++ b/MineSharp.Core/Serialization/RegexHelper.cs @@ -0,0 +1,25 @@ +using System.Text.RegularExpressions; + +namespace MineSharp.Core.Serialization; + +/// +/// Provides helper methods for working with regular expressions. +/// +public static class RegexHelper +{ + /// + /// Matches the entire input string against the regular expression. + /// + /// The regular expression to match against. + /// The input string to match. + /// A object if the entire input string matches the regular expression; otherwise, null. + public static Match? MatchEntireString(this Regex regex, string input) + { + var match = regex.Match(input); + if (match.Success && match.Value.Length == input.Length) + { + return match; + } + return null; + } +}