On this page
  1. Related
  2. Mechanics
  3. References
  4. Sources

Silent Hill is Konami’s 1999 PlayStation survival horror game. The code read here is shdecompilations’ silent-hill-decomp, a matching decompilation that builds the US release; its game logic is in the overlay BODYPROG.BIN, and each enemy’s code is compiled into the map overlays that use it.

The pocket radio is a proximity-warning: it picks enemies by distance on the ground plane, with no line-of-sight test, and in dark interiors only those in Harry’s own 40-unit chunk count1.

The flashlight makes Harry easier to notice. Once it is half lit, the game reads the map’s flashlight-on preset instead of its dark one2, and enemies use those flags for how far they notice him: a larval stalker in a dark map targets him from 17 units with the light on and from 2.5 with it off3, and a walking creeper’s range also depends on its facing4.

Of the healing items, a Health Drink restores 40 of Harry’s 100 health, a First Aid Kit 80, and an Ampoule 100; the Ampoule then keeps healing him from a store of 3005. An ammunition pickup gives 15 handgun bullets or 6 shells. Easy at least doubles that, Normal and Hard leave it alone, and an Extra Options setting multiplies it up to six times6.

Mechanics

MechanicKindHow
Proximity warningAudio cuethe pocket radio plays static for the two nearest enemies within 32 units, by distance alone, louder as they close in
Light detectionOwn lightin dark maps the flashlight lengthens the range at which enemies notice Harry, from 2.5 to 17 units for a larval stalker
Healing itemInstanta Health Drink adds 40 health, a First Aid Kit 80, and an Ampoule 100, out of 100
Healing itemOver timean Ampoule also fills a store of 300 that restores health as it drains
AmmunitionSeparate countsseparate counts for handgun bullets, shotgun shells, and rifle shells; pickups give 15, 6, and 6, doubled at least on Easy
Every claim about this game, by concept (6)
AmmunitionIn the US build, Event_CommonItemTake gives 15 handgun bullets, 6 shotgun shells, or 6 rifle shells per ammunition pickup, times a multiplier of extraBulletAdjust + 1, which is at least 2 on Easy; healing pickups always give one item. Normal and Hard don't change the count. extraBulletAdjust is an Extra Options setting: the options screen allows 1 plus one step for each of the five low bits set in extraOptionsEnabled, so ×1 to ×6.Code
Healing itemIn the US build, using a healing item from the inventory (Inventory_PlayerItemScroll) adds 40 health for a Health Drink, 80 for a First Aid Kit, and 100 for an Ampoule, then clamps Harry's health to 0–100 and spends one item. An Ampoule also sets healthSaturation to 300 (a set, not an add). Player_LogicUpdate lowers healthSaturation by g_DeltaTime on each update and, while any remains, raises Harry's health by the same amount, capped at 100. So an Ampoule fills health and then keeps restoring it, up to 300 more in all (computed), with the store draining even while health is full.Code
Light detectionIn the US build, the flashlight swaps which environment preset's flags are live, and enemy code reads those flags. Gfx_EffectsUpdate moves flashlightIntensity toward 1 while the flashlight is on and toward 0 while it's off, by 4 × g_DeltaTime per update, and func_8003F838 copies the live flags (field_154) from the flashlight-off preset (field_1C[0]) while the intensity is under 0.5 and from the flashlight-on preset (field_1C[1]) from 0.5 up. WorldEnv_MapPresetSet gives a map whose header's unnamed field_16 is 2 the preset pair 6 and 3, or 7 and 4 in low-visibility interiors (off, then on). Presets 6 and 7 set SpecialEnvEventFlags_DarkEnvironment and SpecialEnvEventFlags_FlashlightAllowed; presets 3 and 4 set FlashlightAllowed without DarkEnvironment. A map whose field_16 is 1 gets preset 1 or 2 for both, which set neither flag, and Gfx_MapEnvStepUpdate marks the flashlight unavailable whenever the two presets are the same. So in those dark maps, the flags read as dark with the flashlight off and as lit with it on; map events can load other pairs.Code
In the US build, LarvalStalker_ControlUpdate sets the larval stalker's target position to Harry's whenever his distance on the ground plane is under 2 × base + step, with base and step picked by the live light flags (silent-hill/flashlight-presets): 2.5 and 8 when neither DarkEnvironment nor FlashlightAllowed is set, 3.5 and 10 when only FlashlightAllowed is, and 0.5 and 1.5 otherwise. Computed, that's 13 world units in fog presets; in the dark map presets, 17 units with the flashlight on and 2.5 with it off. This check reads no facing and no line of sight.Code
In the US build, a walking creeper (Creeper_ControlWalkForward) retargets Harry's position when func_8006FD90 passes, with arguments picked by the live light flags (silent-hill/flashlight-presets). func_8006FD90 scales a range by how directly the creeper faces Harry, f = 1 − |angle off its heading|/180° (1 straight ahead, 0 behind), raised to the power count + 1; it fails beyond base + step × that, and otherwise a ray test toward Harry (Ray_CharaTraceQuery) decides. With only FlashlightAllowed set, the creeper calls it with count 0, base 12, and step 8: 12 units behind to 20 ahead. With FlashlightAllowed and DarkEnvironment, count 2, base 0.8, step 0.4: 0.8 to 1.2 units. With neither flag, count 1, base 4, step 12: 4 to 16 units (all computed). A separate check (func_80070360 with 0.5) or an alert flag also retargets it.Code
Proximity warningIn the US build (SLUS-00707), Game_NpcUpdate picks the enemies the pocket radio reacts to by distance alone, with no line-of-sight test. An enemy (a character id up to Chara_MonsterCybil, the code's last common enemy) counts while its health is above 0, its CharaFlag_NoRadioStatic flag is clear (Split Head's init sets it; other setters weren't read), and its squared distance from Harry on the ground plane, x and z with height ignored, is under 32² world units (a unit is Q12 1.0). In maps flagged MapFlag_Interior with one or two active chunks, which the code calls low-visibility interiors, the enemy must also share Harry's 40-unit chunk cell on both axes. The code keeps the three nearest, and the radio's two static channels (g_RadioNoise[2]) each follow one of the nearest two, only while ItemToggleFlag_RadioOn is set. Each channel is panned toward its enemy, and its volume falls with distance: the attenuation is the distance converted by Q12_TO_Q4 (sixteenths of a unit, by the decompilers' reading, which their comment "TODO: Is it Q4 here?" doubts), halved outside low-visibility interiors, capped at 255, and Sd_SfxAttributesUpdate takes that share out of 255 from the volume. Computed under that reading, the static falls silent at about 16 units in a low-visibility interior and about 32 units elsewhere.Code

References

  1. ^In the US build (SLUS-00707), Game_NpcUpdate picks the enemies the pocket radio reacts to by distance alone, with no line-of-sight test. An enemy (a character id up to Chara_MonsterCybil, the code's last common enemy) counts while its health is above 0, its CharaFlag_NoRadioStatic flag is clear (Split Head's init sets it; other setters weren't read), and its squared distance from Harry on the ground plane, x and z with height ignored, is under 32² world units (a unit is Q12 1.0). In maps flagged MapFlag_Interior with one or two active chunks, which the code calls low-visibility interiors, the enemy must also share Harry's 40-unit chunk cell on both axes. The code keeps the three nearest, and the radio's two static channels (g_RadioNoise[2]) each follow one of the nearest two, only while ItemToggleFlag_RadioOn is set. Each channel is panned toward its enemy, and its volume falls with distance: the attenuation is the distance converted by Q12_TO_Q4 (sixteenths of a unit, by the decompilers' reading, which their comment "TODO: Is it Q4 here?" doubts), halved outside low-visibility interiors, capped at 255, and Sd_SfxAttributesUpdate takes that share out of 255 from the volume. Computed under that reading, the static falls silent at about 16 units in a low-visibility interior and about 32 units elsewhere.Codeshdecompilations/silent-hill-decomprevised
    How we know
    218    posXShift6 = Q12_TO_Q6(g_SysWork.playerWork.player.position.vx);
    219    posZShift6 = Q12_TO_Q6(g_SysWork.playerWork.player.position.vz);
    ⋮
    227        closestNpcInfos[j].distanceToNpc = SQUARE(32);
    
    src/bodyprog/events/npc_main.c lines 218–229 at 405a621 · upstream ↗
    243        // Only process enemy NPC for radio interference.
    244        if (curNpc->model.charaId <= CHARA_LAST_ENEMY_ID)
    ⋮
    246            // Compute square integer distance from player to NPC.
    247            curDistToNpc = Q12_SQUARE_PRECISE(Q12_TO_Q6(curNpc->position.vx) - posXShift6) +
    248                           Q12_SQUARE_PRECISE(Q12_TO_Q6(curNpc->position.vz) - posZShift6);
    ⋮
    250            // Check if map is low-visibility interior.
    251            isLowVisInterior = false;
    252            if (g_MapOverlayHdr.mapInfo->flags & MapFlag_Interior)
    ⋮
    254                isLowVisInterior = (g_MapOverlayHdr.mapInfo->flags & (MapFlag_OneActiveChunk |
    255                                                                      MapFlag_TwoActiveChunks)) > 0;
    ⋮
    262                if (curNpc->health <= Q12(0.0f)             ||
    263                    curNpc->flags & CharaFlag_NoRadioStatic ||
    ⋮
    
    src/bodyprog/events/npc_main.c lines 243–287 at 405a621 · upstream ↗
    7#define CHUNK_SIZE                        40.0f
    
    include/bodyprog/map/terrain.h line 7 at 405a621 · upstream ↗
    #define CHARA_LAST_ENEMY_ID Chara_MonsterCybil
    include/bodyprog/chara/chara.h#L15 · upstream ↗
    62    splitHead->flags    |= CharaFlag_NoRadioStatic | CharaFlag_Hit;
    
    src/maps/characters/split_head.c line 62 at 405a621 · upstream ↗
    155    s_CloseNpcInfo  closestNpcInfos[3];
    
    src/bodyprog/events/npc_main.c line 155 at 405a621 · upstream ↗
    181    s32 func_800382B0(s32 bitIdx)
    ⋮
    185        for (i = 0; i < (ARRAY_SIZE(closestNpcInfos) - 1); i++)
    ⋮
    197    static s32 func_800382EC()
    ⋮
    201        for (i = 0; i < (ARRAY_SIZE(closestNpcInfos) - 1); i++)
    
    src/bodyprog/events/npc_main.c lines 181–201 at 405a621 · upstream ↗
    68s_RadioNoise g_RadioNoise[2];
    
    src/bodyprog/events/game_sys_states.c line 68 at 405a621 · upstream ↗
    362    if (!(g_SavegamePtr->itemToggleFlags & ItemToggleFlag_RadioOn))
    ⋮
    364        return;
    
    src/bodyprog/events/npc_main.c lines 362–365 at 405a621 · upstream ↗
    423            isExterior = false;
    424            if (!(g_MapOverlayHdr.mapInfo->flags & MapFlag_Interior) ||
    425                !(g_MapOverlayHdr.mapInfo->flags & (MapFlag_OneActiveChunk | MapFlag_TwoActiveChunks)))
    ⋮
    427                isExterior = true;
    ⋮
    433                balance      = Vc_StereoBalanceGet(&closeNpcInfo->position);
    ⋮
    435                // Compute volume from distance.
    436                distToNpc = Q12_TO_Q4(SquareRoot12(Q12(closeNpcInfo->distanceToNpc)));
    437                if (isExterior)
    ⋮
    439                    distToNpc = DIV_FAST(distToNpc, 2);
    ⋮
    442                // Compute clamped volume from Q4 distance. TODO: Is it Q4 here?
    443                vol = CLAMP(distToNpc, Q8(0.0f), Q8_CLAMPED(1.0f));
    ⋮
    445                Sd_SfxAttributesUpdate(Sfx_RadioInterferenceLoop + l, balance, vol, 0);
    
    src/bodyprog/events/npc_main.c lines 423–445 at 405a621 · upstream ↗
    560    convertedVol                = vol;
    561    convertedVol                = g_Sd_VabPlayingInfo.volumeLeft - ((g_Sd_VabPlayingInfo.volumeLeft * (convertedVol)) / 255);
    
    src/bodyprog/sound/sd_call.c lines 560–561 at 405a621 · upstream ↗

    Checked against the evidence by its author, an AI model, 2026-10-22.

    Revised after a check by a second AI model, 2026-10-22.

    In the claim ledger

  2. ^In the US build, the flashlight swaps which environment preset's flags are live, and enemy code reads those flags. Gfx_EffectsUpdate moves flashlightIntensity toward 1 while the flashlight is on and toward 0 while it's off, by 4 × g_DeltaTime per update, and func_8003F838 copies the live flags (field_154) from the flashlight-off preset (field_1C[0]) while the intensity is under 0.5 and from the flashlight-on preset (field_1C[1]) from 0.5 up. WorldEnv_MapPresetSet gives a map whose header's unnamed field_16 is 2 the preset pair 6 and 3, or 7 and 4 in low-visibility interiors (off, then on). Presets 6 and 7 set SpecialEnvEventFlags_DarkEnvironment and SpecialEnvEventFlags_FlashlightAllowed; presets 3 and 4 set FlashlightAllowed without DarkEnvironment. A map whose field_16 is 1 gets preset 1 or 2 for both, which set neither flag, and Gfx_MapEnvStepUpdate marks the flashlight unavailable whenever the two presets are the same. So in those dark maps, the flags read as dark with the flashlight off and as lit with it on; map events can load other pairs.Codeshdecompilations/silent-hill-decomp
    How we know
    393    if (g_SysWork.field_2388.isFlashlightOn)
    ⋮
    395        g_SysWork.field_2388.flashlightIntensity += Q12_MULT_FLOAT_PRECISE(g_DeltaTime, 4.0f);
    ⋮
    397    else
    ⋮
    399        g_SysWork.field_2388.flashlightIntensity -= Q12_MULT_FLOAT_PRECISE(g_DeltaTime, 4.0f);
    ⋮
    402    g_SysWork.field_2388.flashlightIntensity = CLAMP(g_SysWork.field_2388.flashlightIntensity, Q12(0.0f), Q12(1.0f));
    
    src/bodyprog/world/world_effects.c lines 393–403 at 405a621 · upstream ↗
    412    if (ptr->primitiveType == PrimitiveType_None)
    ⋮
    414        ptr->field_1C[0] = ptr->field_84[0];
    415        ptr->field_1C[1] = ptr->field_84[1];
    
    src/bodyprog/world/world_effects.c lines 412–415 at 405a621 · upstream ↗
    430    func_8003F838(&ptr->field_154, &ptr->field_1C[0], &ptr->field_1C[1], ptr->flashlightIntensity);
    
    src/bodyprog/world/world_effects.c line 430 at 405a621 · upstream ↗
    636static void func_8003F838(s_StructUnk3* target, s_StructUnk3* envSettings0, s_StructUnk3* envSettings1, q19_12 weight) // 0x8003F838
    ⋮
    649    // Copy enviroment flags.
    650    if (weight < Q12(0.5f))
    ⋮
    652        target->effectsInfo.flags.field_00[0] = envSettings0->effectsInfo.flags.field_00[0];
    ⋮
    654    else
    ⋮
    656        target->effectsInfo.flags.field_00[0] = envSettings1->effectsInfo.flags.field_00[0];
    
    src/bodyprog/world/world_effects.c lines 636–657 at 405a621 · upstream ↗
    319    if (preset0 == preset1)
    ⋮
    321        g_SysWork.field_2388.isFlashlightUnavailable = true;
    ⋮
    336    Gfx_FogParametersSet(&g_SysWork.field_2388.field_84[0], preset0);
    337    Gfx_FogParametersSet(&g_SysWork.field_2388.field_84[1], preset1);
    
    src/bodyprog/world/world_effects.c lines 316–337 at 405a621 · upstream ↗
    22static s_MapEnvPresetIdxs D_800A9F80 = { 1, 1  };
    23static s_MapEnvPresetIdxs D_800A9F84 = { 2, 2  };
    24static s_MapEnvPresetIdxs D_800A9F88 = { 6, 3  };
    25static s_MapEnvPresetIdxs D_800A9F8C = { 7, 4  };
    
    src/bodyprog/world/world_effects.c lines 22–25 at 405a621 · upstream ↗
    210    switch (mapHdr->field_16)
    ⋮
    212        case 1:
    213            if (hasActiveChunk)
    ⋮
    215                presetIdxPtr = &D_800A9F84;
    ⋮
    217            else
    ⋮
    219                presetIdxPtr = &D_800A9F80;
    ⋮
    221            break;
    ⋮
    223        case 2:
    224            if (hasActiveChunk)
    ⋮
    226                presetIdxPtr = &D_800A9F8C;
    ⋮
    228            else
    ⋮
    230                presetIdxPtr = &D_800A9F88;
    ⋮
    
    src/bodyprog/world/world_effects.c lines 210–232 at 405a621 · upstream ↗
    5s_MapEffectsInfo MAP_EFFECTS_INFOS[21] = {
    ⋮
    7        .flags                  = { .field_00 = { SpecialEnvEventFlags_None, 0xFF, UnkGfxEnum_0, 0 }},
    ⋮
    23        .flags                  = { .field_00 = { SpecialEnvEventFlags_None, 0xFF, UnkGfxEnum_0, 0 }},
    ⋮
    39        .flags                  = { .field_00 = { SpecialEnvEventFlags_None, 0xFF, UnkGfxEnum_2, 0 }},
    ⋮
    55        .flags                  = { .field_00 = { (SpecialEnvEventFlags_FlashlightAllowed | SpecialEnvEventFlags_EnableBrightness | SpecialEnvEventFlags_EnableLensflare), 0xFF, UnkGfxEnum_1, 0 }},
    ⋮
    71        .flags                  = { .field_00 = { (SpecialEnvEventFlags_FlashlightAllowed | SpecialEnvEventFlags_EnableBrightness | SpecialEnvEventFlags_EnableLensflare), 0xFF, UnkGfxEnum_1, 0 }},
    ⋮
    87        .flags                  = { .field_00 = { (SpecialEnvEventFlags_FlashlightAllowed | SpecialEnvEventFlags_EnableBrightness | SpecialEnvEventFlags_EnableLensflare), 0xFF, UnkGfxEnum_1, 0 }},
    ⋮
    103        .flags                  = { .field_00 = { (SpecialEnvEventFlags_DarkEnvironment | SpecialEnvEventFlags_FlashlightAllowed | SpecialEnvEventFlags_EnableBrightness), 0xFF, UnkGfxEnum_0, 0 }},
    ⋮
    119        .flags                  = { .field_00 = { (SpecialEnvEventFlags_DarkEnvironment | SpecialEnvEventFlags_FlashlightAllowed | SpecialEnvEventFlags_EnableBrightness), 0xFF, UnkGfxEnum_0, 0 }},
    
    src/bodyprog/sys/map_info.c lines 5–119 at 405a621 · upstream ↗

    Checked against the evidence by its author, an AI model, 2026-10-22.

    Checked by a second AI model, 2026-10-22.

    In the claim ledger

  3. ^In the US build, LarvalStalker_ControlUpdate sets the larval stalker's target position to Harry's whenever his distance on the ground plane is under 2 × base + step, with base and step picked by the live light flags (silent-hill/flashlight-presets): 2.5 and 8 when neither DarkEnvironment nor FlashlightAllowed is set, 3.5 and 10 when only FlashlightAllowed is, and 0.5 and 1.5 otherwise. Computed, that's 13 world units in fog presets; in the dark map presets, 17 units with the flashlight on and 2.5 with it off. This check reads no facing and no line of sight.Codeshdecompilations/silent-hill-decomp
    How we know
    84void LarvalStalker_ControlUpdate(s_SubCharacter* larvalStalker)
    
    src/maps/characters/larval_stalker.c line 84 at 405a621 · upstream ↗
    111    idxInfo = g_SysWork.field_2388.field_154.effectsInfo.flags.field_0 & (SpecialEnvEventFlags_DarkEnvironment | SpecialEnvEventFlags_FlashlightAllowed);
    112    if (idxInfo == 0)
    ⋮
    114        baseDistMax = Q12(2.5f);
    115        distStep = Q12(8.0f);
    ⋮
    117    else if (idxInfo == 2)
    ⋮
    119        baseDistMax = Q12(3.5f);
    120        distStep = Q12(10.0f);
    ⋮
    122    else
    ⋮
    124        baseDistMax = Q12(0.5f);
    125        distStep = Q12(1.5f);
    ⋮
    128    if (Math_Vector2MagCalcSafeQ6(g_SysWork.playerWork.player.position.vx - larvalStalker->position.vx,
    129                            g_SysWork.playerWork.player.position.vz - larvalStalker->position.vz) < ((baseDistMax * 2) + distStep))
    ⋮
    
    src/maps/characters/larval_stalker.c lines 111–133 at 405a621 · upstream ↗
    298    SpecialEnvEventFlags_DarkEnvironment   = 1 << 0, // Double check.
    299    SpecialEnvEventFlags_FlashlightAllowed = 1 << 1,
    
    include/game.h lines 298–299 at 405a621 · upstream ↗

    Checked against the evidence by its author, an AI model, 2026-10-22.

    Checked by a second AI model, 2026-10-22.

    In the claim ledger

  4. ^In the US build, a walking creeper (Creeper_ControlWalkForward) retargets Harry's position when func_8006FD90 passes, with arguments picked by the live light flags (silent-hill/flashlight-presets). func_8006FD90 scales a range by how directly the creeper faces Harry, f = 1 − |angle off its heading|/180° (1 straight ahead, 0 behind), raised to the power count + 1; it fails beyond base + step × that, and otherwise a ray test toward Harry (Ray_CharaTraceQuery) decides. With only FlashlightAllowed set, the creeper calls it with count 0, base 12, and step 8: 12 units behind to 20 ahead. With FlashlightAllowed and DarkEnvironment, count 2, base 0.8, step 0.4: 0.8 to 1.2 units. With neither flag, count 1, base 4, step 12: 4 to 16 units (all computed). A separate check (func_80070360 with 0.5) or an alert flag also retargets it.Codeshdecompilations/silent-hill-decomp
    How we know
    372void Creeper_ControlWalkForward(s_SubCharacter* creeper)
    ⋮
    384    if (((g_SysWork.field_2388.field_154.effectsInfo.flags.field_0 & (SpecialEnvEventFlags_DarkEnvironment | SpecialEnvEventFlags_FlashlightAllowed)) == SpecialEnvEventFlags_FlashlightAllowed && func_8006FD90(creeper, 0, Q12(12.0f), Q12(8.0f))) ||
    385        ((g_SysWork.field_2388.field_154.effectsInfo.flags.field_00[0] & SpecialEnvEventFlags_FlashlightAllowed) &&
    386         (g_SysWork.field_2388.field_154.effectsInfo.flags.field_00[0] & SpecialEnvEventFlags_DarkEnvironment) && func_8006FD90(creeper, 2, Q12(0.8f), Q12(0.4f))) ||
    387        (!(g_SysWork.field_2388.field_154.effectsInfo.flags.field_0 & (SpecialEnvEventFlags_DarkEnvironment | SpecialEnvEventFlags_FlashlightAllowed)) && func_8006FD90(creeper, 1, Q12(4.0f), Q12(12.0f))) ||
    388        (func_80070360(creeper, 0, Q12(0.5f)) || creeperProps.flags & CreeperFlag_Alerted))
    ⋮
    390        creeperProps.targetPositionX = playerChara.position.vx;
    391        creeperProps.targetPositionZ = playerChara.position.vz;
    
    src/maps/characters/creeper.c lines 372–392 at 405a621 · upstream ↗
    137bool func_8006FD90(s_SubCharacter* chara, s32 count, q19_12 baseDistMax, q19_12 distStep) // 0x8006FD90
    ⋮
    149                                         g_SysWork.playerWork.player.position.vz - chara->position.vz) -
    ⋮
    152        distMult = (Math_AngleNormalizeSigned(ratan2(g_SysWork.playerWork.player.position.vx - chara->position.vx,
    153                                                     g_SysWork.playerWork.player.position.vz - chara->position.vz) -
    154                                              chara->rotation.vy) * 2) + Q12_ANGLE(360.0f);
    ⋮
    158        distMult = (Q12_ANGLE(180.0f) - Math_AngleNormalizeSigned((ratan2(g_SysWork.playerWork.player.position.vx - chara->position.vx,
    159                                                                          g_SysWork.playerWork.player.position.vz - chara->position.vz) -
    160                                                                   chara->rotation.vy))) * 2;
    ⋮
    163    for (i = distMult; count > 0; count--)
    ⋮
    165        distMult = Q12_MULT_PRECISE(distMult, i);
    ⋮
    168    dist = Math_Vector2MagCalcSafeQ6(g_SysWork.playerWork.player.position.vx - chara->position.vx,
    169                                     g_SysWork.playerWork.player.position.vz - chara->position.vz);
    ⋮
    
    src/bodyprog/collision/los.c lines 137–171 at 405a621 · upstream ↗
    173        return false;
    ⋮
    195    return !Ray_CharaTraceQuery(&sp10, &pos, &offset, chara) || sp20.vx != Q12(0.0f);
    
    src/bodyprog/collision/los.c lines 172–195 at 405a621 · upstream ↗

    Checked against the evidence by its author, an AI model, 2026-10-22.

    Checked by a second AI model, 2026-10-22.

    In the claim ledger

  5. ^In the US build, using a healing item from the inventory (Inventory_PlayerItemScroll) adds 40 health for a Health Drink, 80 for a First Aid Kit, and 100 for an Ampoule, then clamps Harry's health to 0–100 and spends one item. An Ampoule also sets healthSaturation to 300 (a set, not an add). Player_LogicUpdate lowers healthSaturation by g_DeltaTime on each update and, while any remains, raises Harry's health by the same amount, capped at 100. So an Ampoule fills health and then keeps restoring it, up to 300 more in all (computed), with the store draining even while health is full.Codeshdecompilations/silent-hill-decomp
    How we know
    1995void Inventory_PlayerItemScroll(u32* selectedItemId) // 0x800523D8
    
    src/bodyprog/items/item_screens_3.c line 1995 at 405a621 · upstream ↗
    2516                switch (g_SavegamePtr->items[g_SysWork.invItemSelectedIdx].id)
    ⋮
    2518                    case InvItemId_HealthDrink:
    2519                    case InvItemId_FirstAidKit:
    2520                    case InvItemId_Ampoule:
    2521                        switch (g_SavegamePtr->items[g_SysWork.invItemSelectedIdx].id)
    ⋮
    2523                            case InvItemId_FirstAidKit:
    2524                                g_SysWork.playerWork.player.health += Q12(80.0f);
    2525                                break;
    ⋮
    2527                            case InvItemId_HealthDrink:
    2528                                g_SysWork.playerWork.player.health += Q12(40.0f);
    2529                                break;
    ⋮
    2531                            case InvItemId_Ampoule:
    ⋮
    
    src/bodyprog/items/item_screens_3.c lines 2516–2539 at 405a621 · upstream ↗
    1230void Player_LogicUpdate(s_SubCharacter* player, s_PlayerExtra* extra, GsCOORDINATE2* coords) // 0x80071CE8
    
    src/bodyprog/player_control.c line 1230 at 405a621 · upstream ↗
    1289    g_SavegamePtr->healthSaturation -= g_DeltaTime;
    1290    g_SavegamePtr->healthSaturation = CLAMP(g_SavegamePtr->healthSaturation, Q12(0.0f), Q12(300.0f));
    ⋮
    1292    if (g_SavegamePtr->healthSaturation != Q12(0.0f))
    ⋮
    1294        playerChara.health += g_DeltaTime;
    1295        playerChara.health  = CLAMP(playerChara.health, Q12(0.0f), Q12(100.0f));
    
    src/bodyprog/player_control.c lines 1289–1296 at 405a621 · upstream ↗

    Checked against the evidence by its author, an AI model, 2026-10-22.

    Checked by a second AI model, 2026-10-22.

    In the claim ledger

  6. ^In the US build, Event_CommonItemTake gives 15 handgun bullets, 6 shotgun shells, or 6 rifle shells per ammunition pickup, times a multiplier of extraBulletAdjust + 1, which is at least 2 on Easy; healing pickups always give one item. Normal and Hard don't change the count. extraBulletAdjust is an Extra Options setting: the options screen allows 1 plus one step for each of the five low bits set in extraOptionsEnabled, so ×1 to ×6.Codeshdecompilations/silent-hill-decomp
    How we know
    7#define DEFAULT_PICKUP_ITEM_COUNT      1
    8#define HANDGUN_AMMO_PICKUP_ITEM_COUNT 15
    9#define SHOTGUN_AMMO_PICKUP_ITEM_COUNT 6
    10#define RIFLE_AMMO_PICKUP_ITEM_COUNT   6
    
    include/bodyprog/items.h lines 7–10 at 405a621 · upstream ↗
    1104void Event_CommonItemTake(u32 pickupType, e_EventFlag eventFlagIdx) // 0x800879FC
    ⋮
    1106    #define EASY_DIFFICULTY_AMMO_COUNT_MULT_MIN 2
    ⋮
    1111    ammoCountMult = g_GameWork.config.extraBulletAdjust + 1;
    1112    if (g_SavegamePtr->gameDifficulty == GameDifficulty_Easy)
    ⋮
    1114        ammoCountMult = CLAMP(ammoCountMult, EASY_DIFFICULTY_AMMO_COUNT_MULT_MIN, ammoCountMult);
    ⋮
    1121            Event_ItemTake(InvItemId_FirstAidKit, DEFAULT_PICKUP_ITEM_COUNT, eventFlagIdx, MapMsgIdx_FirstAidSelect);
    ⋮
    1133            Event_ItemTake(InvItemId_HandgunBullets, ammoCountMult * HANDGUN_AMMO_PICKUP_ITEM_COUNT, eventFlagIdx, MapMsgIdx_HandgunAmmoSelect);
    ⋮
    1137            Event_ItemTake(InvItemId_ShotgunShells, ammoCountMult * SHOTGUN_AMMO_PICKUP_ITEM_COUNT, eventFlagIdx, MapMsgIdx_ShotgunAmmoSelect);
    ⋮
    1141            Event_ItemTake(InvItemId_RifleShells, ammoCountMult * RIFLE_AMMO_PICKUP_ITEM_COUNT, eventFlagIdx, MapMsgIdx_RifleAmmoSelect);
    
    src/bodyprog/events/events_util.c lines 1104–1142 at 405a621 · upstream ↗
    136            g_ExtraOptionsMenu_BulletMultMax     = 1;
    137            unlockedOptFlags                     = g_GameWork.config.extraOptionsEnabled;
    ⋮
    139            // Set available bullet multiplier.
    140            for (i = 0; i < 5; i++)
    ⋮
    142                if (unlockedOptFlags & (1 << i))
    ⋮
    144                    g_ExtraOptionsMenu_BulletMultMax++;
    
    src/screens/options/options.c lines 136–146 at 405a621 · upstream ↗
    460                    g_GameWork.config.extraBulletAdjust = g_GameWork.config.extraBulletAdjust + (g_ExtraOptionsMenu_BulletMultMax - 1);
    ⋮
    462                g_GameWork.config.extraBulletAdjust = g_GameWork.config.extraBulletAdjust % g_ExtraOptionsMenu_BulletMultMax;
    
    src/screens/options/options.c lines 460–462 at 405a621 · upstream ↗

    Checked against the evidence by its author, an AI model, 2026-10-22.

    Checked by a second AI model, 2026-10-22.

    In the claim ledger

Sources

Code