The overhaul mod this workspace is building: a crossover of Against the Storm and Surviving Mars, for the base game. These pages are the mod's design and current state: most features are built (untested in-game), a few still planned - each feature's Status says which.
Three pillars: randomize (each run is unique), rebalance (no single best path), escalate (harder over time). Each feature has a status (Planned, Confirmed with the designer, Built untested, Implemented) and the base-game code that identifies the target.
Base-game features the mod removes entirely: the "No ..." list. The Status column tracks each feature from design to in-game test (Planned, Confirmed, Built untested, Tested). The Base-game code column names where the feature lives; the Implementation column records how the mod removes it.
| Feature | Status | What it does | Base-game code | Implementation |
|---|---|---|---|---|
| No asteroids | Tested | Removes the Below and Beyond asteroid expeditions (mining temporary asteroid maps by rocket). | Below and Beyond (whole DLC, picard.hpk) | Below and Beyond stays loaded (its darkness overlay powers "Map black at start"), so the content is switched off in code instead of by disabling the DLC. Root gate: IsDlcAccessible("picard") reads the shared g_AccessibleDlc table (Lua/init.lua:29-30); setting g_AccessibleDlc["picard"] = false makes it return false for the whole session, so every runtime Below-and-Beyond check hides itself - the Exotic Minerals resource, the Asteroid Lander rocket, planetary and map-switch UI, and the tech/building preset filters. (Overriding the IsDlcAccessible function from a mod does nothing: mod Lua is sandboxed, only shared-table writes propagate.) The research field and build-menu buildings are stripped explicitly too as a belt. In Code/StS_NoBelowAndBeyond.lua. |
| No underground | Tested | Removes the Below and Beyond underground map layer (caves and deep deposits explored with rovers). | Below and Beyond (whole DLC, picard.hpk) | Same g_AccessibleDlc["picard"] = false root gate as No asteroids, so the underground techs, buildings and map-switch UI all disappear. As a guarantee Colony:UnlockUnderground() is also neutralised (Lua/Cheats.lua:298), so the underground map can never open. Same Code/StS_NoBelowAndBeyond.lua. |
| No Game Rules selector | Tested (v16) | Removes the Game Rules row from the new-game setup screen, so the player never picks game rules. Inflation is applied separately as an always-on custom rule, not a selectable game rule (see the Inflation always on feature). | GetMissionParamUICategories (Lua/X/XPGMission.lua:172-190), consumed at Lua/XTemplates/PGMissionSponsor.lua:131; the row is one key of the MissionParams table (Lua/PreGameMission.lua:47-54) | Wrap GetMissionParamUICategories at ClassesBuilt and drop the entry whose id is idGameRules. Additive: MissionParams.idGameRules and all game-rule machinery stay intact (nil'ing it would crash the game-rule code), only the row is hidden. The table entry is not nil'd out, which would crash ReloadGameRules (Lua/GameRules.lua:1-7); CalcChallengeRating is already guarded for a missing param (Lua/PreGameMission.lua:582-584). In Code/StS_StartupMenu.lua. |
| No starting hints | Tested (2.106); New Anomalies drop built untested | Removes the base-game onboarding that appears as a run begins, so the mod's own opening notifications (sponsor pick, commander pick, map conditions, General Rules, pre-founder event) are not buried. Four things go: the beginner hints (Welcome to Mars, Rocket, Probes, and the first-landing batch - Building Construction, Camera Controls, Universal Depot, Sensor Tower, Resupply, Priority, Game Speed, Research Available); the automatic "Sector scanned" card for the one or two sectors scanned at the start; and the Green Planet terraforming start cards - the Terraforming Introduction, plus, with a terraforming-focused sponsor, the parameter and building cards (Atmosphere, Temperature, Vegetation, Water, Seeds, Buildings, Special Projects) that its start-granted techs raise during setup; and the "New Anomalies" card for the anomalies the start scan uncovers. This is the start-only set: later situational hints (Dust Storm, Cold Wave, Tourists, Renegades, Earthsick), the build-something-first suggestions, and any terraforming card or anomaly the player reveals later in the run all still appear. In Code/StS_NoStartVanillaHints.lua. | Hints show through HintTrigger(id) (Lua/Hints.lua:13); the opening set is HintGameStart (Lua/Hints.lua:237), HintRocket (Lua/Buildings/RocketBase.lua:1361), HintProbes (Lua/OrbitalProbe.lua:143) and the first-landing batch gated by first_arrival (Lua/Buildings/RocketBase.lua:477-488). The cards are added via AddOnScreenNotification (Lua/UI/OnScreenNotification.lua:650): the scan card at Lua/Exploration.lua:119 for the starting sectors scanned in Exploration:InitialExplore (Lua/Exploration.lua:983-1000); the terraforming cards with preset group Terraforming (armstrong/Presets/OnScreenNotificationPreset.lua), the intro on CityStart and the parameter/building cards on TechResearched (armstrong/Code/Terraforming.lua, Vegetation.lua, Research.lua). The "New Anomalies" card is raised by HandleNewObjsNotif(g_RecentlyRevAnomalies, "NewAnomalies", ...), a 1s game-time batch loop (Lua/Buildings/Anomaly.lua:427, Lua/UI/OnScreenNotification.lua:525). | Two mechanisms. The eleven opening hints are pre-seeded as disabled: HintTrigger early-returns when g_ActiveHints[id] already exists (Lua/Hints.lua:17) and the game's own HintDisable(id) sets that entry disabled (Lua/Hints.lua:116), so at PostNewGame and CityStart, before any hint fires, the mod calls HintDisable on exactly those eleven and every later trigger is a no-op while all other hints keep working. The two card types are dropped before they are ever created: the mod wraps AddOnScreenNotification (the same global Green Planet itself overrides, armstrong/Code/OnScreenNotification.lua) and, while game time is 100 or less, returns without adding the card for id SectorScanned and for any card whose preset group is Terraforming. The 100-or-less window is the game's own marker for the starting sectors (it withholds per-sector research below that, Lua/Exploration.lua:122); a terraforming tech cannot be researched that early, so only the start batch is caught. The "New Anomalies" card batches a game-second after the setup scan (past that window, and the game may be paused there), so NewAnomalies is dropped for the whole first game-hour instead (GameTime() at most const.HourDuration); the player cannot reveal a new anomaly by scanning that early, so only the start card is caught. Every scan, terraforming card or anomaly later in the run notifies normally. In Code/StS_NoStartVanillaHints.lua. |
| No Extractor AI breakthrough | Tested (v2.218) | Removes the Extractor AI breakthrough entirely, so it never enters a game's breakthrough pool and the player can never find it. Extractor AI let Metals and Rare Metals Extractors run with no crew at 50 performance, which is overpowered for this mod. Its full effect is on the vanilla Breakthroughs page. In StS as Code/StS_BanExtractorAI.lua. | Data/TechPreset.lua ExtractorAI (SortKey 47); the breakthrough pool is built from Presets.TechPreset.Breakthroughs filtered by Colony:TechAvailableCondition (Lua/Buildings/Anomaly.lua:583-598), which ANDs tech:condition() (Lua/Research.lua:180-183) | On ClassesBuilt, set the ExtractorAI TechPreset's condition to return false - the same technique StS_NoBelowAndBeyond.lua (RemovePicardTechs) uses to strip Below-and-Beyond techs. A breakthrough enters the seeding pool only when tech:condition() is true, so it is dropped from both the subsurface-anomaly path (City:InitBreakThroughAnomalies) and the planetary / reward path (Colony:GetUnregisteredBreakthroughs). |
Base game (Steam app 464920) plus the Space Race (gagarin.hpk) and
Below and Beyond (picard.hpk) DLC, decompiled (method:
docs/reading-surviving-mars-code.md).
Asteroid and underground content is switched off while
Below and Beyond stays loaded (its darkness overlay powers "Map black at start"):
IsDlcAccessible("picard") reads the shared g_AccessibleDlc
table (Lua/init.lua:29-30), so setting g_AccessibleDlc["picard"] = false
turns off every runtime picard check at once (resource, rocket, UI, tech and building
filters). The mod requires every DLC and warns at load if any is missing
(Code/StS_RequireDlc.lua). See also
docs/rival-tech-exchange.md.
What makes each playthrough different: randomized values, and information hidden
or chosen during play so no two runs start the same. Status is Planned, PoC built,
or Built. The last column points to the base-game code, with
UNVERIFIED for a spot not yet located.
| Feature | Status | What it does | Base-game code |
|---|---|---|---|
| Map black at start | Tested (v2.22, needs B&B) | The surface starts black; each scanned sector reveals, its neighbours partly reveal and stay scannable, and every farther sector is dark, shows no resource hint or buildable area, and cannot be selected for scanning - so exploration spreads outward from the start. Building is refused outside a build radius around the nearest scanned sector (130% of the sector width, 53248 world units on a standard map), so a Sensor Tower cannot be plopped into the dark for a free scan boost. In StS as Code/StS_HiddenMap.lua (see notes). | hr.RenderRevealDarkness + RevealDarkness objects (Below & Beyond overlay forced onto the surface); rollover and scan gates via OverviewModeDialog:GenerateSectorRolloverContext, MapSector:CanBeScanned, OrbitalProbe:ScanSector; build block via ConstructionController:UpdateConstructionStatuses (Lua/Construction/Construction.lua:1582,1883) |
| Random colony site | Tested v2.9 | The pre-game Colony Site screen (site pick plus the Threats and Resources readout) is skipped: NEXT on the rocket payload screen starts the game on a random landing spot instead of opening the site chooser, and that button is relabeled "START ON RANDOM SITE". Requirement: every existing landing spot must be equally likely - latitude and longitude are drawn uniformly over the whole-degree grid (poles included), not the game's 5 quickstart spots and not the equator-biased sphere pick. In StS as Code/StS_RandomColonySite.lua (see notes). | LandingSiteObjectCreateAndLoad (Lua/UI/PlanetUI.lua:1515); uniform coordinate draw via AsyncRand, committed with GetOverlayValues (Lua/UI/PlanetUI.lua:1594); start via GenerateCurrentRandomMap (Lua/UI/PreGameMenus.lua:241) |
| Chaos Theory (random tech order) | Tested v38 | Each new colony gets a custom weighted shuffle of every discoverable research field (not the base game's full Chaos Theory rule). For each tech an ordering value is computed as its average position (the middle of its designed rank range, (from+to)/2) plus the sum of ten random draws (each AsyncRand(9), an integer 0-8); the field's techs are then sorted by that value. Summing ten flat draws forms a bell curve (central limit theorem), so most techs land a handful of ranks from their usual spot and only rarely far - the order is randomized every game but still loosely follows the intended progression. Widening the draw or adding more draws increases the spread. Funding techs are clamped to their own range so their one-time funding grants still land at the intended research depth. Computed once at colony creation, saved, and never re-rolled on load. In StS as Code/StS_ChaosTheory.lua. | reorder wraps Colony:InitResearch (Lua/Colony.lua:8,64); a field is shown in tech_field order via Research:UITechField (Lua/Research.lua:1027-1051); per-slot cost re-assigned from the field ramp (Lua/Research.lua:153-170); funding techs = presets with an Effect_Funding sub-object; armed at OnMsg.PreNewGame before Colony:Init (Lua/Mysteries/Mysteries.lua:46, Lua/Colony.lua:63-64) |
| In-game first rocket | Tested | The pre-game Rocket Payload (loadout) screen is removed, so setup goes straight to the map: the mission "sponsor" screen's NEXT is retargeted from the payload screen to the landing/map step (and relabeled "START ON RANDOM SITE"). You then call your first rocket in-game from Resupply and it arrives with zero flight time; you still pay the normal cargo cost, and every rocket after it uses normal travel. That first rocket also opens pre-filled with a default loadout you can edit or clear (own page: First rocket loadout; Tested v2.146). In StS as Code/StS_FirstRocketInGame.lua. | sponsor NEXT node retargeted "payload" to "landing" (Lua/XTemplates/PGMissionSponsor.lua:194-209); first-call promotion in City:OrderLanding (Lua/City.lua:316-329) to the instant path in RocketBase:FlyToMars (Lua/Buildings/RocketBase.lua:241,297-301) |
| Rocket | Tested v2.9 | One of the six Map conditions, rolled once per colony (its medium is the middle package pods): it sets the starting fleet and rocket travel time as one of three packages - 1 rocket with standard travel (fast), 1 rocket with double travel time plus 5 free Supply Pods (pods), or 2 rockets with triple travel time (big). Travel is a +0/+100/+200% modifier on the two travel consts that regular supply rockets read, plus the same x1/x2/x3 on the custom-travel rockets (Dragon, Zeus, Expedition Rocket) so they slow with the tier while keeping their speed edge; Supply Pods keep a fixed travel speed. Applied on CityStart/LoadGame, idempotent. In StS as Code/StS_ConditionRocket.lua. | GetStartingRockets (Lua/City.lua:310); travel consts read at Lua/Buildings/RocketBase.lua:240,826, modified via g_Consts:SetModifier (Lua/Modifiers.lua:180); free pods replicate RewardSupplyPods (Lua/ClassDefs/ClassDef-Effects.generated.lua:1781-1785) |
| Sensor Towers | Tested v2.222 | One of the six Map conditions, rolled once per colony: it trades how far a sensor tower's scan-speed boost reaches against how strong that boost is. The extreme options are wide reach with a weaker boost (reach x1.33, boost x0.67) or short reach with a stronger boost (reach x0.67, boost x1.33); the medium option leaves both as they are. Applied from a stashed base on CityStart/LoadGame so a mod reload never compounds; the full-boost radius is left untouched, so only the falloff length and the peak boost move. Supersedes the earlier flat-50% range-cut proof of concept. In StS as Code/StS_RandomTowerRange.lua. | Lua/_GameConst.lua:109-111; Lua/Exploration.lua:284-306 |
| Vistas | Tested v2.222 | One of the six Map conditions, rolled once per colony: it shifts the map's mix of two scenic surface deposits, Vistas (dome comfort) and Research Sites (research boost). As each such deposit is uncovered by exploration, on an extreme roll each one of the source type has a 33% chance to flip to the other - one extreme yields about +33% more Vistas (a third of Research Sites flip), the other about +33% more Research Sites (a third of Vistas flip); the medium roll leaves the map's split unchanged. Only deposits placed by the map are swapped: deposits granted by story events are left alone. In StS as Code/StS_ConditionVistas.lua. | RevealDeposits (Lua/Exploration.lua:417) wrapped; EffectDepositMarker:SpawnDeposit (Lua/Buildings/EffectDeposit.lua:10) overridden to re-point deposit_type between BeautyEffectDeposit and ResearchEffectDeposit (Lua/Buildings/EffectDeposit.lua:108,154); story deposits bypass it via direct PlaceDeposit (Lua/ClassDefs/ClassDef-Effects.generated.lua:2189,2412) |
| Funding | Tested v2.222 | One of the six Map conditions, always active, rolled once per colony: a coupled +/-20% trade-off between the two funding streams, or an even split. On metals the Rare Metals export price is +20% and tourist funding -20%; on tourism the reverse; on even both are unchanged. It never raises both. Applied as two g_Consts modifiers on CityStart/LoadGame, set absolutely each time so a reload never compounds. This replaces the earlier +50% single-edge design (a Rare Metals, tourist, or research tech-grant edge). In StS as Code/StS_ConditionFunding.lua. | Rare Metals price ExportPricePreciousMetals (Lua/_const.lua:628-632) read in Funding:CalcBaseExportFunding (Lua/Funding.lua:38-40); tourist payout TouristFundingMultiplier (Lua/_const.lua:203-207) read in HolidayRating:RewardMoney (Lua/HolidayRating.lua:59,63); both set via g_Consts:SetModifier (Lua/Modifiers.lua:180, percent a delta on 100 at :62,99) |
| Choices during play | Tested (v2.41, in-game + log): Sponsor, Commander and Mystery each a setup-screen multi-select pool feeding the later 3-way choice; preselection and the "N of M possible" row summary verified in-game. v2.42 hides Below and Beyond options and dedups Space Race duplicates (built on top, not re-tested in-game). v2.50: the commander offer draws the three from distinct groups (Water/Colonist/Economy/Other), DLC commanders (Geo Engineer, Transport Tycoon) included - tested and done. v2.64: each sponsor option in the pick card lists its vehicle, building, perk and colonist trait, and the Sponsor notification sits above the Commander one - tested. v2.115: vanilla Politician (dropped from the roster) is hidden from the Possible Commanders checklist, matching the Russia sponsor drop - tested. (Commander/sponsor deposit reveal, tech top-up and rebalances still built untested.) | The Sponsor, Commander and Mystery are each chosen in two stages. On the setup screen each is a multi-select checklist - "Possible Sponsors", "Possible Commanders", "Possible Mysteries" - of which options may appear this run; every option is checked by default, except mysteries, where only the ones you have not finished are pre-checked (read from AccountStorage.FinishedMysteries). The collapsed row shows "All possible" or "N of M possible". The mod's later random choice then offers three drawn from the checked pool: the Sponsor at game start and the Commander at colony start via a non-dismissable click notification (Commander notes here), and the Mystery on sol 50 via an event (notes) - one per difficulty tier where the pool allows. Below and Beyond options and vanilla Politician (unsupported) are hidden and Space Race's same-id duplicates deduped. Each sponsor option in the pick card lists its unique vehicle and building (each with a short description), its perk and its colonist trait, the four columns of the sponsor table. See the Run setup overview. | sponsor GetMissionSponsor (Lua/PreGameMission.lua:254), commander GetCommanderProfile (Lua/PreGameMission.lua:268), mystery Mysteries:SelectMystery / CheatStartMystery (Lua/Mysteries/Mysteries.lua:11,114); choice WaitStoryBitPopup (Lua/MarsStoryBits.lua:58), notification AddCustomOnScreenNotification (Lua/UI/OnScreenNotification.lua:708); setup pool a multi-select checkbox item routed through PGMissionItem (Lua/XTemplates/PGMissionItem.lua), cloned from GameRuleItem with GetCheckboxImage (Lua/GameRules.lua:112), in Code/StS_MissionPoolUI.lua |
| Pre-Founder event choice | Built, untested (v48) | The vanilla Pre-Founder Stage event pool is emptied and replaced by the mod's own set of 15 balanced hand-authored events (around 500 M each, no trap choice), each replacing the vanilla subset that rolled a 40% chance to be in the pool. In the night between Sol 2 and Sol 3 the mod force-fires one of them at random: you never pick which event you get, only how you answer its 3 or 4 options. The tech reveals are spread one per event across seven of them - the next 3 techs in a field for Terraforming, Biotech, Engineering, Robotics, Physics and Social, plus a Cross-Discipline Survey that reveals the next tech in 3 different random fields. The full event and reward list is in story-events.html. In StS as Code/StS_PreFounderEventChoice.lua (see notes). Rocket Short-circuit is untouched (it fires on a manual rocket launch, not the pre-founder tick). The vanilla pool is documented in story-events.html. | the 19 Tick_BeforeFounders StoryBits (Data/StoryBit/Boost*.lua); per-game pool via EnableChance/Enabled (Docs/ModItemStoryBit.md.html), runtime list g_StoryBitStates (Lua/Buildings/PlanetaryAnomaly.lua:171); random pick via AsyncRand then force-fire ForceActivateStoryBit (Lua/Buildings/PlanetaryAnomaly.lua:338) |
| Any-time events disabled | Built, untested (v2.261) | The vanilla Any-time story-event group - the periodic events with no colony-stage gate - is not supported. It holds two events, Fickle Economics (a random import price rises) and Power Surge (a power network threatens to overload); the mod empties the whole group so neither ever fires, the same empty-pool method the Pre-Founder replacement uses. Detail on Story events. In StS as Code/StS_DisableAnyTimeEvents.lua. | the Tick-category StoryBits (Data/StoryBit/IncreaseResourceCost.lua, PowerSurge.lua; group in Data/StoryBitCategory.lua); disabled via EnableChance/Enabled on each preset |
| Survey Data Gathered | Tested v2.79 (mechanic); blue frame v2.81; landing-gated notification tested 2.112 | A Sol 1 event. Once the first rocket lands on Mars a non-dismissable, clickable "Survey in Progress" notification (the built-in blue notification frame - the game ships no yellow one) counts down the disaster-warning way ("Survey completes in <countdown>"); clicking it opens a message box explaining the survey and the choice to come. At Sol 1 20:00 it resolves into a choice popup, "Survey Data Gathered", regardless of when the rocket landed. The player names one of the six research fields - Terraforming, Biotech, Engineering, Robotics, Physics, Social, in research-screen order - and its next 2 technologies are revealed, using the same reveal a Tech Anomaly uses (stopping early if a field has fewer than 2 left). Fires once, new colonies only; it announces itself, so it is not in the General Rules readout. In StS as Code/StS_SurveyDataGathered.lua. | fields GetAvailablePresets(Presets.TechFieldPreset.Default) filtered by discoverable (Data/TechFieldPreset.lua; Terraforming armstrong/Presets/TechFieldPreset.lua); reveal DiscoverTechInField (Lua/Research.lua:214), count TechCount (:526); countdown notification AddCustomOnScreenNotification with display_countdown/game_time (Lua/UI/OnScreenNotification.lua:704-706,319-345); popup WaitStoryBitPopup (Lua/MarsStoryBits.lua:58); notification gated on the first Mars landing, Msg("RocketLanded") (Lua/Buildings/RocketBase.lua:381); clock UIColony.day/hour, game starts Sol 1 hour 6 (Lua/Colony.lua:10-11,183-192), NewHour (Lua/DayTime.lua:73) |
| Goal timeline | Built, untested (v2.291) | Replaces the sponsor's five fixed Mission Goals with five goal picks on a rising schedule (Sols 3, 6, 12, 20, 30). Each step draws six candidate goals - two timed, four untimed - and offers three at random (one timed, two untimed); you pick one and its reward is granted on completion. Every step carries one of each of six reward kinds: research points and research-per-Sol (the step's two timed goals, so only one research reward is ever offered together), a supply pod of advanced resources, a prefab building (worth half its resupply cost), Funding, and a flex slot (specialist applicants, a tech reveal, or a Rare Metal deposit - never a Water deposit). Reward values track a per-step band (untimed 500 / 750 / 1,000 / 1,250 / 1,500; a timed goal pays 30% more). Where no single building fits the prefab band the slot grants two (2 Stirling Generators, 2 Triboelectric Scrubbers, or an MDS Laser plus a Fusion Reactor). After Goal 5 the Sol 50 Mystery takes over. Full pool, values and rules on goals-and-timer.html; prefab reward values on prefabs.html. In StS as Code/StS_GoalTimeline.lua and Code/StS_GoalsPage.lua. | goal machinery Lua/Colony.lua MissionGoalUpdate / SetupMissionGoals (goal:Completed then reward:Execute); objective types Data/SponsorGoals.lua; reward effects Lua/ClassDefs/ClassDef-Effects.generated.lua (RewardFunding, RewardResearchPoints, RewardSponsorResearch, RewardPrefab, RewardApplicants, RevealNextTechInField, SpawnRocketInOrbit, SpawnSubsurfaceDeposits); choice popup Lua/MarsStoryBits.lua:58 |
| Map conditions | Foundation Tested v2.9; Mission Goals page readout + read-once green notification Tested v2.111; unified two-medium-four-extreme roll over six conditions Tested v2.222 (see the Map conditions page for per-condition status) | Each new colony gets six conditions (like Against the Storm's map modifiers), fixed for its life, shown in a click-to-read "Map Conditions" notification (green frame) and listed permanently on the Mission Goals page below the commander profile: Rocket (fleet, travel time, free pods), Dust Storm vs Cold Wave prevalence, a Funding trade-off (Rare Metals vs tourist funding), Sensor Towers (range vs boost), Vistas vs Research Sites, and a map-derived Topography Metal & Rare Metal shift (flat -20% / rough 0% / steep +20%). Every condition has a medium (default) value and two extremes, and across all six the colony always lands on exactly two medium and four extreme; Topography is not rolled (the map fixes it) but counts toward that target, so the five rolled conditions fill the rest. The shipped foundation rolls the six-condition plan, resolves each value on demand through a read API (StS_GetMapCondition), and shows the notification (each line falls back to the raw rolled value until its condition file adds a readout); the same readout is appended to the Mission Goals page below the commander profile, and clicking the notification opens the readout then clears it for good (close_on_read plus a per-colony read flag keep it from reappearing once read); each condition's gameplay effect lives in its own follow-up file. In StS as Code/StS_MapModifiers.lua; full design and per-condition game-code translation on its own page: map-conditions.html. | roll persisted in a GlobalVar (Docs/LuaSavegame.md.html:11,19), rolled at OnMsg.NewGame like SessionRandom (Lua/Colony.lua:89-94); notification AddCustomOnScreenNotification (Lua/UI/OnScreenNotification.lua:708, green frame priority = "NormalTerraforming" at :478, close_on_read at :230-233), click popup CreateMessageBox (Lua/UI/MarsMessageQuestionBox.lua:133); page readout appended in MissionProfileDlg:GetMissonProfileText (Lua/MissionProfileDlg.lua:24-58); Topography from GetMapChallengeRating (Lua/PreGameMission.lua:568) bucketed like the local MapChallengeRatingToDifficulty (Lua/UI/PlanetUI.lua:93-103) |
| General Rules | Tested v2.56; green frame v2.111; 86-char per-line cap Tested v2.117 | A companion to the "Map Conditions" notification: a single dismissable on-screen notification (green frame, like the terraforming notifications) whose readout lists ONLY the invisible, map-wide rules the player cannot learn anywhere else - removed systems (Below and Beyond off, rival exploits gone), tech-order randomization, the escalation curves (storms, meteors, inflation, research decay), and map-wide tweaks (idle/shut-down maintenance, resource parity, cold ground). Deliberately NOT listed, because each is already shown to the player: per-building and per-tech changes (now on the building's own description and on the tech that unlocks it), the in-play picks (sponsor/commander/mystery/Pre-Founder announce themselves when they fire), resupply costs (the Resupply screen), sponsor numbers (the sponsor panel), the per-run Map conditions (their own notification), and self-evident on-screen facts. The message box does not scroll (its XScrollArea is present but non-functional), so the readout is kept short enough to fit one screen, with each body line capped at 86 characters (the length of the "Rivals give no help" line) so it renders on one screen line without wrapping. Clicking opens the readout and removes the notification (close_on_read); a per-colony read flag keeps it from reappearing once read, and it re-shows on CityStart/LoadGame until read. In StS as Code/StS_GeneralRules.lua. Filter and no-scroll rule documented in CLAUDE.md "What belongs in the General Rules notification". | AddCustomOnScreenNotification (Lua/UI/OnScreenNotification.lua:708, green frame priority = "NormalTerraforming" at :478, close_on_read at :230-233); click popup CreateMessageBox (Lua/UI/MarsMessageQuestionBox.lua:133); non-scrolling box Lua/XTemplates/MarsMessageBox.lua:44-64; read flag in a GlobalVar (Docs/LuaSavegame.md.html:11,19) |
| Setup screen "Effects" box | Tested v2.44 (mechanism; the four-section text was expanded in later builds) | On the mission setup screen the "Effects" box, in vanilla, lists the combined effects of the chosen sponsor, commander and game rules, and shows the word "Random" when that list is empty. Because the mod hides the sponsor and commander from the summary and removes the game-rules row, the box always showed the bare "Random". It is replaced with a four-section campaign summary - Randomized Setup, Randomized Map Conditions, Escalating Difficulty, Balanced Gameplay - that describes what the mod does. Tutorial and challenge runs keep the base text. In StS as Code/StS_SetupEffectsText.lua. | GetDescrEffects (Lua/PreGameMission.lua:535-566; "Random" fallback at :562-564), rendered by PGMissionObject:GetEffects (Lua/X/XPGMission.lua:232-234) |
The surface starts fully black and is uncovered by scanning. Implemented and tested
in-game as
Code/StS_HiddenMap.lua.
Requires Below & Beyond (picard): the darkness is that DLC's underground
overlay, forced onto the surface.
Every sector is in one of three stages, re-evaluated as sectors are scanned. A sector is stage 3 when it is unexplored and none of its eight neighbours is scanned; stage 2 when it is unexplored but touches a scanned sector; stage 1 when it is scanned.
Exploration:InitialExplore,
Lua/Exploration.lua:983-1035, picked by InitialReveal at
:896-981), so there is always a lit starting point.Surviving Mars has no surface fog of war; the terrain is fully visible from the first frame. The only real terrain-darkness is the Below & Beyond underground overlay, and it can be driven on the surface:
hr.RenderRevealDarkness. The
surface ships with it 0 (off); setting it to 1 turns the pass
on. This one flag was the whole blocker. Found via ChoGGi's "Show All Underground" mod,
which sets it to 0 to reveal everything.hr.RevealDarknessY
/ RevealDarknessYThreshold / RevealDarknessRevealed (false =
darkness active) / RevealDarknessFalloff. The mod computes the band once
per map from the terrain height (GetActiveTerrain():GetAreaHeight()).RenderRevealDarkness back to
0, so a real-time thread re-applies the vars every 0.5 s, but only while a
map with sectors is live (calling GetActiveTerrain off-map throws a
terrain != nullptr error).RevealDarkness object at the sector
centre with SetRadius. The radius is not in world units: ~600 covers one
sector (a sector is ~40960 wide), roughly an 88x scale, so a world-unit value reveals
the whole planet. Placed for every scanned sector at start and on
Msg("SectorScanned", ...) (Lua/Exploration.lua:120).The base game ships only stubs (Lua/RevealDarkness.lua:9,21,
CanReveal returns false), which is why the DLC is required. It
coexists with "No underground / No asteroids": that content is removed in code so the DLC
can stay loaded for this overlay (see Cut features).
Four base-game methods are wrapped at ClassesBuilt:
OverviewModeDialog:GenerateSectorRolloverContext
(Lua/UI/OverviewModeDialog.lua:522-664) concatenates the whole rollover
body into one RolloverText (the "High chance..." hint at string id 4053,
the buildable area at id 4051). For a stage-3 sector the wrap replaces that body with
just the status line, hiding the buildable area and the resource hint while leaving the
scan-interaction hint.MapSector:CanBeScanned (Lua/Exploration.lua:166,
gated by QueueForExploration at :596) returns false
for stage-3 sectors, so they cannot be queued; the "scan available" notification and the
rollover scan hints read the same method and follow.OrbitalProbe:ScanSector (Lua/OrbitalProbe.lua:87)
scans without checking CanBeScanned, so the wrap ignores stage-3 targets
there too (blocks Blue Sun / AdaptedProbes probing a far tile).ConstructionController:UpdateConstructionStatuses
(Lua/Construction/Construction.lua:1582) gathers the placement cursor's
statuses; the wrap appends an error-type status (StS_DarkSector) when the
cursor sits outside the build radius. The build radius is the on-screen boundary
around scanned sectors: a spot is buildable while it is within
BUILD_RADIUS_PCT of the sector width from the nearest scanned sector centre,
and blocked past that. On the standard sector width of 40960 world units and the default
BUILD_RADIUS_PCT = 130 that is a radius of 53248 world units. Lower
BUILD_RADIUS_PCT to decrease the radius. Placement is refused when the first
status is an error (:1883) and any error sorts ahead of every warning
(:80-87), so the cursor turns red and the click does nothing. The base game's
own "Unexplored sector" status is only a warning (:26), which is why
building in the dark was allowed before. This blocks every building, Sensor Towers
included.The darkness is a height-band fog, so it is camera-height dependent: zoomed in a sector reads black, zoomed far out more of the map reads revealed. Caves never show this because underground clamps the camera zoom; the surface allows full zoom-out. Tuning the band, or constraining zoom, is the open item.
Considered but not used, kept as a fallback if the DLC dependency is ever dropped:
LockCamera / UnlockCamera freeze all camera movement while
leaving the UI clickable (Lua/Camera.lua:29-37, Lua/PhotoMode.lua:171-174),
and cameraRTS.SetCamera aims at the starting sector
(Exploration.InitialSector). That hides the map by pinning the view rather
than darkening the terrain, and needs no DLC.
Base game, paths relative to external/SurvivingMars. Sensor scanning,
sector reveal and the initial reveal are in Lua/Exploration.lua; the sector
rollover and scan click in Lua/UI/OverviewModeDialog.lua; orbital probe
scanning in Lua/OrbitalProbe.lua. The underground darkness system is in
Lua/RevealDarkness.lua (base stubs), driven in the Below & Beyond DLC and
via hr.RenderRevealDarkness (found in ChoGGi's "Show All Underground");
camera locking in Lua/Camera.lua and Lua/PhotoMode.lua.
The pre-game Colony Site screen is skipped: the player never picks a landing spot and never sees the map's Threats and Resources in advance. Clicking NEXT on the rocket payload screen starts the game on a random landing spot, and that button is relabeled START ON RANDOM SITE. Implemented as Code/StS_RandomColonySite.lua. No DLC required.
A landing spot is a whole-degree (latitude, longitude) coordinate on Mars, and the map is a
deterministic hash of that coordinate (seed = xxhash(lat, long),
Lua/UI/PlanetUI.lua:1591), so each coordinate is one fixed map. The requirement is
that every such coordinate has the same chance of being drawn. Two of the game's
own helpers are deliberately not used because they violate this:
GenerateRandomMapParams (Lua/PreGameMission.lua:655) draws only
from the 5 landing spots flagged quickstart, so it can produce just 5 maps.GenerateRandomLandingLocation / SeedToSpherePoint
(Lua/UI/PlanetUI.lua:1605) picks a point uniformly over the sphere's
surface area. Because meridians converge at the poles, a 1°×1° cell near
the equator covers far more area than one near a pole, so equator coordinates would be picked
far more often and pole coordinates rarely - not equal per spot.Instead the mod draws latitude and longitude directly and uniformly over the whole-degree
grid, so every coordinate (poles included) is equally likely:
lat = (AsyncRand(181) - 90) * 60 gives -90..90° and
long = (AsyncRand(360) - 180) * 60 gives -180..179° (180 and -180 are the same
meridian, so 360 distinct values avoids double-counting). AsyncRand(n) returns
0..n-1 (Lua/PreGameMission.lua:664); coordinates are in arc-minutes (degree×60),
the unit the presets and RoundCoordToFullDegrees use
(Lua/UI/PlanetScene.lua:248).
New Game runs one dialog (PGMission) with three internal screens in order:
sponsor, payload, landing
(Lua/XTemplates/PGMission.lua:11). The payload screen's NEXT action only
switches the dialog to the landing screen
(OnActionEffect="mode", OnActionParam="landing",
Lua/XTemplates/PGMissionPayload.lua:141-147). That landing screen
is the Colony Site (PGMissionLandingSpot), which lists THREATS (string id
4271) and RESOURCES (id 4270) and whose START button picks the map and begins the game
(Lua/XTemplates/PGMissionLandingSpot.lua:192-197,361,401). The mod makes the
game start at the point the Colony Site screen would open, so that screen never functions.
The Colony Site screen builds its context object through the global
LandingSiteObjectCreateAndLoad (Lua/UI/PlanetUI.lua:1515). The
same function feeds two other screens - the challenge landing screen and the in-game
planetary view for expeditions and asteroids - so it must not be hijacked wholesale. The
three callers are distinguishable by their flags: the challenge screen passes
challenge_mode = true, the planetary view passes
planetary_view = true, and only the pre-game mission call passes neither
(Lua/XTemplates/PGMissionLandingSpot.lua:7,
PGChallengeLandingSpot.lua:7, PlanetaryView.lua:7). The mod wraps
the function and acts only when both flags are false, leaving the other two screens
untouched.
lat, long are passed to
GetOverlayValues, which writes latitude, longitude
and seed onto g_CurrentMapParams
(Lua/UI/PlanetUI.lua:1594-1602) - the fields START requires. The rocket name
chosen on the payload screen is left untouched (unlike
GenerateRandomMapParams:670, which would regenerate it).CreateRealTimeThread: WaitWarnAboutSkippedMods(),
MarkNameAsUsed("Rocket", g_CurrentMapParams.rocket_name_base),
GenerateCurrentRandomMap() (Lua/UI/PreGameMenus.lua:241,
matching Lua/XTemplates/PGMissionLandingSpot.lua:192-197).Because NEXT now starts the game, the payload button is relabeled. Its label is the
string NEXT (id 5453), shared by the sponsor, payload and landing
screens, so the shared string is left alone; instead the mod sets ActionName
on the one payload next action node inside
XTemplates.PGMissionPayload (found by a small recursive walk of the template
tree). Setting a template action's ActionName at run time is the game's own
idiom (Lua/XTemplates/GameCheatShortcuts.lua:440,
Lua/XTemplates/PlanetaryView.lua:1165). The new label is
Untranslated("START ON RANDOM SITE"), which sidesteps any translation-id
clash. Both the wrap and the relabel install at ClassesBuilt.
Skipping the screen also removes its CUSTOM-coordinates entry, which let a player type
exact coordinates to replay a specific map (Lua/XTemplates/PGMissionLandingSpot.lua:247),
and the up-front Altitude / Temperature / Topography readout, not only Threats and
Resources.
The Commander Profile is chosen in two stages. On the setup screen the Commander row is a multi-select "Possible Commanders" checklist of which commanders can appear this run (all checked by default; Below and Beyond commanders and vanilla Politician hidden). The colony then starts with no commander, and a non-dismissable "Select your Commander" notification appears on the map at colony start; clicking it opens a choice of a random three drawn from the checked commanders and the chosen one's bonuses are applied live. In Code/StS_CommanderChoice.lua, matching the sponsor choice; the setup checklist is the shared framework in Code/StS_MissionPoolUI.lua (mechanism described under Choose your Mystery). No DLC required.
The choice notification. A non-dismissable "Select your Commander" notification is added
on CityStart and LoadGame (guarded, so it never shows once the commander
is chosen). Clicking it opens the choice; picking a commander removes the notification. Same
click-notification pattern as the sponsor and the "Map Conditions" readout
(AddCustomOnScreenNotification / RemoveOnScreenNotification,
Lua/UI/OnScreenNotification.lua:708,778); the callback is a named global so it
persists by name across a save.
The commander is read everywhere through GetCommanderProfile()
(Lua/PreGameMission.lua:268-276) and applied at colony start in
Colony:Init (Lua/Colony.lua:53, :81) and
Colony:GameStart (Lua/Colony.lua:396, :403); its
bonus_rockets is read by GetStartingRockets (Lua/City.lua:310-313).
The mod wraps GetCommanderProfile to return a custom no-effect placeholder
(PlaceholderCommanderProfile, defined in items.lua)
until the player has chosen, so every start-time read gets an empty profile and the colony
begins with no commander bonus. The placeholder replaces the game's built-in None
profile, a degenerate stub with no display name or effect that made the in-game Mission Profile
dialog throw when it resolved the commander text, hiding the Mission Goals list; the placeholder
carries the same no-effect behaviour but valid, neutral fields, so the dialog renders and the
goals show. Reading at the consumption point makes this immune to the pre-game flow resetting
mission params. The "chosen" state is a key on g_CurrentMissionParams, which is wiped per new
game by InitNewGameMissionParams (Lua/PreGameMission.lua:187-209) and
persisted by PersistSave (:221-225), so it is false for a fresh colony
and restored on load.
Three separate places show the commander before the game, each patched additively:
GetMissionParamUICategories
(Lua/X/XPGMission.lua:172-190), the same wrap used for the Game Rules row.GetDescrEffects
(Lua/PreGameMission.lua:535-566), which reads the raw
g_CurrentMissionParams.idCommanderProfile (:550) and prints a
"Commander Profile:" header plus effect only when that effect is non-empty (:554-558).
The mod runs the original with the commander forced to None (whose effect is blank),
so the whole block is skipped, then restores the value.PopupNotificationPreset WelcomeGameInfo, Data/PopupNotificationPreset.lua:319-330)
hardcodes a "Commander Profile: <commander_name>" line that with no commander renders empty.
The mod rewrites that preset's text to the same body and Sponsor line without the commander line.Clicking the "Select your Commander" notification shows
WaitStoryBitPopup (Lua/MarsStoryBits.lua:58-79), the game's own choice
popup, with a random three drawn from the full commander pool (every profile except the
None and Random placeholders, so the mod's added Prospector is included),
re-rolled each game. On the pick it reproduces every bonus colony start would have applied:
profile:EffectsApply(UIColony) runs Funding,
ModifyLabel (via SetLabelModifier, which applies to existing and future buildings),
GrantTech (colony:SetTechResearched) and ModifyLabelOverTime
(Lua/MarsGameEffects.lua:85,164,398).tech1..tech5 (the non-effect grants, e.g. City Mayor's Mars
Nouveau) - granted with UIColony:SetTechResearched (Lua/Research.lua:269),
the same call GrantTech uses.City:CreateSupplyShips (Lua/City.lua:305-306):
PlaceBuildingIn(GetRocketClass(), map_id, {city}).Exploration:InitialExplore
(Lua/Exploration.lua:1002-1024) for its Hydro Engineer and Astrogeologist, which never
fires with no commander, so the mod replicates it on the pick (find the nearest unplaced
SubsurfaceDepositMarker near the initial sector and PlaceDeposit). In this
mod the Water deposit belongs to the new Prospector, not Hydro Engineer (see the rebalance below).SetTechResearched, which (unlike normal research) reveals no replacement while the field
still has research available (Lua/Research.lua:293-297), dropping the field below the 1
from 1 tech revealed per field.
The mod tops every discoverable field back to 1 revealed-unresearched with
DiscoverTechInField (Lua/Research.lua:214); researched techs are always also
discovered (:278), so discovered - researched is the revealed-unresearched count.The commander roster is built from scratch (full list: Commander Profiles):
every offered profile is wiped and rebuilt to one perk, one free tech and one prefab or unit, sorted into
four groups (Water, Colonist, Economy, Other), 16 profiles, 4 per group. The apply path is unchanged -
EffectsApply runs the perk (Effect_ModifyLabel) and the prefab
(Effect_GrantPrefab), tech1 grants the free tech, bonus_rockets spawns
Rocket Scientist's rocket, the deposit reveal covers Prospector and Geologist, and a supply-pod cargo drop
delivers the three rover units (RC Transport, RC Explorer, RC Commander). The vanilla preset ids are reused
so id-coupled perks still fire - only Psychologist needs it, its +5 rest-sanity reading
Lua/Units/Colonist.lua:1692. The pick mechanism (checklist, notification, popup) is tested; the
from-scratch roster is Built, untested (v2.95). As a PoC this targets new colonies; on load of a picked
save the choice and its baked-in bonuses persist and nothing re-applies.
The Mystery is chosen in two stages. On the setup screen the Mystery row is a multi-select "Possible Mysteries" checklist of which mysteries can appear this run; the colony then starts with no mystery, and on sol 50 an event offers three of the checked mysteries and the chosen one starts live. In Code/StS_MysteryChoice.lua, with the shared checklist framework in Code/StS_MissionPoolUI.lua. No DLC needed.
The base game already appends every accessible mystery to the Mystery row's item list at
ClassesBuilt (Lua/PreGameMission.lua:154-178). The mod replaces that row's
list item with a checkbox item (cloned from the shipped Game Rules checkbox,
Lua/XTemplates/GameRuleItem.lua, using GetCheckboxImage,
Lua/GameRules.lua:112) whose checkbox toggles a set on
g_CurrentMissionParams, and hides the "Random"/"None" pseudo-entries. Mysteries you have
not finished are pre-checked and finished ones start unchecked, read from
AccountStorage.FinishedMysteries (set on MysteryEnd,
Lua/Mysteries/Mysteries.lua:102-110); finished mysteries keep their green check. The
collapsed row shows "All possible" or "N of M possible" by wrapping
PropChoice:OnPropUpdate (Lua/XTemplates/PropChoice.lua:28-56). The set
persists and resets with the rest of the mission params (PersistSave /
InitNewGameMissionParams, Lua/PreGameMission.lua).
At colony start Colony:Init calls SelectMystery (Lua/Colony.lua:63),
which reads g_CurrentMissionParams.idMystery and sets self.mystery_id
(Lua/Mysteries/Mysteries.lua:11-44). The mod overrides it to select nothing for a normal
game (tutorial and challenge keep the base behavior). The override must be installed on
g_Classes.Colony, not the Mysteries parent: HGE flattens inherited methods
into the subclass at build time, so overriding the parent after ClassesBuilt does not
reach Colony's already-flattened copy.
At sol 50 20:00 - checked on NewHour, firing once day == STS_MYSTERY_DAY
and hour >= STS_MYSTERY_HOUR (or any later sol, as a load backstop), the same evening
hour as the Sol 1 survey and the goal picks - a
WaitStoryBitPopup (Lua/MarsStoryBits.lua:58-79) offers three mysteries drawn
from the checked pool: one random pick from each difficulty tier where the pool has one, then the
remaining slots filled from the rest of the pool. Difficulty is the mystery class's
challenge_mod - 20 Easy, 40 Normal, 60 Hard - and the label is already in the
display name ("... (Easy/Normal/Hard)"). If the pool is empty (every mystery unchecked) it falls back
to the full accessible list. Picking one starts it by replicating CheatStartMystery
(Lua/Mysteries/Mysteries.lua:114-165) minus its cheat gate: set mystery_id, add
the mystery's techs to research, then InitMysteries which creates the mystery object and
fires MysteryChosen.
Tested (v2.41, in-game). The colony starts with no mystery, the setup checklist pre-checks unfinished mysteries and shows the "N of M possible" summary, and the sol-50 event offers three from the checked pool. New colonies only; a chosen mystery persists in the save. The v2.42 follow-up hides Below and Beyond mysteries from the list (built, not re-tested in-game). Known base-game trait: mysteries are authored to begin at colony start, so a chosen mystery's opening event may fire right away when it starts on sol 50.
The vanilla Pre-Founder Stage event pool is emptied and replaced by the mod's own hand-authored events. In the night between Sol 2 and Sol 3 the mod force-fires one of them at random: the player never picks which event fires, only how to answer its replies. So exactly one pre-founder event happens per game. In StS as Code/StS_PreFounderEventChoice.lua. No DLC required. The vanilla pool - 19 events, triggers, choices and rewards - is documented in story-events.html.
The pre-founder events are the StoryBits with Category = "Tick_BeforeFounders"
(Data/StoryBit/Boost*.lua, 19 in the base game). The engine adds each to the
per-game triggerable list at game start only if it is Enabled and passes its
EnableChance roll (Docs/ModItemStoryBit.md.html: EnableChance is "the
initial chance for this StoryBit to be added to the list of StoryBits which may be triggered in
each game"); the runtime list is g_StoryBitStates, keyed by id
(Lua/Buildings/PlanetaryAnomaly.lua:171). At ClassesBuilt the mod sets
EnableChance = 0 and Enabled = false on every such StoryBit, so none is
added at start and none fires on its own. Verified by the CityStart log line, which
lists the pre-founder ids still in g_StoryBitStates (expected: none). Rocket
Short-circuit (Boost17) is not in this category - it fires on a manual rocket launch,
its own trigger - so it is left untouched.
In the Sol 2 to Sol 3 window (checked each game hour and on load; skipped under the tutorial
and the Story Bits Disabled game rule), the mod picks one of its own StS_PF_ events at
random - only ones the player currently qualifies for, i.e. whose every StoryBit Prerequisite passes
now (the same cond:Evaluate the engine runs to gate a normal tick; the pre-founder
prerequisites are colony-wide, and the events with none always qualify), and drops any whose reward
would be redundant. The chosen event is force-fired with ForceActivateStoryBit(id, MainMapID, false)
- the same call shipped code uses (Lua/Buildings/PlanetaryAnomaly.lua:338) - which
starts the event immediately, ignoring suppression, delay and prerequisites
(Docs/ModItemStoryBit.md.html, ActivateStoryBit). The chosen preset is flipped back to
Enabled / EnableChance 100 just before, so its one-time state records
normally. The chosen id and a "shown" flag live on g_CurrentMissionParams (wiped per
new game, persisted on load), exactly as the Commander and Mystery choices; a force-fired one-time
StoryBit records its state, so nothing re-fires on load.
Built, untested (v48). Two engine behaviours the build's logs confirm: that disabling
keeps the events out of g_StoryBitStates at start (the CityStart log), and
that ForceActivateStoryBit fires the chosen event despite the empty pool (the fire log
prints state_before=false state_after=true).
The shipped code in Code/StS_MapModifiers.lua is the foundation of the map-conditions system: it rolls each colony's conditions, stores them, exposes them to the per-condition code, and shows the notification. It applies no gameplay effect - every condition's effect is built in its own follow-up file. The full per-condition design and game-code translation is on the Map conditions page; this section documents only what the foundation does. No DLC required.
Five conditions are rolled once per colony; the sixth (Topography) is derived from the map, not rolled.
Each rolled condition has one medium value (rocket "pods", the others "even") and two
extremes (rocket fast/big, weather dust/cold, funding metals/tourism,
sensor range/boost, vistas vistas/research). Across all six conditions every run
lands on exactly two medium and four extreme. Topography is not rolled but counts toward the target
(rough is its medium, flat/steep its extremes), so the five rolled conditions fill the rest: rough
Topography leaves one medium and four extreme among the five, an extreme Topography two medium and
three extreme. Because Topography is only known reliably in-game, the roll persists a plan - a
Fisher-Yates-shuffled priority order of the five plus a drawn extreme direction for each - and
StS_GetMapCondition resolves the medium/extreme split on demand against the live Topography
(the first N in the order are medium, N = 1 if rough else 2). Draws use
AsyncRand(n) (0..n-1).
The plan (the priority order plus each extreme's direction) lives in one
GlobalVar("StS_MapConditions", false), which the engine
serializes into the save automatically (Docs/LuaSavegame.md.html:11,19). It is rolled in
OnMsg.NewGame - which fires on a new colony but not on load - guarded by an "already
set" check, the same pattern the base game uses for its own session randomness
(g_SessionSeed = g_SessionSeed or AsyncRand(), Lua/Colony.lua:89-94). This
replaces the earlier three binary slots and their
StS_GetMapModifier API; the only external reader
(Code/StS_HarsherDisasters.lua) fetches the old function with rawget and treats
it as absent, so dropping it does not crash.
Three globals, for the per-condition files to consume (they fetch via
rawget(_G, "StS_GetMapCondition"), since reading an undefined global throws in the mod
sandbox):
StS_GetMapCondition(name) returns a condition's current value: for a rolled condition,
resolved from the stored plan against the live Topography (see The roll); topography itself
is derived live. Topography reads the active map's challenge rating
(ActiveMapData.challenge_rating, engine-populated per map, so it is valid in-game and after
load, for random and fixed maps alike) and buckets it the way the
game's own local MapChallengeRatingToDifficulty does (Lua/UI/PlanetUI.lua:93-103):
≤59 flat, ≤99 rough, else steep
(folding the game's Steep and Mountainous). A value not rolled returns nil, which every consumer treats
as its default.StS_MapConditionText - a table each condition fills at ClassesBuilt with a
function returning its readout line (StS_MapConditionText.sensor = function() ... end). Empty
in the foundation.StS_MapConditionOrder - the fixed display order
rocket, weather, funding, sensor, vistas, topography.A pinned notification is added on CityStart and LoadGame with the game's
modding hook AddCustomOnScreenNotification (Lua/UI/OnScreenNotification.lua:708)
as dismissable = false, expiration = -1 so it stays for the whole run (idempotent by id, so
re-adding on load does not duplicate). Clicking it opens a CreateMessageBox
(Lua/UI/MarsMessageQuestionBox.lua:133) that walks StS_MapConditionOrder and
prints one line per condition: the condition's own readout if it has registered one, otherwise a
Label: value fallback so the notification is meaningful before any effect file ships. The
click callback is a named global so it need not be persisted. Player-facing wording is a first draft.
Tested v2.9. The roll, persistence, read API and notification are in place, and all seven per-condition effects are now built (untested) in their own files. Targets new colonies: a colony started before the foundation shipped has no roll, so it shows no notification until a new game.
Retuned mechanics and rising pressure: no single best path, and a game that gets
harder over time. Status is Implemented, Confirmed, or Planned. The last column
points to the base-game code, with UNVERIFIED for a spot not yet
located.
| Feature | Status | What it does | Base-game code |
|---|---|---|---|
| Building and tech descriptions | Tested v2.56 | Every building the mod rebalances states its new, non-vanilla values on its own in-game description, and the tech that unlocks it does the same, so the player reads the change where he builds or researches instead of guessing from remembered vanilla values. Covers MOXIE, Power Accumulator, Sensor Tower, the three large factories and the Ranch, plus the unlock techs Low-G Hydrosynthesis, Micro Manufacturing and 3D Machining. Set on BuildingTemplates[id], ClassTemplates.Building[id] and the tech's TechDef[id] at ClassesBuilt, with the base text stashed once so reloads never compound. In StS across the building rebalance files (for example Code/StS_MoxieRebalance.lua, Code/StS_FactoryStaffingCurve.lua, Code/StS_FoodRebalance.lua). | description on BuildingTemplate (Data/BuildingTemplate.lua); tech description via the TechDef GlobalMap (ClassDef-PresetDefs.generated.lua:1116); translated-string append (CargoTransporter.lua:978) |
| Resupply cost rebalance | Tested v2.9 | Retunes Earth import costs and weights plus the Supply Pod price. Singly-bought items (per unit): Orbital Probe 100 to 200 M and 1,000 to 2,000 kg (Adapted Probes still halves the price to 100 M), Supply Pod 100 to 200 M, Drone Hub 150 to 200 M, Drone weight 1,000 to 500 kg. Bulk resources (per 5 units): Food 20 to 40 M and 2,000 to 5,000 kg, Machine Parts 90 to 100 M, Polymers 70 to 150 M, Electronics 100 to 150 M; Metals and Concrete unchanged. In StS as Code/StS_ResupplyCosts.lua. | Data/Cargo.lua; Lua/ResupplyItems.lua:19-72; sponsor pod_price (Lua/Buildings/SupplyPod.lua:165-171) |
| Rocket launch fuel | Tested v2.121 | Every rocket needs 40% more Fuel to launch, for every sponsor - extending to the whole map the launch-fuel drawback the Paradox Interactive sponsor carries alone in vanilla. It is a percentage on each rocket's own launch fuel, so a standard rocket rises from 50 to 70 and SpaceY's lower-fuel Dragon Rocket scales from its own base and stays proportionally lower; Paradox keeps its own flat +30 on top (a standard rocket 70 + 30 = 100). The Advanced Martian Engines tech's flat -20 then lowers a standard rocket to 50 (its description now states this). The map-wide rule is announced on the General Rules readout, not on a building. In StS as Code/StS_RocketFuelCost.lua. | each rocket's launch_fuel (standard rocket base 50, Data/BuildingTemplate.lua:2910) via a percent modifier on the AllRockets label - the same Effect_ModifyLabel path the Paradox sponsor's flat +30 uses (Data/MissionSponsorPreset.lua:857-861, Lua/MarsGameEffects.lua:164-176); modifiers combine as base × (100 + Σpercent) / 100 + Σamount (Lua/Modifiers.lua:24,99); applied on CityStart/LoadGame, idempotent (Lua/LabelContainer.lua:38-43) |
| Inflation always on | Tested v54 | A custom inflation, always on for every colony (the player cannot turn it off, the Game Rules selector is removed). It replaces the base game's Inflation game rule (+10% every 20 sols) and is faster and broader: prices rise by 1% of their base each sol starting on sol 2 (sol 1 stays at base, +0%; sol N is +(N-1)%), capping at sol 201 (+200%, triple price) and held flat after - the shared sol-201 endpoint of the Escalate ramps. Applied to every import (all resources, all prefabs, rovers) plus the Rocket purchase price and the Supply Pod price. In StS as Code/StS_Inflation.lua. | cargo via ModifyResupplyParams (Lua/ResupplyItems.lua:93); Rocket price g_Consts.RocketPrice (Lua/X/XPGMission.lua:398); Supply Pod price GetMissionSponsor().pod_price (Lua/Buildings/SupplyPod.lua:166); driven each sol on NewDay (Lua/DayTime.lua:56) |
| Research output decay | Tested v2.31 | Every research point the colony earns is worth less the longer the game runs - the output-side mirror of an additive cost inflation, replacing the old per-field cost ramp. Each tech keeps its honest base cost; instead the value of each point falls on the shared sol-201 curve: output = 100 / (100 + steps), steps = clamp(sol - 1, 0, 200), so 100% at sol 1, 50% at sol 101, 33% at sol 201, then held. Drives both real accrual and the on-screen "Research per Sol". In StS as Code/StS_ResearchOutputDecay.lua; overview Escalation to sol 201. | Research:ModifyResearchPoints (Lua/Research.lua:624) - the chokepoint every earned point flows through (AddResearchPoints :680) and what GetEstimatedRP returns for the UI (:805); paired with UnmodifyResearchPoints (:634) so the leftover carryover (:698-699) stays exact. Wrapped on g_Classes.Colony (flattened). |
| Terraforming tech cost | Tested v2.251 | Green Planet ships every Terraforming research tech at twice the standard per-tier cost: the field's 22-tier cost curve is the standard 1000..40000 curve every other field uses, doubled to 2000..80000, and only the Terraforming Initiative sponsor discounted it, so for every other sponsor terraforming cost double. This halves the field's base costs, so terraforming techs cost the standard per-tier amount for all sponsors. The Terraforming Initiative sponsor's own discount is a separate lever, retuned from 50% to 30% (in Code/StS_SponsorEffects.lua), so that sponsor pays 30% below a standard tech (0.7x) instead of merely cancelling the old doubling. New colonies only (an existing save baked each tech's cost at colony start). In StS as Code/StS_TerraformingTechCost.lua. | field costs armstrong/Presets/TechFieldPreset.lua vs the standard curve Data/TechFieldPreset.lua:5-28, read once per colony by Research:InitResearch (Lua/Research.lua:153-166); the sponsor's Effect_TechBoost {Field="Terraforming"} applied via BoostTechField and combined as MulDivRound(cost, 100 - boost, 100) in Research:TechCost (Lua/Research.lua:356-382,400), field boosts summed and capped at 80% (:412) |
| Harsher disasters | Tested v2.9 | Dust Storms and Cold Waves escalate over the game, each rising linearly from 10% of sols active on day 1 to 50% on day 201 (20% by day 51, 30% by day 101), then capped. Intent: start around High and reach roughly Extremely High by day 51, then climb past any vanilla level - the numbers are kept round (10-50%) for readability, so they do not match vanilla exactly. From day 100 the two can run at the same time. Overrides the landing site's rating so every run escalates the same. Storm type is pinned to a third each (plain / Great / Electrostatic), ~67% dangerous. The build drives the linear duty-cycle curve above (see notes). Full matrix: disaster difficulty. | disaster data (see disasters.html); Lua/MapSettings.lua, Lua/DustStorm.lua, Lua/ColdWave.lua |
| Meteor Storms scatter map-wide | Tested v2.14 | A regular (weather) Meteor Storm drops its meteors on independent random passable tiles across the whole map instead of concentrating them in one drifting ~1 km capsule. Vanilla covers only ~13% of the map in a single stripe, so a storm either misses the base or hammers one spot of it; this keeps the same meteor count and cadence (~58 per sol) but spreads the barrage map-wide, so most storms clip the colony in rough proportion to its footprint rather than wiping one area. Scripted story/mystery strikes (the "Meteor Storm on your colony" punishment, the Bomb, the Last War bombardment) are left aimed at their dome. In StS as Code/StS_MeteorScatter.lua; base mechanic and coverage math in meteors.html; difficulty scale in disaster difficulty. | Lua/Meteors.lua:88-106 (SpawnMeteor: no pos uses GetRandomPassable, uniform whole-map, Lua/Pathfinding.lua:1-6); ambient storm call has no pos (:304) vs scripted strike passing a dome pos (Lua/ClassDefs/ClassDef-Effects.generated.lua:2504); thread keying via CurrentThread() (Lua/Buildings/BaseBuilding.lua:400) |
| Meteor and Dust Devil escalation | Tested v2.9 | The mod escalates Meteors and Dust Devils over the game to fixed abstract targets, scaling linearly from day 1 to day 201 then held: Meteors 0.1% to 1% of the map struck per sol and Dust Devils 0.1% to 1% of the map covered (both 0.325% by day 51, 0.55% by day 101). These are abstractions, not game settings - see Disaster abstractions for how they are translated into the spawn parameters that produce them (storm-driven meteors; devil wave interval). Alongside the amount, every meteor is pinned large and every devil Major and Electric. Sits alongside the live Meteor scatter and the Harsher disasters (Dust / Cold) curve. In StS as Code/StS_MeteorDevilEscalation.lua. Full matrix: disasters.html. | Meteors storm_spawntime / spawntime / multispawn_chance (Data/MapSettings_Meteor.lua, Lua/Meteors.lua); Dust Devils spawntime / count_min/max / spawn_chance / major_chance (Data/MapSettings_DustDevils.lua, Lua/DustDevils.lua); driven per sol like Code/StS_HarsherDisasters.lua |
| Dust Devils during Dust Storms | Built, untested (v1.9) | In the base game Dust Devils cannot exist while a Dust Storm is up - the wave scheduler parks and a starting storm deletes every live devil. The mod lets the two coexist (as it already does for Dust Storm + Cold Wave), so the escalating devil coverage is not throttled by the mod's frequent storms. Done additively, without replacing the base scheduler or removing the deletion: on storm start a mod handler respawns and then maintains the day's target devil population until the storm clears. In StS as Code/StS_DevilsInStorms.lua (see notes). | base pause on HasDustStorm (Lua/DustDevils.lua:192,203, markers :154); storm-start wipe OnMsg.DustStorm (Lua/DustDevils.lua:472) fired by StartDustStorm (Lua/DustStorm.lua:142); spawn via GenerateDustDevilIn |
| Cold ground rewards | Built, untested (v2.7) | Turns cold ground (surface heat below 210, the vanilla cold-penalty threshold) from a pure penalty into a tradeoff: Solar Panels and Wind Turbines on it produce +20% power, research buildings in a dome on it produce +20% research, and every building on it accumulates 20% less dust (passive, storms and devils). Re-checked hourly so it follows cold waves, heaters and terraforming; the vanilla cold penalty is left in place. Full logic and hooks: Cold ground page. In StS as Code/StS_ColdGround.lua. | see the Cold ground page Sources; key hooks Lua/Buildings/ColdSensitive.lua:40-42, Lua/Heat.lua:165, Lua/RequiresMaintenance.lua:178 |
| Earthsick from Morale | Built, untested (v2.96) | Earthsick no longer comes from Comfort hitting 0. Instead each sol a colonist below Morale 50 has a chance to turn Earthsick - chance = 100 - 2 × Morale percent, certain at Morale 0 and none at 50+ - and recovers once Morale reaches 70. Comfort still matters, but only as one of the three stats that feed Morale. Immunities are unchanged (Martianborn, Refugee, the IMM sponsor, Iron Colonists), and each fresh case still removes 2 applicants. In StS as Code/StS_EarthsickChance.lua; full page: earthsick.html. | Colonist:ChangeComfort (Lua/Units/Colonist.lua:3256-3281) replaced to drop the Comfort trigger and clear; per-sol roll and recovery on NewDay (Lua/DayTime.lua:56) over UIColony:GetCityLabels("Colonist"); GetMorale (Lua/Units/Colonist.lua:3285-3287); warning text Lua/StatusEffects.lua:323 |
This mechanic is no longer in the mod. It was replaced by
research output decay (Code/StS_ResearchOutputDecay.lua):
instead of making techs more expensive, the mod now shrinks research output over time. The
detailed notes below describe the old cost ramp and are kept only until this section is
rewritten.
Each technology already researched in a field adds a flat +10% to the research-point
cost of every other not-yet-researched technology in that same field. It is
additive, not compounding: with N techs already researched in the field, an unresearched tech in
it costs base × (100 + 10×N) / 100 - so the 1st tech in a field is base cost,
the 2nd +10%, the 3rd +20%, the 4th +30%, and on. Fields are independent: researching in Physics never
changes a Biotech cost. Intent: an incentive to skip the techs you do not need, field by field.
Implemented as
Code/StS_ResearchCostRamp.lua. No DLC
required.
Research:TechCost(tech_id) (Lua/Research.lua:356) is the only function that
returns a tech's point cost. It is what the colony pays - the research-completion check reads it
(AddResearchPoints, Lua/Research.lua:686; also the cheapest-tech pick at
:654 and the queue-cost preview at :395) - and what the player sees:
the "Research cost" line in a tech's rollover flows GetResearchInfo
(Lua/Research.lua:751) -> TechPreset:Getcost
(Lua/ClassDefs/ClassDef-PresetDefs.generated.lua:1140) -> the rollover text
(Lua/Research.lua:1351). Wrapping this one function makes the ramp both real and visible
with no second edit.
For the priced tech's field (its own status.field) the wrapper counts the
other techs actually researched in that field: it walks the field's tech list, skips the
priced tech itself, and counts only those whose status.researched is greater than
0. Both rules are necessary, and together they make the tech-select rollover and the research-queue
line show the same number:
ResearchQueueCost
(Lua/Research.lua:384-398), which temporarily writes the priced tech's own
status.researched before calling TechCost (its way of pricing a repeatable
tech queued more than once). Counting the priced tech would then read that temporary value and add a
bogus +10% to the very first queued tech, so the queue read 1,100 while the rollover read 1,000.
Excluding the priced tech makes both paths agree.0 is true, and the
temporary value ResearchQueueCost writes for an unresearched tech is 0. The
game's own Research:IsTechResearched returns that 0 (truthy), so counting by
truthiness would miscount an unresearched tech as researched. Testing researched > 0
explicitly avoids that.status.researched is a count
that is >= 1 once researched, so a repeatable tech is counted once from its first
research and its repeats never add; and being self-excluded, it never ramps its own repeat cost.Effect_GrantTech bonus, the mod's per-sponsor starting tech, and the
picked commander's tech1..5 (e.g. City Mayor's Mars Nouveau, Prospector's Deep Water
Extraction) and Effect_GrantTech (e.g. Deep Scanning) - is excluded from the count,
so it never raises its field's cost: it is completely free, and the other techs in its field do not get
more expensive because of it. It has to be tracked explicitly, because SetTechResearched
records nothing that tells a free grant from paid research (both set status.researched = 1).
Each grant site records the tech id into a per-colony, save-persisted set
(StS_FreeGrantedTechs) - the sponsor techs on the StS_SponsorChosen event
(StS_SponsorStartingTech), the commander
techs when the profile is applied
(StS_CommanderChoice), both re-marked on
load - and the per-field count skips any tech in that set. (Before v3 a free tech counted like any
researched one and raised the rest of its field +10% from colony start.)"Breakthroughs"
(Lua/Research.lua:299; Data/TechFieldPreset.lua:203, a non-discoverable
field) and is listed into tech_field["Breakthroughs"] at InitResearch, so
each researched breakthrough raises every other breakthrough by +10%. Mysteries and Storybits are
their own separate groups (Data/TechFieldPreset.lua:211,220) and ramp only within
themselves.One hook. At ClassesBuilt the mod wraps TechCost on
g_Classes.Colony - the class UIColony is actually instantiated from
(Colony:new(), Lua/Colony.lua:99) - not on the Research
parent it is inherited from. HGE flattens inherited methods into the subclass at build time, so
overriding the parent after ClassesBuilt does not reach Colony's
already-flattened copy; this is the same log-confirmed reason
StS_MysteryChoice overrides
g_Classes.Colony.SelectMystery rather than the Mysteries parent. The wrapper
runs the original, reads the field's count of OTHER researched techs (priced tech excluded,
researched > 0, free sponsor/commander techs excluded), and returns MulDivRound(cost, 100 + 10×N, 100)
(integer fixed-point, this is an integer-only Lua build). Because the result is a multiplier, the true original is stashed once on the class table and the
wrapper is always rebuilt from that stash, so the re-firing ClassesBuilt never double-wraps
or compounds. No cap: with many techs in a field the last few get expensive, which is the point.
Tested. In-game on a fresh colony every field's first tech reads its base cost, identical in
the tech-select rollover and the research queue (the earlier select-vs-queue +10% mismatch is gone), and
the ramp is per field. Verified from the colony-start dump (base = select = queue in every
field at zero researched) and confirmed in play.
The mod's disaster difficulty is written as abstract target numbers, one per threat, chosen to be human-comparable (the matrix on the Disasters page):
Code/StS_HarsherDisasters.lua.For whoever implements the Meteor and Dust Devil scaling (LLM or human): these target numbers are not game settings and cannot be written into a preset directly - the game has no "meteors per sol" or "percent of map covered" field. You must translate each target into the spawn parameters that produce it, running the wiki's derivations in reverse:
storm_spawntime for the storm part, spawntime /
multispawn_chance for the individual part. Derivation:
meteors.html, disasters.html.spawntime,
count_min/max, spawn_chance (and major_chance for the ring
mix). Derivation: dust-devils.html#coverage.Always verify the implemented spawn settings reproduce the target number (in-game or by the formula), not just that the code runs. The abstraction is the spec; the preset values are the means.
Dust Storms and Cold Waves escalate over the whole game and, from sol 100, can run at the same time. The mod drives each of the two to a fixed duty-cycle curve - the fraction of the time that disaster is active on the map - and overrides whatever Dust Storm / Cold Wave rating the landing site rolled, so every run follows the same escalation. Built in v1.9 and awaiting its first in-game test - Code/StS_HarsherDisasters.lua. No DLC required (both are base-game disasters). The base mechanics and the numbers this builds on are in disasters.html.
Each disaster is active on a plain linear ramp: 10% of sols on day 1 to 50% on day 201
(20% at day 51, 30% at day 101), overlap unlocking at day 100. (Meteors and Dust Devils escalate
on the same 0.1% -> 1% curve, in
disaster abstractions /
StS_MeteorDevilEscalation.lua.) The curve is the
matrix; it is absolute, replacing the site's rolled
intensity entirely.
Duty cycle is the share of the time one disaster is active: d = length / (length + gap).
Dust Storm and Cold Wave each target the same d independently. In the base game the two
never overlap and are not frequent or long enough to fill much of the time (see
how much of the time a storm is running and
how the two are kept apart); the mod raises each
toward half the sols and lets them overlap past day 100 (below).
Each disaster's duty rises on a fixed linear curve, the same for every landing site:
| Day | % of sols active |
|---|---|
| 1 | 10% |
| 51 | 20% |
| 101 | 30% |
| 201 and after | 50% (cap) |
To climb the curve the mod scales each storm's length up and the gap between storms down together
(an equal split, so "longer" and "more frequent" contribute the same): the target ratio
r = length/gap = d/(1 - d), and each storm is scaled from its day-1 baseline (Dust Storm
30 h / 180 h, Cold Wave 60 h / 360 h, both r0 = 1/6) by f = sqrt(r / r0) -
length len0 * f, gap gap0 / f - with a +/-25% random band. The resulting
averages:
| Day | % active | Dust Storm (length / gap) | Cold Wave (length / gap) |
|---|---|---|---|
| 1 | 10% | ~24 h / ~9.2 sols | ~49 h / ~18.4 sols |
| 51 | 20% | ~37 h / ~6.1 sols | ~74 h / ~12.3 sols |
| 101 | 30% | ~48 h / ~4.7 sols | ~96 h / ~9.4 sols |
| 201 | 50% | ~74 h / ~3.1 sols | ~147 h / ~6.1 sols |
There is no length step-down: each disaster's duty is independent, so overlap unlocking at day 100 (below) changes only whether the two may co-exist, not the length/gap math.
The base game allows only one weather disaster at a time through the shared
IsDisasterActive() check in each scheduler (see
disasters.html). From sol 100 the mod wraps
that check so a lone Dust Storm or Cold Wave no longer reports the map as busy to the
other, letting the two run together; a Mystery Dream or Rain disaster still blocks, as
before. Every disaster effect - freezing, dust, solar penalty, grounded rockets,
stalled construction - reads HasColdWave / HasDustStorm
directly, not IsDisasterActive, so both storms keep full effect when they
overlap; only the screen tint shows one at a time.
Each Dust Storm rolls plain, great (double dust) or electrostatic (lightning) at
about a third each, replacing the base game's low great / electrostatic chances. Set in
the storm-type roll (Lua/DustStorm.lua:376-385) by raising both chances;
the engine's 0-100 roll makes it 33.7 / 32.7 / 33.7.
The Weather map condition re-weights the 50/50 split between
the two disasters: a Dust:Cold prevalence edge of +/-33% (dust / even / cold). Inside the same
OverrideDisasterDescriptor wrap, each disaster's target duty is multiplied by its share
(dust: Dust x1.33, Cold x0.67; cold: the reverse; even: x1). The two shares always sum to 2, so the
combined weather load stays exactly on the curve - only the mix shifts. Read from
StS_GetMapCondition("weather"); it replaces the earlier duration-bias slot (the retired
STS_WEATHER_LENGTH_BONUS_PCT / StS_GetMapModifier).
Implemented as Code/StS_HarsherDisasters.lua (v1.9, first in-game test pending). Integer
fixed-point throughout - this is an integer-only Lua build, so the curve math uses MulDivRound
and the integer sqrt, no floats. Four parts, all on data the schedulers already read:
d(sol) (linear 100 -> 500), then
r = d/(1-d), f = sqrt(r/r0) with r0 = 1/6, length
len0*f and gap gap0/f, each emitted as a +/-25% min/max band. The weather
condition scales each disaster's d by its prevalence share first.CityStart/LoadGame
point ActiveMaps[MainMapID].MapSettings_DustStorm/_ColdWave at a
non-forbidden preset so both always run, restarting the scheduler
GlobalGameTimeThread if the site had that threat off (the dead-thread
restart pattern from Lua/Disasters.lua:56-72).OverrideDisasterDescriptor (Lua/MapSettings.lua:130),
its own extension hook, once per storm cycle. The mod wraps it to write the current
sol's length (min_duration / max_duration) and gap
(spawntime / spawntime_random) plus the 34/33 storm-type
chances onto the descriptor. Absolute values every call, so no compounding.IsDisasterActive
(Lua/MapSettings.lua:181): nothing active -> false; before the overlap sol
-> vanilla; Mystery Dream / Rain up -> still blocks; else (a lone Dust Storm and/or Cold
Wave) -> report not-busy, so the two stop blocking each other.Test switches in the file: STS_DEBUG (extra logging plus a one-shot forced
overlap at sol 6, via the schedulers' own trigger messages so no "already present" assert)
and STS_OVERLAP_SOL (lower it, e.g. to 5, to reach overlap without waiting).
Both ship at release values (false / 100); turning them on prints
the per-sol numbers and forces a Dust Storm + Cold Wave overlap at sol 6, so the
escalation can be checked without a 100-sol wait.
The base game never lets a Dust Devil exist while a Dust Storm is on the map: the devil
wave scheduler parks whenever HasDustStorm is true
(Lua/DustDevils.lua:192,203, and the fixed-spot markers at :154), and a
starting storm deletes every live devil through OnMsg.DustStorm
(Lua/DustDevils.lua:472, fired by StartDustStorm's
Msg("DustStorm"), Lua/DustStorm.lua:142). Because the mod drives Dust
Storms up to 50% of sols by day 201, that suppression would keep realized Dust Devil coverage far
below the escalation target, so the mod lets the two coexist - the same idea as the Dust Storm +
Cold Wave overlap above. Built in v1.9, awaiting its first in-game test -
Code/StS_DevilsInStorms.lua.
Why not the storm / cold-wave trick. That overlap was unlocked by wrapping one function,
IsDisasterActive, which only the two schedulers read while the actual effects read
HasDustStorm / HasColdWave directly. Dust Devils read
HasDustStorm directly, and that function is shared with every storm effect, so it
cannot be wrapped globally - and there is an active per-storm deletion to fight on top. So the same
one-line trick does not apply.
The approach: respawn and maintain, fully additive. Rather than replace the base scheduler or remove the deletion handler (both need engine-internal reach), the mod leaves the base untouched and fills only the storm window:
OnMsg.DustStorm
handler starts a game-time thread. The thread sleeps a tick first, so it is guaranteed to run
after the base deletion regardless of handler order, then seeds the day's target
population and keeps topping it back up until HasDustStorm goes false.How many to spawn. Not a live count - the base wipe runs before the mod handler, so the
pre-storm number is already gone by the time we could read it, and it fluctuates anyway. Instead the
target is the average devils-present N the escalation already aims for, derived from the live
descriptor (count per wave x lifetime / wave interval, matching coverage = N x dust-ring area /
map area). Because GenerateDustDevilIn always bakes a full fresh trajectory (no public way to
seed a half-spent devil), the thread staggers its spawns by the descriptor's spawn delay so their deaths
desync into the steady-state spread rather than a synchronized cohort. Minions add almost no area
(dust-devils.html), so with the storm suppression
removed the coverage target maps cleanly onto spawn rate x lifetime x ring area.
All through public calls the base game already uses - GenerateDustDevilIn,
HasDustStorm, GetRandomPassableAwayFromBuilding - with no
GlobalGameTimeThreadFuncs swap, no OnMsg removal and no bytecode probe. The base
wipe is a one-shot at storm start (OnMsg.DustStorm, Lua/DustDevils.lua:472), so a
devil spawned after that Msg("DustStorm") is not wiped and lives out the storm.
Base game, paths relative to external/SurvivingMars; Space Race in
gagarin.hpk (decompiled). Disaster mechanics:
disasters.html.