Requisites
Map editing tools, such as:
- Ozonex FaF Editor (https://github.com/ozonexo3/FAForeverMapEditor/releases)
or - GPG Editor (https://wiki.faforever.com/index.php?title=Map_Editing_Tools)
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 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!