Piper Method

Tick

Sim::step(n) runs n calls of tick. dt comes from params. A non-positive dt returns immediately. The method-level walk of a walker through this order is in the algorithm note.

Order inside tick:

  1. lifts — run_lifts. sync_lifts keeps one Lift car per elevator pair (a teleport pair with polygon landings and no walked zone), in the order pairs first appear, with landings every stop lowest floor first; it is rebuilt, and any riders released, only when the set of pairs or their landings changes. A walker counts as waiting at a landing (lift_waiting_at) while on the elevator route, walking, bound off this floor, and within LIFT_CALL (6 m) of the landing landing_for gives them, which is the nearest landing of any elevator on their floor. The next stop is a landing that has a call or a rider whose destination is that floor, preferring the current direction; quiet floors are skipped. A car with nobody inside leaves for the nearest such stop when nobody waits where it stands. A loaded car leaves when it holds pair_capacity, or pair_hold s have passed since the last boarding with nobody else waiting at that landing, or LIFT_HOLD_MAX (3) × pair_hold have passed regardless. A run takes pair_delay × the number of storeys. On arrival only riders bound for that floor step out; the rest stay aboard. A boarder stands still inside the car (car_stand) and rides at car_z. On arrival at a landing with a door (Teleport::lift_door, a scenario::LiftDoor whose normal points away from the landing centroid), the riders move to Lift::exiting and step_out_row releases them a row at a time: door width / LIFT_SPREAD abreast (1–3), LIFT_SPREAD (0.55 m) apart, LIFT_EXIT_OUT (0.45 m) in front of the door line (the middle when a slot is not walkable). A row goes once no walker on that floor is within LIFT_SPREAD of its slots, or LIFT_EXIT_WAIT (2 s) after it became due, and the next is due LIFT_EXIT_GAP (0.5 s) later. A car does not leave while it is unloading. At a landing without a door every rider is released at once: teleport_until is set to now and the destination is spread LIFT_SPREAD apart, three across and then in rows away from the landing, falling back to the landing point when a spot is not walkable or not in clear line. enter_elevator boards a walker only while the car stands at that landing with doors open (lift_open_at: not running, not unloading, below capacity). With a door, the trigger in try_enter_teleport is less than LIFT_DOOR_REACH (0.6 m) in front of the door line and within its width, and the walker heads for the door middle (Teleport::entry, which landing_for, elevator_hops and the crossing goal use); without one it is the landing polygon or within ELEVATOR_REACH (1 m) of its edge, heading for the centroid. A boarder is then in Teleport with teleport_until infinite and Agent::lift set, and refresh_teleport_waits leaves them alone. d in lift_waiting_at is the distance to the door segment, or to the landing edge without a door; a walker more than LIFT_BEHIND (0.3 m) behind the door line is not waiting. While the car is not open at their landing, a waiter's v_des is scaled by ((d − LIFT_STAND) / 0.8) clamped to 0–1, so they stand about LIFT_STAND (1.4 m) out, and track_progress never counts a waiter within LIFT_STAND + 1.5 m as stuck.
  2. release_teleports — walkers whose delay has elapsed are placed on the destination landing, locked out of that pair for teleport_lock_s, and repath.
    • then stores — run_stores: each InStore walker past store_until takes the first clear spot of STORE_EXIT_TRIES (7) along the door, alternating out from its middle in 0.18 steps of its length (kept within 0.15–0.85), STORE_EXIT_OUT (0.55 m) outside the line, clear of every walker on the floor by two radii; with none clear it retries in STORE_EXIT_RETRY (0.5 s). It leaves walking at 0.3 m/s outward, frees its slot, and next_stop picks what is next. Entry is in interactions: try_despawn hands a walker with a store_goal to try_enter_store, which needs it standing in that store's door portal on its floor, then takes the first free slot below capacity, adds to Sim::store_visits, and draws store_until; with no slot it marks the store visited and calls next_stop. choose_store and the next-stop rules are in behaviours.
  3. clear_ignores — drop expired hole/obstacle mutes.
  4. spawn_tick — portals emit. sample_spawn takes the target from draw_target when the portal has targets: a pick on rng over the entries with weight above 0 whose portal accepts arrivals, proportional to weight. It sets Agent::drawn_target. With no such entry it falls back to assign_target (target, then the trip rotation). spawn_at is spawn_one (the walker) then, for a portal with mode_share, draw_leave, then spawn_group for a visitor. draw_leave picks park, pick-up or walk on rng by weight; park picks a lot with a portal by bay count and sets Ride::ToCar { lot, id: None }, pick-up a kerb with a portal by bay count and sets Ride::Pickup, and either makes that portal the target (or the exit_target of a visitor) with drawn_target.
    • then trains — run_trains: a track whose doors have just opened splits its alight count over its doors (the remainder rotates between trains through TrainState::alight_turn); each door then releases a row every Track::alight_row_s (60 × alight_lanes / alight_rate) while it has alighters left and its doors are open. A row is one spawn_at_pin per point of Track::alight_points: alight_lanes (floor(door_width / train::ALIGHT_PITCH), 0.6 m, at least 1) points spread evenly across the door, train::ALIGHT_ROW (0.3 m) into the train. A point with a walker within 2 × radius + 0.04 is skipped this row; a row that places nobody waits for the next tick. Closing doors cancel the rest and send released boarders back to their line.
    • then crossing_holds — clears Traffic::holds and, for each crossing with spans, pushes a Hold at each span's entry s when it is held: a signalled crossing from CROSSING_CLEAR_S (3 s) before walk until walk ends, or any crossing with a walker found through a PedGrid of walking, off-belt walkers (PedGrid::for_box over the polygon's bounds grown by CROSSING_REACH, 1.5 m) who is on its floor and inside the polygon, or moving at SIGNAL_STILL or more, not held by SignalHold::Wait (last tick's), with path_entry within CROSSING_REACH.
    • then traffic — Traffic::step(net, time, dt) in traffic.rs, on Scenario::traffic, with the sim time at the start of the tick setting signal aspects. Traffic::events is cleared. run_stops moves each kerb- or lot-bound vehicle's Stop through its StopPhase: a Gate countdown of gate_s ends in ToBay; a ToBay vehicle whose front is on its bay's lane within ARRIVE_GAP (1 m) of mouth_in and slower than ARRIVE_SPEED (0.5 m/s) stops and starts CrawlIn for Stop::crawl s (bay distance over CRAWL_SPEED 1.5 m/s, at least CRAWL_MIN 1 s), while one more than ARRIVE_GAP past it goes back to Approach without a bay; a finished CrawlIn moves the vehicle from vehicles to Traffic::aside as Docked, with Stop::left set to the kerb's pickup_s when Vehicle::pickup, else infinite at a kerb or lot with a portal (until Traffic::release), else the kerb's dwell_s or the lot's park_s, and pushes Event::Docked; a finished CrawlOut clears the stop. pull_out counts down each docked left and, once it is at most 0, moves the vehicle back onto its exit lane at mouth_out with speed 0 when no Hold on that lane lies between its rear and mouth_out plus its gap, can_pull_out holds and Network::leave finds a sink (the quickest by summed lane travel_s, ties to the first sink): target Sink, that route, CrawlOut, Crawling, and Event::Left. index lists each lane's vehicles front first, with ties broken by id. assign_bays walks each kerb's and lot's waiting vehicles (bound there, in Approach, nearest the head first, ties by id; taken is read off every vehicle's Stop::bay), gives bays as in vehicles, and copies the bay's pose and mouths into the Stop; a lot with gate_s above 0 sends its one served vehicle to Gate. A waiting vehicle more than ARRIVE_GAP past its head with no bay gets the leave route and a Sink target. The lanes are re-indexed. mark_filters sets Vehicle::filtering. change_lanes runs lane_change (MOBIL) for each vehicle in index order and re-indexes after each change; a change replaces the route with the rest from the new lane, leg 0, and clears Vehicle::passes. plan drops passes the vehicle is past, then rebuilds links per vehicle: a Stop at the end of a lane it must change out of, a Stop one standstill gap past its queue head (hold_of: the kerb's from, or the lot's gate) in Approach and Gate, or past mouth_in in ToBay, a Stop at each red or amber line it has no Pass::Go for (amber adds Go or Halt once; green clears both), and per conflict, in two passes, gap acceptance (gap_open, granting Pass::Conflict), a give-way Stop at the zone entry until granted, and a Follow link to every vehicle ahead in the zone order. Every vehicle's IDM acceleration is computed from desired and leader before any of them moves, then all are integrated and lat eases toward its line; a vehicle at gate service or crawling (manoeuvring) is held at speed 0 instead, and every other adds dt − Δs / v0 to Vehicle::delay. advance carries fronts past a lane end onto the next leg of the route, except onto a leg reached by a lane change, and removes Sink-bound vehicles past their sink (Traffic::arrived, their delay into delay_total). index, mark_filters and plan run again. settle walks each lane front first and pulls back any vehicle within MIN_GAP (0.2 m) of its leader, then sets Gate or Crawling for those phases, else Queued, Yielding (stopped at a give-way) or Driving; a Yielding vehicle held by a give-way stop adds dt to Vehicle::waited, which plan zeroes once it passes a conflict. spawn adds rate_per_min × dt / 60 to each source's backlog (cap SOURCE_BACKLOG 20) and emits one vehicle when the backlog is at least 1 and entry_clear holds (each vehicle counts where at puts its front on the source lane, and those on the lanes it leads onto count past its end); draw picks the target among those that accepts the mode and gives a kerb- or lot-bound vehicle a Stop in Approach. Before the draws, each pending pick-up call (Traffic::call_pickup) launches from its source once entry_clear holds, one per source per step, with its called id, 0 occupants and Vehicle::pickup; a source that launched one draws nothing that step. tally adds dt to the run time and, for each kerb whose kerb_queue is spilling, to its spill time. Lanes are indexed only over vehicles, so docked vehicles in aside are invisible to following, conflicts, lane changes and overlaps; frame draws both. Traffic::holds are read as stopped leaders and are never cleared by the step; whoever sets them clears them. Traffic::new seeds its own Lcg from seed × 0x9e3779b9 ^ 0x5bd1e995. The rules are in vehicles.
    • then vehicles — couple_vehicles. Each Event::Docked of a vehicle that is not a pick-up, at a kerb or lot whose portal names a pedestrian portal (portal_of), queues a Party of its occupants at that portal with Ride::In { id, from }. Each Return past its due becomes a Party heading for the portal of its kerb or lot, with Ride::Pickup from a kerb or Ride::ToCar { lot, id } from a lot. run_parties steps out as many of each party as there is floor for: the first through spawn_one (its target overridden when the party has one, visits cleared, came_by and ride set), the rest one at a time through add_members; a party left short waits PARTY_RETRY_S (0.5 s, Party::retry) before trying again, and a vehicle's occupants drops to 0 once all are out. run_pickups goes over walkers with Ride::Pickup: an id no longer bound_for that kerb is dropped; a waiting walker with none takes a groupmate's or calls one (call_pickup), and leaves on foot (Done) when no running source (rate_per_min above 0) can reach the kerb; a waiting walker whose vehicle stands Docked in that kerb gets in (Done, occupants + 1). release_vehicles releases with traffic::BOARD_S (3 s) every vehicle in aside held for walkers (a pick-up, or left infinite) whose id no live walker's Ride::vehicle, no party and no Return from a lot names. Then crossing_open is set from open_crossings: a zebra is open when Traffic::can_stop_for holds on every span, a signalled crossing while it shows walk.
  5. zone_holds — first entry into a zone with stop_delay_s zeros velocity until the delay elapses.
    • then visitors — run_visitors, for visitors walking. Stamina drains by stamina_drain / 100 × speed × dt unless held, and rises by recover_sit while loitering on a seat or recover_stand while held on a Rest spot. A walker with Agent::spot (a Spot: point, floor, SpotKind Browse, Rest or Wait, held, give-up time) is held there once within ARRIVE + 0.15, with hold_until at BROWSE_S × 0.6–1.6 for a leader's Browse and infinite otherwise; an unreached spot is dropped at SPOT_GIVE_UP (60 s) or on a floor change. A leader drops a held Browse when its hold runs out and a Rest when the lowest stamina in its party reaches rest_to; a seated rester is released by the same test. Walkers whose party's lowest stamina is under rest_below call seek_rest every REST_LOOK_S (3 s), never inside a connector. seek_rest scores free seats (attractor_load 0, p_affect above 0) by centroid distance − REST_SEAT_BONUS and Sim::rest_spots by distance, plus REST_CROWD_M × CrowdMap::density + REST_TRAFFIC_M × Occupancy::traffic_near (passes per metre of width per minute over REST_SPACE). It skips rest spots with a walker or another spot within REST_SPACE. A seat goes through take_seat; a standing spot becomes a Rest Spot. rest_spots is a REST_STEP lattice, standable at REST_CLEAR, with a solid or the floor edge within REST_EDGE and no zone or portal box within REST_MOUTH_GAP. logical_hops and drive_point route to spot while it is set; queues, gates, connectors, door lines and try_despawn ignore a walker holding one.
    • then groups — run_groups. spawn_at calls spawn_group for a visitor: a size from group_sizes, then add_members places that many less one standable 0.6–1.6 m round the leader on rng, with group = leader index + 1, leader, rank, Agent::members extended, the leader's came_by and ride (a pick-up not yet waiting), and every desired speed in the group set to the slowest. Party members out of a vehicle join the same way. Each tick a member copies the leader's target, store_goal, visits_left and exit_target when they differ (copy_plan) and rebuilds. A member's own next_stop goes through follow_plan: a Wait spot where it stands while the leader is in a store or bound for the one it just tried, otherwise copy_plan. A Wait is released once the leader is walking, out, and bound elsewhere. While the leader holds a spot or sits resting, a member without one takes a spot of the same kind on a GROUP_STOP_RING (0.9 m) ring round it, at angle (rank + k) × 60° for the first standable k; it drops it once the leader has none. A member whose leader is done or removed is cut loose and plans alone. In acceleration, the general branch (no queue or door-line aim) calls group_steer. ped_social treats groupmates as in forces. offer_attractors skips members. track_progress does not count a member within GROUP_FORM_REACH of its leader, or a leader with a member more than group_gap off, as stuck, and unstick skips a member near its leader.
  6. offer_attractors / release_loiter — capture and release. See behaviours. A seat (Attractor::seat) is offered within SEAT_REACH (4 m) of its centroid when the walker's velocity points at it (cosine above SEAT_AHEAD, 0.3), and the offer bit clears past SEAT_REACH + 1. seat_spot picks the standing spot: each edge midpoint pushed out by radius + SEAT_GAP (0.12 m), kept if it and one radius further are inside the floor and not solid_at, nearest the walker. take_seat stores it in Agent::seat_from and rebuilds only that walker's path. drive_point and logical_hops use the spot while it is set. Within ARRIVE of it the walker is moved to the centroid and held. leave_seat puts them back on the spot and sets the ignore bit of every seat. Only loitering freezes a captured walker (acceleration, integrate, unstick), so a seat taker still walks and is released when stuck. Seats are never nav blocks and never trigger repath_others.
  7. connectors — pick_connectors: walkers on the stair route who still have to change floor, and are not on a belt, captured, or already inside a connector or gate, weigh every hallway connector that passes connector_serves. The cost in seconds is the walk to mouth_outside at desired_speed (at least the walk to the queue_tail_hop point plus the line behind the mouth, when there is one), plus max(0, ahead × CONNECTOR_HEADWAY_S / lanes − walk), plus the crossing time (axis_len / fixed_speed_mps, default CONNECTOR_BELT, for an escalator; axis_len / (desired_speed × speed_factor) plus STAIR_UP_EFFORT_S or STAIR_DOWN_EFFORT_S for a stair). ahead counts walkers holding that connector on the same floor whose mouth distance is shorter. lanes is 1 when is_single_file, else width / CONNECTOR_LANE. The result is kept in Agent::connector with the floor it is for. It is re-weighed every CONNECTOR_RECHECK s (staggered), with CONNECTOR_SWITCH_S hysteresis, and is final within CONNECTOR_COMMIT of the mouth. A change from the zone the path was built for (the held one, or else the nearest by choose_walked_zone's biased distance) rebuilds the path. choose_walked_zone returns the held connector while it still serves the walker on that floor.
    • then gates — pick_gates: walkers within GATE_LOOK of a gate bank weigh each serving gate (walk to the mouth plus GATE_WAIT per person bound for it and closer, or riding) and keep the result in Agent::gate. Re-weighed every GATE_RECHECK s with GATE_SWITCH hysteresis, final inside GATE_COMMIT. A change rebuilds the path; choose_gate and the queue step read the held gate.
  8. queues — find each gate or escalator queue and its tail, then give each waiting walker a target on the line (plan_queues, QUEUE_* constants). Every walker bound for the zone inside its corridor counts, at any speed, so the line does not vanish when its head steps forward. Members chain back from the mouth; the one at rank k > 0 holds queue_gap (QUEUE_GAP_GATE 0.75 m at a gate, QUEUE_GAP_BELT 0.6 m at an escalator) behind rank k − 1. Agent::queued_at is the join ticket: every chain member takes one for that zone, and it is dropped once the walker is not walking, is on the belt, is bound for another zone, or is unbound and more than QUEUE_REACH back. The chain's first link reaches QUEUE_HEAD (1.4 m) from the mouth, or QUEUE_HEAD_HELD (3 m) for a walker holding a ticket. A walker is bound for a zone when it is the target of dest or of any of its next QUEUE_HOPS_AHEAD (4) waypoints. Nobody is removed from a chain. Newcomers outside the chain that stand at least as far back as the tail are sorted by distance to the tail and the j-th aims gap × j past it. The slot is clamped to no further back than where the walker stands, so nobody walks back toward the tail; anyone level with or ahead of the tail gets the QUEUE_LOOK aim below instead. Both the chain and the slot aims set QueueAim::hold, which scales v_des by ((d − 0.12) / 0.8) clamped to 0–1, as for door lines. The head keeps its aim right up to boarding (the QUEUE_FREE cut-off applies only behind it): it holds gap behind the last rider on the belt while that point is short of QUEUE_ENTER (0.3 m) into the mouth, and otherwise aims QUEUE_ENTER in without holding, so it walks on board without slowing. Anyone else bound for the mouth outside a formed line aims QUEUE_LOOK ahead along the axis without holding. In the acceleration step, blocked_thin_mouth checks only the single-file zone the walker is lined up on (its held Agent::gate, or else one whose width it stands within), never a neighbour in the bank. While the next rider is still within lane_spacing + 0.12 of the entry, it scales v_des by ((short − 0.05) / 0.8) clamped to 0–1, where short is the distance left along the axis to Zone::mouth_standby (0.55 m out), so the walker eases to a stand there. The acceleration step reads the target and adds the sideways shim to the driving term. A tail at least QUEUE_LINE_MIN (1.8 m) back is also stored in Sim::queue_lines as a nav::QueueLine from the mouth to the tail, and CrowdMap::queue_line hands it to path building. queue_tail_hop returns the axis point queue_gap behind that tail for a walker standing more than queue_corridor + QUEUE_SIDE off the axis and not within QUEUE_BESIDE (2.5 m) of it level with or ahead of the tail, if the point is on the floor and outside obstacles. waypoints_for routes a hop into a belt mouth through that point first, using nav::route_around_ex with Soft { end_free: QUEUE_TAIL_FREE, .. }, which waives crowd and queue cost only within 1 m of the point. Every QUEUE_REPLAN_TICKS (20, staggered by index), an unranked walking walker whose remaining waypoints reach a belt mouth gets rebuild_one when none of its waypoints before the mouth lies within QUEUE_TAIL_MOVED (1.5 m) of the current tail hop.
    • then door_lines — plan_lines. Line l is door l / 2, side +1 (ahead along travel) when l is even. Walkers bound for a door, walking on its floor and not in a connector, choose a line on that track every LINE_REWEIGH (1 s). The cost in metres is LINE_WALK_W (0.5) × the walk to the tail slot, plus LINE_PERSON_M (4 m) per walker joined or coming, plus pull × |door along − preferred along|, plus LINE_MOUTH_M (12 m) × mouth_weight of the tail slot, plus LINE_FULL_M (30 m) once the line has used all its slots. platform_pref(spawn index) gives each walker a preferred spot (a fraction of the track's door span, TrainState::span) and a pull up to LINE_PREF_MAX (1.2), both golden-ratio sequences. mouth_weight is 1 − out / MOUTH_KEEP for a point up to MOUTH_KEEP (4 m) outside a connector end at this floor and within MOUTH_SIDE (1 m) of its width. LINE_SWITCH_M (2 m) hysteresis. A switch sets target to that door's portal and rebuilds the path. Each column of a line is its own file: TrainState::cols holds its start and length in slots, and TrainState::slot(line, col, row) reads it. next_col is the first column that is not full. Walkers coming to a line are sorted by distance to its next free slot and aim at successive free slots, filling columns in next_col order; only the nearest takes a ticket (Agent::line_seq) and its column (Agent::line_col) within LINE_JOIN (0.9 m) of its slot. Joined walkers hold row k of their own column in ticket order, so a release only moves the file it came from up by one. While the track is boarding, each line releases the column head with the lowest ticket every BOARD_GAP_S (1 s), but not while its door still has alighters to come (alight_left > 0) or one on its exit leg is within DOOR_CLEAR (1.5 m) of the door; the walker sets boarding and heads DOOR_AIM_DEPTH (0.4 m) into the door inset. Aims are kept in TrainState::aim only within LINE_AIM_REACH (10 m), with a clear straight walk, and (unless boarding) not while stepping_off; otherwise the path leads. plan_queues skips stepping_off walkers too. Each occupied column is also pushed to Sim::queue_lines as a nav::QueueLine from its first to its last joined slot, with zone id zones.len() + door, so other walkers route round it; the door's own boarders and alighters pass it as own_queue. Each line's slots are built at load from Track::slot_at column by column (train::LINE_COLS_MAX 3; depth LINE_ROWS_MAX 6, or LINE_LAST_ROWS 8 for the last column). A column stops at the first slot that is not walkable, lies in a connector mouth (mouth_weight > 0), is within LINE_POST_CLEAR (1 m) of a hole or obstacle (near_solid), is within LINE_PASSAGE (1.6 m) of a floor-changing connector's footprint (well_gap, which keeps the strip beside a stair well walkable), or has no clear line from the slot before.
    • Alighters: run_trains gives each walker it spawns at a door Agent::exit_to, a point straight out from the door (door_exit). Its depth is the deepest joined slot of that door plus EXIT_PAST (0.8 m), clamped to EXIT_MIN–EXIT_MAX (2–7 m), at the walker's own offset across the door clamped to EXIT_ACROSS (0.35 m); it shrinks in 0.5 m steps down to 1 m until the point is walkable and in clear line. logical_hops makes it the first hop, built with Soft::OFF. On that leg the acceleration step skips sight yaw and adds a sideways pull back onto the exit line, EXIT_SHIM (2 m/s²) × the offset over QUEUE_SHIM_SPAN clamped to ±1. plan_lines clears exit_to once the walker is within the arrival radius of the point along the door's normal, or past it, or no longer walking.
    • then signals — plan_signals fills Sim::signal_hold for walkers who are walking, not on a belt and not captured. Open means Sim::crossing_open. A walker on an open crossing (same floor, inside the polygon) gets Agent::crossing_ok; one on no crossing has it cleared. On a closed crossing the hold is Hurry with crossing_ok, else Wait(0). Off it, path_entry walks the remaining waypoints (or dest) from the walker in SIGNAL_STEP (0.3 m) samples up to SIGNAL_REACH (8 m) and bisects the first sample inside the polygon five times; the nearest entry d over closed crossings gives Wait(d). In the acceleration step Wait(d) scales v_des by ((d − SIGNAL_CURB) / SIGNAL_EASE) clamped to 0–1 (0.35 m, 1.6 m), and Hurry by SIGNAL_HURRY (1.3). After that, whenever the scenario has crossings and the walker is not Hurry, curb_gap takes the nearest body-to-body gap to a body on its touch level that is in front (ahead > 0, and within SIGNAL_SPACE_SIDE (0.7 m) of the heading line or ahead ≥ SIGNAL_SPACE_CONE (0.5) × distance) and blocks: for a Wait walker, anyone held by Wait or slower than SIGNAL_STILL (0.3 m/s); for anyone else, a Wait walker whose heading has a dot product above 0.5 with theirs. v_des is scaled by ((gap − SIGNAL_SPACE) / SIGNAL_SPACE_EASE) clamped to 0–1 (0.45 m, 0.7 m), before the headway cap.
  9. snapshot — one Body per agent: position, velocity (for the look-ahead avoidance in sight_scan), floor key, heading, whether they are in a stair, and drawn height h (the flight height from stair progress, else z). Sight uses h within SIGHT_RISE; contact across a stair mouth uses TOUCH_RISE. The pedestrian hash grid is 2 m cells.
  10. acceleration — pure function of the snapshot. Parallel when rayon is on and there are at least 128 agents. Writes ax, ay and the split terms fd*, fw*, fp*.
  11. integrate — semi-implicit Euler for walkers who are not held and not on a conveyor. v += a dt, cap speed, then for a walker with v_des >= 0.08 trim any backward speed along the drive heading (Deriv::hx, hy, after sight yaw) to BACK_MAX (0.2 m/s), and x += v dt. A step from open floor into solid_at (a floor hole no zone covers, such as a column, or an obstacle) is refused: the walker keeps the axis move that stays clear, x first, and that velocity component is zeroed. Otherwise it stays put. The wall force pushes away from the nearest edge, so a body shoved past one would be held inside. A walker already inside is not checked (contacts eject them). An unstick mute no longer skips this test, so a jammed body cannot walk through a wall. A walker without crossing_ok, not on a belt, is refused the same way when the step goes from outside into a crossing that is not open (crossing_open). A walker without crossing_ok who ends the step on an open crossing gets it set here, so a step on in the last tick of walk is not first seen by plan_signals during flashing. Drop a travelled crumb every 0.4 m (cap 80).
  12. connector_kinematics — board escalators and gates (from BOARD_REACH before the entry edge to 28% along travel, facing the belt by aim or by stride, no position change; a walker whose escalator serves the trip boards wherever it stands inside the footprint); advance belts in order so the leader claims space first, drawing riders onto the centreline at BELT_GLIDE; clamp stair bodies inside the side walls. A rider whose step reaches the exit edge leaves at that step's end point (exit_conveyor); an escalator hands them to the other floor there. dir against runs the belt the other way; a two-way gate boards at either mouth.
  13. project_z — snap z to the nearest floor.
  14. track_approaches — remember which connector the walker entered, and at what progress.
  15. repath_floor_changes — a floor change rebuilds waypoints. A change is rejected if it does not move toward the destination floor or if it returns to the floor just left (about 20 s).
  16. track_progress — accumulate stuck_s when speed stays under 0.18 m/s and the drive point is not getting closer. A walker in a door line and not yet released, or held by SignalHold::Wait, is never stuck. Nor is one, not Hurry, slower than STUCK_SLOW whose path enters a closed crossing within SIGNAL_CROWD_REACH (20 m): they are standing behind the curb crowd. A walker held (hold_until ahead) is skipped, which covers one waiting for a pick-up.
  17. unstick — after unstick_s, the recovery in behaviours. closest_blocker picks the hole or obstacle to mute; a hole is not muted near a wrong stair mouth, or when lift_shaft holds (the centroid of its ring, from walls::hole_ring, lies in a polygon landing of a pair with no walked zone on that floor).
  18. interactions — despawn into the target portal (a train door portal only while its track is boarding), enter a teleport, or advance the waypoint index (arrival radius 0.45 m, or already past it within 1.5 m, both measured to Waypoint::aim, the corner shifted by the walker's lane share of the leg slack). A Waypoint::firm point is past only once the walker is beyond it along its own leg (nx, ny), whatever the next leg. A walked stair fires on a crossing of its landing line, or when a walker still tied to the flight stands off it within STEP_OFF past the line. fire_stair keeps the plan position and velocity. In try_despawn, a walker with Ride::Pickup reaching its kerb portal is not removed: it sets waiting, stops, and is held (hold_until infinite) for vehicles. One with Ride::In is removed and counted into the Return for its vehicle id, created at the first with due = now + that portal's stay_s.
  19. repair_sight — every 0.5 s per walker, rebuild the path if the leg to the current waypoint clips a wall or void. If the new leg is blocked too, the next check waits 2 s. Walkers steering on a door-line aim are skipped.
  20. avoid_crowds — every CROWD_RECHECK (2 s, staggered) per walker not queued, on a door-line aim, waiting at a signal, captured, on a belt or in a connector: nav::crowd_ahead prices the next CROWD_LOOK (15 m) of the path. Samples within CROWD_SELF of the walker, and within QUEUE_REACH of the first gate or escalator mouth on the path, are not counted. If the cost has grown by more than CROWD_REPLAN over Agent::crowd_seen, the path is rebuilt; crowd_seen tracks the lowest cost since.
  21. contacts — resolve_contacts with the tick's snapshot and shut crossings. Every walking, uncaptured body is a disc; it may move unless loitering, on a conveyor, or held (hold_until ahead). Pairs found through a PedGrid of 2 × largest radius cells and passing same_touch are split to r_i + r_j, Gauss–Seidel in index order, up to CONTACT_PASSES (16) sweeps until none is deeper than CONTACT_SLOP (2 mm). Each body's share is half, or all when the other may not move. A share that would cross a wall segment the body is subject to (segment_applies on its wall_floor, exact crossing in segments_cross) slides along that segment instead. If the slide crosses a wall too, or the share would enter a closed crossing without crossing_ok, the body keeps still and the other takes the whole push. After the sweeps, a body that ended inside a solid (solid_at: a hole no zone covers, or an obstacle) is put back where it started that pass, or ejected to the walkable side of the nearest wall if it began the pass inside. Coincident centres split along a fixed per-pair angle. Afterwards, any velocity a body has against its net push is dropped. It runs last, so the stores, unstick and connector steps above cannot leave an overlap either.
  22. occupancy — accumulate the grid and the per-walker 1.5 m disk density, then update Scenario::crowd (nav::CrowdMap) with the walking bodies and Sim::queue_lines. The same pass counts each walker's CRUSH_RADIUS (1 m) disk into Agent::crush_density. Agent::crush_s grows by dt while that is at least los::CRUSH_DENSITY and the walker is not on a belt or captured, and resets otherwise. track_crush folds walkers with crush_s ≥ CRUSH_HOLD_S into Sim::crush. A walker joins a hotspot on its floor that is within CRUSH_MERGE and was flagged within CRUSH_CLEAR_S, which moves the spot toward it by a running mean; otherwise it starts a new one. At most CRUSH_SPOTS are kept, and the oldest cleared spot is dropped first.
  23. Clock advances by dt.

With a door-line aim, the acceleration step drives straight at the aim instead of the waypoint. Holding aims scale v_des by ((d − 0.12) / 0.8) clamped to 0–1 and switch sight yaw off once that is below 1, so a walker eases into its slot and stands.

Acceleration details and the formulas are in forces. connector_overrides is true only while on_conveyor is set, so the Euler step is skipped and the belt move in step 12 is the motion.

load parses the scenario, builds the wall grid and the occupancy grid, places seed agents, runs an initial spawn burst (up to 12 per portal), and builds waypoints. Walls are not rebuilt on later ticks; replace rebuilds them when geometry changes.

Navigation grids (nav.rs, 0.7 m) are cached on the scenario (up to 48 keys) and cleared when a feature is upserted. The key is floor, radius, whether connectors are walkable, and the bounds of currently full attractors. The same grid is the clearance test: a shortcut is rejected when the body would meet a wall, a void, or the closed end of a connector. A segment that starts inside the radius of an edge is allowed as long as it never gets closer to that edge than it starts. That happens when a walker steps off a flight whose end is a wall on the new leg. Each cell also stores its distance to the nearest wall. A* adds cost inside NAV_MARGIN (0.35 m), and the path simplifier keeps that margin where it can, falling back to the body radius in narrow aisles. A lane narrower than the grid can resolve (a 0.9 m gate aisle may hold no cell that fits) is taken straight at body radius when the grid route is missing or more than 1.3 times the direct length plus 1 m, unless that line has soft cost.

Soft costs are added per metre on top of the wall term (nav::Leg::weight), and depend on the step direction. NavGrid::apron holds a 0–1 weight per cell and NavGrid::apron_axis the axis of the mouth it came from, built with the grid: 1 at each connector end that opens onto the floor (gates both ends; escalators and stairs the end whose z_from or z_to is this floor), falling to 0 at APRON_DEPTH (4 m), across the width plus APRON_SIDE (1 m). A step costs APRON_COST (4) times the weight times APRON_ALONG + (1 − APRON_ALONG) × |sin|, where the sine is of the angle between the step and the axis and APRON_ALONG is 0.15.

Soft::wander and Soft::seed come from Agent::wander and Agent::wander_seed, set in spawn_at from the spawn index (a golden-ratio factor 0.5–1.5 on params.wander and a hashed seed), so the rng stream is unchanged; group members copy the leader's. Leg::weight adds WANDER_COST (1.6) × wander × wander_noise, a smoothstep value noise on a WANDER_CELL (7 m) lattice, before the end_free exemption, so it applies over the whole leg. route_to_goal wraps route_soft (route_around_ex is it with a point goal): a wander path longer than detour_max × the straight distance + DETOUR_SLACK (1 m) is checked against the same search with wander 0 and replaced by it when longer than detour_max × its length + DETOUR_SLACK.

The goal is a nav::Goal: Point(x, y) or Region(&Polygon). logical_hops tags the hop into the target portal (spawn/despawn, store door, train door) with Waypoint::region, the portal index, and waypoints_for routes that hop as a region on the portal's floor and keeps the tag on the leg's last waypoint. nav::region_point is the point at least REGION_INSET (0.2 m) inside the polygon nearest a given point (pushed in off the nearest edge, up to 8 times; the centroid if the polygon is too thin). For a region, route_soft starts from the region point nearest the walker, and astar stops at the first open cell whose centre is within REGION_TOUCH (0.5) cells of the polygon, with the distance to the polygon's bounding box as heuristic (so it never overestimates); the path ends at the region point nearest that cell. No route is cached per goal (only grids), so nothing is keyed by portal. In steering, Sim::portal_in_sight replaces the waypoint aim on that last tagged leg while the walker is on the portal's floor, within PORTAL_SIGHT (12 m) of the region point nearest them, and nav::line_clear to it: they aim at that point, recomputed each step. drive_point gives the same point for a target portal on this floor.

Scenario::crowd (nav::CrowdMap) is a separate 1 m grid per floor, not part of the cache key. Each cell keeps four channels as an exponential average over CROWD_TAU (4 s): standing bodies, moving bodies, and the x and y sums of the moving bodies' unit headings, all per m². A body counts as moving in proportion to its speed up to MOVING_SPEED (0.8 m/s). Reads are a 3×3 box mean. A walker heading along u feels still + against + WITH_FLOW × (moving − against), where against = clamp(½ (moving − heading · u), 0, moving) and WITH_FLOW is 0.25. That costs CROWD_COST (2.5) per ped/m² above CROWD_FREE (0.35), capped at CROWD_CAP (2). Each queue line is stamped fresh every tick, not averaged, into the cells within QUEUE_BAND (0.6 m) plus half a cell of it, holding the zone index. A step in a stamped cell costs QUEUE_COST (6) unless the zone is Soft::own_queue.

Zone ends, mouths, lanes, progress, past_exit and step-off points are all taken in the zone frame (Zone::to_local / to_world, see scenario), as s along travel and t across it, so they hold for a zone at any rise_along bearing.

sim::stair_lane gives the lane offset (STAIR_TWO_LANE, STAIR_LANE_WALL) and sim::stair_lane_band the free band around it (STAIR_LANE_INNER, STAIR_LANE_EDGE). The band reads Zone::flow, the up and down counts that count_stair_flow writes after the crowd update: walkers inside the stair, plus holders of it as Agent::connector within STAIR_FLOW_LOOK (6 m) of its mouth. With nobody coming the other way the band is the full width less STAIR_LANE_EDGE on each side; otherwise the inner edge moves so the walker's direction gets mine / (mine + other) of the width, clamped to STAIR_SHARE_MIN–1 − STAIR_SHARE_MIN (0.3–0.7), plus STAIR_LANE_INNER, and never past the lane line. stair_drive_heading treats the flight as opposed when sight sees oncoming walkers (oppose > 0.15) even if the count is zero. sim::stair_mouth places a stair hop inside the keep-side band, 1.15 m outside the mouth. connector_hops (called from stair_hops) plans past it: when the walker is not yet at the right end (at_correct_connector_end, within 1.55 m of that hop), it adds the entry arc and the hop, then the hops a walker standing on the hop would get (the mouth and the walk through the flight), so the path does not end outside the mouth. stair_drive_heading steers walkers who leave the band back in (STAIR_LANE_LOOK); the rule is in behaviours. nav::Soft comes from waypoints_for per hop. own_queue is the gate or escalator the hop leads to. Aprons and crowd are skipped within end_free of the leg end: QUEUE_REACH at a gate or escalator mouth, STAIR_APPROACH (2 m) at a stair mouth, else APRON_DEPTH. Aprons have no start exemption; the direction term lets a walker step straight off a mouth. Crowd and queue are skipped within CROWD_SELF (1.5 m) of the start. A straight cut, in the direct check or in the string pull, is kept only if its sampled soft cost is no more than that of the grid path it replaces, plus 0.1. The whole straight line still wins when its length plus soft cost is no more than the route round.

waypoints_for first asks stair_step_off: while suppress_pair names a stair, the walker is within its width plus 0.4 m, and is between 1 m inside and STAIR_STEP_OFF − 0.3 past the end it is nearer, the plan starts with a firm point STAIR_STEP_OFF (1.8 m) past that end at the walker's lateral offset (clamped 0.3 m inside the side walls), or 0.6 × that when the first is not spot_clear with a clear line. stepping_off is true while that point is the current waypoint. suppress_pair clears 2.2 m clear of the landing lines, so later rebuilds drop the point. waypoints_for stores on each open-floor waypoint the left normal of the leg into it and nav::leg_slack for each side (SLACK_STEPS, capped at LEG_SLACK_MAX). The last waypoint of a hop has taper set; leg_slack then skips the leg's last LEG_TAPER (6 m), and Waypoint::aim scales the shift by (dist − ARRIVE) / LEG_TAPER. Hops through a connector get no slack. Agent::lane is lane_for(spawn index), a golden-ratio sequence, so the rng stream is unchanged.