public sealed override async void ServerInvoke(SpawnConfig config, IProtoTrigger trigger, IServerZone zone)
        {
            var key = new CurrentlyExecutingTaskKey(config, trigger, zone);

            if (!this.executingEntries.Add(key))
            {
                // cannot schedule new request
                Logger.Warning(
                    "The spawning task is already active - cannot schedule a new spawn task until it's completed");
                return;
            }

            await serverSpawnSemaphore.WaitAsync(Api.CancellationToken);

            try
            {
                Logger.Info(new StringBuilder("Spawn script \"", capacity: 256)
                            .Append(this.ShortId)
                            .Append("\" for zone \"")
                            .Append(zone.ProtoGameObject.ShortId)
                            .Append("\": spawn started"));
                await this.ServerRunSpawnTaskAsync(config, trigger, zone);
            }
            finally
            {
                this.executingEntries.Remove(key);
                serverSpawnSemaphore.Release();
            }
        }
Beispiel #2
0
        protected override IGameObjectWithProto ServerSpawnStaticObject(
            IProtoTrigger trigger,
            IServerZone zone,
            IProtoStaticWorldObject protoStaticWorldObject,
            Vector2Ushort tilePosition)
        {
            // ensure there are no cliff neighbor tiles
            foreach (var tileOffset in protoStaticWorldObject.Layout.TileOffsets)
            {
                if (tileOffset.X == 0 &&
                    tileOffset.Y == 0)
                {
                    continue;
                }

                var tile = Server.World.GetTile(tilePosition.X + 2 * tileOffset.X,
                                                tilePosition.Y + 2 * tileOffset.Y);
                foreach (var neighborTile in tile.EightNeighborTiles)
                {
                    if (neighborTile.IsCliffOrSlope)
                    {
                        return(null);
                    }
                }
            }

            return(base.ServerSpawnStaticObject(trigger, zone, protoStaticWorldObject, tilePosition));
        }
Beispiel #3
0
        /// <summary>
        /// Server spawn callback for static object.
        /// </summary>
        /// <param name="trigger">Trigger leading to this spawn.</param>
        /// <param name="zone">Server zone instance.</param>
        /// <param name="protoStaticWorldObject">Prototype of static object to spawn.</param>
        /// <param name="tilePosition">Position to try spawn at.</param>
        protected IGameObjectWithProto ServerSpawnStaticObject(
            IProtoTrigger trigger,
            IServerZone zone,
            IProtoStaticWorldObject protoStaticWorldObject,
            Vector2Ushort tilePosition)
        {
            foreach (var tileOffset in protoStaticWorldObject.Layout.TileOffsets)
            {
                // ensure that each tile in object layout is inside the zone
                if (tileOffset != Vector2Int.Zero &&
                    !zone.IsContainsPosition(tilePosition.AddAndClamp(tileOffset)))
                {
                    // some tile is outside the zone
                    return(null);
                }
            }

            if (!protoStaticWorldObject.CheckTileRequirements(
                    tilePosition,
                    character: null,
                    logErrors: false))
            {
                // cannot spawn static object there
                return(null);
            }

            var spawnedObject = ServerWorldService.CreateStaticWorldObject(protoStaticWorldObject, tilePosition);

            if (spawnedObject == null)
            {
                // cannot spawn static object there
                return(null);
            }

            // if spawned a vegetation - set random growth progress
            if (protoStaticWorldObject is IProtoObjectVegetation protoVegetation)
            {
                double growProgress;
                if (trigger == null ||
                    trigger is TriggerWorldInit)
                {
                    // world initialization spawn
                    growProgress = RandomHelper.RollWithProbability(0.6)
                                       ? 1                          // 60% are spawned in full grown state
                                       : Random.Next(0, 11) / 10.0; // other are spawned with random growth progress
                }
                else
                {
                    // spawn saplings
                    growProgress = 0;
                }

                protoVegetation.ServerSetGrowthProgress(spawnedObject, growProgress);
            }

            ServerDecalsDestroyHelper.DestroyAllDecals(tilePosition, protoStaticWorldObject.Layout);
            return(spawnedObject);
        }
        /// <summary>
        /// Server spawn callback for mob.
        /// </summary>
        /// <param name="trigger">Trigger leading to this spawn.</param>
        /// <param name="zone">Server zone instance.</param>
        /// <param name="protoMob">Prototype of character mob object to spawn.</param>
        /// <param name="tilePosition">Position to try spawn at.</param>
        protected virtual IGameObjectWithProto ServerSpawnMob(
            IProtoTrigger trigger,
            IServerZone zone,
            IProtoCharacterMob protoMob,
            Vector2Ushort tilePosition)
        {
            var worldPosition = tilePosition.ToVector2D();

            if (!ServerCharacterSpawnHelper.IsPositionValidForCharacterSpawn(worldPosition,
                                                                             isPlayer: false))
            {
                // position is not valid for spawning
                return(null);
            }

            return(Server.Characters.SpawnCharacter(
                       protoMob,
                       worldPosition));
        }
Beispiel #5
0
        public sealed override async void ServerInvoke(SpawnConfig config, IProtoTrigger trigger, IServerZone zone)
        {
            var key = new CurrentlyExecutingTaskKey(config, trigger, zone);

            if (!this.executingEntries.Add(key))
            {
                // cannot schedule new request
                Logger.Warning(
                    "The spawning task is already active - cannot schedule a new spawn task until it's completed");
                return;
            }

            // Don't use tasks semaphore during the initial spawn.
            // This way new players cannot connect to the server until the spawn scripts have ended
            // (the initial spawn is not async anyway so semaphore doesn't do anything good here).
            var isInitialSpawn = trigger is null ||
                                 trigger is TriggerWorldInit;

            if (!isInitialSpawn)
            {
                await ServerSpawnTasksSemaphore.WaitAsync(Api.CancellationToken);
            }

            try
            {
                Logger.Info(new StringBuilder("Spawn script \"", capacity: 256)
                            .Append(this.ShortId)
                            .Append("\" for zone \"")
                            .Append(zone.ProtoGameObject.ShortId)
                            .Append("\": spawn started"));

                await this.ServerRunSpawnTaskAsync(config, trigger, zone);
            }
            finally
            {
                this.executingEntries.Remove(key);

                if (!isInitialSpawn)
                {
                    ServerSpawnTasksSemaphore.Release();
                }
            }
        }
        /// <summary>
        /// Server spawn callback (generic).
        /// </summary>
        /// <param name="trigger">Trigger leading to this spawn.</param>
        /// <param name="zone">Server zone instance.</param>
        /// <param name="protoGameObject">Prototype of object to spawn.</param>
        /// <param name="tilePosition">Position to try spawn at.</param>
        protected IGameObjectWithProto ServerSpawn(
            IProtoTrigger trigger,
            IServerZone zone,
            IProtoGameObject protoGameObject,
            Vector2Ushort tilePosition)
        {
            switch (protoGameObject)
            {
            case IProtoStaticWorldObject protoStaticWorldObject:
                return(this.ServerSpawnStaticObject(trigger, zone, protoStaticWorldObject, tilePosition));

            case IProtoCharacterMob protoMob:
                return(this.ServerSpawnMob(trigger, zone, protoMob, tilePosition));

            case IProtoItem protoItem:
                return(this.ServerSpawnItem(trigger, zone, protoItem, tilePosition));

            default:
                throw new Exception("Server don't know how to spawn this type of object - " + protoGameObject);
            }
        }
Beispiel #7
0
        /// <summary>
        /// Server spawn callback for item.
        /// </summary>
        /// <param name="trigger">Trigger leading to this spawn.</param>
        /// <param name="zone">Server zone instance.</param>
        /// <param name="protoItem">Prototype of item to spawn.</param>
        /// <param name="tilePosition">Position to try spawn at.</param>
        protected IGameObjectWithProto ServerSpawnItem(
            IProtoTrigger trigger,
            IServerZone zone,
            IProtoItem protoItem,
            Vector2Ushort tilePosition)
        {
            var container = ObjectGroundItemsContainer.ServerTryGetOrCreateGroundContainerAtTile(
                tilePosition,
                writeWarningsToLog: false);

            if (container == null)
            {
                // cannot spawn item there
                return(null);
            }

            return(Server.Items.CreateItem(protoItem, container)
                   .ItemAmounts
                   .Keys
                   .FirstOrDefault());
        }
        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);
                }
            }
        }
 public CurrentlyExecutingTaskKey(SpawnConfig config, IProtoTrigger trigger, IServerZone zone)
 {
     this.config  = config;
     this.trigger = trigger;
     this.zone    = zone;
 }
Beispiel #10
0
 public abstract Task ServerInvoke(TScriptConfig config, IProtoTrigger trigger, IServerZone zone);
Beispiel #11
0
 public void ServerInvoke(IProtoTrigger trigger, IServerZone serverZoneInstance)
 {
     this.ZoneScript.ServerInvoke((TProtoConfig)this, trigger, serverZoneInstance);
 }
        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);
                }
            }
        }