diff --git a/GameServer/Controllers/CreatureController.cs b/GameServer/Controllers/CreatureController.cs index cb99fc7..862e22a 100644 --- a/GameServer/Controllers/CreatureController.cs +++ b/GameServer/Controllers/CreatureController.cs @@ -14,6 +14,9 @@ using Protocol; namespace GameServer.Controllers; internal class CreatureController : Controller { + private const float DynamicSpawnRadius = 5000; + private const float DynamicSpawnPositionDelta = 2500; + private readonly EntitySystem _entitySystem; private readonly EntityFactory _entityFactory; private readonly ModelManager _modelManager; @@ -21,6 +24,7 @@ internal class CreatureController : Controller private readonly IGameActionListener _listener; private readonly GameplayFeatureSettings _gameplayFeatures; + private readonly Vector _lastDynamicSpawnPos; public CreatureController(PlayerSession session, EntitySystem entitySystem, EntityFactory entityFactory, ModelManager modelManager, ConfigManager configManager, IOptions gameplayFeatures, IGameActionListener listener) : base(session) { @@ -30,6 +34,8 @@ internal class CreatureController : Controller _configManager = configManager; _listener = listener; _gameplayFeatures = gameplayFeatures.Value; + + _lastDynamicSpawnPos = new(); } public async Task JoinScene(int instanceId) @@ -92,6 +98,14 @@ internal class CreatureController : Controller public void OnPlayerPositionChanged() { _modelManager.Player.Position.MergeFrom(GetPlayerEntity()!.Pos); + + if (_lastDynamicSpawnPos.GetDistance(_modelManager.Player.Position) >= DynamicSpawnPositionDelta) + { + _lastDynamicSpawnPos.MergeFrom(_modelManager.Player.Position); + + ClearInactiveEntities(); + SpawnDynamicEntities(); + } } [GameEvent(GameEventType.VisionSkillChanged)] @@ -276,19 +290,44 @@ internal class CreatureController : Controller } private void CreateWorldEntities() + { + _lastDynamicSpawnPos.MergeFrom(_modelManager.Player.Position.Clone()); + SpawnDynamicEntities(); + } + + private void ClearInactiveEntities() + { + _entitySystem.Destroy(_entitySystem.EnumerateEntities() + .Where(e => e is MonsterEntity && e.DynamicId != 0 && + e.Pos.GetDistance(_modelManager.Player.Position) > DynamicSpawnRadius).ToArray()); + } + + private void SpawnDynamicEntities() { Vector playerPos = _modelManager.Player.Position; - // Test monster - MonsterEntity monster = _entityFactory.CreateMonster(106003002); // Turtle. - monster.Pos = new() - { - X = playerPos.X + 250, - Y = playerPos.Y + 250, - Z = playerPos.Z - }; + // Currently only monsters + IEnumerable entitiesToSpawn = _configManager.Enumerate() + .Where(config => config.MapId == 8 && Math.Abs(config.Transform[0].X / 100 - playerPos.X) < DynamicSpawnRadius && Math.Abs(config.Transform[0].Y / 100 - playerPos.Y) < DynamicSpawnRadius && + config.BlueprintType.StartsWith("Monster")); - _entitySystem.Add([monster]); - monster.InitProps(_configManager.GetConfig(600000100)!); + List spawnMonsters = []; + foreach (LevelEntityConfig levelEntity in entitiesToSpawn) + { + if (_entitySystem.HasDynamicEntity(levelEntity.EntityId)) continue; + + MonsterEntity monster = _entityFactory.CreateMonster(levelEntity.EntityId); + monster.Pos = new() + { + X = levelEntity.Transform[0].X / 100, + Y = levelEntity.Transform[0].Y / 100, + Z = levelEntity.Transform[0].Z / 100 + }; + + monster.InitProps(_configManager.GetConfig(600000100)!); + spawnMonsters.Add(monster); + } + + _entitySystem.Add(spawnMonsters); } } diff --git a/GameServer/Extensions/Logic/MathExtensions.cs b/GameServer/Extensions/Logic/MathExtensions.cs new file mode 100644 index 0000000..a272742 --- /dev/null +++ b/GameServer/Extensions/Logic/MathExtensions.cs @@ -0,0 +1,13 @@ +using Protocol; + +namespace GameServer.Extensions.Logic; +internal static class MathExtensions +{ + public static float GetDistance(this Vector self, Vector other) + { + float x = self.X - other.X; + float y = self.Y - other.Y; + + return (float)Math.Sqrt(x * x + y * y); + } +} diff --git a/GameServer/Network/Kcp/KcpConnection.cs b/GameServer/Network/Kcp/KcpConnection.cs index ebba63b..892b410 100644 --- a/GameServer/Network/Kcp/KcpConnection.cs +++ b/GameServer/Network/Kcp/KcpConnection.cs @@ -8,6 +8,7 @@ internal class KcpConnection : IConnection { private readonly byte[] _recvBuffer; private readonly KcpConversation _conv; + private readonly ManualResetEvent _sendEvent; private uint _upStreamSeqNo; private uint _downStreamSeqNo; @@ -15,6 +16,7 @@ internal class KcpConnection : IConnection { _conv = conv; _recvBuffer = GC.AllocateUninitializedArray(8192); + _sendEvent = new ManualResetEvent(true); } public bool Active => !_conv.TransportClosed; @@ -51,7 +53,16 @@ internal class KcpConnection : IConnection MessageManager.EncodeMessage(memory[BaseMessage.LengthFieldSize..], message); if (_conv == null) throw new InvalidOperationException("Trying to send message when conv is null"); + + if (!_sendEvent.WaitOne(0)) + { + await Task.Yield(); + _ = _sendEvent.WaitOne(); + } + + _sendEvent.Reset(); await _conv.SendAsync(memoryOwner.Memory[..networkSize]); + _sendEvent.Set(); } private uint NextUpStreamSeqNo() diff --git a/GameServer/Systems/Entity/EntityBase.cs b/GameServer/Systems/Entity/EntityBase.cs index 977986c..1012f86 100644 --- a/GameServer/Systems/Entity/EntityBase.cs +++ b/GameServer/Systems/Entity/EntityBase.cs @@ -13,6 +13,7 @@ internal abstract class EntityBase public Rotator Rot { get; set; } public bool Active { get; set; } + public int DynamicId { get; protected set; } public EntityState State { get; protected set; } diff --git a/GameServer/Systems/Entity/EntitySystem.cs b/GameServer/Systems/Entity/EntitySystem.cs index 86a8f08..56c97eb 100644 --- a/GameServer/Systems/Entity/EntitySystem.cs +++ b/GameServer/Systems/Entity/EntitySystem.cs @@ -5,11 +5,14 @@ namespace GameServer.Systems.Entity; internal class EntitySystem { private readonly List _entities; + private readonly List _dynamicEntityIds; + private readonly IGameActionListener _listener; public EntitySystem(IGameActionListener listener) { _entities = []; + _dynamicEntityIds = []; _listener = listener; } @@ -26,15 +29,26 @@ internal class EntitySystem throw new InvalidOperationException($"EntitySystem::Create - entity with id {entity.Id} already exists"); _entities.Add(entity); + + if (entity.DynamicId != 0) + _dynamicEntityIds.Add(entity.DynamicId); } _ = _listener.OnEntitiesAdded(entities); } + public bool HasDynamicEntity(int dynamicId) + { + return _dynamicEntityIds.Contains(dynamicId); + } + public void Destroy(IEnumerable entities) { foreach (EntityBase entity in entities) + { _ = _entities.Remove(entity); + _ = _dynamicEntityIds.Remove(entity.DynamicId); + } _ = _listener.OnEntitiesRemoved(entities); } diff --git a/GameServer/Systems/Entity/MonsterEntity.cs b/GameServer/Systems/Entity/MonsterEntity.cs index bb3d3c3..225c2dc 100644 --- a/GameServer/Systems/Entity/MonsterEntity.cs +++ b/GameServer/Systems/Entity/MonsterEntity.cs @@ -8,6 +8,7 @@ internal class MonsterEntity : EntityBase public MonsterEntity(long id, int configId, IGameActionListener listener) : base(id, listener) { ConfigId = configId; + DynamicId = configId; } public int ConfigId { get; } @@ -33,17 +34,20 @@ internal class MonsterEntity : EntityBase fsm.Fsms.Add(new DFsm { FsmId = 10007, // Main State Machine - CurrentState = 10013, // Battle Branching - Status = 1, // ?? - Flag = (int)EFsmStateFlag.Confirmed + CurrentState = 10013 // Battle Branching }); fsm.Fsms.Add(new DFsm { FsmId = 10007, // Main State Machine - CurrentState = 10015, // Moving Combat - Status = 1, // ?? - Flag = (int)EFsmStateFlag.Confirmed + CurrentState = 10015 // Moving Combat + }); + + // Some monsters need weapon + fsm.Fsms.Add(new DFsm + { + FsmId = 100, + CurrentState = 9 // [9 - Empty hand, 10 - Crowbar, 11 - flamethrower, 12 - chainsaw, 13 - electric blade, 14 - sniper rifle] }); } diff --git a/GameServer/gameplay.json b/GameServer/gameplay.json index 4312214..48edf50 100644 --- a/GameServer/gameplay.json +++ b/GameServer/gameplay.json @@ -6,13 +6,13 @@ "HeadFrame": 80060009, "Characters": [ 1402, 1302, 1203 ], "Position": { - "X": -35823, - "Y": 67132, - "Z": 4067 + "X": -45000, + "Y": 67800, + "Z": 2600 } }, "Features": { "TeleportByMapMark": true, - "UnlimitedEnergy": true + "UnlimitedEnergy": false } } \ No newline at end of file