private void World_ObjectEnterScope(IGameObjectWithProto obj)
 {
     if (obj.GameObjectType == GameObjectType.Character)
     {
         var character = (ICharacter)obj;
         ComponentCharacterTracer.Init(character);
     }
 }
Beispiel #2
0
 private void WorldObjectLeftScopeHandler(IGameObjectWithProto obj)
 {
     if (this.viewModelsDictionary.TryGetValue(obj.ProtoGameObject, out var weakReference) &&
         weakReference.TryGetTarget(out var item) &&
         !item.IsDisposed)
     {
         item.Count--;
     }
 }
        public ServerDamageSourceEntry(IGameObjectWithProto byGameObject)
        {
            this.ProtoEntity = byGameObject.ProtoGameObject;

            if (byGameObject is ICharacter character &&
                !character.IsNpc)
            {
                this.Name = byGameObject.Name;
            }
Beispiel #4
0
        public ServerDamageSourceEntry(IGameObjectWithProto byGameObject)
        {
            this.ProtoEntity = byGameObject.ProtoGameObject;

            if (byGameObject is ICharacter byPlayerCharacter &&
                !byPlayerCharacter.IsNpc)
            {
                this.Name    = byPlayerCharacter.Name;
                this.ClanTag = PlayerCharacter.GetPublicState(byPlayerCharacter).ClanTag;
            }
Beispiel #5
0
        private static ICharacter GetAttackerCharacter(IGameObjectWithProto damageSource, out IProtoSkill protoSkill)
        {
            switch (damageSource)
            {
            case ICharacter character:
                protoSkill = !character.IsNpc
                                     ? PlayerCharacter.GetPrivateState(character)
                             .WeaponState.ProtoWeapon?.WeaponSkillProto
                                     : null;
                return(character);

            case { ProtoGameObject: IProtoStatusEffect } :
Beispiel #6
0
        protected override void ServerOnObjectSpawned(IGameObjectWithProto spawnedObject)
        {
            // spawn some guardian mobs so it will be harder to claim this deposit
            var objectGeothermalSpring = (IStaticWorldObject)spawnedObject;

            ServerMobSpawnHelper.ServerTrySpawnMobsCustom(
                protoMob: Api.GetProtoEntity <MobCloakedLizard>(),
                countToSpawn: 3,
                excludeBounds: objectGeothermalSpring.Bounds.Inflate(1),
                maxSpawnDistanceFromExcludeBounds: 2,
                noObstaclesCheckRadius: 0.5,
                maxAttempts: 200);
        }
        public void ServerReduceHealth(double damage, IGameObjectWithProto damageSource)
        {
            if (damage <= 0)
            {
                return;
            }

            if (this.HealthCurrent <= 0)
            {
                return;
            }

            if (damageSource != null)
            {
                // it's important to register the damage source before the damage is applied
                // (to use it in case of the subsequent death)
                CharacterDamageTrackingSystem.ServerRegisterDamage(damage,
                                                                   (ICharacter)this.GameObject,
                                                                   new ServerDamageSourceEntry(damageSource));
            }

            var newHealth = this.HealthCurrent - damage;

            if (newHealth <= 0 &&
                ((ICharacter)this.GameObject).IsNpc &&
                damageSource?.ProtoGameObject is IProtoStatusEffect)
            {
                var attackerCharacter = GetAttackerCharacter(damageSource, out _);
                if (attackerCharacter is null ||
                    attackerCharacter.IsNpc)
                {
                    // Don't allow killing mob by a status effect which is NOT added by a player character.
                    // This is a workaround to kill quests which cannot be finished
                    // when creature is killed by a status effect.
                    // TODO: Should be removed when we enable the damage tracking for mobs damage.
                    newHealth = float.Epsilon;
                }
            }

            this.ServerSetHealthCurrent((float)newHealth);

            if (newHealth <= 0)
            {
                var attackerCharacter = GetAttackerCharacter(damageSource, out var weaponSkill);
                ServerCharacterDeathMechanic.OnCharacterKilled(
                    attackerCharacter,
                    targetCharacter: (ICharacter)this.GameObject,
                    weaponSkill);
            }
        }
Beispiel #8
0
        public void ServerReduceHealth(double damage, IGameObjectWithProto damageSource)
        {
            if (damage <= 0)
            {
                return;
            }

            if (this.HealthCurrent <= 0)
            {
                return;
            }

            var damagedCharacter = (ICharacter)this.GameObject;

            if (damageSource is not null)
            {
                // it's important to register the damage source before the damage is applied
                // (to use it in case of the subsequent death)
                CharacterDamageTrackingSystem.ServerRegisterDamage(damage,
                                                                   damagedCharacter,
                                                                   new ServerDamageSourceEntry(damageSource));
            }

            var newHealth = this.HealthCurrent - damage;

            if (newHealth <= 0 &&
                damagedCharacter.IsNpc &&
                damageSource?.ProtoGameObject is IProtoStatusEffect)
            {
                var attackerCharacter = GetAttackerCharacter(damageSource, out _);
                if (attackerCharacter is null ||
                    attackerCharacter.IsNpc)
                {
                    // don't allow killing a mob by a status effect which is NOT added by a player character
                    newHealth = float.Epsilon;
                }
            }

            this.ServerSetHealthCurrent((float)newHealth);

            if (newHealth <= 0)
            {
                var attackerCharacter = GetAttackerCharacter(damageSource, out var weaponSkill);
                ServerCharacterDeathMechanic.OnCharacterKilled(
                    attackerCharacter,
                    targetCharacter: damagedCharacter,
                    weaponSkill);
            }
        }
Beispiel #9
0
        public void ServerReduceHealth(double damage, IGameObjectWithProto damageSource)
        {
            if (damage <= 0)
            {
                return;
            }

            if (damageSource != null)
            {
                // it's important to register the damage source before the damage is applied
                // (to use it in case of the subsequent death)
                CharacterDamageTrackingSystem.ServerRegisterDamage(damage,
                                                                   (ICharacter)this.GameObject,
                                                                   new ServerDamageSourceEntry(damageSource));
            }

            var newHealth = this.HealthCurrent - damage;

            this.ServerSetHealthCurrent((float)newHealth);
        }
        private static ICharacter GetAttackerCharacter(IGameObjectWithProto damageSource, out IProtoSkill protoSkill)
        {
            switch (damageSource)
            {
            case ICharacter character:
                protoSkill = !character.IsNpc
                                     ? character.GetPrivateState <PlayerCharacterPrivateState>().WeaponState
                             .ProtoWeapon?.WeaponSkillProto
                                     : null;
                return(character);

            case { } when damageSource.ProtoGameObject is IProtoStatusEffect:
                var statusEffectPublicState = damageSource.GetPublicState <StatusEffectPublicState>();
                protoSkill = statusEffectPublicState.ServerStatusEffectWasAddedByCharacterWeaponSkill;
                return(statusEffectPublicState.ServerStatusEffectWasAddedByCharacter);

            default:
                protoSkill = null;
                return(null);
            }
        }
        private async Task ServerRunSpawnTaskAsync(SpawnConfig config, IProtoTrigger trigger, IServerZone zone)
        {
            // The spawning algorithm is inspired by random scattering approach described in the article
            // www.voidinspace.com/2013/06/procedural-generation-a-vegetation-scattering-tool-for-unity3d-part-i/
            // but we use a quadtree instead of polygon for the spawn zone definition.
            if (zone.IsEmpty)
            {
                if (trigger != null)
                {
                    Logger.Important($"Cannot spawn at {zone} - the zone is empty");
                }
                else
                {
                    // the spawn script is triggered from the editor system
                    Logger.Dev($"Cannot spawn at {zone} - the zone is empty");
                }

                return;
            }

            var isInitialSpawn   = trigger == null || trigger is TriggerWorldInit;
            var yieldIfOutOfTime = isInitialSpawn
                                       ? () => Task.CompletedTask
                                       : (Func <Task>)Core.YieldIfOutOfTime;

            if (!isInitialSpawn)
            {
                // ensure that spawn execution is invoked in the end of frame
                await Core.AwaitEndOfFrame;
            }

            await yieldIfOutOfTime();

            var stopwatchTotal     = Stopwatch.StartNew();
            var zonePositionsCount = zone.PositionsCount;

            // please note: this operation is super heavy as it collects spawn areas with objects
            // that's why it's made async
            var spawnZoneAreas = await ServerSpawnZoneAreasHelper.ServerGetCachedZoneAreaAsync(zone, yieldIfOutOfTime);

            await this.ServerFillSpawnAreasInZoneAsync(zone,
                                                       spawnZoneAreas,
                                                       yieldIfOutOfTime);

            var maxSpawnFailedAttemptsInRow = isInitialSpawn
                                                  ? InitialSpawnMaxSpawnFailedAttemptsInRow
                                                  : DefaultSpawnMaxSpawnFailedAttemptsInRow;

            maxSpawnFailedAttemptsInRow = (int)Math.Min(int.MaxValue,
                                                        maxSpawnFailedAttemptsInRow * this.MaxSpawnAttemptsMultiplier);

            // calculate how many objects are already available
            var spawnedObjectsCount = spawnZoneAreas
                                      .Values
                                      .SelectMany(_ => _.WorldObjectsByPreset)
                                      .GroupBy(_ => _.Key)
                                      .ToDictionary(g => g.Key, g => g.Sum(list => list.Value.Count));

            await yieldIfOutOfTime();

            // calculate how many objects we need to spawn
            using var allSpawnRequestsTempList = Api.Shared.WrapInTempList(
                      this.SpawnList
                      .Where(preset => preset.Density > 0)
                      .Select(preset => this.ServerCreateSpawnRequestForPreset(
                                  preset,
                                  config,
                                  zone,
                                  zonePositionsCount,
                                  spawnedObjectsCount)));

            var allSpawnRequests = allSpawnRequestsTempList.AsList();

            var mobsTrackingManager = SpawnedMobsTrackingManagersStore.Get(this, zone);
            var physicsSpace        = Server.World.GetPhysicsSpace();
            var checkPlayersNearby  = !this.CanSpawnIfPlayersNearby;

            // stage 1: global spawn
            using var activeSpawnRequestsList = Api.Shared.WrapInTempList(
                      allSpawnRequests.Where(request => !request.UseSectorDensity &&
                                             request.CountToSpawn > 0));

            while (activeSpawnRequestsList.Count > 0)
            {
                await yieldIfOutOfTime();

                var spawnPosition = zone.GetRandomPosition(Random);
                TrySpawn(spawnPosition,
                         out _,
                         out _);
            }

            stopwatchTotal.Stop();
            var timeSpentSpawnGlobal = stopwatchTotal.Elapsed;

            await yieldIfOutOfTime();

            // stage 2: sector spawn
            stopwatchTotal.Restart();
            if (allSpawnRequests.Any(r => r.UseSectorDensity))
            {
                maxSpawnFailedAttemptsInRow = DefaultAreaSpawnAttemptsCountPerPreset;
                if (isInitialSpawn)
                {
                    maxSpawnFailedAttemptsInRow *= 16;
                }

                using var areasList = Api.Shared.WrapInTempList(spawnZoneAreas.Values);
                areasList.AsList().Shuffle();

                foreach (var area in areasList.AsList())
                {
                    await yieldIfOutOfTime();

                    if (activeSpawnRequestsList.Count > 0)
                    {
                        activeSpawnRequestsList.Clear();
                    }

                    foreach (var spawnRequest in allSpawnRequests)
                    {
                        if (!spawnRequest.UseSectorDensity)
                        {
                            continue;
                        }

                        if (IsAreaLocalDensityExceeded(spawnRequest, spawnRequest.Preset, area, out _))
                        {
                            // already spawned too many objects of the required type in the area
                            continue;
                        }

                        activeSpawnRequestsList.Add(spawnRequest);
                        spawnRequest.FailedAttempts = 0;
                    }

                    if (activeSpawnRequestsList.Count == 0)
                    {
                        continue;
                    }

                    // make a few attempts to spawn in this area
                    var attempts = activeSpawnRequestsList.Count * DefaultAreaSpawnAttemptsCountPerPreset;
                    for (var attempt = 0; attempt < attempts; attempt++)
                    {
                        await yieldIfOutOfTime();

                        var spawnPosition = area.GetRandomPositionInside(zone, Random);
                        TrySpawn(spawnPosition,
                                 out var spawnRequest,
                                 out var isSectorDensityExceeded);

                        if (isSectorDensityExceeded)
                        {
                            // sector density exceeded for this spawn request
                            activeSpawnRequestsList.Remove(spawnRequest);
                        }

                        if (activeSpawnRequestsList.Count == 0)
                        {
                            // everything for this sector has been spawned
                            break;
                        }
                    }
                }
            }

            stopwatchTotal.Stop();
            var timeSpentSpawnAreas = stopwatchTotal.Elapsed;

            var sb = new StringBuilder("Spawn script \"", capacity: 1024)
                     .Append(this.ShortId)
                     .Append("\" for zone \"")
                     .Append(zone.ProtoGameObject.ShortId)
                     .AppendLine("\": spawn completed. Stats:")
                     .Append("Time spent (including wait time distributed across frames): ")
                     .Append((timeSpentSpawnGlobal + timeSpentSpawnAreas).TotalMilliseconds.ToString("0.#"))
                     .Append("ms (global: ")
                     .Append(timeSpentSpawnGlobal.TotalMilliseconds.ToString("0.#"))
                     .Append("ms, areas: ")
                     .Append(timeSpentSpawnAreas.TotalMilliseconds.ToString("0.#"))
                     .AppendLine("ms)")
                     .AppendLine("Spawned objects:")
                     .AppendLine(
                "[format: * preset: +spawnedNowCount, currentCount/desiredCount=ratio%, currentDensity%/requiredDensity%]");

            foreach (var request in allSpawnRequests)
            {
                var currentDensity = request.CurrentCount / (double)zonePositionsCount;
                sb.Append("* ")
                .Append(request.UseSectorDensity ? "(sector) " : "(global) ")
                .Append(request.Preset.PrintEntries())
                .AppendLine(":")
                .Append("   +")
                .Append(request.SpawnedCount)
                .Append(", ")
                .Append(request.CurrentCount)
                .Append('/')
                .Append(request.DesiredCount)
                .Append('=')
                // it's normal if we will have NaN if desired count is zero
                .Append((request.CurrentCount / (double)request.DesiredCount * 100d).ToString("0.##"))
                .Append("%, ")
                .Append((currentDensity * 100).ToString("0.###"))
                .Append("%/")
                .Append((request.Density * 100).ToString("0.###"))
                .AppendLine("%");
            }

            Logger.Important(sb);

            void TrySpawn(
                Vector2Ushort spawnPosition,
                out SpawnRequest spawnRequest,
                out bool isSectorDensityExceeded)
            {
                spawnRequest = activeSpawnRequestsList.AsList().TakeByRandom(Random);
                var spawnProtoObject = spawnRequest.Preset.GetRandomObjectProto();
                IGameObjectWithProto spawnedObject = null;

                if (ServerIsCanSpawn(
                        spawnRequest,
                        spawnRequest.Preset,
                        spawnZoneAreas,
                        spawnPosition,
                        physicsSpace,
                        out var spawnZoneArea,
                        out isSectorDensityExceeded))
                {
                    if (!checkPlayersNearby ||
                        !ServerIsAnyPlayerNearby(spawnPosition))
                    {
                        spawnedObject = this.ServerSpawn(trigger, zone, spawnProtoObject, spawnPosition);
                    }
                }

                if (spawnedObject == null)
                {
                    // cannot spawn
                    if (++spawnRequest.FailedAttempts >= maxSpawnFailedAttemptsInRow)
                    {
                        // too many attempts failed - stop spawning this preset
                        activeSpawnRequestsList.Remove(spawnRequest);
                    }

                    return;
                }

                if (this.hasServerOnObjectSpawnedMethodOverride)
                {
                    try
                    {
                        this.ServerOnObjectSpawned(spawnedObject);
                    }
                    catch (Exception ex)
                    {
                        Logger.Exception(ex);
                    }
                }

                // spawned successfully
                // register object in zone area
                if (spawnZoneArea == null)
                {
                    throw new Exception("Should be impossible");
                }

                spawnZoneArea.Add(spawnRequest.Preset, spawnPosition);

                if (spawnProtoObject is IProtoCharacterMob)
                {
                    mobsTrackingManager.Add((ICharacter)spawnedObject);
                }

                spawnRequest.OnSpawn();
                spawnRequest.FailedAttempts = 0;
                if (spawnRequest.CountToSpawn == 0)
                {
                    activeSpawnRequestsList.Remove(spawnRequest);
                }
            }
        }
 protected virtual void ServerOnObjectSpawned(IGameObjectWithProto spawnedObject)
 {
 }
        private async Task ServerRunSpawnTaskAsync(SpawnConfig config, IProtoTrigger trigger, IServerZone zone)
        {
            // The spawning algorithm is inspired by random scattering approach described in the article
            // www.voidinspace.com/2013/06/procedural-generation-a-vegetation-scattering-tool-for-unity3d-part-i/
            // but we use a quadtree instead of polygon for the spawn zone definition.
            if (zone.IsEmpty)
            {
                if (trigger != null)
                {
                    Logger.Important($"Cannot spawn at {zone} - the zone is empty");
                }
                else
                {
                    // the spawn script is triggered from the editor system
                    Logger.Dev($"Cannot spawn at {zone} - the zone is empty");
                }

                return;
            }

            var isInitialSpawn   = trigger == null || trigger is TriggerWorldInit;
            var yieldIfOutOfTime = isInitialSpawn
                                       ? () => Task.CompletedTask
                                       : (Func <Task>)Core.YieldIfOutOfTime;

            await yieldIfOutOfTime();

            var stopwatchTotal     = Stopwatch.StartNew();
            var zonePositionsCount = zone.PositionsCount;

            // please note: this operation is super heavy as it collects spawn areas with objects
            // that's why it's made async
            var spawnZoneAreas = await ServerSpawnZoneAreasHelper.ServerGetCachedZoneAreaAsync(zone, yieldIfOutOfTime);

            await this.ServerFillSpawnAreasInZoneAsync(zone,
                                                       spawnZoneAreas,
                                                       yieldIfOutOfTime);

            var maxSpawnFailedAttemptsInRow = isInitialSpawn
                                                  ? InitialSpawnMaxSpawnFailedAttemptsInRow
                                                  : DefaultSpawnMaxSpawnFailedAttemptsInRow;

            maxSpawnFailedAttemptsInRow = (int)(maxSpawnFailedAttemptsInRow * this.MaxSpawnAttempsMultiplier);

            // calculate how many objects are already available
            var spawnedObjectsCount = spawnZoneAreas
                                      .Values
                                      .SelectMany(_ => _.WorldObjectsByPreset)
                                      .GroupBy(_ => _.Key)
                                      .ToDictionary(g => g.Key, g => g.Sum(list => list.Value.Count));

            await yieldIfOutOfTime();

            var densityMultiplier = config.DensityMultiplier;

            // calculate how many objects we need to spawn
            var allSpawnRequests =
                this.SpawnList
                .Where(preset => preset.Density > 0)
                .Select(
                    preset =>
            {
                var density      = preset.Density * densityMultiplier;
                var desiredCount = (int)(density * zonePositionsCount);
                var currentCount = spawnedObjectsCount.Find(preset);
                //if (isInitialSpawn)
                //{
                var countToSpawn = Math.Max(0, desiredCount - currentCount);
                //}

                // TODO: refactor this to be actually useful with local density
                //else // if this is an iteration spawn request
                //{
                //    // limit count to spawn to match the iteration limit
                //    var fractionSpawned = Math.Min(currentCount / (double)desiredCount, 1.0);
                //    var fractionRange = preset.IterationLimitFractionRange;
                //
                //    countToSpawn = (int)Math.Ceiling
                //        (desiredCount * fractionRange.GetByFraction(1 - fractionSpawned));
                //}

                // we're not using this feature
                NoiseSelector
                tileRandomSelector =
                    null;                 //this.CreateTileRandomSelector(density, preset, desiredCount);

                var useSectorDensity = preset.PresetUseSectorDensity &&
                                       ((density * SpawnZoneAreaSize * SpawnZoneAreaSize)
                                        >= SectorDensityThreshold);

                return(new SpawnRequest(preset,
                                        desiredCount,
                                        currentCount,
                                        countToSpawn,
                                        density,
                                        tileRandomSelector,
                                        useSectorDensity));
            })
                .ToList();

            var mobsTrackingManager = SpawnedMobsTrackingManagersStore.Get(this, zone);
            var playersPositions    = Server
                                      .Characters.EnumerateAllPlayerCharacters(onlyOnline: true, exceptSpectators: true)
                                      .Select(p => p.TilePosition)
                                      .ToList();

            var physicsSpace = Server.World.GetPhysicsSpace();

            // stage 1: global spawn
            var activeSpawnRequestsList = allSpawnRequests.Where(request => !request.UseSectorDensity &&
                                                                 request.CountToSpawn > 0)
                                          .ToList();

            while (activeSpawnRequestsList.Count > 0)
            {
                await yieldIfOutOfTime();

                var spawnPosition = zone.GetRandomPosition(Random);
                TrySpawn(spawnPosition, checkPlayersNearby: true, out _, out _);
            }

            stopwatchTotal.Stop();
            var timeSpentSpawnGlobal = stopwatchTotal.Elapsed;

            await yieldIfOutOfTime();

            // stage 2: sector spawn
            stopwatchTotal.Restart();
            if (allSpawnRequests.Any(r => r.UseSectorDensity))
            {
                maxSpawnFailedAttemptsInRow = AreaSpawnAttempsCountPerPreset;
                if (isInitialSpawn)
                {
                    maxSpawnFailedAttemptsInRow *= 16;
                }

                var areasList = spawnZoneAreas.Values.ToList();
                areasList.Shuffle();

                foreach (var area in areasList)
                {
                    await yieldIfOutOfTime();

                    if (activeSpawnRequestsList.Count > 0)
                    {
                        activeSpawnRequestsList.Clear();
                    }

                    foreach (var spawnRequest in allSpawnRequests)
                    {
                        if (!spawnRequest.UseSectorDensity)
                        {
                            continue;
                        }

                        if (IsAreaLocalDensityExceeded(spawnRequest, spawnRequest.Preset, area, out _))
                        {
                            // already spawned too many objects of the required type in the area
                            continue;
                        }

                        activeSpawnRequestsList.Add(spawnRequest);
                        spawnRequest.FailedAttempts = 0;
                    }

                    if (activeSpawnRequestsList.Count == 0)
                    {
                        continue;
                    }

                    if (ServerIsAnyPlayerNearby(area, playersPositions))
                    {
                        continue;
                    }

                    // make a few attempts to spawn in this area
                    var attempts = activeSpawnRequestsList.Count * AreaSpawnAttempsCountPerPreset;
                    for (var attempt = 0; attempt < attempts; attempt++)
                    {
                        await yieldIfOutOfTime();

                        var spawnPosition = area.GetRandomPositionInside(zone, Random);
                        TrySpawn(spawnPosition,
                                 checkPlayersNearby: false,
                                 out var spawnRequest,
                                 out var isSectorDensityExceeded);

                        if (isSectorDensityExceeded)
                        {
                            // sector density exceeded for this spawn request
                            activeSpawnRequestsList.Remove(spawnRequest);
                        }

                        if (activeSpawnRequestsList.Count == 0)
                        {
                            // everything for this sector has been spawned
                            break;
                        }
                    }
                }
            }

            stopwatchTotal.Stop();
            var timeSpentSpawnAreas = stopwatchTotal.Elapsed;

            var sb = new StringBuilder("Spawn script \"", capacity: 1024)
                     .Append(this.ShortId)
                     .Append("\" for zone \"")
                     .Append(zone.ProtoGameObject.ShortId)
                     .AppendLine("\": spawn completed. Stats:")
                     .Append("Time spent (including wait time distributed across frames): ")
                     .Append((timeSpentSpawnGlobal + timeSpentSpawnAreas).TotalMilliseconds.ToString("0.#"))
                     .Append("ms (global: ")
                     .Append(timeSpentSpawnGlobal.TotalMilliseconds.ToString("0.#"))
                     .Append("ms, areas: ")
                     .Append(timeSpentSpawnAreas.TotalMilliseconds.ToString("0.#"))
                     .AppendLine("ms)")
                     .AppendLine("Spawned objects:")
                     .AppendLine(
                "[format: * preset: +spawnedNowCount, currentCount/desiredCount=ratio%, currentDensity%/requiredDensity%]");

            foreach (var request in allSpawnRequests)
            {
                var currentDensity = request.CurrentCount / (double)zonePositionsCount;
                sb.Append("* ")
                .Append(request.UseSectorDensity ? "(sector) " : ("(global) "))
                .Append(request.Preset.PrintEntries())
                .AppendLine(":")
                .Append("   +")
                .Append(request.SpawnedCount)
                .Append(", ")
                .Append(request.CurrentCount)
                .Append('/')
                .Append(request.DesiredCount)
                .Append('=')
                // it's normal if we will have NaN if desired count is zero
                .Append((request.CurrentCount / (double)request.DesiredCount * 100d).ToString("0.##"))
                .Append("%, ")
                .Append((currentDensity * 100).ToString("0.###"))
                .Append("%/")
                .Append((request.Density * 100).ToString("0.###"))
                .AppendLine("%");
            }

            Logger.Important(sb);

            void TrySpawn(
                Vector2Ushort spawnPosition,
                bool checkPlayersNearby,
                out SpawnRequest spawnRequest,
                out bool isSectorDensityExceeded)
            {
                spawnRequest = activeSpawnRequestsList.TakeByRandom(Random);
                if (spawnRequest.TileRandomSelector != null &&
                    !spawnRequest.TileRandomSelector
                    .IsMatch(spawnPosition.X, spawnPosition.Y, rangeMultiplier: 1))
                {
                    // the spawn position doesn't satisfy the the random selector
                    isSectorDensityExceeded = false;
                    return;
                }

                var spawnProtoObject = spawnRequest.Preset.GetRandomObjectProto();
                IGameObjectWithProto spawnedObject = null;

                if (ServerIsCanSpawn(
                        spawnRequest,
                        spawnRequest.Preset,
                        spawnZoneAreas,
                        spawnPosition,
                        physicsSpace,
                        out var spawnZoneArea,
                        out isSectorDensityExceeded))
                {
                    if (!checkPlayersNearby ||
                        !ServerIsAnyPlayerNearby(spawnPosition, playersPositions))
                    {
                        spawnedObject = this.ServerSpawn(trigger, zone, spawnProtoObject, spawnPosition);
                    }
                }

                if (spawnedObject == null)
                {
                    // cannot spawn
                    if (++spawnRequest.FailedAttempts >= maxSpawnFailedAttemptsInRow)
                    {
                        // too many attempts failed - stop spawning this preset
                        activeSpawnRequestsList.Remove(spawnRequest);
                    }

                    return;
                }

                // spawned successfully
                // register object in zone area
                if (spawnZoneArea == null)
                {
                    throw new Exception("Should be impossible");
                }

                spawnZoneArea.Add(spawnRequest.Preset, spawnPosition);

                if (spawnProtoObject is IProtoCharacterMob)
                {
                    mobsTrackingManager.Add((ICharacter)spawnedObject);
                }

                spawnRequest.OnSpawn();
                spawnRequest.FailedAttempts = 0;
                if (spawnRequest.CountToSpawn == 0)
                {
                    activeSpawnRequestsList.Remove(spawnRequest);
                }
            }
        }