You are viewing a potentially older version of this package. View all versions.
Videowiz92-LoopLagLimiter-1.2.0 icon

LoopLagLimiter

Fixes late-loop lag: caps runaway Meat Hook pulls, dead orbs, post-teleporter waves and the physics catch-up spiral. Optional proc limits (covers modded item tiers) and a built-in lag profiler.

Date uploaded a day ago
Version 1.2.0
Download link Videowiz92-LoopLagLimiter-1.2.0.zip
Downloads 118
Dependency string Videowiz92-LoopLagLimiter-1.2.0

This mod requires the following mods to function

bbepis-BepInExPack-5.4.2122 icon
bbepis-BepInExPack

Unified BepInEx all-in-one modding pack - plugin framework, detour library

Preferred version: 5.4.2122

README

LoopLagLimiter

Performance fixes for long, heavily looped Risk of Rain 2 runs, the kind where the game sinks to one frame every few seconds once the teleporter bosses spawn.

Every fix targets a specific runaway system found by profiling a real stage-45 run. The defaults only kick in at extreme scale. A normal run never comes near the projectile and orb caps, and the rest only removes work that can't change the outcome. Everything can be changed in-game with Risk of Options (optional; hover a setting to read its description) or in BepInEx/config/com.Videowiz92.LoopLagLimiter.cfg.

Results from the run it was built on: a Halcyon Shrine fight that froze the game, and teleporter fights running at one frame every 20 seconds, became playable. The worst frame in the same fight went from 24 s to under 3 s.

At a glance

On by default What it stops
Physics catch-up cap The lag spiral where each slow frame makes the next one slower
Meat Hook pull cap Hundreds of stacked pull effects searching for targets every tick
Orbs with dead targets Orbs flying forever at enemies that already died
Side director credit cap Endless enemy waves after the teleporter and Halcyon Shrine
Projectile cap Tens of thousands of Sticky Bombs, fireballs, daggers... alive at once
Orb cap Thousands of Polylute, Meat Hook, Noxious Thorn... orbs in flight at once
Total projectile cap Over a thousand projectiles alive even with every kind under its own cap
Projectile fire-rate cap Items creating and destroying thousands of projectiles per frame (Ghost Blades)
Money & price overflow guards Gold wrapping to near zero and prices turning negative deep into a loop
Heavy-load mode (below 15 FPS only) Hundreds of gold nuggets, healing orbs, ammo packs and items piling up on the ground while the game is already overloaded
Gasoline chain guard A stack-overflow crash that freezes the game completely
Visual effect cap Thousands of explosions, lightning strikes and sparks spawned every second
Exception spam guards Errors that cancel Resonance Disc beams and flood the log
Off by default
Per-item proc limits "At most N per second" for specific items
Diagnostics Find out what is lagging your run

How each mechanic works

Physics catch-up cap

Setting: General → Max catch-up per frame (seconds), default 0.1

Risk of Rain 2 runs its physics and combat at a fixed 60 ticks per second. When a frame takes too long, Unity runs extra ticks to catch up before drawing the next frame. Those extra ticks make that frame slow too, so it falls further behind: a lag spiral. In testing, every bad frame was running 20 ticks.

This caps how much catch-up one frame may do. At 0.1 s (about 6 ticks), the game plays in slow motion instead of freezing when it can't keep up, but only below roughly 10 FPS; normal play is unaffected. 0 means one tick per frame (always slow-motion below 60 FPS); -1 leaves the game's default alone. Same idea as NoFrameSkip, so don't run both.

Meat Hook pull cap

Setting: Limits → Max pull effects per prefab, default 25

Every Sentient Meat Hook proc leaves behind a short-lived pull effect that drags nearby enemies in. At high proc rates these appear faster than they expire. Test runs had 300–800 at once, each searching for targets every physics tick, costing up to 1.1 s per frame. This keeps the newest 25 of each kind and removes the oldest first. Hook damage is not affected, only how hard enemies get pulled. Applies to any free-standing pull effect, not just Meat Hook's; anything that is or belongs to a character, ally, minion or deployable is never removed.

Orbs with dead targets

Setting: Limits → Drop orbs whose target died, default on

Many items fire "orbs": Polylute, Meat Hook, Noxious Thorn vines, Ukulele lightning, healing orbs. An orb flies to its target and then does its effect. If the target dies first, some orbs keep updating anyway, and some even outlived the stage. Since they can never hit anything, they are removed. Nothing you'd notice is lost.

Side director credit cap

Settings: Limits → Max camp director credits (3,000), Camp director names, Also cap Halcyon Shrine wave credits (on)

Enemies are spawned by "directors" that spend monster credits. When the teleporter finishes, its leftover credits are handed to the stage's camp director, and the Halcyon Shrine has a wave director of its own. Deep into a loop those leftovers are enormous (525,000 and 650,000+ credits in testing), so they spawn wave after wave after the fight is over. Each kill then triggers your on-kill items, which is what made those waves so laggy. These side directors are clamped to 3,000 credits. The main stage director and the teleporter boss fight are never touched.

Projectile cap

Setting: Limits → Max alive projectiles per kind, default 500

Proc items create projectiles: Sticky Bombs, Molten Perforator fireballs, Ceremonial Daggers, missiles, fireworks... At high stacks nearly every hit creates one, and hits come from everything else, so they pile up faster than they expire. One Halcyon Shrine fight had 10,577 Sticky Bombs and 4,264 fireballs alive at once, costing about 14 s of a 24 s frame plus physics and network sync for every one of them.

Once 500 projectiles of one kind exist, new ones of that kind aren't fired until some expire. It works for every item, vanilla or modded, without needing a list. Projectiles that create a character (allies, minions, clones such as Goobo Jr.'s) are never blocked; a projectile that is itself a deployable, like Ghost Blades, is not an ally and stays limited. The cost is some damage at extreme scale, where everything dies instantly anyway. Host only.

Total projectile cap

Setting: Limits → Max alive projectiles total, default 1000

The per-kind cap stops any one item from running away, but several items can each sit just under it. One fight had 494 fireballs, 470 Sticky Bombs and 431 daggers: 1,437 projectiles in all, with the physics, network sync and explosions to match.

Once 1,000 projectiles of all kinds exist together, only the big kinds, those with more than a tenth of the total (100+) alive, stop firing until the total drops back under 1,000. Kinds with fewer keep working, so a pile of Sticky Bombs never blocks a survivor's skill, a rare item or an enemy's attack. The same ally exemption applies. Host only.

Orb cap

Setting: Limits → Max alive orbs per kind, default 300

The same idea for orbs. Once 300 orbs of one kind are in flight, new ones of that kind wait until some land. Testing saw 1,500 Polylute orbs in flight at once, and Meat Hook and Noxious Thorn orbs hit the cap thousands of times per fight. Host only.

Projectile fire-rate cap

Setting: Limits → Max projectiles fired per kind per tick, default 30

Some projectiles are limited by the game to a few alive at once. Ghost Blades, for example, keep at most 30, and each new one destroys the oldest. A big enough build fires them so fast that thousands are created and destroyed every frame: 4,960 in a single frame in testing, which froze the game for 33 seconds, almost all of it spent creating and deleting objects. The alive cap can't catch this, because only 30 ever exist. This caps how many projectiles of one kind may be fired per physics tick (60 per second). It covers every item, and projectiles that spawn allies are never limited. Host only.

Money and price overflow guards

Settings: Limits → Prevent money overflow and Prevent price overflow, both on

Money is stored as an unsigned 32-bit number. Deep into a loop, per-kill gold pushes it past about 4.29 billion, where it wraps back to almost nothing; the HUD also shows anything above about 2.15 billion as negative. Gains now stop at 2,147,483,647, the highest value the HUD shows correctly.

Prices have the same problem. Chest, shrine and shop prices are base cost times a difficulty multiplier, and deep into a loop the result no longer fits and becomes a negative price (−2,147,483,648). Overflowed prices become 2,147,483,647 instead, the same number money stops at, so once your money reaches the maximum you can still buy anything. Normal prices are untouched.

Both guards rely on the small preloader patcher that ships with the mod (LoopLagLimiter.Patcher.dll, installed to BepInEx/patchers). It stops the game from inlining the tiny money and price methods, which would otherwise skip the guards. Money is also clamped in its setter and by a periodic check, so changes made by other mods are covered too.

Neither is a lag fix, just loop-scale bugs. Enemy health and damage are floating-point numbers, so they don't wrap like this. Host only.

Heavy-load mode: physics pickups and item drop queue

Settings: Heavy load → Heavy load below FPS (15), Pause gold nuggets (on), Pause other physics pickups (on), Clear pickups already on the ground (on), Pause Sky Meadow falling rocks (on), Rock ramp-up after heavy load (60 s), Max items on the ground (50), Queued items released per second (2), Warn before leaving with queued items (on), Warning distance (30 m)

These only act while the game is under heavy load: the average frame rate has stayed below 15 FPS for 2 seconds. It ends once FPS is back above 20 for 3 seconds. Normal runs never get there, so normal gameplay is untouched.

  • Gold nuggets (Ghor's Tome bonus money packs) are not spawned under heavy load. By then enemies die instantly, and testing saw 255 nuggets on the ground at once, each a physics object with sound. They are also skipped whenever every player's money is already at the maximum (see the money guard), because they couldn't give anything.
  • Other physics pickups are not spawned under heavy load either: Monster Tooth healing orbs, Bandolier ammo packs, buff pickups, and modded variants such as ItemQualities' quality Monster Tooth orbs. Testing saw 108 healing orbs and 73 ammo packs alive at once. There is no list of names: anything with a pickup component (healing, ammo, buff, money, or one that flies to players) counts, so new modded ones are covered too. Real items, item droplets, command/choice pickups and anything that is a character are never touched.
  • Pickups already on the ground are cleared. The two settings above stop new ones from spawning; this removes the ones that were already lying around when heavy load started, then checks again every second while it lasts. Only the kinds you have paused are removed, and real items are never removed.
  • Sky Meadow falling rocks stop falling under heavy load. The stage fires them for as long as you stay, and they take hits like enemies, so every proc chain lands on them as well. Rocks already falling are left alone. When heavy load ends, the rate comes back gradually: none at first, half after 30 seconds, normal after 60. If heavy load returns, they stop again.
  • Item drops queue. Once 50 items are on the ground or falling, new drops wait in a queue instead of piling up. Each time the count drops below 50 (someone picks an item up), the next queued item drops where it would have. Queued items keep the spacing they were dropped with: if the teleporter spaces boss items half a second apart, they come out half a second apart, and that spacing is paused while the ground is full. So grabbing a pile of items lets the next ones fall one by one instead of all at once. When heavy load ends, the queue empties at the same spacing, never faster than 2 items per second. Queued items left when the stage ends are lost, just like items left on the ground, so while items are still queued, a big red chat warning appears when a player gets near a fully charged teleporter or a portal (at most every 15 seconds), and once as soon as the teleporter finishes charging. Everyone sees it, mod or not.

Items that spawn in some other way (chests, shrines, printers) are not affected. Host only.

Gasoline chain guard

Settings: Limits → Max Gasoline chain depth (20), Max queued Gasoline explosions per tick (200)

Gasoline's explosion happens inside the death that triggered it. If the explosion kills another enemy, that death runs Gasoline again, one level deeper, and so on. In a dense pack of low-health enemies (a Halcyon Shrine wave, say) the chain goes hundreds of levels deep until the game runs out of stack space and freezes completely (StackOverflowException).

Past 20 levels, the next explosion is put in a queue and fires on the next physics tick, starting a fresh chain. Every explosion still happens, just spread over a few ticks. Host only.

Exception spam guards

Settings: Fixes → Suppress known exception spam (on), Methods to guard

Two game methods throw errors over and over in late runs. The worst is Resonance Disc beams hitting a blocking shield: the error cancels the entire beam, and its full stack trace is written to the log (console, BepInEx GUI and disk) every time. The error is caught, the beam treats the shield as "not blocking" and carries on, and only the first occurrence is logged.

Per-item proc limits (off by default)

Settings: Proc limits → one "per second" slider per item, plus Extra limits

This is the finer-grained option. Where the caps above limit how many of something exist at once, these limit how often a specific item may fire: e.g. "at most 30 Sticky Bombs per second". Covered: Sentient Meat Hook, Polylute, Ukulele, Plasma Shrimp, Charged Perforator, AtG Missile, Sticky Bomb, Molten Perforator, Ceremonial Dagger and Bundle of Fireworks (Pocket I.C.B.M. multiplies missiles and fireworks). The limit applies to what the proc creates, so modded copies of an item (like ItemQualities' quality tiers) count toward the same limit. Extra limits takes any other orb or projectile as Name=N;Name=N; diagnostics reports show the names. Fewer procs means less damage.

Visual effect cap

Setting: Limits → Visual effects per tick limit, default 75

Every proc spawns a visual effect (an explosion, a lightning strike, a spark), and each one costs time to create, play its sound and draw. One frame had 2,932 of them, with Charged Perforator strikes alone costing about 0.3 ms each. At most 75 effects are spawned per physics tick; the rest are skipped. It is purely visual: damage, procs and hit detection are unaffected. In testing it cut the total time spent in stalled frames from 26 s to under 8 s over the same fight.

1.1.0 had this as "Max visual effects per tick", off by default. 1.2.0 replaces it with a new setting so everyone gets 75; if you had set the old one to a number yourself, that number is kept. 0 turns it off.

Diagnostics (off by default)

Settings: Diagnostics → Enable diagnostics (restart required), keys F9 / F10

When on, every frame slower than 250 ms writes a report to BepInEx/LoopLagLimiter/ (and the log). It shows time spent in damage, procs, orbs, projectiles, effects, stat recalculation and directors, who triggered each, what exists at that moment (bodies, projectiles, orbs, physics objects, director credits), and how often each limit fired. F10 turns on deep profiling, which times every component's update in the game and every mod (a few seconds' hitch when toggled). F9 writes a report on demand. F9/F10 do nothing until diagnostics are enabled.

Compatibility

  • Doesn't reference the game's code directly, so a game update can only disable an individual fix (logged), not crash the mod.
  • Ships two DLLs: the plugin (BepInEx/plugins) and a tiny preloader patcher (BepInEx/patchers). r2modman installs both.
  • Most limits act on the host. Clients in multiplayer don't need special setup, and players without the mod can still join.
  • Don't use with NoFrameSkip. The catch-up cap does the same thing. Or set "Max catch-up per frame" to -1 to leave it to NoFrameSkip.
  • Works alongside ItemQualities; the caps and proc limits cover its quality items.
  • Allies are never touched. The only things ever removed are pull effects and orbs whose target died. Allies that disappear soon after spawning are the game's own limits (each ally type has a maximum count; the oldest is replaced).
  • Pausing doesn't remove lag that comes from how much is on screen. The game still has to draw everything while paused.
  • Updating keeps your existing settings. A new default only applies to settings you haven't saved yet.
  • If the game ever sticks on a white screen at boot with this mod, set Register Risk of Options page = false in section "0. Troubleshooting" of the config and report it on GitHub.

Credits

  • ProcLimiter by DrinkableWater (source): the idea of per-item proc cooldowns to fight late-game lag. ProcLimiter is deprecated and was built for game version 1.2.3. LoopLagLimiter's proc limits are a from-scratch reimplementation (no code reused) that works on current versions and covers modded item variants.
  • NoFrameSkip by McAwesome (source, MIT): the idea of capping Time.maximumDeltaTime to stop the physics catch-up spiral. Reimplemented here; please support the original.
  • Risk of Options by Rune580: in-game settings page.

Made by Videowiz92.

CHANGELOG

1.2.0

  • New: projectile fire-rate cap, on by default (30 per kind per physics tick, covers every item). Ghost Blades are kept at 30 alive by the game, so each new blade destroys the oldest; 4,960 were created and destroyed in one frame, freezing the game for 33 seconds. The alive cap couldn't catch that because only 30 ever existed.
  • Fix: the "never block ally-spawning projectiles" check also matched projectiles that are themselves deployables (Ghost Blades), which exempted them from every cap. It now only matches projectiles that create a character (ally, minion, clone), and the log says which component matched.
  • New: money overflow guard, on by default. Money stops at 2,147,483,647 instead of wrapping past ~4.29 billion back to near zero (and showing as negative on the HUD above ~2.15 billion).
  • Fix: the money guard never ran in-game, because the game inlines the tiny GiveMoney method into its callers and skips the hook. A new preloader patcher (BepInEx/patchers/LoopLagLimiter.Patcher.dll) marks GiveMoney, the money setter and the price methods as non-inlinable before the game loads. Money is now also clamped in its setter and by a periodic backstop.
  • New: heavy-load mode (only below 15 FPS, so normal runs are untouched): gold nuggets from Ghor's Tome are paused, and item drops queue once 50 items are on the ground, dropping one at a time as items are picked up. Gold nuggets are also skipped whenever every player's money is maxed. While items are queued, a big red chat warning appears when a player is near a charged teleporter or portal, so nobody leaves items behind. Other physics pickups (Monster Tooth healing orbs, Bandolier ammo packs, buff pickups and modded quality variants) are paused under heavy load too, with their own setting. When heavy load starts (and every second while it lasts), the nuggets and pickups already on the ground are cleared too.
  • Changed: the visual effect cap is now on by default at 75 per tick (purely visual; cut total stall time from 26 s to under 8 s in testing). It is a new setting, "Visual effects per tick limit", so 1.1.0 configs get 75 too; a non-zero value set under the old "Max visual effects per tick" is carried over.
  • Changed: queued item drops keep the spacing they were dropped with (e.g. the teleporter's delay between boss items), and that spacing pauses while the ground is full, so picking up a pile of items no longer releases the backlog all at once.
  • New: Sky Meadow falling rocks pause under heavy load, then ramp back from none to normal over 60 seconds once the game recovers.
  • Fix: queued item drops left over when a run ends are discarded quietly instead of logging an error for each one.
  • New: total projectile cap (Limits → Max alive projectiles total, 1000). Once that many projectiles exist together, kinds with 100+ alive stop firing until the total drops; smaller kinds (skills, rare items, enemy attacks) are unaffected.
  • Config sections renumbered to match the settings page order: 0. Troubleshooting, 1. General, 2. Limits, 3. Heavy load, 4. Fixes, 5. Proc limits, 6. Diagnostics. Settings saved under the old section names are carried over automatically.
  • New: price overflow guard, on by default. Prices that overflow into negatives (base cost times a huge loop difficulty multiplier) become 2,147,483,647, the same ceiling as money, so items stay buyable once money is maxed.

1.1.0

  • New: Gasoline chain guard (on by default). Gasoline's explosion runs inside the death that triggered it, so a dense pack of low-health enemies (e.g. a Halcyon Shrine wave deep into a loop) could chain deep enough to crash the game with a stack overflow, freezing it completely. Past 20 levels, the next explosion is queued for the next physics tick; every explosion still happens.
  • New: projectile cap, on by default (500 alive per kind, covers every item). A looped Halcyon Shrine fight had 10,577 Sticky Bombs alive at once. The old opt-in "Max alive projectiles per prefab" setting is replaced by "Max alive projectiles per kind".
  • New: orb cap, on by default (300 in flight per kind, covers every item). The same fight had 1,500 Polylute orbs in flight.
  • New: the credit cap now also covers the Halcyon Shrine's wave director (seen at 650,000+ credits).
  • Fix: projectile counting is now correct even if the game recycles projectile objects, and a burst fired within one tick can no longer overshoot the cap.
  • Safety: the pull cap only removes free-standing pull effects (never characters, allies, minions or deployables), and projectiles that spawn something (allies, minions, clones like Goobo Jr.'s) are never blocked by any cap or limit.
  • New: Bundle of Fireworks proc limit (off by default). Fireworks combined with Pocket I.C.B.M. can flood the stage with rockets.
  • README: new "How each mechanic works" section explaining every setting, its trade-off and default.
  • README: diagnostics keys (F9/F10) only work after turning on "Enable diagnostics" and restarting.

1.0.0

  • First release.
  • On by default: physics catch-up cap, Meat Hook pull cap, removal of orbs with dead targets, camp director credit cap, exception spam guards.
  • Optional: per-second proc limits, max alive projectiles per prefab, max visual effects per tick.
  • Diagnostics mode with stall reports and deep profiling (off by default).
  • Risk of Options settings page with icon (optional dependency).