Requisites

Map editing tools, such as:

At the moment of writing the Ozonex editor doesn't support weather markers. You'll need the GPG editor to get this all to work.

Weather

Weather generators are rare, if not at all, used by maps in the vault. A lot of the original maps were supposed to use them but the code to generate the weather is all commented out. In turn, maps generally have no weather at all.

weather-stratus.png
Weather on the map Rainmakers Survival

Generating weather is fairly easy but you need to know the exact steps. The GPG editor allows the creation of the proper markers. Creating and customizing the weather involves three steps: placing the proper markers in the GPG editor, editing the script file of the map and then editing those markers inside the save file through a text editor.

GPG editor

Open up your map and open up the marker tools (F6). From there on navigate completely to the right and at the end you'll find the markers:

  • Weather Definition
  • Weather Generator

Place one of both of them in the center of the map. Then save the map and close the GPG editor - this prevents you from accidentally overriding the next step. You can have multiple generators, but for the sake of the article, I'll assume that you only have one.

You can view and edit the properties of a marker inside the editor. Select a marker and extent the 'Marker Editor' window vertically. This will reveal all the properties of, for example, a Weather Definition marker.

The weather can not be rendered and inspected inside the editor. You'll need to open up the map in Forged Alliance in order to inspect it. Constantly copying your map from the editor folder to the game folder is a bit of a pickle - therefore we use a text editor to edit the properties. This allows you to iterate more quickly when you restart your map to inspect the results.

Script file

Open up the _script.lua file from your map in your favorite text editor. A typical script file from a non-adaptive map looks like this:

local ScenarioUtils = import('/lua/sim/ScenarioUtilities.lua')
local ScenarioFramework = import('/lua/ScenarioFramework.lua')

function OnPopulate()
    ScenarioUtils.InitializeArmies()

    -- optional, only if applicable
    ScenarioFramework.SetPlayableArea('AREA_1' , false)
end

function OnStart(scenario)
end

We want to add in the weather thread so that clouds are slowly generated over time. Change it to:

local ScenarioUtils = import('/lua/sim/ScenarioUtilities.lua')
local ScenarioFramework = import('/lua/ScenarioFramework.lua')

-- for weather generation
local Weather = import('/lua/weather.lua')

function OnPopulate()
    ScenarioUtils.InitializeArmies()

    -- optional, only if applicable
    ScenarioFramework.SetPlayableArea('AREA_1' , false)
end

function OnStart(scenario)
    -- for weather generation
    Weather.CreateWeather()
end

With the current weather definition and generator clouds should already be generating. Start the game, disable fog of war and make sure that it is. Then it is time to start tweaking!

If you have no vision over a weather generator marker then you will not see the clouds that are being generated.

Save file

Open up the _save.lua file from your map in your favorite text editor. Any editor will suffice, as long as it has a search function.

Weather definition

Search the file (CTRL + F) for 'Weather Definition'. You'll find in the markers chain a marker that looks like the following:

['Weather Definition 00'] = {
    ['WeatherType03Chance'] = FLOAT( 0.300000 ),
    ['color'] = STRING( 'FF808000' ),
    ['WeatherType01'] = STRING( 'SnowClouds' ),
    ['WeatherType02'] = STRING( 'WhiteThickClouds' ),
    ['WeatherType04'] = STRING( 'None' ),
    ['WeatherDriftDirection'] = VECTOR3( 1, 0, 0 ),
    ['WeatherType01Chance'] = FLOAT( 0.300000 ),
    ['WeatherType03'] = STRING( 'WhitePatchyClouds' ),
    ['WeatherType04Chance'] = FLOAT( 0.100000 ),
    ['MapStyle'] = STRING( 'Tundra' ),
    ['WeatherType02Chance'] = FLOAT( 0.300000 ),
    ['hint'] = BOOLEAN( true ),
    ['type'] = STRING( 'Weather Definition' ),
    ['prop'] = STRING( '/env/common/props/markers/M_Defensive_prop.bp' ),
    ['orientation'] = VECTOR3( 0, -0, 0 ),
    ['position'] = VECTOR3( 522.5, 68.8418, 541.5 ),
},

These values are scrambled - they can appear in any order. The important bits are:

  • WeatherType01 / 02 / 03 / 04: The type of weather that will be generated by all weather generators.
  • WeatherChance01 / 02 / 03 / 04: The chance that the given type will be generated.
  • MapStyle: Determines in what table the weather types will be searched for, such as 'Desert' or 'Evergreen'.
  • Weather Drift Direction: This value is not referenced in the code and is therefore not used. The direction that the clouds slowly move towards. You typically want this to match the direction of your water. With the standard camera the positive x-axis points to the right of a map, the positive y-axis points to the sky and the positive z-axis points the bottom of a map.

All values are case sensitive. As an example, it is 'Dsert' and not 'desert'.

Values that are not used by the code but by the editor:

  • prop, orientation, color, hint

Each theme has its own list of weather that can share names but look different. Therefore choosing the right map style is important. We can find all the available weather as comments in one of the lua files of the base game:

Map Style Types:
	Desert
	Evergreen
	Geothermal
	Lava
	RedRock
	Tropical
	Tundra
	
Style Weather Types: 
	Desert
		LightStratus -
	Evergreen
		CumulusClouds -
		StormClouds -
		RainClouds - WARNING, only use these a ForceType on a weather generator, max 2 per map
	Geothermal
	Lava
	RedRock
		LightStratus -
	Tropical
		LightStratus -
	Tundra
		WhitePatchyClouds - 
		SnowClouds - WARNING, only use these a ForceType on a weather generator, max 2 per map
	
	All Styles:
		Notes: ( Cirrus style cloud emitters, should be used on a ForceType Weather Generator, placed )
				( in the center of a map. Take note that the these are sized specific for map scale    )
		CirrusSparse256 - 
		CirrusMedium256 - 
		CirrusHeavy256 - 
		CirrusSparse512 -
		CirrusMedium512 - 
		CirrusHeavy512 - 
		CirrusSparse1024 - 
		CirrusMedium1024 - 
		CirrusHeavy1024 - 
		CirrusSparse4096 - 
		CirrusMedium4096 - 
		CirrusHeavy4096 - 

This defines all the weather that is available. The RainClouds and SnowClouds generate rain and snow respectively - these should be used sparsely. An unlisted entry is 'None', which indicates that the no weather may be generated at all.

Weather generator

Search the file (CTRL + F) for 'Weather Generator'. You'll find in the markers chain a marker that looks like the following:

['Weather Generator 00'] = {
    ['cloudCountRange'] = FLOAT( 0.000000 ),
    ['color'] = STRING( 'FF808000' ),
    ['ForceType'] = STRING( 'None' ),
    ['cloudHeightRange'] = FLOAT( 15.000000 ),
    ['cloudSpread'] = FLOAT( 150.000000 ),
    ['spawnChance'] = FLOAT( 1.000000 ),
    ['cloudEmitterScaleRange'] = FLOAT( 0.000000 ),
    ['cloudEmitterScale'] = FLOAT( 1.000000 ),
    ['cloudCount'] = FLOAT( 10.000000 ),
    ['cloudHeight'] = FLOAT( 180.000000 ),
    ['hint'] = BOOLEAN( true ),
    ['type'] = STRING( 'Weather Generator' ),
    ['prop'] = STRING( '/env/common/props/markers/M_Defensive_prop.bp' ),
    ['orientation'] = VECTOR3( 0, -0, 0 ),
    ['position'] = VECTOR3( 518.5, 68.6699, 542.5 ),
},

These values are scrambled - they can appear in any order. The important bits are:

  • ForceType: Forces the weather on this generator to the given type. It can only be forced into emitters that are part of the map style chosen in the weather definition marker. Is 'None' by default: this allows the weather definition marker to determine its type. As an example, given that the MapStyle is Evergreen we can set it to CumulusClouds, StormClouds or RainClouds.
  • cloudSpread: How spread out the clouds are.
  • spawnChance: Determines whether or not it can have phases where there are no clouds generated at all.
  • cloudEmitterScale: The absolute scale of all clouds.
  • cloudEmitterScaleRange: A range value that randomizes the absolute scale.
  • cloudCount: The absolute number of clouds available at once.
  • cloudCountRange: A range value that randomizes the absolute number.
  • cloudHeight: The absolute height at which the clouds can spawn, starting from the position of the marker.
  • cloudHeightRange: A range value that randomizes the absolute height.

As an example on the range variables: if the absolute value is 90 and the range is 15 then the final value is randomly chosen from the range [75,105].

All values are case sensitive. As an example, it is 'None' and not 'none'.

Values that are not used by the code but by the editor:

  • prop, orientation, color, hint

Under the hood

It is always good to dive into the code and discover how values are used in computations. This will bring in more perspective as to what is possible and leaves out speculation on what you feel it is doing.

Let's start off with reading out the markers - turning the concrete data into a more abstract form that allows us to construct the weather.

function GetWeatherMarkerData(MapScale)
  local markers = ScenarioUtils.GetMarkers()
  local WeatherDefinition = {}
  local ClusterDataList = {}
  local defaultcloudclusterSpread = math.floor(((MapScale[1] + MapScale[2]) * 0.5) * 0.15)

  // Make a list of all the markers in the scenario that are of the markerType
  if markers then
    for k, v in markers do
      // Read in weather cluster positions and data
      if v.type == 'Weather Generator' then
        table.insert( ClusterDataList, { 
          clusterSpread = v.cloudSpread or defaultcloudclusterSpread, 
          cloudCount = v.cloudCount or 10, 
          cloudCountRange = v.cloudCountRange or 0,
          cloudHeight = v.cloudHeight or 180,
          cloudHeightRange = v.cloudHeightRange or 10,
          position = v.position,
          emitterScale = v.cloudEmitterScale or 1,
          emitterScaleRange = v.cloudEmitterScaleRange or 0,
          forceType = v.ForceType or "None",
          spawnChance = v.spawnChance or 1,
        } )
      // Read in weather definition
          elseif v.type == 'Weather Definition' then
      if table.getn( WeatherDefinition ) > 0 then
        LOG('WARNING: Weather, multiple weather definitions found. Last read Weather definition will override any previous ones.')
      end					                
      WeatherDefinition = {
        MapStyle = v.MapStyle or "None",
        WeatherTypes = {
          {
            Type = v.WeatherType01 or "None",
            Chance = v.WeatherType01Chance or 0.25,
          },
          {
            Type = v.WeatherType02 or "None",
            Chance = v.WeatherType02Chance or 0.25,
          },
          {
            Type = v.WeatherType03 or "None",
            Chance = v.WeatherType03Chance or 0.25,
          },
          {
            Type = v.WeatherType04 or "None",
            Chance = v.WeatherType04Chance or 0.25,
          },															
        },
        Direction = v.WeatherDriftDirection or {0,0,0},
      }
          end
      end
  end
  return WeatherDefinition,ClusterDataList
end

This shows us what data is relevant and transformed into another format. We can also see that there are a lot of sane defaults provided. And last but certainly not least: we can see what the new names for the data is that is used throughout the code.

function CreateWeatherThread()
  local MapScale = ScenarioInfo.size // x,z map scaling
  local WeatherDefinition, ClusterData = GetWeatherMarkerData(MapScale)
  local MapStyle = WeatherDefinition.MapStyle
  local WeatherEffectsType = GetRandomWeatherEffectType( WeatherDefinition )
  //WeatherEffectsType = 'StormClouds'
  if WeatherEffectsType == 'None' then
    return
  end	
  
  local numClusters = table.getn( ClusterData )
  if not WeatherDefinition.WeatherTypes and numClusters then
    LOG(' WARNING: Weather, no [Weather Definition] marker placed, with [Weather Generator] markers placed in map, aborting weather generation')
    return 
  end
    
  // If we have any clusters, then generate cluster list
  if numClusters != 0 then
    local notfoundMapStyle = true
    for k, v in MapStyleList do
      if MapStyle == v then
        SpawnWeatherAtClusterList( ClusterData, MapStyle, WeatherEffectsType )
        notfoundMapStyle = false
      end
    end
    
    if notfoundMapStyle and (MapStyle != 'None') then
      LOG(' WARNING: Weather Map style [' .. MapStyle .. '] not defined. Define this as one of the Map Style Definitions. ' .. repr(MapStyleList))
    end
  end
end

For each cluster, which is a generator, the data that has been transformed before is being used to call the function to start generating clouds on top of those generators.

The cloud types are directly determined from the given map style that is defined in the weather definition marker - only the cloud types form that style are available because that is where the code is searching for to find a matching type.

function SpawnWeatherAtClusterList( ClusterData, MapStyle, EffectType )
  local numClusters = table.getn( ClusterData )
  local WeatherEffects = MapWeatherList[MapStyle][EffectType]
  
  // Exit out early, if for some reason, we have no effects defined for this
  if (WeatherEffects == nil) or (WeatherEffects != nil and (table.getn(WeatherEffects) == 0)) then
    return	
  end
  
  // Parse through cluster position and datal
  for i = 1, numClusters do
    // Determine whether current cluster should spawn or not
    if ClusterData[i].spawnChance < 1 then
      local pick
      if util.GetRandomFloat( 0, 1 ) > ClusterData[i].spawnChance then
        LOG( 'Cluster ' .. i .. ' No clouds generated ' )
        continue
      end
    end
  
    local clusterSpreadHalfSize = ClusterData[i].clusterSpread * 0.5
    local numCloudsPerCluster = nil
    if ClusterData[i].cloudCountRange != 0 then
      numCloudsPerCluster = util.GetRandomInt(ClusterData[i].cloudCount - ClusterData[i].cloudCountRange / 2,ClusterData[i].cloudCount + ClusterData[i].cloudCountRange / 2)
    else
      numCloudsPerCluster = ClusterData[i].cloudCount
    end
    local clusterEffectMaxScale = ClusterData[i].emitterScale + ClusterData[i].emitterScaleRange
    local clusterEffectMinScale = ClusterData[i].emitterScale - ClusterData[i].emitterScaleRange
    
    // Calculate weather cluster entity positional range
    local LeftX = ClusterData[i].position[1] - clusterSpreadHalfSize
    local TopZ = ClusterData[i].position[3] - clusterSpreadHalfSize
    local RightX = ClusterData[i].position[1] + clusterSpreadHalfSize
    local BottomZ = ClusterData[i].position[3] + clusterSpreadHalfSize		
    
    // Get base height and height range
    local BaseHeight = ClusterData[i].position[2] + ClusterData[i].cloudHeight
    local HeightOffset = ClusterData[i].cloudHeightRange	
    
    // Choose weather cluster effects
    local clusterWeatherEffects = WeatherEffects
    local numEffects = table.getn(WeatherEffects) 
    if ClusterData[i].forceType != "None" then
      clusterWeatherEffects = MapWeatherList[MapStyle][ClusterData[i].forceType] 
      LOG( 'Force Effect Type: ', ClusterData[i].forceType )			
      numEffects = table.getn(clusterWeatherEffects) 
    end
    
    // Generate Clouds for our cluster
    for j = 0, numCloudsPerCluster do
      local cloud = Entity()
      local x = util.GetRandomInt( LeftX, RightX )
      local y = BaseHeight + util.GetRandomInt(-HeightOffset,HeightOffset)
      local z = util.GetRandomInt( TopZ, BottomZ )
      Warp( cloud, Vector(x,y,z) )	
      
      local EmitterGroupSeed = util.GetRandomInt(1,numEffects)
      local numEmitters = table.getn(clusterWeatherEffects[EmitterGroupSeed])
      local effects = clusterWeatherEffects[EmitterGroupSeed]
      
      for k, v in clusterWeatherEffects[EmitterGroupSeed] do
        CreateEmitterAtBone(cloud,-2,-1,v):ScaleEmitter(util.GetRandomFloat( clusterEffectMaxScale, clusterEffectMinScale ))					
      end
    end
  end
end

This is where the generators are turned into emitters. All the data is transformed (again) into the proper format that dictates how large the emitter can be, how much it should spawn and at what height it should spawn.

The type of a specific generator can be overridden when you force its type - this is determined when the type of emitter is chosen. Even for the forced type it only searches through the weather available in the selected map style for the forced type.

The emitters are attached to a (dummy) entity. If a player has no vision over this entity then the attached emitters do not spawn.

Frequently asked Questions (FAQ)

I don't see any clouds

Make sure that:

  • Your script file has been updated properly. You can check the logs (F9) - if there is a warning then you did something wrong. If all is good then you should see something similar somewhere in your log to:
INFO: Weather Definition {
INFO:   Direction={ 15, -30, 0, type="VECTOR3" },
INFO:   MapStyle="Tundra",
INFO:   WeatherTypes={
INFO:     { Chance=0.30000001192093, Type="WhitePatchyClouds" },
INFO:     { Chance=0.30000001192093, Type="WhitePatchyClouds" },
INFO:     { Chance=0.30000001192093, Type="None" },
INFO:     { Chance=0.10000000149012, Type="None" }
INFO:   }
INFO: }
INFO: Weather Effect Type: \000WhitePatchyClouds
  • You need to have at least one generate that has its 'ForceType' value to 'None'. Make sure that the word starts with a capital letter.
  • You need vision over the weather generator marker in order for it to generate. You can test this best by having no fog of war on your map.

About you

If you have interesting sources, approaches, opinions or ideas that aren't listed yet but may be valuable to the article: feel free to leave a message down below or contact me on Discord. The idea is to create a bunch of resources to share our knowledge surrounding various fields of development in Supreme Commander.

If you've used this resource for one of your maps feel free to make a post below: I'd love to know about it!