diff --git a/Obsidian.API/Configuration/ServerConfiguration.cs b/Obsidian.API/Configuration/ServerConfiguration.cs index 9304fa2b6..cba40e458 100644 --- a/Obsidian.API/Configuration/ServerConfiguration.cs +++ b/Obsidian.API/Configuration/ServerConfiguration.cs @@ -1,15 +1,16 @@ -using Obsidian.API.Configuration; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace Obsidian.API.Configuration; public sealed class ServerConfiguration { private byte viewDistance = 10; + private byte simulationDistance = 10; + private ushort entityBroadcastRangePercentage = 100; // Anything lower than 3 will cause weird artifacts on the client. private const byte MinimumViewDistance = 3; - + private const byte MinimumSimulationDistance = 5; /// /// Enabled Remote Console operation. /// @@ -84,6 +85,18 @@ public byte ViewDistance set => viewDistance = value >= MinimumViewDistance ? value : MinimumViewDistance; } + public byte SimulationDistance + { + get => simulationDistance; + set => simulationDistance = value > this.ViewDistance ? value >= MinimumSimulationDistance ? value : MinimumSimulationDistance : ViewDistance; + } + + public ushort EntityBroadcastRangePercentage + { + get => entityBroadcastRangePercentage; + set => Math.Max((ushort)10, value); + } + public int PregenerateChunkRange { get; set; } = 15; // by default, pregenerate range from -15 to 15; public ServerListQuery ServerListQuery { get; set; } = ServerListQuery.Full; diff --git a/Obsidian.API/_Enums/Pose.cs b/Obsidian.API/_Enums/Pose.cs index b5e7b7b05..6ee351e93 100644 --- a/Obsidian.API/_Enums/Pose.cs +++ b/Obsidian.API/_Enums/Pose.cs @@ -14,5 +14,21 @@ public enum Pose : int Sneaking, - Dying + LongJumper, + + Dying, + + Croaking, + + UsingTongue, + + Sitting, + + Roaring, + + Smiffing, + + Emerging, + + Digging } diff --git a/Obsidian.API/_Interfaces/IEntity.cs b/Obsidian.API/_Interfaces/IEntity.cs index 3252d86bd..5692f9dfe 100644 --- a/Obsidian.API/_Interfaces/IEntity.cs +++ b/Obsidian.API/_Interfaces/IEntity.cs @@ -44,16 +44,25 @@ public interface IEntity public bool Summonable { get; } public bool IsFireImmune { get; } - public Task RemoveAsync(); - public Task TickAsync(); - public Task DamageAsync(IEntity source, float amount = 1.0f); + public ValueTask RemoveAsync(); + public ValueTask TickAsync(); + public ValueTask DamageAsync(IEntity source, float amount = 1.0f); - public Task KillAsync(IEntity source); - public Task KillAsync(IEntity source, ChatMessage message); + public ValueTask KillAsync(IEntity source); + public ValueTask KillAsync(IEntity source, ChatMessage message); - public Task TeleportAsync(IWorld world); - public Task TeleportAsync(IEntity to); - public Task TeleportAsync(VectorF pos); + public ValueTask TeleportAsync(IWorld world); + public ValueTask TeleportAsync(IEntity to); + public ValueTask TeleportAsync(VectorF pos); + + public bool IsInRange(IEntity entity, float distance); + + /// + /// Spawns the specified entity to player nearby in the world. + /// + /// The velocity the entity should spawn with + /// Additional data for the entity. More info here: + public void SpawnEntity(Velocity? velocity = null, int additionalData = 0); public IEnumerable GetEntitiesNear(float distance); diff --git a/Obsidian.API/_Interfaces/IWorld.cs b/Obsidian.API/_Interfaces/IWorld.cs index 2762ada58..502fdf79f 100644 --- a/Obsidian.API/_Interfaces/IWorld.cs +++ b/Obsidian.API/_Interfaces/IWorld.cs @@ -1,5 +1,3 @@ -using System.Collections.Concurrent; - namespace Obsidian.API; public interface IWorld : IAsyncDisposable @@ -20,20 +18,18 @@ public interface IWorld : IAsyncDisposable public int LoadedChunkCount { get; } public int ChunksToGenCount { get; } - public int GetTotalLoadedEntities(); - - public Task GetBlockAsync(Vector location); - public Task GetBlockAsync(int x, int y, int z); - public Task SetBlockAsync(Vector location, IBlock block); - public Task SetBlockAsync(int x, int y, int z, IBlock block); + public ValueTask GetBlockAsync(Vector location); + public ValueTask GetBlockAsync(int x, int y, int z); + public ValueTask SetBlockAsync(Vector location, IBlock block); + public ValueTask SetBlockAsync(int x, int y, int z, IBlock block); - public Task SetBlockUntrackedAsync(int x, int y, int z, IBlock block, bool doBlockUpdate); + public ValueTask SetBlockUntrackedAsync(int x, int y, int z, IBlock block, bool doBlockUpdate); - public Task SetBlockUntrackedAsync(Vector location, IBlock block, bool doBlockUpdate); + public ValueTask SetBlockUntrackedAsync(Vector location, IBlock block, bool doBlockUpdate); - public Task GetWorldSurfaceHeightAsync(int x, int z); + public ValueTask GetWorldSurfaceHeightAsync(int x, int z); - public Task SpawnEntityAsync(VectorF position, EntityType type); + public IEntity SpawnEntity(VectorF position, EntityType type); public void SpawnExperienceOrbs(VectorF position, short count); public Task DoWorldTickAsync(); diff --git a/Obsidian/Client.cs b/Obsidian/Client.cs index 2dfe7e169..ded0ce516 100644 --- a/Obsidian/Client.cs +++ b/Obsidian/Client.cs @@ -126,6 +126,7 @@ public sealed class Client : IDisposable /// The connection context associated with the . /// private readonly ConnectionContext connectionContext; + private readonly ILoggerFactory loggerFactory; /// /// Whether the stream has encryption enabled. This can be set to false when the client is connecting through LAN or when the server is in offline mode. @@ -155,7 +156,7 @@ public sealed class Client : IDisposable /// /// Used to log actions caused by the client. /// - public ILogger Logger { get; } + public ILogger Logger { get; private set; } /// /// The player that the client is logged in as. @@ -168,13 +169,13 @@ public sealed class Client : IDisposable /// public string? Brand { get; set; } - public Client(ConnectionContext connectionContext, int playerId, + public Client(ConnectionContext connectionContext, ILoggerFactory loggerFactory, IUserCache playerCache, Server server) { this.connectionContext = connectionContext; + this.loggerFactory = loggerFactory; - id = playerId; LoadedChunks = []; packetCryptography = new(); handler = new(server.Configuration); @@ -183,7 +184,7 @@ public Client(ConnectionContext connectionContext, int playerId, this.server = server; this.userCache = playerCache; - this.Logger = loggerFactory.CreateLogger($"Client{playerId}"); + this.Logger = loggerFactory.CreateLogger("ConnectionHandler"); missedKeepAlives = []; var linkOptions = new DataflowLinkOptions { PropagateCompletion = true }; @@ -330,7 +331,16 @@ public async Task StartConnectionAsync() if (result == EventResult.Cancelled) return; - await handler.HandlePlayPackets(id, data, this); + try + { + await handler.HandlePlayPackets(id, data, this); + } + catch (Exception ex) + { + this.Logger.LogDebug(ex, "Exception thrown"); + } + + break; case ClientState.Closed: default: @@ -442,6 +452,8 @@ private async Task HandleLoginStartAsync(byte[] data) return; } + this.InitializeId(); + Player = new Player(this.cachedUser.Uuid, loginStart.Username, this, world); packetCryptography.GenerateKeyPair(); @@ -462,6 +474,8 @@ private async Task HandleLoginStartAsync(byte[] data) } else { + this.InitializeId(); + Player = new Player(GuidHelper.FromStringHash($"OfflinePlayer:{username}"), username, this, world); this.SendPacket(new LoginSuccess(Player.Uuid, Player.Username) @@ -471,6 +485,12 @@ private async Task HandleLoginStartAsync(byte[] data) } } + private void InitializeId() + { + this.id = Server.GetNextEntityId(); + this.Logger = this.loggerFactory.CreateLogger($"Client({this.id})"); + } + private async Task HandleEncryptionResponseAsync(byte[] data) { if (Player is null) diff --git a/Obsidian/Commands/MainCommandModule.cs b/Obsidian/Commands/MainCommandModule.cs index 49a5228a0..0df371bb0 100644 --- a/Obsidian/Commands/MainCommandModule.cs +++ b/Obsidian/Commands/MainCommandModule.cs @@ -323,7 +323,7 @@ public async Task SpawnEntityAsync(string entityType) return; } - await player.World.SpawnEntityAsync(player.Position, type); + player.World.SpawnEntity(player.Position, type); await player.SendMessageAsync($"Spawning: {type}"); } @@ -342,7 +342,7 @@ public async Task DerpAsync(string entityType) return; } - var frogge = await player.World.SpawnEntityAsync(player.Position, type); + var frogge = player.World.SpawnEntity(player.Position, type); var server = (this.Server as Server)!; _ = Task.Run(async () => diff --git a/Obsidian/Entities/AbstractHorse.cs b/Obsidian/Entities/AbstractHorse.cs index 1df591984..0539fbf5b 100644 --- a/Obsidian/Entities/AbstractHorse.cs +++ b/Obsidian/Entities/AbstractHorse.cs @@ -15,7 +15,7 @@ public async override Task WriteAsync(MinecraftStream stream) await stream.WriteEntityMetdata(16, EntityMetadataType.Byte, this.HorseMask); if (this.Owner != default) - await stream.WriteEntityMetdata(17, EntityMetadataType.OptUuid, Owner, true); + await stream.WriteEntityMetdata(17, EntityMetadataType.OptionalUUID, Owner, true); } public override void Write(MinecraftStream stream) @@ -25,7 +25,7 @@ public override void Write(MinecraftStream stream) stream.WriteEntityMetadataType(16, EntityMetadataType.Byte); stream.WriteUnsignedByte((byte)HorseMask); - stream.WriteEntityMetadataType(17, EntityMetadataType.OptUuid); + stream.WriteEntityMetadataType(17, EntityMetadataType.OptionalUUID); stream.WriteBoolean(true); if (true) stream.WriteUuid(Owner); diff --git a/Obsidian/Entities/Entity.cs b/Obsidian/Entities/Entity.cs index c092f443b..4eb312bbb 100644 --- a/Obsidian/Entities/Entity.cs +++ b/Obsidian/Entities/Entity.cs @@ -68,7 +68,7 @@ public class Entity : IEquatable, IEntity public IGoalController? GoalController { get; set; } #region Update methods - internal virtual async Task UpdateAsync(VectorF position, bool onGround) + internal virtual async ValueTask UpdateAsync(VectorF position, bool onGround) { var isNewLocation = position != Position; @@ -76,7 +76,7 @@ internal virtual async Task UpdateAsync(VectorF position, bool onGround) { var delta = (Vector)((position * 32 - Position * 32) * 128); - this.PacketBroadcaster.BroadcastToWorld(this.World, new UpdateEntityPositionPacket + this.PacketBroadcaster.BroadcastToWorldInRange(this.World, position, new UpdateEntityPositionPacket { EntityId = EntityId, @@ -89,7 +89,7 @@ internal virtual async Task UpdateAsync(VectorF position, bool onGround) await UpdatePositionAsync(position, onGround); } - internal virtual async Task UpdateAsync(VectorF position, Angle yaw, Angle pitch, bool onGround) + internal virtual async ValueTask UpdateAsync(VectorF position, Angle yaw, Angle pitch, bool onGround) { var isNewLocation = position != Position; var isNewRotation = yaw != Yaw || pitch != Pitch; @@ -100,7 +100,7 @@ internal virtual async Task UpdateAsync(VectorF position, Angle yaw, Angle pitch if (isNewRotation) { - this.PacketBroadcaster.BroadcastToWorld(this.World, new UpdateEntityPositionAndRotationPacket + this.PacketBroadcaster.BroadcastToWorldInRange(this.World, position, new UpdateEntityPositionAndRotationPacket { EntityId = EntityId, @@ -116,7 +116,7 @@ internal virtual async Task UpdateAsync(VectorF position, Angle yaw, Angle pitch } else { - this.PacketBroadcaster.BroadcastToWorld(this.World, new UpdateEntityPositionPacket + this.PacketBroadcaster.BroadcastToWorldInRange(this.World, position, new UpdateEntityPositionPacket { EntityId = EntityId, @@ -130,7 +130,7 @@ internal virtual async Task UpdateAsync(VectorF position, Angle yaw, Angle pitch await UpdatePositionAsync(position, yaw, pitch, onGround); } - internal virtual Task UpdateAsync(Angle yaw, Angle pitch, bool onGround) + internal virtual ValueTask UpdateAsync(Angle yaw, Angle pitch, bool onGround) { var isNewRotation = yaw != Yaw || pitch != Pitch; @@ -140,11 +140,24 @@ internal virtual Task UpdateAsync(Angle yaw, Angle pitch, bool onGround) this.SetHeadRotation(yaw); } - return Task.CompletedTask; + return default; } + public bool IsInRange(IEntity entity, float distance) + { + if (this.World != entity.World) + return false; + + var locationDifference = LocationDiff.GetDifference(this.Position, entity.Position); + + distance *= distance; + + return locationDifference.CalculatedDifference <= distance; + } + + public void SetHeadRotation(Angle headYaw) => - this.PacketBroadcaster.BroadcastToWorld(this.World, new SetHeadRotationPacket + this.PacketBroadcaster.BroadcastToWorldInRange(this.World, this.Position, new SetHeadRotationPacket { EntityId = EntityId, HeadYaw = headYaw @@ -152,7 +165,7 @@ public void SetHeadRotation(Angle headYaw) => public void SetRotation(Angle yaw, Angle pitch, bool onGround = true) { - this.PacketBroadcaster.BroadcastToWorld(this.World, new UpdateEntityRotationPacket + this.PacketBroadcaster.BroadcastToWorldInRange(this.World, this.Position, new UpdateEntityRotationPacket { EntityId = EntityId, OnGround = onGround, @@ -214,11 +227,11 @@ public VectorF GetLookDirection() return new(-cosPitch * sinYaw, -sinPitch, cosPitch * cosYaw); } - public async Task RemoveAsync() => await this.world.DestroyEntityAsync(this); + public virtual async ValueTask RemoveAsync() => await this.world.DestroyEntityAsync(this); - private EntityBitMask GenerateBitmask() + protected EntityBitMask GenerateBitmask() { - var mask = EntityBitMask.None; + EntityBitMask mask = EntityBitMask.None; if (Sneaking) { @@ -252,7 +265,7 @@ public virtual async Task WriteAsync(MinecraftStream stream) await stream.WriteEntityMetdata(1, EntityMetadataType.VarInt, Air); - await stream.WriteEntityMetdata(2, EntityMetadataType.OptChat, CustomName!, CustomName != null); + await stream.WriteEntityMetdata(2, EntityMetadataType.OptionalTextComponent, CustomName!, CustomName != null); await stream.WriteEntityMetdata(3, EntityMetadataType.Boolean, CustomNameVisible); await stream.WriteEntityMetdata(4, EntityMetadataType.Boolean, Silent); @@ -270,7 +283,7 @@ public virtual void Write(MinecraftStream stream) stream.WriteEntityMetadataType(1, EntityMetadataType.VarInt); stream.WriteVarInt(Air); - stream.WriteEntityMetadataType(2, EntityMetadataType.OptChat); + stream.WriteEntityMetadataType(2, EntityMetadataType.OptionalTextComponent); stream.WriteBoolean(CustomName is not null); if (CustomName is not null) stream.WriteChat(CustomName); @@ -285,7 +298,7 @@ public virtual void Write(MinecraftStream stream) stream.WriteBoolean(NoGravity); stream.WriteEntityMetadataType(6, EntityMetadataType.Pose); - stream.WriteVarInt((int)Pose); + stream.WriteVarInt((int)this.Pose); stream.WriteEntityMetadataType(7, EntityMetadataType.VarInt); stream.WriteVarInt(PowderedSnowTicks); @@ -293,10 +306,11 @@ public virtual void Write(MinecraftStream stream) public IEnumerable GetEntitiesNear(float distance) => world.GetEntitiesInRange(Position, distance).Where(x => x != this); - public virtual Task TickAsync() => Task.CompletedTask; + //TODO GRAVITY + public virtual ValueTask TickAsync() => default; //TODO check for other entities and handle accordingly - public async Task DamageAsync(IEntity source, float amount = 1.0f) + public async ValueTask DamageAsync(IEntity source, float amount = 1.0f) { Health -= amount; @@ -305,7 +319,7 @@ public async Task DamageAsync(IEntity source, float amount = 1.0f) this.PacketBroadcaster.QueuePacketToWorld(this.World, new EntityAnimationPacket { EntityId = EntityId, - Animation = EntityAnimationType.TakeDamage + Animation = EntityAnimationType.CriticalEffect }); if (living is Player player) @@ -318,8 +332,8 @@ public async Task DamageAsync(IEntity source, float amount = 1.0f) } } - public virtual Task KillAsync(IEntity source) => Task.CompletedTask; - public virtual Task KillAsync(IEntity source, ChatMessage message) => Task.CompletedTask; + public virtual ValueTask KillAsync(IEntity source) => default; + public virtual ValueTask KillAsync(IEntity source, ChatMessage message) => default; public bool Equals([AllowNull] Entity other) { @@ -348,9 +362,9 @@ public bool Equals([AllowNull] Entity other) public override int GetHashCode() => EntityId.GetHashCode(); - public virtual Task TeleportAsync(IWorld world) => Task.CompletedTask; + public virtual ValueTask TeleportAsync(IWorld world) => default; - public async virtual Task TeleportAsync(IEntity to) + public async virtual ValueTask TeleportAsync(IEntity to) { if (to is not Entity target) return; @@ -360,56 +374,33 @@ public async virtual Task TeleportAsync(IEntity to) await world.DestroyEntityAsync(this); world = target.world; - await world.SpawnEntityAsync(to.Position, Type); - - return; - } - - if (VectorF.Distance(Position, to.Position) > 8) - { - this.PacketBroadcaster.QueuePacketToWorld(this.World, new TeleportEntityPacket - { - EntityId = EntityId, - OnGround = OnGround, - Position = to.Position, - Pitch = Pitch, - Yaw = Yaw, - }); + world.SpawnEntity(to.Position, Type); return; } - var delta = (Vector)(to.Position * 32 - Position * 32) * 128; - - this.PacketBroadcaster.QueuePacketToWorld(this.World, new UpdateEntityPositionAndRotationPacket - { - EntityId = EntityId, - Delta = delta, - OnGround = OnGround, - Pitch = Pitch, - Yaw = Yaw - }); + await this.TeleportAsync(to.Position); } - public async virtual Task TeleportAsync(VectorF pos) + public virtual ValueTask TeleportAsync(VectorF pos) { if (VectorF.Distance(Position, pos) > 8) { - this.PacketBroadcaster.QueuePacketToWorld(this.World, new TeleportEntityPacket + this.PacketBroadcaster.QueuePacketToWorld(this.World, 0, new TeleportEntityPacket { EntityId = EntityId, OnGround = OnGround, Position = pos, Pitch = Pitch, - Yaw = Yaw, + Yaw = Yaw }); - return; + return default; } var delta = (Vector)(pos * 32 - Position * 32) * 128; - this.PacketBroadcaster.QueuePacketToWorld(this.World, new UpdateEntityPositionAndRotationPacket + this.PacketBroadcaster.QueuePacketToWorld(this.World, 0, new UpdateEntityPositionAndRotationPacket { EntityId = EntityId, Delta = delta, @@ -417,6 +408,33 @@ public async virtual Task TeleportAsync(VectorF pos) Pitch = Pitch, Yaw = Yaw }); + + return default; + } + + public virtual void SpawnEntity(Velocity? velocity = null, int additionalData = 0) + { + this.PacketBroadcaster.QueuePacketToWorldInRange(this.World, this.Position, new BundledPacket + { + Packets = [ + new SpawnEntityPacket + { + EntityId = this.EntityId, + Uuid = this.Uuid, + Type = this.Type, + Position = this.Position, + Pitch = this.Pitch, + Yaw = this.Yaw, + Data = additionalData, + Velocity = velocity ?? new Velocity(0, 0, 0) + }, + new SetEntityMetadataPacket + { + EntityId = this.EntityId, + Entity = this + } + ] + }, this.EntityId); } public bool TryAddAttribute(string attributeResourceName, float value) => diff --git a/Obsidian/Entities/EntityMetadataType.cs b/Obsidian/Entities/EntityMetadataType.cs index fe6e70817..7106a14f0 100644 --- a/Obsidian/Entities/EntityMetadataType.cs +++ b/Obsidian/Entities/EntityMetadataType.cs @@ -2,53 +2,35 @@ public enum EntityMetadataType : int { - Byte, - - VarInt, - - VarLong, - - Float, - - String, - - Chat, - - OptChat, - - Slot, - - Boolean, - - Rotation, - - Position, - - OptPosition, - - Direction, - - OptUuid, - - BlockId, - - OptBlockId, - - Nbt, - - Particle, - - VillagerData, - - OptVarInt, - - Pose, - - CatVariant, - - FrogVariant, - - GlobalPos, - - PaintingVariant + Byte = 0, + VarInt = 1, + Long = 2, + Float = 3, + String = 4, + TextComponent = 5, + OptionalTextComponent = 6, + Slot = 7, + Boolean = 8, + Rotations = 9, + BlockPos = 10, + OptionalBlockPos = 11, + Direction = 12, + OptionalUUID = 13, + BlockState = 14, + OptionalBlockState = 15, + Nbt = 16, + Particle = 17, + Particles = 18, + VillagerData = 19, + OptionalUnsignedInt = 20, + Pose = 21, + CatVariant = 22, + WolfVariant = 23, + FrongVariant = 24, + OptionalGlobalPos = 25, + PaintingVariant = 26, + SnifferState = 27, + ArmadilloState = 28, + Vector3 = 29, + Quaternion = 30 } diff --git a/Obsidian/Entities/FallingBlock.cs b/Obsidian/Entities/FallingBlock.cs index 4ede94f3e..3a41d92a5 100644 --- a/Obsidian/Entities/FallingBlock.cs +++ b/Obsidian/Entities/FallingBlock.cs @@ -29,7 +29,7 @@ public FallingBlock(VectorF position) : base() DeltaPosition = VectorF.Zero; } - public async override Task TickAsync() + public async override ValueTask TickAsync() { AliveTime++; LastPosition = Position; diff --git a/Obsidian/Entities/ItemEntity.cs b/Obsidian/Entities/ItemEntity.cs index f88560983..faf2f048c 100644 --- a/Obsidian/Entities/ItemEntity.cs +++ b/Obsidian/Entities/ItemEntity.cs @@ -6,6 +6,8 @@ namespace Obsidian.Entities; [MinecraftEntity("minecraft:item")] public partial class ItemEntity : Entity { + private static readonly TimeSpan DropWaitTime = TimeSpan.FromSeconds(3); + public int Id { get; set; } public Material Material => ItemsRegistry.Get(this.Id).Type; @@ -20,7 +22,7 @@ public partial class ItemEntity : Entity public ItemEntity() => this.Type = EntityType.Item; - public override async Task WriteAsync(MinecraftStream stream) + public async override Task WriteAsync(MinecraftStream stream) { await base.WriteAsync(stream); @@ -35,24 +37,24 @@ public override void Write(MinecraftStream stream) stream.WriteItemStack(new ItemStack(this.Material, this.Count, this.ItemMeta)); } - public override async Task TickAsync() + public async override ValueTask TickAsync() { await base.TickAsync(); - if (!CanPickup && this.TimeDropped.Subtract(DateTimeOffset.UtcNow).TotalSeconds > 5) + if (!CanPickup && DateTimeOffset.UtcNow - this.TimeDropped > DropWaitTime) this.CanPickup = true; - foreach (var ent in this.world.GetNonPlayerEntitiesInRange(this.Position, 1.5f)) + foreach (var ent in this.world.GetNonPlayerEntitiesInRange(this.Position, 0.5f)) { - if (ent is ItemEntity item) - { - if (item == this) - continue; + if (ent is not ItemEntity item) + continue; + + if (item == this) + continue; - this.Count += item.Count; + this.Count += item.Count; - await item.RemoveAsync();//TODO find a better way to removed item entities that merged - } + await item.RemoveAsync();//TODO find a better way to removed item entities that merged } } } diff --git a/Obsidian/Entities/Living.cs b/Obsidian/Entities/Living.cs index 8c8eeef76..5c1be7eec 100644 --- a/Obsidian/Entities/Living.cs +++ b/Obsidian/Entities/Living.cs @@ -31,7 +31,7 @@ public Living() activePotionEffects = new ConcurrentDictionary(); } - public override Task TickAsync() + public override ValueTask TickAsync() { foreach (var (potion, data) in activePotionEffects) { @@ -43,7 +43,7 @@ public override Task TickAsync() } } - return Task.CompletedTask; + return default; } public bool HasPotionEffect(PotionEffect potion) @@ -86,7 +86,7 @@ public override async Task WriteAsync(MinecraftStream stream) await stream.WriteEntityMetdata(9, EntityMetadataType.Float, this.Health); - await stream.WriteEntityMetdata(10, EntityMetadataType.VarInt, (int)this.ActiveEffectColor); + await stream.WriteEntityMetdata(10, EntityMetadataType.Particles, (int)this.ActiveEffectColor); await stream.WriteEntityMetdata(11, EntityMetadataType.Boolean, this.AmbientPotionEffect); @@ -94,7 +94,7 @@ public override async Task WriteAsync(MinecraftStream stream) await stream.WriteEntityMetdata(13, EntityMetadataType.VarInt, this.AbsorbedStingers); - await stream.WriteEntityMetdata(14, EntityMetadataType.OptPosition, this.BedBlockPosition, this.BedBlockPosition != Vector.Zero); + await stream.WriteEntityMetdata(14, EntityMetadataType.OptionalBlockPos, this.BedBlockPosition, this.BedBlockPosition != Vector.Zero); } public override void Write(MinecraftStream stream) @@ -107,8 +107,8 @@ public override void Write(MinecraftStream stream) stream.WriteEntityMetadataType(9, EntityMetadataType.Float); stream.WriteFloat(Health); - stream.WriteEntityMetadataType(10, EntityMetadataType.VarInt); - stream.WriteVarInt((int)ActiveEffectColor); + stream.WriteEntityMetadataType(10, EntityMetadataType.Particles);//This is a list of integers? + stream.WriteVarInt(0); stream.WriteEntityMetadataType(11, EntityMetadataType.Boolean); stream.WriteBoolean(AmbientPotionEffect); @@ -119,7 +119,7 @@ public override void Write(MinecraftStream stream) stream.WriteEntityMetadataType(13, EntityMetadataType.VarInt); stream.WriteVarInt(AbsorbedStingers); - stream.WriteEntityMetadataType(14, EntityMetadataType.OptPosition); + stream.WriteEntityMetadataType(14, EntityMetadataType.OptionalBlockPos); stream.WriteBoolean(BedBlockPosition != default); if (BedBlockPosition != default) stream.WritePositionF(BedBlockPosition); diff --git a/Obsidian/Entities/Player.cs b/Obsidian/Entities/Player.cs index d125a311c..1288bf01c 100644 --- a/Obsidian/Entities/Player.cs +++ b/Obsidian/Entities/Player.cs @@ -1,5 +1,6 @@ // This would be saved in a file called [playeruuid].dat which holds a bunch of NBT data. // https://wiki.vg/Map_Format +using Microsoft.CodeAnalysis; using Microsoft.Extensions.Logging; using Obsidian.API._Types; using Obsidian.API.Events; @@ -12,9 +13,11 @@ using Obsidian.Net.Scoreboard; using Obsidian.Registries; using Obsidian.WorldData; +using System.Buffers; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; using System.Net; namespace Obsidian.Entities; @@ -31,7 +34,7 @@ public sealed partial class Player : Living, IPlayer /// protected ILogger Logger { get; private set; } - internal HashSet visiblePlayers = []; + internal HashSet visiblePlayers = []; //TODO: better name?? internal short inventorySlot = 36; @@ -237,7 +240,7 @@ public async Task OpenInventoryAsync(BaseContainer container) await client.QueuePacketAsync(new SetContainerContentPacket(nextId, container.ToList())); } - public async override Task TeleportAsync(VectorF pos) + public async override ValueTask TeleportAsync(VectorF pos) { LastPosition = Position; Position = pos; @@ -263,7 +266,7 @@ await client.QueuePacketAsync(new SynchronizePlayerPositionPacket TeleportId = tid; } - public async override Task TeleportAsync(IEntity to) + public async override ValueTask TeleportAsync(IEntity to) { LastPosition = Position; Position = to.Position; @@ -280,7 +283,7 @@ await client.QueuePacketAsync(new SynchronizePlayerPositionPacket }); } - public async override Task TeleportAsync(IWorld world) + public async override ValueTask TeleportAsync(IWorld world) { if (world is not World w) { @@ -398,7 +401,7 @@ await client.QueuePacketAsync(new SynchronizePlayerPositionPacket } //TODO make IDamageSource - public async override Task KillAsync(IEntity source, ChatMessage deathMessage) + public async override ValueTask KillAsync(IEntity source, ChatMessage deathMessage) { //await this.client.QueuePacketAsync(new PlayerDied //{ @@ -412,7 +415,7 @@ public async override Task KillAsync(IEntity source, ChatMessage deathMessage) await RemoveAsync(); if (source is Player attacker) - attacker.visiblePlayers.Remove(EntityId); + attacker.visiblePlayers.Remove(this); } public async override Task WriteAsync(MinecraftStream stream) @@ -453,13 +456,13 @@ public override void Write(MinecraftStream stream) if (LeftShoulder is not null) { stream.WriteEntityMetadataType(19, EntityMetadataType.Nbt); - stream.WriteVarInt(LeftShoulder); + stream.WriteNbtCompound(new NbtCompound()); } if (RightShoulder is not null) { stream.WriteEntityMetadataType(20, EntityMetadataType.Nbt); - stream.WriteVarInt(RightShoulder); + stream.WriteNbtCompound(new NbtCompound()); } } @@ -680,7 +683,7 @@ public async Task LoadAsync(bool loadFromPersistentWorld = true) //TODO use inventory if has using global data set to true if (persistentDataReader.ReadNextTag() is NbtCompound persistentDataCompound) { - var worldName = persistentDataCompound.GetString("worldName"); + var worldName = persistentDataCompound.GetString("worldName")!; Logger?.LogInformation("persistent world: {worldName}", worldName); @@ -869,7 +872,7 @@ public byte GetNextContainerId() public override string ToString() => Username; - internal async override Task UpdateAsync(VectorF position, bool onGround) + internal async override ValueTask UpdateAsync(VectorF position, bool onGround) { await base.UpdateAsync(position, onGround); @@ -880,7 +883,7 @@ internal async override Task UpdateAsync(VectorF position, bool onGround) await PickupNearbyItemsAsync(); } - internal async override Task UpdateAsync(VectorF position, Angle yaw, Angle pitch, bool onGround) + internal async override ValueTask UpdateAsync(VectorF position, Angle yaw, Angle pitch, bool onGround) { await base.UpdateAsync(position, yaw, pitch, onGround); @@ -891,49 +894,63 @@ internal async override Task UpdateAsync(VectorF position, Angle yaw, Angle pitc await PickupNearbyItemsAsync(); } - internal async override Task UpdateAsync(Angle yaw, Angle pitch, bool onGround) + internal async override ValueTask UpdateAsync(Angle yaw, Angle pitch, bool onGround) { await base.UpdateAsync(yaw, pitch, onGround); await PickupNearbyItemsAsync(); } - private async Task TrySpawnPlayerAsync(VectorF position) + private async ValueTask TrySpawnPlayerAsync(VectorF position) { - foreach (var player in world.GetPlayersInRange(position, ClientInformation.ViewDistance)) + //TODO PROPER DISTANCE CALCULATION + var entityBroadcastDistance = this.world.Configuration.EntityBroadcastRangePercentage; + + foreach (var player in world.GetPlayersInRange(position, entityBroadcastDistance)) { if (player == this) continue; - if (player.Alive && !visiblePlayers.Contains(player.EntityId)) + if (player.Alive && !visiblePlayers.Contains(player)) { - visiblePlayers.Add(player.EntityId); + visiblePlayers.Add(player); - await client.QueuePacketAsync(new SpawnPlayerPacket - { - EntityId = player.EntityId, - Uuid = player.Uuid, - Position = player.Position, - Yaw = player.Yaw, - Pitch = player.Pitch - }); + player.SpawnEntity(); } } - var removed = visiblePlayers.Where(x => !world.Players.Any(p => p.Value == x)).ToArray(); - visiblePlayers.RemoveWhere(x => !world.Players.Any(p => p.Value == x)); + if (visiblePlayers.Count == 0) + return; + + var removed = ArrayPool.Shared.Rent(visiblePlayers.Count); + + var index = 0; + visiblePlayers.RemoveWhere(visiblePlayer => + { + if (!visiblePlayer.IsInRange(this, entityBroadcastDistance)) + { + removed[index++] = visiblePlayer.EntityId; + return true; + } + return false; + }); + + if (index > 0) + await client.QueuePacketAsync(new RemoveEntitiesPacket(removed.ToArray())); - if (removed.Length > 0) - await client.QueuePacketAsync(new RemoveEntitiesPacket(removed)); + ArrayPool.Shared.Return(removed); } - private async Task PickupNearbyItemsAsync(float distance = 0.5f) + private async Task PickupNearbyItemsAsync(float distance = 1.5f) { foreach (var entity in world.GetNonPlayerEntitiesInRange(Position, distance)) { if (entity is not ItemEntity item) continue; + if (!item.CanPickup) + continue; + this.PacketBroadcaster.QueuePacketToWorld(this.World, new PickupItemPacket { CollectedEntityId = item.EntityId, diff --git a/Obsidian/Events/MainEventHandler.cs b/Obsidian/Events/MainEventHandler.cs index d093eef1a..dd7e439cf 100644 --- a/Obsidian/Events/MainEventHandler.cs +++ b/Obsidian/Events/MainEventHandler.cs @@ -336,7 +336,7 @@ public async Task OnPlayerLeave(PlayerLeaveEventArgs e) continue; await other.client.RemovePlayerFromListAsync(player); - if (other.visiblePlayers.Contains(player.EntityId)) + if (other.visiblePlayers.Contains(player)) await other.client.QueuePacketAsync(destroy); } @@ -349,9 +349,10 @@ public async Task OnPlayerJoin(PlayerJoinEventArgs e) var joined = e.Player as Player; var server = e.Server as Server; - joined.world.TryAddPlayer(joined); + joined!.world.TryAddPlayer(joined); + joined!.world.TryAddEntity(joined); - server.BroadcastMessage(new ChatMessage + server!.BroadcastMessage(new ChatMessage { Text = string.Format(server.Configuration.Messages.Join, e.Player.Username), Color = HexColor.Yellow diff --git a/Obsidian/Net/MinecraftStream.Writing.cs b/Obsidian/Net/MinecraftStream.Writing.cs index 268b094c9..89711af69 100644 --- a/Obsidian/Net/MinecraftStream.Writing.cs +++ b/Obsidian/Net/MinecraftStream.Writing.cs @@ -1,4 +1,5 @@ -using Obsidian.API.Advancements; +using Obsidian.API; +using Obsidian.API.Advancements; using Obsidian.API.Crafting; using Obsidian.API.Inventory; using Obsidian.API.Registry.Codecs.ArmorTrims.TrimMaterial; @@ -20,6 +21,7 @@ using Obsidian.Registries; using Obsidian.Serialization.Attributes; using System.Buffers.Binary; +using System.IO; using System.Text; using System.Text.Json; @@ -650,74 +652,20 @@ public void WriteItemStack(ItemStack value) value ??= new ItemStack(0, 0) { Present = true }; var item = value.AsItem(); - WriteVarInt(item.Id); - - if (item.Id != 0) - { - WriteByte((sbyte)value.Count); - - NbtWriter writer = new(this, true); - - ItemMeta meta = value.ItemMeta; - - if (meta.HasTags()) - { - writer.WriteByte("Unbreakable", (byte)(meta.Unbreakable ? 1 : 0)); - - if (meta.Durability > 0) - writer.WriteInt("Damage", meta.Durability); - - if (meta.CustomModelData > 0) - writer.WriteInt("CustomModelData", meta.CustomModelData); + var meta = value.ItemMeta; - if (meta.CanDestroy is not null) - { - writer.WriteListStart("CanDestroy", NbtTagType.String, meta.CanDestroy.Count); - - foreach (var block in meta.CanDestroy) - writer.WriteString(block); - - writer.EndList(); - } - - if (meta.Name is not null) - { - writer.WriteCompoundStart("display"); - - writer.WriteString("Name", new List { meta.Name }.ToJson()); - - if (meta.Lore is not null) - { - writer.WriteListStart("Lore", NbtTagType.String, meta.Lore.Count); - - foreach (var lore in meta.Lore) - writer.WriteString(new List { lore }.ToJson()); - - writer.EndList(); - } - - writer.EndCompound(); - } - else if (meta.Lore is not null) - { - writer.WriteCompoundStart("display"); - - writer.WriteListStart("Lore", NbtTagType.String, meta.Lore.Count); - - foreach (var lore in meta.Lore) - writer.WriteString(new List { lore }.ToJson()); + WriteVarInt(value.Count); - writer.EndList(); + //Stop serializing if item is invalid + if (value.Count <= 0) + return; - writer.EndCompound(); - } - } - writer.WriteString("id", item.UnlocalizedName); - writer.WriteByte("Count", (byte)value.Count); + WriteVarInt(item.Id); + WriteVarInt(0); + WriteVarInt(0); - writer.EndCompound(); - writer.TryFinish(); - } + if (!meta.HasTags()) + return; } [WriteMethod] @@ -811,11 +759,11 @@ public async Task WriteEntityMetdata(byte index, EntityMetadataType type, object await WriteStringAsync((string)value); break; - case EntityMetadataType.Chat: + case EntityMetadataType.TextComponent: await WriteChatAsync((ChatMessage)value); break; - case EntityMetadataType.OptChat: + case EntityMetadataType.OptionalTextComponent: await WriteBooleanAsync(optional); if (optional) @@ -830,14 +778,14 @@ public async Task WriteEntityMetdata(byte index, EntityMetadataType type, object await WriteBooleanAsync((bool)value); break; - case EntityMetadataType.Rotation: + case EntityMetadataType.Rotations: break; - case EntityMetadataType.Position: + case EntityMetadataType.BlockPos: await WritePositionFAsync((VectorF)value); break; - case EntityMetadataType.OptPosition: + case EntityMetadataType.OptionalBlockPos: await WriteBooleanAsync(optional); if (optional) @@ -848,21 +796,21 @@ public async Task WriteEntityMetdata(byte index, EntityMetadataType type, object case EntityMetadataType.Direction: break; - case EntityMetadataType.OptUuid: + case EntityMetadataType.OptionalUUID: await WriteBooleanAsync(optional); if (optional) await WriteUuidAsync((Guid)value); break; - case EntityMetadataType.OptBlockId: + case EntityMetadataType.OptionalBlockState: await WriteVarIntAsync((int)value); break; case EntityMetadataType.Nbt: case EntityMetadataType.Particle: case EntityMetadataType.VillagerData: - case EntityMetadataType.OptVarInt: + case EntityMetadataType.OptionalUnsignedInt: if (optional) { await WriteVarIntAsync(0); diff --git a/Obsidian/Net/Packets/Play/Clientbound/BundleDelimiterPacket.cs b/Obsidian/Net/Packets/Play/Clientbound/BundleDelimiterPacket.cs deleted file mode 100644 index 6da553fa5..000000000 --- a/Obsidian/Net/Packets/Play/Clientbound/BundleDelimiterPacket.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Obsidian.Net.Packets.Play.Clientbound; -public sealed partial class BundleDelimiterPacket : IClientboundPacket -{ - public int Id => 0x00; - - public void Serialize(MinecraftStream stream) { } -} diff --git a/Obsidian/Net/Packets/Play/Clientbound/BundledPacket.cs b/Obsidian/Net/Packets/Play/Clientbound/BundledPacket.cs new file mode 100644 index 000000000..3c6a18e2b --- /dev/null +++ b/Obsidian/Net/Packets/Play/Clientbound/BundledPacket.cs @@ -0,0 +1,26 @@ +namespace Obsidian.Net.Packets.Play.Clientbound; +public sealed class BundledPacket : IClientboundPacket +{ + public required List Packets { get; set; } + + public int Id => 0x00; + + public void Serialize(MinecraftStream stream) + { + using var packetStream = new MinecraftStream(); + + foreach (var packet in this.Packets) + packet.Serialize(packetStream); + + stream.Lock.Wait(); + stream.WriteVarInt(Id.GetVarIntLength()); + stream.WriteVarInt(Id); + + packetStream.Position = 0; + packetStream.CopyTo(stream); + + stream.WriteVarInt(Id.GetVarIntLength()); + stream.WriteVarInt(Id); + stream.Lock.Release(); + } +} diff --git a/Obsidian/Net/Packets/Play/Clientbound/EntityAnimationPacket.cs b/Obsidian/Net/Packets/Play/Clientbound/EntityAnimationPacket.cs index 184b98961..4f209e3a2 100644 --- a/Obsidian/Net/Packets/Play/Clientbound/EntityAnimationPacket.cs +++ b/Obsidian/Net/Packets/Play/Clientbound/EntityAnimationPacket.cs @@ -16,8 +16,7 @@ public partial class EntityAnimationPacket : IClientboundPacket public enum EntityAnimationType : byte { SwingMainArm, - TakeDamage, - LeaveBed, + LeaveBed = 2, SwingOffhand, CriticalEffect, MagicalCriticalEffect diff --git a/Obsidian/Net/Packets/Play/Clientbound/SetBlockDestroyStagePacket.cs b/Obsidian/Net/Packets/Play/Clientbound/SetBlockDestroyStagePacket.cs index 975a1c6ec..474b619a1 100644 --- a/Obsidian/Net/Packets/Play/Clientbound/SetBlockDestroyStagePacket.cs +++ b/Obsidian/Net/Packets/Play/Clientbound/SetBlockDestroyStagePacket.cs @@ -8,7 +8,7 @@ public partial class SetBlockDestroyStagePacket : IClientboundPacket public int EntityId { get; init; } [Field(1)] - public VectorF Position { get; init; } + public Vector Position { get; init; } /// /// 0-9 to set it, any other value to remove it. diff --git a/Obsidian/Net/Packets/Play/Clientbound/SpawnPlayerPacket.cs b/Obsidian/Net/Packets/Play/Clientbound/SpawnPlayerPacket.cs deleted file mode 100644 index f27296e3b..000000000 --- a/Obsidian/Net/Packets/Play/Clientbound/SpawnPlayerPacket.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Obsidian.Serialization.Attributes; - -namespace Obsidian.Net.Packets.Play.Clientbound; - -public partial class SpawnPlayerPacket : IClientboundPacket -{ - [Field(0), VarLength] - public int EntityId { get; init; } - - [Field(1)] - public Guid Uuid { get; init; } - - [Field(2), DataFormat(typeof(double))] - public VectorF Position { get; init; } - - [Field(3)] - public Angle Yaw { get; init; } - - [Field(4)] - public Angle Pitch { get; init; } - - public int Id => 0x03; -} diff --git a/Obsidian/Net/Packets/Play/Serverbound/ClickContainerPacket.cs b/Obsidian/Net/Packets/Play/Serverbound/ClickContainerPacket.cs index e286515a8..d0b6d40e9 100644 --- a/Obsidian/Net/Packets/Play/Serverbound/ClickContainerPacket.cs +++ b/Obsidian/Net/Packets/Play/Serverbound/ClickContainerPacket.cs @@ -132,7 +132,7 @@ public async ValueTask HandleAsync(Server server, Player player) var item = new ItemEntity { - EntityId = player + player.world.GetTotalLoadedEntities() + 1, + EntityId = Server.GetNextEntityId(), Count = 1, Id = removedItem.AsItem().Id, Glowing = true, diff --git a/Obsidian/Net/Packets/Play/Serverbound/PlayerActionPacket.cs b/Obsidian/Net/Packets/Play/Serverbound/PlayerActionPacket.cs index af2a9ffa5..9f89630b9 100644 --- a/Obsidian/Net/Packets/Play/Serverbound/PlayerActionPacket.cs +++ b/Obsidian/Net/Packets/Play/Serverbound/PlayerActionPacket.cs @@ -49,70 +49,58 @@ private void BroadcastPlayerAction(Player player, IBlock block) switch (this.Status) { case PlayerActionStatus.DropItem: - { - DropItem(player, 1); - break; - } + { + DropItem(player, 1); + break; + } case PlayerActionStatus.DropItemStack: - { - DropItem(player, 64); - break; - } + { + DropItem(player, 64); + break; + } case PlayerActionStatus.StartedDigging: case PlayerActionStatus.CancelledDigging: break; case PlayerActionStatus.FinishedDigging: - { - player.PacketBroadcaster.QueuePacketToWorld(player.World, new SetBlockDestroyStagePacket { - EntityId = player, - Position = this.Position, - DestroyStage = -1 - }); + player.PacketBroadcaster.QueuePacketToWorld(player.world, 0, new SetBlockDestroyStagePacket + { + EntityId = player, + Position = this.Position, + DestroyStage = -1 + }, player.EntityId); - var droppedItem = ItemsRegistry.Get(block.Material); + var droppedItem = ItemsRegistry.Get(block.Material); - if (droppedItem.Id == 0) { break; } + if (droppedItem.Id == 0) { break; } - var item = new ItemEntity - { - EntityId = player + player.world.GetTotalLoadedEntities() + 1, - Count = 1, - Id = droppedItem.Id, - Glowing = true, - World = player.world, - Position = this.Position, - PacketBroadcaster = player.PacketBroadcaster, - }; - - player.world.TryAddEntity(item); - - player.PacketBroadcaster.QueuePacketToWorld(player.World, new SpawnEntityPacket - { - EntityId = item.EntityId, - Uuid = item.Uuid, - Type = EntityType.Item, - Position = item.Position, - Pitch = 0, - Yaw = 0, - Data = 1, - Velocity = Velocity.FromVector(new VectorF( - Globals.Random.NextFloat() * 0.5f, - Globals.Random.NextFloat() * 0.5f, - Globals.Random.NextFloat() * 0.5f)) - }); - - player.PacketBroadcaster.QueuePacketToWorld(player.World, new SetEntityMetadataPacket - { - EntityId = item.EntityId, - Entity = item - }); - break; - } + var item = new ItemEntity + { + EntityId = Server.GetNextEntityId(), + Count = 1, + Id = droppedItem.Id, + World = player.world, + Position = (VectorF)this.Position + 0.5f, + PacketBroadcaster = player.PacketBroadcaster, + }; + + player.world.TryAddEntity(item); + + item.SpawnEntity(Velocity.FromBlockPerTick(GetRandDropVelocity(), GetRandDropVelocity(), GetRandDropVelocity())); + + break; + } } } - private void DropItem(Player player, sbyte amountToRemove) + private static float GetRandDropVelocity() + { + var f = Globals.Random.NextFloat(); + + return f * 0.5f; + } + + private static void DropItem(Player player, sbyte amountToRemove) { var droppedItem = player.GetHeldItem(); @@ -123,10 +111,9 @@ private void DropItem(Player player, sbyte amountToRemove) var item = new ItemEntity { - EntityId = player + player.world.GetTotalLoadedEntities() + 1, + EntityId = Server.GetNextEntityId(), Count = amountToRemove, Id = droppedItem.AsItem().Id, - Glowing = true, World = player.world, PacketBroadcaster = player.PacketBroadcaster, Position = loc @@ -138,22 +125,7 @@ private void DropItem(Player player, sbyte amountToRemove) var vel = Velocity.FromDirection(loc, lookDir);//TODO properly shoot the item towards the direction the players looking at - player.PacketBroadcaster.QueuePacketToWorld(player.World, new SpawnEntityPacket - { - EntityId = item.EntityId, - Uuid = item.Uuid, - Type = EntityType.Item, - Position = item.Position, - Pitch = 0, - Yaw = 0, - Data = 1, - Velocity = vel - }); - player.PacketBroadcaster.QueuePacketToWorld(player.World, new SetEntityMetadataPacket - { - EntityId = item.EntityId, - Entity = item - }); + item.SpawnEntity(vel); player.Inventory.RemoveItem(player.inventorySlot, amountToRemove); diff --git a/Obsidian/Server.cs b/Obsidian/Server.cs index 8d2d4edc4..b6b694018 100644 --- a/Obsidian/Server.cs +++ b/Obsidian/Server.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Connections; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -30,6 +31,7 @@ using System.Net; using System.Net.Sockets; using System.Reflection; +using System.Runtime.CompilerServices; using System.Text; using System.Threading; @@ -37,6 +39,8 @@ namespace Obsidian; public sealed partial class Server : IServer { + private static int EntityCounter = 0; + #if RELEASE public const string VERSION = "0.1"; #else @@ -232,13 +236,14 @@ public void BroadcastMessage(PlayerChatMessagePacket message) /// public void BroadcastMessage(string message) { - var chatMessage = ChatMessage.Simple(string.Empty) - .AddExtra(message); + var chatMessage = ChatMessage.Simple(message); _chatMessagesQueue.Enqueue(new SystemChatMessagePacket(chatMessage, false)); _logger.LogInformation(message); } + public static int GetNextEntityId() => Interlocked.Increment(ref EntityCounter); + /// /// Starts this server asynchronously. /// @@ -387,7 +392,7 @@ private async Task AcceptClientsAsync() } // TODO Entity ids need to be unique on the entire server, not per world - var client = new Client(connection, Math.Max(0, _clients.Count + this.DefaultWorld.GetTotalLoadedEntities()), this.loggerFactory, this.userCache, this); + var client = new Client(connection, this.loggerFactory, this.userCache, this); _clients.Add(client); _ = ExecuteAsync(client); diff --git a/Obsidian/Services/PacketBroadcaster.cs b/Obsidian/Services/PacketBroadcaster.cs index f3ed6dca8..39681c39e 100644 --- a/Obsidian/Services/PacketBroadcaster.cs +++ b/Obsidian/Services/PacketBroadcaster.cs @@ -24,8 +24,10 @@ public PacketBroadcaster(IServer server, ILoggerFactory loggerFactory, IServerEn public void QueuePacket(IClientboundPacket packet, params int[] excludedIds) => this.priorityQueue.Enqueue(new() { Packet = packet, ExcludedIds = excludedIds }, 1); - public void QueuePacketToWorld(IWorld world, IClientboundPacket packet, params int[] excludedIds) => - this.priorityQueue.Enqueue(new() { Packet = packet, ExcludedIds = excludedIds }, 1); + public void QueuePacketToWorld(IWorld world, IClientboundPacket packet, params int[] excludedIds) + { + this.priorityQueue.Enqueue(new() { Packet = packet, ToWorld = world, ExcludedIds = excludedIds }, 1); + } public void QueuePacketToWorld(IWorld world, int priority, IClientboundPacket packet, params int[] excludedIds) => this.priorityQueue.Enqueue(new() { Packet = packet, ExcludedIds = excludedIds, ToWorld = world }, priority); @@ -35,20 +37,52 @@ public void QueuePacket(IClientboundPacket packet, int priority, params int[] ex public void Broadcast(IClientboundPacket packet, params int[] excludedIds) { - foreach (var player in this.server.Players.Cast().Where(player => excludedIds.Contains(player.EntityId))) + foreach (var player in this.server.Players.Cast().Where(player => !excludedIds.Contains(player.EntityId))) player.client.SendPacket(packet); } + public void BroadcastToWorldInRange(IWorld toWorld, VectorF location, IClientboundPacket packet, params int[] excludedIds) + { + if (toWorld is not World world) + return; + + foreach (var player in world.GetPlayersInRange(location, world.Configuration.EntityBroadcastRangePercentage)) + player.client.SendPacket(packet); + } + + public void QueuePacketToWorldInRange(IWorld toWorld, VectorF location, IClientboundPacket packet, params int[] excludedIds) + { + if (toWorld is not World world) + return; + + var includedIDs = world.GetPlayersInRange(location, world.Configuration.EntityBroadcastRangePercentage) + .Select(x => x.EntityId) + .ToHashSet(); + + excludedIds = excludedIds.Concat(world.Players.Values + .Select(x => x.EntityId) + .Where(x => !includedIDs.Contains(x))) + .ToArray(); + + this.priorityQueue.Enqueue(new() + { + Packet =packet, + ToWorld = world, + ExcludedIds = excludedIds, + }, 1); + } + + public void BroadcastToWorld(IWorld toWorld, IClientboundPacket packet, params int[] excludedIds) { if (toWorld is not World world) return; - foreach (var player in world.Players.Values.Where(player => excludedIds.Contains(player.EntityId))) + foreach (var player in world.Players.Values.Where(player => !excludedIds.Contains(player.EntityId))) player.client.SendPacket(packet); } - protected override async Task ExecuteAsync(CancellationToken stoppingToken) + protected async override Task ExecuteAsync(CancellationToken stoppingToken) { using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(20)); @@ -56,7 +90,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (await timer.WaitForNextTickAsync(stoppingToken)) { - if (!this.priorityQueue.TryDequeue(out var queuedPacket, out _)) + if (!this.priorityQueue.TryDequeue(out var queuedPacket, out var priority)) continue; if (queuedPacket.ToWorld is World toWorld) @@ -69,6 +103,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) foreach (var player in this.server.Players.Cast().Where(player => queuedPacket.ExcludedIds != null && !queuedPacket.ExcludedIds.Contains(player.EntityId))) await player.client.QueuePacketAsync(queuedPacket.Packet); + } } catch (Exception e) when (e is not OperationCanceledException) @@ -104,6 +139,8 @@ public interface IPacketBroadcaster /// The list of entity ids to exlude from the broadcast. public void BroadcastToWorld(IWorld toWorld, IClientboundPacket packet, params int[] excludedIds); + public void BroadcastToWorldInRange(IWorld world, VectorF location, IClientboundPacket packet, params int[] excludedIds); + /// /// Puts the packet in a priority queue for processing then broadcasting when dequeued. /// @@ -113,6 +150,8 @@ public interface IPacketBroadcaster /// /// Packets queued without a priority set will be queued up with a priority of 1. public void QueuePacketToWorld(IWorld toWorld, IClientboundPacket packet, params int[] excludedIds); + public void QueuePacketToWorldInRange(IWorld world, VectorF location, IClientboundPacket packet, params int[] excludedIds); + /// /// Puts the packet in a priority queue for processing then broadcasting when dequeued. /// diff --git a/Obsidian/Utilities/Extensions.StreamWriting.cs b/Obsidian/Utilities/Extensions.StreamWriting.cs new file mode 100644 index 000000000..8167f9b3a --- /dev/null +++ b/Obsidian/Utilities/Extensions.StreamWriting.cs @@ -0,0 +1,22 @@ +using Obsidian.Net; + +namespace Obsidian.Utilities; +public partial class Extensions +{ + public static void Write(this ItemStack itemStack, MinecraftStream stream) + { + var item = itemStack.AsItem(); + var meta = itemStack.ItemMeta; + + stream.WriteVarInt(itemStack.Count); + + //Stop serializing if item is invalid + if (itemStack.Count <= 0) + return; + + stream.WriteVarInt(item.Id); + + if (!meta.HasTags()) + return; + } +} diff --git a/Obsidian/Utilities/LocationDiff.cs b/Obsidian/Utilities/LocationDiff.cs new file mode 100644 index 000000000..3d5066df5 --- /dev/null +++ b/Obsidian/Utilities/LocationDiff.cs @@ -0,0 +1,19 @@ +namespace Obsidian.Utilities; + +internal readonly struct LocationDiff +{ + public required float DifferenceX { get; init; } + + public required float DifferenceY { get; init; } + + public required float DifferenceZ { get; init; } + + public float CalculatedDifference => this.DifferenceX * this.DifferenceX + this.DifferenceZ * this.DifferenceZ; + + public static LocationDiff GetDifference(VectorF entityLocation, VectorF location) => new() + { + DifferenceX = entityLocation.X - location.X, + DifferenceY = entityLocation.Y - location.Y, + DifferenceZ = entityLocation.Z - location.Z, + }; +} diff --git a/Obsidian/WorldData/Generators/GenHelper.cs b/Obsidian/WorldData/Generators/GenHelper.cs index f1d7c0b13..1753dd1e4 100644 --- a/Obsidian/WorldData/Generators/GenHelper.cs +++ b/Obsidian/WorldData/Generators/GenHelper.cs @@ -35,11 +35,11 @@ public async ValueTask SetBlockAsync(Vector position, IBlock block, Chunk? chunk public ValueTask SetBlockAsync(int x, int y, int z, IBlock block, Chunk? chunk) => SetBlockAsync(new Vector(x, y, z), block, chunk); - public Task SetBlockAsync(int x, int y, int z, IBlock block) => world.SetBlockUntrackedAsync(x, y, z, block, false); + public ValueTask SetBlockAsync(int x, int y, int z, IBlock block) => world.SetBlockUntrackedAsync(x, y, z, block, false); - public Task SetBlockAsync(Vector position, IBlock block) => world.SetBlockUntrackedAsync(position, block, false); + public ValueTask SetBlockAsync(Vector position, IBlock block) => world.SetBlockUntrackedAsync(position, block, false); - public async Task GetBlockAsync(Vector position, Chunk? chunk) + public async ValueTask GetBlockAsync(Vector position, Chunk? chunk) { if (chunk is Chunk c && position.X >> 4 == c.X && position.Z >> 4 == c.Z) { @@ -48,11 +48,11 @@ public async ValueTask SetBlockAsync(Vector position, IBlock block, Chunk? chunk return await world.GetBlockAsync(position); } - public Task GetBlockAsync(int x, int y, int z, Chunk? chunk) => GetBlockAsync(new Vector(x, y, z), chunk); + public ValueTask GetBlockAsync(int x, int y, int z, Chunk? chunk) => GetBlockAsync(new Vector(x, y, z), chunk); - public Task GetBlockAsync(int x, int y, int z) => world.GetBlockAsync(x, y, z); + public ValueTask GetBlockAsync(int x, int y, int z) => world.GetBlockAsync(x, y, z); - public Task GetBlockAsync(Vector position) => world.GetBlockAsync(position); + public ValueTask GetBlockAsync(Vector position) => world.GetBlockAsync(position); public async ValueTask GetWorldHeightAsync(int x, int z, Chunk? chunk) { diff --git a/Obsidian/WorldData/Region.cs b/Obsidian/WorldData/Region.cs index de3dab057..73bbd4748 100644 --- a/Obsidian/WorldData/Region.cs +++ b/Obsidian/WorldData/Region.cs @@ -134,13 +134,7 @@ internal async Task SerializeChunkAsync(Chunk chunk) internal async Task BeginTickAsync(CancellationToken cts = default) { - //var timer = new BalancingTimer(50, cts); - //while (await timer.WaitForNextTickAsync()) - //{ - - //} - - await Task.WhenAll(Entities.Select(entityEntry => entityEntry.Value.TickAsync())); + await Parallel.ForEachAsync(Entities.Values, cts, async (entity, cts) => await entity.TickAsync()); List neighborUpdates = []; List delayed = []; @@ -159,7 +153,7 @@ internal async Task BeginTickAsync(CancellationToken cts = default) if (updateNeighbor) { neighborUpdates.Add(bu); } } } - delayed.ForEach(i => AddBlockUpdate(i)); + delayed.ForEach(AddBlockUpdate); neighborUpdates.ForEach(async u => await u.world.BlockUpdateNeighborsAsync(u)); } diff --git a/Obsidian/WorldData/World.cs b/Obsidian/WorldData/World.cs index 28101f750..e7f623bc2 100644 --- a/Obsidian/WorldData/World.cs +++ b/Obsidian/WorldData/World.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Logging; +using Microsoft.CodeAnalysis; +using Microsoft.Extensions.Logging; using Obsidian.API.Configuration; using Obsidian.API.Registry.Codecs.Dimensions; using Obsidian.API.Utilities; @@ -83,8 +84,6 @@ internal World(ILogger logger, Type generatorType, IWorldManager worldManager) this.WorldManager = worldManager; } - public int GetTotalLoadedEntities() => Regions.Values.Sum(e => e == null ? 0 : e.Entities.Count); - public void InitGenerator() => this.Generator.Init(this); public ValueTask DestroyEntityAsync(Entity entity) @@ -128,7 +127,7 @@ public ValueTask DestroyEntityAsync(Entity entity) /// Whether to enqueue a job to generate the chunk if it doesn't exist and return null. /// When set to false, a partial Chunk is returned. /// Null if the region or chunk doesn't exist yet. Otherwise the full chunk or a partial chunk. - public async Task GetChunkAsync(int chunkX, int chunkZ, bool scheduleGeneration = true) + public async ValueTask GetChunkAsync(int chunkX, int chunkZ, bool scheduleGeneration = true) { Region? region = GetRegionForChunk(chunkX, chunkZ) ?? LoadRegion(chunkX >> Region.CubicRegionSizeShift, chunkZ >> Region.CubicRegionSizeShift); @@ -175,17 +174,17 @@ public ValueTask DestroyEntityAsync(Entity entity) /// /// When set to false, a partial Chunk is returned. /// Null if the region or chunk doesn't exist yet. Otherwise the full chunk or a partial chunk. - public Task GetChunkAsync(Vector worldLocation, bool scheduleGeneration = true) => GetChunkAsync(worldLocation.X.ToChunkCoord(), worldLocation.Z.ToChunkCoord(), scheduleGeneration); + public ValueTask GetChunkAsync(Vector worldLocation, bool scheduleGeneration = true) => GetChunkAsync(worldLocation.X.ToChunkCoord(), worldLocation.Z.ToChunkCoord(), scheduleGeneration); - public Task GetBlockAsync(Vector location) => GetBlockAsync(location.X, location.Y, location.Z); + public ValueTask GetBlockAsync(Vector location) => GetBlockAsync(location.X, location.Y, location.Z); - public async Task GetBlockAsync(int x, int y, int z) + public async ValueTask GetBlockAsync(int x, int y, int z) { var c = await GetChunkAsync(x.ToChunkCoord(), z.ToChunkCoord(), false); return c?.GetBlock(x, y, z); } - public async Task GetWorldSurfaceHeightAsync(int x, int z) + public async ValueTask GetWorldSurfaceHeightAsync(int x, int z) { var c = await GetChunkAsync(x.ToChunkCoord(), z.ToChunkCoord(), false); return c?.Heightmaps[ChunkData.HeightmapType.MotionBlocking] @@ -200,25 +199,25 @@ public ValueTask DestroyEntityAsync(Entity entity) return c?.GetBlockEntity(x, y, z); } - public Task SetBlockEntity(Vector blockPosition, NbtCompound tileEntityData) => SetBlockEntity(blockPosition.X, blockPosition.Y, blockPosition.Z, tileEntityData); - public async Task SetBlockEntity(int x, int y, int z, NbtCompound tileEntityData) + public ValueTask SetBlockEntity(Vector blockPosition, NbtCompound tileEntityData) => SetBlockEntity(blockPosition.X, blockPosition.Y, blockPosition.Z, tileEntityData); + public async ValueTask SetBlockEntity(int x, int y, int z, NbtCompound tileEntityData) { var c = await GetChunkAsync(x.ToChunkCoord(), z.ToChunkCoord(), false); c?.SetBlockEntity(x, y, z, tileEntityData); } - public Task SetBlockAsync(int x, int y, int z, IBlock block) => SetBlockAsync(new Vector(x, y, z), block); + public ValueTask SetBlockAsync(int x, int y, int z, IBlock block) => SetBlockAsync(new Vector(x, y, z), block); - public async Task SetBlockAsync(Vector location, IBlock block) + public async ValueTask SetBlockAsync(Vector location, IBlock block) { await SetBlockUntrackedAsync(location.X, location.Y, location.Z, block); this.BroadcastBlockChange(block, location); } - public Task SetBlockAsync(int x, int y, int z, IBlock block, bool doBlockUpdate) => SetBlockAsync(new Vector(x, y, z), block, doBlockUpdate); + public ValueTask SetBlockAsync(int x, int y, int z, IBlock block, bool doBlockUpdate) => SetBlockAsync(new Vector(x, y, z), block, doBlockUpdate); - public async Task SetBlockAsync(Vector location, IBlock block, bool doBlockUpdate) + public async ValueTask SetBlockAsync(Vector location, IBlock block, bool doBlockUpdate) { await SetBlockUntrackedAsync(location.X, location.Y, location.Z, block, doBlockUpdate); this.BroadcastBlockChange(block, location); @@ -237,9 +236,9 @@ private void BroadcastBlockChange(IBlock block, Vector location) public IEnumerable PlayersInRange(Vector location) => this.Players.Values.Where(player => player.client.LoadedChunks.Contains(location.ToChunkCoord())); - public Task SetBlockUntrackedAsync(Vector location, IBlock block, bool doBlockUpdate = false) => SetBlockUntrackedAsync(location.X, location.Y, location.Z, block, doBlockUpdate); + public ValueTask SetBlockUntrackedAsync(Vector location, IBlock block, bool doBlockUpdate = false) => SetBlockUntrackedAsync(location.X, location.Y, location.Z, block, doBlockUpdate); - public async Task SetBlockUntrackedAsync(int x, int y, int z, IBlock block, bool doBlockUpdate = false) + public async ValueTask SetBlockUntrackedAsync(int x, int y, int z, IBlock block, bool doBlockUpdate = false) { if (doBlockUpdate) { @@ -295,19 +294,17 @@ public IEnumerable GetNonPlayerEntitiesInRange(VectorF location, float d // Iterate over chunks, taking one from each region, then getting the region itself for (int x = left; x <= right; x += Region.CubicRegionSize) { - for (int y = top; y >= bottom; y -= Region.CubicRegionSize) + for (int z = top; z >= bottom; z -= Region.CubicRegionSize) { - if (GetRegionForChunk(x, y) is not Region region) + if (GetRegionForChunk(x, z) is not Region region) continue; // Return entities in range foreach ((_, Entity entity) in region.Entities) { - VectorF entityLocation = entity.Position; - float differenceX = entityLocation.X - location.X; - float differenceY = entityLocation.Y - location.Y; + var locationDifference = LocationDiff.GetDifference(entity.Position, location); - if (differenceX * differenceX + differenceY * differenceY <= distance) + if (locationDifference.CalculatedDifference <= distance) { yield return entity; } @@ -339,11 +336,9 @@ public IEnumerable GetPlayersInRange(VectorF location, float distance) foreach ((_, Player player) in Players) { - VectorF playerLocation = player.Position; - float differenceX = playerLocation.X - location.X; - float differenceY = playerLocation.Y - location.Y; + var locationDifference = LocationDiff.GetDifference(player.Position, location); - if (differenceX * differenceX + differenceY * differenceY <= distance) + if (locationDifference.CalculatedDifference <= distance) { yield return player; } @@ -410,12 +405,6 @@ public async Task DoWorldTickAsync() //Tick regions within the world manager await Task.WhenAll(this.Regions.Values.Select(r => r.BeginTickAsync())); - - //// Check for chunks to load every second - //if (LevelData.Time % 20 == 0) - //{ - // await ManageChunksAsync(); - //} } #region world loading/saving @@ -571,7 +560,7 @@ public async Task UnloadRegionAsync(int regionX, int regionZ) await r.FlushAsync(); } - public async Task ScheduleBlockUpdateAsync(BlockUpdate blockUpdate) + public async ValueTask ScheduleBlockUpdateAsync(BlockUpdate blockUpdate) { blockUpdate.Block ??= await GetBlockAsync(blockUpdate.position); (int chunkX, int chunkZ) = blockUpdate.position.ToChunkCoord(); @@ -645,41 +634,29 @@ public IEntity SpawnFallingBlock(VectorF position, Material mat) FallingBlock entity = new(position) { Type = EntityType.FallingBlock, - EntityId = GetTotalLoadedEntities() + 1, + EntityId = Server.GetNextEntityId(), World = this, PacketBroadcaster = this.PacketBroadcaster, Block = BlocksRegistry.Get(mat) }; - this.PacketBroadcaster.QueuePacketToWorld(this, new SpawnEntityPacket - { - EntityId = entity.EntityId, - Uuid = entity.Uuid, - Type = entity.Type, - Position = entity.Position, - Pitch = 0, - Yaw = 0, - Data = entity.Block.GetHashCode() - }); + + entity.SpawnEntity(null, entity.Block.GetHashCode()); TryAddEntity(entity); return entity; } - public async Task SpawnEntityAsync(VectorF position, EntityType type) + public IEntity SpawnEntity(VectorF position, EntityType type) { - // Arrow, Boat, DragonFireball, AreaEffectCloud, EndCrystal, EvokerFangs, ExperienceOrb, - // FireworkRocket, FallingBlock, Item, ItemFrame, Fireball, LeashKnot, LightningBolt, - // LlamaSpit, Minecart, ChestMinecart, CommandBlockMinecart, FurnaceMinecart, HopperMinecart - // SpawnerMinecart, TntMinecart, Painting, Tnt, ShulkerBullet, SpectralArrow, EnderPearl, Snowball, SmallFireball, - // Egg, ExperienceBottle, Potion, Trident, FishingBobber, EyeOfEnder + if (type == EntityType.ExperienceOrb) + throw new NotImplementedException($"EntityType {type} is not supported."); if (type == EntityType.FallingBlock) - { return SpawnFallingBlock(position + (0, 20, 0), Material.Sand); - } + //TODO improve this Entity entity; if (type.IsNonLiving()) { @@ -687,54 +664,25 @@ public async Task SpawnEntityAsync(VectorF position, EntityType type) { Type = type, Position = position, - EntityId = GetTotalLoadedEntities() + 1, + EntityId = Server.GetNextEntityId(), World = this, PacketBroadcaster = this.PacketBroadcaster }; - - if (type == EntityType.ExperienceOrb || type == EntityType.ExperienceBottle) - { - //TODO - } - else - { - this.PacketBroadcaster.QueuePacketToWorld(this, new SpawnEntityPacket - { - EntityId = entity.EntityId, - Uuid = entity.Uuid, - Type = entity.Type, - Position = position, - Pitch = 0, - Yaw = 0, - Data = 0, - Velocity = new Velocity(0, 0, 0) - }); - } } else { entity = new Living { Position = position, - EntityId = GetTotalLoadedEntities() + 1, + EntityId = Server.GetNextEntityId(), Type = type, World = this, PacketBroadcaster = this.PacketBroadcaster }; - - this.PacketBroadcaster.QueuePacketToWorld(this, new SpawnEntityPacket - { - EntityId = entity.EntityId, - Uuid = entity.Uuid, - Type = type, - Position = position, - Pitch = 0, - Yaw = 0, - HeadYaw = 0, - Velocity = new Velocity(0, 0, 0) - }); } + entity.SpawnEntity(); + TryAddEntity(entity); return entity; @@ -877,7 +825,8 @@ await Parallel.ForEachAsync(Enumerable.Range(-regionPregenRange, regionPregenRan var cps = completedChunks / (stopwatch.ElapsedMilliseconds / 1000.0); int remain = ChunksToGenCount / (int)cps; Console.Write("\r{0} chunks/second - {1}% complete - {2} seconds remaining ", cps.ToString("###.00"), pctComplete, remain); - if (completedChunks % 1024 == 0) { // For Jon when he's doing large world gens + if (completedChunks % 1024 == 0) + { // For Jon when he's doing large world gens await FlushRegionsAsync(); } }