using System.Collections.Generic; using System.Linq; using Content.Shared.Weather; using Robust.Shared.Map; using Robust.Shared.Prototypes; using Robust.Shared.Timing; using Robust.Shared.GameObjects; using Content.Server.Atmos.Components; using Content.Server.Atmos.EntitySystems; using Content.Shared.Atmos; using Content.Shared.Maps; using Robust.Shared.Map.Components; using Content.Server.Chat.Systems; using Content.Shared.Light.EntitySystems; using Content.Shared.Light.Components; using System; namespace Content.Server.Weather; /// /// System responsible for managing dynamic weather changes and temperature adjustments for exposed tiles in a grid. /// public sealed class WeatherNomadsSystem : EntitySystem { // Dependencies injected via IoC [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly SharedWeatherSystem _weatherSystem = default!; [Dependency] private readonly AtmosphereSystem _atmosphere = default!; [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly SharedRoofSystem _roofSystem = default!; [Dependency] private readonly ITileDefinitionManager _tileDefManager = default!; [Dependency] private readonly SharedMapSystem _mapSystem = default!; [Dependency] private readonly ChatSystem _chat = default!; /// /// Structure representing properties of a weather type. /// private class WeatherType { public string? PrototypeId { get; set; } // ID of the weather prototype, null for "Clear" public int Weight { get; set; } // Weight for weather transition order (unused now, kept for compatibility) public float MinTemperature { get; set; } // Minimum temperature in Kelvin public float MaxTemperature { get; set; } // Maximum temperature in Kelvin } /// /// Dictionary defining available weather types and their properties. /// private readonly Dictionary _weatherTypes = new() { { "Clear", new WeatherType { PrototypeId = "", Weight = 0, MinTemperature = 293.15f, MaxTemperature = 293.15f } }, { "Rain", new WeatherType { PrototypeId = "Rain", Weight = 1, MinTemperature = 278.15f, MaxTemperature = 288.15f } }, { "Storm", new WeatherType { PrototypeId = "Storm", Weight = 3, MinTemperature = 273.15f, MaxTemperature = 278.15f } }, { "SnowfallLight", new WeatherType { PrototypeId = "SnowfallLight", Weight = 4, MinTemperature = 268.15f, MaxTemperature = 273.15f } }, { "SnowfallMedium", new WeatherType { PrototypeId = "SnowfallMedium", Weight = 5, MinTemperature = 258.15f, MaxTemperature = 268.15f } }, { "SnowfallHeavy", new WeatherType { PrototypeId = "SnowfallHeavy", Weight = 6, MinTemperature = 243.15f, MaxTemperature = 258.15f } }, { "Hail", new WeatherType { PrototypeId = "Hail", Weight = 7, MinTemperature = 273.15f, MaxTemperature = 278.15f } }, { "Sandstorm", new WeatherType { PrototypeId = "Sandstorm", Weight = 9, MinTemperature = 293.15f, MaxTemperature = 313.15f } }, { "SandstormHeavy", new WeatherType { PrototypeId = "SandstormHeavy", Weight = 10, MinTemperature = 293.15f, MaxTemperature = 313.15f } }, }; /// /// Dictionary mapping (Biome, Season, Precipitation) to specific weather types. /// private static readonly Dictionary<(Biome, string, Precipitation), string> _weatherTransitionMap = new() { // Summer { (Biome.Tundra, "Summer", Precipitation.Dry), "Clear" }, { (Biome.Tundra, "Summer", Precipitation.LightWet), "SnowfallLight" }, { (Biome.Tundra, "Summer", Precipitation.HeavyWet), "SnowfallMedium" }, { (Biome.Tundra, "Summer", Precipitation.Storm), "SnowfallHeavy" }, { (Biome.Taiga, "Summer", Precipitation.Dry), "Clear" }, { (Biome.Taiga, "Summer", Precipitation.LightWet), "Rain" }, { (Biome.Taiga, "Summer", Precipitation.HeavyWet), "Rain" }, { (Biome.Taiga, "Summer", Precipitation.Storm), "Hail" }, { (Biome.Temperate, "Summer", Precipitation.Dry), "Clear" }, { (Biome.Temperate, "Summer", Precipitation.LightWet), "Rain" }, { (Biome.Temperate, "Summer", Precipitation.HeavyWet), "Rain" }, { (Biome.Temperate, "Summer", Precipitation.Storm), "Storm" }, { (Biome.Sea, "Summer", Precipitation.Dry), "Clear" }, { (Biome.Sea, "Summer", Precipitation.LightWet), "Rain" }, { (Biome.Sea, "Summer", Precipitation.HeavyWet), "Rain" }, { (Biome.Sea, "Summer", Precipitation.Storm), "Storm" }, { (Biome.SemiArid, "Summer", Precipitation.Dry), "Clear" }, { (Biome.SemiArid, "Summer", Precipitation.LightWet), "Clear" }, { (Biome.SemiArid, "Summer", Precipitation.HeavyWet), "Rain" }, { (Biome.SemiArid, "Summer", Precipitation.Storm), "Rain" }, { (Biome.Desert, "Summer", Precipitation.Dry), "Clear" }, { (Biome.Desert, "Summer", Precipitation.LightWet), "Clear" }, { (Biome.Desert, "Summer", Precipitation.HeavyWet), "Sandstorm" }, { (Biome.Desert, "Summer", Precipitation.Storm), "SandstormHeavy" }, { (Biome.Savanna, "Summer", Precipitation.Dry), "Clear" }, { (Biome.Savanna, "Summer", Precipitation.LightWet), "Clear" }, { (Biome.Savanna, "Summer", Precipitation.HeavyWet), "Rain" }, { (Biome.Savanna, "Summer", Precipitation.Storm), "Storm" }, { (Biome.Jungle, "Summer", Precipitation.Dry), "Clear" }, { (Biome.Jungle, "Summer", Precipitation.LightWet), "Rain" }, { (Biome.Jungle, "Summer", Precipitation.HeavyWet), "Storm" }, { (Biome.Jungle, "Summer", Precipitation.Storm), "Storm" }, // Spring { (Biome.Tundra, "Spring", Precipitation.Dry), "Clear" }, { (Biome.Tundra, "Spring", Precipitation.LightWet), "SnowfallLight" }, { (Biome.Tundra, "Spring", Precipitation.HeavyWet), "SnowfallMedium" }, { (Biome.Tundra, "Spring", Precipitation.Storm), "SnowfallHeavy" }, { (Biome.Taiga, "Spring", Precipitation.Dry), "Clear" }, { (Biome.Taiga, "Spring", Precipitation.LightWet), "Rain" }, { (Biome.Taiga, "Spring", Precipitation.HeavyWet), "SnowfallLight" }, { (Biome.Taiga, "Spring", Precipitation.Storm), "SnowfallHeavy" }, { (Biome.Temperate, "Spring", Precipitation.Dry), "Clear" }, { (Biome.Temperate, "Spring", Precipitation.LightWet), "Rain" }, { (Biome.Temperate, "Spring", Precipitation.HeavyWet), "Storm" }, { (Biome.Temperate, "Spring", Precipitation.Storm), "SnowfallMedium" }, { (Biome.Sea, "Spring", Precipitation.Dry), "Clear" }, { (Biome.Sea, "Spring", Precipitation.LightWet), "Rain" }, { (Biome.Sea, "Spring", Precipitation.HeavyWet), "Rain" }, { (Biome.Sea, "Spring", Precipitation.Storm), "Storm" }, { (Biome.SemiArid, "Spring", Precipitation.Dry), "Clear" }, { (Biome.SemiArid, "Spring", Precipitation.LightWet), "Clear" }, { (Biome.SemiArid, "Spring", Precipitation.HeavyWet), "Rain" }, { (Biome.SemiArid, "Spring", Precipitation.Storm), "Rain" }, { (Biome.Desert, "Spring", Precipitation.Dry), "Clear" }, { (Biome.Desert, "Spring", Precipitation.LightWet), "Clear" }, { (Biome.Desert, "Spring", Precipitation.HeavyWet), "Rain" }, { (Biome.Desert, "Spring", Precipitation.Storm), "Sandstorm" }, { (Biome.Savanna, "Spring", Precipitation.Dry), "Clear" }, { (Biome.Savanna, "Spring", Precipitation.LightWet), "Clear" }, { (Biome.Savanna, "Spring", Precipitation.HeavyWet), "Rain" }, { (Biome.Savanna, "Spring", Precipitation.Storm), "Storm" }, { (Biome.Jungle, "Spring", Precipitation.Dry), "Clear" }, { (Biome.Jungle, "Spring", Precipitation.LightWet), "Rain" }, { (Biome.Jungle, "Spring", Precipitation.HeavyWet), "Rain" }, { (Biome.Jungle, "Spring", Precipitation.Storm), "Storm" }, // Autumn { (Biome.Tundra, "Autumn", Precipitation.Dry), "Clear" }, { (Biome.Tundra, "Autumn", Precipitation.LightWet), "SnowfallLight" }, { (Biome.Tundra, "Autumn", Precipitation.HeavyWet), "SnowfallMedium" }, { (Biome.Tundra, "Autumn", Precipitation.Storm), "SnowfallHeavy" }, { (Biome.Taiga, "Autumn", Precipitation.Dry), "Clear" }, { (Biome.Taiga, "Autumn", Precipitation.LightWet), "Rain" }, { (Biome.Taiga, "Autumn", Precipitation.HeavyWet), "SnowfallLight" }, { (Biome.Taiga, "Autumn", Precipitation.Storm), "SnowfallHeavy" }, { (Biome.Temperate, "Autumn", Precipitation.Dry), "Clear" }, { (Biome.Temperate, "Autumn", Precipitation.LightWet), "Rain" }, { (Biome.Temperate, "Autumn", Precipitation.HeavyWet), "Storm" }, { (Biome.Temperate, "Autumn", Precipitation.Storm), "SnowfallMedium" }, { (Biome.Sea, "Autumn", Precipitation.Dry), "Clear" }, { (Biome.Sea, "Autumn", Precipitation.LightWet), "Rain" }, { (Biome.Sea, "Autumn", Precipitation.HeavyWet), "Rain" }, { (Biome.Sea, "Autumn", Precipitation.Storm), "Storm" }, { (Biome.SemiArid, "Autumn", Precipitation.Dry), "Clear" }, { (Biome.SemiArid, "Autumn", Precipitation.LightWet), "Clear" }, { (Biome.SemiArid, "Autumn", Precipitation.HeavyWet), "Rain" }, { (Biome.SemiArid, "Autumn", Precipitation.Storm), "Rain" }, { (Biome.Desert, "Autumn", Precipitation.Dry), "Clear" }, { (Biome.Desert, "Autumn", Precipitation.LightWet), "Clear" }, { (Biome.Desert, "Autumn", Precipitation.HeavyWet), "Rain" }, { (Biome.Desert, "Autumn", Precipitation.Storm), "Sandstorm" }, { (Biome.Savanna, "Autumn", Precipitation.Dry), "Clear" }, { (Biome.Savanna, "Autumn", Precipitation.LightWet), "Clear" }, { (Biome.Savanna, "Autumn", Precipitation.HeavyWet), "Rain" }, { (Biome.Savanna, "Autumn", Precipitation.Storm), "Storm" }, { (Biome.Jungle, "Autumn", Precipitation.Dry), "Clear" }, { (Biome.Jungle, "Autumn", Precipitation.LightWet), "Rain" }, { (Biome.Jungle, "Autumn", Precipitation.HeavyWet), "Rain" }, { (Biome.Jungle, "Autumn", Precipitation.Storm), "Storm" }, // Winter { (Biome.Tundra, "Winter", Precipitation.Dry), "Clear" }, { (Biome.Tundra, "Winter", Precipitation.LightWet), "SnowfallMedium" }, { (Biome.Tundra, "Winter", Precipitation.HeavyWet), "SnowfallHeavy" }, { (Biome.Tundra, "Winter", Precipitation.Storm), "SnowfallHeavy" }, { (Biome.Taiga, "Winter", Precipitation.Dry), "Clear" }, { (Biome.Taiga, "Winter", Precipitation.LightWet), "SnowfallLight" }, { (Biome.Taiga, "Winter", Precipitation.HeavyWet), "SnowfallHeavy" }, { (Biome.Taiga, "Winter", Precipitation.Storm), "SnowfallHeavy" }, { (Biome.Temperate, "Winter", Precipitation.Dry), "Clear" }, { (Biome.Temperate, "Winter", Precipitation.LightWet), "SnowfallLight" }, { (Biome.Temperate, "Winter", Precipitation.HeavyWet), "SnowfallMedium" }, { (Biome.Temperate, "Winter", Precipitation.Storm), "SnowfallHeavy" }, { (Biome.Sea, "Winter", Precipitation.Dry), "Clear" }, { (Biome.Sea, "Winter", Precipitation.LightWet), "Rain" }, { (Biome.Sea, "Winter", Precipitation.HeavyWet), "Storm" }, { (Biome.Sea, "Winter", Precipitation.Storm), "Storm" }, { (Biome.SemiArid, "Winter", Precipitation.Dry), "Clear" }, { (Biome.SemiArid, "Winter", Precipitation.LightWet), "Rain" }, { (Biome.SemiArid, "Winter", Precipitation.HeavyWet), "Rain" }, { (Biome.SemiArid, "Winter", Precipitation.Storm), "Storm" }, { (Biome.Desert, "Winter", Precipitation.Dry), "Clear" }, { (Biome.Desert, "Winter", Precipitation.LightWet), "Clear" }, { (Biome.Desert, "Winter", Precipitation.HeavyWet), "Rain" }, { (Biome.Desert, "Winter", Precipitation.Storm), "Rain" }, { (Biome.Savanna, "Winter", Precipitation.Dry), "Clear" }, { (Biome.Savanna, "Winter", Precipitation.LightWet), "Rain" }, { (Biome.Savanna, "Winter", Precipitation.HeavyWet), "Storm" }, { (Biome.Savanna, "Winter", Precipitation.Storm), "Storm" }, { (Biome.Jungle, "Winter", Precipitation.Dry), "Clear" }, { (Biome.Jungle, "Winter", Precipitation.LightWet), "Rain" }, { (Biome.Jungle, "Winter", Precipitation.HeavyWet), "Storm" }, { (Biome.Jungle, "Winter", Precipitation.Storm), "Storm" }, }; /// /// Initializes the system and subscribes to relevant events. /// public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnMapInit); } /// /// Handles the initialization of weather for a map when it is first created. /// private void OnMapInit(EntityUid uid, WeatherNomadsComponent component, MapInitEvent args) { component.CurrentPrecipitation = Precipitation.Dry; component.CurrentWeather = "Clear"; component.NextSwitchTime = _timing.CurTime + TimeSpan.FromMinutes(GetRandomPrecipitationDuration(component)); component.NextSeasonChange = _timing.CurTime + TimeSpan.FromMinutes(GetRandomSeasonDuration(component)); Dirty(uid, component); UpdateTileWeathers(uid, component); _chat.DispatchGlobalAnnouncement($"Current season: {component.CurrentSeason}", "World", false, null, null); } /// /// Updates the weather system periodically, switching precipitation and season states as needed. /// public override void Update(float frameTime) { base.Update(frameTime); var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var nomads)) { // Handle season changes if (_timing.CurTime >= nomads.NextSeasonChange) { var oldSeason = nomads.CurrentSeason; nomads.CurrentSeason = GetNextSeason(nomads.CurrentSeason); nomads.NextSeasonChange = _timing.CurTime + TimeSpan.FromMinutes(GetRandomSeasonDuration(nomads)); Dirty(uid, nomads); _chat.DispatchGlobalAnnouncement($"Changed season to {nomads.CurrentSeason}", null, false, null, null); UpdateTileWeathers(uid, nomads); } // Handle precipitation changes if (_timing.CurTime < nomads.NextSwitchTime) { continue; } var oldPrecipitation = nomads.CurrentPrecipitation; nomads.CurrentPrecipitation = GetNextPrecipitation(nomads.CurrentPrecipitation); nomads.NextSwitchTime = _timing.CurTime + TimeSpan.FromMinutes(GetRandomPrecipitationDuration(nomads)); Dirty(uid, nomads); UpdateTileWeathers(uid, nomads); } } /// /// Updates weather effects for each tile based on biome, season, and global precipitation. /// private void UpdateTileWeathers(EntityUid uid, WeatherNomadsComponent nomads) { var mapId = Transform(uid).MapID; var gridUid = GetGridUidForMap(mapId); if (gridUid == null) { Log.Warning($"No grid found for map {mapId}"); return; } if (!TryComp(gridUid.Value, out var grid)) return; if (!TryComp(gridUid.Value, out var gridAtmosphere)) return; RoofComponent? roofComp = TryComp(gridUid.Value, out var rc) ? rc : null; foreach (var tile in gridAtmosphere.Tiles.Values) { var tileRef = grid.GetTileRef(tile.GridIndices); if (tileRef.Tile.IsEmpty) continue; // Skip empty tiles // Get biome from tile definition var tileDef = (ContentTileDefinition)_tileDefManager[tileRef.Tile.TypeId]; if (!Enum.TryParse(tileDef.Biome, true, out var biome)) { biome = Biome.Temperate; // Fallback to Temperate if biome string is invalid Log.Warning($"Invalid biome '{tileDef.Biome}' for tile at {tileRef.GridIndices}, defaulting to Temperate"); } if (_weatherTransitionMap.TryGetValue((biome, nomads.CurrentSeason, nomads.CurrentPrecipitation), out var weatherType)) { ApplyWeatherToTile(uid, nomads, gridUid.Value, tileRef, weatherType, grid, gridAtmosphere, roofComp); } else { Log.Warning($"No weather mapping found for Biome: {biome}, Season: {nomads.CurrentSeason}, Precipitation: {nomads.CurrentPrecipitation}"); } } } /// /// Applies weather effects and temperature to a specific tile. /// private void ApplyWeatherToTile(EntityUid weatherUid, WeatherNomadsComponent nomads, EntityUid gridUid, TileRef tileRef, string weatherType, MapGridComponent grid, GridAtmosphereComponent gridAtmosphere, RoofComponent? roofComp) { if (!CanWeatherAffect(gridUid, grid, tileRef, roofComp)) return; var tile = gridAtmosphere.Tiles[tileRef.GridIndices]; if (tile.Air == null) return; if (!_weatherTypes.TryGetValue(weatherType, out var weatherData)) { Log.Warning($"Weather type {weatherType} not found in _weatherTypes"); return; } // Update CurrentWeather if it has changed if (nomads.CurrentWeather != weatherType) { nomads.CurrentWeather = weatherType; Dirty(weatherUid, nomads); } // Apply weather visuals globally var mapId = Transform(gridUid).MapID; if (!string.IsNullOrEmpty(weatherData.PrototypeId) && _prototypeManager.TryIndex(weatherData.PrototypeId, out var proto)) { _weatherSystem.SetWeather(mapId, proto, null); } else { _weatherSystem.SetWeather(mapId, null, null); } // Adjust temperature var temperature = (float)(weatherData.MinTemperature + (weatherData.MaxTemperature - weatherData.MinTemperature) * Random.Shared.NextDouble()); var air = tile.Air; if (air.Immutable) { var newAir = new GasMixture(); newAir.CopyFrom(air); air = newAir; } air.Temperature = temperature; } /// /// Gets the next precipitation state in the cycle. /// private Precipitation GetNextPrecipitation(Precipitation current) { return current switch { Precipitation.Dry => Precipitation.LightWet, Precipitation.LightWet => Precipitation.HeavyWet, Precipitation.HeavyWet => Precipitation.Storm, Precipitation.Storm => Precipitation.Dry, _ => Precipitation.Dry // Default to Dry if something goes wrong }; } /// /// Generates a random duration for a season based on component settings. /// private double GetRandomSeasonDuration(WeatherNomadsComponent component) { var duration = Random.Shared.Next(component.MinSeasonMinutes, component.MaxSeasonMinutes + 1); return duration; } /// /// Generates a random duration for a precipitation change based on component settings. /// private double GetRandomPrecipitationDuration(WeatherNomadsComponent component) { var duration = Random.Shared.Next(component.MinPrecipitationDurationMinutes, component.MaxPrecipitationDurationMinutes + 1); return duration; } /// /// Determines if weather can affect a specific tile, based on roof coverage, tile type, and blocking entities. /// private bool CanWeatherAffect(EntityUid gridUid, MapGridComponent grid, TileRef tileRef, RoofComponent? roofComp) { if (tileRef.Tile.IsEmpty) return true; if (roofComp != null && _roofSystem.IsRooved((gridUid, grid, roofComp), tileRef.GridIndices)) return false; var tileDef = (ContentTileDefinition)_tileDefManager[tileRef.Tile.TypeId]; if (!tileDef.Weather) return false; var anchoredEntities = _mapSystem.GetAnchoredEntitiesEnumerator(gridUid, grid, tileRef.GridIndices); while (anchoredEntities.MoveNext(out var ent)) { if (HasComp(ent.Value)) return false; } return true; } /// /// Retrieves the EntityUid of the grid associated with a given map ID. /// Assumes one grid per map for simplicity. /// private EntityUid? GetGridUidForMap(MapId mapId) { var grids = _mapManager.GetAllMapGrids(mapId); return grids.Any() ? grids.First().Owner : null; } /// /// Gets the next season in the cycle. /// private string GetNextSeason(string current) { return current switch { "Spring" => "Summer", "Summer" => "Autumn", "Autumn" => "Winter", "Winter" => "Spring", _ => "Spring" // Default to Spring if something goes wrong }; } }