Multiplayer icon indicating copy to clipboard operation
Multiplayer copied to clipboard

InvalidOperationException when ticking quests during async time due to modification during enumeration

Open Tick-git opened this issue 6 months ago • 0 comments

Label: Multifaction, Async, Bug

Bug in class: MultiplayerAsyncQuest System.InvalidOperationException: Collection was modified; enumeration operation may not execute.

Description

An InvalidOperationException is thrown during .TickQuests() because the underlying quest collection is modified during enumeration. This occurs when a quest ends during QuestTick(), triggering a Harmony postfix on Quest.End(). The postfix removes the quest from the cache via TryRemoveCachedQuest, which modifies the list currently being iterated.

Stacktrace.txt

Context

This issue occurred while testing #508. In that PR, ancient exoremains are spawned on maps for each faction. After destroying the remains and defeating the threat from the ship, players can extract a mecha link. Implanting the link triggers a hidden quest. Once the quest finishes, the exception is thrown. (Quest: ..\RimWorld\Data\Biotech\Defs\QuestScriptDefs\Script_StartingMech.xml)

While this was triggered by a specific Biotech quest, it’s likely that similar behavior can occur with other quests that complete during a tick.

Steps to Reproduce

  • Host a multiplayer game with async time, multifaction, and Biotech enabled.
  • (Optional) Use dev tools to give pawns strong gear and high shooting skills.
  • Destroy the ancient exorider remains.
  • Decrypt the mech transponder.
  • Accept the resulting quest.
  • Defeat the threat that arrives via ship.
  • Extract the mecha chip.
  • Implant the chip.
  • After a few ticks, a hidden quest finishes.
  • You receive the reward and the error is thrown

Potential fix

Modify the RemoveQuestFromCacheOnQuestEnd Harmony postfix on Quest.End() so that it adds the quest to a pendingRemovals list instead of removing it immediately. Then, after all quests are ticked in TickQuests, iterate over pendingRemovals and remove them safely.

Tick-git avatar Jun 23 '25 10:06 Tick-git