Skip to content

StationpediaSearch

The Stationpedia search pipeline: from the search field's onValueChanged callback through DoSearch's async UniTask into StationpediaPage.IsRegexMatch. This page documents the trigger chain, the async state-machine split (and why a Postfix on DoSearch cannot filter results), and the IsRegexMatch single-caller fact that makes it the correct filter target.

Search mechanism

Stationpedia.DoSearch(string hash, string pattern, CancellationTokenSource) at game.cs:230584 is the ONLY path that iterates StationpediaPages for display (search results). Driven by SearchField.onValueChangedSearchBehaviourStartSearchCountdownClearAndStartSearchForceSearchDoSearch.

DoSearch calls StationpediaPage.IsRegexMatch(hash, pattern) per page at game.cs:230602. This two-argument overload has exactly ONE caller in the entire game assembly (DoSearch). It returns bool indicating a match.

StationpediaPage.IsRegexMatch(string text, string pattern) body at game.cs:233709:

public bool IsRegexMatch(string text, string pattern)
{
    if (text == PrefabHashString) return true;
    if (!Match(pattern, Title) && !Match(pattern, Key))
        return Match(pattern, Description);
    return true;
}

Truncates at 255 chars for regex matching.

Separate HelpReference.IsRegexMatch(string pattern) at game.cs:221825 is unrelated (IC10 help panel). Do NOT patch the one-arg overload.

Why we patch IsRegexMatch and not DoSearch directly: DoSearch is private async UniTask, meaning Harmony patches the state-machine kick-off method, the patch receives the UniTask return before iteration has run. A Postfix on DoSearch can't filter results because the results haven't been materialized at postfix time. IsRegexMatch, by contrast, is a regular synchronous method called from inside the async state machine's MoveNext, and Harmony-patched synchronous methods called from inside an async body ARE redirected correctly. This is why IsRegexMatch is the right lever. Full body of DoSearch (game.cs:230584-230614):

private async UniTask DoSearch(string hash, string pattern, CancellationTokenSource cancelToken)
{
    if (string.IsNullOrEmpty(pattern) && string.IsNullOrEmpty(hash))
    {
        ClearPreviousSearch();
        NoResultsFromSearchText.SetActive(value: true);
        return;
    }
    NoResultsFromSearchText.SetActive(value: false);
    await UniTask.SwitchToThreadPool();
    int count = 0;
    int i = StationpediaPages.Count - 1;
    while (i >= 0 && count < searchResultsPerPage)
    {
        if (cancelToken.IsCancellationRequested) return;
        if (!string.IsNullOrEmpty(StationpediaPages[i].Title)
            && !StationpediaPages[i].Title.Equals("Search")
            && StationpediaPages[i].IsRegexMatch(hash, pattern))
        {
            StationpediaPage page = _linkIdLookup[StationpediaPages[i].Key];
            SPDAListItem insert = _SPDASearchInserts[count];
            MakePage(page, insert).Forget();
            await UniTask.Delay(20, DelayType.UnscaledDeltaTime);
            count++;
        }
        i--;
    }
    await UniTask.SwitchToMainThread();
    NoResultsFromSearchText.SetActive(count == 0);
}

Iterates StationpediaPages in reverse, up to searchResultsPerPage (100) results. Note it mutates pre-pooled _SPDASearchInserts in-flight rather than returning a list; patch targets that need to filter the final list would need to trace into MakePage or patch _SPDASearchInserts population, both hairier than patching IsRegexMatch.

Search trigger chain (onValueChanged -> DoSearch)

Chain: onValueChanged -> SearchBehaviour(text) -> if non-empty, SetPage("Search") + StartSearchCountdown (400ms debounce) -> WaitStartSearch -> ClearAndStartSearch(text) -> ClearPreviousSearch + ForceSearch -> builds regex pattern -> DoSearch(hash, pattern, cancelToken) -> iterates StationpediaPages in reverse, calls IsRegexMatch, MakePage for each hit. Enter-key / submit triggers the same chain via StartSearchNow (zero-delay variant).

DoSearch is async UniTask (cannot postfix directly)

Async methods that invoke Harmony-patched code cannot be filtered by patching the async method itself: the compiler's state-machine rewrite leaves only a kick-off stub under the original name, and a Postfix on that stub fires before the body runs. See ../Patterns/AsyncHarmonyTrap.md for the generalized pattern and fix. For Stationpedia search specifically: DoSearch (game.cs:230584) is async UniTask and mutates the pooled _SPDASearchInserts list in-flight, so there is no __result to edit. The mod patches StationpediaPage.IsRegexMatch instead, which is synchronous, returns bool, and has exactly one caller (DoSearch). A Postfix with ref bool __result = false on IsRegexMatch short-circuits the downstream MakePage call.

Verification history

  • 2026-04-20: page created from the Research migration; verbatim content lifted from F0208 (primary), F0219f, and F0219g.
  • 2026-04-20: F0219g section replaced with pointer to Patterns/AsyncHarmonyTrap.md per Phase 6 Pass B recommendation.

Open questions

None at creation.