Skip to content

Commit 18df3a1

Browse files
authored
Implement some clientbound packets (#480)
1 parent 422f73c commit 18df3a1

17 files changed

+415
-25
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
11
namespace Obsidian.API;
2+
3+
/// <summary>
4+
/// Interface for network serializable objects. Due to the nature of Obsidian being a server-side only software,
5+
/// both <see cref="Write"/> and <see cref="Read"/> methods are not always necessary to implement. In that case,
6+
/// a <see cref="NotImplementedException"/> is thrown when the method is unintendedly called.
7+
/// </summary>
8+
/// <typeparam name="TValue">Type of the value to be serialized.</typeparam>
29
public interface INetworkSerializable<TValue>
310
{
11+
/// <summary>
12+
/// Writes the value to the network stream.
13+
/// </summary>
14+
/// <param name="value">Value to be serialized.</param>
15+
/// <param name="writer">Network stream writer.</param>
416
public static abstract void Write(TValue value, INetStreamWriter writer);
17+
18+
/// <summary>
19+
/// Reads the value from the network stream.
20+
/// </summary>
21+
/// <param name="reader">Network stream reader.</param>
22+
/// <returns>Deserialized value.</returns>
523
public static abstract TValue Read(INetStreamReader reader);
624
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
11
namespace Obsidian.API;
2+
3+
/// <summary>
4+
/// Represents a network packet that can be transferred between the client and the server.
5+
/// </summary>
26
public interface IPacket
37
{
8+
/// <summary>
9+
/// The ID of the packet.
10+
/// </summary>
411
public int Id { get; }
512
}
613

14+
/// <summary>
15+
/// Represents a network packet that can be sent to the client by the server.
16+
/// </summary>
717
public interface IClientboundPacket : IPacket
818
{
19+
/// <summary>
20+
/// Writes the data content of the packet to the stream.
21+
/// </summary>
22+
/// <param name="writer">The stream writer to write the data to.</param>
923
public void Serialize(INetStreamWriter writer);
1024
}

Obsidian.API/_Types/TradeEntry.cs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
using Obsidian.API.Inventory;
2+
3+
namespace Obsidian.API;
4+
5+
/// <summary>
6+
/// Represents a trade entry offered by a villager or wandering trader.
7+
/// </summary>
8+
public sealed record TradeEntry : INetworkSerializable<TradeEntry>
9+
{
10+
/// <summary>
11+
/// The first input item of the trade. The required count of the first input is the "base price" of the trade.
12+
/// The final price = base + floor(base * <see cref="Multiplier"/> * <see cref="Demand"/>) + <see cref="Discount"/>.
13+
/// </summary>
14+
public TradeItem FirstInput { get; set; }
15+
16+
/// <summary>
17+
/// The output item of the trade.
18+
/// </summary>
19+
public ItemStack Output { get; set; }
20+
21+
/// <summary>
22+
/// The second input item of the trade. The required count of the second input is not affected by discount
23+
/// or demand.
24+
/// </summary>
25+
public TradeItem SecondInput { get; set; }
26+
27+
/// <summary>
28+
/// Whether the trade is disabled.
29+
/// </summary>
30+
public bool IsDisabled { get; set; }
31+
32+
/// <summary>
33+
/// Number of times the trade has been used.
34+
/// </summary>
35+
public int UsedCount { get; set; }
36+
37+
/// <summary>
38+
/// The maximum number of times the trade can be used before it becomes disabled.
39+
/// </summary>
40+
public int MaxCount { get; set; }
41+
42+
/// <summary>
43+
/// Number of XPs the villager will earn after each trade.
44+
/// </summary>
45+
public int XP { get; set; }
46+
47+
/// <summary>
48+
/// Used in calculating the final price. Can be zero or negative.
49+
/// </summary>
50+
public int Discount { get; set; }
51+
52+
/// <summary>
53+
/// Used in calculating the final price. Can be low (0.05) or high (0.2).
54+
/// </summary>
55+
public float Multiplier { get; set; }
56+
57+
/// <summary>
58+
/// Used in calculating the final price. Negative values are treated as zero.
59+
/// </summary>
60+
public int Demand { get; set; }
61+
62+
public static void Write(TradeEntry value, INetStreamWriter writer)
63+
{
64+
TradeItem.Write(value.FirstInput, writer);
65+
writer.WriteItemStack(value.Output);
66+
TradeItem.Write(value.SecondInput, writer);
67+
writer.WriteBoolean(value.IsDisabled);
68+
writer.WriteInt(value.UsedCount);
69+
writer.WriteInt(value.MaxCount);
70+
writer.WriteInt(value.XP);
71+
writer.WriteInt(value.Discount);
72+
writer.WriteFloat(value.Multiplier);
73+
writer.WriteInt(value.Demand);
74+
}
75+
76+
// No need to implement
77+
public static TradeEntry Read(INetStreamReader reader) => throw new NotImplementedException();
78+
}

Obsidian.API/_Types/TradeItem.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using Obsidian.API.Inventory;
2+
3+
namespace Obsidian.API;
4+
5+
/// <summary>
6+
/// Represents an item stack supplied as the "price" in a <see cref="TradeEntry"/>.
7+
/// </summary>
8+
public sealed record TradeItem : INetworkSerializable<TradeItem>
9+
{
10+
/// <summary>
11+
/// The item ID.
12+
/// </summary>
13+
public int ID { get; set; }
14+
15+
/// <summary>
16+
/// The item count.
17+
/// </summary>
18+
public int Count { get; set; }
19+
20+
/// <summary>
21+
/// The item's components. An item stack must match all the component requirements to be considered a
22+
/// valid input.
23+
/// </summary>
24+
public List<IDataComponent> Components { get; set; }
25+
26+
/// <summary>
27+
/// Initializes a new instance of the <see cref="TradeItem"/> record.
28+
/// </summary>
29+
/// <param name="id">The item ID.</param>
30+
/// <param name="count">The item count.</param>
31+
/// <param name="components">The item's components.</param>
32+
public TradeItem(int id, int count, List<IDataComponent> components)
33+
{
34+
ID = id;
35+
Count = count;
36+
Components = components;
37+
}
38+
39+
public static void Write(TradeItem value, INetStreamWriter writer)
40+
{
41+
writer.WriteVarInt(value.ID);
42+
writer.WriteVarInt(value.Count);
43+
writer.WriteLengthPrefixedArray((c) =>
44+
{
45+
writer.WriteVarInt(c.Type);
46+
c.Write(writer);
47+
}, value.Components);
48+
}
49+
50+
// No need to implement
51+
public static TradeItem Read(INetStreamReader reader) => throw new NotImplementedException();
52+
}

Obsidian/Entities/Entity.cs

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -400,26 +400,26 @@ public virtual ValueTask TeleportAsync(VectorF pos)
400400
public virtual void SpawnEntity(Velocity? velocity = null, int additionalData = 0)
401401
{
402402
this.PacketBroadcaster.QueuePacketToWorldInRange(this.World, this.Position, new BundledPacket
403-
{
404-
Packets = [
405-
new AddEntityPacket
406-
{
407-
EntityId = this.EntityId,
408-
Uuid = this.Uuid,
409-
Type = this.Type,
410-
Position = this.Position,
411-
Pitch = this.Pitch,
412-
Yaw = this.Yaw,
413-
Data = additionalData,
414-
Velocity = velocity ?? new Velocity(0, 0, 0)
415-
},
416-
new SetEntityDataPacket
417-
{
418-
EntityId = this.EntityId,
419-
Entity = this
420-
}
421-
]
422-
}, this.EntityId);
403+
(
404+
[
405+
new AddEntityPacket
406+
{
407+
EntityId = this.EntityId,
408+
Uuid = this.Uuid,
409+
Type = this.Type,
410+
Position = this.Position,
411+
Pitch = this.Pitch,
412+
Yaw = this.Yaw,
413+
Data = additionalData,
414+
Velocity = velocity ?? new Velocity(0, 0, 0)
415+
},
416+
new SetEntityDataPacket
417+
{
418+
EntityId = this.EntityId,
419+
Entity = this
420+
}
421+
]
422+
), this.EntityId);
423423
}
424424

425425
public bool TryAddAttribute(string attributeResourceName, float value) =>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Obsidian.Net.Packets.Play.Clientbound;
2+
3+
/// <summary>
4+
/// Used to wrap a <see cref="BundledPacket"/>.
5+
/// </summary>
6+
public partial class BundleDelimiterPacket
7+
{ }

Obsidian/Net/Packets/Play/Clientbound/BundledPacket.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
namespace Obsidian.Net.Packets.Play.Clientbound;
2-
public partial class BundledPacket : ClientboundPacket
2+
3+
/// <summary>
4+
/// Represents a bundle of packets that the client should handle in the same tick.
5+
/// A <see cref="BundleDelimiterPacket"/> is sent before and after the content packets are sent to the client.
6+
/// Vanilla Minecraft client doesn't allow more than 4096 packets in one bundle.
7+
/// </summary>
8+
public partial class BundledPacket(List<ClientboundPacket> packets) : ClientboundPacket
39
{
4-
public required List<ClientboundPacket> Packets { get; set; }
10+
/// <summary>
11+
/// The packets in this bundle.
12+
/// </summary>
13+
public List<ClientboundPacket> Packets { get; set; } = packets;
514

615
public override int Id => 0;
716

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
using Obsidian.Serialization.Attributes;
2+
3+
namespace Obsidian.Net.Packets.Play.Clientbound;
4+
5+
/// <summary>
6+
/// Sent to tell the client to open the horse GUI. Opening other GUIs are done via <see cref="OpenScreenPacket"/>.
7+
/// </summary>
8+
/// <remarks>
9+
/// Initializes a new instance of the <see cref="HorseScreenOpenPacket"/> class.
10+
/// </remarks>
11+
/// <param name="windowId">The identifier of the window to open.</param>
12+
/// <param name="columnCount">How many columns the horse inventory should have.</param>
13+
/// <param name="entityId">The owner entity of the window.</param>
14+
public partial class HorseScreenOpenPacket(int windowId, int columnCount, int entityId)
15+
{
16+
/// <summary>
17+
/// The identifier of the window to open.
18+
/// </summary>
19+
[Field(0), VarLength]
20+
public int WindowID { get; set; } = windowId;
21+
22+
/// <summary>
23+
/// How many columns the horse inventory should have.
24+
/// </summary>
25+
[Field(1), VarLength]
26+
public int ColumnCount { get; set; } = columnCount;
27+
28+
/// <summary>
29+
/// The owner entity of the window.
30+
/// </summary>
31+
[Field(2)]
32+
public int EntityID { get; set; } = entityId;
33+
34+
public override void Serialize(INetStreamWriter writer)
35+
{
36+
writer.WriteVarInt(WindowID);
37+
writer.WriteVarInt(ColumnCount);
38+
writer.WriteInt(EntityID);
39+
}
40+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using Obsidian.API;
2+
using Obsidian.Serialization.Attributes;
3+
4+
namespace Obsidian.Net.Packets.Play.Clientbound;
5+
6+
/// <summary>
7+
/// The list of trades a villager is offering.
8+
/// </summary>
9+
public partial class MerchantOffersPacket
10+
{
11+
/// <summary>
12+
/// The window ID of the villager's trade list.
13+
/// </summary>
14+
[Field(0), VarLength]
15+
public int WindowId { get; set; }
16+
17+
/// <summary>
18+
/// The offered trades list.
19+
/// </summary>
20+
[Field(1)]
21+
public List<TradeEntry> Offers { get; set; }
22+
23+
/// <summary>
24+
/// The level of the villager. 1: Novice, 2: Apprentice, 3: Journeyman, 4: Expert, 5: Master.
25+
/// </summary>
26+
[Field(2), VarLength]
27+
public int VillagerLevel { get; set; }
28+
29+
/// <summary>
30+
/// The total experience of the villager. Always 0 for wandering traders.
31+
/// </summary>
32+
[Field(3), VarLength]
33+
public int VillagerExperience { get; set; }
34+
35+
/// <summary>
36+
/// True if the villager is a regular villager, false if it is a wandering trader.
37+
/// </summary>
38+
[Field(4)]
39+
public bool IsRegularVillager { get; set; }
40+
41+
/// <summary>
42+
/// True if the villager can restock their inventory, false otherwise.
43+
/// </summary>
44+
[Field(5)]
45+
public bool CanRestock { get; set; }
46+
47+
public override void Serialize(INetStreamWriter writer)
48+
{
49+
writer.WriteVarInt(WindowId);
50+
writer.WriteLengthPrefixedArray((o) => TradeEntry.Write(o, writer), Offers);
51+
writer.WriteVarInt(VillagerLevel);
52+
writer.WriteVarInt(VillagerExperience);
53+
writer.WriteBoolean(IsRegularVillager);
54+
writer.WriteBoolean(CanRestock);
55+
}
56+
}

Obsidian/Net/Packets/Play/Clientbound/OpenBookPacket.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@
22

33
namespace Obsidian.Net.Packets.Play.Clientbound;
44

5+
/// <summary>
6+
/// Sent to the client to open a book GUI.
7+
/// </summary>
58
public partial class OpenBookPacket
69
{
10+
/// <summary>
11+
/// The hand that is holding the book.
12+
/// </summary>
713
[Field(0), ActualType(typeof(int)), VarLength]
814
public Hand Hand { get; set; }
915

0 commit comments

Comments
 (0)