diff --git a/Dynamic Asteroids/Data/Scripts/DynamicAsteroids/AsteroidEntities/AsteroidEntity.cs b/Dynamic Asteroids/Data/Scripts/DynamicAsteroids/AsteroidEntities/AsteroidEntity.cs index 2ac35d1..de90ec1 100644 --- a/Dynamic Asteroids/Data/Scripts/DynamicAsteroids/AsteroidEntities/AsteroidEntity.cs +++ b/Dynamic Asteroids/Data/Scripts/DynamicAsteroids/AsteroidEntities/AsteroidEntity.cs @@ -87,7 +87,8 @@ public class AsteroidEntity : MyEntity, IMyDestroyableObject { public static AsteroidEntity CreateAsteroid(Vector3D position, float size, Vector3D initialVelocity, AsteroidType type, Quaternion? rotation = null, long? entityId = null) { var ent = new AsteroidEntity(); try { - if (entityId.HasValue) + // Only set EntityId if we're the server + if (entityId.HasValue && MyAPIGateway.Session.IsServer) ent.EntityId = entityId.Value; var massRange = AsteroidSettings.MinMaxMassByType[type]; diff --git a/Dynamic Asteroids/Data/Scripts/DynamicAsteroids/AsteroidEntities/AsteroidSettings.cs b/Dynamic Asteroids/Data/Scripts/DynamicAsteroids/AsteroidEntities/AsteroidSettings.cs index 4b3951f..6a701ce 100644 --- a/Dynamic Asteroids/Data/Scripts/DynamicAsteroids/AsteroidEntities/AsteroidSettings.cs +++ b/Dynamic Asteroids/Data/Scripts/DynamicAsteroids/AsteroidEntities/AsteroidSettings.cs @@ -24,10 +24,10 @@ public static class AsteroidSettings public static int SaveStateInterval = 600; public static int NetworkMessageInterval = 60; public static int SpawnInterval = 6; - public static int UpdateInterval = 60; - public static int NetworkUpdateInterval = 6; //this is the network metal pipe noise + public static int UpdateInterval = 60; //TODO: remove it does nothing atm + public static int NetworkUpdateInterval = 120; //this is the network metal pipe noise. or not actually what the FUCK is this load from public static int MaxAsteroidCount = 20000; - public static int MaxAsteroidsPerZone = 100; + public static int MaxAsteroidsPerZone = 500; public static int MaxTotalAttempts = 100; public static int MaxZoneAttempts = 50; public static double ZoneRadius = 10000.0; diff --git a/Dynamic Asteroids/Data/Scripts/DynamicAsteroids/AsteroidSpawner.cs b/Dynamic Asteroids/Data/Scripts/DynamicAsteroids/AsteroidSpawner.cs index 7e2ee32..015522b 100644 --- a/Dynamic Asteroids/Data/Scripts/DynamicAsteroids/AsteroidSpawner.cs +++ b/Dynamic Asteroids/Data/Scripts/DynamicAsteroids/AsteroidSpawner.cs @@ -255,10 +255,14 @@ public void ProcessMessages(Action sendAction) { private AsteroidStateCache _stateCache = new AsteroidStateCache(); private NetworkMessageCache _messageCache = new NetworkMessageCache(); + private ZoneNetworkManager _networkManager; + public AsteroidSpawner(RealGasGiantsApi realGasGiantsApi) { _realGasGiantsApi = realGasGiantsApi; + _networkManager = new ZoneNetworkManager(); } + public void Init(int seed) { if (!MyAPIGateway.Session.IsServer) return; Log.Info("Initializing AsteroidSpawner"); @@ -298,16 +302,31 @@ public void Close() { } } + public bool AreAsteroidsEnabled() { + bool enabled = AsteroidSettings.EnableGasGiantRingSpawning || + (AsteroidSettings.ValidSpawnLocations != null && + AsteroidSettings.ValidSpawnLocations.Count > 0); + Log.Info($"Asteroids enabled check: {enabled} (GasGiantRing: {AsteroidSettings.EnableGasGiantRingSpawning}, ValidLocations: {AsteroidSettings.ValidSpawnLocations?.Count ?? 0})"); + return enabled; + } + #region Player and Zone Management - private void AssignZonesToPlayers() { //TODO: all zone stuff in one class and asteroid in another, break the monolith of asteroidspawner.cs + private void AssignZonesToPlayers() { + if (!AreAsteroidsEnabled()) { + playerZones.Clear(); + return; + } + List players = new List(); MyAPIGateway.Players.GetPlayers(players); - foreach (IMyPlayer player in players) { Vector3D playerPosition = player.GetPosition(); - PlayerMovementData data; + if (Vector3D.IsZero(playerPosition) || double.IsNaN(playerPosition.X) || + double.IsNaN(playerPosition.Y) || double.IsNaN(playerPosition.Z)) { + continue; + } - // Check player speed first + PlayerMovementData data; if (playerMovementData.TryGetValue(player.IdentityId, out data)) { if (data.Speed > AsteroidSettings.ZoneSpeedThreshold) { Log.Info($"Player {player.DisplayName} moving too fast ({data.Speed:F2} m/s), skipping zone assignment."); @@ -318,20 +337,19 @@ private void AssignZonesToPlayers() { //TODO: all zone stuff in one class and a AsteroidZone existingZone; if (playerZones.TryGetValue(player.IdentityId, out existingZone)) { existingZone.LastActiveTime = DateTime.UtcNow; - if (!existingZone.IsPointInZone(playerPosition)) { - // Start transition to removal existingZone.MarkForRemoval(); - - // Only create new zone if old one is fully removed if (existingZone.State == ZoneState.Removed) { - var newZone = new AsteroidZone(playerPosition, AsteroidSettings.ZoneRadius); - playerZones[player.IdentityId] = newZone; + AsteroidZone removedZone; + playerZones.TryRemove(player.IdentityId, out removedZone); } } } - else if (data?.Speed <= AsteroidSettings.ZoneSpeedThreshold) { - playerZones[player.IdentityId] = new AsteroidZone(playerPosition, AsteroidSettings.ZoneRadius); + // Missing the else case where we create a new zone! + else { + var newZone = new AsteroidZone(playerPosition, AsteroidSettings.ZoneRadius); + playerZones.TryAdd(player.IdentityId, newZone); + Log.Info($"Created new zone for player at {playerPosition}"); } } } @@ -509,20 +527,6 @@ private AsteroidZone FindAlternateZone(Vector3D position, AsteroidZone excludeZo .OrderBy(z => Vector3D.Distance(position, z.Center)) .FirstOrDefault(); } - private AsteroidZone GetCachedZone(long playerId, Vector3D playerPosition) { - ZoneCache cache; - if (_zoneCache.TryGetValue(playerId, out cache) && !cache.IsExpired()) { - if (cache.Zone.IsPointInZone(playerPosition)) - return cache.Zone; - } - - AsteroidZone zone = new AsteroidZone(playerPosition, AsteroidSettings.ZoneRadius); - _zoneCache.AddOrUpdate(playerId, - new ZoneCache { Zone = zone, LastUpdateTime = DateTime.UtcNow }, - (key, oldCache) => new ZoneCache { Zone = zone, LastUpdateTime = DateTime.UtcNow }); - - return zone; - } #endregion #region Zone Updates @@ -549,12 +553,16 @@ public void MergeZones() { AssignMergedZonesToPlayers(mergedZones); } public void UpdateZones() { + if (!AreAsteroidsEnabled()) return; + List players = new List(); MyAPIGateway.Players.GetPlayers(players); Dictionary updatedZones = new Dictionary(); HashSet oldZones = new HashSet(playerZones.Values); foreach (IMyPlayer player in players) { + if (player?.Character == null) continue; + Vector3D playerPosition = player.GetPosition(); PlayerMovementData data; if (playerMovementData.TryGetValue(player.IdentityId, out data)) { @@ -563,34 +571,52 @@ public void UpdateZones() { } } - // Check if player is in any existing zone bool playerInZone = false; foreach (AsteroidZone zone in playerZones.Values) { if (zone.IsPointInZone(playerPosition)) { playerInZone = true; - oldZones.Remove(zone); // Remove from oldZones as it's still active + oldZones.Remove(zone); + + // Important: Update zone position but maintain asteroid tracking + var existingAsteroids = zone.ContainedAsteroids.ToList(); + zone.Center = playerPosition; + zone.LastActiveTime = DateTime.UtcNow; + + // Validate asteroids are still in the zone + foreach (var asteroidId in existingAsteroids) { + var asteroid = MyEntities.GetEntityById(asteroidId) as AsteroidEntity; + if (asteroid != null && !zone.IsPointInZone(asteroid.PositionComp.GetPosition())) { + zone.ContainedAsteroids.Remove(asteroidId); + zone.AsteroidCount = Math.Max(0, zone.AsteroidCount - 1); + } + } + updatedZones[player.IdentityId] = zone; break; } } if (!playerInZone) { - AsteroidZone newZone = new AsteroidZone(playerPosition, AsteroidSettings.ZoneRadius); + var newZone = new AsteroidZone(playerPosition, AsteroidSettings.ZoneRadius); + // Check for existing asteroids in the new zone's area + var entities = new HashSet(); + MyAPIGateway.Entities.GetEntities(entities); + foreach (var entity in entities) { + var asteroid = entity as AsteroidEntity; + if (asteroid != null && newZone.IsPointInZone(asteroid.PositionComp.GetPosition())) { + newZone.ContainedAsteroids.Add(asteroid.EntityId); + newZone.AsteroidCount++; + } + } updatedZones[player.IdentityId] = newZone; } } - // Clean up asteroids in zones that are no longer active foreach (var oldZone in oldZones) { - Log.Info($"Zone at {oldZone.Center} is no longer active, cleaning up asteroids"); CleanupZone(oldZone); } playerZones = new ConcurrentDictionary(updatedZones); - - if (!MyAPIGateway.Utilities.IsDedicated) { - MainSession.I.UpdateClientZones(playerZones.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)); - } } private void CleanupUnassignedZones() { var assignedZones = new HashSet(playerZones.Values); @@ -616,9 +642,17 @@ private void CleanupZone(AsteroidZone zone) { } } } + private int _zoneUpdateTimer = 0; + private const int ZONE_UPDATE_INTERVAL = 60; // Once per second public void SendZoneUpdates() { - if (!MyAPIGateway.Session.IsServer) - return; + if (!MyAPIGateway.Session.IsServer) return; + if (!AreAsteroidsEnabled()) return; + + _zoneUpdateTimer++; + if (_zoneUpdateTimer < ZONE_UPDATE_INTERVAL) return; + _zoneUpdateTimer = 0; + + if (!HaveZonesChanged()) return; var zonePacket = new ZoneUpdatePacket(); var mergedZoneIds = new HashSet(); @@ -648,10 +682,27 @@ public void SendZoneUpdates() { }); } + + // Serialize and send byte[] messageBytes = MyAPIGateway.Utilities.SerializeToBinary(zonePacket); MyAPIGateway.Multiplayer.SendMessageToOthers(32001, messageBytes); } + private Dictionary _lastZonePositions = new Dictionary(); + private bool HaveZonesChanged() { + bool changed = false; + foreach (var zone in playerZones.Values) + { + Vector3D lastPos; + if (!_lastZonePositions.TryGetValue(zone.EntityId, out lastPos) || + Vector3D.DistanceSquared(lastPos, zone.Center) > 1) { + changed = true; + _lastZonePositions[zone.EntityId] = zone.Center; + } + } + return changed || _lastZonePositions.Count != playerZones.Count; + } + #endregion #region Asteroid Management @@ -667,41 +718,6 @@ public void AddAsteroid(AsteroidEntity asteroid) { public bool TryRemoveAsteroid(AsteroidEntity asteroid) { return _asteroids.TryTake(out asteroid); } - public bool ContainsAsteroid(long asteroidId) { - return _asteroids.Any(a => a.EntityId == asteroidId); - } - private void LoadAsteroidsInRange(Vector3D playerPosition, AsteroidZone zone) { - int skippedCount = 0; - int respawnedCount = 0; - List skippedPositions = new List(); - List respawnedPositions = new List(); - foreach (AsteroidState state in _despawnedAsteroids.ToArray()) { - if (!zone.IsPointInZone(state.Position)) continue; - bool tooClose = _asteroids.Any(a => Vector3D.DistanceSquared(a.PositionComp.GetPosition(), state.Position) < AsteroidSettings.MinDistanceFromPlayer * AsteroidSettings.MinDistanceFromPlayer); - if (tooClose) { - skippedCount++; - skippedPositions.Add(state.Position); - continue; - } - respawnedCount++; - respawnedPositions.Add(state.Position); - AsteroidEntity asteroid = AsteroidEntity.CreateAsteroid(state.Position, state.Size, Vector3D.Zero, state.Type); - asteroid.EntityId = state.EntityId; - _asteroids.Add(asteroid); - AsteroidNetworkMessage message = new AsteroidNetworkMessage(state.Position, state.Size, Vector3D.Zero, Vector3D.Zero, state.Type, false, asteroid.EntityId, false, true, Quaternion.Identity); - byte[] messageBytes = MyAPIGateway.Utilities.SerializeToBinary(message); - MyAPIGateway.Multiplayer.SendMessageToOthers(32000, messageBytes); - _despawnedAsteroids.Remove(state); - - _updateQueue.Enqueue(asteroid); - } - if (skippedCount > 0) { - Log.Info($"Skipped respawn of {skippedCount} asteroids due to proximity to other asteroids or duplicate ID."); - } - if (respawnedCount > 0) { - Log.Info($"Respawned {respawnedCount} asteroids at positions: {string.Join(", ", respawnedPositions.Select(p => p.ToString()))}"); - } - } private void RemoveAsteroid(AsteroidEntity asteroid) { if (asteroid == null) return; @@ -710,18 +726,45 @@ private void RemoveAsteroid(AsteroidEntity asteroid) { _pendingRemovals.Enqueue(asteroid.EntityId); } + // Remove from all zone tracking + foreach (var zone in playerZones.Values) { + zone.ContainedAsteroids.Remove(asteroid.EntityId); + zone.TransferredFromOtherZone.Remove(asteroid.EntityId); + } + MyEntities.Remove(asteroid); asteroid.Close(); AsteroidEntity removed; _asteroids.TryTake(out removed); + + Log.Info($"Successfully removed asteroid {asteroid.EntityId}"); } catch (Exception ex) { Log.Exception(ex, typeof(AsteroidSpawner), $"Error removing asteroid {asteroid?.EntityId}"); } } public void SpawnAsteroids(List zones) { - if (!MyAPIGateway.Session.IsServer) return; // Ensure only server handles spawning + if (!MyAPIGateway.Session.IsServer) return; + if (!AreAsteroidsEnabled()) return; + + Log.Info($"Starting asteroid spawn cycle for {zones.Count} zones"); + + // Update zone positions based on player positions + List players = new List(); + MyAPIGateway.Players.GetPlayers(players); + foreach (AsteroidZone zone in zones) { + var zoneOwner = players.FirstOrDefault(p => + playerZones.ContainsKey(p.IdentityId) && + playerZones[p.IdentityId] == zone); + + if (zoneOwner != null) { + Vector3D playerPos = zoneOwner.GetPosition(); + // Update zone position to follow player + zone.Center = playerPos; + zone.LastActiveTime = DateTime.UtcNow; + } + } int totalSpawnAttempts = 0; if (AsteroidSettings.MaxAsteroidCount == 0) { @@ -737,6 +780,14 @@ public void SpawnAsteroids(List zones) { UpdatePlayerMovementData(); foreach (AsteroidZone zone in zones) { + Log.Info($"Checking zone at {zone.Center}:" + + $"\n - Current count: {zone.TotalAsteroidCount}/{AsteroidSettings.MaxAsteroidsPerZone}" + + $"\n - Is marked for removal: {zone.IsMarkedForRemoval}" + + $"\n - Can spawn: {zone.TotalAsteroidCount < AsteroidSettings.MaxAsteroidsPerZone}"); + + if (zone.IsMarkedForRemoval) continue; + if (zone.TotalAsteroidCount >= AsteroidSettings.MaxAsteroidsPerZone) continue; + int asteroidsSpawned = 0; int zoneSpawnAttempts = 0; @@ -748,29 +799,28 @@ public void SpawnAsteroids(List zones) { } bool skipSpawning = false; - List players = new List(); - MyAPIGateway.Players.GetPlayers(players); - foreach (IMyPlayer player in players) { Vector3D playerPosition = player.GetPosition(); if (!zone.IsPointInZone(playerPosition)) continue; + PlayerMovementData data; if (!playerMovementData.TryGetValue(player.IdentityId, out data)) continue; + if (IsPlayerMovingTooFast(player)) { skipSpawning = true; + zone.CurrentSpeed = data.Speed; // Update zone speed tracking Log.Info($"Skipping asteroid spawning for player {player.DisplayName} due to high speed: {data.Speed:F2} m/s > {AsteroidSettings.ZoneSpeedThreshold} m/s"); break; } } - if (skipSpawning) { - continue; - } + if (skipSpawning) continue; while (zone.AsteroidCount < AsteroidSettings.MaxAsteroidsPerZone && - asteroidsSpawned < 10 && - zoneSpawnAttempts < AsteroidSettings.MaxZoneAttempts && - totalSpawnAttempts < AsteroidSettings.MaxTotalAttempts) { + asteroidsSpawned < 10 && + zoneSpawnAttempts < AsteroidSettings.MaxZoneAttempts && + totalSpawnAttempts < AsteroidSettings.MaxTotalAttempts) { + Vector3D newPosition; bool isInRing = false; bool validPosition = false; @@ -780,7 +830,6 @@ public void SpawnAsteroids(List zones) { newPosition = zone.Center + RandVector() * AsteroidSettings.ZoneRadius; zoneSpawnAttempts++; totalSpawnAttempts++; - //Log.Info($"Attempting to spawn asteroid at {newPosition} (attempt {totalSpawnAttempts})"); if (AsteroidSettings.EnableGasGiantRingSpawning && _realGasGiantsApi != null && _realGasGiantsApi.IsReady) { ringInfluence = _realGasGiantsApi.GetRingInfluenceAtPositionGlobal(newPosition); @@ -796,15 +845,15 @@ public void SpawnAsteroids(List zones) { } } while (!validPosition && - zoneSpawnAttempts < AsteroidSettings.MaxZoneAttempts && - totalSpawnAttempts < AsteroidSettings.MaxTotalAttempts); + zoneSpawnAttempts < AsteroidSettings.MaxZoneAttempts && + totalSpawnAttempts < AsteroidSettings.MaxTotalAttempts); - if (zoneSpawnAttempts >= AsteroidSettings.MaxZoneAttempts || totalSpawnAttempts >= AsteroidSettings.MaxTotalAttempts) + if (zoneSpawnAttempts >= AsteroidSettings.MaxZoneAttempts || + totalSpawnAttempts >= AsteroidSettings.MaxTotalAttempts) break; Vector3D newVelocity; if (!AsteroidSettings.CanSpawnAsteroidAtPoint(newPosition, out newVelocity, isInRing)) { - //Log.Info($"Cannot spawn asteroid at {newPosition}, skipping."); continue; } @@ -819,11 +868,6 @@ public void SpawnAsteroids(List zones) { return; } - if (zone.AsteroidCount >= AsteroidSettings.MaxAsteroidsPerZone) { - Log.Info($"Zone at {zone.Center} has reached its maximum asteroid count ({AsteroidSettings.MaxAsteroidsPerZone}). Skipping further spawning in this zone."); - break; - } - float spawnChance = isInRing ? MathHelper.Lerp(0.1f, 1f, ringInfluence) * AsteroidSettings.MaxRingAsteroidDensityMultiplier : 1f; @@ -843,19 +887,23 @@ public void SpawnAsteroids(List zones) { Quaternion rotation = Quaternion.CreateFromYawPitchRoll( (float)MainSession.I.Rand.NextDouble() * MathHelper.TwoPi, (float)MainSession.I.Rand.NextDouble() * MathHelper.TwoPi, - (float)MainSession.I.Rand.NextDouble() * MathHelper.TwoPi); + (float)MainSession.I.Rand.NextDouble() * MathHelper.TwoPi + ); AsteroidEntity asteroid = AsteroidEntity.CreateAsteroid(newPosition, size, newVelocity, type, rotation); - - if (asteroid == null) continue; - _asteroids.Add(asteroid); - zone.AsteroidCount++; - spawnedPositions.Add(newPosition); - - _messageCache.AddMessage(new AsteroidNetworkMessage(newPosition, size, newVelocity, Vector3D.Zero, type, false, asteroid.EntityId, false, true, rotation)); - asteroidsSpawned++; - - Log.Info($"Spawned asteroid at {newPosition} with size {size} and type {type}"); + if (asteroid != null) { + _asteroids.Add(asteroid); + zone.ContainedAsteroids.Add(asteroid.EntityId); // Add to tracking + zone.AsteroidCount++; + spawnedPositions.Add(newPosition); + + _messageCache.AddMessage(new AsteroidNetworkMessage( + newPosition, size, newVelocity, Vector3D.Zero, type, + false, asteroid.EntityId, false, true, rotation)); + + asteroidsSpawned++; + Log.Info($"Spawned asteroid {asteroid.EntityId} at {newPosition} with size {size} and type {type}"); + } } totalAsteroidsSpawned += asteroidsSpawned; @@ -982,138 +1030,71 @@ public void UpdateTick() { if (!MyAPIGateway.Session.IsServer) return; - // Do normal operations first - AssignZonesToPlayers(); - MergeZones(); - UpdateZones(); - SendZoneUpdates(); + // Skip all processing if spawning is disabled + bool spawningEnabled = (AreAsteroidsEnabled()); - try { - List players = new List(); - MyAPIGateway.Players.GetPlayers(players); - foreach (IMyPlayer player in players) { - AsteroidZone zone = GetCachedZone(player.IdentityId, player.GetPosition()); - if (zone != null) { - LoadAsteroidsInRange(player.GetPosition(), zone); + if (!spawningEnabled) { + if (playerZones.Count > 0) { + playerZones.Clear(); + Log.Info("Spawning disabled - cleared all zones"); + } + if (_asteroids.Count > 0) { + foreach (var asteroid in _asteroids.ToList()) { + RemoveAsteroid(asteroid); } + Log.Info("Spawning disabled - cleared all asteroids"); } + return; + } - _networkMessageTimer++; - if (_networkMessageTimer >= AsteroidSettings.NetworkUpdateInterval) { - SendNetworkMessages(); - _networkMessageTimer = 0; - } - // Normal update logic... - if (_updateIntervalTimer <= 0) { - UpdateAsteroids(playerZones.Values.ToList()); - ProcessAsteroidUpdates(); - _updateIntervalTimer = AsteroidSettings.UpdateInterval; - } - else { - _updateIntervalTimer--; - } + try { + AssignZonesToPlayers(); - if (_spawnIntervalTimer > 0) { - _spawnIntervalTimer--; + // Only process zones if we have any + if (playerZones.Count > 0) { + MergeZones(); + UpdateZones(); + SendZoneUpdates(); } - else { - SpawnAsteroids(playerZones.Values.ToList()); - _spawnIntervalTimer = AsteroidSettings.SpawnInterval; + + // Only process network messages if we have active zones and asteroids + if (playerZones.Count > 0 && _asteroids.Count > 0) { + _networkMessageTimer++; + if (_networkMessageTimer >= AsteroidSettings.NetworkUpdateInterval) { + SendNetworkMessages(); + _networkMessageTimer = 0; + } } - SendPositionUpdates(); - // Only run validation every 10 seconds - if (_updateIntervalTimer % 600 == 0) { - try { - ValidateAsteroidTracking(); + // Only process spawning if enabled + if (spawningEnabled) { + if (_spawnIntervalTimer <= 0) { + SpawnAsteroids(playerZones.Values.ToList()); + _spawnIntervalTimer = AsteroidSettings.SpawnInterval; } - catch (Exception ex) { - Log.Exception(ex, typeof(AsteroidSpawner), "Error triggering validation"); + else { + _spawnIntervalTimer--; } } - // Check if cleanup should run - if (!_isCleanupRunning && (DateTime.UtcNow - _lastCleanupTime).TotalSeconds >= CLEANUP_COOLDOWN_SECONDS) { + // Regular maintenance + if (_updateIntervalTimer % 600 == 0) { + ValidateAsteroidTracking(); + } + + if (!_isCleanupRunning && + (DateTime.UtcNow - _lastCleanupTime).TotalSeconds >= CLEANUP_COOLDOWN_SECONDS) { if (_asteroids.Count > 0) { - Log.Info($"Starting cleanup check"); CleanupOrphanedAsteroids(); } } - if (MyAPIGateway.Session.IsServer) { - ProcessPendingRemovals(); - } - + ProcessPendingRemovals(); } catch (Exception ex) { Log.Exception(ex, typeof(AsteroidSpawner), "Error in UpdateTick"); } } - private void UpdateAsteroids(List zones) { - var asteroidsToRemove = new List(); - var currentAsteroids = _asteroids.ToList(); - var activeZones = zones.Where(z => !z.IsMarkedForRemoval).ToList(); - - // Clear transferred tracking at start of update - foreach (var zone in zones) { - zone.TransferredFromOtherZone.Clear(); - } - - foreach (var asteroid in currentAsteroids) { - if (asteroid == null || asteroid.MarkedForClose) - continue; - - Vector3D asteroidPosition = asteroid.PositionComp.GetPosition(); - bool inAnyZone = false; - AsteroidZone primaryZone = null; - - // First pass - find primary zone (closest) - double closestDistance = double.MaxValue; - foreach (var zone in activeZones) { - if (zone.IsPointInZone(asteroidPosition)) { - double distance = Vector3D.DistanceSquared(asteroidPosition, zone.Center); - if (distance < closestDistance) { - closestDistance = distance; - primaryZone = zone; - inAnyZone = true; - } - } - } - - // Second pass - handle zone tracking - if (inAnyZone) { - // Add to primary zone's direct containment - primaryZone.ContainedAsteroids.Add(asteroid.EntityId); - - // Mark as transferred in other overlapping zones - foreach (var zone in activeZones) { - if (zone != primaryZone && zone.IsPointInZone(asteroidPosition)) { - zone.TransferredFromOtherZone.Add(asteroid.EntityId); - } - } - } - else { - // Not in any active zone - check if it's in a removing zone - var removingZone = zones.FirstOrDefault(z => - z.IsMarkedForRemoval && - z.IsPointInZone(asteroidPosition) && - !z.CanBeRemoved()); - - if (removingZone == null) { - asteroidsToRemove.Add(asteroid); - } - } - } - - // Process removals in batches - const int REMOVAL_BATCH_SIZE = 10; - for (int i = 0; i < asteroidsToRemove.Count; i += REMOVAL_BATCH_SIZE) { - var batch = asteroidsToRemove.Skip(i).Take(REMOVAL_BATCH_SIZE); - foreach (var asteroid in batch) { - RemoveAsteroid(asteroid); - } - } - } #endregion @@ -1125,120 +1106,26 @@ public void SendNetworkMessages() { if (!MyAPIGateway.Session.IsServer || !MyAPIGateway.Utilities.IsDedicated) return; - try { - // Process immediate messages first - int immediateMessagesSent = 0; - _messageCache.ProcessMessages(message => { - if (message.EntityId == 0) { - Log.Warning("Attempted to send message for asteroid with ID 0"); - return; - } - - AsteroidEntity asteroid = MyEntities.GetEntityById(message.EntityId) as AsteroidEntity; - if (asteroid == null) { - Log.Warning($"Attempted to send update for non-existent asteroid {message.EntityId}"); - return; - } - - // Only send if the asteroid has changed significantly - var state = _stateCache.GetState(asteroid.EntityId); - if (state != null && !state.HasChanged(asteroid)) { - return; - } - - AsteroidNetworkMessage updateMessage = new AsteroidNetworkMessage( - asteroid.PositionComp.GetPosition(), - asteroid.Properties.Diameter, - asteroid.Physics.LinearVelocity, - asteroid.Physics.AngularVelocity, - asteroid.Type, - false, - asteroid.EntityId, - false, - false, - Quaternion.CreateFromRotationMatrix(asteroid.WorldMatrix) - ); + // Skip if spawning is disabled or no active content + bool spawningEnabled = AreAsteroidsEnabled(); - byte[] messageBytes = MyAPIGateway.Utilities.SerializeToBinary(updateMessage); - if (messageBytes == null || messageBytes.Length == 0) { - Log.Warning("Failed to serialize network message"); - return; - } - - MyAPIGateway.Multiplayer.SendMessageToOthers(32000, messageBytes); - immediateMessagesSent++; - }); - - // Process batched updates if it's time - if (_networkMessageTimer >= AsteroidSettings.NetworkUpdateInterval) { - var batchPacket = new AsteroidBatchUpdatePacket(); - List players = new List(); - MyAPIGateway.Players.GetPlayers(players); - - foreach (AsteroidEntity asteroid in _asteroids) { - if (asteroid == null || asteroid.MarkedForClose) continue; - - bool shouldUpdate = false; - foreach (IMyPlayer player in players) { - if (ShouldUpdateAsteroid(asteroid, player.GetPosition())) { - shouldUpdate = true; - break; - } - } - - if (shouldUpdate) { - _stateCache.UpdateState(asteroid); - } - } - - var dirtyAsteroids = _stateCache.GetDirtyAsteroids(); - if (dirtyAsteroids.Count > 0) { - SendBatchedUpdates(dirtyAsteroids); - } - - _stateCache.ClearDirtyStates(); - } - - if (immediateMessagesSent > 0) { - Log.Info($"Server: Successfully sent {immediateMessagesSent} immediate asteroid updates"); - } - } - catch (Exception ex) { - Log.Exception(ex, typeof(AsteroidSpawner), "Error sending network messages"); + if (!spawningEnabled || playerZones.Count == 0 || _asteroids.Count == 0) { + return; } - } - public void SendPositionUpdates() { - if (!MyAPIGateway.Session.IsServer) return; try { - List players = new List(); - MyAPIGateway.Players.GetPlayers(players); - - foreach (AsteroidEntity asteroid in _asteroids) { - if (asteroid == null || asteroid.MarkedForClose) continue; - - bool shouldUpdate = false; - foreach (IMyPlayer player in players) { - if (ShouldUpdateAsteroid(asteroid, player.GetPosition())) { - shouldUpdate = true; - break; - } - } + ProcessImmediateMessages(); - if (shouldUpdate) { - _stateCache.UpdateState(asteroid); + if (_networkMessageTimer >= AsteroidSettings.NetworkUpdateInterval) { + var playerPositions = GetPlayerPositions(); + if (playerPositions.Count > 0) { + _networkManager.UpdateZoneAwareness(playerPositions, playerZones); + ProcessBatchedUpdates(); } } - - var dirtyAsteroids = _stateCache.GetDirtyAsteroids(); - if (dirtyAsteroids.Count > 0) { - SendBatchedUpdates(dirtyAsteroids); - } - - _stateCache.ClearDirtyStates(); } catch (Exception ex) { - Log.Exception(ex, typeof(AsteroidSpawner), "Error sending position updates"); + Log.Exception(ex, typeof(AsteroidSpawner), "Error sending network messages"); } } private void ProcessPendingRemovals() { @@ -1259,53 +1146,93 @@ private void ProcessPendingRemovals() { _lastRemovalBatch = DateTime.UtcNow; } } - public void ProcessAsteroidUpdates() { - var dirtyAsteroids = _stateCache.GetDirtyAsteroids() - .OrderBy(a => GetDistanceToClosestPlayer(a.Position));// Prioritize by distance + private bool ShouldUpdateAsteroid(AsteroidEntity asteroid, Vector3D playerPos) { + double distance = Vector3D.Distance(asteroid.PositionComp.GetPosition(), playerPos); - int updatesProcessed = 0; + // More aggressive distance-based throttling + if (distance < 1000) return _networkMessageTimer % 2 == 0; // Every 2 ticks + if (distance < 5000) return _networkMessageTimer % 15 == 0; // Every 15 ticks + if (distance < 10000) return _networkMessageTimer % 30 == 0; // Every 30 ticks + return _networkMessageTimer % 60 == 0; // Every 60 ticks for distant asteroids + } + private void ProcessImmediateMessages() { + int immediateMessagesSent = 0; + _messageCache.ProcessMessages(message => { + if (message.EntityId == 0) { + Log.Warning("Attempted to send message for asteroid with ID 0"); + return; + } - foreach (AsteroidState asteroidState in dirtyAsteroids) { - if (updatesProcessed >= UpdatesPerTick) break; + AsteroidEntity asteroid = MyEntities.GetEntityById(message.EntityId) as AsteroidEntity; + if (asteroid == null) { + Log.Warning($"Attempted to send update for non-existent asteroid {message.EntityId}"); + return; + } - // Prepare and send an update message to clients - AsteroidNetworkMessage message = new AsteroidNetworkMessage( - asteroidState.Position, asteroidState.Size, asteroidState.Velocity, - Vector3D.Zero, asteroidState.Type, false, asteroidState.EntityId, - false, true, asteroidState.Rotation); + var state = _stateCache.GetState(asteroid.EntityId); + if (state != null && !state.HasChanged(asteroid)) { + return; + } + + AsteroidNetworkMessage updateMessage = new AsteroidNetworkMessage( + asteroid.PositionComp.GetPosition(), + asteroid.Properties.Diameter, + asteroid.Physics.LinearVelocity, + asteroid.Physics.AngularVelocity, + asteroid.Type, + false, + asteroid.EntityId, + false, + false, + Quaternion.CreateFromRotationMatrix(asteroid.WorldMatrix) + ); + + byte[] messageBytes = MyAPIGateway.Utilities.SerializeToBinary(updateMessage); + if (messageBytes == null || messageBytes.Length == 0) { + Log.Warning("Failed to serialize network message"); + return; + } + + MyAPIGateway.Multiplayer.SendMessageToOthers(32000, messageBytes); + immediateMessagesSent++; + }); - _messageCache.AddMessage(message);// Add to message cache for processing - updatesProcessed++; + if (immediateMessagesSent > 0) { + Log.Info($"Server: Processed {immediateMessagesSent} immediate messages"); } } - private void SendBatchedUpdates(List updates) { - const int MAX_UPDATES_PER_PACKET = 50; + private void ProcessBatchedUpdates() { + List players = new List(); + MyAPIGateway.Players.GetPlayers(players); - // Sort updates by priority (distance to closest player) - updates.Sort((a, b) => - GetDistanceToClosestPlayer(a.Position) - .CompareTo(GetDistanceToClosestPlayer(b.Position))); + if (players.Count == 0) { + Log.Info("No players to update, skipping batch update"); + return; + } - // Split into smaller batches - for (int i = 0; i < updates.Count; i += MAX_UPDATES_PER_PACKET) { - var batch = updates.Skip(i).Take(MAX_UPDATES_PER_PACKET); - var packet = new AsteroidBatchUpdatePacket(); - packet.Updates.AddRange(batch); + foreach (AsteroidEntity asteroid in _asteroids) { + if (asteroid == null || asteroid.MarkedForClose) continue; - byte[] data = MyAPIGateway.Utilities.SerializeToBinary(packet); - MyAPIGateway.Multiplayer.SendMessageToOthers(32000, data); + bool shouldUpdate = false; + foreach (IMyPlayer player in players) { + if (ShouldUpdateAsteroid(asteroid, player.GetPosition())) { + shouldUpdate = true; + break; + } + } - Log.Info($"Sent batch update containing {batch.Count()} asteroids"); + if (shouldUpdate) { + _stateCache.UpdateState(asteroid); + } } - } - private bool ShouldUpdateAsteroid(AsteroidEntity asteroid, Vector3D playerPos) { - double distance = Vector3D.Distance(asteroid.PositionComp.GetPosition(), playerPos); - // Update closer asteroids more frequently - if (distance < 1000) return _networkMessageTimer % 2 == 0; - if (distance < 5000) return _networkMessageTimer % 10 == 0; - if (distance < 10000) return _networkMessageTimer % 20 == 0; - return _networkMessageTimer % 30 == 0; + var dirtyAsteroids = _stateCache.GetDirtyAsteroids(); + if (dirtyAsteroids.Count > 0) { + // Replace SendBatchedUpdates call with ZoneNetworkManager version + _networkManager.SendBatchedUpdates(dirtyAsteroids, playerZones); + } + + _stateCache.ClearDirtyStates(); } #endregion @@ -1407,7 +1334,7 @@ public double GetSmoothedSpeed() { #region Utility Methods private double GetDistanceToClosestPlayer(Vector3D position) { - double minDistance = double.MaxValue; + if (!AreAsteroidsEnabled()) return double.MaxValue; double minDistance = double.MaxValue; foreach (AsteroidZone zone in playerZones.Values) { double distance = Vector3D.DistanceSquared(zone.Center, position); if (distance < minDistance) @@ -1421,8 +1348,21 @@ private Vector3D RandVector() { double sinPhi = Math.Sin(phi); return Math.Pow(rand.NextDouble(), 1 / 3d) * new Vector3D(sinPhi * Math.Cos(theta), sinPhi * Math.Sin(theta), Math.Cos(phi)); } + private Dictionary GetPlayerPositions() { + var positions = new Dictionary(); + var players = new List(); + MyAPIGateway.Players.GetPlayers(players); + + foreach (var player in players) { + if (player?.Character != null) { + positions[player.IdentityId] = player.GetPosition(); + } + } + + return positions; + } #endregion - + private const int VALIDATION_BATCH_SIZE = 100; // Only check this many entities per validation private const double VALIDATION_MAX_TIME_MS = 16.0; // Max milliseconds to spend on validation (1 frame at 60fps) private int _lastValidatedIndex = 0; // Track where we left off @@ -1432,14 +1372,12 @@ private void ValidateAsteroidTracking() { var trackedIds = _asteroids.Select(a => a.EntityId).ToHashSet(); var entities = new HashSet(); MyAPIGateway.Entities.GetEntities(entities); - - // Convert to list for indexed access var entityList = entities.Skip(_lastValidatedIndex).Take(VALIDATION_BATCH_SIZE).ToList(); - int untrackedCount = 0; + + var untrackedAsteroids = new List(); int processedCount = 0; foreach (var entity in entityList) { - // Check if we're taking too long if ((DateTime.UtcNow - startTime).TotalMilliseconds > VALIDATION_MAX_TIME_MS) { Log.Warning($"Validation taking too long - processed {processedCount} entities before timeout"); break; @@ -1448,36 +1386,56 @@ private void ValidateAsteroidTracking() { var asteroid = entity as AsteroidEntity; if (asteroid != null && !asteroid.MarkedForClose) { if (!trackedIds.Contains(asteroid.EntityId)) { - untrackedCount++; + untrackedAsteroids.Add(asteroid); Log.Warning($"Found untracked asteroid {asteroid.EntityId} at {asteroid.PositionComp.GetPosition()}"); } } processedCount++; } - // Update the index for next time + // Handle untracked asteroids + if (untrackedAsteroids.Count > 0) { + bool inActiveZone = false; + foreach (var asteroid in untrackedAsteroids) { + // Check if asteroid is in any active zone + foreach (var zone in playerZones.Values) { + if (zone.IsPointInZone(asteroid.PositionComp.GetPosition())) { + // Add to tracking if in active zone + _asteroids.Add(asteroid); + zone.ContainedAsteroids.Add(asteroid.EntityId); + inActiveZone = true; + Log.Info($"Recovered untracked asteroid {asteroid.EntityId} in active zone"); + break; + } + } + + // Remove if not in any active zone + if (!inActiveZone) { + Log.Info($"Removing untracked asteroid {asteroid.EntityId} outside active zones"); + RemoveAsteroid(asteroid); + } + } + } + _lastValidatedIndex += processedCount; if (_lastValidatedIndex >= entities.Count) { - _lastValidatedIndex = 0; // Reset when we've checked everything - - if (untrackedCount > 0) { - Log.Warning($"Validation cycle complete: Found {untrackedCount} untracked asteroids in world"); + _lastValidatedIndex = 0; + if (untrackedAsteroids.Count > 0) { + Log.Warning($"Validation cycle complete: Found and handled {untrackedAsteroids.Count} untracked asteroids"); } } double elapsedMs = (DateTime.UtcNow - startTime).TotalMilliseconds; - if (elapsedMs > 5.0) // Log if taking more than 5ms - { + if (elapsedMs > 5.0) { Log.Info($"Asteroid validation took {elapsedMs:F2}ms to process {processedCount} entities"); } } catch (Exception ex) { Log.Exception(ex, typeof(AsteroidSpawner), "Error in asteroid validation"); - _lastValidatedIndex = 0; // Reset on error + _lastValidatedIndex = 0; } } - } } \ No newline at end of file diff --git a/Dynamic Asteroids/Data/Scripts/DynamicAsteroids/MainSession.cs b/Dynamic Asteroids/Data/Scripts/DynamicAsteroids/MainSession.cs index 9e6d67d..a00e0b8 100644 --- a/Dynamic Asteroids/Data/Scripts/DynamicAsteroids/MainSession.cs +++ b/Dynamic Asteroids/Data/Scripts/DynamicAsteroids/MainSession.cs @@ -25,6 +25,7 @@ public partial class MainSession : MySessionComponentBase { public AsteroidSpawner _spawner; private int _saveStateTimer; private int _networkMessageTimer; + private bool _isProcessingMessage = false; public RealGasGiantsApi RealGasGiantsApi { get; private set; } private int _testTimer = 0; private KeenRicochetMissileBSWorkaroundHandler _missileHandler; @@ -56,6 +57,13 @@ public override void LoadData() { _spawner.Init(seed); } + if (!MyAPIGateway.Session.IsServer) { + // Wait a few frames before starting + MyAPIGateway.Utilities.InvokeOnGameThread(() => { + MyAPIGateway.Utilities.InvokeOnGameThread(CleanupClientState); + }); + } + // Register network handlers for both client and server MyAPIGateway.Multiplayer.RegisterSecureMessageHandler(32000, OnSecureMessageReceived); MyAPIGateway.Multiplayer.RegisterSecureMessageHandler(32001, OnSecureMessageReceived); @@ -91,9 +99,9 @@ public override void BeforeStart() { protected override void UnloadData() { try { Log.Info("Unloading data in MainSession"); + if (_spawner != null) { if (MyAPIGateway.Session.IsServer) { - // Remove SaveAsteroidState call var asteroidsToRemove = _spawner.GetAsteroids().ToList(); foreach (var asteroid in asteroidsToRemove) { try { @@ -104,38 +112,42 @@ protected override void UnloadData() { Log.Exception(removeEx, typeof(MainSession), "Error removing asteroid during unload"); } } - _spawner.Close(); _spawner = null; } } - MyAPIGateway.Multiplayer.UnregisterSecureMessageHandler(32000, OnSecureMessageReceived); - MyAPIGateway.Multiplayer.UnregisterSecureMessageHandler(32001, OnSecureMessageReceived); - MyAPIGateway.Multiplayer.UnregisterSecureMessageHandler(32002, OnSettingsSyncReceived); - MyAPIGateway.Utilities.MessageEntered -= OnMessageEntered; - MyVisualScriptLogicProvider.PlayerConnected -= OnPlayerConnected; if (RealGasGiantsApi != null) { RealGasGiantsApi.Unload(); RealGasGiantsApi = null; } + if (!MyAPIGateway.Session.IsServer) { + Log.Info("Client session detected, performing final cleanup"); + CleanupClientState(); + } + + MyAPIGateway.Multiplayer.UnregisterSecureMessageHandler(32000, OnSecureMessageReceived); + MyAPIGateway.Multiplayer.UnregisterSecureMessageHandler(32001, OnSecureMessageReceived); + MyAPIGateway.Multiplayer.UnregisterSecureMessageHandler(32002, OnSettingsSyncReceived); + MyAPIGateway.Utilities.MessageEntered -= OnMessageEntered; + MyVisualScriptLogicProvider.PlayerConnected -= OnPlayerConnected; + _missileHandler.Unload(); AsteroidSettings.SaveSettings(); Log.Close(); I = null; + } catch (Exception ex) { MyLog.Default.WriteLine($"Error in UnloadData: {ex}"); try { Log.Exception(ex, typeof(MainSession), "Error in UnloadData"); } - catch { - } + catch { } } } - private void OnMessageEntered(string messageText, ref bool sendToOthers) { IMyPlayer player = MyAPIGateway.Session.Player; if (player == null || !IsPlayerAdmin(player)) return; @@ -527,55 +539,96 @@ private void ProcessServerMessage(AsteroidNetworkMessage message, ulong steamId) } private void RemoveAsteroidOnClient(long entityId) { - Log.Info($"Client: Removing asteroid with ID {entityId}"); + try { + Log.Info($"Client: Removing asteroid with ID {entityId}"); - AsteroidEntity asteroid = MyEntities.GetEntityById(entityId) as AsteroidEntity; - if (asteroid != null) { - try { - MyEntities.Remove(asteroid); - asteroid.Close(); - Log.Info($"Client: Successfully removed asteroid {entityId}"); - } - catch (Exception ex) { - Log.Exception(ex, typeof(MainSession), $"Error removing asteroid {entityId} on client"); + // Remove from tracking first + _knownAsteroidIds.Remove(entityId); + _serverPositions.Remove(entityId); + _serverRotations.Remove(entityId); + + // Then remove the entity + var asteroid = MyEntities.GetEntityById(entityId) as AsteroidEntity; + if (asteroid != null) { + try { + MyEntities.Remove(asteroid); + asteroid.Close(); + Log.Info($"Client: Successfully removed asteroid {entityId}"); + } + catch (Exception ex) { + Log.Warning($"Error removing asteroid {entityId}: {ex.Message}"); + } } } - else { - Log.Warning($"Client: Could not find asteroid with ID {entityId} to remove"); + catch (Exception ex) { + Log.Exception(ex, typeof(MainSession), $"Error removing asteroid {entityId} on client"); } } private void CreateNewAsteroidOnClient(AsteroidNetworkMessage message) { - try { - // Don't generate random rotation, use exactly what the server sent - var asteroid = AsteroidEntity.CreateAsteroid( - message.GetPosition(), - message.Size, - message.GetVelocity(), - message.GetType(), - message.GetRotation(), // Use server's rotation - message.EntityId - ); + // Always run asteroid creation on the game thread + MyAPIGateway.Utilities.InvokeOnGameThread(() => { + try { + Log.Info($"Attempting to create asteroid {message.EntityId}"); + + // Double check for existing entity + var existingEntity = MyEntities.GetEntityById(message.EntityId); + if (existingEntity != null) { + Log.Warning($"Found existing entity {message.EntityId}, removing it first"); + existingEntity.Close(); + MyEntities.Remove(existingEntity); + + // Force a frame update + MyAPIGateway.Utilities.InvokeOnGameThread(() => { + var asteroid = AsteroidEntity.CreateAsteroid( + message.GetPosition(), + message.Size, + message.GetVelocity(), + message.GetType(), + message.GetRotation(), + message.EntityId + ); - if (asteroid != null) { - if (asteroid.Physics != null) { - asteroid.Physics.LinearVelocity = message.GetVelocity(); - asteroid.Physics.AngularVelocity = message.GetAngularVelocity(); + if (asteroid != null) { + Log.Info($"Successfully created asteroid {message.EntityId}"); + _knownAsteroidIds.Add(message.EntityId); + } + }); } + else { + var asteroid = AsteroidEntity.CreateAsteroid( + message.GetPosition(), + message.Size, + message.GetVelocity(), + message.GetType(), + message.GetRotation(), + message.EntityId + ); - Log.Info($"Client: Successfully created asteroid {message.EntityId} with server rotation"); + if (asteroid != null) { + Log.Info($"Successfully created asteroid {message.EntityId}"); + _knownAsteroidIds.Add(message.EntityId); + } + } } - else { - Log.Warning($"Client: Failed to create asteroid {message.EntityId}"); + catch (Exception ex) { + Log.Warning($"Failed to create asteroid {message.EntityId}: {ex.Message}"); } - } - catch (Exception ex) { - Log.Exception(ex, typeof(MainSession), "Error creating asteroid on client"); - } + }); } + private HashSet _knownAsteroidIds = new HashSet(); + private void ProcessClientMessage(AsteroidNetworkMessage message) { + if (_isProcessingMessage) { + Log.Warning($"Skipping message for {message.EntityId} - already processing a message"); + return; + } + try { + _isProcessingMessage = true; + Log.Info($"Processing client message for asteroid {message.EntityId} (IsRemoval: {message.IsRemoval}, IsInitialCreation: {message.IsInitialCreation})"); + if (!NetworkMessageVerification.ValidateMessage(message)) { Log.Warning($"Client received invalid message - ID: {message.EntityId}"); return; @@ -583,53 +636,170 @@ private void ProcessClientMessage(AsteroidNetworkMessage message) { if (message.IsRemoval) { RemoveAsteroidOnClient(message.EntityId); + _knownAsteroidIds.Remove(message.EntityId); return; } + bool isKnown = _knownAsteroidIds.Contains(message.EntityId); AsteroidEntity existingAsteroid = MyEntities.GetEntityById(message.EntityId) as AsteroidEntity; - if (message.IsInitialCreation) { + + // For initial creation or if we detect a duplicate ID + if (message.IsInitialCreation || existingAsteroid != null) { + Log.Info($"Handling {(message.IsInitialCreation ? "initial creation" : "duplicate")} for asteroid {message.EntityId}"); + if (existingAsteroid != null) { - Log.Warning($"Received creation message for existing asteroid {message.EntityId}"); - return; + Log.Info($"Removing existing asteroid {message.EntityId} before recreation"); + RemoveAsteroidOnClient(message.EntityId); } - // On initial creation, don't generate random rotation, use server's - CreateNewAsteroidOnClient(message); + try { + CreateNewAsteroidOnClient(message); + _knownAsteroidIds.Add(message.EntityId); + Log.Info($"Successfully created asteroid {message.EntityId}"); + } + catch (Exception ex) { + Log.Warning($"Failed to create asteroid {message.EntityId}: {ex.Message}"); + // Ensure cleanup in case of failure + RemoveAsteroidOnClient(message.EntityId); + _knownAsteroidIds.Remove(message.EntityId); + } } - else if (existingAsteroid != null) { - UpdateExistingAsteroidOnClient(existingAsteroid, message); + // For regular updates to known asteroids + else if (isKnown) { + existingAsteroid = MyEntities.GetEntityById(message.EntityId) as AsteroidEntity; + if (existingAsteroid != null) { + UpdateExistingAsteroidOnClient(existingAsteroid, message); + } + else { + // Known asteroid but entity missing - recreate it + Log.Info($"Recreating missing known asteroid {message.EntityId}"); + try { + CreateNewAsteroidOnClient(new AsteroidNetworkMessage( + message.GetPosition(), message.Size, message.GetVelocity(), + message.GetAngularVelocity(), message.GetType(), false, + message.EntityId, false, true, message.GetRotation())); + } + catch (Exception ex) { + Log.Warning($"Failed to recreate known asteroid {message.EntityId}: {ex.Message}"); + _knownAsteroidIds.Remove(message.EntityId); + } + } } + // For unknown asteroids that aren't marked as initial creation else { - Log.Warning($"Received update for non-existent asteroid {message.EntityId}"); - CreateNewAsteroidOnClient(new AsteroidNetworkMessage( - message.GetPosition(), - message.Size, - message.GetVelocity(), - message.GetAngularVelocity(), - message.GetType(), - false, - message.EntityId, - false, - true, - message.GetRotation() // Use server's rotation - )); + Log.Info($"Received update for unknown asteroid {message.EntityId}, treating as new"); + try { + // Force IsInitialCreation to true for unknown asteroids + var newMessage = new AsteroidNetworkMessage( + message.GetPosition(), message.Size, message.GetVelocity(), + message.GetAngularVelocity(), message.GetType(), false, + message.EntityId, false, true, message.GetRotation()); + + CreateNewAsteroidOnClient(newMessage); + _knownAsteroidIds.Add(message.EntityId); + } + catch (Exception ex) { + Log.Warning($"Failed to create unknown asteroid {message.EntityId}: {ex.Message}"); + } } } catch (Exception ex) { Log.Exception(ex, typeof(MainSession), $"Error processing client message"); } + finally { + _isProcessingMessage = false; + } } - - // Add a separate method for batch updates private void ProcessBatchMessage(AsteroidBatchUpdatePacket packet) { try { + Log.Info($"Processing batch update with {packet.Removals?.Count ?? 0} removals, " + + $"{packet.Updates?.Count ?? 0} updates, " + + $"{packet.Spawns?.Count ?? 0} spawns"); + + // Handle removals first if (packet.Removals != null && packet.Removals.Count > 0) { foreach (long entityId in packet.Removals) { RemoveAsteroidOnClient(entityId); + _knownAsteroidIds.Remove(entityId); + } + } + + // Handle state updates + if (packet.Updates != null && packet.Updates.Count > 0) { + foreach (var state in packet.Updates) { + var asteroid = MyEntities.GetEntityById(state.EntityId) as AsteroidEntity; + if (asteroid != null) { + // Update existing asteroid + try { + MatrixD worldMatrix = MatrixD.CreateFromQuaternion(state.Rotation); + worldMatrix.Translation = state.Position; + asteroid.WorldMatrix = worldMatrix; + + if (asteroid.Physics != null) { + asteroid.Physics.LinearVelocity = state.Velocity; + asteroid.Physics.AngularVelocity = state.AngularVelocity; + } + } + catch (Exception ex) { + Log.Warning($"Failed to update asteroid {state.EntityId}: {ex.Message}"); + } + } + else if (!_knownAsteroidIds.Contains(state.EntityId)) { + // Create missing asteroid + Log.Info($"Creating missing asteroid from batch update: {state.EntityId}"); + try { + var message = new AsteroidNetworkMessage( + state.Position, + state.Size, + state.Velocity, + state.AngularVelocity, + state.Type, + false, + state.EntityId, + false, + true, // Treat as initial creation + state.Rotation + ); + CreateNewAsteroidOnClient(message); + _knownAsteroidIds.Add(state.EntityId); + } + catch (Exception ex) { + Log.Warning($"Failed to create missing asteroid {state.EntityId}: {ex.Message}"); + } + } } } - // Process other batch data as needed... + // Handle new spawns + if (packet.Spawns != null && packet.Spawns.Count > 0) { + foreach (var spawn in packet.Spawns) { + try { + if (MyEntities.GetEntityById(spawn.EntityId) != null) { + Log.Warning($"Received spawn packet for existing asteroid {spawn.EntityId}, removing first"); + RemoveAsteroidOnClient(spawn.EntityId); + } + + var message = new AsteroidNetworkMessage( + spawn.Position, + spawn.Size, + spawn.Velocity, + spawn.AngularVelocity, + spawn.Type, + false, + spawn.EntityId, + false, + true, + spawn.Rotation + ); + CreateNewAsteroidOnClient(message); + _knownAsteroidIds.Add(spawn.EntityId); + Log.Info($"Created new asteroid from spawn packet: {spawn.EntityId}"); + } + catch (Exception ex) { + Log.Warning($"Failed to process spawn packet for asteroid {spawn.EntityId}: {ex.Message}"); + } + } + } } catch (Exception ex) { Log.Exception(ex, typeof(MainSession), "Error processing batch update"); @@ -758,50 +928,65 @@ public void UpdateClientZones(Dictionary serverZones) { } } } - + private Dictionary _lastProcessedZonePositions = new Dictionary(); private void ProcessZoneMessage(byte[] message) { try { var zonePacket = MyAPIGateway.Utilities.SerializeFromBinary(message); - if (zonePacket?.Zones == null) - return; + if (zonePacket?.Zones == null || zonePacket.Zones.Count == 0) return; - // In singleplayer, we should get zones directly from the spawner - if (MyAPIGateway.Session.IsServer && !MyAPIGateway.Utilities.IsDedicated) { - if (_spawner != null) { - UpdateClientZones(_spawner.playerZones.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)); + // Clear all existing known asteroid IDs when zones change significantly + bool significantZoneChange = false; + foreach (var zoneData in zonePacket.Zones) { + Vector3D lastPos; + if (_lastProcessedZonePositions.TryGetValue(zoneData.PlayerId, out lastPos)) { + if (Vector3D.DistanceSquared(lastPos, zoneData.Center) > AsteroidSettings.ZoneRadius * AsteroidSettings.ZoneRadius) { + significantZoneChange = true; + break; + } + } + else { + significantZoneChange = true; } - return; } - var previousZones = new Dictionary(_clientZones); - _clientZones.Clear(); + if (significantZoneChange) { + Log.Info("Significant zone change detected, clearing client state"); + _knownAsteroidIds.Clear(); + _serverPositions.Clear(); + _serverRotations.Clear(); + } - foreach (var zoneData in zonePacket.Zones) { - var newZone = new AsteroidZone(zoneData.Center, zoneData.Radius) { - IsMarkedForRemoval = !zoneData.IsActive, - IsMerged = zoneData.IsMerged, - CurrentSpeed = zoneData.CurrentSpeed - }; - _clientZones[zoneData.PlayerId] = newZone; - previousZones.Remove(zoneData.PlayerId); - } - - // Handle removed zones - foreach (var removedZone in previousZones.Values) { - _lastRemovedZones.Enqueue(removedZone); - while (_lastRemovedZones.Count > 5) - _lastRemovedZones.Dequeue(); - - if (!MyAPIGateway.Session.IsServer) { - var entities = new HashSet(); - MyAPIGateway.Entities.GetEntities(entities); - - foreach (var entity in entities) { - var asteroid = entity as AsteroidEntity; - if (asteroid != null && removedZone.IsPointInZone(asteroid.PositionComp.GetPosition())) { - RemoveAsteroidOnClient(asteroid.EntityId); + if (!MyAPIGateway.Session.IsServer) { + _clientZones.Clear(); + foreach (var zoneData in zonePacket.Zones) { + var newZone = new AsteroidZone(zoneData.Center, zoneData.Radius) { + IsMarkedForRemoval = !zoneData.IsActive, + IsMerged = zoneData.IsMerged, + CurrentSpeed = zoneData.CurrentSpeed, + LastActiveTime = DateTime.UtcNow + }; + _clientZones[zoneData.PlayerId] = newZone; + _lastProcessedZonePositions[zoneData.PlayerId] = zoneData.Center; + } + + // Clean up asteroids that are no longer in any zone + var entities = new HashSet(); + MyAPIGateway.Entities.GetEntities(entities); + foreach (var entity in entities) { + var asteroid = entity as AsteroidEntity; + if (asteroid == null) continue; + + bool inAnyZone = false; + foreach (var zone in _clientZones.Values) { + if (zone.IsPointInZone(asteroid.PositionComp.GetPosition())) { + inAnyZone = true; + break; } } + + if (!inAnyZone) { + RemoveAsteroidOnClient(asteroid.EntityId); + } } } } @@ -852,5 +1037,73 @@ private void SendSettingsToClient(ulong steamId) { Log.Exception(ex, typeof(MainSession), "Error sending settings to client"); } } + + private void CleanupClientState() { + Log.Info("Starting client state cleanup..."); + + // First, clear all our tracking + _knownAsteroidIds.Clear(); + _serverPositions.Clear(); + _serverRotations.Clear(); + _clientZones.Clear(); + + // Force a game engine update to ensure entity lists are current + MyAPIGateway.Utilities.InvokeOnGameThread(() => { + try { + Log.Info("Performing entity cleanup on game thread"); + var entities = new HashSet(); + MyAPIGateway.Entities.GetEntities(entities); + + // First pass: Close all asteroids + foreach (var entity in entities) + { + AsteroidEntity asteroid = entity as AsteroidEntity; + if (asteroid != null) { + try { + Log.Info($"Closing asteroid {asteroid.EntityId}"); + asteroid.Close(); + } + catch (Exception ex) { + Log.Warning($"Error closing asteroid {asteroid.EntityId}: {ex.Message}"); + } + } + } + + // Second pass: Remove all asteroids + foreach (var entity in entities) + { + AsteroidEntity asteroid = entity as AsteroidEntity; + if (asteroid != null) { + try { + Log.Info($"Removing asteroid {asteroid.EntityId} from entities"); + MyEntities.Remove(asteroid); + } + catch (Exception ex) { + Log.Warning($"Error removing asteroid {asteroid.EntityId}: {ex.Message}"); + } + } + } + + // Verify cleanup + entities.Clear(); + MyAPIGateway.Entities.GetEntities(entities); + var remainingAsteroids = entities.Where(e => e is AsteroidEntity).ToList(); + if (remainingAsteroids.Any()) { + Log.Warning($"Found {remainingAsteroids.Count} remaining asteroids after cleanup:"); + foreach (var asteroid in remainingAsteroids) { + Log.Warning($" - Asteroid {asteroid.EntityId} still exists"); + } + } + } + catch (Exception ex) { + Log.Exception(ex, typeof(MainSession), "Error during entity cleanup"); + } + }); + + // Wait a frame to ensure cleanup is complete + MyAPIGateway.Utilities.InvokeOnGameThread(() => { + Log.Info("Cleanup verification complete"); + }); + } } } \ No newline at end of file diff --git a/Dynamic Asteroids/Data/Scripts/DynamicAsteroids/ZoneNetworkManager.cs b/Dynamic Asteroids/Data/Scripts/DynamicAsteroids/ZoneNetworkManager.cs new file mode 100644 index 0000000..22a9a79 --- /dev/null +++ b/Dynamic Asteroids/Data/Scripts/DynamicAsteroids/ZoneNetworkManager.cs @@ -0,0 +1,83 @@ +using Sandbox.ModAPI; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using VRage.Game.ModAPI; +using VRageMath; + +namespace DynamicAsteroids.Data.Scripts.DynamicAsteroids { + public class ZoneNetworkManager { + private Dictionary> _playerZoneAwareness = new Dictionary>(); + private const double ZONE_AWARENESS_RADIUS = 25000; // Only sync zones within this range + + public void UpdateZoneAwareness( + Dictionary playerPositions, + ConcurrentDictionary zones) { + foreach (var player in playerPositions) { + if (!_playerZoneAwareness.ContainsKey(player.Key)) { + _playerZoneAwareness[player.Key] = new HashSet(); + } + + // Find zones this player should be aware of + var relevantZones = zones.Where(z => + Vector3D.Distance(player.Value, z.Value.Center) <= ZONE_AWARENESS_RADIUS); + + _playerZoneAwareness[player.Key] = new HashSet(relevantZones.Select(z => z.Key)); + } + } + + public void SendBatchedUpdates(List updates, ConcurrentDictionary zones) { + if (updates == null || updates.Count == 0 || zones == null || zones.Count == 0) { + return; + } + + // Group updates by zone + var updatesByZone = updates.GroupBy(u => { + var zone = zones.FirstOrDefault(z => z.Value.IsPointInZone(u.Position)); + return zone.Key; + }).Where(g => g.Key != 0); // Filter out updates with no zone + + if (!updatesByZone.Any()) return; + + foreach (var zoneGroup in updatesByZone) { + // Find players who should receive these updates + var relevantPlayers = _playerZoneAwareness + .Where(p => p.Value.Contains(zoneGroup.Key)) + .Select(p => p.Key) + .ToList(); + + if (relevantPlayers.Count == 0) continue; + + // Send batched updates only to relevant players + const int MAX_UPDATES_PER_PACKET = 25; + for (int i = 0; i < zoneGroup.Count(); i += MAX_UPDATES_PER_PACKET) { + var batch = zoneGroup.Skip(i).Take(MAX_UPDATES_PER_PACKET).ToList(); + if (batch.Count == 0) continue; + + var packet = new AsteroidBatchUpdatePacket(); + packet.Updates.AddRange(batch); + + byte[] data = MyAPIGateway.Utilities.SerializeToBinary(packet); + + // Send only to players who care about this zone + foreach (var playerId in relevantPlayers) { + var steamId = GetSteamId(playerId); + if (steamId != 0) { + MyAPIGateway.Multiplayer.SendMessageTo(32000, data, steamId); + } + } + } + } + } + + private ulong GetSteamId(long playerId) { + var players = new List(); + MyAPIGateway.Players.GetPlayers(players); + var player = players.FirstOrDefault(p => p.IdentityId == playerId); + return player?.SteamUserId ?? 0; + } + } +}