bitmax/docs/handbook/10-vol-I-qbittorrz.md
monster 87cfead4d3 docs: complete the Dev Handbook
Adds the 5 chapters that hit a transient rate-limit on first pass:
- 01 Architecture & the Shared DNA
- 02 Building a New Volume (the Cookbook)
- 10 Vol I Internals — qBitTorrz
- 12 Vol III Internals — MACRO_VIRUS.XLS
- 13 Vol IV Internals — UPLINK

The Dev Handbook (Disc One) is now complete at 10 chapters; the Lore Bible (Disc Two) was already complete.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 16:21:31 +10:00

19 KiB
Raw Blame History

Vol I Internals — qBitTorrz

The annotated internals of the baseline volume: state shape, the two-term download model, seeding income, the catalog tier ladder, the swarm systems, hardware tiers, the upgrade store, and Client Migration. Everything here is cited from index.html; the numbers are the real numbers.

qBitTorrz is the reference implementation. Every later volume re-derives the scaffolding it establishes — freshState/save/load/sanitizeState, the dt-capped tick, simulateAway offline catch-up, toast. The novel hook is the two-term download model: a torrent downloads at min(Max Line Speed, Seeds × Bandwidth/Peer), and almost every system exists to push one of those two terms.

The G state shape

G is the single global, seeded by freshState() (line 530) and round-tripped through localStorage under SAVE_KEY = "qbittorrz_save_v1". The annotated keys:

Key Type Meaning
v number Save schema version (1).
data number Data — the spendable currency (bytes uploaded, banked).
totalUp / totalDown number Lifetime-of-run upload / download counters (HUD + ratio).
runUp number Upload accrued this prestige run — the sole input to creditsFor.
credits number Crypto-Credits (prestige currency).
prestiges number Migration count.
lifetimeUp number Upload across all runs; never reset.
torrents array Active torrents (each {uid,cid,name,downloaded,uploaded,status,…}).
upgrades object id → level; cleared on prestige.
perks object id → level; persists across prestige.
seq number Monotonic torrent-uid source.
sel number/null Selected torrent uid.
filter string Sidebar status filter (all/downloading/…).
dtab string Active detail tab.
sci bool Scientific-notation toggle (View menu).
seenCatalog object Catalog ids already viewed in the Add dialog (drives the new badge).
trackerList array Announced trackers {url,seeds,peers,status}.
rssFilter / rssOn string / bool RSS glob and auto-add toggle.
burstUntil,nextBurst,throttleUntil,nextThrottle number Event timers (ms).
irc,nextIrc,nextMagnet array/number IRC log and drop schedulers.
lastSave,started number Timestamps; lastSave anchors offline catch-up.
sidebarW,detailH number Persisted splitter geometry.

sanitizeState() (line 1577) repairs every one of these — num() clamps non-finite/negative numbers, object fields that arrive as arrays or null are reset, and torrents whose cid is not in CAT_BY_ID are dropped. A future-dated lastSave self-heals so a clock skew can't grant infinite offline time.

The download model

This is the load-bearing system. Read it in three parts: the line (your ceiling), the peer fraction (how much of the ceiling you actually get), and the realization that multiplies them.

The line: clientDown() and sqrt scaling

lineSpeed() is literally clientDown() (line 625). Line speed is not a fixed hardware number — it scales with the largest file you've unlocked:

const BASE_DOWN = 1000;   // B/s anchor before scaling/multipliers
const MIN_SIZE  = 4e3;    // smallest catalog file (Tier A)
function maxUnlockedSize(){
  let m = MIN_SIZE;
  for(const c of CATALOG){ if(c.size>m && catalogUnlocked(c)) m=c.size; }
  return m;
}
function clientDown(){ return BASE_DOWN * Math.sqrt(maxUnlockedSize()/MIN_SIZE) * downMult(); }

The sqrt is the keystone. File sizes span seventeen orders of magnitude (4e33.0e20); a linear line term would make late files either trivial or eternal. The square root halves the exponent the player must cover with bandwidth upgrades while leaving big files a real sink. Worked, with downMult()===1:

Largest unlocked clientDown()
Tier A only (4e3) 1,000 B/s
Cat6 (Wikipedia 9.0e8) ≈ 474,342 B/s
Private (Every_Linux_ISO_Ever 2.4e11) ≈ 7,745,967 B/s
Fiber (CERN…root 1.9e14) ≈ 217,944,947 B/s
Singularity (The_Final_Hash 3.0e20) ≈ 2.74e11 B/s

DEV NOTE — unlocking a tier is the bandwidth grant. Because maxUnlockedSize() reads the catalog through catalogUnlocked, buying cat6 simultaneously reveals MB-class files and raises the line to download them, so the player never unlocks content they're structurally unable to finish. The corollary: maxUnlockedSize() iterates the full CATALOG on every clientDown() call, and clientDown() runs per-torrent per-tick — cheap at 23 entries, but the first thing to memoize if the catalog grows.

downMult() (line 568) stacks the multiplicative download bonuses: bandwidth (×2.2/level via HW_TIERS), overclock (×1.8/level), the dpi perk, the ×10 DHT burst while now()<G.burstUntil, and the ×0.4 ISP throttle while now()<G.throttleUntil && !hasUnlock('quantum').

The peer fraction: seeds and downFrac

Seeds are assembled bottom-up. torrentSeeds(t) (line 616) sums the catalog swarm, baseSeedBonus(), and trackerSeedBonus(), then applies a per-torrent prioritization multiplier:

function torrentSeeds(t){
  const c=CAT_BY_ID[t.cid];
  const base=(c?c.seeds:1)+baseSeedBonus()+trackerSeedBonus();
  return Math.max(1, Math.round(base*(1+peerPrioBonus(t))));
}
  • baseSeedBonus() = 2*lvl('upnp') + 2 if magnet is owned + 2*plvl('seed0') — global seed floor.
  • trackerSeedBonus() sums seeds over G.trackerList — the announce loop.
  • peerPrioBonus(t) = +0.15 for dropLeechers and +0.15 for prioSeeds (the two Peers-tab checkboxes), applied multiplicatively to the assembled base.

The fraction of the line you actually hit:

const SAT_SEEDS = 12;   // connected seeds that saturate the line
function perPeerMult(){ return Math.pow(UP_BY_ID.perpeer.mult, lvl('perpeer')); } // ×1.3/level
function downFrac(seeds){ return Math.min(1, seeds/SAT_SEEDS*perPeerMult()); }

Below SAT_SEEDS worth of effective peers you are peer-limited: a fresh catalog file with seeds: 6 and no perpeer upgrades runs at 6/12 = 0.50 — half its line. The two ways out are announcing trackers (more seeds) and perpeer (each level multiplies the fraction by 1.3, so the General tab reports the falling "seeds to saturate" count). At 12 effective seeds, downFrac clamps to 1.0 and the torrent goes line-limited.

The realization

function torrentDownSpeed(t){
  if(t.status!=='downloading') return 0;
  return lineSpeed() * downFrac(torrentSeeds(t));
}

That product is the in-game promise made concrete: lineSpeed × min(1, seeds/SAT_SEEDS × perPeerMult) equals min(lineSpeed, lineSpeed × seeds × perPeerMult / SAT_SEEDS) — the line, or the peer term, whichever binds. The General tab labels which is active via the (line-limited) / (peer-limited) tag from downFrac(...)>=1.

Seeding income

Income is simpler — there is no peer gating on upload. A completed torrent flips to seeding and earns:

function torrentUpSpeed(t){
  if(t.status!=='seeding') return 0;
  const c=CAT_BY_ID[t.cid];
  return (c.up * upMultGlobal()) + seedboxFlat();
}

c.up is the per-torrent base income. upMultGlobal() (line 577) multiplies upslot (×1.55/level), compress (×1.8/level), ratio (×1.12/level), the ghost perk, and the same ×0.4 throttle penalty. seedboxFlat() is lvl('seedbox') * 5e5 — a flat addend per torrent, strongest on small files and dominant early after Private. In step() (line 668) the gain lands in four counters at once: G.data, G.totalUp, G.runUp, G.lifetimeUp. The status-bar Ratio is the run aggregate G.totalUp/G.totalDown, distinct from each torrent's own uploaded/c.size.

The catalog and tier gating

CATALOG (line 402) is 23 fixed entries — {id,name,size,up,seeds,req,rare?}. req is the unlock id that reveals the entry; catalogUnlocked(c) resolves it, routing darknet/singularity to hasPerkUnlock (prestige perks) and everything else to hasUnlock (bought upgrades):

function catalogUnlocked(c){
  if(!c.req) return true;
  if(c.req==='darknet') return hasPerkUnlock('darknet');
  if(c.req==='singularity') return hasPerkUnlock('singularity');
  return hasUnlock(c.req);
}

The six tiers, each ~10³10⁴× the previous in both size and income:

Tier req Size range Example Gated by
Public A null 4 kB 6.5 MB tiny_release_notes.txt always
Public B cat6 44 MB 900 MB Wikipedia_Full_Text_Dump.xml Cat6 upgrade
Private private 4.2 GB 240 GB GameOfThorns.S01.REMUX.2160p Private Tracker Invite
Fiber fiber 1.6 TB 190 TB CERN_Collider_Event_Logs.root Fiber-Optic Trunk
Darknet darknet 1.3 PB 600 PB The_Library_Of_Everything.iso perk (prestige)
Singularity singularity 5 EB 300 EB The_Final_Hash.dat perk (gated behind Darknet)

rare:true entries (t11, t15, t19, t22) stall at 99.9% — see the swarm systems. The unlock chain Public → Cat6 → Private → Fiber → Darknet → Singularity is enforced by the upgrades' reqUp (private requires cat6; fiber requires private) and the perks' reqPerk (singularity requires darknet). The last two tiers are only reachable through migration, which is the structural reason to prestige.

The swarm systems

Trackers / announce

announceTracker() (line 736) spends Data to push a {url,seeds,peers,status} onto G.trackerList, drawing from TRACKER_URLS (falling back to generated .onion relays once the pool is exhausted). Cost escalates with how many you've added past the two defaults:

function announceCost(){
  const added=Math.max(0, (G.trackerList||[]).length-2);
  return Math.max(120, totalUpSpeed()*18) * Math.pow(1.85, added);
}

Each tracker adds 24 seeds to every torrent (via trackerSeedBonus), which is the primary lever against peer-limiting.

Peer prioritization

The Peers detail tab exposes two per-torrent checkboxes, dropLeechers and prioSeeds, wired through peer-toggle. Each contributes +0.15 in peerPrioBonus(t), so both ticked is ×1.30 on assembled seeds. (The per-row Prioritize/Ban buttons are cosmetic — peer-boost nudges burstUntil by 2.5 s, peer-ban only toasts.)

RSS glob filter

globToRe(glob) (line 715) compiles a shell-glob into a case-insensitive RegExp: it escapes regex metacharacters, then maps *.* and ?., defaulting to /.*/i on empty or malformed input. When rss is unlocked, rssAutoAdd() runs each tick: if a slot is free, it filters the catalog to unlocked entries matching rssFilter, sorts by up descending, and adds the single highest-income match. It is a hands-off "always fill the best slot" automation, not a queue.

IRC announce-bot magnets

ircMagnetDrop() (line 751) periodically pushes a magnet line tagging a random unlocked cid; the IRC tab renders it with a clickable [🧲 grab]. grabMagnet(cid) calls addTorrent(cid, true, 0.25) — the third arg is a head-start fraction, so the torrent is born 25% downloaded, skipping the slow peer-limited opening:

function addTorrent(cid, silent, headstart){
  // …
  downloaded: headstart? Math.min(c.size*headstart, c.size*0.999) : 0,

The Math.min(..., c.size*0.999) cap matters for rares — a head-start can't accidentally vault a rare torrent past its stall point.

DEV NOTE — the rare-stall mechanic is the one place the idle contract bites. A rare torrent freezes at exactly c.size*0.999, sets status:'stalled', and earns nothing until forceComplete() (the Request last piece / bribe action) seeds the final block. Because simulateAway() reproduces the same stall logic (line 1636), a rare left downloading while you're away sits at 99.9% — not seeding — when you return. Intended friction, but the canonical "why didn't my offline earnings include that torrent" support question. See QA Playbook & Post-Mortems for the broader dead-zone discussion.

DHT bursts and ISP throttle

handleEvents() (line 679) schedules both. With dht owned, a burst sets burstUntil = now()+8000 and a ×10 download multiplier fires in downMult(), on a 4590 s cadence. With private owned but not quantum, the ISP throttle sets throttleUntil for 916 s and applies ×0.4 to both downMult() and upMultGlobal(). quantum (Quantum Encryption Protocol) is a hard immunity — the throttle branch checks !hasUnlock('quantum') in three places (the two multipliers and the status banner).

Hardware tiers

HW_TIERS (line 494) is the 13-name ladder the bandwidth upgrade walks. hwTierName() clamps the level into the array and appends a +N suffix past the end:

const HW_TIERS = ['56k Dial-up Modem','ISDN Line','ADSL Broadband','SDSL Line','Cable Broadband',
  'VDSL2','Gigabit Fiber','10G Fiber','Metro Ethernet','OC-768 Backbone','Tier-1 Telecom Trunk',
  'Dark Fiber Mesh','Quantum Backbone'];
function hwTierName(){
  const l=lvl('bandwidth');
  return HW_TIERS[Math.min(l,HW_TIERS.length-1)] + (l>=HW_TIERS.length?(' +'+(l-HW_TIERS.length+1)):'');
}

These names are pure flavor over the bandwidth multiplier — throughput comes from downMult(), not the string — but they anchor the lore's climb from dial-up to a Quantum Backbone.

The upgrades store

UPGRADES (line 438) drives the three-tab store (public / private / darknet). Repeatable upgrades cost floor(base * growth^level); unlock kinds are one-time at base. The full table:

id tab kind base growth effect cap
bandwidth public hardware 64 1.7 ×2.2 line / level 60
perpeer public perpeer 320 1.6 ×1.3 per-peer / level 30
upslot public mult-up 90 1.55 ×1.55 income / level 200
upnp public seeds 140 1.7 +2 seeds / level 60
storage1 public storage 600 1.9 +1 slot / level 40
ratio public ratio 260 1.5 ×1.12 income / level 150
cat6 public unlock 1,800 reveal Public B
dht public unlock 4,200 enable ×10 bursts
private private unlock 9,000 reveal Private (req cat6)
magnet private unlock 24,000 +2 base seeds (req private)
rss private unlock 60,000 RSS auto-add (req private)
seedbox private flat-up 40,000 1.8 +5e5 flat income / level 120
storage2 private storage 5e4 1.75 +1 slot / level (req private) 60
fiber darknet unlock 5e6 reveal Fiber (req private)
quantum darknet unlock 2e7 throttle immunity (req fiber)
overclock darknet mult-down 3e7 1.7 ×1.8 download / level (req fiber) 200
compress darknet mult-up 3.5e7 1.7 ×1.8 income / level (req fiber) 200

upgradeReqMet(u) enforces reqUp; buyUpgrade(id) (line 838) deducts Data, increments the level, and on unlocks fires diegetic IRC system lines (cat6, private, fiber). maxSlots() is 3 + storage1 + storage2 + plvl('rep') — the storage upgrades plus the prestige rep perk.

Prestige: Client Migration

Prestige is reframed as redeploying onto a Virtual Seedbox Cluster. The yield is sub-linear in run-upload:

function creditsFor(runUp){
  if(runUp<1e9) return 0;
  return Math.floor(Math.pow(runUp/1e9, 0.42));
}

So 1e9 → 1 credit, 1e10 → 2, 1e12 → 18. The 1e9 floor is the gate the toolbar button and modal enforce. doPrestige() (line 884) banks the gain and performs a surgical reset:

G.credits+=gain; G.prestiges++;
G.data=0; G.runUp=0; G.torrents=[]; G.upgrades={}; G.sel=null;
G.trackerList=defaultTrackers();
G.burstUntil=0;G.nextBurst=0;G.throttleUntil=0;G.nextThrottle=0;G.nextMagnet=0;

Wiped: Data, run upload, torrents, all bought upgrades, trackers, and event timers. Kept: credits, perks, prestiges, lifetimeUp. Credits buy PERKS (line 480) via buyPerk:

id persistent effect base cost growth cap
dpi +25% global download / level 1 1.6 50
ghost +25% global income / level 1 1.6 50
rep +1 permanent slot / level 2 1.8 25
seed0 +2 base seeds / level 2 1.7 25
darknet unlock Petabyte tier (once) 3
singularity unlock Exabyte+ tier (once, req darknet) 40

darknet and singularity are the only path to the top two catalog tiers — catalogUnlocked routes their req exclusively through hasPerkUnlock. The first migration is therefore the structural mid-game wall: you cannot brute-force into Petabyte files with Data alone.

DEV NOTE — perks compound multiplicatively with run upgrades because perkMult feeds the same downMult/upMultGlobal products that bandwidth/overclock/compress do. Two dpi levels (×1.25²×1.56) carry across the wipe, so each migration starts from a higher floor even though local hardware is gone. That is the whole feel of prestige here — you lose the machine, you keep the protocol.

· · ·

Balance levers & if you change this

  • BASE_DOWN (1000) and the sqrt in clientDown() — the master pacing dial. Dropping the sqrt to a smaller exponent stretches every download; removing it makes late tiers either trivial or impossible. Touch this and re-derive the line table above before shipping.
  • SAT_SEEDS (12) and perpeer mult (1.3) — govern how punishing peer-limiting is and how fast perpeer trivializes it. Raising SAT_SEEDS makes the announce/peer loop matter for longer; raising the mult lets a few perpeer levels obsolete it.
  • Catalog size/up/seeds and the per-tier 10³10⁴ gaps — the spine. maxUnlockedSize() reads size for the line, so a new top entry silently raises everyone's ceiling. Keep new entries in tier order or the sqrt math skips rungs.
  • creditsFor exponent (0.42) and the 1e9 floor — prestige cadence. A higher exponent makes each migration worth more credits and shortens the perk grind; lowering the floor lets players prestige before they've meaningfully scaled, which devalues the run.
  • seedboxFlat (+5e5/level, flat) vs. the multiplicative income chain — the flat addend is intentionally strongest on small files and early Private. If you make it scale, you erase the reason to prefer big seeders late-game.
  • Event tunables — burst ×10 / 8 s on a 4590 s cadence; throttle ×0.4 / 916 s on 80160 s. quantum is a binary immunity. Rebalance the throttle and you change how badly the player needs quantum before Fiber.
  • sanitizeState() is the safety net — any new G field must be added there with a sane default, or a pre-update save will load it as undefined and a tick will likely write NaN into G.data. Add the key to freshState and sanitizeState together.