Skip to the text

A random generator carries state. Taking a value advances that state; seeding it replaces the state. When unrelated systems share a generator, their histories meet at the next draw.

There is more than one stream

The game-owned global generator, the library shuffle generator, and object-owned generators have separate state. The process also initializes another engine from random_device. Treating all of them as one sequence gives the wrong dependencies.

The global game generator starts with seed 4357. Its raw result has 31 bits. Bounded integers use modulo reduction, while the float helper scales a value and rounds it to float32.11The shared game generatorThe game-owned MT19937 starts at 4357, returns its low 31 bits, uses modulo for bounded integers, and divides floating draws by 2147483647.0 before float32 conversion. A branch census finds 38 calls to the global seed wrapper, including 30 that pass time(NULL). Separate object-owned generators and a random_device-seeded process engine also exist. These game reseeds do not reach libc++’s shuffle engine.0x1034ff9440x103608ff40x1036090b40x10360921c

Up to build 4.2.4.399, Evolution's result selection instead calls std::random_shuffle. Its library engine is shared with other list shuffles, including Modern portal queues. Resetting the game's global generator does not reset that library engine.22The separate libc++ shuffle streamThe imported __rs_get and __rs_default symbols identify two-argument std::random_shuffle. Its forward permutation uses uniform_int_distribution with rejection. Live PlayCover captures record raw outputs matching mt19937 seed 5489, complete permutations and selections; the host-library probe is separate evidence. Recorded iPad outcomes corroborate specified routes without identifying the loaded iPad library. Evolution and Modern portals share this library stream.0x1009a92800x1001210440x10001bf38 Build 4.2.6.401 chooses Evolution results with a generator of the artifact's own, as Evolution's choice in build 4.2.6.401 describes.

Plate 13.1The game’s global generator, seeded with 4357, and the library’s mt19937 engine, seeded with 5489: each cell is one raw output, in order. The reseed passes the clock second 1790000000.

Step through

  1. 1Two sequences. Each generator moves only when something takes a value from it.
  2. 2Game draws. Three values taken from the game’s generator move it to position 3; the library engine has not moved.
  3. 3A library shuffle. Shuffling Egypt’s four-slot portal list takes 3 outputs of the library engine; the game’s generator has not moved.
  4. 4A clock reseed. Seeding the game’s generator with time(NULL) = 1790000000 replaces its state: it starts again from that seed’s first output. The library engine keeps its position.

One extra draw moves the next answer

Imagine one generator whose next values are assigned to a glove roll, a loot roll, and a placement choice. If another operation consumes a value before the loot roll, the loot routine receives a different part of the sequence.

No direct call from one feature to the other is necessary. Shared state is enough. An unused result still advances the generator if the draw already happened.

The on-kill reward routine illustrates this: it can draw even when the plant has no corresponding reward rate. An artifact branch can likewise draw values and then overwrite them. The number of draws consumed by a feature is part of its effect on later consumers.

A shuffle adds another variable. Permuting a list of nn entries makes n−1n-1 distribution calls, and rejection sampling can consume more than one engine value per call. Changing the candidate count can therefore change how far the stream advances.

Plate 13.2The game’s global generator from seed 4357, its next outputs taken in turn by the chapter’s three imagined consumers.

Step through

  1. 1As planned. The glove roll, the loot roll and the placement choice take outputs 0, 1 and 2.
  2. 2One extra draw. Another operation takes output 1 first: the loot roll now receives output 2 and the placement choice output 3, though neither feature calls the other.

Many features reset the same generator

The shared game generator has 38 reseed call sites, and 30 pass time(NULL). This is a pattern across plants, loot, and artifacts. The clock has one-second resolution, and every reseed replaces the generator's accumulated state.33Clock reseeding is shared across systemsThe branch census finds 38 calls to the shared seed wrapper, 30 passing time(NULL). Attributed clock callers include the weighted loot selector, its coin-spring sibling, the Gashapon spin, three instruction-equivalent Zomboss loot implementations, and two Gardener Grass paths. The loot selector reseeds immediately before its bounded draw; Gashapon reseeds at spin entry before its later selection logic. These sites all replace state at 0x104e3e580. A second reseed discards earlier intervening draws; only draws after that reseed can shift a subsequent consumer. The libc++ shuffle engine is a separate object.0x1034fca380x10198019c0x10197f7a40x1010952480x1000e172c0x101eeab000x10229a9c80x100b21ec80x100b24b1c

CallerWhere the reset happens
Weighted loot selectorImmediately before its bounded weighted draw
Coin-spring lootIn the related loot-selection routine
GashaponAt spin entry, before its later draws
Zomboss lootIn three copies of the same loot override
Gardener Grass firingBefore checking the level needed for its optional draw
Gardener Grass critical hitImmediately before its bounded draw

All of these callers reset the same object. A loot routine can therefore change the random state later read by a plant or artifact, just as a plant can change the state later used by loot.

Two draws with the same bound, each taken immediately after a reseed to the same second, receive the same first result. Draws between those two resets are discarded by the second reset. A draw taken after the second reset but before the next consumer does change which output that consumer receives.

Gardener Grass adds an ordering mistake

Gardener Grass's fire routine reseeds before checking whether the plant is level 5. Below that level, it does not take the optional draw: it changes the shared state without using a value itself.44A firing routine resets unrelated random stateGardener Grass’s fire routine calls time(NULL) and reseeds before checking whether the plant is level 5. The lower-level branch takes no draw after this reset. Its effect-object critical-hit path also reseeds immediately before a bounded-100 draw. Equal seeds yield the same first result; an intervening draw after a reseed can change which output a consumer receives.0x100b21e980x100b21ec80x100b248980x100b24b1c

Moving that level check earlier would remove this particular no-draw reset. It would not fix the wider coupling: the other clock-seeded callers would still replace the shared state. The general issue is a local feature resetting a process-wide dependency.

Plate 13.3Draws just after reseeds of the game’s generator with time(NULL) = 1790000000, under the bound 100. Each cell is one raw output of the state the reseed leaves.

Step through

  1. 1Reseed, then draw. A caller reseeds with 1790000000 and draws at once: output 0 of that state gives 66 under the bound 100.
  2. 2Draws in between. Other consumers take outputs 1 and 2 of the same state.
  3. 3A reseed in the same second. Another caller reseeds with 1790000000: the state is replaced, outputs 1 and 2 no longer matter, and its first draw with the same bound repeats 66.
  4. 4The next second. A reseed with 1790000001 starts another sequence; its first draw gives 21.

A good generator cannot repair the wrong selection loop

Gashapon's five-entry selector builds cumulative weights but draws again as it visits each entry. This is different from drawing once and finding the interval containing that value.

For cumulative totals CiC_i and total weight TT, its idealized selection shares are

P(i)=[∏j<i(1−CjT)]CiT.P(i)=\left[\prod_{j<i}\left(1-\frac{C_j}{T}\right)\right]\frac{C_i}{T}.

With five equal weights, those shares are 20%, 32%, 28.8%, 15.36%, and 3.84%. The input weights are equal, but the output frequencies are not.55Redrawing inside a cumulative scanThe five-entry selector constructs cumulative weights, but its loop back-edge returns to the random draw. Entry i receives product(1-Cj/T) for earlier entries times Ci/T. With five equal weights the idealized shares are 0.2, 0.32, 0.288, 0.1536, and 0.0384. This equal-weight example demonstrates the algorithm; it is not an attribution of the artifact’s unbound resource weight vectors. The inclusive float endpoint also permits a no-selection remainder.0x1010952240x1010953e40x10109540c

share of the weightsshare the loop gives
20%
32%
28.8%
15.36%
3.84%
entry 1entry 2entry 3entry 4entry 5
Plate 13.4Gashapon’s five-entry selector with weights 1, 1, 1, 1, 1: it builds cumulative weights but draws again at each entry it visits, so the shares it gives differ from the shares the weights ask for.P(2) = (1 − C₁/T) × C₂/T = (1 − 1/5) × 2/5 = 32%

This is a much larger distortion than the tiny modulo bias at ordinary small bounds. A cumulative-weight selector should draw once and find the interval containing that value; drawing again at each comparison changes the distribution.

Notes

The evidence behind each claim: the code path, and the native addresses where it was traced.

  1. 1

    The shared game generator

    The game-owned MT19937 starts at 4357, returns its low 31 bits, uses modulo for bounded integers, and divides floating draws by 2147483647.0 before float32 conversion. A branch census finds 38 calls to the global seed wrapper, including 30 that pass time(NULL). Separate object-owned generators and a random_device-seeded process engine also exist. These game reseeds do not reach libc++’s shuffle engine.

    0x1034ff9440x103608ff40x1036090b40x10360921c
  2. 2

    The separate libc++ shuffle stream

    The imported __rs_get and __rs_default symbols identify two-argument std::random_shuffle. Its forward permutation uses uniform_int_distribution with rejection. Live PlayCover captures record raw outputs matching mt19937 seed 5489, complete permutations and selections; the host-library probe is separate evidence. Recorded iPad outcomes corroborate specified routes without identifying the loaded iPad library. Evolution and Modern portals share this library stream.

    0x1009a92800x1001210440x10001bf38
  3. 3

    Clock reseeding is shared across systems

    The branch census finds 38 calls to the shared seed wrapper, 30 passing time(NULL). Attributed clock callers include the weighted loot selector, its coin-spring sibling, the Gashapon spin, three instruction-equivalent Zomboss loot implementations, and two Gardener Grass paths. The loot selector reseeds immediately before its bounded draw; Gashapon reseeds at spin entry before its later selection logic. These sites all replace state at 0x104e3e580. A second reseed discards earlier intervening draws; only draws after that reseed can shift a subsequent consumer. The libc++ shuffle engine is a separate object.

    0x1034fca380x10198019c0x10197f7a40x1010952480x1000e172c0x101eeab000x10229a9c80x100b21ec80x100b24b1c
  4. 4

    A firing routine resets unrelated random state

    Gardener Grass’s fire routine calls time(NULL) and reseeds before checking whether the plant is level 5. The lower-level branch takes no draw after this reset. Its effect-object critical-hit path also reseeds immediately before a bounded-100 draw. Equal seeds yield the same first result; an intervening draw after a reseed can change which output a consumer receives.

    0x100b21e980x100b21ec80x100b248980x100b24b1c
  5. 5

    Redrawing inside a cumulative scan

    The five-entry selector constructs cumulative weights, but its loop back-edge returns to the random draw. Entry i receives product(1-Cj/T) for earlier entries times Ci/T. With five equal weights the idealized shares are 0.2, 0.32, 0.288, 0.1536, and 0.0384. This equal-weight example demonstrates the algorithm; it is not an attribution of the artifact’s unbound resource weight vectors. The inclusive float endpoint also permits a no-selection remainder.

    0x1010952240x1010953e40x10109540c