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>
277 lines
19 KiB
Markdown
277 lines
19 KiB
Markdown
# 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**:
|
||
|
||
```js
|
||
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 (`4e3` → `3.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:
|
||
|
||
```js
|
||
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:
|
||
|
||
```js
|
||
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
|
||
|
||
```js
|
||
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:
|
||
|
||
```js
|
||
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):
|
||
|
||
```js
|
||
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:
|
||
|
||
```js
|
||
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 2–4 `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:
|
||
|
||
```js
|
||
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 45–90 s cadence. With `private` owned but not `quantum`, the ISP throttle sets `throttleUntil` for 9–16 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:
|
||
|
||
```js
|
||
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:
|
||
|
||
```js
|
||
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:
|
||
|
||
```js
|
||
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 45–90 s cadence; throttle ×0.4 / 9–16 s on 80–160 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.
|