[{"data":1,"prerenderedAt":1485},["ShallowReactive",2],{"post-\u002Fposts\u002Freverse-engineering-fortnite-festival":3},{"id":4,"title":5,"body":6,"description":15,"extension":1476,"meta":1477,"navigation":772,"order":93,"path":1479,"seo":1480,"stem":1481,"summary":1482,"visibility":1483,"__hash__":1484},"posts\u002Fposts\u002Freverse-engineering-fortnite-festival.md","Fortnite Festival from the Outside",{"type":7,"value":8,"toc":1456},"minimark",[9,16,19,35,38,41,48,53,56,59,62,65,79,135,138,152,218,255,259,262,327,330,452,474,478,481,484,487,490,493,497,500,510,520,524,527,530,533,536,539,609,614,617,620,623,626,630,633,642,645,649,652,656,667,845,860,863,867,870,873,880,887,890,997,1015,1019,1022,1025,1030,1038,1042,1045,1048,1051,1054,1057,1060,1064,1067,1070,1074,1077,1081,1090,1101,1107,1122,1191,1206,1261,1264,1271,1425,1439,1443,1446,1449,1452],[10,11,12],"p",{},[13,14,15],"em",{},"Disclaimer: this is done for educational and research purposes only. I will not be making the source code public.",[10,17,18],{},"Through the Fire and Flames runs seven minutes and the part that beats me is deep into it. I played it enough that my hands memorised a wrong version, and now the real notes and the remembered notes fight each other on the way down the highway. I've had that block for over a year, most of the time the song has existed in Festival. Every attempt at the section that's beating me starts at the top of the song.",[10,20,21,22,26,27,34],{},"In any other rhythm game this is solved. I've played them most of my life: osu! around 2016, Robeats religiously through lockdown ",[23,24,25],"note",{},"Robeats because I was deep in the Roblox community at the time, primarily Discord bots and QA for Roblox-related communities. The rhythm game is the part of it I kept.",", then ",[28,29,33],"a",{"href":30,"rel":31},"https:\u002F\u002Fquavergame.com",[32],"nofollow","Quaver",", where I peaked inside the global top 1,700 in 4K. You convert the chart, mirror it, and your pattern memory stops recognising it, so you read it fresh again. That trick dies in Festival, because Festival is five lanes and a five-lane mirror isn't the clean swap a four-lane mirror is: the middle stays put and the hand balance changes completely. There's no practice mode either, no section looping, no speed control, none of what every rhythm game I've touched in a decade ships with.",[10,36,37],{},"I bounced off Festival at launch, for what it's worth: no scroll speed control, the old pre-\"pro\" colours, and lift notes I could not read at all. What pulled me back was a TikTok showing Magical Cure! Love Shot! in the game. It looked fun, I reinstalled, and I full-comboed it within a few days despite never having played five keys before. Keep the lifts in mind, because the mechanic that ran me off is the one I ended up teaching the tooling to draw.",[10,39,40],{},"So I built it. Rust is a language I've spent years in and this was the right kind of problem for it, something I needed and something I'd learn from.",[10,42,43,44,47],{},"Festival's publicly known Spark Tracks API ",[23,45,46],{},"The most thoroughly researched API in the game, in the sense that everyone who has ever gone looking has found it, and it still only hands back track data."," hands you song data and nothing else. No score, no live state, not even where you are in the song. Everything here reads the game from the outside: it reverse-engineers the scoring off pixels recorded from the screen, and drives an overlay that syncs to a live song by pattern-matching my own playing against the chart. None of it touches the game's code or memory.",[49,50,52],"h2",{"id":51},"the-shape","The shape",[10,54,55],{},"The order things got built in is why the architecture looks the way it does, and the first piece predates the project by a long way.",[10,57,58],{},"I sometimes follow Star Power paths, the optimiser-computed points in a song where activating overdrive is worth the most, and holding a path in your head mid-song is its own problem. So at some point I made a small overlay that fetched the chart's path from CHOpt, watched Festival's log file to see which song had started, and drew the path on screen while I played. It synced by me pressing a key to step through the activations. Manual and dumb, it worked, and then it sat there for months.",[10,60,61],{},"The actual project started when Flames finally made me snap. I built the practice client, a whole playable rhythm game pointed at one job. Then I wanted to rewatch my real runs, so replays came next, fed through the same engine. Then I thought I'd like to see my own Festival scores live while I play, and remembered I had already built the log-reading thing. The old path follower got absorbed as the companion, grew a leaderboard of my previous scores, and lost its manual key press for something much better.",[10,63,64],{},"That reuse is the whole design. The client already had hit detection, scoring, a highway and a results screen, which is everything a replay needs. Capturing what happens in the real game and feeding it through the client I already trust beats building a second scorer that would drift out of agreement with the first. Everything gets judged by the same engine, whether the notes came from a practice run or from Fortnite.",[10,66,67,68,72,73,78],{},"It's a Rust workspace of three crates. ",[69,70,71],"code",{},"core"," is the shared library: chart parsing, AES MIDI decryption, the SQLite database and settings. The launcher is a ",[28,74,77],{"href":75,"rel":76},"https:\u002F\u002Ftauri.app",[32],"Tauri"," 2 app with a React frontend that owns everything outside the highway. The practice client is a third crate the launcher spawns as a separate process.",[80,81,87],"pre",{"className":82,"code":83,"filename":84,"language":85,"meta":86,"style":86},"language-bash shiki shiki-themes dromzeh","crates\u002F\n├── core\u002F       # shared: chart parse, AES decrypt, SQLite, settings\n├── launcher\u002F   # Tauri 2 + React: song list, analysis, sections, replays, overlay\n└── practice\u002F   # macroquad + egui gameplay client, spawned per session\n","tree","bash","",[69,88,89,98,112,123],{"__ignoreMap":86},[90,91,94],"span",{"class":92,"line":93},"line",1,[90,95,97],{"class":96},"sxxka","crates\u002F\n",[90,99,101,104,108],{"class":92,"line":100},2,[90,102,103],{"class":96},"├──",[90,105,107],{"class":106},"sRQ_c"," core\u002F",[90,109,111],{"class":110},"sAzwi","       # shared: chart parse, AES decrypt, SQLite, settings\n",[90,113,115,117,120],{"class":92,"line":114},3,[90,116,103],{"class":96},[90,118,119],{"class":106}," launcher\u002F",[90,121,122],{"class":110},"   # Tauri 2 + React: song list, analysis, sections, replays, overlay\n",[90,124,126,129,132],{"class":92,"line":125},4,[90,127,128],{"class":96},"└──",[90,130,131],{"class":106}," practice\u002F",[90,133,134],{"class":110},"   # macroquad + egui gameplay client, spawned per session\n",[10,136,137],{},"The two processes deliberately share almost nothing: a SQLite database in WAL mode, an argv contract, and an exit code. The launcher owns the song list, section management and analysis. The client keeps its verified timing and scoring stack and the launcher never links against it, so neither can quietly break the other.",[10,139,140,141,146,147,151],{},"Inside, the client is a game state machine. ",[142,143,145],"speak",{"as":144},"macro-quad","macroquad"," handles the 3D rendering of the note highway and ",[142,148,150],{"as":149},"ee gooey","egui"," handles menus and HUD as an immediate-mode overlay; each frame polls input, advances logic and renders. Its states are short, because most of them went away when it moved under the launcher.",[80,153,158],{"className":154,"code":155,"filename":156,"language":157,"meta":86,"style":86},"language-rust shiki shiki-themes dromzeh","enum AppState {\n    Practicing(PracticingState),\n    Results(ResultsState),\n    WatchingReplay(WatchingReplayState),\n}\n","practice\u002Fapp.rs","rust",[69,159,160,174,188,200,212],{"__ignoreMap":86},[90,161,162,166,170],{"class":92,"line":93},[90,163,165],{"class":164},"spHEY","enum",[90,167,169],{"class":168},"s1mqe"," AppState",[90,171,173],{"class":172},"sQTeU"," {\n",[90,175,176,179,182,185],{"class":92,"line":100},[90,177,178],{"class":96},"    Practicing",[90,180,181],{"class":172},"(",[90,183,184],{"class":168},"PracticingState",[90,186,187],{"class":172},"),\n",[90,189,190,193,195,198],{"class":92,"line":114},[90,191,192],{"class":96},"    Results",[90,194,181],{"class":172},[90,196,197],{"class":168},"ResultsState",[90,199,187],{"class":172},[90,201,202,205,207,210],{"class":92,"line":125},[90,203,204],{"class":96},"    WatchingReplay",[90,206,181],{"class":172},[90,208,209],{"class":168},"WatchingReplayState",[90,211,187],{"class":172},[90,213,215],{"class":92,"line":214},5,[90,216,217],{"class":172},"}\n",[10,219,220,221,224,225,224,228,231,232,235,236,239,240,243,244,247,248,254],{},"The old ",[69,222,223],{},"Loading",", ",[69,226,227],{},"SongSelect",[69,229,230],{},"Playing"," and ",[69,233,234],{},"Paused"," states are gone. Song browsing belongs to the launcher now, so a spawned client goes straight into gameplay. Live play always enters ",[69,237,238],{},"Practicing",", where a full song is simply a section spanning the whole chart; from there it moves to ",[69,241,242],{},"Results",", or, when the launcher spawns it with ",[69,245,246],{},"--replay",", into ",[142,249,251],{"as":250},"Watching Replay",[69,252,253],{},"WatchingReplay",", where recorded input events run back against the original chart. Some of this diverges from Festival on purpose, because it was built around how I wanted to practise rather than as a clone, and most of those differences are settings you can tune.",[49,256,258],{"id":257},"charts","Charts",[10,260,261],{},"Each decrypted MIDI carries instrument tracks at four difficulty tiers, and the MIDI note numbers map to fret positions.",[263,264,265,281],"table",{},[266,267,268],"thead",{},[269,270,271,275,278],"tr",{},[272,273,274],"th",{},"Difficulty",[272,276,277],{},"MIDI note range",[272,279,280],{},"Frets",[282,283,284,296,306,316],"tbody",{},[269,285,286,290,293],{},[287,288,289],"td",{},"Easy",[287,291,292],{},"60-63",[287,294,295],{},"4",[269,297,298,301,304],{},[287,299,300],{},"Medium",[287,302,303],{},"72-75",[287,305,295],{},[269,307,308,311,314],{},[287,309,310],{},"Hard",[287,312,313],{},"84-87",[287,315,295],{},[269,317,318,321,324],{},[287,319,320],{},"Expert",[287,322,323],{},"96-100",[287,325,326],{},"5",[10,328,329],{},"Markers around those encode the mechanics. Note 116 defines overdrive phrase boundaries, where note-on starts a phrase and note-off ends it. Base+6 through base+10 are lift notes. Notes sharing a MIDI tick are detected as chords. What comes out the other side is one struct per note.",[80,331,334],{"className":154,"code":332,"filename":333,"language":157,"meta":86,"style":86},"pub struct Note {\n    tick: u32,\n    time_ms: f64,\n    fret: u8, \u002F\u002F 1\n    sustain_ticks: u32,\n    sustain_ms: f64,\n    sustain_beats: f32, \u002F\u002F 2\n    flags: NoteFlags, \u002F\u002F 3\n}\n","core\u002Fchart\u002Fnote.rs",[69,335,336,349,364,376,392,403,415,431,447],{"__ignoreMap":86},[90,337,338,341,344,347],{"class":92,"line":93},[90,339,340],{"class":164},"pub",[90,342,343],{"class":164}," struct",[90,345,346],{"class":168}," Note",[90,348,173],{"class":172},[90,350,351,355,358,361],{"class":92,"line":100},[90,352,354],{"class":353},"sH9Dd","    tick",[90,356,357],{"class":172},":",[90,359,360],{"class":168}," u32",[90,362,363],{"class":172},",\n",[90,365,366,369,371,374],{"class":92,"line":114},[90,367,368],{"class":353},"    time_ms",[90,370,357],{"class":172},[90,372,373],{"class":168}," f64",[90,375,363],{"class":172},[90,377,378,381,383,386,389],{"class":92,"line":125},[90,379,380],{"class":353},"    fret",[90,382,357],{"class":172},[90,384,385],{"class":168}," u8",[90,387,388],{"class":172},",",[90,390,391],{"class":110}," \u002F\u002F 1\n",[90,393,394,397,399,401],{"class":92,"line":214},[90,395,396],{"class":353},"    sustain_ticks",[90,398,357],{"class":172},[90,400,360],{"class":168},[90,402,363],{"class":172},[90,404,406,409,411,413],{"class":92,"line":405},6,[90,407,408],{"class":353},"    sustain_ms",[90,410,357],{"class":172},[90,412,373],{"class":168},[90,414,363],{"class":172},[90,416,418,421,423,426,428],{"class":92,"line":417},7,[90,419,420],{"class":353},"    sustain_beats",[90,422,357],{"class":172},[90,424,425],{"class":168}," f32",[90,427,388],{"class":172},[90,429,430],{"class":110}," \u002F\u002F 2\n",[90,432,434,437,439,442,444],{"class":92,"line":433},8,[90,435,436],{"class":353},"    flags",[90,438,357],{"class":172},[90,440,441],{"class":168}," NoteFlags",[90,443,388],{"class":172},[90,445,446],{"class":110}," \u002F\u002F 3\n",[90,448,450],{"class":92,"line":449},9,[90,451,217],{"class":172},[10,453,454,459,460,464,465,469,470,473],{},[455,456,458],"clause",{"n":457},"1","The fret is 0 to 4",", green through orange. ",[455,461,463],{"n":462},"2","The sustain is kept three ways at once",", in ticks, milliseconds and beats, because the highway scales tails by beat and the scorer pays them by beat while everything else runs in milliseconds. ",[455,466,468],{"n":467},"3","The flags are a bitfield"," from the ",[69,471,472],{},"bitflags"," crate: tap, overdrive, lift, chord. The same MIDI also carries a SECTION track of named landmarks, intro, verse, chorus and so on, and the launcher reads those as the anchors you build practice sections around.",[49,475,477],{"id":476},"the-clock","The clock",[10,479,480],{},"The client's audio position originally came straight from the audio engine. That was the wrong source of truth, and it took a while to admit it.",[10,482,483],{},"The engine reports position in roughly 45ms steps, one audio buffer at a time. The first release smoothed the steps over with interpolate-then-snap logic, which traded visible stepping for wobble and was not an improvement. In a game where the perfect window is ±25ms, a clock that quantises at 45ms is disqualified before the smoothing starts.",[10,485,486],{},"The fix inverts the relationship. A wall-anchored timer is the position: anchor the moment playback starts, advance on the monotonic clock, re-anchor on every seek, pause and speed change. The engine's reported position is consulted only as a drift check, through one small pure function that takes the predicted position and the raw report and returns a correction only when they disagree by more than 150ms.",[10,488,489],{},"When I finally probed the engine headless for a full song, it tracked the wall clock within ±7ms over 412 seconds with all five stems in lockstep, which means the safety net has likely never fired in anger. The stepping was the problem, not the engine's accuracy.",[10,491,492],{},"At practice speeds the anchor advances at the playback rate, so slowing a section to 0.5x slows time itself. The highway, the count-in and the hit windows all stay glued to the audio for free, and because hit windows are defined in chart time, half speed also means double the real-time margin for your hands, which is the entire point of a practice speed.",[49,494,496],{"id":495},"sections","Sections",[10,498,499],{},"Sections are created against a picture of the song rather than typed as timestamps. The launcher renders the whole chart as a notes-per-second heat strip, resampling the cached 500ms NPS buckets into a fixed number of columns with area weighting so long songs don't alias, and draws the chart's own section names underneath as landmarks. You drag across the strip and the create form prefills with the range. Clicking a section label snaps to that section exactly and takes its name.",[10,501,502,503,231,506,509],{},"Saved sections live in the shared database and each row has a play button. Launching one forwards the range on the command line as ",[69,504,505],{},"--range-start-ms",[69,507,508],{},"--range-end-ms",", and the client turns that into a synthetic section through the same code path a chart section uses, so count-in, looping and auto-restart behave identically whether the range came from the chart or from a drag.",[10,511,512,513,519],{},"On the client side the practice engine in ",[142,514,516],{"as":515},"game, practice dot r-s",[69,517,518],{},"game\u002Fpractice.rs"," runs the loop. A count-in plays the audio with no notes for a number of beats, four by default, so you can orient yourself before notes appear. Auto-restart watches the miss count during a section and loops back to the start, count-in and all, once it crosses the threshold. Every run, a whole song or a single section, is recorded as a stream of timestamped fret presses, releases and overdrive activations, saved to the shared database as one row: the summary next to the whole event list. A run you quit halfway is kept too, and replays exactly that far.",[49,521,523],{"id":522},"the-scoring","The scoring",[10,525,526],{},"The engine agrees with the game to within 25 points on a 335,000-point run, and every constant in it traces to a frame of recorded gameplay. The reason any of it exists is that I was convinced my own client was broken.",[10,528,529],{},"It started with Paradise City. Playing it in the client felt late compared to Festival, off in a way I couldn't pin down, and it was driving me mental. So I recorded myself playing it in Fortnite, recorded the same song in the client, and compared the two videos frame by frame expecting to find drift. There wasn't any, the notes were exactly on time in both.",[10,531,532],{},"The real cause was the stem mix. The client boosts the lead stem, the instrument you're playing, at 1.5x to copy Festival's you-hear-yourself behaviour, and it was loud enough to warp my sense of where the beat was. The client was innocent, my ears were the bug.",[10,534,535],{},"The comparison rig turned out to be worth far more than the bug it acquitted. Two recordings and a frame counter. If I could line video up against the client's scoring, I could line video up against Festival's scoring, and Festival publishes every number that matters, sixty times a second, in the corner of the screen.",[10,537,538],{},"The rules as they actually are:",[263,540,541,551],{},[266,542,543],{},[269,544,545,548],{},[272,546,547],{},"Rule",[272,549,550],{},"Value",[282,552,553,561,569,577,585,593,601],{},[269,554,555,558],{},[287,556,557],{},"Base note",[287,559,560],{},"30 points",[269,562,563,566],{},[287,564,565],{},"Perfect (±25ms)",[287,567,568],{},"×1.2 → 36",[269,570,571,574],{},[287,572,573],{},"Good (±100ms)",[287,575,576],{},"×1.0 → 30",[269,578,579,582],{},[287,580,581],{},"Combo",[287,583,584],{},"Delayed: steps every ten notes, the eleventh is the first 2x",[269,586,587,590],{},[287,588,589],{},"Combo cap",[287,591,592],{},"4x lead and drums · 6x bass and vocals",[269,594,595,598],{},[287,596,597],{},"Sustain",[287,599,600],{},"12 points per beat × combo; under one beat pays nothing",[269,602,603,606],{},[287,604,605],{},"Overdrive",[287,607,608],{},"×2 on whatever is true; the window extends when another phrase completes inside it",[610,611,613],"h3",{"id":612},"the-screen-as-a-ledger","The screen as a ledger",[10,615,616],{},"One run recorded at 60fps, the score counter cropped from every frame, binarised and OCR'd: 18,671 frames. The counter snaps instantly on every note, and a cumulative, monotonic counter sampled at 60Hz isn't a picture of the score, it's a ledger of every note's exact value.",[10,618,619],{},"On a full combo with no overdrive the decode is total. Combo equals note index, so the multiplier schedule is fully determined, and every increment factors uniquely as a perfect or a good at a known multiplier. You get a verdict per note out of the pixels.",[10,621,622],{},"Everything surprising came out of that one recording. The results screen number is a count-up animation, which matters if a screenshot catches it mid-roll and the game appears to disagree with itself by six and a half thousand points. Sustains under a beat pay nothing: an entire section of quarter-beat trails paid a flat +144 per note, no trickle. Qualifying sustains are multiplied: one isolated 1.5-beat hold, sitting before an eleven-second break with nothing else to contaminate the reading, paid exactly 72 points, which is 18 for the sustain times the 4x multiplier that supposedly never applies there.",[10,624,625],{},"A second recording with overdrive settled the window-extension rule the same way, from the lengths of the doubled regions alone. When the engine's total first landed within spitting distance of the game's, my honest reaction at the desk was: what have I made? At that point it wasn't a practice tool. I'd made an entire rhythm game.",[610,627,629],{"id":628},"the-last-few-hundred-points","The last few hundred points",[10,631,632],{},"The last few hundred points are not reachable, and the reason is better than a bug. The design window for a perfect is ±25ms, but the game judges a keypress on its own engine tick, Unreal sampling input on its schedule, while the capture timestamps the same keypress on a wall clock. Measured from outside, the effective boundary between the two clocks landed at 27.0ms in one session and 25.5ms in another, with mixed early and late signs that rule out a constant offset. A dense chart stacks enough notes right on that boundary that the wobble is worth a few hundred points on any fixed window.",[634,635,637],"fig",{"caption":636},"The perfect-window boundary as measured from outside the game. The design window is ±25 ms, the two sessions landed at 25.5 ms and 27.0 ms, and the engine adopts the midpoint of the two measurements.",[638,639],"plot-chart",{":data":640,":max":641},"[{\"label\":\"Design window\",\"value\":25,\"display\":\"25 ms\"},{\"label\":\"Session B\",\"value\":25.5,\"display\":\"25.5 ms\"},{\"label\":\"Session A\",\"value\":27,\"display\":\"27.0 ms\"},{\"label\":\"Adopted\",\"value\":26.25,\"display\":\"26.25 ms\",\"em\":true}]","30",[10,643,644],{},"The engine takes the midpoint, 26.25ms, and stops there, because the residual isn't an error in either scorer. It's two clocks watching one finger and disagreeing about the edge. The verification loop is the same every time: play a session in the real game, capture every input with timestamps, score it through the one engine, and compare against what the game displayed, note by note when a recording exists.",[49,646,648],{"id":647},"the-anchor","The anchor",[10,650,651],{},"The companion side of the launcher needs to know what's happening inside a running song. Festival's API only lists songs and says nothing about a live one. Fortnite tells you anyway, if you listen in the right places, but it takes two of them and neither is enough alone.",[610,653,655],{"id":654},"the-log","The log",[10,657,658,659,662,663,666],{},"The first place is the log file. The game writes ",[69,660,661],{},"LogPilgrim*"," lines ",[23,664,665],{},"Pilgrim is Festival's internal codename."," for every meaningful transition, so tailing the log and running each line through a handful of regexes gets you a clean event stream.",[80,668,671],{"className":154,"code":669,"filename":670,"language":157,"meta":86,"style":86},"pub enum LogEvent {\n    SongStarted { instrument: Instrument, difficulty: String }, \u002F\u002F 1\n    SongLoaded { song_id: String }, \u002F\u002F 2\n    SongIntro, \u002F\u002F 3\n    EnteredBackstage,\n    LeftBackstage,\n    StoppingSong,\n}\n\nstatic DIFFICULTY_REGEX: Lazy\u003CRegex> = Lazy::new(|| {\n    Regex::new(r\"LogPilgrimGameEvaluator: \\[....\\] : Song data set\\. [0-9]* gems found for \").unwrap()\n});\n","companion\u002Flog.rs",[69,672,673,685,716,734,743,750,757,764,768,774,814,839],{"__ignoreMap":86},[90,674,675,677,680,683],{"class":92,"line":93},[90,676,340],{"class":164},[90,678,679],{"class":164}," enum",[90,681,682],{"class":168}," LogEvent",[90,684,173],{"class":172},[90,686,687,690,693,696,698,701,703,706,708,711,714],{"class":92,"line":100},[90,688,689],{"class":168},"    SongStarted",[90,691,692],{"class":172}," {",[90,694,695],{"class":353}," instrument",[90,697,357],{"class":172},[90,699,700],{"class":168}," Instrument",[90,702,388],{"class":172},[90,704,705],{"class":353}," difficulty",[90,707,357],{"class":172},[90,709,710],{"class":168}," String",[90,712,713],{"class":172}," },",[90,715,391],{"class":110},[90,717,718,721,723,726,728,730,732],{"class":92,"line":114},[90,719,720],{"class":168},"    SongLoaded",[90,722,692],{"class":172},[90,724,725],{"class":353}," song_id",[90,727,357],{"class":172},[90,729,710],{"class":168},[90,731,713],{"class":172},[90,733,430],{"class":110},[90,735,736,739,741],{"class":92,"line":125},[90,737,738],{"class":168},"    SongIntro",[90,740,388],{"class":172},[90,742,446],{"class":110},[90,744,745,748],{"class":92,"line":214},[90,746,747],{"class":168},"    EnteredBackstage",[90,749,363],{"class":172},[90,751,752,755],{"class":92,"line":405},[90,753,754],{"class":168},"    LeftBackstage",[90,756,363],{"class":172},[90,758,759,762],{"class":92,"line":417},[90,760,761],{"class":168},"    StoppingSong",[90,763,363],{"class":172},[90,765,766],{"class":92,"line":433},[90,767,217],{"class":172},[90,769,770],{"class":92,"line":449},[90,771,773],{"emptyLinePlaceholder":772},true,"\n",[90,775,777,780,784,786,789,792,795,798,801,803,806,809,812],{"class":92,"line":776},10,[90,778,779],{"class":164},"static",[90,781,783],{"class":782},"sneuw"," DIFFICULTY_REGEX",[90,785,357],{"class":172},[90,787,788],{"class":168}," Lazy",[90,790,791],{"class":172},"\u003C",[90,793,794],{"class":168},"Regex",[90,796,797],{"class":172},">",[90,799,800],{"class":172}," =",[90,802,788],{"class":168},[90,804,805],{"class":172},"::",[90,807,808],{"class":96},"new",[90,810,811],{"class":172},"(||",[90,813,173],{"class":172},[90,815,817,821,823,825,827,830,833,836],{"class":92,"line":816},11,[90,818,820],{"class":819},"s_Mgl","    Regex",[90,822,805],{"class":172},[90,824,808],{"class":96},[90,826,181],{"class":172},[90,828,829],{"class":106},"r\"LogPilgrimGameEvaluator: \\[....\\] : Song data set\\. [0-9]* gems found for \"",[90,831,832],{"class":172},").",[90,834,835],{"class":96},"unwrap",[90,837,838],{"class":172},"()\n",[90,840,842],{"class":92,"line":841},12,[90,843,844],{"class":172},"});\n",[10,846,847,848,851,852,855,856,859],{},"Three lines out of that file matter. The ",[455,849,850],{"n":457},"song data line"," carries the instrument and the difficulty, the ",[455,853,854],{"n":462},"loading line"," carries the id, and reaching the ",[455,857,858],{"n":467},"intro state"," is what finally arms the matcher.",[10,861,862],{},"That stream tells you which song started and on which instrument, but not where the player is inside it. The song-started line is honest about the fact and useless as a clock: it fires around the start, not at it, off by a second or so, and in multiplayer the gap varies with loading and connection wait. A second doesn't sound like much until you remember a dense chart runs 16 notes a second. So the log arms the matcher and does nothing else.",[610,864,866],{"id":865},"the-pattern","The pattern",[10,868,869],{},"I sat on the rest of this for ages and pivoted more times than I want to admit. OCR off the screen was the obvious idea, and it's disqualified by its own delay. The log is off by a second. At one point I was ready to track by measure and just eat being a little wrong. Locking onto the first note the player hits sounds right and dies to reality, because one stray tap while waiting for the song anchors the whole overlay to a lie.",[10,871,872],{},"What actually cracked it was noticing I had already built the answer. Replay mode is an engine whose entire job is taking a stream of timestamped presses and running it against a chart, and a live Fortnite session is that same stream arriving in real time. So the companion runs replay mode in the background on my own keys, and the only thing it still needs from Fortnite is where zero is. The player is the clock, but plural: not one press, a pattern of them.",[10,874,875,876,879],{},"Fret presses are captured with ",[69,877,878],{},"GetAsyncKeyState"," polling on a dedicated thread, timestamped on the monotonic clock into a small ring buffer. The same poll reads the fret bindings from the shared settings, so it always watches the keys you actually play on.",[10,881,882,883,886],{},"Polling over a keyboard hook is deliberate and it's about latency. A low-level hook (",[69,884,885],{},"WH_KEYBOARD_LL",") inserts itself into the input delivery chain, so every keystroke in the system synchronously waits on your callback before the game sees it, and a slow moment in your process becomes input lag in theirs. Polling never touches that chain. It reads the key state table Windows already maintains, the game's input path stays exactly as long as it was, and the worst the companion can do to your timing is nothing at all.",[10,888,889],{},"The chart's opening is reduced to onset groups: clusters of note times with the lanes they occupy. Incoming presses get clustered the same way, and the matcher tries to align the two sequences. It takes a window of the first few onset groups, derives the offset that would map a candidate press cluster onto the first onset, and checks whether the following clusters land within tolerance of the following onsets on compatible lanes. A skip parameter slides the window forward, so a pre-tap or a flubbed opening note can't poison the lock.",[80,891,894],{"className":154,"code":892,"filename":893,"language":157,"meta":86,"style":86},"const MATCH_TOL_MS: f64 = 120.0; \u002F\u002F 1\nconst MATCH_GROUPS: usize = 3; \u002F\u002F 2\n\npub enum Lifecycle {\n    Disarmed,\n    Listening,\n    Locked, \u002F\u002F 3\n    Degraded, \u002F\u002F 4\n}\n","companion\u002Fmatch.rs",[69,895,896,924,945,949,960,967,974,983,993],{"__ignoreMap":86},[90,897,898,901,904,906,908,910,913,916,919,922],{"class":92,"line":93},[90,899,900],{"class":164},"const",[90,902,903],{"class":782}," MATCH_TOL_MS",[90,905,357],{"class":172},[90,907,373],{"class":168},[90,909,800],{"class":172},[90,911,912],{"class":782}," 120",[90,914,915],{"class":172},".",[90,917,918],{"class":782},"0",[90,920,921],{"class":172},";",[90,923,391],{"class":110},[90,925,926,928,931,933,936,938,941,943],{"class":92,"line":100},[90,927,900],{"class":164},[90,929,930],{"class":782}," MATCH_GROUPS",[90,932,357],{"class":172},[90,934,935],{"class":168}," usize",[90,937,800],{"class":172},[90,939,940],{"class":782}," 3",[90,942,921],{"class":172},[90,944,430],{"class":110},[90,946,947],{"class":92,"line":114},[90,948,773],{"emptyLinePlaceholder":772},[90,950,951,953,955,958],{"class":92,"line":125},[90,952,340],{"class":164},[90,954,679],{"class":164},[90,956,957],{"class":168}," Lifecycle",[90,959,173],{"class":172},[90,961,962,965],{"class":92,"line":214},[90,963,964],{"class":168},"    Disarmed",[90,966,363],{"class":172},[90,968,969,972],{"class":92,"line":405},[90,970,971],{"class":168},"    Listening",[90,973,363],{"class":172},[90,975,976,979,981],{"class":92,"line":417},[90,977,978],{"class":168},"    Locked",[90,980,388],{"class":172},[90,982,446],{"class":110},[90,984,985,988,990],{"class":92,"line":433},[90,986,987],{"class":168},"    Degraded",[90,989,388],{"class":172},[90,991,992],{"class":110}," \u002F\u002F 4\n",[90,994,995],{"class":92,"line":449},[90,996,217],{"class":172},[10,998,999,1002,1003,1006,1007,1010,1011,1014],{},[455,1000,1001],{"n":457},"Each onset has to land within 120ms",", half an NPS bucket, and ",[455,1004,1005],{"n":462},"three distinct groups have to agree"," before anything commits. ",[455,1008,1009],{"n":467},"Then the offset becomes the anchor",": song position is the monotonic clock minus that offset, from then on, forever, with no further press needed. ",[455,1012,1013],{"n":295},"If ten seconds pass with no clean lock",", it falls back to a rough wall-clock estimate from the song-started line rather than showing nothing at all. You sync the overlay to the game by playing the game. The opening notes are the handshake.",[49,1016,1018],{"id":1017},"the-overlay","The overlay",[10,1020,1021],{},"The overlay is a native Win32 layered window, per-pixel alpha, clickthrough, always on top, drawn with GDI rather than a webview, because it has to coexist with a fullscreen game without stealing input or a compositor's worth of resources.",[10,1023,1024],{},"What it draws is the chart as a difficulty graph: the whole song's notes-per-second curve with a sweep line riding across it at the locked position, so the dense stretch is visible before it arrives, and a live score checked against my previous scores on that chart so a run has something to beat.",[1026,1027],"video-clip",{"caption":1028,"src":1029},"Beyond the Flame (Expert Lead) in-game, the overlay pinned bottom right. The sweep line rides the difficulty graph at my position, with a leaderboard of my previous scores above it. None of it reads the game: it runs entirely off the pattern-match lock from the opening notes. The clacking is my keyboard through the mic, the closest this build gets to hitsounds.","https:\u002F\u002Fx.dromzeh.dev\u002FShareX\u002F2026\u002F07\u002Ffnf-ingame-overlay.mp4",[634,1031,1033],{"caption":1032},"Two sources, one anchor. The log says which song and arms the matcher; the key poll supplies the presses. Once three onset groups agree, everything downstream runs off the monotonic clock and neither source is consulted again.",[1034,1035],"flow-diagram",{":columns":1036,":edges":1037},"[[{\"id\":\"log\",\"title\":\"Log tail\",\"sub\":\"LogPilgrim lines\"},{\"id\":\"keys\",\"title\":\"Key poll\",\"sub\":\"GetAsyncKeyState\"}],[{\"id\":\"matcher\",\"title\":\"Onset matcher\",\"sub\":\"3 groups, 120 ms\"}],[{\"id\":\"clock\",\"title\":\"Locked clock\",\"sub\":\"Monotonic minus offset\"}],[{\"id\":\"overlay\",\"title\":\"Overlay\",\"sub\":\"Difficulty graph, score\"},{\"id\":\"path\",\"title\":\"Path box\",\"sub\":\"Overdrive activations\"}]]","[{\"from\":\"log\",\"to\":\"matcher\",\"label\":\"Arms\"},{\"from\":\"keys\",\"to\":\"matcher\",\"label\":\"Presses\"},{\"from\":\"matcher\",\"to\":\"clock\"},{\"from\":\"clock\",\"to\":\"overlay\"},{\"from\":\"clock\",\"to\":\"path\"}]",[610,1039,1041],{"id":1040},"the-path-box","The path box",[10,1043,1044],{},"The overlay's other half is the descendant of that first path follower, minus the part where I press a key to advance it. A path box sits on top of the highway, pinned wherever I want it, listing the overdrive activations CHOpt worked out for the chart, with the one you're on marked.",[10,1046,1047],{},"Making that mark track the run on its own is the hard part, because Fortnite never tells the overlay where you are in the path any more than it tells it the time. It infers that too. A scoring pass runs over the run so far and counts the double-score windows you have actually opened, not the ones you whiffed, and that count is the index into the path list, so the marker only moves when the game moves.",[10,1049,1050],{},"Activation is detected the same way position is: your own key. Hitting the bound overdrive key records an activation into the capture and steps the highlight forward. Whether that press did anything is decided afterwards, by scoring it against the star power you had banked at that moment. Fire with an empty bank and you open no window, and the box flashes for a couple of seconds to tell you the activation was wasted.",[10,1052,1053],{},"Underneath, a status line reads the windows you've opened against how many the path expects by now, so you can see whether you're on plan, ahead of it, or behind. There's no callout for a mistimed phrase; a skipped or early activation shows up as running behind, and as the live score slipping under the leaderboard mark.",[10,1055,1056],{},"The count can drift. It rides on the lock holding, the clock staying aligned to the game, the scorer matching every activation to a window, and a 5ms input poll that can miss a fast double-tap. Any of those can leave it a window ahead or behind where you really are in the printed path. So two keys nudge the highlight by one in either direction, and a third freezes the auto-tracking once it's right, so a good manual correction isn't overwritten on the next pass.",[10,1058,1059],{},"The scoring pass runs slow on purpose. It re-reads the run about once a second, so the leaderboard score trails Fortnite's instant counter by up to a beat. Recomputing the whole run continuously would put real work right next to the input capture, and the companion's one rule, the same reason it polls the keyboard instead of hooking it, is to never cost the game a millisecond.",[49,1061,1063],{"id":1062},"replays","Replays",[10,1065,1066],{},"A replay is a real run fed back through the same engine, so the client can show you the run the way the game never does. Seeking inside one works by replaying every recorded event from the start up to the target timestamp, reconstructing combo, score, overdrive meter and sustain states at that point, which is cheap because the event list is already in order.",[10,1068,1069],{},"The viewer has an analysis mode. Turn it on and every note wears its timing from the moment it spawns rather than the moment you would have hit it: gold when the press landed inside the perfect window, blue when it was early, pink when it was late, a dark X where the note was missed. A whole run's timing reads at a glance, so a section that's quietly rushing shows up as a wall of blue long before it ever costs you a note. None of that is numbers on screen. The millisecond spread shows in a corner histogram, and the full per-note breakdown lives in the headless timing tools.",[1026,1071],{"caption":1072,"src":1073},"The same Beyond the Flame run in the client's replay viewer, the original Fortnite capture in the corner. One chart time drives both, so the highway and the real footage stay locked frame for frame. Analysis mode, toggled through the clip, tints each note by how it was timed: gold inside the perfect window, blue early, pink late, a dark X for a miss.","https:\u002F\u002Fx.dromzeh.dev\u002FShareX\u002F2026\u002F07\u002Freplay-btf-fnclient-analysis.mp4",[10,1075,1076],{},"Both are driven by one chart time, so the client's highway and the game's own footage stay locked frame for frame. That's the end-to-end check. If the replay, the timing and the scoring agree with what actually happened, they agree with the video too.",[49,1078,1080],{"id":1079},"lifts","Lifts",[10,1082,1083,1084,1089],{},"Path images come from ",[28,1085,1088],{"href":1086,"rel":1087},"https:\u002F\u002Fgithub.com\u002FGenericMadScientist\u002FCHOpt",[32],"CHOpt",", a Star Power path optimiser built for Rock Band and the like with Fortnite Festival support. The launcher's relationship with it is stranger than it should be. It ships two builds side by side, stock CHOpt and a fork with Festival's scoring model patched in, and the counterintuitive part is which binary does what.",[10,1091,1092,1093,1096,1097,1100],{},"The fork runs with ",[69,1094,1095],{},"--engine rb"," and produces the path text; stock runs with ",[69,1098,1099],{},"--engine fnf"," and produces the score and the image. It reads exactly backwards, and it's the empirically proven recipe: the fork's scoring model makes the right activation decisions but doesn't total correctly, stock totals correctly but paths worse, and no single binary gets both right. Every song runs through both and the launcher stitches the results into one answer.",[10,1102,1103,1104],{},"The image half is where lifts come in. Stock CHOpt supports Festival but didn't know lift notes existed. Its parser's note range stopped short of the lift markers, so they were dropped before the renderer ever saw them. In the MIDI, lifts live at fixed offsets above each difficulty's base note, keys 102 to 106 on Expert, one per lane. ",[23,1105,1106],{},"The green-lane lift shares a key with a force marker, and in Festival charts the lift reading wins.",[10,1108,1109,1110,1115,1116,1121],{},"The patch is three small layers on ",[28,1111,1114],{"href":1112,"rel":1113},"https:\u002F\u002Fgithub.com\u002Fdromzeh\u002FCHOpt\u002Ftree\u002Flift-triangles",[32],"a branch",", and the first layer isn't in CHOpt at all. Chart parsing lives in ",[28,1117,1120],{"href":1118,"rel":1119},"https:\u002F\u002Fgithub.com\u002FGenericMadScientist\u002FSightRead",[32],"SightRead",", a separate library CHOpt pins as a submodule. SightRead reads the MIDI into the note stream CHOpt paths and draws over, so a note class it never emits is one CHOpt can never see. The flag has to start there.",[80,1123,1128],{"className":1124,"code":1125,"filename":1126,"language":1127,"meta":86,"style":86},"language-c shiki shiki-themes dromzeh","enum NoteFlags : std::uint32_t {\n    \u002F\u002F ...\n    FLAGS_FORCE_STRUM = 1U \u003C\u003C 7,\n    FLAGS_LIFT        = 1U \u003C\u003C 8,\n    \u002F\u002F ...\n};\n","sightread\u002Fnote_track.hpp","c",[69,1129,1130,1142,1147,1166,1182,1186],{"__ignoreMap":86},[90,1131,1132,1134,1137,1140],{"class":92,"line":93},[90,1133,165],{"class":164},[90,1135,1136],{"class":819}," NoteFlags : std::",[90,1138,1139],{"class":164},"uint32_t",[90,1141,173],{"class":172},[90,1143,1144],{"class":92,"line":100},[90,1145,1146],{"class":110},"    \u002F\u002F ...\n",[90,1148,1149,1152,1155,1158,1161,1164],{"class":92,"line":114},[90,1150,1151],{"class":819},"    FLAGS_FORCE_STRUM ",[90,1153,1154],{"class":172},"=",[90,1156,1157],{"class":782}," 1U",[90,1159,1160],{"class":172}," \u003C\u003C",[90,1162,1163],{"class":782}," 7",[90,1165,363],{"class":172},[90,1167,1168,1171,1173,1175,1177,1180],{"class":92,"line":125},[90,1169,1170],{"class":819},"    FLAGS_LIFT        ",[90,1172,1154],{"class":172},[90,1174,1157],{"class":782},[90,1176,1160],{"class":172},[90,1178,1179],{"class":782}," 8",[90,1181,363],{"class":172},[90,1183,1184],{"class":92,"line":214},[90,1185,1146],{"class":110},[90,1187,1188],{"class":92,"line":405},[90,1189,1190],{"class":172},"};\n",[10,1192,1193,1194,1197,1198,1201,1202,1205],{},"A collection pass walks the MIDI track once and records where lifts occur. Each difficulty's five lift keys are ",[69,1195,1196],{},"base + 6"," through ",[69,1199,1200],{},"base + 10"," in green-to-orange order, so the lane is the key minus that base, and a velocity-zero note-on is skipped. ",[23,1203,1204],{},"A note-on with velocity zero is a note-off in disguise."," An application pass then sets the flag on any real note sharing a lift's tick and lane, and touches nothing else. That last part is the whole safety argument: positions, lengths, counts and every other flag stay exactly as they were, so the optimiser is handed an identical chart. Both passes run only for the Fortnite track type, and both hang off one line in the note builder.",[80,1207,1210],{"className":1124,"code":1208,"filename":1209,"language":1127,"meta":86,"style":86},"auto notes = notes_from_event_track(event_track, {}, TrackType::FortniteFestival);\napply_fortnite_lifts(notes, fortnite_lifts_from_midi(midi_track));\n","sightread\u002Fmidi_parser.cpp",[69,1211,1212,1238],{"__ignoreMap":86},[90,1213,1214,1217,1219,1222,1224,1227,1229,1232,1235],{"class":92,"line":93},[90,1215,1216],{"class":819},"auto notes ",[90,1218,1154],{"class":172},[90,1220,1221],{"class":96}," notes_from_event_track",[90,1223,181],{"class":172},[90,1225,1226],{"class":819},"event_track",[90,1228,388],{"class":172},[90,1230,1231],{"class":172}," {},",[90,1233,1234],{"class":819}," TrackType::FortniteFestival",[90,1236,1237],{"class":172},");\n",[90,1239,1240,1243,1245,1248,1250,1253,1255,1258],{"class":92,"line":100},[90,1241,1242],{"class":96},"apply_fortnite_lifts",[90,1244,181],{"class":172},[90,1246,1247],{"class":819},"notes",[90,1249,388],{"class":172},[90,1251,1252],{"class":96}," fortnite_lifts_from_midi",[90,1254,181],{"class":172},[90,1256,1257],{"class":819},"midi_track",[90,1259,1260],{"class":172},"));\n",[10,1262,1263],{},"Rather than fork the parser library for a flag and two passes, the branch vendors it: the submodule becomes plain committed files with the patch applied, which keeps the whole change self-contained in one branch CI can build with no upstream involvement.",[10,1265,1266,1267,1270],{},"From there CHOpt carries the flag from a parsed note to a drawn one. The image builder fills a per-lane lift mask on ",[69,1268,1269],{},"DrawnNote",", set only for the note's active lanes and only when the flag is present, and the renderer reads that mask back out as a bitfield to decide what to draw. A note whose every active lane is a lift is drawn as triangles only, so its sprite is suppressed; a lift lane sitting over an otherwise normal note gets a triangle on top of the usual sprite.",[80,1272,1275],{"className":1124,"code":1273,"filename":1274,"language":1127,"meta":86,"style":86},"constexpr int HALF_WIDTH = 4;\nconstexpr int HALF_HEIGHT = 5;\n\nconst auto apex_x = x + HALF_WIDTH; \u002F\u002F 1\nconst auto base_x = x - HALF_WIDTH; \u002F\u002F 2\n\nm_image.draw_triangle(apex_x, cy, base_x, cy - HALF_HEIGHT,\n                      base_x, cy + HALF_HEIGHT, colour, 1.0F); \u002F\u002F 3\n","chopt\u002Fimage_builder.cpp",[69,1276,1277,1296,1312,1316,1338,1358,1362,1397],{"__ignoreMap":86},[90,1278,1279,1282,1285,1288,1290,1293],{"class":92,"line":93},[90,1280,1281],{"class":819},"constexpr ",[90,1283,1284],{"class":164},"int",[90,1286,1287],{"class":819}," HALF_WIDTH ",[90,1289,1154],{"class":172},[90,1291,1292],{"class":782}," 4",[90,1294,1295],{"class":172},";\n",[90,1297,1298,1300,1302,1305,1307,1310],{"class":92,"line":100},[90,1299,1281],{"class":819},[90,1301,1284],{"class":164},[90,1303,1304],{"class":819}," HALF_HEIGHT ",[90,1306,1154],{"class":172},[90,1308,1309],{"class":782}," 5",[90,1311,1295],{"class":172},[90,1313,1314],{"class":92,"line":114},[90,1315,773],{"emptyLinePlaceholder":772},[90,1317,1318,1320,1323,1325,1328,1331,1334,1336],{"class":92,"line":125},[90,1319,900],{"class":164},[90,1321,1322],{"class":819}," auto apex_x ",[90,1324,1154],{"class":172},[90,1326,1327],{"class":819}," x ",[90,1329,1330],{"class":172},"+",[90,1332,1333],{"class":819}," HALF_WIDTH",[90,1335,921],{"class":172},[90,1337,391],{"class":110},[90,1339,1340,1342,1345,1347,1349,1352,1354,1356],{"class":92,"line":214},[90,1341,900],{"class":164},[90,1343,1344],{"class":819}," auto base_x ",[90,1346,1154],{"class":172},[90,1348,1327],{"class":819},[90,1350,1351],{"class":172},"-",[90,1353,1333],{"class":819},[90,1355,921],{"class":172},[90,1357,430],{"class":110},[90,1359,1360],{"class":92,"line":405},[90,1361,773],{"emptyLinePlaceholder":772},[90,1363,1364,1367,1370,1372,1375,1377,1380,1382,1385,1387,1390,1392,1395],{"class":92,"line":417},[90,1365,1366],{"class":819},"m_image.",[90,1368,1369],{"class":96},"draw_triangle",[90,1371,181],{"class":172},[90,1373,1374],{"class":819},"apex_x",[90,1376,388],{"class":172},[90,1378,1379],{"class":819}," cy",[90,1381,388],{"class":172},[90,1383,1384],{"class":819}," base_x",[90,1386,388],{"class":172},[90,1388,1389],{"class":819}," cy ",[90,1391,1351],{"class":172},[90,1393,1394],{"class":819}," HALF_HEIGHT",[90,1396,363],{"class":172},[90,1398,1399,1402,1404,1406,1408,1410,1412,1415,1417,1420,1423],{"class":92,"line":433},[90,1400,1401],{"class":819},"                      base_x",[90,1403,388],{"class":172},[90,1405,1389],{"class":819},[90,1407,1330],{"class":172},[90,1409,1394],{"class":819},[90,1411,388],{"class":172},[90,1413,1414],{"class":819}," colour",[90,1416,388],{"class":172},[90,1418,1419],{"class":782}," 1.0F",[90,1421,1422],{"class":172},");",[90,1424,446],{"class":110},[10,1426,1427,1430,1431,1434,1435,1438],{},[455,1428,1429],{"n":457},"The apex points right",", down the lane, and ",[455,1432,1433],{"n":462},"the base sits behind it",", nine pixels tall. It's drawn in the ",[455,1436,1437],{"n":467},"lane's own colour"," with a 1px black outline, which is enough to read at path-image scale without competing with the sprites. Regression on both engines confirmed non-Festival output byte-identical and Festival paths unchanged.",[49,1440,1442],{"id":1441},"the-end","The end",[10,1444,1445],{},"I won't be releasing the client or its source, and the reason is the project's own design. Everything here works from the outside: it reads the screen, tails the log, captures my own keys. Those are exactly the primitives an autoplay cheat needs, minus one step I have no interest in providing. Festival's leaderboards already carry obvious cheaters and nothing much seems to happen to them, so I'm not adding a starter kit.",[10,1447,1448],{},"I've left out the audio import, the per-chart offset calibration, and the settings layer that keeps the launcher and the client agreeing about which keys are which.",[10,1450,1451],{},"And the song that started it: thanks to the client I can read Flames lead consistently now, and I full-combo it in there almost every run. In Fortnite itself I still haven't, because somewhere in the last two minutes my nerves show up, every time, and that is not something a practice client can fix.",[1453,1454,1455],"style",{},"html pre.shiki code .spHEY, html code.shiki .spHEY{--shiki-default:var(--code-keyword)}html pre.shiki code .s_Mgl, html code.shiki .s_Mgl{--shiki-default:var(--color-body)}html pre.shiki code .sQTeU, html code.shiki .sQTeU{--shiki-default:var(--code-punct)}html pre.shiki code .sAzwi, html code.shiki .sAzwi{--shiki-default:var(--code-comment);--shiki-default-font-style:italic}html pre.shiki code .sneuw, html code.shiki .sneuw{--shiki-default:var(--code-literal)}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sxxka, html code.shiki .sxxka{--shiki-default:var(--code-function)}html pre.shiki code .sRQ_c, html code.shiki .sRQ_c{--shiki-default:var(--code-string)}html pre.shiki code .s1mqe, html code.shiki .s1mqe{--shiki-default:var(--code-type)}html pre.shiki code .sH9Dd, html code.shiki .sH9Dd{--shiki-default:var(--color-text)}",{"title":86,"searchDepth":100,"depth":100,"links":1457},[1458,1459,1460,1461,1462,1466,1470,1473,1474,1475],{"id":51,"depth":100,"text":52},{"id":257,"depth":100,"text":258},{"id":476,"depth":100,"text":477},{"id":495,"depth":100,"text":496},{"id":522,"depth":100,"text":523,"children":1463},[1464,1465],{"id":612,"depth":114,"text":613},{"id":628,"depth":114,"text":629},{"id":647,"depth":100,"text":648,"children":1467},[1468,1469],{"id":654,"depth":114,"text":655},{"id":865,"depth":114,"text":866},{"id":1017,"depth":100,"text":1018,"children":1471},[1472],{"id":1040,"depth":114,"text":1041},{"id":1062,"depth":100,"text":1063},{"id":1079,"depth":100,"text":1080},{"id":1441,"depth":100,"text":1442},"md",{"canListen":1478},false,"\u002Fposts\u002Freverse-engineering-fortnite-festival",{"title":5,"description":15},"posts\u002Freverse-engineering-fortnite-festival","Rebuilding Festival's clock, its scoring and the practice mode it never shipped, in Rust, without touching the game.","published","pfmAXKhG0-cR2dSt0B7RL0Gh5I7EJmJ3jnGPuD6vxa8",1790346826779]