/// <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 virtual IGameObjectWithProto ServerSpawnItem( IProtoTrigger trigger, IServerZone zone, IProtoItem protoItem, Vector2Ushort tilePosition) { var container = ObjectGroundItemsContainer.ServerTryGetOrCreateGroundContainerAtTile( forCharacter: null, tilePosition, writeWarningsToLog: false); if (container == null) { // cannot spawn item there return(null); } return(Server.Items.CreateItem(protoItem, container) .ItemAmounts .Keys .FirstOrDefault()); }
protected override int SharedCalculatePresetDesiredCount( ObjectSpawnPreset preset, IServerZone zone, int currentCount, int desiredCountByDensity) { // apply the server rate desiredCountByDensity = (int)Math.Round(desiredCountByDensity * this.spawnRateMultiplier, MidpointRounding.AwayFromZero); if (Api.IsEditor) { return(desiredCountByDensity); } // throttle spawn to ensure even distribution of spawned objects during specified hours since the startup const double spawnSpreadDurationHours = 48; var hoursSinceWorldCreation = Api.Server.Game.SecondsSinceWorldCreation / (60 * 60); // apply the timegate offset as there should be no deposits spawn until Xenogeology is available for research var timeGateHours = Api.GetProtoEntity <TechGroupXenogeologyT3>().TimeGatePvP / (60 * 60); if (timeGateHours > 0) { // as there is a timegate ensure the spawn could start immediately after it's expired without requiring any additional time timeGateHours -= spawnSpreadDurationHours / desiredCountByDensity; timeGateHours = Math.Max(0, timeGateHours); hoursSinceWorldCreation -= timeGateHours; hoursSinceWorldCreation = Math.Max(0, hoursSinceWorldCreation); } if (hoursSinceWorldCreation >= spawnSpreadDurationHours) { return(desiredCountByDensity); } return((int)(desiredCountByDensity * hoursSinceWorldCreation / spawnSpreadDurationHours)); }
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); } } }
/// <summary> /// Get all (already populated) areas with their objects inside the zone /// </summary> private async Task ServerFillSpawnAreasInZoneAsync( IServerZone zone, IReadOnlyDictionary <Vector2Ushort, SpawnZoneArea> areas, Func <Task> callbackYieldIfOutOfTime) { // this is a heavy method so we will try to yield every 100 objects to reduce the load const int defaultCounterToYieldValue = 100; var counterToYield = defaultCounterToYieldValue; // this check has a problem - it returns only objects strictly inside the zone, // but we also need to consider objects nearby the zone for restriction presets //await zone.PopulateStaticObjectsInZone(tempList, callbackYieldIfOutOfTime); TempListAllStaticWorldObjects.Clear(); await Api.Server.World.GetStaticWorldObjectsAsync(TempListAllStaticWorldObjects); foreach (var staticObject in TempListAllStaticWorldObjects) { await YieldIfOutOfTime(); if (staticObject.IsDestroyed) { continue; } var position = staticObject.TilePosition; var area = GetArea(position, isMobTrackingEnumeration: false); if (area is null) { continue; } var protoStaticWorldObject = staticObject.ProtoStaticWorldObject; if (!(protoStaticWorldObject is ObjectGroundItemsContainer)) { if (protoStaticWorldObject.IsIgnoredBySpawnScripts) { // we don't consider padding to certain objects such as ground decals // (though they still might affect spawn during the tiles check) continue; } // create entry for regular static object var preset = this.FindPreset(protoStaticWorldObject); if (preset != null && preset.Density > 0 && !zone.IsContainsPosition(staticObject.TilePosition)) { // this object is a part of the preset spawn list but it's not present in the zone // don't consider this object // TODO: this might cause a problem if there is a padding to this object check continue; } area.Add(preset, position); continue; } // ground container object var itemsContainer = ObjectGroundItemsContainer.GetPublicState(staticObject).ItemsContainer; foreach (var item in itemsContainer.Items) { // create entry for each item in the ground container var preset = this.FindPreset(item.ProtoItem); if (preset != null && preset.Density > 0 && !zone.IsContainsPosition(staticObject.TilePosition)) { // this object is a part of the preset spawn list but it's not present in the zone // don't consider this object continue; } area.Add(preset, position); } } var mobsTrackingManager = SpawnedMobsTrackingManagersStore.Get(this, zone); foreach (var mob in mobsTrackingManager.EnumerateAll()) { await YieldIfOutOfTime(); var position = mob.TilePosition; var area = GetArea(position, isMobTrackingEnumeration: true); area?.Add(this.FindPreset(mob.ProtoCharacter), position); } return; SpawnZoneArea GetArea(Vector2Ushort tilePosition, bool isMobTrackingEnumeration) { var zoneChunkStartPosition = SpawnZoneArea.CalculateStartPosition(tilePosition); if (areas.TryGetValue(zoneChunkStartPosition, out var area)) { return(area); } if (isMobTrackingEnumeration) { return(null); } return(null); // throw new Exception("No zone area found for " + tilePosition); //var newArea = new SpawnZoneArea(zoneChunkStartPosition, zoneChunk); //areas.Add(newArea); //return newArea; } Task YieldIfOutOfTime() { if (--counterToYield > 0) { return(Task.CompletedTask); } counterToYield = defaultCounterToYieldValue; return(callbackYieldIfOutOfTime()); } }
public CurrentlyExecutingTaskKey(SpawnConfig config, IProtoTrigger trigger, IServerZone zone) { this.config = config; this.trigger = trigger; this.zone = zone; }
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(); } }
protected virtual void OnZoneStop(IServerZone zone) { }
/// <summary> /// Get all (already populated) areas with their objects inside the zone /// </summary> private async Task ServerFillSpawnAreasInZoneAsync( IServerZone zone, IReadOnlyDictionary <Vector2Ushort, SpawnZoneArea> areas, Func <Task> callbackYieldIfOutOfTime) { // this is a heavy method so we will try to yield every 100 objects to reduce the load const int defaultCounterToYieldValue = 100; var counterToYield = defaultCounterToYieldValue; using (var tempList = Api.Shared.GetTempList <IStaticWorldObject>()) { await zone.PopulateStaticObjectsInZone(tempList, callbackYieldIfOutOfTime); foreach (var staticObject in tempList) { await YieldIfOutOfTime(); if (staticObject.IsDestroyed) { continue; } var position = staticObject.TilePosition; var area = GetArea(position, isMobTrackingEnumeration: false); var protoStaticWorldObject = staticObject.ProtoStaticWorldObject; if (!(protoStaticWorldObject is ObjectGroundItemsContainer)) { if (protoStaticWorldObject.Kind == StaticObjectKind.FloorDecal) { // we don't consider padding to decal objects // (though they still might affect spawn during tile check) continue; } // create entry for regular static object area.Add(this.FindPreset(protoStaticWorldObject), position); continue; } // ground container object var itemsContainer = ObjectGroundItemsContainer.GetPublicState(staticObject).ItemsContainer; foreach (var item in itemsContainer.Items) { // create entry for each item in the ground container area.Add(this.FindPreset(item.ProtoItem), position); } } } var mobsTrackingManager = SpawnedMobsTrackingManagersStore.Get(this, zone); foreach (var mob in mobsTrackingManager.EnumerateAll()) { await YieldIfOutOfTime(); var position = mob.TilePosition; var area = GetArea(position, isMobTrackingEnumeration: true); area?.Add(this.FindPreset(mob.ProtoCharacter), position); } return; SpawnZoneArea GetArea(Vector2Ushort tilePosition, bool isMobTrackingEnumeration) { var zoneChunkStartPosition = SpawnZoneArea.CalculateStartPosition(tilePosition); if (areas.TryGetValue(zoneChunkStartPosition, out var area)) { return(area); } if (isMobTrackingEnumeration) { return(null); } throw new Exception("No zone area found for " + tilePosition); //var newArea = new SpawnZoneArea(zoneChunkStartPosition, zoneChunk); //areas.Add(newArea); //return newArea; } Task YieldIfOutOfTime() { if (--counterToYield > 0) { return(Task.CompletedTask); } counterToYield = defaultCounterToYieldValue; return(callbackYieldIfOutOfTime()); } }
public abstract Task ServerInvoke(TScriptConfig config, IProtoTrigger trigger, IServerZone zone);
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); } } }
ServerGetCachedZoneAreaAsync( IServerZone zone, Func <Task> callbackYieldIfOutOfTime) { if (!Api.IsEditor) { if (ZoneAreasCache.TryGetValue(zone, out var result)) { // found cached entry - cleanup it foreach (var pair in result) { foreach (var list in pair.Value.WorldObjectsByPreset) { if (list.Value.Count > 0) { list.Value.Clear(); } } } return(result); } } var zoneChunks = await ZoneChunksHelper.CalculateZoneChunks(zone.QuadTree, ProtoZoneSpawnScript.SpawnZoneAreaSize, callbackYieldIfOutOfTime); var areas = new Dictionary <Vector2Ushort, ProtoZoneSpawnScript.SpawnZoneArea>(capacity: 16); await PopulateArea(); if (!Api.IsEditor) { ZoneAreasCache[zone] = areas; } return(areas); async Task PopulateArea() { // this is a heavy method so we will try to yield every few nodes to reduce the load const int defaultCounterToYieldValue = 100; var counterToYield = defaultCounterToYieldValue; foreach (var position in zone.AllPositions) { await YieldIfOutOfTime(); var zoneChunkStartPosition = ProtoZoneSpawnScript.SpawnZoneArea.CalculateStartPosition(position); if (areas.ContainsKey(zoneChunkStartPosition)) { // area already exists continue; } // create new zone area var zoneChunk = zoneChunks[zoneChunkStartPosition]; var spawnZoneArea = new ProtoZoneSpawnScript.SpawnZoneArea(zoneChunkStartPosition, zoneChunk); areas.Add(zoneChunkStartPosition, spawnZoneArea); } Task YieldIfOutOfTime() { if (--counterToYield > 0) { return(Task.CompletedTask); } counterToYield = defaultCounterToYieldValue; return(callbackYieldIfOutOfTime()); } } }
private static Vector2Ushort?TryFindZoneSpawnPosition( ICharacter character, IServerZone spawnZone, Random random, bool isRespawn) { var characterDeathPosition = Vector2Ushort.Zero; if (isRespawn && character.ProtoCharacter is PlayerCharacter) { var privateState = PlayerCharacter.GetPrivateState(character); if (!privateState.LastDeathTime.HasValue) { return(null); } characterDeathPosition = privateState.LastDeathPosition; } var restrictedZone = ZoneSpecialConstructionRestricted.Instance.ServerZoneInstance; for (var attempt = 0; attempt < SpawnInZoneAttempts; attempt++) { var randomPosition = spawnZone.GetRandomPosition(random); if (isRespawn) { var sqrDistance = randomPosition.TileSqrDistanceTo(characterDeathPosition); if (sqrDistance > MaxDistanceWhenRespawnSqr || sqrDistance < MinDistanceWhenRespawnSqr) { // too close or too far for the respawn continue; } } if (restrictedZone.IsContainsPosition(randomPosition)) { // the position is inside the restricted zone continue; } if (worldService.GetTile(randomPosition).ProtoTile is IProtoTileCold) { // cannot respawn in cold biome continue; } if (!LandClaimSystem.SharedIsPositionInsideOwnedOrFreeArea(randomPosition, character, requireFactionPermission: false)) { // the land is claimed by another player continue; } if (ServerCharacterSpawnHelper.IsPositionValidForCharacterSpawn(randomPosition.ToVector2D(), isPlayer: true)) { // valid position found return(randomPosition); } } return(null); }
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(); } } }