# Worked example: the 2.5x Kerbolar System

The 2.5x Kerbolar System mod is Redux's whole galaxy, Drast and Beyl included, at 2.5 times the size. The mod ships its own campaign pack. A player selects this pack when the player starts a campaign. A campaign that does not have the features of the mod on keeps the normal Redux galaxy without changes.

The mod has three files: a `swinfo.json`, one Patch Manager Lua file and a localization CSV. The full source is on [GitHub](https://github.com/cheese3660/2.5x-Kerbolar-System). The excerpts on this page come from `KerbolarUpscale.lua` in version 0.6.0. The mod uses [campaign features](/doc/campaign-features-9YIewcIArR), patches and campaign packs together. Thus, it is a good example of how these items work together.

## The plan

* Declare two features: `Rescale` for the galaxy and `Antennas` for the relay ranges.
* In the `Rescale` feature, patch these items in place:
  * all bodies
  * the science regions and discoverables of each body
  * the orbits of the galaxy
  * the atmospheres and clouds
* In the `Antennas` feature, increase the range of all transmitters.
* Add a campaign pack that turns on all of Redux and the two features.

The mod does not copy assets. A campaign with the features on gets the Redux galaxy with the patches applied. A campaign without the features does not run the patches.

## Features

The mod declares its features in `swinfo.json`. Each feature requires the Redux feature that it changes:

```json
"features": {
    "Rescale": {
        "requires": ["Ksp2Redux:Galaxy"]
    },
    "Antennas": {
        "requires": ["Ksp2Redux:Parts"]
    }
}
```

The two features also require the default feature of the mod, `KerbolarUpscale:KerbolarUpscale`. This requirement is implicit. Because the mod has two features, a player can turn on one feature without the other. For example, a player can turn on the larger galaxy in the feature editor without the longer antenna ranges.

The Lua file starts with the constants that all patches use:

```lua
local PACK = "Kerbolar25x"
local RESCALE = 2.5
-- Atmospheres thicken more slowly than the bodies grow
local ATMOSPHERE_RESCALE = math.log((16.0 / 9.0) + ((2.0 * RESCALE) / 9.0)) / math.log(2.0)
```

## Bodies

The mod patches an explicit list of the Redux bodies. It does not use `"*"`. Thus, a body that a different mod adds to the galaxy keeps its own data. The list is the `RADII` table. This table also gives the original radius of each body. The discoverables patch uses this radius.

Each patch is inside `PM:WithFeature("Rescale", ...)` and runs `:Late()`. Thus, each patch sees the content that Redux adds, for example Drast, Beyl and the discoverables of Kerbin:

```lua
PM:WithFeature("Rescale", function()
    -- Kerbol and Jool have no discoverables, so those patches match nothing
    for bodyName in pairs(RADII) do
        local assetPrefix = string.lower(bodyName) .. "_science_regions"
        PM.Planets:Patch("Rescale" .. bodyName):Late():Named(bodyName):Do(rescaleBody)
        PM.Science:PatchRegions("Rescale" .. bodyName .. "Regions"):Late():Named(assetPrefix):Do(rescaleRegions)
        PM.Science:PatchDiscoverables("Rescale" .. bodyName .. "Discoverables")
            :Late()
            :Named(assetPrefix .. "_discoverables")
            :Do(rescaleDiscoverables)
    end
    -- ... the galaxy, atmosphere and cloud patches below sit in the same scope
end)
```

`rescaleBody` makes these changes to each body:

* It increases the radius.
* It scales the terrain to agree with the radius.
* It moves out all values that the game measures from the surface.

```lua
local function rescaleBody(body)
    local oldRadius = body.radius
    if body.radius ~= nil then
        body.radius = body.radius * RESCALE
    end
    -- Terrain grows with the body, so mountains keep their shape rather than flattening into a sphere
    body.TerrainHeightMultiplier = RESCALE
    if body.MinTerrainHeight ~= nil then
        body.MinTerrainHeight = body.MinTerrainHeight * RESCALE
    end
    if body.MaxTerrainHeight ~= nil then
        body.MaxTerrainHeight = body.MaxTerrainHeight * RESCALE
    end
    -- The terrain is built this far below sea level, so it scales with the terrain to keep the shorelines in place
    if body.oceanAltitude ~= nil then
        body.oceanAltitude = body.oceanAltitude * RESCALE
    end
    if body.atmosphereDepth ~= nil then
        body.atmosphereDepth = body.atmosphereDepth * ATMOSPHERE_RESCALE
    end
    if body.ForcedSphereOfInfluence ~= nil then
        body.ForcedSphereOfInfluence = body.ForcedSphereOfInfluence * RESCALE
    end
    -- ... inverseRotThresholdAltitude, StarLuminosity, surface objects and atmosphere curves follow
end
```

A few things are easy to miss:

* `TerrainHeightMultiplier`: Without it the mountains keep their stock height on a body 2.5 times bigger, and small bodies like Drast come out nearly round.
* `oceanAltitude`: The terrain is built this far below sea level. Left at the stock value under 2.5 times taller terrain, the seabed rises out of the ocean and most of it disappears.
* `ForcedSphereOfInfluence`: Bodies with a forced sphere of influence keep it unless you scale it yourself.
* **Objects placed on the surface,** such as the space center's sim objects in `LocalSimObjectsData`, are positions from the body's center. Move them out by the change in radius and keep their height above sea level. Scaling the whole position by 2.5 also multiplies their altitude, and the space center campus, which cannot grow, ends up hundreds of meters above its pad.
* `StarLuminosity`: It scales with the square of the distance, so Kerbol's goes up by `RESCALE * RESCALE` to keep the planets as warm as before.
* **Atmosphere curves** use altitude as the key. Thus, the patch multiplies their key times by `ATMOSPHERE_RESCALE`.

## Science

Region altitudes and discoverable positions are in meters. Thus, the same loop patches both of them. The regions patch multiplies the situation altitudes:

```lua
local function rescaleRegions(regions)
    local situations = regions.SituationData
    if situations == nil then
        return
    end

    situations.HighOrbitMaxAltitude = situations.HighOrbitMaxAltitude * RESCALE
    situations.LowOrbitMaxAltutude = situations.LowOrbitMaxAltutude * ATMOSPHERE_RESCALE
    situations.AtmosphereMaxAltutude = situations.AtmosphereMaxAltutude * ATMOSPHERE_RESCALE
end
```

Each discoverable moves out along its own direction by the change in radius. Thus, it keeps its height above the surface. This calculation uses the original radius of each body from `RADII`. The detection radius of each discoverable is multiplied by `RESCALE`.

## The galaxy

A patch in the same `Rescale` scope increases the orbits. It patches the Redux galaxy in place:

```lua
PM.Planets:PatchDefaultGalaxy("RescaleOrbits"):Late():Do(function(galaxy)
    for _, entry in ipairs(galaxy.CelestialBodies) do
        local orbit = entry.OrbitProperties
        if orbit ~= nil and orbit.semiMajorAxis ~= nil then
            orbit.semiMajorAxis = orbit.semiMajorAxis * RESCALE
        end
    end
end)
```

The mod does not add a second galaxy. The pack uses the Redux `GalaxyDefinition_Default`. This galaxy is the default value of the `Galaxy` field of a pack. The `Rescale` feature controls if the galaxy is rescaled.

## Atmospheres and clouds

These patches are also in the `Rescale` scope. Each override that the mod creates or patches applies only in campaigns that have the feature on. Each of the five bodies with an atmosphere gets a new atmosphere override. The `ATMOSPHERES` table gives the dimensions:

```lua
for bodyName, atmosphere in pairs(ATMOSPHERES) do
    PM.Planets:CreateAtmosphereOverride(bodyName, function(atmosphereOverride)
        atmosphereOverride.BottomRadius = atmosphere.radius * RESCALE
        atmosphereOverride.AtmosphereHeight = atmosphere.depth * ATMOSPHERE_RESCALE
        atmosphereOverride.AbsorptionHeightMinMax = {
            x = atmosphere.absorptionMin * ATMOSPHERE_RESCALE,
            y = atmosphere.absorptionMax * ATMOSPHERE_RESCALE,
        }
    end)
end
```

Redux already has tuned cloud overrides for Kerbin, Eve and Jool. The mod patches these overrides in place and moves the clouds up:

```lua
for _, bodyName in ipairs({ "kerbin", "eve", "jool" }) do
    PM.Planets:PatchCloudOverride("RescaleClouds" .. bodyName)
        :Late()
        :Named("volume_cloud_override_" .. bodyName)
        :Do(function(cloudOverride)
            cloudOverride.planetRadius = CLOUD_PLANET_RADII[bodyName] * RESCALE
            for _, cloudLayer in pairs(cloudOverride.cumulusList) do
                scaleCloudLayer(cloudLayer)
            end
        end)
end
```

Laythe and Duna have stock clouds. The mod creates new cloud overrides for them with `PM.Planets:CreateCloudOverride`. The layers of these overrides append the stock heights multiplied by `ATMOSPHERE_RESCALE`.

## Antennas

The antenna patch is in its own feature. Thus, the antenna patch can be on while the rescale is off, or off while the rescale is on:

```lua
-- Antennas reach 2.5 times as far, so relays cover the wider orbits
PM:WithFeature("Antennas", function()
    PM.Parts:Patch("RescaleAntennas")
        :Late()
        :Has("Module_DataTransmitter")
        :Do(function(part)
            local transmitter = part.Module_DataTransmitter.Data_Transmitter
            transmitter.CommunicationRange = transmitter.CommunicationRange * RESCALE
        end)
end)
```

The game does not save antenna range with a vessel. Thus, craft in a 2.5x campaign get the longer range when they load.

## The campaign pack

The pack turns on all of Redux and the two features of the mod. The pack does not list the default feature of the mod, because the two features require it:

```lua
-- The campaign pack: Redux's whole game, with its galaxy rescaled and the antennas stretched to match
PM.CampaignPacks:CreateCampaignPack(PACK, function(campaignPack)
    campaignPack.Features = { "Ksp2Redux:All", "KerbolarUpscale:Rescale", "KerbolarUpscale:Antennas" }
    campaignPack.CampaignPackDescriptionLocalizationKey = "CampaignPacks/Description/" .. PACK
end)
```

The pack is the only item that the mod creates outside its features. The game shows campaign packs in all feature sets. Thus, the pack shows on the new campaign menu when the mod is installed.

## Names

The pack's card reads its text from localization keys, shipped in `localizations/kerbolar25x_loc.csv` next to the Lua file:

```text
Key,Type,Desc,English
CampaignPacks/Kerbolar25x,Text,,2.5x Kerbolar System
CampaignPacks/Description/Kerbolar25x,Text,,"The complete Redux experience in a Kerbolar System 2.5 times the size"
```

The mod does not ship names for its features. Thus, the feature editor shows their ids, `KerbolarUpscale:Rescale` and `KerbolarUpscale:Antennas`. To give the features names, add `Feature/KerbolarUpscale/Rescale/Name` and `Feature/KerbolarUpscale/Rescale/Description` rows to the same CSV. See [Naming features for players](/doc/campaign-features-9YIewcIArR).

## Checking it worked

Start a campaign from the pack. Then examine `pm_summary.log`:

* The log has a section for the feature set of that campaign. The heading of the section lists its features.
* This section shows all rescale patches as applied.
* The section for a normal Redux campaign lists the same patches under **Removed Patches** with the status `FEATURE`.

In game, do these checks:

* From orbit, make sure that the radius and terrain of Kerbin are correct.
* In the VAB, compare the range of an antenna with the range in a normal Redux campaign.