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:
- lifts —
run_lifts.sync_liftskeeps oneLiftcar per elevator pair (a teleport pair with polygon landings and no walked zone), in the order pairs first appear, withlandingsevery 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 withinLIFT_CALL(6 m) of the landinglanding_forgives 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 holdspair_capacity, orpair_holds have passed since the last boarding with nobody else waiting at that landing, orLIFT_HOLD_MAX(3) ×pair_holdhave passed regardless. A run takespair_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 atcar_z. On arrival at a landing with a door (Teleport::lift_door, ascenario::LiftDoorwhose normal points away from the landing centroid), the riders move toLift::exitingandstep_out_rowreleases them a row at a time:door width / LIFT_SPREADabreast (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 withinLIFT_SPREADof its slots, orLIFT_EXIT_WAIT(2 s) after it became due, and the next is dueLIFT_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_untilis set to now and the destination is spreadLIFT_SPREADapart, 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_elevatorboards 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 intry_enter_teleportis less thanLIFT_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, whichlanding_for,elevator_hopsand the crossing goal use); without one it is the landing polygon or withinELEVATOR_REACH(1 m) of its edge, heading for the centroid. A boarder is then inTeleportwithteleport_untilinfinite andAgent::liftset, andrefresh_teleport_waitsleaves them alone.dinlift_waiting_atis the distance to the door segment, or to the landing edge without a door; a walker more thanLIFT_BEHIND(0.3 m) behind the door line is not waiting. While the car is not open at their landing, a waiter'sv_desis scaled by((d − LIFT_STAND) / 0.8)clamped to 0–1, so they stand aboutLIFT_STAND(1.4 m) out, andtrack_progressnever counts a waiter withinLIFT_STAND + 1.5m as stuck. - 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: eachInStorewalker paststore_untiltakes the first clear spot ofSTORE_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 inSTORE_EXIT_RETRY(0.5 s). It leaves walking at 0.3 m/s outward, frees its slot, andnext_stoppicks what is next. Entry is in interactions:try_despawnhands a walker with astore_goaltotry_enter_store, which needs it standing in that store's door portal on its floor, then takes the first free slot belowcapacity, adds toSim::store_visits, and drawsstore_until; with no slot it marks the store visited and callsnext_stop.choose_storeand the next-stop rules are in behaviours.
- then stores —
- clear_ignores — drop expired hole/obstacle mutes.
- spawn_tick — portals emit.
sample_spawntakes the target fromdraw_targetwhen the portal hastargets: a pick onrngover the entries with weight above 0 whose portal accepts arrivals, proportional to weight. It setsAgent::drawn_target. With no such entry it falls back toassign_target(target, then the trip rotation).spawn_atisspawn_one(the walker) then, for a portal withmode_share,draw_leave, thenspawn_groupfor a visitor.draw_leavepicks park, pick-up or walk onrngby weight; park picks a lot with aportalby bay count and setsRide::ToCar { lot, id: None }, pick-up a kerb with aportalby bay count and setsRide::Pickup, and either makes that portal the target (or theexit_targetof a visitor) withdrawn_target.- then trains —
run_trains: a track whose doors have just opened splits itsalightcount over its doors (the remainder rotates between trains throughTrainState::alight_turn); each door then releases a row everyTrack::alight_row_s(60 × alight_lanes / alight_rate) while it has alighters left and its doors are open. A row is onespawn_at_pinper point ofTrack::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 within2 × radius + 0.04is 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::holdsand, for each crossing withspans, pushes aHoldat each span's entryswhen it is held: a signalled crossing fromCROSSING_CLEAR_S(3 s) before walk until walk ends, or any crossing with a walker found through aPedGridof walking, off-belt walkers (PedGrid::for_boxover the polygon's bounds grown byCROSSING_REACH, 1.5 m) who is on its floor and inside the polygon, or moving atSIGNAL_STILLor more, not held bySignalHold::Wait(last tick's), withpath_entrywithinCROSSING_REACH. - then traffic —
Traffic::step(net, time, dt)intraffic.rs, onScenario::traffic, with the sim time at the start of the tick setting signal aspects.Traffic::eventsis cleared.run_stopsmoves each kerb- or lot-bound vehicle'sStopthrough itsStopPhase: aGatecountdown ofgate_sends inToBay; aToBayvehicle whose front is on its bay's lane withinARRIVE_GAP(1 m) ofmouth_inand slower thanARRIVE_SPEED(0.5 m/s) stops and startsCrawlInforStop::crawls (bay distance overCRAWL_SPEED1.5 m/s, at leastCRAWL_MIN1 s), while one more thanARRIVE_GAPpast it goes back toApproachwithout a bay; a finishedCrawlInmoves the vehicle fromvehiclestoTraffic::asideasDocked, withStop::leftset to the kerb'spickup_swhenVehicle::pickup, else infinite at a kerb or lot with aportal(untilTraffic::release), else the kerb'sdwell_sor the lot'spark_s, and pushesEvent::Docked; a finishedCrawlOutclears the stop.pull_outcounts down each dockedleftand, once it is at most 0, moves the vehicle back onto its exit lane atmouth_outwith speed 0 when noHoldon that lane lies between its rear andmouth_outplus its gap,can_pull_outholds andNetwork::leavefinds a sink (the quickest by summed lanetravel_s, ties to the first sink): targetSink, that route,CrawlOut,Crawling, andEvent::Left.indexlists each lane's vehicles front first, with ties broken by id.assign_bayswalks each kerb's and lot's waiting vehicles (bound there, inApproach, nearest the head first, ties by id;takenis read off every vehicle'sStop::bay), gives bays as in vehicles, and copies the bay's pose and mouths into theStop; a lot withgate_sabove 0 sends its one served vehicle toGate. A waiting vehicle more thanARRIVE_GAPpast its head with no bay gets theleaveroute and aSinktarget. The lanes are re-indexed.mark_filterssetsVehicle::filtering.change_lanesrunslane_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,leg0, and clearsVehicle::passes.plandrops passes the vehicle is past, then rebuildslinksper vehicle: aStopat the end of a lane it must change out of, aStopone standstill gap past its queue head (hold_of: the kerb'sfrom, or the lot's gate) inApproachandGate, or pastmouth_ininToBay, aStopat each red or amber line it has noPass::Gofor (amber addsGoorHaltonce; green clears both), and per conflict, in two passes, gap acceptance (gap_open, grantingPass::Conflict), a give-wayStopat the zone entry until granted, and aFollowlink to every vehicle ahead in the zone order. Every vehicle's IDM acceleration is computed fromdesiredandleaderbefore any of them moves, then all are integrated andlateases toward its line; a vehicle at gate service or crawling (manoeuvring) is held at speed 0 instead, and every other addsdt − Δs / v0toVehicle::delay.advancecarries fronts past a lane end onto the next leg of the route, except onto a leg reached by a lane change, and removesSink-bound vehicles past their sink (Traffic::arrived, their delay intodelay_total).index,mark_filtersandplanrun again.settlewalks each lane front first and pulls back any vehicle withinMIN_GAP(0.2 m) of its leader, then setsGateorCrawlingfor those phases, elseQueued,Yielding(stopped at a give-way) orDriving; aYieldingvehicle held by a give-way stop addsdttoVehicle::waited, whichplanzeroes once it passes a conflict.spawnaddsrate_per_min × dt / 60to each source's backlog (capSOURCE_BACKLOG20) and emits one vehicle when the backlog is at least 1 andentry_clearholds (each vehicle counts whereatputs its front on the source lane, and those on the lanes it leads onto count past its end);drawpicks the target among those thatacceptsthe mode and gives a kerb- or lot-bound vehicle aStopinApproach. Before the draws, each pending pick-up call (Traffic::call_pickup) launches from its source onceentry_clearholds, one per source per step, with its called id, 0 occupants andVehicle::pickup; a source that launched one draws nothing that step.tallyaddsdtto the run time and, for each kerb whosekerb_queueis spilling, to its spill time. Lanes are indexed only overvehicles, so docked vehicles inasideare invisible to following, conflicts, lane changes andoverlaps;framedraws both.Traffic::holdsare read as stopped leaders and are never cleared by the step; whoever sets them clears them.Traffic::newseeds its ownLcgfromseed × 0x9e3779b9 ^ 0x5bd1e995. The rules are in vehicles. - then vehicles —
couple_vehicles. EachEvent::Dockedof a vehicle that is not a pick-up, at a kerb or lot whoseportalnames a pedestrian portal (portal_of), queues aPartyof itsoccupantsat that portal withRide::In { id, from }. EachReturnpast itsduebecomes aPartyheading for the portal of its kerb or lot, withRide::Pickupfrom a kerb orRide::ToCar { lot, id }from a lot.run_partiessteps out as many of each party as there is floor for: the first throughspawn_one(its target overridden when the party has one, visits cleared,came_byandrideset), the rest one at a time throughadd_members; a party left short waitsPARTY_RETRY_S(0.5 s,Party::retry) before trying again, and a vehicle'soccupantsdrops to 0 once all are out.run_pickupsgoes over walkers withRide::Pickup: an id no longerbound_forthat 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_minabove 0) can reach the kerb; a waiting walker whose vehicle standsDockedin that kerb gets in (Done,occupants+ 1).release_vehiclesreleases withtraffic::BOARD_S(3 s) every vehicle inasideheld for walkers (a pick-up, orleftinfinite) whose id no live walker'sRide::vehicle, no party and noReturnfrom a lot names. Thencrossing_openis set fromopen_crossings: a zebra is open whenTraffic::can_stop_forholds on every span, a signalled crossing while it shows walk.
- then trains —
- zone_holds — first entry into a zone with
stop_delay_szeros velocity until the delay elapses.- then visitors —
run_visitors, for visitors walking. Stamina drains bystamina_drain / 100× speed ×dtunless held, and rises byrecover_sitwhileloiteringon a seat orrecover_standwhile held on a Rest spot. A walker withAgent::spot(aSpot: point, floor,SpotKindBrowse, Rest or Wait,held, give-up time) is held there once withinARRIVE + 0.15, withhold_untilatBROWSE_S× 0.6–1.6 for a leader's Browse and infinite otherwise; an unreached spot is dropped atSPOT_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 reachesrest_to; a seated rester is released by the same test. Walkers whose party's lowest stamina is underrest_belowcallseek_resteveryREST_LOOK_S(3 s), never inside a connector.seek_restscores free seats (attractor_load0,p_affectabove 0) by centroid distance −REST_SEAT_BONUSandSim::rest_spotsby distance, plusREST_CROWD_M×CrowdMap::density+REST_TRAFFIC_M×Occupancy::traffic_near(passes per metre of width per minute overREST_SPACE). It skips rest spots with a walker or another spot withinREST_SPACE. A seat goes throughtake_seat; a standing spot becomes a RestSpot.rest_spotsis aREST_STEPlattice,standableatREST_CLEAR, with a solid or the floor edge withinREST_EDGEand no zone or portal box withinREST_MOUTH_GAP.logical_hopsanddrive_pointroute tospotwhile it is set; queues, gates, connectors, door lines andtry_despawnignore a walker holding one. - then groups —
run_groups.spawn_atcallsspawn_groupfor a visitor: a size fromgroup_sizes, thenadd_membersplaces that many less onestandable0.6–1.6 m round the leader onrng, withgroup= leader index + 1,leader,rank,Agent::membersextended, the leader'scame_byandride(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'starget,store_goal,visits_leftandexit_targetwhen they differ (copy_plan) and rebuilds. A member's ownnext_stopgoes throughfollow_plan: a Wait spot where it stands while the leader is in a store or bound for the one it just tried, otherwisecopy_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 aGROUP_STOP_RING(0.9 m) ring round it, at angle(rank + k) × 60°for the firststandablek; 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) callsgroup_steer.ped_socialtreats groupmates as in forces.offer_attractorsskips members.track_progressdoes not count a member withinGROUP_FORM_REACHof its leader, or a leader with a member more thangroup_gapoff, as stuck, andunstickskips a member near its leader.
- then visitors —
- offer_attractors / release_loiter — capture and release. See behaviours. A seat (
Attractor::seat) is offered withinSEAT_REACH(4 m) of its centroid when the walker's velocity points at it (cosine aboveSEAT_AHEAD, 0.3), and the offer bit clears pastSEAT_REACH + 1.seat_spotpicks 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 notsolid_at, nearest the walker.take_seatstores it inAgent::seat_fromand rebuilds only that walker's path.drive_pointandlogical_hopsuse the spot while it is set. WithinARRIVEof it the walker is moved to the centroid and held.leave_seatputs them back on the spot and sets the ignore bit of every seat. Onlyloiteringfreezes a captured walker (acceleration, integrate, unstick), so a seat taker still walks and is released when stuck. Seats are never nav blocks and never triggerrepath_others. - 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 passesconnector_serves. The cost in seconds is the walk tomouth_outsideatdesired_speed(at least the walk to thequeue_tail_hoppoint plus the line behind the mouth, when there is one), plusmax(0, ahead × CONNECTOR_HEADWAY_S / lanes − walk), plus the crossing time (axis_len / fixed_speed_mps, defaultCONNECTOR_BELT, for an escalator;axis_len / (desired_speed × speed_factor)plusSTAIR_UP_EFFORT_SorSTAIR_DOWN_EFFORT_Sfor a stair).aheadcounts walkers holding that connector on the same floor whose mouth distance is shorter.lanesis 1 whenis_single_file, elsewidth / CONNECTOR_LANE. The result is kept inAgent::connectorwith the floor it is for. It is re-weighed everyCONNECTOR_RECHECKs (staggered), withCONNECTOR_SWITCH_Shysteresis, and is final withinCONNECTOR_COMMITof the mouth. A change from the zone the path was built for (the held one, or else the nearest bychoose_walked_zone's biased distance) rebuilds the path.choose_walked_zonereturns the held connector while it still serves the walker on that floor.- then gates —
pick_gates: walkers withinGATE_LOOKof a gate bank weigh each serving gate (walk to the mouth plusGATE_WAITper person bound for it and closer, or riding) and keep the result inAgent::gate. Re-weighed everyGATE_RECHECKs withGATE_SWITCHhysteresis, final insideGATE_COMMIT. A change rebuilds the path;choose_gateand the queue step read the held gate.
- then gates —
- 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 rankk > 0holdsqueue_gap(QUEUE_GAP_GATE0.75 m at a gate,QUEUE_GAP_BELT0.6 m at an escalator) behind rankk − 1.Agent::queued_atis 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 thanQUEUE_REACHback. The chain's first link reachesQUEUE_HEAD(1.4 m) from the mouth, orQUEUE_HEAD_HELD(3 m) for a walker holding a ticket. A walker is bound for a zone when it is the target ofdestor of any of its nextQUEUE_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 thej-th aimsgap × jpast 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 theQUEUE_LOOKaim below instead. Both the chain and the slot aims setQueueAim::hold, which scalesv_desby((d − 0.12) / 0.8)clamped to 0–1, as for door lines. The head keeps its aim right up to boarding (theQUEUE_FREEcut-off applies only behind it): it holdsgapbehind the last rider on the belt while that point is short ofQUEUE_ENTER(0.3 m) into the mouth, and otherwise aimsQUEUE_ENTERin without holding, so it walks on board without slowing. Anyone else bound for the mouth outside a formed line aimsQUEUE_LOOKahead along the axis without holding. In the acceleration step,blocked_thin_mouthchecks only the single-file zone the walker is lined up on (its heldAgent::gate, or else one whose width it stands within), never a neighbour in the bank. While the next rider is still withinlane_spacing + 0.12of the entry, it scalesv_desby((short − 0.05) / 0.8)clamped to 0–1, whereshortis the distance left along the axis toZone::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 leastQUEUE_LINE_MIN(1.8 m) back is also stored inSim::queue_linesas anav::QueueLinefrom the mouth to the tail, andCrowdMap::queue_linehands it to path building.queue_tail_hopreturns the axis pointqueue_gapbehind that tail for a walker standing more thanqueue_corridor + QUEUE_SIDEoff the axis and not withinQUEUE_BESIDE(2.5 m) of it level with or ahead of the tail, if the point is on the floor and outside obstacles.waypoints_forroutes a hop into a belt mouth through that point first, usingnav::route_around_exwithSoft { end_free: QUEUE_TAIL_FREE, .. }, which waives crowd and queue cost only within 1 m of the point. EveryQUEUE_REPLAN_TICKS(20, staggered by index), an unranked walking walker whose remaining waypoints reach a belt mouth getsrebuild_onewhen none of its waypoints before the mouth lies withinQUEUE_TAIL_MOVED(1.5 m) of the current tail hop.- then door_lines —
plan_lines. Linelis doorl / 2, side+1(ahead along travel) whenlis even. Walkers bound for a door, walking on its floor and not in a connector, choose a line on that track everyLINE_REWEIGH(1 s). The cost in metres isLINE_WALK_W(0.5) × the walk to the tail slot, plusLINE_PERSON_M(4 m) per walker joined or coming, pluspull × |door along − preferred along|, plusLINE_MOUTH_M(12 m) ×mouth_weightof the tail slot, plusLINE_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 apullup toLINE_PREF_MAX(1.2), both golden-ratio sequences.mouth_weightis1 − out / MOUTH_KEEPfor a point up toMOUTH_KEEP(4 m) outside a connector end at this floor and withinMOUTH_SIDE(1 m) of its width.LINE_SWITCH_M(2 m) hysteresis. A switch setstargetto that door's portal and rebuilds the path. Each column of a line is its own file:TrainState::colsholds its start and length inslots, andTrainState::slot(line, col, row)reads it.next_colis 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 innext_colorder; only the nearest takes a ticket (Agent::line_seq) and its column (Agent::line_col) withinLINE_JOIN(0.9 m) of its slot. Joined walkers hold rowkof 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 everyBOARD_GAP_S(1 s), but not while its door still has alighters to come (alight_left > 0) or one on its exit leg is withinDOOR_CLEAR(1.5 m) of the door; the walker setsboardingand headsDOOR_AIM_DEPTH(0.4 m) into the door inset. Aims are kept inTrainState::aimonly withinLINE_AIM_REACH(10 m), with a clear straight walk, and (unless boarding) not whilestepping_off; otherwise the path leads.plan_queuesskipsstepping_offwalkers too. Each occupied column is also pushed toSim::queue_linesas anav::QueueLinefrom its first to its last joined slot, with zone idzones.len() + door, so other walkers route round it; the door's own boarders and alighters pass it asown_queue. Each line's slots are built at load fromTrack::slot_atcolumn by column (train::LINE_COLS_MAX3; depthLINE_ROWS_MAX6, orLINE_LAST_ROWS8 for the last column). A column stops at the first slot that is not walkable, lies in a connector mouth (mouth_weight > 0), is withinLINE_POST_CLEAR(1 m) of a hole or obstacle (near_solid), is withinLINE_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_trainsgives each walker it spawns at a doorAgent::exit_to, a point straight out from the door (door_exit). Its depth is the deepest joined slot of that door plusEXIT_PAST(0.8 m), clamped toEXIT_MIN–EXIT_MAX(2–7 m), at the walker's own offset across the door clamped toEXIT_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_hopsmakes it the first hop, built withSoft::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 overQUEUE_SHIM_SPANclamped to ±1.plan_linesclearsexit_toonce 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_signalsfillsSim::signal_holdfor walkers who are walking, not on a belt and not captured. Open meansSim::crossing_open. A walker on an open crossing (same floor, inside the polygon) getsAgent::crossing_ok; one on no crossing has it cleared. On a closed crossing the hold isHurrywithcrossing_ok, elseWait(0). Off it,path_entrywalks the remaining waypoints (ordest) from the walker inSIGNAL_STEP(0.3 m) samples up toSIGNAL_REACH(8 m) and bisects the first sample inside the polygon five times; the nearest entrydover closed crossings givesWait(d). In the acceleration stepWait(d)scalesv_desby((d − SIGNAL_CURB) / SIGNAL_EASE)clamped to 0–1 (0.35 m, 1.6 m), andHurrybySIGNAL_HURRY(1.3). After that, whenever the scenario has crossings and the walker is notHurry,curb_gaptakes the nearest body-to-body gap to a body on its touch level that is in front (ahead > 0, and withinSIGNAL_SPACE_SIDE(0.7 m) of the heading line orahead ≥ SIGNAL_SPACE_CONE(0.5) × distance) and blocks: for aWaitwalker, anyone held byWaitor slower thanSIGNAL_STILL(0.3 m/s); for anyone else, aWaitwalker whose heading has a dot product above 0.5 with theirs.v_desis scaled by((gap − SIGNAL_SPACE) / SIGNAL_SPACE_EASE)clamped to 0–1 (0.45 m, 0.7 m), before the headway cap.
- then door_lines —
- snapshot — one
Bodyper agent: position, velocity (for the look-ahead avoidance insight_scan), floor key, heading, whether they are in a stair, and drawn heighth(the flight height from stair progress, elsez). Sight useshwithinSIGHT_RISE; contact across a stair mouth usesTOUCH_RISE. The pedestrian hash grid is 2 m cells. - acceleration — pure function of the snapshot. Parallel when
rayonis on and there are at least 128 agents. Writesax, ayand the split termsfd*,fw*,fp*. - integrate — semi-implicit Euler for walkers who are not held and not on a conveyor.
v += a dt, cap speed, then for a walker withv_des >= 0.08trim any backward speed along the drive heading (Deriv::hx, hy, after sight yaw) toBACK_MAX(0.2 m/s), andx += v dt. A step from open floor intosolid_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 withoutcrossing_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 withoutcrossing_okwho 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 byplan_signalsduring flashing. Drop a travelled crumb every 0.4 m (cap 80). - connector_kinematics — board escalators and gates (from
BOARD_REACHbefore 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 atBELT_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.diragainstruns the belt the other way; a two-way gate boards at either mouth. - project_z — snap
zto the nearest floor. - track_approaches — remember which connector the walker entered, and at what progress.
- 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).
- track_progress — accumulate
stuck_swhen 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 bySignalHold::Wait, is never stuck. Nor is one, notHurry, slower thanSTUCK_SLOWwhose path enters a closed crossing withinSIGNAL_CROWD_REACH(20 m): they are standing behind the curb crowd. A walker held (hold_untilahead) is skipped, which covers one waiting for a pick-up. - unstick — after
unstick_s, the recovery in behaviours.closest_blockerpicks the hole or obstacle to mute; a hole is not muted near a wrong stair mouth, or whenlift_shaftholds (the centroid of its ring, fromwalls::hole_ring, lies in a polygon landing of a pair with no walked zone on that floor). - 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'slaneshare of the leg slack). AWaypoint::firmpoint 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 withinSTEP_OFFpast the line.fire_stairkeeps the plan position and velocity. Intry_despawn, a walker withRide::Pickupreaching its kerb portal is not removed: it setswaiting, stops, and is held (hold_untilinfinite) for vehicles. One withRide::Inis removed and counted into theReturnfor its vehicle id, created at the first withdue= now + that portal'sstay_s. - 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.
- 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_aheadprices the nextCROWD_LOOK(15 m) of the path. Samples withinCROWD_SELFof the walker, and withinQUEUE_REACHof the first gate or escalator mouth on the path, are not counted. If the cost has grown by more thanCROWD_REPLANoverAgent::crowd_seen, the path is rebuilt;crowd_seentracks the lowest cost since. - contacts —
resolve_contactswith 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_untilahead). Pairs found through aPedGridof2 × largest radiuscells and passingsame_touchare split tor_i + r_j, Gauss–Seidel in index order, up toCONTACT_PASSES(16) sweeps until none is deeper thanCONTACT_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_applieson itswall_floor, exact crossing insegments_cross) slides along that segment instead. If the slide crosses a wall too, or the share would enter a closed crossing withoutcrossing_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. - occupancy — accumulate the grid and the per-walker 1.5 m disk density, then update
Scenario::crowd(nav::CrowdMap) with the walking bodies andSim::queue_lines. The same pass counts each walker'sCRUSH_RADIUS(1 m) disk intoAgent::crush_density.Agent::crush_sgrows bydtwhile that is at leastlos::CRUSH_DENSITYand the walker is not on a belt or captured, and resets otherwise.track_crushfolds walkers withcrush_s ≥ CRUSH_HOLD_SintoSim::crush. A walker joins a hotspot on its floor that is withinCRUSH_MERGEand was flagged withinCRUSH_CLEAR_S, which moves the spot toward it by a running mean; otherwise it starts a new one. At mostCRUSH_SPOTSare kept, and the oldest cleared spot is dropped first. - 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.