From a6a8efa661fa3983cc44e859d5e50d6d8ce3dac2 Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Fri, 16 Oct 2026 06:22:05 +0200 Subject: [PATCH] feat(sources): ZZZ's City Fund pass windows from Fandom Co-Authored-By: Claude Opus 5.5 --- docs/SOURCES.md | 9 +++++++ .../fandom-city-fund-2026-10-16.expected.json | 23 ++++++++++++++++ fixtures/zzz/fandom-city-fund-2026-10-16.html | 1 + src/ingest/adapters/index.ts | 26 +++++++++++++++++++ src/ingest/parsers/fandomTable.ts | 5 ++++ test/adapters/fandomTable.test.ts | 4 ++- test/adapters/game8.test.ts | 2 ++ 7 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 fixtures/zzz/fandom-city-fund-2026-10-16.expected.json create mode 100644 fixtures/zzz/fandom-city-fund-2026-10-16.html diff --git a/docs/SOURCES.md b/docs/SOURCES.md index 89ddc1a..f321dff 100644 --- a/docs/SOURCES.md +++ b/docs/SOURCES.md @@ -90,6 +90,7 @@ Two things this method cannot tell you, and both matter: | Genshin Impact (Fandom) | `genshin-impact.fandom.com/api.php?…page=Event` | `fandom`, fifth template | yes (via `api.php`) | **BUILT** 2026-09-12 — adds 11 events Game8 missed; Game8 priority preserves localStorage IDs | | Star Rail — Nameless Honor | `honkai-star-rail.fandom.com/api.php?…page=Nameless_Honor` | new (`fandomtable`) | yes (via `api.php`) | **BUILT** 2026-10-16 — the pass window as one `other` event, day precision | | Genshin — Miliastra Pass | `genshin-impact.fandom.com/api.php?…page=Miliastra_Pass` | existing (`fandomtable`) | yes (via `api.php`) | **BUILT** 2026-10-16 — the live pass run as one `other` event, day precision | +| ZZZ — New Eridu City Fund | `zenless-zone-zero.fandom.com/api.php?…page=New_Eridu_City_Fund` | existing (`fandomtable`) | yes (via `api.php`) | **BUILT** 2026-10-16 — the live fund run as one `other` event, day precision | | hololive Dreams | `holodori.wiki/wiki/Events` (still) | `holodoriwiki` | yes, but nothing new to fetch | **LAPSED** 2026-10-14 — NamuWiki is behind a Cloudflare challenge, the official site posts no events, Game8 has no page | --- @@ -878,6 +879,14 @@ passed. A `fetched` with more than zero events means the wiki is back. **Conduct:** the `RobotsCache` with the real User-Agent got `403` (the interstitial challenge) for `robots.txt` on 2026-10-16, so this host rests on the person-recorded reading for `genshin-fandom-events` and the standing `--assume-robots-on-403` decision. The API answered `200` to the real User-Agent for the single fixture fetch. +## 19. ZZZ — New Eridu City Fund window (Fandom) — BUILT 2026-10-16 + +**Source:** `https://zenless-zone-zero.fandom.com/api.php?action=parse&page=New_Eridu_City_Fund&prop=text&formatversion=2&format=json` (page `https://zenless-zone-zero.fandom.com/wiki/New_Eridu_City_Fund`), source id `zzz-fandom-city-fund`. Third pass-window source; no parser change beyond one `PASS_SOURCES` entry (constant title). + +**What it is:** the first table on the page, `Image | Name | Start | End | Version`, 20 rows (1.0, 2024-07-04, to 3.2, 2026-09-09 to 2026-10-19; counted independently of the parser). Each row not ended by `ctx.now` becomes one `other` event titled `New Eridu City Fund`, day precision. At capture (now 2026-10-16) one row is not ended (3.2) and one event is emitted. The second table (`Language | Official Name`) is not a pass table and is ignored. + +**Conduct:** the `RobotsCache` gets `403` (the interstitial challenge) for this host's `robots.txt`. The repository owner read it in a browser on 2026-10-16: the `User-agent: *` group allows `/api.php?`, `/api.php?action=` and `/api.php?*&action=` and disallows only `/wiki/Special:`, `/wiki/User_talk:`, `/wiki/Template:`, `/wiki/Template_talk:`, `/wiki/Help:`, `/wiki/User:`, `/wiki/UserProfile:` (and language variants); `Disallow: /` applies only to named crawlers (SemrushBot, serpstatbot, GPTBot, ImagesiftBot, CCBot, OAI-SearchBot, ClaudeBot, GrokBot, xAI-SearchBot, xAI-Grok, GrokApp, xAI-Bot); `ias_crawler` has `Crawl-delay: 1`. With the standing `--assume-robots-on-403` decision that covers this source. The API answered `200` to the real User-Agent for the single fixture fetch. + ## What every one of these costs, beyond the source Adding a game is never only a `SOURCES` entry: diff --git a/fixtures/zzz/fandom-city-fund-2026-10-16.expected.json b/fixtures/zzz/fandom-city-fund-2026-10-16.expected.json new file mode 100644 index 0000000..9303005 --- /dev/null +++ b/fixtures/zzz/fandom-city-fund-2026-10-16.expected.json @@ -0,0 +1,23 @@ +[ + { + "id": "zzz:new-eridu-city-fund:2026-09-09", + "game": "zzz", + "title": "New Eridu City Fund", + "type": "other", + "summary": null, + "startsAt": "2026-09-09T00:00:00.000Z", + "startPrecision": "day", + "endsAt": "2026-10-19T00:00:00.000Z", + "endPrecision": "day", + "regionScoped": false, + "regionEnds": null, + "sourceUrl": "https://zenless-zone-zero.fandom.com/wiki/New_Eridu_City_Fund/2026-09-09", + "sourceId": "zzz-fandom-city-fund", + "status": "published", + "confidence": 0.85, + "extractionMethod": "parser", + "version": 1, + "firstSeenAt": "2026-10-16T00:00:00.000Z", + "updatedAt": "2026-10-16T00:00:00.000Z" + } +] diff --git a/fixtures/zzz/fandom-city-fund-2026-10-16.html b/fixtures/zzz/fandom-city-fund-2026-10-16.html new file mode 100644 index 0000000..70e2479 --- /dev/null +++ b/fixtures/zzz/fandom-city-fund-2026-10-16.html @@ -0,0 +1 @@ +{"parse":{"title":"New Eridu City Fund","pageid":3085,"text":"

\n

\n\nThe New Eridu City Fund is a Battle Pass in Zenless Zone Zero. Players can complete various daily, weekly and Season Tasks in order to gain City Fund EXP.\n

Each of the 50 levels in the New Eridu City Fund requires 1,000 EXP. Upon reaching each level, players will unlock rewards depending on their Plan type (Basic Plan, Growth Plan, or Premium Plan). \"Polychrome\" Polychrome ×150 can also be used to purchase level(s).\n

\n\n\n

Event Details[]

\n

Duration[]

\n
  • After version update, around 10:00:00 (GMT+8) — two days before the next version update, 03:59:59 (server time)
  • \n
  • Purchasing of the Growth Plan, Premium Plan, and Plan Upgrade for the current event will close at 02:59:59 (server time) of the cutoff day, therefore one hour before the event ends. Once purchasing is closed, Proxies may still complete Expansion Tasks and claim rewards. Please pay attention to purchase availability times and the event duration.
\n

Requirements[]

\n\n

Overview[]

\n
  • At the start of each season of the New Eridu City Fund, the Basic Plan is automatically unlocked. Proxies can complete Expansion Tasks for experience and increase their Fund Level to obtain more rewards.
  • \n
  • Proxies who have already unlocked the \"Growth Plan\" can purchase the \"Plan Upgrade\" to upgrade to the \"Premium Plan.\"
  • \n
  • After unlocking the Growth Plan or Premium Plan and reaching City Fund Level Lv. 30, Proxies can claim the Great Builder's Certificate. The Great Builder's Certificate is a time-limited reward for the current City Fund period and will not be retained for the next New Eridu City fund period. It will expire automatically if not used within the time limit.
\n

Choose Rewards[]

\n

From Version 2.5, upon opening the New Eridu City Fund, the player can choose to boost several of the reward items. There are a set of Base Rewards, from which the player can choose 3 to boost, and a set of Advanced Rewards, from which the player can select only 1 to boost. Depending on the items chosen to boost, the Basic Plan and Growth Plan reward tracks will change accordingly.\n

\n

Base Rewards[]

\n

The player can pick any 3 of the following to boost:\n

\n\n

Advanced Rewards[]

\n

The player can pick any 1 of the following to boost:\n

\n\n

Changes[]

\n

Depending on the items chosen, the reward track for each plan will change:\n

\n
  • Basic Plan:\n
    • Levels 1, 6, 9, 11, 16, 19, 29, 39, 44, and 49 will change.
  • \n
  • Growth Plan:\n
    • (TBA)
\n

Event Runs[]

\n
Current Event Run
\n
Asia
Ends in:
October 18, 2026 07:59:59 PM UTC
Europe
Ends in:
October 19, 2026 02:59:59 AM UTC
America
Ends in:
October 19, 2026 08:59:59 AM UTC
\n
\n

Plans[]

\n

Basic Plan[]

\n

The Basic Plan is free and unlocked by default. It awards the following items upon reaching their corresponding levels:\n

\n
500,000
\n

Growth Plan[]

\n

The Growth Plan can be purchased for USD $9.99 and will unlock the \"Growth Plan\" tier for the current Fund Period. In addition to the Basic Plan rewards, it grants:\n

\n\n
900,000

Note: Out of those additional rewards, only the limited Avatar and 680 of the Polychromes are given immediately. All other additional rewards, including the remaining Polychrome stash, are locked behind progression.\n

Premium Plan[]

\n

The Premium Plan can be purchased for USD $21.99 (USD $19.99 First Premium Plan Purchase), or $11.99 (USD $9.99 First Premium Plan Purchase) if upgrading from the Growth Plan. The Premium Plan includes everything in the Growth Plan, as well as the following items:\n

\n\n

Note: All of the additional rewards of the Premium Plan are given immediately.\n

History[]

\n

New Eridu City Fund has 20 occurrences:\n

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
Image\nName\nStart\nEnd\nVersion\n
\"New\nNew Eridu City Fund\nJuly 4, 2024\nAugust 11, 2024\n1.0\n
\"New\nNew Eridu City Fund\nAugust 14, 2024\nSeptember 23, 2024\n1.1\n
\"New\nNew Eridu City Fund\nSeptember 25, 2024\nNovember 4, 2024\n1.2\n
\"New\nNew Eridu City Fund\nNovember 6, 2024\nDecember 16, 2024\n1.3\n
\"New\nNew Eridu City Fund\nDecember 18, 2024\nJanuary 20, 2025\n1.4\n
\"New\nNew Eridu City Fund\nJanuary 22, 2025\nMarch 10, 2025\n1.5\n
\"New\nNew Eridu City Fund\nMarch 12, 2025\nApril 21, 2025\n1.6\n
\"New\nNew Eridu City Fund\nApril 23, 2025\nJune 2, 2025\n1.7\n
\"New\nNew Eridu City Fund\nJune 6, 2025\nJuly 14, 2025\n2.0\n
\"New\nNew Eridu City Fund\nJuly 16, 2025\nSeptember 1, 2025\n2.1\n
\"New\nNew Eridu City Fund\nSeptember 4, 2025\nOctober 13, 2025\n2.2\n
\"New\nNew Eridu City Fund\nOctober 15, 2025\nNovember 24, 2025\n2.3\n
\"New\nNew Eridu City Fund\nNovember 26, 2025\nDecember 29, 2025\n2.4\n
\"New\nNew Eridu City Fund\nDecember 30, 2025\nFebruary 2, 2026\n2.5\n
\"New\nNew Eridu City Fund\nFebruary 6, 2026\nMarch 23, 2026\n2.6\n
\"New\nNew Eridu City Fund\nMarch 24, 2026\nMay 4, 2026\n2.7\n
\"New\nNew Eridu City Fund\nMay 6, 2026\nJune 14, 2026\n2.8\n
\"New\nNew Eridu City Fund\nJune 17, 2026\nJuly 28, 2026\n3.0\n
\"New\nNew Eridu City Fund\nJuly 29, 2026\nSeptember 7, 2026\n3.1\n
\"New\nNew Eridu City Fund\nSeptember 9, 2026\nOctober 19, 2026\n3.2\n
\n

Other Languages[]

\n\n
LanguageOfficial Name
EnglishNew Eridu City Fund
Chinese
(Simplified)
丽都城募
Lìdū Chéngmù
Chinese
(Traditional)
麗都城募
Lìdū Chéngmù
Japaneseエリーファンド
Erī Fando
Korean리두 펀드
Ridu Peondeu
SpanishFinanciación de Nueva Eridu
FrenchFinancement de Ridu
RussianФонд Риду
Fond Ridu
ThaiNew Eridu City Fund
VietnameseRidu Chiêu Mộ
GermanRidu-Fonds
IndonesianDana Kota New Eridu
PortugueseFundos de Nova Eridu
\n

Change History[]

\n
\n

Version 2.5\n

\n
  • Upon purchasing the Growth Plan, the player will now immediately receive \"Polychrome\" Polychrome ×680, instead of needing to reach level 50.
  • \n
  • The unique Avatar was moved from the Premium Plan to the Growth Plan.
  • \n
  • The unique Namecard was moved from the Premium Plan to the Growth Plan, unlocking at level 10.
  • \n
  • The amount of Polychrome for the Premium Plan was increased from 200 to 208.
  • \n
  • The rewards were modified for certain levels:\n
    • Basic Plan:\n
    • \n
    • Growth Plan:\n
      • Level 5: Polychrome ×100, was changed to Ether Battery x2, and Original Chip ×12 was changed to Original Chip ×18
      • \n
      • Level 10: Encrypted Master Tape ×1, was changed to Polychrome x160, and Original Chip ×12 was changed to the exclusive Namecard
      • \n
      • Level 15: Ether Battery ×3, was changed to Ether Battery ×2
      • \n
      • Level 20: Encrypted Master Tape ×1, was changed to Polychrome x160
      • \n
      • Level 25: Ether Battery ×3, was changed to Ether Battery ×2
      • \n
      • Level 30: Encrypted Master Tape ×1, was changed to Polychrome x160
      • \n
      • Level 40: Encrypted Master Tape ×1, was changed to Polychrome x160, and Original Chip ×45 was changed to W-Engine Microcontroller Unit x1
      • \n
      • Level 45: Hi-Fi Master Copy ×10, was changed to Polychrome x160
      • \n
      • Level 50: Polychrome ×680, was changed to Resonating Core x1
\n
\n

Version 1.2\n

\n
  • Adjusted maximum level from 50 to 60\n
\n
\n

Version 1.0\n

\n
  • New Eridu City Fund was released.
\n\n\n\n\n
"}} \ No newline at end of file diff --git a/src/ingest/adapters/index.ts b/src/ingest/adapters/index.ts index 5d9d522..9038ee7 100644 --- a/src/ingest/adapters/index.ts +++ b/src/ingest/adapters/index.ts @@ -158,6 +158,32 @@ const SOURCES: SourceSpec[] = [ url: "https://genshin-impact.fandom.com/api.php?action=parse&page=Miliastra_Pass&prop=text&formatversion=2&format=json", parserId: "fandomtable", }, + { + id: "zzz-fandom-city-fund", + game: "zzz", + // The API, as every Fandom source (AGENTS.md § Fandom). This host's + // robots.txt answers our client (`RobotsCache`, the real User-Agent) with + // 403, the interstitial challenge. The repository owner read + // https://zenless-zone-zero.fandom.com/robots.txt in a browser on + // 2026-10-16: the `User-agent: *` group has `Allow: /api.php?`, + // `Allow: /api.php?action=` and `Allow: /api.php?*&action=`, then + // `Disallow:` only for /wiki/Special:, /wiki/User_talk:, /wiki/Template:, + // /wiki/Template_talk:, /wiki/Help:, /wiki/User: and /wiki/UserProfile: + // (and their language variants); no `Disallow: /` for `*`. `Disallow: /` + // applies only to named crawlers (SemrushBot, serpstatbot, GPTBot, + // ImagesiftBot, CCBot, OAI-SearchBot, ClaudeBot, GrokBot, xAI-SearchBot, + // xAI-Grok, GrokApp, xAI-Bot), none of which we are; `ias_crawler` has + // `Crawl-delay: 1`. That reading and the standing `--assume-robots-on-403` + // decision (scripts/refresh-sources.ts, AGENTS.md § Scraping conduct) are + // what cover this source. The API path answered 200 to the real + // User-Agent for the single fixture fetch; no browser-shaped headers. + // + // A pass history: the `fandomtable` reader finds the `Name | Start | End | + // Version` table by heading; the constant title comes from `PASS_SOURCES` + // in `parsers/fandomTable.ts`, keyed by this id. + url: "https://zenless-zone-zero.fandom.com/api.php?action=parse&page=New_Eridu_City_Fund&prop=text&formatversion=2&format=json", + parserId: "fandomtable", + }, { id: "r1999-fandom-events", game: "r1999", diff --git a/src/ingest/parsers/fandomTable.ts b/src/ingest/parsers/fandomTable.ts index 2c4899d..d41159b 100644 --- a/src/ingest/parsers/fandomTable.ts +++ b/src/ingest/parsers/fandomTable.ts @@ -58,6 +58,11 @@ export const PASS_SOURCES: Readonly> = { pass: "Miliastra Pass", title: (rowName) => `Miliastra Pass: ${rowName}`, }, + "zzz-fandom-city-fund": { + // One fund, one name; the Name cell is the run's own label, so a constant. + pass: "New Eridu City Fund", + title: () => "New Eridu City Fund", + }, }; /** The columns a pass table is recognised by, as normalised heading text. */ diff --git a/test/adapters/fandomTable.test.ts b/test/adapters/fandomTable.test.ts index a6a1688..9f215d3 100644 --- a/test/adapters/fandomTable.test.ts +++ b/test/adapters/fandomTable.test.ts @@ -111,7 +111,7 @@ describe("fandomtable canParse across every fixture", () => { expect(files.length).toBeGreaterThan(20); }); - test("true on the two pass-history fixtures only", async () => { + test("true on the three pass-history fixtures only", async () => { const accepted: string[] = []; for (const f of files) { if (fandomTableParser.canParse(await Bun.file(f).text())) accepted.push(f); @@ -119,6 +119,7 @@ describe("fandomtable canParse across every fixture", () => { expect(accepted).toEqual([ "fixtures/genshin/fandom-miliastra-pass-2026-10-16.html", FIXTURE, + "fixtures/zzz/fandom-city-fund-2026-10-16.html", ]); }); @@ -126,6 +127,7 @@ describe("fandomtable canParse across every fixture", () => { for (const f of [ FIXTURE, "fixtures/genshin/fandom-miliastra-pass-2026-10-16.html", + "fixtures/zzz/fandom-city-fund-2026-10-16.html", ]) { const text = await Bun.file(f).text(); const claimants = PARSERS.filter((p) => p.canParse(text)).map((p) => p.id); diff --git a/test/adapters/game8.test.ts b/test/adapters/game8.test.ts index 37300c7..116d310 100644 --- a/test/adapters/game8.test.ts +++ b/test/adapters/game8.test.ts @@ -75,6 +75,8 @@ const CASES: Array<{ adapter: Adapter; fixture: string; now?: string }> = [ { adapter: adapter("hsr-fandom-nameless-honor"), fixture: "fixtures/hsr/fandom-nameless-honor-2026-10-16", now: "2026-10-16T00:00:00.000Z" }, // Genshin's Miliastra Pass history (same reader). { adapter: adapter("genshin-fandom-miliastra-pass"), fixture: "fixtures/genshin/fandom-miliastra-pass-2026-10-16", now: "2026-10-16T00:00:00.000Z" }, + // ZZZ's New Eridu City Fund history (same reader). + { adapter: adapter("zzz-fandom-city-fund"), fixture: "fixtures/zzz/fandom-city-fund-2026-10-16", now: "2026-10-16T00:00:00.000Z" }, ]; async function runAdapter(adapter: Adapter, fixture: string, now: string = NOW) {