ctrl_link.cpp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. #include "ctrl_link.h"
  2. #include "radio.h"
  3. #include "ctrl_bonds.h"
  4. #include "ctrl_usb.h"
  5. #include "report45.h"
  6. #include "deck_input.h"
  7. #include "triton.h"
  8. #include <Adafruit_TinyUSB.h>
  9. #include <Arduino.h>
  10. #include <string.h>
  11. int g_linkSlot = -1;
  12. unsigned long g_linkAliveMs = 0;
  13. // Link is considered up while we've heard a poll within this window (the puck polls ~250 Hz).
  14. #define LINK_ALIVE_MS 1000u
  15. // RX dwell per loop. One transaction (catch a poll, reply) per loop -> ~200 Hz, plenty for input, and
  16. // short enough that USB/CDC servicing isn't starved. Tunable on hardware.
  17. #define RX_WIN_US 4500u
  18. // Time the puck's RX window stays open for our reply; keep the turnaround well under this.
  19. static uint8_t g_seq = 0;
  20. // Last time we caught a frame on the SESSION address (NOT the ch2 discovery beacon). This is the
  21. // truthful "we're actually exchanging with the puck" signal that drives ctrlLinkUp() / the UI
  22. // "available" tile -- hearing the discovery beacon alone does NOT mean the session works.
  23. static unsigned long g_sessRxMs = 0;
  24. // SESSION-CHANNEL HUNT. The E1 host frame advertises a "primary" channel, but that channel mostly
  25. // carries the puck's E1/E2 keepalive -- the ACTIVE session (E3 input poll + our F1 reply) rides a small
  26. // channel set, NOT necessarily the advertised primary (proven by the real-puck capture sniff1.json:
  27. // E1/E2 on ch68, the E3/F1 session on ch48; and by puck_sniffer.ino's session-hunt). So once we adopt a
  28. // session we DWELL across {primary, then these candidates} on the session address until a poll (E2/E3)
  29. // appears, then PARK on that channel. If the parked channel goes dry (QoS hop / puck moved), un-park and
  30. // hunt again. Mirrors puck_sniffer.ino HUNT_CH.
  31. static const uint8_t HUNT_CH[] = { 2, 80, 18, 46, 76, 22, 68, 52, 55, 56, 57 };
  32. static uint8_t g_huntIdx = 0;
  33. static uint8_t g_parkCh = 0; // 0 = hunting; non-zero = parked on this channel
  34. static unsigned long g_huntStepMs = 0;
  35. #define HUNT_DWELL_MS 40u // per-channel dwell while hunting
  36. #define RECONNECT_MS \
  37. 4000u // dry this long -> abandon the session, re-catch E1 on ch2
  38. // connected-mode frame-type diagnostics (reset + printed each second in the status line)
  39. static uint16_t g_cntE2, g_cntE3, g_cntE7, g_cntE4, g_cntOut, g_cntOther;
  40. // of the E3 polls: how many were bare/0x45 INPUT polls vs feature/identity GETs (0x83/0xAE/0x87). Tells
  41. // us whether the puck is actually polling for input or stuck looping enumeration GETs.
  42. static uint16_t g_cntE3in, g_cntE3get;
  43. // last non-0x45 report id the puck asked for via an E3 GET sub-TLV (0 = none)
  44. static uint8_t g_getReq = 0;
  45. // distinct channels seen in E4 channel-map commands this second (128-bit) -> reveals the hop pattern:
  46. // a small fixed set is followable; a large/changing set is the unsolved fast-AFH sequence.
  47. static uint8_t g_e4set[16];
  48. // 256-bit: frame types we've already dumped a sample of
  49. static uint8_t g_seenDump[32];
  50. // 256-bit: GET report ids we've dumped a poll+response sample for
  51. static uint8_t g_seenReq[32];
  52. // ---- build replies into rftx (ESB DPL RAM layout: [LENGTH][S1][payload...]) ----
  53. static void buildF1()
  54. {
  55. // payload = [0xF1][tlen][ttype=6][report45(46)] -> length = 3 + REPORT45_LEN
  56. rftx[0] = (uint8_t)(3 + REPORT45_LEN); // LENGTH (payload byte count)
  57. rftx[1] = (uint8_t)((((g_pid++) & 3) << 1) | 1); // S1: PID<<1 | NO_ACK
  58. rftx[2] = 0xF1;
  59. rftx[3] = REPORT45_LEN; // TLV len
  60. rftx[4] = 6; // TLV type 6 (HID report)
  61. // neutral report (idle controller) when not forwarding or input has gone stale
  62. bool fresh = deckForwarding() && (millis() - deckLastInputMs() < 200u);
  63. if (fresh) {
  64. buildReport45(rftx + 5, g_seq++);
  65. } else {
  66. memset(rftx + 5, 0, REPORT45_LEN);
  67. rftx[5] = 0x45;
  68. rftx[6] = g_seq++;
  69. }
  70. }
  71. // Build an F1 carrying a feature/command RESPONSE (type-6 TLV value = [cmd][len][payload], the
  72. // command-channel form a real controller returns -- WITH the inner length byte, unlike the 0x45 input
  73. // report) for when the puck relays one of Steam's enumeration GETs (0x83 attrs / 0xAE serial / 0x87
  74. // settings) over RF. The puck relays this value straight back to Steam as the feature response.
  75. static void buildFeatureF1(uint8_t reqid, const uint8_t *param, uint8_t plen)
  76. {
  77. rftx[1] = (uint8_t)((((g_pid++) & 3) << 1) | 1);
  78. rftx[2] = 0xF1;
  79. rftx[4] = 6; // TLV type 6
  80. uint8_t v[64];
  81. uint8_t vlen = ctrlFeatureResp(reqid, param, plen, v);
  82. if (vlen > 60)
  83. vlen = 60;
  84. rftx[3] = vlen; // TLV len
  85. memcpy(rftx + 5, v, vlen);
  86. rftx[0] = (uint8_t)(3 + vlen); // LENGTH (payload byte count)
  87. }
  88. static void buildF3()
  89. {
  90. // Reply to the puck's E7 awake/version probe with a STATUS report. The real puck's RX parser
  91. // (proteus FUN_0000ba38, F3 branch) reads the connected-state byte at payload[1] (== rftx[3]) and
  92. // only raises its internal "controller connected" signal if that byte is NONZERO -- so leaving it 0
  93. // makes the puck bond us but never ACTIVATE us (no enumeration). Set payload[1]=1 (connected). Also
  94. // set payload[4] (rftx[6]=1) which is where OpenPuck reads its F3 version, so this works both ways.
  95. rftx[0] = 5;
  96. rftx[1] = (uint8_t)((((g_pid++) & 3) << 1) | 1);
  97. rftx[2] = 0xF3;
  98. // payload[1] = connected state (real puck reads param_2[6]) -- MUST be nonzero
  99. rftx[3] = 1;
  100. rftx[4] = 0;
  101. rftx[5] = 0;
  102. rftx[6] = 1; // payload[4] = version (OpenPuck reads rfrx[6])
  103. }
  104. // Transmit whatever is staged in rftx on the currently-configured channel/address, then return to RX
  105. // config. Mirrors rf_link's TX path; radio is left disabled at exit.
  106. static void txStaged()
  107. {
  108. NRF_RADIO->TASKS_DISABLE = 1;
  109. RWAIT_DISABLED();
  110. NRF_RADIO->EVENTS_DISABLED = 0;
  111. NRF_RADIO->PACKETPTR = (uint32_t)rftx;
  112. NRF_RADIO->SHORTS = RADIO_SHORTS_READY_START_Msk |
  113. RADIO_SHORTS_END_DISABLE_Msk;
  114. NRF_RADIO->EVENTS_DISABLED = 0;
  115. NRF_RADIO->TASKS_TXEN = 1;
  116. RWAIT_DISABLED();
  117. NRF_RADIO->EVENTS_DISABLED = 0;
  118. NRF_RADIO->PACKETPTR = (uint32_t)rfrx;
  119. }
  120. // One RX->maybe-reply transaction on (base,prefix,ch). Returns the received type byte (rfrx[2]) or 0.
  121. // `reply` selects whether we answer polls (true once connected) or just listen (discovery).
  122. static uint8_t rxOnce(const uint8_t *base, uint8_t prefix, uint8_t ch,
  123. uint16_t winUs, bool reply)
  124. {
  125. rfConfig(ch);
  126. rfSetAddr(base, prefix);
  127. NRF_RADIO->PACKETPTR = (uint32_t)rfrx;
  128. rfrx[0] = 0;
  129. NRF_RADIO->SHORTS = RADIO_SHORTS_READY_START_Msk;
  130. NRF_RADIO->EVENTS_END = 0;
  131. NRF_RADIO->TASKS_RXEN = 1;
  132. uint32_t t0 = micros();
  133. while (!NRF_RADIO->EVENTS_END && (uint32_t)(micros() - t0) < winUs) {
  134. }
  135. uint8_t type = 0;
  136. if (NRF_RADIO->EVENTS_END) {
  137. NRF_RADIO->EVENTS_END = 0;
  138. g_rfRxCount++;
  139. bool crcok = NRF_RADIO->CRCSTATUS & 1;
  140. uint8_t rxlen = rfrx[0];
  141. if (crcok && rxlen && rxlen <= 96) {
  142. type = rfrx[2];
  143. if (reply) {
  144. // DIAGNOSTIC: tally what the puck polls us with, and dump a sample of any frame we
  145. // DON'T answer with input -- this is how we find identity/feature queries (the likely
  146. // reason Steam can't enumerate the controller). Surfaced in the 1Hz status line.
  147. if (type == 0xE2)
  148. g_cntE2++;
  149. else if (type == 0xE3) {
  150. g_cntE3++;
  151. // E3 may carry a GET sub-TLV [len][01=GET][reportid]; split input vs feature GET
  152. if (rxlen >= 4 && rfrx[4] == 0x01 &&
  153. rfrx[5] != 0x45) {
  154. g_getReq = rfrx[5];
  155. g_cntE3get++;
  156. } else
  157. g_cntE3in++;
  158. } else if (type == 0xE7)
  159. g_cntE7++;
  160. else if (type == 0xE4)
  161. g_cntE4++;
  162. else if (type >= 0x80 && type <= 0x89)
  163. g_cntOut++;
  164. else if (type != 0xE1)
  165. g_cntOther++;
  166. // dump first sample of any non-input-poll type (E1/E4/E7/output/other)
  167. if (type != 0xE2 && type != 0xE3 &&
  168. !(g_seenDump[type >> 3] &
  169. (1 << (type & 7)))) {
  170. g_seenDump[type >> 3] |=
  171. (1 << (type & 7));
  172. if (Serial.availableForWrite() > 70) {
  173. Serial.printf(
  174. "# RFtype %02X len%u:",
  175. type, rxlen);
  176. uint8_t m = rxlen + 2 < 26 ?
  177. rxlen + 2 :
  178. 26;
  179. for (uint8_t i = 0; i < m; i++)
  180. Serial.printf("%02X",
  181. rfrx[i]);
  182. Serial.println();
  183. }
  184. }
  185. // E1 = host-frame keepalive (no reply expected); follow channel hops.
  186. if (type == 0xE1) {
  187. uint8_t newCh = rfrx[11];
  188. if (newCh && newCh != g_sessCh)
  189. g_sessCh = newCh;
  190. } else if (type == 0xE4) {
  191. // channel-map update [E4][ch][02][50]. Once we report connected (F3), the
  192. // real puck switches to SYNCHRONIZED freq+ADDRESS hopping: per RF_CONNECTED.md
  193. // it rotates (base,prefix) AND channel each timeslot from a sequence seeded by
  194. // the session address -- the channel-map generator the RE never cracked. E4's
  195. // [ch] is only the channel; we'd also need to rotate the prefix in lockstep, so
  196. // jumping to E4's channel alone lands us on the WRONG ADDRESS -> rx=0 -> flap.
  197. // So DON'T chase it -- just record it; the hunt/park logic re-finds the puck.
  198. // (Real-puck connected mode needs that hop generator; use an OpenPuck for a
  199. // working single-channel link.)
  200. uint8_t nc = rfrx[3];
  201. if (rxlen >= 2 && nc >= 1 && nc <= 80)
  202. g_e4set[nc >> 3] |=
  203. (1 << (nc & 7));
  204. } else if (!deckForwarding()) {
  205. // NOT forwarding -> stay SILENT (no F3/F1). We still hear the puck's polls
  206. // (so the UI shows the puck "available"), but we never answer, so the puck
  207. // never marks us connected and the HOST shows no controller. The controller
  208. // only appears once the user taps to forward (deckForwarding() -> true).
  209. } else if (type == 0xE7) {
  210. buildF3();
  211. txStaged();
  212. } else if (type >= 0xE0 && type <= 0xEF) {
  213. // E3 GET: if it requests a NON-0x45 report (Steam enumeration -> 0x83
  214. // attrs / 0xAE serial / 0x87 settings), answer THAT report; a bare poll
  215. // or a 0x45 request gets the input report.
  216. if (type == 0xE3 && rxlen >= 4 &&
  217. rfrx[4] == 0x05) {
  218. // Relayed Steam OUTPUT (SET): E3 [tlvlen][05][rid][data]. The puck
  219. // forwards Steam's rumble/haptic/LED writes this way (legacy type-05
  220. // form). Hand rumble (0x80) / haptic pulses (0x82/0x8F) to the Deck so
  221. // it buzzes; relays are NO_ACK so we DON'T reply.
  222. uint8_t rl =
  223. (rfrx[3] >= 1) ?
  224. (uint8_t)(rfrx[3] -
  225. 1) :
  226. 0;
  227. deckForwardHaptic(rfrx[5],
  228. &rfrx[6], rl);
  229. } else if (type == 0xE3 && rxlen >= 4 &&
  230. rfrx[4] == 0x01 &&
  231. rfrx[5] != 0x45) {
  232. uint8_t plen =
  233. (rxlen >= 5) ?
  234. (uint8_t)(rxlen -
  235. 4) :
  236. 0;
  237. buildFeatureF1(rfrx[5],
  238. &rfrx[6], plen);
  239. // dump the GET poll + our response once per reqid (verify framing)
  240. uint8_t rq = rfrx[5];
  241. if (!(g_seenReq[rq >> 3] &
  242. (1 << (rq & 7))) &&
  243. Serial.availableForWrite() >
  244. 100) {
  245. g_seenReq[rq >> 3] |=
  246. (1 << (rq & 7));
  247. Serial.printf(
  248. "# GET %02X poll[",
  249. rq);
  250. uint8_t m =
  251. rxlen + 2 < 20 ?
  252. rxlen + 2 :
  253. 20;
  254. for (uint8_t i = 0;
  255. i < m; i++)
  256. Serial.printf(
  257. "%02X",
  258. rfrx[i]);
  259. Serial.print("] resp[");
  260. uint8_t r =
  261. rftx[0] + 2 < 30 ?
  262. rftx[0] +
  263. 2 :
  264. 30;
  265. for (uint8_t i = 0;
  266. i < r; i++)
  267. Serial.printf(
  268. "%02X",
  269. rftx[i]);
  270. Serial.println("]");
  271. }
  272. } else {
  273. buildF1();
  274. }
  275. txStaged();
  276. }
  277. }
  278. return type;
  279. }
  280. }
  281. NRF_RADIO->TASKS_DISABLE = 1;
  282. RWAIT_DISABLED();
  283. NRF_RADIO->EVENTS_DISABLED = 0;
  284. return type;
  285. }
  286. // Listen on the shared "ibex"/ch2 rendezvous for a discovery E1 that matches a stored bond. On a match,
  287. // adopt the advertised session base/prefix/channel and switch to connected.
  288. static void discoveryScan()
  289. {
  290. uint8_t t = rxOnce(g_rfBase, g_rfPrefix, 2, RX_WIN_US, false);
  291. if (t != 0xE1)
  292. return;
  293. const uint8_t *proteus =
  294. rfrx + 3; // payload [proteus_uuid 4][ibex_uuid 4]
  295. const uint8_t *ibex = rfrx + 7;
  296. int slot = ctrlBondMatch(proteus, ibex);
  297. if (slot < 0) {
  298. // We hear the puck's beacon but its UUIDs don't match any stored bond -- the controller
  299. // side of the pairing didn't land, or we're paired to a different puck. Very useful signal.
  300. static unsigned long lastMiss = 0;
  301. if (Serial.availableForWrite() > 40 &&
  302. millis() - lastMiss > 1000) {
  303. lastMiss = millis();
  304. Serial.printf(
  305. "# E1 heard, no bond match: puuid=%02X%02X%02X%02X iuuid=%02X%02X%02X%02X\n",
  306. proteus[0], proteus[1], proteus[2], proteus[3],
  307. ibex[0], ibex[1], ibex[2], ibex[3]);
  308. }
  309. return;
  310. }
  311. // adopt the advertised session address (rfHostFrameOnce layout: ch@[11], base@[15..19], prefix@[19])
  312. uint8_t advCh = rfrx[11];
  313. memcpy(g_sessBase, rfrx + 15, 4);
  314. g_sessPrefix = rfrx[19];
  315. if (advCh)
  316. g_sessCh = advCh;
  317. g_linkSlot = slot;
  318. g_linkAliveMs = millis();
  319. g_parkCh = 0; // start hunting the session channel set from scratch
  320. g_huntIdx = 0;
  321. g_huntStepMs = millis();
  322. deckText("link: adopted session");
  323. if (Serial.availableForWrite() > 40)
  324. Serial.printf(
  325. "# LINK adopted slot=%d base=%02X%02X%02X%02X/%02X ch=%u\n",
  326. slot, g_sessBase[0], g_sessBase[1], g_sessBase[2],
  327. g_sessBase[3], g_sessPrefix, g_sessCh);
  328. }
  329. bool ctrlLinkUp()
  330. {
  331. // "up" = we caught a real SESSION-address frame recently (a poll/keepalive on the adopted
  332. // base+channel), not merely the ch2 discovery beacon -- so the UI tile only goes tappable when
  333. // the puck is genuinely polling us.
  334. return g_linkSlot >= 0 && (millis() - g_sessRxMs) < LINK_ALIVE_MS;
  335. }
  336. void ctrlLinkTask()
  337. {
  338. // nothing to do until at least one puck is bonded
  339. bool anyBond = false;
  340. for (int i = 0; i < NBOND; i++)
  341. if (g_bond[i].used)
  342. anyBond = true;
  343. if (!anyBond) {
  344. static unsigned long lastNb = 0;
  345. if (Serial.availableForWrite() > 40 &&
  346. millis() - lastNb > 2000) {
  347. lastNb = millis();
  348. Serial.println(
  349. "# no bonds stored -- pair first (controller never got 0xEE)");
  350. }
  351. g_linkSlot = -1;
  352. return;
  353. }
  354. // 1 Hz status line for a serial monitor: searching vs connected, channel, RX count.
  355. {
  356. static unsigned long stMs = 0;
  357. static uint32_t lastRx = 0;
  358. if (Serial.availableForWrite() > 50 &&
  359. millis() - stMs >= 1000) {
  360. stMs = millis();
  361. Serial.printf(
  362. "# link slot=%d %s ch=%u park=%u rx=%lu/s\n",
  363. g_linkSlot,
  364. ctrlLinkUp() ? "CONNECTED" : "searching",
  365. g_sessCh, g_parkCh,
  366. (unsigned long)(g_rfRxCount - lastRx));
  367. // poll-type histogram. e3in = INPUT polls (we send 0x45); e3get = identity/feature GETs.
  368. // If e3in stays ~0 while e3get is high, the puck is stuck enumerating and never asks for
  369. // input -> the controller can't show as active no matter what we send.
  370. Serial.printf(
  371. "# polls e3in=%u e3get=%u e2=%u e7=%u e4=%u out=%u oth=%u getReq=%02X\n",
  372. g_cntE3in, g_cntE3get, g_cntE2, g_cntE7,
  373. g_cntE4, g_cntOut, g_cntOther, g_getReq);
  374. // if the puck flooded E4 this second, list the distinct channels it commanded
  375. if (g_cntE4) {
  376. char cb[160];
  377. int n = 0;
  378. for (int c = 0;
  379. c <= 100 && n < (int)sizeof(cb) - 5; c++)
  380. if (g_e4set[c >> 3] & (1 << (c & 7)))
  381. n += snprintf(cb + n,
  382. sizeof(cb) - n,
  383. "%d ", c);
  384. cb[n] = 0;
  385. Serial.printf("# E4 channels: %s\n", cb);
  386. }
  387. memset(g_e4set, 0, sizeof g_e4set);
  388. g_cntE2 = g_cntE3 = g_cntE7 = g_cntE4 = g_cntOut =
  389. g_cntOther = g_cntE3in = g_cntE3get = 0;
  390. lastRx = g_rfRxCount;
  391. // re-arm the one-shot frame/GET dumps every ~8s so they stay capturable in the app log
  392. static uint8_t reArm = 0;
  393. if (++reArm >= 8) {
  394. reArm = 0;
  395. memset(g_seenReq, 0, sizeof g_seenReq);
  396. memset(g_seenDump, 0, sizeof g_seenDump);
  397. }
  398. }
  399. }
  400. // No session adopted yet -> hunt the E1 host frame on the shared ch2/"ibex" rendezvous.
  401. if (g_linkSlot < 0) {
  402. discoveryScan();
  403. return;
  404. }
  405. // Adopted a session, but the puck has gone fully dry for a long time -> abandon it and re-catch the
  406. // E1 (its primary channel may have changed). Only after RECONNECT_MS so a hunt sweep has a chance.
  407. if ((millis() - g_linkAliveMs) >= RECONNECT_MS) {
  408. g_linkSlot = -1;
  409. g_parkCh = 0;
  410. g_huntIdx = 0;
  411. deckText("link: lost, rescanning");
  412. if (Serial.availableForWrite() > 40)
  413. Serial.println("# LINK lost, rescanning ch2");
  414. return;
  415. }
  416. // Pick the channel to listen on: parked channel if we have one, else sweep {primary, HUNT_CH...}.
  417. uint8_t tryCh = g_parkCh ? g_parkCh :
  418. (g_huntIdx == 0 ?
  419. g_sessCh :
  420. HUNT_CH[(g_huntIdx - 1) %
  421. (uint8_t)sizeof HUNT_CH]);
  422. uint8_t t = rxOnce(g_sessBase, g_sessPrefix, tryCh, RX_WIN_US, true);
  423. if (t == 0xE1) {
  424. // keepalive beacon on the session base: refreshes the advertised primary channel (rxOnce already
  425. // copied it into g_sessCh) + proves the puck is present, but it is NOT a poll -> keep hunting.
  426. g_linkAliveMs = millis();
  427. } else if (t >= 0xE0 && t <= 0xEF) {
  428. // a real poll (E2/E3/E7) -> the session lives on THIS channel; PARK and answer it from here.
  429. bool wasUp = (millis() - g_sessRxMs) < LINK_ALIVE_MS;
  430. g_parkCh = tryCh;
  431. g_sessCh = tryCh;
  432. g_linkAliveMs = g_sessRxMs = millis();
  433. if (!wasUp) {
  434. deckText("link: session live");
  435. if (Serial.availableForWrite() > 40)
  436. Serial.printf(
  437. "# SESSION live: type=%02X ch=%u base=%02X%02X%02X%02X/%02X\n",
  438. t, tryCh, g_sessBase[0], g_sessBase[1],
  439. g_sessBase[2], g_sessBase[3],
  440. g_sessPrefix);
  441. }
  442. } else {
  443. // nothing this dwell. parked channel gone dry -> resume hunting (QoS hop / puck moved)
  444. if (g_parkCh && (millis() - g_sessRxMs) >= LINK_ALIVE_MS)
  445. g_parkCh = 0;
  446. if (!g_parkCh && (millis() - g_huntStepMs) >= HUNT_DWELL_MS) {
  447. g_huntStepMs = millis();
  448. g_huntIdx++;
  449. }
  450. }
  451. }