Merge pull request #16 from Golumpa/dev/0.3.0

Release version 0.3.0 with expanded decoding and mapping support
This commit is contained in:
Golumpa 2026-08-19 14:06:05 +01:00 • committed by GitHub
commit 93c6626640
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 1492 additions and 151 deletions

View file

@ -1,4 +1,4 @@
name: Update reward mappings
name: Update mappings
on:
workflow_dispatch:
@ -14,7 +14,7 @@ permissions:
pull-requests: write
concurrency:
group: update-reward-mappings
group: update-mappings
cancel-in-progress: false
jobs:
@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest
env:
AUTOMATION_BRANCH: dev/automated-mapping-update
AUTOMATION_BRANCH: dev/automated-mapping-update-${{ github.ref_name }}
NTE_ASSETS_REF: ${{ inputs.source_ref }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -32,7 +32,7 @@ jobs:
uses: actions/checkout@v5
with:
fetch-depth: 0
ref: ${{ github.event.repository.default_branch }}
ref: ${{ github.ref_name }}
- name: Get Python
uses: actions/setup-python@v6
@ -42,7 +42,7 @@ jobs:
- name: Install project
run: python -m pip install -e .
- name: Rebuild reward mappings
- name: Rebuild mappings
run: python tools/update_mappings.py --source-ref "$NTE_ASSETS_REF" --apply
- name: Run tests
@ -83,15 +83,15 @@ jobs:
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -B "$AUTOMATION_BRANCH"
git add mappings/arcs.json mappings/characters.json mappings/items.json
git commit -m "chore: update reward mappings from NTE_Assets"
git add mappings/arcs.json mappings/characters.json mappings/items.json mappings/achievements.json
git commit -m "chore: update mappings from NTE_Assets"
git push --force origin "HEAD:refs/heads/$AUTOMATION_BRANCH"
- name: Open or update pull request
if: steps.changes.outputs.changed == 'true'
shell: bash
env:
BASE_BRANCH: ${{ github.event.repository.default_branch }}
BASE_BRANCH: ${{ github.ref_name }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
python - <<'PY'
@ -108,7 +108,7 @@ jobs:
f"- {changes['additions']} additions\n"
f"- {changes['updates']} updates\n"
f"- {changes['deletions']} deletions\n\n"
"The workflow rebuilt all reward mappings, resolved English names through "
"The workflow rebuilt all reward and achievement mappings, resolved English text through "
"`Localization/en/game.json`, and passed the complete test suite. Pool mapping "
"files and UID inputs were not changed.\n\n"
f"[Workflow run]({os.environ['RUN_URL']})\n"
@ -125,12 +125,12 @@ jobs:
if [ -n "$pr_number" ]; then
gh pr edit "$pr_number" \
--title "chore: update reward mappings from NTE_Assets" \
--title "chore: update mappings from NTE_Assets" \
--body-file build/mapping-update/pull-request-body.md
else
gh pr create \
--head "$AUTOMATION_BRANCH" \
--base "$BASE_BRANCH" \
--title "chore: update reward mappings from NTE_Assets" \
--title "chore: update mappings from NTE_Assets" \
--body-file build/mapping-update/pull-request-body.md
fi

View file

@ -4,7 +4,7 @@
# NTE History Exporter
Prototype CLI exporter for **Neverness to Everness** pull history — decodes your own game traffic into sanitized JSON ready for tracker import.
Prototype CLI exporter for **Neverness to Everness** pull history and achievements — decodes your own game traffic into sanitized JSON.
![Python](https://img.shields.io/badge/python-3.10+-blue)
![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-0078D6)
@ -23,10 +23,17 @@ Prototype CLI exporter for **Neverness to Everness** pull history — decodes yo
## What It Does
The exporter decodes Permanent Board, Limited Character Board, Arc Miracle Box, and Mystery Box history pages from captured UDP data, applies conservative timestamp-boundary handling, and writes sanitized JSON suitable for tracker import.
The exporter decodes Permanent Board, Limited Character Board, Arc Miracle Box,
and Mystery Box history pages from captured UDP data. It also captures tracked
achievements during login, including completed and in-progress records. Pull
history is written as tracker-ready JSON and achievements use a separate
versioned JSON format.
> [!NOTE]
> The import JSON contains decoded history rows and the shareable NTE user UID when it can be detected. It does **not** export tokens, account IDs, role IDs, device IDs, server IPs, raw packets, cookies, session data, or other capture metadata.
> Export JSON contains decoded history or achievement records and the shareable
> NTE user UID when it can be detected. It does **not** export tokens, account
> IDs, role IDs, device IDs, server IPs, raw packets, cookies, session data, or
> other capture metadata.
## Requirements
@ -90,7 +97,11 @@ From source:
Or simply double-click **`run-exporter.cmd`**.
> [!IMPORTANT]
> For automatic user UID and account-server detection, launch the tool **before pressing Start on the game's main menu**. If you are already in game, history capture can still work; the tool will ask for missing account details before saving.
> Start the exporter **before pressing Start on the game's main menu**. This is
> required for achievement capture and enables automatic user UID and
> account-server detection. If already in game, pull-history capture still
> works, but achievements require returning to the main menu and logging in
> again; missing account details are requested before saving.
Once running, open any supported history board in game. The tool keeps listening until you press any key. After it prints and saves the results, press any key again to close the exporter. Exports are written under `exports\` as:
@ -98,6 +109,7 @@ Once running, open any supported history board in game. The tool keeps listening
- `<user_uid>_Limited_<date_time>.json`
- `<user_uid>_Arc_<date_time>.json`
- `<user_uid>_MysteryBox_<date_time>.json`
- `<user_uid>_Achievements_<date_time>.json`
If the user UID is not detected automatically, the console asks for it before saving. Leaving it blank saves as `unknown_<banner>_<date_time>.json`, but may prevent import on some trackers.
@ -110,6 +122,25 @@ Exports are not copied to the clipboard by default. Add `--copy-clipboard` to co
If a page response is missed, the exporter reports the missing page number while capture is still running. Leave the exporter open, close and reopen that history board, then scroll down again. Scrolling backward within the existing view does not request the cached pages again. The replacement capture is accepted and the tool confirms when the gap has been recovered. If reopening the board still produces no page messages, return to the main menu and re-enter the game to start a fresh connection.
### Achievement export
Start the live exporter before pressing Start on the game's main menu. The
tracked achievement state is loaded during login, captured automatically, and written to
`exports/<user_uid>_Achievements_*.json` as a versioned achievement export with
account metadata, completion totals, and records grouped by achievement category.
Both completed and in-progress achievements are included with their current
progress, status, and completion time when available. Generated metadata adds
the localized title and description, target, quality, and rewards from the
current `NTE_Assets` achievement table. Newly captured IDs that are not yet in
the bundled mapping still export normally; only their optional display metadata
is omitted until the mappings are refreshed.
PlayStation trophy entries are retained and identified separately
even though they are not shown in the in-game achievement list. The console
reports completed and in-progress totals for in-game and PlayStation records as
soon as the list has been decoded. Every observed record without a completion
timestamp is classified as `in_progress`, even when its numeric progress is
zero; compound achievements can track hidden checklist progress this way.
#### Linux/macOS
Downloaded executable, using `sudo` when your system requires elevated capture permission:
@ -150,7 +181,7 @@ Decodes a `mitmproxy .flows` capture instead of listening live — used for rese
| Flag | Effect |
| --------- | --------------------------------------------------- |
| `--live` | Capture live UDP traffic instead of reading a file. |
| `--live` | Capture live traffic, including achievements loaded at login. |
| `--debug` | Also write the research CSV and privacy-safe capture diagnostics. |
| `--user-uid <uid>` | Override the auto-detected NTE user UID in the JSON export. |
| `--copy-clipboard` | Copy a single live export JSON to clipboard after saving. |
@ -175,22 +206,22 @@ The exporter automatically includes the shareable NTE user UID when it appears i
## Mapping maintenance
Reward metadata can be rebuilt directly from the latest NTE_Assets tables and
English translation files. The reward snapshot may change IDs while UID inputs
remain untouched:
Reward and achievement metadata can be rebuilt directly from the latest
NTE_Assets tables and English translation files. Mapping snapshots may change
IDs while UID inputs remain untouched:
Run **Update reward mappings** from the GitHub Actions tab to generate and test
Run **Update mappings** from the GitHub Actions tab to generate and test
the snapshot in a reviewable pull request, or run it locally:
```powershell
python tools/update_mappings.py
```
This rebuilds reviewable reward-map candidates directly from NTE_Assets under
This rebuilds reviewable mapping candidates directly from NTE_Assets under
`build/mapping-update/`; committed mappings are untouched unless `--apply` is
explicitly supplied. The snapshot may include additions, updates, and removals,
while pool mappings and UID inputs remain untouched. See
[Reward mapping updates](docs/mapping-updates.md) for the source rules and
[Mapping updates](docs/mapping-updates.md) for the source rules and
review workflow.
## Privacy

View file

@ -15,7 +15,7 @@ decoder-only offsets.
"capture_source": "npcap",
"exporter": {
"name": "nte-history-exporter",
"version": "0.2.1"
"version": "0.3.0"
},
"banner": {
"id": "Lottery_Permanent",
@ -84,6 +84,12 @@ Current stable pool IDs:
Avoid using `banner.name`, `reward_name`, `reward_type` or `reward_rank` as primary IDs. They
are useful display fields, but may change when mapping files are updated.
Known `reward_id` values are matched case-insensitively and emitted using the
canonical casing from the bundled mapping. This prevents packet/asset casing
differences from dropping display metadata or producing different downstream
IDs. An unknown reward keeps the casing decoded from the packet. Reward IDs are
not UID inputs, so canonicalizing their casing does not change pull UIDs.
## Stability notes
Every JSON record is exported with a stable `uid`. Re-scanning deeper history can
@ -176,6 +182,79 @@ Mystery Box:
}
```
## Achievement export
Achievements are written separately as
`<user_uid>_Achievements_<date_time>.json`. The document contains only records
observed in the account's login response; definitions found only in the asset
mapping are not synthesized into the export.
```json
{
"format": "nte-achievement-export",
"format_version": 1,
"game": "Neverness to Everness",
"source": "live_capture",
"capture_source": "npcap",
"exporter": {
"name": "nte-history-exporter",
"version": "0.3.0"
},
"scan": {
"in_game": {
"total_achievements": 367,
"completed_achievements": 310,
"in_progress_achievements": 57
},
"playstation": {
"total_achievements": 35,
"completed_achievements": 31,
"in_progress_achievements": 4
}
},
"user_uid": "218216016349",
"server_id": "23003",
"account_region": "EU",
"categories": {
"battle": [
{
"id": "Battle_25",
"platform": "in_game",
"status": "in_progress",
"progress": 20,
"completed": false,
"completed_at": null,
"name": "Devil Within II",
"description": "Trigger Hexed ×50.",
"quality": "high",
"target": 50,
"rewards": [
{"item_id": "Annulith", "amount": 10}
]
}
]
}
}
```
Achievement fields:
- `scan.in_game` and `scan.playstation` separate records visible in game from
PlayStation trophy records and report completed/in-progress totals.
- `categories` groups records by the prefix of their captured ID. The category
is therefore not repeated on every record.
- `status` is `completed` when completion ticks are present and `in_progress`
otherwise. An observed compound achievement can be in progress while its
numeric `progress` is zero because individual checklist state is server-side.
- `completed_at` is a decoded UTC time for completed records and `null` for
in-progress records.
- `name`, `description`, `quality`, `target`, and `rewards` are optional display
metadata from `mappings/achievements.json`.
- A newly captured ID missing from the bundled mapping still exports its core
captured fields; only optional display metadata is omitted.
- Asset definitions absent from the network response are not exported because
absence cannot distinguish unstarted, locked, or unavailable content.
## CSV diagnostics
CSV exports are mainly for debugging the decoder. Tools should prefer JSON

View file

@ -21,5 +21,13 @@ Known limitations:
- The file adapter reads mitmproxy `.flows` captures for research and testing.
- Npcap is Windows-only and is not redistributed with this project; Linux and macOS use their system libpcap.
- Reward keys decode to their reward id string, so unknown rewards still export a usable `reward_id`; display names/ranks come from the mapping JSON files and should be expanded as new rewards appear.
- Achievement exports contain only records observed in the login response.
Asset-only definitions are not synthesized because absence cannot distinguish
unstarted, locked, or unavailable content. Unknown captured achievement IDs
still export without optional display metadata.
- Observed achievements have only `completed` and `in_progress` states. Some
compound achievements expose numeric progress `0` while the server tracks
hidden checklist progress, so every observed record without completion ticks
is classified as `in_progress`.
Privacy guardrail: raw packet data must not be included in sanitized exports.

View file

@ -1,11 +1,12 @@
# Reward mapping updates
# Mapping updates
`tools/update_mappings.py` rebuilds `arcs.json`, `characters.json`, and
`items.json` directly from the current
`tools/update_mappings.py` rebuilds `arcs.json`, `characters.json`,
`items.json`, and `achievements.json` directly from the current
[`Waifus-Grace/NTE_Assets`](https://github.com/Waifus-Grace/NTE_Assets)
tables.
English display names come from `Localization/en/game.json`. The data tables
English display names and achievement descriptions come from
`Localization/en/game.json`. The data tables
provide localization namespace/key references and quality metadata; their
embedded `LocalizedString` values are deliberately ignored.
@ -18,15 +19,17 @@ game whose asset tables evolve over time.
### GitHub Actions
Run **Update reward mappings** from the repository's Actions tab. The optional
`source_ref` input accepts an NTE_Assets branch, tag, or commit and defaults to
`main`.
Run **Update mappings** from the repository's Actions tab. **Use workflow
from** selects the exporter branch whose updater is run and the target branch
for the resulting pull request. The `source_ref` input separately selects an
NTE_Assets branch, tag, or commit and defaults to `main`.
The workflow rebuilds the mappings, runs the complete test suite, uploads the
JSON review report, and opens a pull request from
`dev/automated-mapping-update`. Running it again updates the same open pull
request. If the authoritative snapshot has not changed, it reports zero changes
and does not create a commit or pull request.
`dev/automated-mapping-update-<selected-branch>`. Running it again for the same
selected branch updates that branch's open pull request. If the authoritative
snapshot has not changed, it reports zero changes and does not create a commit
or pull request.
The repository setting **Allow GitHub Actions to create and approve pull
requests** must be enabled for automatic pull-request creation. The workflow
@ -54,7 +57,7 @@ To use an existing local checkout without network access:
python tools/update_mappings.py --assets-root path\to\NTE_Assets
```
The default output is `build/mapping-update/` and contains the three candidate
The default output is `build/mapping-update/` and contains the four candidate
mapping files plus `mapping-update-report.json`. The report lists additions,
updates, and deletions by file and records SHA-256 hashes for every source
table.
@ -67,15 +70,21 @@ python -m pytest -q
```
For CI, `--check` exits with status 1 whenever the generated snapshot differs
from the committed reward mappings. It still writes the staged artifacts.
from the committed mappings. It still writes the staged artifacts.
## Source rules
- Every achievement in `DT_AchievementConfigInfo` is emitted to
`achievements.json` with its localized title and description, target,
category, quality, and rewards.
- Every Arc in `DT_ForkItemData` is emitted to `arcs.json`.
- Every character in `DT_Character` is emitted to `characters.json`.
- Other pull rewards are selected by `GachaIllustrate` and resolved to the
inventory tables for quality and localization keys. Appearance-table IDs
provide canonical casing for glider rewards.
- Vehicle liveries are resolved from their illustration name and icon. Rows
whose icon identifies a different reward are treated as unreleased upstream
placeholders and skipped until the assets are corrected.
- Mystery Box rewards are selected from every row in
`DT_GashaponLotteryGlobal`. No event IDs are hard-coded, so future rotations
are included automatically. Rewards resolve through the inventory, capital,
@ -91,12 +100,12 @@ from the committed reward mappings. It still writes the staged artifacts.
## UID compatibility boundary
This updater reads and writes only the three reward mapping files. It never
This updater reads and writes only the generated reward and achievement mapping files. It never
reads or writes the permanent, limited, beginner, or Arc pool mapping files.
Banner IDs, timestamps, record ordering, format version, and all UID inputs are
therefore unchanged by mapping synchronization.
Reward IDs and display metadata may change or disappear when the authoritative
NTE_Assets snapshot changes. Existing exported history files remain unchanged;
new exports describe rewards using the current asset snapshot. UID stability is
independent of reward display mappings.
Reward and achievement IDs or display metadata may change or disappear when
the authoritative NTE_Assets snapshot changes. Existing exports remain
unchanged; new exports use the current asset snapshot. UID stability is
independent of display mappings.

View file

@ -1,6 +1,21 @@
# Packet Format Notes
This prototype supports separate Monopoly, Arc/Gashapon, and Mystery Box history decoders.
This prototype supports separate Monopoly, Arc/Gashapon, Mystery Box history,
and achievement decoders.
## Achievements
The account's achievement component arrives in an inbound TCP game-data block
during login. The block is LZ4-decoded and contains `AchievementRecord` entries.
Each record provides an achievement ID, an unsigned numeric progress value, and
a .NET completion timestamp. A non-zero timestamp marks a completed record.
Every observed record without completion ticks is treated as in progress;
compound achievements can have server-side checklist progress while exposing a
numeric value of zero.
Only records present in the network response are exported. Display metadata is
looked up case-insensitively in `mappings/achievements.json`, because captured
and asset-table ID casing can differ. Missing metadata never prevents export.
## Decoder strategy
@ -86,6 +101,10 @@ Decoded fields:
Arc/Gashapon history uses the same scheme at `ASCII * 2`.
- The decoder decodes the key to its id string and looks up display metadata
in `mappings/arcs.json`, `mappings/characters.json`, and `mappings/items.json`.
Lookups are case-insensitive across Monopoly, Arc, and Mystery Box decoders;
known IDs are emitted using mapping-canonical casing.
Achievement display metadata is generated separately in
`mappings/achievements.json`.
Unknown rewards still export their decoded id with empty name/rank.
Page and row numbers are research metadata only. They must not be used for permanent dedupe because they shift when new history appears.

450
mappings/achievements.json Normal file
View file

@ -0,0 +1,450 @@
{
"battle_10": {"name": "Here It Comes! V", "description": "Use Ultimate ×300.", "category": "fight", "quality": "execellent", "target": 300, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"battle_11": {"name": "Speed Above All I", "description": "Trigger Critical Dodge ×10.", "category": "fight", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"battle_12": {"name": "Speed Above All II", "description": "Trigger Critical Dodge ×50.", "category": "fight", "quality": "normal", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"battle_13": {"name": "Speed Above All III", "description": "Trigger Critical Dodge ×100.", "category": "fight", "quality": "normal", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"battle_14": {"name": "Speed Above All IV", "description": "Trigger Critical Dodge ×200.", "category": "fight", "quality": "high", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"battle_15": {"name": "Speed Above All V", "description": "Trigger Critical Dodge ×300.", "category": "fight", "quality": "execellent", "target": 300, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"battle_16": {"name": "Crumbles at Touch I", "description": "In battle, Break enemies ×5.", "category": "fight", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"battle_17": {"name": "Crumbles at Touch II", "description": "In battle, Break enemies ×10.", "category": "fight", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"battle_18": {"name": "Crumbles at Touch III", "description": "In battle, Break enemies ×50.", "category": "fight", "quality": "normal", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"battle_19": {"name": "Crumbles at Touch IV", "description": "In battle, Break enemies ×100.", "category": "fight", "quality": "high", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"battle_20": {"name": "Crumbles at Touch V", "description": "In battle, Break enemies ×200.", "category": "fight", "quality": "execellent", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"battle_21": {"name": "Harvest Season I", "description": "Trigger Blossom ×10.", "category": "fight", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"battle_22": {"name": "Harvest Season II", "description": "Trigger Blossom ×50.", "category": "fight", "quality": "high", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"battle_23": {"name": "Harvest Season III", "description": "Trigger Blossom ×200.", "category": "fight", "quality": "execellent", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"battle_24": {"name": "Devil Within I", "description": "Trigger Hexed ×10.", "category": "fight", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"battle_25": {"name": "Devil Within II", "description": "Trigger Hexed ×50.", "category": "fight", "quality": "high", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"battle_26": {"name": "Devil Within III", "description": "Trigger Hexed ×200.", "category": "fight", "quality": "execellent", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"battle_27": {"name": "Stopped Thinking I", "description": "Trigger Remora ×10.", "category": "fight", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"battle_28": {"name": "Stopped Thinking II", "description": "Trigger Remora ×50.", "category": "fight", "quality": "high", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"battle_29": {"name": "Stopped Thinking III", "description": "Trigger Remora ×200.", "category": "fight", "quality": "execellent", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"battle_30": {"name": "Death Nova I", "description": "Trigger Nova ×10.", "category": "fight", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"battle_31": {"name": "Death Nova II", "description": "Trigger Nova ×50.", "category": "fight", "quality": "high", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"battle_32": {"name": "Death Nova III", "description": "Trigger Nova ×200.", "category": "fight", "quality": "execellent", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"battle_33": {"name": "Scarlet Corruption I", "description": "Trigger Scorch ×10.", "category": "fight", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"battle_34": {"name": "Scarlet Corruption II", "description": "Trigger Scorch ×50.", "category": "fight", "quality": "high", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"battle_35": {"name": "Scarlet Corruption III", "description": "Trigger Scorch ×200.", "category": "fight", "quality": "execellent", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"battle_36": {"name": "Soul Color I", "description": "Trigger Stain ×10.", "category": "fight", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"battle_37": {"name": "Soul Color II", "description": "Trigger Stain ×50.", "category": "fight", "quality": "high", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"battle_38": {"name": "Soul Color III", "description": "Trigger Stain ×200.", "category": "fight", "quality": "execellent", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"battle_39": {"name": "Bumper Harvest I", "description": "Trigger Charge ×10.", "category": "fight", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"battle_40": {"name": "Bumper Harvest II", "description": "Trigger Charge ×50.", "category": "fight", "quality": "high", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"battle_41": {"name": "Bumper Harvest III", "description": "Trigger Charge ×200.", "category": "fight", "quality": "execellent", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"battle_42": {"name": "Malevolence Unbound I", "description": "Trigger Discord ×10.", "category": "fight", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"battle_43": {"name": "Malevolence Unbound II", "description": "Trigger Discord ×50.", "category": "fight", "quality": "high", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"battle_44": {"name": "Malevolence Unbound III", "description": "Trigger Discord ×200.", "category": "fight", "quality": "execellent", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"battle_49": {"name": "Variation Movement I", "description": "Cast Skill ×10.", "category": "fight", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"battle_50": {"name": "Variation Movement II", "description": "Cast Skill ×50.", "category": "fight", "quality": "normal", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"battle_51": {"name": "Variation Movement III", "description": "Cast Skill ×100.", "category": "fight", "quality": "normal", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"battle_52": {"name": "Variation Movement IV", "description": "Cast Skill ×200.", "category": "fight", "quality": "high", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"battle_53": {"name": "Variation Movement V", "description": "Cast Skill ×300.", "category": "fight", "quality": "execellent", "target": 300, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"battle_54": {"name": "Death is the Final Movement of All Life I", "description": "Defeat enemies with Ultimate ×10.", "category": "fight", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"battle_55": {"name": "Death is the Final Movement of All Life II", "description": "Defeat enemies with Ultimate ×50.", "category": "fight", "quality": "high", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"battle_56": {"name": "Death is the Final Movement of All Life III", "description": "Defeat enemies with Ultimate ×200.", "category": "fight", "quality": "execellent", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"battle_6": {"name": "Here It Comes! I", "description": "Use Ultimate ×10.", "category": "fight", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"battle_63": {"name": "Come At Me!", "description": "In battle, have a single character with a Shield of over 5,000 points.", "category": "fight", "quality": "normal", "target": 5000, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"battle_64": {"name": "Non-Lethal Defense", "description": "In battle, have a single character with a Shield of over 10,000 points.", "category": "fight", "quality": "high", "target": 10000, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"battle_65": {"name": "Absolute Defense", "description": "In battle, have a single character with a Shield of over 30,000 points.", "category": "fight", "quality": "execellent", "target": 30000, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"battle_7": {"name": "Here It Comes! II", "description": "Use Ultimate ×50.", "category": "fight", "quality": "normal", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"battle_8": {"name": "Here It Comes! III", "description": "Use Ultimate ×100.", "category": "fight", "quality": "normal", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"battle_9": {"name": "Here It Comes! IV", "description": "Use Ultimate ×200.", "category": "fight", "quality": "high", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"develop_1": {"name": "Hethereau Beginner", "description": "Raise Hunter Level to Lvl 10.", "category": "develop", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_10": {"name": "One-Man Army IV", "description": "Raise 1 character to Lvl 80.", "category": "develop", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"develop_11": {"name": "Four-Man Squad I", "description": "Raise 4 characters to Lvl 20.", "category": "develop", "quality": "normal", "target": 4, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_12": {"name": "Four-Man Squad II", "description": "Raise 4 characters to Lvl 40.", "category": "develop", "quality": "normal", "target": 4, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_13": {"name": "Four-Man Squad III", "description": "Raise 4 characters to Lvl 60.", "category": "develop", "quality": "high", "target": 4, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"develop_14": {"name": "Four-Man Squad IV", "description": "Raise 4 characters to Lvl 80.", "category": "develop", "quality": "execellent", "target": 4, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"develop_15": {"name": "There are seven more fighters just as tough as me!", "description": "Raise 8 characters to Lvl 80.", "category": "develop", "quality": "execellent", "target": 8, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"develop_16": {"name": "Touching the Forbidden Edge I", "description": "Raise 1 Arc to Lvl 20.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_17": {"name": "Touching the Forbidden Edge II", "description": "Raise 1 Arc to Lvl 40.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_18": {"name": "Touching the Forbidden Edge III", "description": "Raise 1 Arc to Lvl 60.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_19": {"name": "Touching the Forbidden Edge IV", "description": "Raise 1 Arc to Lvl 80.", "category": "develop", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"develop_2": {"name": "The Master Shows the Way…", "description": "Raise Hunter Level to Lvl 20.", "category": "develop", "quality": "normal", "target": 20, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_20": {"name": "Duel Master I", "description": "Raise 4 Arcs to Lvl 20.", "category": "develop", "quality": "normal", "target": 4, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_21": {"name": "Duel Master II", "description": "Raise 4 Arcs to Lvl 40.", "category": "develop", "quality": "normal", "target": 4, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_22": {"name": "Duel Master III", "description": "Raise 4 Arcs to Lvl 60.", "category": "develop", "quality": "high", "target": 4, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"develop_23": {"name": "Duel Master IV", "description": "Raise 4 Arcs to Lvl 80.", "category": "develop", "quality": "execellent", "target": 4, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"develop_24": {"name": "Duel Master V", "description": "Raise 8 Arcs to Lvl 80.", "category": "develop", "quality": "execellent", "target": 8, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"develop_25": {"name": "The Nameless", "description": "Upgrade 1 Orange Console Module to Lvl 10.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_26": {"name": "Stick Fighter", "description": "Upgrade 1 Orange Console Module to Lvl 20.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_27": {"name": "Half-Hearted Devotion", "description": "Upgrade 6 Orange Console Modules to Lvl 10.", "category": "develop", "quality": "normal", "target": 6, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_28": {"name": "Retro King", "description": "Upgrade 6 Orange Console Modules to Lvl 20.", "category": "develop", "quality": "high", "target": 6, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"develop_29": {"name": "Hardcore Gamer", "description": "Upgrade 1 Orange Console Cartridge to Lvl 10.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_3": {"name": "Ultra-Spec Growth", "description": "Raise Hunter Level to Lvl 30.", "category": "develop", "quality": "high", "target": 30, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"develop_30": {"name": "Platinum Trophy", "description": "Upgrade 1 Orange Console Cartridge to Lvl 20.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_31": {"name": "Digital Collectibles", "description": "Upgrade 3 Orange Console Cartridges to Lvl 10.", "category": "develop", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_32": {"name": "World Dominator!", "description": "Upgrade 3 Orange Console Cartridges to Lvl 20.", "category": "develop", "quality": "high", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"develop_33": {"name": "Definition of Power I", "description": "Upgrade any character's Skill ×5.", "category": "develop", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_34": {"name": "Definition of Power II", "description": "Upgrade any character's Skill ×20.", "category": "develop", "quality": "high", "target": 20, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"develop_35": {"name": "Definition of Power III", "description": "Upgrade any character's Skill ×50.", "category": "develop", "quality": "execellent", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"develop_36": {"name": "Final Turn I", "description": "Upgrade any character's Ultimate level ×5.", "category": "develop", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_37": {"name": "Final Turn II", "description": "Upgrade any character's Ultimate level ×20.", "category": "develop", "quality": "high", "target": 20, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"develop_38": {"name": "Final Turn III", "description": "Upgrade any character's Ultimate level ×50.", "category": "develop", "quality": "execellent", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"develop_39": {"name": "Consecutive Normal Punches I", "description": "Upgrade any character's Basic Attack ×5.", "category": "develop", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_4": {"name": "I Haven't Reached My Limit Yet", "description": "Raise Hunter Level to Lvl 40.", "category": "develop", "quality": "high", "target": 40, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"develop_40": {"name": "Consecutive Normal Punches II", "description": "Upgrade any character's Basic Attack ×20.", "category": "develop", "quality": "high", "target": 20, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"develop_41": {"name": "Consecutive Normal Punches III", "description": "Upgrade any character's Basic Attack ×50.", "category": "develop", "quality": "execellent", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"develop_42": {"name": "Invited Guest", "description": "Obtain a total of 5 characters.", "category": "develop", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_43": {"name": "Travel Companions", "description": "Obtain a total of 10 characters.", "category": "develop", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_44": {"name": "Careful Maintenance", "description": "Obtain 1 Arc.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_45": {"name": "Fine Collection", "description": "Obtain a total of 10 Arcs.", "category": "develop", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_46": {"name": "True Connoisseur", "description": "Obtain a total of 50 Arcs.", "category": "develop", "quality": "normal", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_47": {"name": "Glory Hunter", "description": "Obtain a total of 100 Arcs.", "category": "develop", "quality": "high", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"develop_48": {"name": "Coins Keep Rolling In", "description": "Obtain a total of 100,000 Beetle Coins.", "category": "develop", "quality": "normal", "target": 100000, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_49": {"name": "Big Spender", "description": "Obtain a total of 500,000 Beetle Coins.", "category": "develop", "quality": "normal", "target": 500000, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_5": {"name": "SSS-Class Anomaly Conqueror", "description": "Raise Hunter Level to Lvl 50.", "category": "develop", "quality": "execellent", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"develop_50": {"name": "Walking Collector", "description": "Obtain a total of 1,000,000 Beetle Coins.", "category": "develop", "quality": "normal", "target": 1000000, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_51": {"name": "Entomologist", "description": "Obtain a total of 10,000,000 Beetle Coins.", "category": "develop", "quality": "high", "target": 10000000, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"develop_52": {"name": "Luck and Strategy Combined", "description": "Make 10 rolls.", "category": "develop", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_53": {"name": "Dice Never Leave Your Hand", "description": "Make 100 rolls.", "category": "develop", "quality": "high", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"develop_54": {"name": "Fate in Your Hands a Thousand Times", "description": "Make 1,000 rolls.", "category": "develop", "quality": "execellent", "target": 1000, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"develop_6": {"name": "I Acknowledge You as the Strongest", "description": "Raise Hunter Level to Lvl 60.", "category": "develop", "quality": "execellent", "target": 60, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"develop_7": {"name": "One-Man Army I", "description": "Raise 1 character to Lvl 20.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_8": {"name": "One-Man Army II", "description": "Raise 1 character to Lvl 40.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"develop_9": {"name": "One-Man Army III", "description": "Raise 1 character to Lvl 60.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"explore_1": {"name": "Oraoraoraoraora!", "description": "Complete Anomaly Commission: Backstreet Boxer", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_10": {"name": "Where Did My Inspiration Go?!", "description": "Complete Anomaly Commission: Inspiration Terminator", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_11": {"name": "Red Light, Green Light", "description": "Complete Anomaly Commission: After School", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_12": {"name": "Rain's Lament", "description": "Complete Anomaly Commission: Prepare for Rain", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_13": {"name": "Pop Art", "description": "Complete Anomaly Commission: Lightspeed Tagger", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_14": {"name": "Cherry Blush Returns to Evening White", "description": "Complete Anomaly Commission: Crimson Hexblade", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_15": {"name": "It's starting to clear up now!", "description": "Complete Anomaly Commission: Dragon Gate Substitute", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_16": {"name": "Blackjack I", "description": "Open 1 \"21's Gift.\"", "category": "explore", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"explore_17": {"name": "Blackjack II", "description": "Open 10 \"21's Gifts.\"", "category": "explore", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"explore_18": {"name": "Blackjack III", "description": "Open 50 \"21's Gifts.\"", "category": "explore", "quality": "normal", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"explore_19": {"name": "33729 I", "description": "Unlock 1 \"ReroRero Phone Booth.\"", "category": "explore", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"explore_2": {"name": "Muda Muda Muda!", "description": "Complete Anomaly Commission: Rooftop Boxer", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_20": {"name": "33729 II", "description": "Unlock 5 \"ReroRero Phone Booths.\"", "category": "explore", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"explore_21": {"name": "33729 III", "description": "Unlock 10 \"ReroRero Phone Booths.\"", "category": "explore", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"explore_22": {"name": "Orichalcum Cage", "description": "Reach Divination Lvl 3.", "category": "explore", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"explore_23": {"name": "Mithiril Cage", "description": "Reach Divination Lvl 5.", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_24": {"name": "Adamant Cage", "description": "Reach Divination Lvl 10.", "category": "explore", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"explore_25": {"name": "Skyline I", "description": "Unlock a total of 1 Wertheimer Tower.", "category": "explore", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"explore_26": {"name": "Skyline II", "description": "Unlock a total of 5 Wertheimer Towers.", "category": "explore", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"explore_27": {"name": "Urban Life Starting from Zero", "description": "Log in for 1 day in total", "category": "explore", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"explore_28": {"name": "Day Seven", "description": "Log in for 7 days in total", "category": "explore", "quality": "normal", "target": 7, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"explore_29": {"name": "New Hethereau Resident", "description": "Log in for 30 days in total", "category": "explore", "quality": "high", "target": 30, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_3": {"name": "Silent Testimony", "description": "Complete Anomaly Commission: Case of Serial Explosion", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_30": {"name": "Tow Shadow", "description": "Complete Anomaly Commission: Invasion! Octopus Trailer!", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_31": {"name": "Ghost of Whitetail Ridge", "description": "Complete Anomaly Commission: Ghost of Whitetail Ridge", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_32": {"name": "Extreme Rally Fanatic", "description": "Complete Anomaly Commission: Ghost Boulevard Wild Rally", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_33": {"name": "Beep, boop boop! Beep boop beep!", "description": "Complete Anomaly Commission: Beat King", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_34": {"name": "Little Bear's Patchwork", "description": "Complete Anomaly Commission: Sad Bears Don't Cry", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_35": {"name": "Quickcharge Compatible", "description": "Complete Anomaly Commission: Power Saver", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_36": {"name": "Night of K.O.", "description": "Complete Anomaly Commission: Party Boxer", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_37": {"name": "ONE ON ONE!", "description": "Complete Anomaly Commission: Court Boxer", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_38": {"name": "Never Stop Leaping", "description": "Complete Anomaly Commission: Heaven's Forsaken", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_39": {"name": "Another round?", "description": "Complete Anomaly Commission: Wandering Puppets", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_4": {"name": "Snap! Don't Blink!", "description": "Complete Anomaly Commission: Brand Anniversary", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_40": {"name": "You Moved First!", "description": "Complete Anomaly Commission: Change Your Perspective?", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_41": {"name": "52 Hertz", "description": "Complete Anomaly Commission: Deep Blue Sorrow", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_42": {"name": "Reverse Mystery Box", "description": "Complete Anomaly Commission: Whose Leak Is This?", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_43": {"name": "What You See Is Real", "description": "Complete Anomaly Commission: This is not a painting", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_44": {"name": "New Colors", "description": "Complete Anomaly Commission: Plot Hole Repair Project", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_45": {"name": "Nightmare Begins", "description": "Reach an ending in Anomaly Commission: Nightmare-Bound", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_46": {"name": "Meow Meow Championship", "description": "Complete Anomaly Commission: Yarn Ball or Fons?", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_47": {"name": "Clear Weather?", "description": "Complete Anomaly Commission: Prayers for Clear Skies", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_48": {"name": "Let It Go", "description": "Complete Anomaly Commission: Clearing After Snowfall", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_49": {"name": "All Good Things Must Come to an End", "description": "Complete Anomaly Commission: Kindness Hard to Refuse", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_5": {"name": "Normal Summon!", "description": "Complete Anomaly Commission: C's Cube", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_50": {"name": "Into the Empty Nest", "description": "Complete Anomaly Commission: Call of the Lost", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_51": {"name": "Butterfly in a Jar", "description": "Complete Anomaly Commission: Let's Go See a Movie", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_54": {"name": "Awakening Moment", "description": "Complete Anomaly Commission: The Book of Post-Adolescence", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_55": {"name": "Cagebreaker", "description": "Complete the Anomaly Commission: Sunken Door", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_56": {"name": "Thus Spoke Sunya", "description": "Complete the Anomaly Commission: Fawn Alone", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_57": {"name": "Falling Petals", "description": "Complete the Anomaly Commission: Twin Blooms", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_58": {"name": "Lights Out", "description": "Complete the Anomaly Commission: Lantern Parade", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_59": {"name": "This Bear Will Lead!", "description": "Complete the Anomaly Commission: Storybook Skirmish", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_6": {"name": "Special Summon!", "description": "Complete Anomaly Commission: C's Tower", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_60": {"name": "Broken Refrain", "description": "Complete the Anomaly Commission: Finale: Da Capo", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_7": {"name": "Welcome!", "description": "Complete Anomaly Commission: Red Door", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_8": {"name": "KeepOut!!", "description": "Complete Anomaly Commission: Ride Assault", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"explore_9": {"name": "In books there lies…", "description": "Complete Anomaly Commission: Video Cemetery", "category": "explore", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_11": {"name": "Kiroumaru's Second Caretaker", "description": "Reach Sakiri Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_12": {"name": "Eternal Home", "description": "Reach Sakiri Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"friendship_14": {"name": "Friendly Sparring", "description": "Reach Daffodill Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_15": {"name": "As Far as the Eye Can See", "description": "Reach Daffodill Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"friendship_17": {"name": "Personal Encyclopedia", "description": "Reach Edgar Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_18": {"name": "Clear Answer", "description": "Reach Edgar Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"friendship_2": {"name": "The Combo Called Mint Chocolate", "description": "Reach Mint Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_23": {"name": "Keep It Up, Key Player!", "description": "Reach Nanally Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_24": {"name": "Coluccis' Irreplaceable Right Hand", "description": "Reach Nanally Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"friendship_3": {"name": "The Wind That Lingers for You", "description": "Reach Mint Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"friendship_32": {"name": "Me?", "description": "Give Mint gifts ×3.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"friendship_33": {"name": "All for me?", "description": "Give Mint gifts ×10.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_34": {"name": "Best Buds", "description": "Give Baicang gifts ×3.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"friendship_35": {"name": "\"Such Generosity!\"", "description": "Give Baicang gifts ×10.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_36": {"name": "Sunya Care Guide", "description": "Give Adler gifts ×3.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"friendship_37": {"name": "Enlightenment Will Remember…", "description": "Give Adler gifts ×10.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_38": {"name": "Beware of Hungry Ghosts", "description": "Give Sakiri gifts ×3.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"friendship_39": {"name": "Kiroumaru! No Eating!", "description": "Give Sakiri gifts ×10.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_40": {"name": "Grateful for Your Care", "description": "Give Daffodill gifts ×3.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"friendship_41": {"name": "Sincere Intentions", "description": "Give Daffodill gifts ×10.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_42": {"name": "Will Treasure This…", "description": "Give Edgar gifts ×3.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"friendship_43": {"name": "Treasured Memories", "description": "Give Edgar gifts ×10.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_46": {"name": "No B-Bribes Allowed!", "description": "Give Nanally gifts ×3.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"friendship_47": {"name": "Gladly Accepted!", "description": "Give Nanally gifts ×10.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_5": {"name": "Slacking Buddy", "description": "Reach Baicang Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_54": {"name": "Making Connections", "description": "Reach Affection Lvl 5 with 5 characters total.", "category": "social", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"friendship_55": {"name": "Social Butterfly", "description": "Reach Affection Lvl 5 with 10 characters total.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_56": {"name": "Affection Collector", "description": "Reach Affection Lvl 10 with 5 characters total.", "category": "social", "quality": "execellent", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"friendship_58": {"name": "Reliable Partner", "description": "Reach Skia Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_59": {"name": "Part of the Routine", "description": "Reach Skia Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"friendship_6": {"name": "All Prayers for You", "description": "Reach Baicang Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"friendship_60": {"name": "Social Networking", "description": "Give Skia gifts ×3.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"friendship_61": {"name": "Too Kind…", "description": "Give Skia gifts ×10.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_68": {"name": "Plans with You", "description": "Reach Hathor Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_69": {"name": "Exclusive Channel", "description": "Reach Hathor Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"friendship_70": {"name": "Rationality Offline", "description": "Give Hathor gifts ×3.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"friendship_71": {"name": "Delivery of the \"Exception\"", "description": "Give Hathor gifts ×10.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_73": {"name": "Rose's Blessing", "description": "Reach Jiuyuan Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_74": {"name": "Nightingale's Favor", "description": "Reach Jiuyuan Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"friendship_75": {"name": "Do Not Feed the Riddler", "description": "Give Jiuyuan gifts ×3.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"friendship_76": {"name": "Rose of the Lost", "description": "Give Jiuyuan gifts ×10.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_78": {"name": "Wanna Be Friends?", "description": "Reach Haniel Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_79": {"name": "Sharing Earbuds", "description": "Reach Haniel Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"friendship_8": {"name": "The Second Pillar", "description": "Reach Adler Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_80": {"name": "Does this count as free?", "description": "Give Haniel gifts ×3.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"friendship_81": {"name": "President of the Paranormal Studies Support Club", "description": "Give Haniel gifts ×10.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_88": {"name": "Within Paradise", "description": "Reach Fadia Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_89": {"name": "Loving Gaze", "description": "Reach Fadia Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"friendship_9": {"name": "Highest Priority Order", "description": "Reach Adler Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"friendship_90": {"name": "Beware of Addiction", "description": "Give Fadia gifts ×3.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"friendship_91": {"name": "Enjoy a Little More", "description": "Give Fadia gifts ×10.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_92": {"name": "Rice Ball Partner", "description": "Reach Chiz Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_93": {"name": "Where My Heart Finds Peace", "description": "Reach Chiz Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"friendship_94": {"name": "Exclusive Sales Rep", "description": "Give Chiz gifts ×3.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"friendship_95": {"name": "More Precious Than Fons", "description": "Give Chiz gifts ×10.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_96": {"name": "Bond & Harmony", "description": "Reach Aurelia Affection Lvl 5.", "category": "social", "quality": "high", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"friendship_97": {"name": "Dearest Friend", "description": "Reach Aurelia Affection Lvl 10.", "category": "social", "quality": "execellent", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"friendship_98": {"name": "In This Moment", "description": "Gift Aurelia 3 times.", "category": "social", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"friendship_99": {"name": "Spring and Sea", "description": "Gift Aurelia 10 times.", "category": "social", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"interest_1": {"name": "Water Soluble", "description": "Collapse in water due to stamina depletion.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"interest_10": {"name": "Fluffy Addiction", "description": "Pet small animals ×10.", "category": "interest", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"interest_11": {"name": "Zootopia", "description": "Pet small animals ×50.", "category": "interest", "quality": "high", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"interest_12": {"name": "Just One More Pet… Please!", "description": "Pet small animals ×100.", "category": "interest", "quality": "execellent", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"interest_13": {"name": "Witness Journal", "description": "Complete the \"Photography\" series of easter egg quests.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"interest_14": {"name": "Non-Intrusive Documentation", "description": "Complete the \"Bird Enthusiast Guide\" easter egg quest.", "category": "interest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"interest_15": {"name": "The Dart's Fantastic Journey", "description": "Complete the \"Find the Dart\" easter egg quest.", "category": "interest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"interest_16": {"name": "A Pack of Spicy Sticks Promise", "description": "Complete the \"Little Boy at the Vending Machine\" easter egg quest.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"interest_17": {"name": "Fake Car Owner Chronicles", "description": "Complete the \"Car Enthusiast\" easter egg quest.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"interest_18": {"name": "Photo Op with Anomalies", "description": "Complete the \"Anomaly Photographer\" series easter egg quests.", "category": "interest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"interest_19": {"name": "World Changer", "description": "Complete the \"The Look of Eclipse\" series easter egg quests.", "category": "interest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"interest_2": {"name": "Leap of Faith", "description": "Fall from the wrong height.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"interest_21": {"name": "Broken Machine's Generosity", "description": "Complete the \"Broken Vending Machine\" easter egg quest.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"interest_22": {"name": "Pets' Secret Cafeteria", "description": "Complete the \"Pet Shop Empty Bowl\" easter egg quest.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"interest_23": {"name": "Honest Body, Lying Lips", "description": "Complete the \"Radio Listener\" easter egg quest.", "category": "interest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"interest_24": {"name": "Vehicle Destroyer", "description": "Complete the \"Vehicle Brothers\" easter egg quest.", "category": "interest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"interest_25": {"name": "Gangster Dreams", "description": "Complete the \"The Great Coluccis\" easter egg quest.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"interest_26": {"name": "Next Time for Sure!", "description": "Complete the \"Deadline Pressure\" easter egg quest.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"interest_27": {"name": "The Devoured Melody", "description": "Complete the \"Campgrounds - Careless Tourist\" easter egg quest.", "category": "interest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"interest_28": {"name": "Multi-Location Check-in Expert", "description": "Complete the \"Other World Tourist\" easter egg quest.", "category": "interest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"interest_29": {"name": "The Other Side of the Legend", "description": "Complete the \"Nanally Anomaly Rumors\" easter egg quest.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"interest_3": {"name": "Fate's Calling", "description": "Perform divination 1 time.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"interest_30": {"name": "Omnic Crisis…?", "description": "Complete the \"Robot Command Conflict\" easter egg quest.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"interest_31": {"name": "Danzaburou Will Never Be Enslaved!", "description": "Complete the \"Danzaburou\" series easter egg quests.", "category": "interest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"interest_32": {"name": "Wind Walker", "description": "Experience a weather change for the first time.", "category": "interest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"interest_33": {"name": "The Whisker's Net Catches All", "description": "After summoning Officer Whisker using the Arc Hethereau's Keeper, get punished by the Whisker.", "category": "interest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"interest_34": {"name": "Stars Falling Like Rain", "description": "Witness a meteor shower.", "category": "interest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"interest_35": {"name": "365 Miles", "description": "Unlock the ReroRero Phone Booth on top of the Midas Arc Workshop.", "category": "interest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"interest_36": {"name": "The Sound of Music", "description": "Play the piano.", "category": "interest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"interest_37": {"name": "It's 100!", "description": "Roll 100 with dice.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"interest_38": {"name": "I'll Be Back", "description": "Restart at the bonfire 10 times after defeat.", "category": "interest", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"interest_39": {"name": "Mind the Hot Chocolate", "description": "Receive burn damage 33 times.", "category": "interest", "quality": "normal", "target": 33, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"interest_4": {"name": "Worldline Convergence", "description": "Perform 10 Divinations.", "category": "interest", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"interest_40": {"name": "Hold Your Breath", "description": "Take Mold damage 33 times.", "category": "interest", "quality": "normal", "target": 33, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"interest_41": {"name": "Nah, I'd Win", "description": "Fail \"Test Your Luck\" at the Lucky Fuzzy 33 times.", "category": "interest", "quality": "normal", "target": 33, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"interest_42": {"name": "Pinky Promise", "description": "Gaze at the night sky with Linko at Hethereau Skytower, and make a pinky promise beneath the stars.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"interest_5": {"name": "Omens of Impending Change", "description": "Perform 50 Divinations.", "category": "interest", "quality": "execellent", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"interest_6": {"name": "Mysterious Big Eater", "description": "Fill up the Hunger Bar ×5.", "category": "interest", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"interest_7": {"name": "Pac-man!", "description": "Fill up the Hunger Bar ×20.", "category": "interest", "quality": "high", "target": 20, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"interest_8": {"name": "The Art of Inhaling Food", "description": "Fill up the Hunger Bar ×50.", "category": "interest", "quality": "execellent", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"interest_9": {"name": "Can I Pet It!", "description": "Pet small animals 1 time.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_1": {"name": "Trying to Breathe in the Concrete Jungle", "description": "Upgrade Life Skills once.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_100": {"name": "Silent Fall", "description": "Encounter Daffodill training in Miguel District.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_101": {"name": "Hmm! Hmm! Observation Log", "description": "Encounter Edgar at Nacupeda Park, studying and copying Nacupeda.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_102": {"name": "Guardian of Order", "description": "Encounter Skia at the street-side bulletin board, checking the announcements.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_103": {"name": "Thoughtful Butler", "description": "Encounter Adler shopping for groceries at Origami Market.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_104": {"name": "A Glass of Water: Half Full", "description": "Obtain 1 \"Glass of Water\" at the auction.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_105": {"name": "Our Path", "description": "Obtain 1 \"Dream Path\" at the auction.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_106": {"name": "Who Says True Love Weighs You Down?", "description": "Obtain 1 \"Butterflies to Flowers\" at the auction.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_107": {"name": "The Kapé", "description": "Obtain 1 \"Celebrations to: Kapé\" at the auction.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_108": {"name": "Future Audience", "description": "Obtain 1 \"Azure Strings\" at the auction.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_109": {"name": "Because I Love Soccer", "description": "Obtain 1 \"Golden Flying Ball\" at the auction.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_13": {"name": "Business Power, Full Throttle!", "description": "Accumulate 5 stars in Active Tycoon Gameplay.", "category": "city", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_14": {"name": "Chosen by the Stars", "description": "Accumulate 10 stars in Active Tycoon Gameplay.", "category": "city", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Life_15": {"name": "Golden Age", "description": "Accumulate 20 stars in Active Tycoon Gameplay.", "category": "city", "quality": "execellent", "target": 20, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"Life_16": {"name": "Full of Energy!", "description": "Spend a total of 50 City Stamina.", "category": "city", "quality": "normal", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_17": {"name": "Burn Your Cosmos", "description": "Spend a total of 500 City Stamina.", "category": "city", "quality": "high", "target": 500, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Life_18": {"name": "High-End Business War", "description": "Spend a total of 2,000 City Stamina.", "category": "city", "quality": "execellent", "target": 2000, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"Life_19": {"name": "Mysterious Touch", "description": "Upgrade Anomaly Furniture once.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_2": {"name": "Player One", "description": "Upgrade Life Skills ×10.", "category": "city", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Life_20": {"name": "Connecting Hearts with Furniture", "description": "Upgrade Anomaly Furniture ×10.", "category": "city", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Life_21": {"name": "Advanced Revelations", "description": "Upgrade Anomaly Furniture ×50.", "category": "city", "quality": "execellent", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"Life_22": {"name": "Going Once, Going Twice", "description": "Participate in 1 auction.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_23": {"name": "Bidding Fanatic", "description": "Participate in 10 auctions.", "category": "city", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Life_24": {"name": "Everything Has a Price", "description": "Participate in 100 auctions.", "category": "city", "quality": "execellent", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"Life_3": {"name": "Max Out City Life!", "description": "Upgrade Life Skills ×50.", "category": "city", "quality": "execellent", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"Life_31": {"name": "Unit-01", "description": "Obtain 1 vehicle.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_32": {"name": "Mobile Trio", "description": "Obtain 3 vehicles.", "category": "city", "quality": "high", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Life_33": {"name": "The Seven", "description": "Obtain 7 vehicles.", "category": "city", "quality": "execellent", "target": 7, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"Life_4": {"name": "Want Some Beans?", "description": "Unlock 1 coffee shop.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_5": {"name": "Coffee Connoisseur in Training", "description": "Unlock 3 coffee shops.", "category": "city", "quality": "high", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Life_6": {"name": "Regional Flavor Master", "description": "Unlock 5 coffee shops.", "category": "city", "quality": "execellent", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"Life_67": {"name": "Real Estate Qualified", "description": "Acquire your first house.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_68": {"name": "Dual Residence Plan", "description": "Obtain a total of 2 houses.", "category": "city", "quality": "high", "target": 2, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Life_69": {"name": "Life Winner", "description": "Obtain a total of 4 houses.", "category": "city", "quality": "execellent", "target": 4, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"Life_70": {"name": "More Than Four Walls", "description": "Acquire furniture for the first time.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_71": {"name": "Storage Enthusiast", "description": "Obtain a total of 50 furniture.", "category": "city", "quality": "high", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Life_72": {"name": "The Room Comes Alive", "description": "Obtain a total of 100 furniture.", "category": "city", "quality": "execellent", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"Life_76": {"name": "Every Move is Magic", "description": "Win a prize from the claw machine once.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_77": {"name": "Plushie Maniac", "description": "Successfully grab prizes from the claw machine ×10.", "category": "city", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Life_78": {"name": "Time to Teach!", "description": "Successfully grab prizes from the claw machine ×100.", "category": "city", "quality": "high", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Life_79": {"name": "I Found The Fons by the Roadside", "description": "Turn in 10 Lost Items to the Security Office.", "category": "city", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_80": {"name": "Honest Finder", "description": "Turn in 50 Lost Items to the Security Office.", "category": "city", "quality": "high", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Life_81": {"name": "The Beacon of Humanity", "description": "Turn in 100 Lost Items to the Security Office.", "category": "city", "quality": "high", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Life_82": {"name": "Brave Passerby", "description": "Stop 10 illegal city activities.", "category": "city", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_83": {"name": "Whisker's Buddy", "description": "Stop 50 illegal city activities.", "category": "city", "quality": "high", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Life_84": {"name": "Embodiment of Justice", "description": "Stop 100 illegal city activities.", "category": "city", "quality": "execellent", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"Life_88": {"name": "\"At First, I Refused…\"", "description": "Find Mint learning Hair Care at the Pet Store.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_89": {"name": "Direct Hire", "description": "Find Nanally announcing the Coluccis' recruitment everywhere.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_90": {"name": "Bountiful Dreams", "description": "Find Nanally taking a nap.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_91": {"name": "Jelly Belly", "description": "Find Lacrimosa pondering how to carry large amounts of Tomato Jelly.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_92": {"name": "Once Upon a Dream", "description": "Find Lacrimosa fast asleep.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_93": {"name": "Expert Meets Expert", "description": "Find Hotori negotiating prices with the owner at Good Joe.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_94": {"name": "The Queen Wears Landric", "description": "Find Fadia returning from a shopping spree.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_95": {"name": "Speed Bows Down", "description": "Find Jiuyuan surrounded by Regalia service consultants.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_96": {"name": "New Energy Transport", "description": "Find Sakiri riding Kiroumaru around town.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_97": {"name": "Slacking Off Feels So Good", "description": "Find Baicang at Bagel eating his own melon.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_98": {"name": "Path of Balanced Efficiency", "description": "Encounter Hathor at the gas station after she just refueled.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Life_99": {"name": "You too?", "description": "Encounter Haniel cheering in the crowd at a Kokoro Rider show.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Play_1": {"name": "Mhm!", "description": "Receive Nacupeda's blessing.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Play_10": {"name": "Infinite Acceleration", "description": "Defeat the Meridian Racing Team.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Play_11": {"name": "Wind Barrier", "description": "Defeat the Tempestas Racing Team.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Play_12": {"name": "Uncrowned King", "description": "Defeat the Uncrowned King Racing Team.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Play_13": {"name": "Lightning Start", "description": "Win your first Racing PVP battle.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Play_14": {"name": "Speed Demon", "description": "Win 10 Racing PVP battles.", "category": "play", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Play_15": {"name": "King of Speed", "description": "Win 50 Racing PVP battles.", "category": "play", "quality": "execellent", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"Play_17": {"name": "Block Vanishing Act", "description": "Place 1st in Tetrominoes.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Play_18": {"name": "Space Management Master", "description": "Place 1st in Tetrominoes ×10.", "category": "play", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Play_19": {"name": "Less Is More", "description": "Place 1st in Tetrominoes ×30.", "category": "play", "quality": "execellent", "target": 30, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"Play_2": {"name": "Mhm! Mhm!", "description": "Receive 10 Nacupeda's blessings.", "category": "play", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Play_27": {"name": "First Pot of Gold", "description": "Collect 10,000 Fons in a single round of Pink Paws Heist.", "category": "play", "quality": "normal", "target": 10000, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Play_28": {"name": "Fons's Warudo", "description": "Collect 50,000 Fons in a single round of Pink Paws Heist.", "category": "play", "quality": "high", "target": 50000, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Play_29": {"name": "Ladon Approved", "description": "Collect 100,000 Fons in a single round of Pink Paws Heist.", "category": "play", "quality": "high", "target": 100000, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Play_3": {"name": "Mhm! Mhm! Mhm!", "description": "Receive 30 Nacupeda's blessings.", "category": "play", "quality": "high", "target": 30, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Play_30": {"name": "First Passenger", "description": "Drive a taxi to pick up 5 passengers.", "category": "play", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}, {"item_id": "Vehicle020_skin_1_Painting", "amount": 1}]},
"Play_31": {"name": "Swift at Your Service", "description": "Drive a taxi to pick up 10 passengers.", "category": "play", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}, {"item_id": "Vehicle020_skin_1_CeilingLight", "amount": 1}]},
"Play_32": {"name": "Have a Safe Trip", "description": "Drive a taxi to pick up 30 passengers.", "category": "play", "quality": "high", "target": 30, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Play_33": {"name": "First Order", "description": "Complete City Commission ×1.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Play_34": {"name": "Skilled Courier", "description": "Complete City Commission ×10.", "category": "play", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Play_35": {"name": "City Neighbors", "description": "Complete City Commission ×30.", "category": "play", "quality": "high", "target": 30, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Play_36": {"name": "Hethereau Shores", "description": "Catch a total of 50 fish in Sea Angler.", "category": "play", "quality": "normal", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Play_37": {"name": "Every Angler's Dream", "description": "Catch a total of 100 fish in Sea Angler.", "category": "play", "quality": "high", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Play_38": {"name": "Fishing God", "description": "Catch a total of 200 fish in Sea Angler.", "category": "play", "quality": "high", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Play_39": {"name": "Breakout Together", "description": "Use a special spot in the scene to leave the Detention Facility and successfully escape.", "category": "city", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Play_4": {"name": "Mhm…?", "description": "Receive Nacupeda's punishment for the first time.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Play_40": {"name": "Dawn in Detention", "description": "Survive the first round of detention shenanigans and discover one surprise… like finding your socks still match.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}, {"item_id": "PrisonCoin", "amount": 1}]},
"Play_41": {"name": "Detention Strikes Back", "description": "Complete the next round of detention chaos and uncover two surprises—bonus points if nobody notices you napping.", "category": "city", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}, {"item_id": "PrisonCoin", "amount": 1}]},
"Play_42": {"name": "Return of the Dawn", "description": "Master all the wacky encounters in detention and reap three unexpected rewards—because yes, even detention has its plot twists.", "category": "city", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}, {"item_id": "Fons", "amount": 10000}]},
"Play_43": {"name": "Fight Club", "description": "Participate in Fight Club for the first time.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Play_44": {"name": "Hethereau Brawler King", "description": "Win the championship in Fight Club for the first time.", "category": "play", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}, {"item_id": "Title_PrisonFight_001", "amount": 1}]},
"Play_45": {"name": "Rookie OP", "description": "Earn a total of 10,000 Praise Jams on Bagel.", "category": "play", "quality": "normal", "target": 10000, "rewards": [{"item_id": "Annulith", "amount": 5}, {"item_id": "Title_Bagel_001", "amount": 1}]},
"Play_46": {"name": "Bagel Celebrity", "description": "Earn a total of 50,000 Praise Jams on Bagel.", "category": "play", "quality": "high", "target": 50000, "rewards": [{"item_id": "Annulith", "amount": 10}, {"item_id": "Title_Bagel_002", "amount": 1}]},
"Play_47": {"name": "Influential User", "description": "Earn a total of 100,000 Praise Jams on Bagel.", "category": "play", "quality": "execellent", "target": 100000, "rewards": [{"item_id": "Annulith", "amount": 20}, {"item_id": "Title_Bagel_003", "amount": 1}]},
"Play_48": {"name": "Stormwall", "description": "Defeat Lil' Nado on Easy Sparring.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Play_49": {"name": "Stormbreaker", "description": "Defeat Lil' Nado on Serious Match.", "category": "play", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Play_5": {"name": "Mhm? Mhm?", "description": "Receive 10 Nacupeda's punishments.", "category": "play", "quality": "high", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Play_50": {"name": "I Am the Storm", "description": "Defeat Lil' Nado on Final Bout.", "category": "play", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"Play_51": {"name": "Chest Master", "description": "Open 100 chests.", "category": "play", "quality": "high", "target": 100, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Play_54": {"name": "Appraise It!", "description": "Obtain 7 pieces of equipment with an appraisal value of 33.", "category": "play", "quality": "high", "target": 7, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Play_55": {"name": "Watch Your Step!", "description": "In the Chocolate Volcano, defeat 3 monsters using lava.", "category": "play", "quality": "high", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Play_56": {"name": "Continental Breakfast", "description": "Clear Red Mist Invasion, Hop Once: Cake Ace, Hop Twice: Cake Ace, Defend Against Bubbles, Leave None Standing, and Baaastille for the first time.", "category": "play", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Play_57": {"name": "I Choose You!", "description": "Hire Mercenary Fuzzy once.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Play_58": {"name": "Miracle Spike", "description": "Land one perfect spike once in Volley Star.", "category": "play", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Play_59": {"name": "No Continues", "description": "Clear all the stages in Volley Star.", "category": "play", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Play_6": {"name": "Mhm? Mhm? Mhm?!", "description": "Receive 30 Nacupeda's punishments.", "category": "play", "quality": "high", "target": 30, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Play_60": {"name": "Crowned Champions!", "description": "Win the championship in Volley Star's Championship mode.", "category": "play", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Play_61": {"name": "Court Conqueror", "description": "Reach No. 1 in the Volley Star World Rankings.", "category": "play", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Play_62": {"name": "Global Elite", "description": "Place in the top 10 of the Volley Star World Rankings.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Play_63": {"name": "Going Pro", "description": "Recruit 3 players in Volley Star.", "category": "play", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Play_64": {"name": "Full Roster", "description": "Recruit 7 players in Volley Star.", "category": "play", "quality": "high", "target": 7, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Play_65": {"name": "Maxed Out", "description": "Max out 1 player's talent in Volley Star.", "category": "play", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"Play_7": {"name": "End of Fun", "description": "Defeat the Sunday Racing Team.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Play_8": {"name": "Wild Mini Cars", "description": "Defeat the Coastal Drift Racing Team.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Play_9": {"name": "Lightning Speed: Dead End", "description": "Defeat the Full Speed Ahead Racing Team.", "category": "play", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_001": {"name": "Ultimate Anomaly Hunter III", "description": "Upgrade 1 character to Lvl 60.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_002": {"name": "Ultimate Anomaly Hunter II", "description": "Upgrade 1 character to Lvl 40.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_003": {"name": "Ultimate Anomaly Hunter I", "description": "Upgrade 1 character to Lvl 20.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_004": {"name": "Brawl III", "description": "Upgrade 1 Arc to Lvl 60.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_005": {"name": "Brawl II", "description": "Upgrade 1 Arc to Lvl 40.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_006": {"name": "Brawl I", "description": "Upgrade 1 Arc to Lvl 20.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_007": {"name": "Classic Clear", "description": "Upgrade 1 Orange Console Module to Lvl 20.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_008": {"name": "OG Legend", "description": "Upgrade 1 Orange Console Cartridge to Lvl 10.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_009": {"name": "Hyperdimensional Awakening", "description": "Increase Hunter Level to Lvl 35.", "category": "develop", "quality": "normal", "target": 35, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_010": {"name": "Rising Hunter", "description": "Increase Hunter Level to Lvl 25.", "category": "develop", "quality": "normal", "target": 25, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_011": {"name": "Flip-Flopping Future", "description": "Perform 10 Divinations.", "category": "interest", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_012": {"name": "33729", "description": "Unlock 5 ReroRero Phone Booths.", "category": "develop", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_013": {"name": "Mhm? Mhm? Mhm?!", "description": "Receive 30 Nacupeda's Punishments.", "category": "play", "quality": "normal", "target": 30, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_014": {"name": "Mhm! Mhm! Mhm!", "description": "Receive 30 Nacupeda's Blessings.", "category": "play", "quality": "normal", "target": 30, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_015": {"name": "Homecoming Day", "description": "Obtain your first house.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_016": {"name": "First Ride", "description": "Obtain 1 vehicle.", "category": "city", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_017": {"name": "Speed Above All", "description": "Trigger Critical Dodge ×200.", "category": "fight", "quality": "normal", "target": 200, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_018": {"name": "Life-Sim", "description": "Upgrade Life Skills ×10.", "category": "develop", "quality": "normal", "target": 10, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_019": {"name": "Beetle Coin Researcher", "description": "Obtain a total of 1,000,000 Beetle Coins.", "category": "develop", "quality": "normal", "target": 1000000, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_020": {"name": "Hit Me!", "description": "Open 21's Gift ×50.", "category": "develop", "quality": "normal", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_021": {"name": "Let's Start a Band!", "description": "Complete the A Legendary(?) Band's First Page series quests.", "category": "quest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_022": {"name": "Alea Jacta Est", "description": "Complete the Unforeseen Yet Foretold series quests.", "category": "quest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_023": {"name": "Ultimate Racer", "description": "Obtain 7 vehicles.", "category": "city", "quality": "normal", "target": 7, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_024": {"name": "Neon Master", "description": "Upgrade Life Skills ×50.", "category": "develop", "quality": "normal", "target": 50, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_025": {"name": "Beetle Bounty", "description": "Obtain a total of 10,000,000 Beetle Coins.", "category": "develop", "quality": "normal", "target": 10000000, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_026": {"name": "Ultimate Anomaly Hunter IV", "description": "Upgrade 1 character to Lvl 80.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_027": {"name": "Brawl IV", "description": "Upgrade 1 Arc to Lvl 80.", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_028": {"name": "Limits of 8-bit", "description": "Upgrade 3 Orange Console Cartridges to Lvl 20.", "category": "develop", "quality": "normal", "target": 3, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_029": {"name": "It's Snowing!", "description": "Complete the A Retrograde Poem for You series quests.", "category": "quest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_030": {"name": "Free Pass", "description": "Unlock a total of 5 Wertheimer Towers.", "category": "explore", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_031": {"name": "Fate's Echo", "description": "Reach Divination Lvl 10.", "category": "interest", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_032": {"name": "Lone Signal", "description": "Complete Anomaly Commission: Deep Blue Sorrow", "category": "develop", "quality": "normal", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_033": {"name": "It said go for it!", "description": "Defeat enemies with Ultimate ×500.", "category": "fight", "quality": "normal", "target": 500, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_034": {"name": "Affection Master", "description": "Reach Affection Lvl 10 with 5 characters total.", "category": "social", "quality": "normal", "target": 5, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"Playstation_035": {"name": "Variation Movement", "description": "Cast Redirect Skill ×300.", "category": "fight", "quality": "normal", "target": 300, "rewards": [{"item_id": "Annulith", "amount": 5}]},
"quest_1": {"name": "Where Dreams Begin!", "description": "Complete the \"Unforeseen Yet Foretold\" series quests.", "category": "quest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"quest_10": {"name": "A Rainy Day in Hethereau", "description": "Complete the \"Weather Watch\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"quest_11": {"name": "\"Win Friends and Influence People\"", "description": "Complete the \"Seeing and Believing\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"quest_12": {"name": "The Wall Artist", "description": "Complete the \"Another Art on the Wall\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"quest_13": {"name": "Alice's Baking Wonderland", "description": "Complete the \"Sweet Cakes, Bitter Cakes…\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"quest_14": {"name": "Rules, Compliance, Containment", "description": "Complete the \"Deal? Deal!\" series quests.", "category": "quest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"quest_15": {"name": "Want to eat… Tomato Jelly", "description": "Complete the \"Tomato Jelly Rampage: Live Report!\" series quests.", "category": "quest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"quest_16": {"name": "Don't Stop Me Now!", "description": "Complete the \"Drift Away, Legend!\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"quest_17": {"name": "Beep! Citizen ID", "description": "Complete the \"New Anomaly Citizen ID!\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"quest_18": {"name": "The Last Bite is the Setting Sun", "description": "Complete the \"Let's Go, Delicious Investigators!\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"quest_19": {"name": "Yesterday Rose as Always", "description": "Complete the \"Books Are Like Old Friends\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"quest_2": {"name": "Photo Studio Fright Night", "description": "Complete the \"Good Business, Better Treasure\" series quests.", "category": "quest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"quest_20": {"name": "It's! Snowing!", "description": "Complete the \"A Retrograde Poem for You\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"quest_21": {"name": "A Letter from the Past", "description": "Complete the \"Miles Apart\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"quest_22": {"name": "Heroes Not Selling, Help Us Out", "description": "Complete the \"Heroes Don't Always Arrive in Moments of Glory\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"quest_24": {"name": "Life Is One Long Journey", "description": "Complete the \"Goodbye, Yellow Flag\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"quest_25": {"name": "The Train Keeps Moving", "description": "Complete the A Legendary(?) Band's First Page series quest.", "category": "quest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"quest_29": {"name": "Debts and Dues", "description": "Complete Questline: The Scarlet Letter Analysis.", "category": "quest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"quest_3": {"name": "The Moon and Six Fons", "description": "Complete the \"Pawn Moonlight\" series quests.", "category": "quest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"quest_30": {"name": "Detention Blues", "description": "Complete the \"A Breeze Through the Cage\" questline.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"quest_31": {"name": "Dreams Adrift", "description": "Complete the \"Dreamwalk Corridor\" questline.", "category": "quest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"quest_32": {"name": "Dragon Hunter", "description": "Clear \"999 Nights\" on Normal difficulty.", "category": "quest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}, {"item_id": "Title_FinalTower_001", "amount": 1}]},
"quest_33": {"name": "Fuzzy Hero", "description": "Clear \"999 Nights\" on Hard difficulty.", "category": "play", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}, {"item_id": "Title_FinalTower_002", "amount": 1}]},
"quest_34": {"name": "Legend of Warren", "description": "Clear \"999 Nights\" on Nightmare difficulty.", "category": "play", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}, {"item_id": "Title_FinalTower_003", "amount": 1}]},
"quest_36": {"name": "Adventure Awaits", "description": "Leave the Fuzzy Village.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"quest_37": {"name": "One Fuzzy Move Ahead", "description": "Complete Quest: The Fuzzy's Gambit.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"quest_38": {"name": "Gone Bad", "description": "Complete Quest: Fuzzy Club.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"quest_39": {"name": "Nothing Really Happened", "description": "Complete Quest: All Quiet in the Future.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"quest_4": {"name": "Taygedo the Rizzler", "description": "Complete the \"Love that Begins with Lies\" series quests.", "category": "quest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"quest_40": {"name": "Sword or Guitar?", "description": "Complete Quest: Rex Fuzzendae.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"quest_41": {"name": "Fuzzy's Precious! Grrgl!", "description": "Complete Quest: Gone Fuzzy.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"quest_42": {"name": "Hardworking Fuzzy", "description": "Complete Quest: The Hero Is a Part-Timer.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"quest_43": {"name": "Clear Horizon", "description": "Complete the Fogden Game series quests.\n", "category": "quest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"quest_5": {"name": "Just Like Autumn", "description": "Complete the \"Where Words Sprout, Fall Unfolds\" series quests.", "category": "quest", "quality": "execellent", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 20}]},
"quest_6": {"name": "300% Sync Rate!", "description": "Complete the \"We! Are! One!\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"quest_7": {"name": "Waste Sorting", "description": "Complete the \"Fluffed Away?!\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"quest_8": {"name": "Guess Who I Am?", "description": "Complete the \"Transform! Huhehaha!\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]},
"quest_9": {"name": "Small Size, Big Appetite!", "description": "Complete \"The Records Collector\" series quests.", "category": "quest", "quality": "high", "target": 1, "rewards": [{"item_id": "Annulith", "amount": 10}]}
}

View file

@ -10,8 +10,10 @@
"fork_Butterfly": {"name": "Reality Refuge", "rank": "S"},
"fork_Castle": {"name": "Call of the Twisted City", "rank": "A"},
"fork_Crowbar": {"name": "Time Bandit", "rank": "A"},
"fork_DemonBlade": {"name": "Ravenous Blade", "rank": "S"},
"fork_Door": {"name": "The Wrong Gate", "rank": "S"},
"fork_dustbin": {"name": "Dangerous Game", "rank": "B"},
"fork_GoldRecord": {"name": "Voice of the Voyager", "rank": "S"},
"fork_GoldWool": {"name": "What's Desired", "rank": "S"},
"fork_jiaojuan": {"name": "Shiny Days", "rank": "A"},
"fork_jingmotingyuan": {"name": "Camellia Society", "rank": "S"},

View file

@ -9,6 +9,7 @@
"1023": {"name": "Baicang", "rank": "S"},
"1025": {"name": "Hathor", "rank": "S"},
"1033": {"name": "Adler", "rank": "A"},
"1036": {"name": "Zankou", "rank": "S"},
"1039": {"name": "Fadia", "rank": "S"},
"1046": {"name": "Zero", "rank": "S"},
"1051": {"name": "Zero", "rank": "S"},
@ -18,7 +19,9 @@
"1056": {"name": "Lacrimosa", "rank": "S"},
"1070": {"name": "Aurelia", "rank": "A"},
"1071": {"name": "Chaos", "rank": "S"},
"1072": {"name": "Linko", "rank": "S"},
"1073": {"name": "Chiz", "rank": "S"},
"1075": {"name": "Iroi", "rank": "S"},
"1076": {"name": "Shinku", "rank": "S"}
"1076": {"name": "Shinku", "rank": "S"},
"1091": {"name": "Lacrimosa", "rank": "S"}
}

View file

@ -1,40 +1,51 @@
{
"BussinessCard_MangHe_bsj": {"type": "cosmetic", "name": "Trackside Shadow", "rank": "S"},
"CharacterUpMaterial_lv3": {"type": "item", "name": "Elite Hunter Guide", "rank": "A"},
"CityAbility_UpMaterial": {"type": "item", "name": "Dreamless Seed", "rank": "B"},
"Dice_ticket_01": {"type": "item", "name": "Warp Piece", "rank": "S"},
"Dice_ticket_02": {"type": "item", "name": "Lost Piece", "rank": "A"},
"Dicelimite": {"type": "item", "name": "Solid Dice", "rank": "S"},
"DiceNormal": {"type": "item", "name": "Fabricated Dice", "rank": "S"},
"Fashion_1051_rpg_level3_Base": {"type": "cosmetic", "name": "Glory Days", "rank": "S"},
"Fashion_1051_rpg_level3_Dye1": {"type": "cosmetic", "name": "Glory Days - Braveheart", "rank": "S"},
"Fashion_1051_rpg_level3_Dye2": {"type": "cosmetic", "name": "Glory Days - Starfall", "rank": "S"},
"Fashion_1075_rpg_level2_Base": {"type": "cosmetic", "name": "Archmage", "rank": "S"},
"Fashion_character_1004_01": {"type": "cosmetic", "name": "Gilded Rhapsody", "rank": "S"},
"Fashion_character_1010": {"type": "cosmetic", "name": "Phoenix Kick", "rank": "S"},
"Fashion_character_1010_swimsuit": {"type": "cosmetic", "name": "Sunny Surf", "rank": "S"},
"Fashion_character_1019_swimsuit": {"type": "cosmetic", "name": "Saltwater Taffy", "rank": "S"},
"Fashion_character_1036_03": {"type": "cosmetic", "name": "Autumn Vignette", "rank": "S"},
"Fashion_character_1051_07": {"type": "cosmetic", "name": "Perfect Partners", "rank": "S"},
"Fashion_character_1051_swimsuit": {"type": "cosmetic", "name": "Cloudless Summer", "rank": "S"},
"Fashion_character_1052_01": {"type": "cosmetic", "name": "Priceless Orchid", "rank": "S"},
"Fashion_character_1071_01": {"type": "cosmetic", "name": "Clear Skies", "rank": "S"},
"Fashion_character_1075_01": {"type": "cosmetic", "name": "Summer Dream", "rank": "S"},
"Fashion_character_1072_01": {"type": "cosmetic", "name": "Surfing Among Stars", "rank": "S"},
"Fashion_character_1076_01": {"type": "cosmetic", "name": "Student of Terrasea", "rank": "S"},
"Fashion_Glide_1004": {"type": "cosmetic", "name": "Tomato Duo", "rank": "A"},
"Fashion_Glide_1010": {"type": "cosmetic", "name": "Underboss-of-the-Underboss", "rank": "A"},
"Fashion_Glide_1036": {"type": "cosmetic", "name": "Scarlet Sash", "rank": "A"},
"Fashion_Glide_1052": {"type": "cosmetic", "name": "Orchid Breeze", "rank": "A"},
"Fashion_Glide_1071": {"type": "cosmetic", "name": "Skyrider", "rank": "A"},
"Fashion_glide_1075": {"type": "cosmetic", "name": "Skyrider", "rank": "A"},
"Fashion_Glide_1072": {"type": "cosmetic", "name": "Sheepcopter", "rank": "A"},
"Fashion_Glide_1075": {"type": "cosmetic", "name": "Sheepcopter", "rank": "A"},
"Fashion_Glide_1076": {"type": "cosmetic", "name": "Overcast Canopy", "rank": "A"},
"Fashion_vehicle_1004_V021": {"type": "cosmetic", "name": "Tomato Cruise - Livery", "rank": "S"},
"Fashion_vehicle_1010_V008": {"type": "cosmetic", "name": "Tiger Incoming! - Livery", "rank": "S"},
"Fashion_vehicle_1036_V043": {"type": "cosmetic", "name": "Sandy Mirage - Livery", "rank": "S"},
"Fashion_vehicle_1052_V024": {"type": "cosmetic", "name": "Autumn Haze - Livery", "rank": "S"},
"Fashion_vehicle_1071_V010": {"type": "cosmetic", "name": "Hound Blitz - Livery", "rank": "S"},
"Fashion_vehicle_1075_V006": {"type": "cosmetic", "name": "Hidden Dragon - Livery", "rank": "S"},
"Fashion_vehicle_1075_V006": {"type": "cosmetic", "name": "Three's Company - Livery", "rank": "S"},
"Fashion_vehicle_1076_V024": {"type": "cosmetic", "name": "Hidden Dragon - Livery", "rank": "S"},
"Fashion_vehicleSkin_1052_V016": {"type": "cosmetic", "name": "Golden Breeze - Livery", "rank": "S"},
"vehicle039": {"type": "item", "name": "Draco", "rank": "S"},
"Fashion_1051_rpg_level3_Dye1": {"type": "cosmetic", "name": "Glory Days - Braveheart", "rank": "S"},
"Fashion_1051_rpg_level3_Dye2": {"type": "cosmetic", "name": "Glory Days - Starfall", "rank": "S"},
"Fashion_1051_rpg_level3_Base": {"type": "cosmetic", "name": "Glory Days", "rank": "S"},
"SpecialGift_ticket": {"type": "item", "name": "Floe Cinema Ticket", "rank": "A"},
"Fons": {"type": "item", "name": "Fons", "rank": "A"},
"CharacterUpMaterial_lv3": {"type": "item", "name": "Elite Hunter Guide", "rank": "A"},
"Gold": {"type": "item", "name": "Beetle Coin", "rank": "B"},
"CityAbility_UpMaterial": {"type": "item", "name": "Dreamless Seed", "rank": "B"},
"SpecialGift_letter": {"type": "item", "name": "A Handwritten Letter", "rank": "S"},
"Frame_MangHe_shinku": {"type": "cosmetic", "name": "Ember Trail", "rank": "S"},
"Fashion_character_1051_07": {"type": "cosmetic", "name": "Perfect Partners", "rank": "S"},
"vehicle026": {"type": "item", "name": "Porsche 918 Spyder", "rank": "S"},
"Frame_MangHe_bsj": {"type": "cosmetic", "name": "Heartbeat Moment", "rank": "S"},
"BussinessCard_MangHe_bsj": {"type": "cosmetic", "name": "Trackside Shadow", "rank": "S"}
"Frame_MangHe_shinku": {"type": "cosmetic", "name": "Ember Trail", "rank": "S"},
"Frame_MangHe_summer_01": {"type": "cosmetic", "name": "Endless Summer", "rank": "S"},
"Gold": {"type": "item", "name": "Beetle Coin", "rank": "B"},
"SpecialGift_letter": {"type": "item", "name": "A Handwritten Letter", "rank": "S"},
"SpecialGift_ticket": {"type": "item", "name": "Floe Cinema Ticket", "rank": "A"},
"vehicle026": {"type": "item", "name": "Porsche 918 Spyder", "rank": "S"},
"vehicle039": {"type": "item", "name": "Draco", "rank": "S"},
"Vehicle044": {"type": "item", "name": "Tide", "rank": "S"},
"weeklycloneboss_selectbox_02": {"type": "item", "name": "Anomaly Pilgrimage Material Selection Box", "rank": "S"}
}

View file

@ -1,7 +1,7 @@
[project]
name = "nte-history-exporter"
version = "0.2.1"
description = "Cross-platform Neverness to Everness pull-history exporter."
version = "0.3.0"
description = "Cross-platform Neverness to Everness pull-history and achievement exporter."
requires-python = ">=3.10"
dependencies = []

View file

@ -1 +1 @@
__version__ = "0.2.1"
__version__ = "0.3.0"

View file

@ -20,7 +20,7 @@ def build_parser() -> argparse.ArgumentParser:
description="Decode NTE pull history from mitmproxy .flows captures or live capture."
)
parser.add_argument("capture_source", nargs="?", help="mitmproxy .flows file; omit when using --live")
parser.add_argument("--live", action="store_true", help="capture live UDP traffic instead of reading a .flows file")
parser.add_argument("--live", action="store_true", help="capture live game traffic instead of reading a .flows file")
parser.add_argument("--flow-index", type=int, default=None)
parser.add_argument("--interface-ip", default=None, help="local IPv4 address to bind for live capture")
parser.add_argument(

View file

@ -62,7 +62,7 @@ def print_banner() -> None:
print()
print(rule("="))
print(style(f" NTE History Exporter v{EXPORTER_VERSION}", BOLD, CYAN))
print(style(f" {GAME_NAME} pull history -> tracker JSON", DIM))
print(style(f" {GAME_NAME} history and achievements -> JSON", DIM))
print(style(f" {REPOSITORY_URL}", DIM))
heart = "❤️" if (getattr(sys.stdout, "encoding", None) or "").lower().replace("-", "") == "utf8" else "<3"
print(style(" Created with ", DIM) + style(heart, RED) + style(" by Golumpa", DIM))
@ -75,23 +75,29 @@ def print_live_instructions(local_ip: str, backend: str = "windows_raw", detail:
backend_detail = f" ({detail})" if detail and detail != local_ip else ""
print(style(f" Capture backend: {backend}{backend_detail}", DIM))
print()
print(style(" How to export your pull history", BOLD))
print(" 1. For automatic user UID detection, start this tool")
print(" before pressing Start on the game's main menu.")
print(" Already in game? You can still capture history;")
print(" the tool will ask for your UID if it cannot detect it.")
print(" 2. Open a supported history screen:")
print(style(" Monopoly > Standard Board history", CYAN))
print(style(" Monopoly > Limited Character Board history", CYAN))
print(style(" Gashapon > Arc Miracle Box history", CYAN))
print(style(" Gashapon > Mystery Box history", CYAN))
print(" 3. Start at page 1 and scroll down through every page")
print(" you want exported.")
print(" 4. Scroll one page past where you plan to stop so the")
print(" last pull group can be confirmed as complete.")
print(" 5. You can open several boards in the same session.")
print(style(" Before you begin", BOLD))
print(" Start this exporter before pressing Start on the game's")
print(" main menu. This lets it capture your achievements and")
print(" detect your account UID and server automatically.")
print()
print(style(" Waiting for history pages... press any key here when done.", BOLD, GREEN))
print(" Already in game? Pull history still works, but achievement")
print(" capture requires restarting the game. Missing account details")
print(" will be requested when your exports are saved.")
print()
print(style(" Pull history", BOLD))
print(" Open a supported history screen, start at page 1, then")
print(" scroll through every page you want to export. Go one page")
print(" further so the exporter can confirm the final pull group.")
print()
print(style(" Monopoly:", DIM) + " Standard Board or Limited Character Board")
print(style(" Gashapon:", DIM) + " Arc Miracle Box or Mystery Box")
print(style(" You can capture more than one board in the same session.", DIM))
print()
print(style(" Achievements", BOLD))
print(" Your tracked achievements are captured automatically when")
print(" you log in after following the step above.")
print()
print(style(" Ready - use the game, then press any key here when finished.", BOLD, GREEN))
print(rule())
@ -100,6 +106,29 @@ def print_page_captured(label: str, page: int | None, *, recaptured: bool = Fals
print(style(" + ", GREEN, BOLD) + label + style(f" {action} {page}", DIM))
def print_achievements_captured(
in_game_completed: int,
in_game_in_progress: int,
playstation_completed: int,
playstation_in_progress: int,
) -> None:
print(style(" + ", GREEN, BOLD) + "Achievements captured")
print(
style(
f" In-game {in_game_completed} completed, "
f"{in_game_in_progress} in progress",
DIM,
)
)
print(
style(
f" PlayStation {playstation_completed} completed, "
f"{playstation_in_progress} in progress",
DIM,
)
)
def print_missing_pages(label: str, pages: list[int]) -> None:
page_list = ", ".join(str(page) for page in pages)
print(style(" ! ", YELLOW, BOLD) + style(f"{label} missing page(s): {page_list}.", YELLOW, BOLD))
@ -169,7 +198,7 @@ def print_problem(text: str) -> None:
def print_update_available(current_version: str, latest_version: str, release_url: str) -> None:
print()
print(style(f" Update available: v{current_version} -> {latest_version}", YELLOW, BOLD))
print(style(" Some banner/reward mappings may be incomplete in this version.", YELLOW))
print(style(" Some reward or achievement mappings may be incomplete in this version.", YELLOW))
print(style(" Updating is recommended for the most accurate export labels.", YELLOW))
print(style(f" Download when ready: {release_url}", YELLOW))

View file

@ -13,7 +13,7 @@ ARC_BANNER_NAME = "Arc Miracle Box"
MYSTERY_BOX_BANNER_ID = "Gashapon_MysteryBox"
MYSTERY_BOX_BANNER_NAME = "Mystery Box"
EXPORTER_NAME = "nte-history-exporter"
EXPORTER_VERSION = "0.2.1"
EXPORTER_VERSION = "0.3.0"
HISTORY_REQUEST_BANNER = 4220
HISTORY_REQUEST_LENGTH = 45

View file

@ -0,0 +1,169 @@
import re
import struct
from dataclasses import dataclass
from datetime import datetime, timezone
ACHIEVEMENT_RECORD_MARKER = b"AchievementRecord\0"
ACHIEVEMENT_ID = re.compile(rb"([A-Za-z][A-Za-z0-9]*_[A-Za-z0-9_]+)\0")
RECORD_COUNT_OFFSET = 22
RECORDS_OFFSET = 38
COMPLETION_TICKS_OFFSET = 8
MAX_ACHIEVEMENTS = 1_000
DOTNET_EPOCH_TICKS = 621_355_968_000_000_000
DOTNET_TICKS_PER_SECOND = 10_000_000
@dataclass(frozen=True)
class AchievementRecord:
achievement_id: str
progress: int
completion_ticks: int
@property
def completed(self) -> bool:
return self.completion_ticks != 0
@property
def status(self) -> str:
if self.completed:
return "completed"
# Presence in the server response means the achievement has tracked
# state. Compound achievements can have hidden checklist progress while
# exposing a numeric progress value of zero to the client.
return "in_progress"
@property
def completed_at(self) -> str | None:
if not self.completed:
return None
unix_seconds = (
self.completion_ticks - DOTNET_EPOCH_TICKS
) / DOTNET_TICKS_PER_SECOND
try:
return datetime.fromtimestamp(unix_seconds, timezone.utc).strftime(
"%Y-%m-%d %H:%M:%S"
)
except (OverflowError, OSError, ValueError):
return None
def extract_achievement_ids(stream: bytes) -> list[str]:
return [
record.achievement_id
for record in extract_achievement_records(stream)
if record.completed
]
def extract_achievement_records(stream: bytes) -> list[AchievementRecord]:
for block in _game_data_blocks(stream):
decoded = _decompress_lz4_block(block)
marker = decoded.find(ACHIEVEMENT_RECORD_MARKER)
if marker < 0:
continue
records = _achievement_records(decoded, marker)
if records:
return records
return []
def reassemble_tcp_segments(segments: list[tuple[int, bytes]]) -> bytes:
if not segments:
return b""
output = bytearray()
start = min(sequence for sequence, _payload in segments)
for sequence, payload in sorted(set(segments)):
offset = sequence - start
if offset > len(output):
return b""
output.extend(payload[max(0, len(output) - offset) :])
return bytes(output)
def _achievement_records(decoded: bytes, marker: int) -> list[AchievementRecord]:
# The component header stores its record count before a fixed schema descriptor.
if marker + RECORDS_OFFSET > len(decoded):
return []
count = struct.unpack_from("<I", decoded, marker + RECORD_COUNT_OFFSET)[0]
if count > MAX_ACHIEVEMENTS:
return []
matches = list(ACHIEVEMENT_ID.finditer(decoded, marker + RECORDS_OFFSET))
if len(matches) < count:
return []
records = []
for match in matches[:count]:
# Each name is followed by an unsigned progress value and completion ticks.
progress_offset = match.end()
ticks_offset = match.end() + COMPLETION_TICKS_OFFSET
if ticks_offset + 8 > len(decoded):
return []
records.append(
AchievementRecord(
achievement_id=match.group(1).decode("ascii"),
progress=struct.unpack_from("<Q", decoded, progress_offset)[0],
completion_ticks=struct.unpack_from("<Q", decoded, ticks_offset)[0],
)
)
return records
def _game_data_blocks(stream: bytes):
position = 0
while position + 4 <= len(stream):
frame_size = struct.unpack_from("<I", stream, position)[0]
frame_end = position + 4 + frame_size
if not frame_size or frame_end > len(stream):
return
frame = stream[position + 4 : frame_end]
if len(frame) < 4:
return
header_size = struct.unpack_from("<I", frame)[0]
size_offset = 12 + header_size
data_offset = size_offset + 4
if data_offset <= len(frame):
data_size = struct.unpack_from("<I", frame, size_offset)[0]
if data_size and data_offset + data_size <= len(frame):
yield frame[data_offset : data_offset + data_size]
position = frame_end
def _decompress_lz4_block(data: bytes) -> bytes:
output = bytearray()
position = 0
while position < len(data):
if len(data) - position <= 3 and not any(data[position:]):
break
token = data[position]
position += 1
literal_length, position = _lz4_length(data, position, token >> 4)
literal_end = position + literal_length
if literal_end > len(data):
return b""
output.extend(data[position:literal_end])
position = literal_end
if position == len(data) or (
len(data) - position <= 3 and not any(data[position:])
):
break
if position + 2 > len(data):
return b""
offset = struct.unpack_from("<H", data, position)[0]
position += 2
if not offset or offset > len(output):
return b""
match_length, position = _lz4_length(data, position, token & 15)
for _ in range(match_length + 4):
output.append(output[-offset])
return bytes(output)
def _lz4_length(data: bytes, position: int, length: int) -> tuple[int, int]:
if length != 15:
return length, position
while position < len(data):
value = data[position]
position += 1
length += value
if value != 255:
return length, position
return length, position

View file

@ -21,7 +21,7 @@ from nte_history_exporter.constants import (
)
from nte_history_exporter.decoder.boundary import select_continuous_run_from_page_1
from nte_history_exporter.decoder.run import fmt_packet_time
from nte_history_exporter.mappings import ARC_META
from nte_history_exporter.mappings import ARC_META_BY_CASEFOLD
from nte_history_exporter.decoder.structured_protocol import (
StructuredProtocolAssembler,
StructuredRecord,
@ -95,7 +95,7 @@ def _parse_legacy_arc_response(response: bytes) -> list[dict[str, Any]]:
timestamp_raw = response[pos : pos + 8]
pos += 8
arc_id = decode_arc_key(name_raw) or name_raw.hex()
meta = ARC_META.get(arc_id, {})
canonical_id, meta = _resolve_arc_metadata(arc_id)
try:
ticks, unix_seconds, timestamp_decoded = decode_arc_timestamp(timestamp_raw)
except ValueError:
@ -107,7 +107,7 @@ def _parse_legacy_arc_response(response: bytes) -> list[dict[str, Any]]:
"record_len": pos - start,
"reward_key_hex": name_raw.hex(),
"reward_type": "arc",
"reward_id": arc_id,
"reward_id": canonical_id,
"reward_name": meta.get("name", "UNKNOWN"),
"reward_rank": meta.get("rank", ""),
"type_key_hex": type_raw.hex(),
@ -122,18 +122,15 @@ def _parse_legacy_arc_response(response: bytes) -> list[dict[str, Any]]:
return records
def _arc_metadata(arc_id: str) -> dict[str, Any]:
direct = ARC_META.get(arc_id)
if direct is not None:
return direct
folded = arc_id.casefold()
return next((meta for item_id, meta in ARC_META.items() if item_id.casefold() == folded), {})
def _resolve_arc_metadata(arc_id: str) -> tuple[str, dict[str, Any]]:
canonical_id, meta = ARC_META_BY_CASEFOLD.get(arc_id.casefold(), (arc_id, {}))
return canonical_id, meta
def structured_arc_rows(structured_rows: list[StructuredRecord]) -> list[dict[str, Any]]:
records = []
for structured in structured_rows:
meta = _arc_metadata(structured.item_id)
canonical_id, meta = _resolve_arc_metadata(structured.item_id)
records.append(
{
"record_start": structured.record_start,
@ -141,7 +138,7 @@ def structured_arc_rows(structured_rows: list[StructuredRecord]) -> list[dict[st
"record_len": structured.record_end - structured.record_start,
"reward_key_hex": "",
"reward_type": "arc",
"reward_id": structured.item_id,
"reward_id": canonical_id,
"reward_name": meta.get("name", "UNKNOWN"),
"reward_rank": meta.get("rank", ""),
"type_key_hex": "",

View file

@ -24,16 +24,11 @@ from nte_history_exporter.constants import (
from nte_history_exporter.decoder.boundary import select_continuous_run_from_page_1
from nte_history_exporter.decoder.protocol import infer_reward_type
from nte_history_exporter.decoder.run import fmt_packet_time
from nte_history_exporter.mappings import REWARDS_BY_ID
from nte_history_exporter.mappings import REWARDS_BY_CASEFOLD
DOTNET_TICKS_PER_SECOND = 10_000_000
MAX_RECORDS_PER_BLOCK = 100
MAX_REWARD_ID_LENGTH = 256
REWARDS_BY_CASEFOLDED_ID = {
reward_id.casefold(): reward for reward_id, reward in REWARDS_BY_ID.items()
}
def is_mystery_box_history_request(content: bytes) -> bool:
if len(content) < MYSTERY_BOX_HISTORY_REQUEST_LENGTH:
return False
@ -108,16 +103,15 @@ def _parse_view(data: bytes) -> list[dict[str, Any]]:
timestamp_raw = data[pos : pos + 8]
pos += 8
ticks, unix_seconds, timestamp_decoded = _decode_timestamp(timestamp_raw)
reward = REWARDS_BY_ID.get(reward_id) or REWARDS_BY_CASEFOLDED_ID.get(
reward_id.casefold(), {}
)
reward = REWARDS_BY_CASEFOLD.get(reward_id.casefold(), {})
canonical_reward_id = reward.get("id", reward_id)
rows.append(
{
"record_start": record_start,
"record_end": pos,
"record_len": pos - record_start,
"reward_type": reward.get("type") or infer_reward_type(reward_id),
"reward_id": reward_id,
"reward_id": canonical_reward_id,
"reward_name": reward.get("name", ""),
"reward_rank": reward.get("rank"),
"quantity": quantity,

View file

@ -15,7 +15,7 @@ from nte_history_exporter.constants import (
TIMESTAMP_TICKS_PER_SECOND,
VALID_DICE_FIELDS,
)
from nte_history_exporter.mappings import REWARDS_BY_ID
from nte_history_exporter.mappings import REWARDS_BY_CASEFOLD
from nte_history_exporter.decoder.structured_protocol import (
StructuredRecord,
parse_structured_records,
@ -61,11 +61,12 @@ def decode_reward_key(raw: bytes) -> str:
def infer_reward_type(reward_id: str) -> str:
if not reward_id:
return ""
if reward_id.startswith("fork_"):
folded = reward_id.casefold()
if folded.startswith("fork_"):
return "arc"
if reward_id.isdigit():
return "character"
if reward_id.startswith("Fashion_"):
if folded.startswith("fashion_"):
return "cosmetic"
return "item"
@ -165,7 +166,7 @@ def classify_result_type(
) -> tuple[str, int | None]:
if dice is None or dice_offset is None:
return "unknown", None
if reward_id == "Dice_ticket_01" and WARP_PIECE_CHASE_PATTERN in chunk_without_marker:
if reward_id.casefold() == "dice_ticket_01" and WARP_PIECE_CHASE_PATTERN in chunk_without_marker:
return "chase_reward", -4
if dice == 0:
return "points_gift", 0
@ -182,13 +183,14 @@ def classify_result_type(
def guess_quantity(chunk_hex: str, reward_id: str, result_type: str | None = None) -> int | None:
if reward_id == "Dice_ticket_01":
folded = reward_id.casefold()
if folded == "dice_ticket_01":
if result_type == "chase_reward":
return 30
return 4
if reward_id == "DiceNormal":
if folded == "dicenormal":
return 1
if reward_id == "Dice_ticket_02":
if folded == "dice_ticket_02":
if "c8b0d4c0" in chunk_hex:
return 50
if "c8b0ccc0" in chunk_hex:
@ -266,7 +268,8 @@ def _decode_aligned_response_records(response_content: bytes) -> list[dict[str,
elif result_type == "chase_reward":
dice = -4
dice_raw = -4
reward = REWARDS_BY_ID.get(reward_id, {})
reward = _reward_metadata(reward_id)
canonical_reward_id = reward.get("id", reward_id)
timestamp_raw = response_content[marker_offset + len(marker) : marker_offset + len(marker) + 8]
timestamp_ticks, timestamp_unix, timestamp_decoded = decode_history_timestamp(timestamp_raw)
chunk_hex = chunk.hex()
@ -288,7 +291,7 @@ def _decode_aligned_response_records(response_content: bytes) -> list[dict[str,
"dice_offset_in_record": dice_offset,
"reward_key_hex": key_hex,
"reward_type": reward.get("type") or infer_reward_type(reward_id),
"reward_id": reward_id,
"reward_id": canonical_reward_id,
"reward_name": reward.get("name", ""),
"reward_rank": reward.get("rank"),
"quantity": guess_quantity(chunk_hex, reward_id, result_type),
@ -335,8 +338,8 @@ def _enrich_heuristic_rows(
return heuristic_rows
for heuristic, structured in zip(heuristic_rows, structured_rows):
if not heuristic.get("reward_id"):
heuristic["reward_id"] = structured.item_id
reward = _reward_metadata(structured.item_id)
heuristic["reward_id"] = reward.get("id", structured.item_id)
heuristic["reward_type"] = reward.get("type") or infer_reward_type(structured.item_id)
heuristic["reward_name"] = reward.get("name", "")
heuristic["reward_rank"] = reward.get("rank")
@ -361,11 +364,7 @@ def _enrich_heuristic_rows(
def _reward_metadata(reward_id: str) -> dict[str, Any]:
direct = REWARDS_BY_ID.get(reward_id)
if direct is not None:
return direct
folded = reward_id.casefold()
return next((meta for item_id, meta in REWARDS_BY_ID.items() if item_id.casefold() == folded), {})
return REWARDS_BY_CASEFOLD.get(reward_id.casefold(), {})
def structured_monopoly_rows(structured_rows: list[StructuredRecord]) -> list[dict[str, Any]]:
@ -373,6 +372,7 @@ def structured_monopoly_rows(structured_rows: list[StructuredRecord]) -> list[di
for row_index, structured in enumerate(structured_rows, start=1):
dice, dice_raw, result_type, result_source = _structured_result(structured.roll_points_raw)
reward = _reward_metadata(structured.item_id)
canonical_reward_id = reward.get("id", structured.item_id)
rows.append(
{
"row": row_index,
@ -391,7 +391,7 @@ def structured_monopoly_rows(structured_rows: list[StructuredRecord]) -> list[di
"dice_offset_in_record": None,
"reward_key_hex": "",
"reward_type": reward.get("type") or infer_reward_type(structured.item_id),
"reward_id": structured.item_id,
"reward_id": canonical_reward_id,
"reward_name": reward.get("name", ""),
"reward_rank": reward.get("rank"),
"quantity": structured.count,

View file

@ -12,6 +12,9 @@ MONOPOLY_MARKER = b"FMonopolyLotteryRecordData"
FORK_MARKER = b"FForkLotteryRecordData"
MAX_ROWS_PER_BLOCK = 100
MAX_STRING_LENGTH = 256
# Bit-packed UDP views can omit up to the final three protocol-padding bytes.
# The declared size covers that padding, while all declared rows remain intact.
MAX_SHIFTED_BLOCK_PADDING_SHORTFALL = 3
DOTNET_EPOCH_TICKS = 621_355_968_000_000_000
DOTNET_TICKS_PER_SECOND = 10_000_000
MIN_UNIX_SECONDS = 1_500_000_000
@ -230,7 +233,11 @@ def _parse_block(
pos += 12
if row_count > MAX_ROWS_PER_BLOCK:
raise StructuredProtocolError(f"row count is too large: {row_count}")
if declared_size > len(data) - pos:
size_shortfall = declared_size - (len(data) - pos)
if size_shortfall > 0 and not (
view_name.startswith("shift8:")
and size_shortfall <= MAX_SHIFTED_BLOCK_PADDING_SHORTFALL
):
raise StructuredProtocolError("declared block size exceeds payload")
reader = _Reader(data, pos)

View file

@ -12,6 +12,8 @@ from nte_history_exporter.constants import (
MYSTERY_BOX_BANNER_ID,
)
from nte_history_exporter.decoder.server_region import account_region_for_server
from nte_history_exporter.decoder.achievement import AchievementRecord
from nte_history_exporter.mappings import ACHIEVEMENTS_BY_CASEFOLD
def build_export_json(
@ -78,6 +80,83 @@ def build_export_json(
return export
def build_achievement_export_json(
achievement_records: list[AchievementRecord],
*,
source: str = "packet_capture",
capture_source: str | None = None,
user_uid: str | None = None,
server_id: str | None = None,
) -> dict[str, Any]:
categories: dict[str, list[dict[str, Any]]] = {}
scan = {
"in_game": {
"total_achievements": 0,
"completed_achievements": 0,
"in_progress_achievements": 0,
},
"playstation": {
"total_achievements": 0,
"completed_achievements": 0,
"in_progress_achievements": 0,
},
}
for achievement in achievement_records:
achievement_id = achievement.achievement_id
is_playstation = achievement_id.casefold().startswith("playstation_")
summary = scan["playstation" if is_playstation else "in_game"]
summary["total_achievements"] += 1
if achievement.completed:
summary["completed_achievements"] += 1
else:
summary["in_progress_achievements"] += 1
category, separator, _number = achievement_id.partition("_")
category_key = category.casefold() if separator else "uncategorized"
record = {
"id": achievement_id,
"platform": "playstation" if is_playstation else "in_game",
"status": achievement.status,
"progress": achievement.progress,
"completed": achievement.completed,
"completed_at": achievement.completed_at,
}
metadata = ACHIEVEMENTS_BY_CASEFOLD.get(achievement_id.casefold())
if metadata:
record.update(
{
"name": metadata["name"],
"description": metadata["description"],
"quality": metadata["quality"],
"target": metadata["target"],
"rewards": metadata["rewards"],
}
)
categories.setdefault(category_key, []).append(record)
export: dict[str, Any] = {
"format": "nte-achievement-export",
"format_version": 1,
"game": GAME_NAME,
"source": source,
}
if capture_source:
export["capture_source"] = capture_source
export["exporter"] = {"name": EXPORTER_NAME, "version": __version__}
export["scan"] = scan
normalized_user_uid = user_uid.strip() if user_uid else ""
normalized_server_id = str(server_id).strip() if server_id else ""
if normalized_user_uid:
export["user_uid"] = normalized_user_uid
if normalized_server_id:
export["server_id"] = normalized_server_id
account_region = account_region_for_server(normalized_server_id)
if account_region:
export["account_region"] = account_region
export["categories"] = categories
return export
def _record_for_export(row: dict[str, Any]) -> dict[str, Any]:
if row.get("pool_group_id") == ARC_BANNER_ID:
return {

View file

@ -9,9 +9,16 @@ from pathlib import Path
from nte_history_exporter import console
from nte_history_exporter.constants import POOL_META
from nte_history_exporter.decoder.achievement import (
extract_achievement_records,
reassemble_tcp_segments,
)
from nte_history_exporter.decoder.boundary import annotate_groups, select_continuous_run_from_page_1
from nte_history_exporter.export.csv_export import write_csv
from nte_history_exporter.export.json_export import build_export_json
from nte_history_exporter.export.json_export import (
build_achievement_export_json,
build_export_json,
)
from nte_history_exporter.live_capture.backends import open_capture_backend
from nte_history_exporter.live_capture.diagnostics import new_diagnostics_path, write_capture_diagnostics
from nte_history_exporter.live_capture.session import LiveHistorySession, UdpPacket
@ -79,6 +86,8 @@ def run_live_capture(
console.print_capture_fallback(capture.fallback_reason)
reported_missing_pages: dict[str, tuple[int, ...]] = {}
active_gap_notices: set[str] = set()
tcp_segments: dict[tuple[str, int, str, int], list[tuple[int, bytes]]] = {}
achievement_records = []
try:
with StopKeyMonitor() as stop_key:
@ -87,6 +96,44 @@ def run_live_capture(
break
if packet is None:
continue
if (
not achievement_records
and packet.protocol == "tcp"
and packet.payload
and packet.dst_ip == local_ip
):
flow = (packet.src_ip, packet.src_port, packet.dst_ip, packet.dst_port)
tcp_segments.setdefault(flow, []).append(
(packet.sequence_number, packet.payload)
)
if not achievement_records:
achievement_records = extract_achievement_records(
reassemble_tcp_segments(tcp_segments[flow])
)
if achievement_records:
in_game = [
record
for record in achievement_records
if not record.achievement_id.casefold().startswith(
"playstation_"
)
]
playstation = [
record
for record in achievement_records
if record.achievement_id.casefold().startswith(
"playstation_"
)
]
console.print_achievements_captured(
sum(record.completed for record in in_game),
sum(record.status == "in_progress" for record in in_game),
sum(record.completed for record in playstation),
sum(
record.status == "in_progress"
for record in playstation
),
)
pair_count_before = len(session.pairs)
matched = session.process_packet(
UdpPacket(
@ -129,21 +176,28 @@ def run_live_capture(
stats = capture.stats()
capture.close()
if stats:
console.print_capture_stats(
stats.received,
stats.dropped,
stats.interface_dropped,
)
exports = []
achievement_path = None
diagnostics_path = None
if write_debug_csv:
diagnostics_path = new_diagnostics_path()
write_capture_diagnostics(diagnostics_path, session.diagnostic_report())
resolved_user_uid = user_uid or session.user_uid
if session.kinds_seen() and not resolved_user_uid:
if (session.kinds_seen() or achievement_records) and not resolved_user_uid:
resolved_user_uid = console.prompt_user_uid()
if achievement_records:
achievement_path = _achievement_path(resolved_user_uid)
achievement_export = build_achievement_export_json(
achievement_records,
source="live_capture",
capture_source=CAPTURE_SOURCE_LABELS.get(capture.name, capture.name),
user_uid=resolved_user_uid,
server_id=session.server_id,
)
achievement_path.write_text(
json.dumps(achievement_export, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
resolved_server_id = session.server_id
if session.kinds_seen() and not resolved_server_id:
resolved_server_id = console.prompt_server_id()
@ -182,14 +236,32 @@ def run_live_capture(
)
console.print_results_header()
if stats:
console.print_capture_stats(
stats.received,
stats.dropped,
stats.interface_dropped,
)
if stats:
print()
if not exports:
if achievement_path is not None:
console.print_success("Achievement export complete.")
console.print_note("No pull history was captured (this is fine if you only wanted achievements).")
print()
console.print_note(f"Export written: {achievement_path}")
else:
console.print_problem("No history pages were captured.")
console.print_note("Make sure the capture backend is running, then reopen the")
console.print_note("history screen and scroll from page 1. If no page messages")
console.print_note("appear, return to the main menu and re-enter the game.")
console.print_note("Reopen a supported history screen and scroll from page 1.")
console.print_note("If no page messages appear, return to the main menu and re-enter the game.")
if diagnostics_path is not None:
console.print_note(f"Diagnostics written: {diagnostics_path}")
return {"exports": [], "diagnostics_path": diagnostics_path}
return {
"exports": [],
"diagnostics_path": diagnostics_path,
"achievement_path": achievement_path,
}
for item in exports:
scan = item["export"]["scan"]
@ -207,6 +279,9 @@ def run_live_capture(
if item["csv_path"] is not None:
console.print_note(f"CSV written: {item['csv_path']}")
console.print_note(f"Export written: {item['json_path']}")
if achievement_path is not None:
print()
console.print_note(f"Export written: {achievement_path}")
if diagnostics_path is not None:
console.print_note(f"Diagnostics written: {diagnostics_path}")
@ -218,7 +293,24 @@ def run_live_capture(
elif copy_clipboard and len(exports) > 1:
console.print_note("Multiple banners captured; clipboard copy skipped so one export")
console.print_note("does not overwrite another.")
return {"exports": exports, "diagnostics_path": diagnostics_path}
return {
"exports": exports,
"diagnostics_path": diagnostics_path,
"achievement_path": achievement_path,
}
def _achievement_path(user_uid: str | None = None) -> Path:
export_dir = Path("exports")
export_dir.mkdir(parents=True, exist_ok=True)
uid_prefix = _safe_filename_part(user_uid) if user_uid else "unknown"
base = export_dir / f"{uid_prefix}_Achievements_{datetime.now():%Y%m%d_%H%M%S}"
path = base.with_suffix(".json")
counter = 2
while path.exists():
path = export_dir / f"{base.name}_{counter}.json"
counter += 1
return path
def export_paths(kind: str, user_uid: str | None = None) -> tuple[Path, Path]:

View file

@ -17,6 +17,7 @@ class ParsedIpPacket:
dst_port: int
payload: bytes
protocol: str = "udp"
sequence_number: int = 0
ParsedIpUdpPacket = ParsedIpPacket
@ -69,12 +70,14 @@ def parse_ipv4_packet(data: bytes) -> ParsedIpPacket | None:
if protocol == 6:
if len(data) < ihl + 20:
return None
src_port, dst_port = struct.unpack_from("!HH", data, ihl)
src_port, dst_port, sequence_number = struct.unpack_from("!HHI", data, ihl)
tcp_header_len = (data[ihl + 12] >> 4) * 4
if tcp_header_len < 20 or len(data) < ihl + tcp_header_len:
return None
payload = data[ihl + tcp_header_len :]
return ParsedIpPacket(src_ip, dst_ip, src_port, dst_port, payload, "tcp")
return ParsedIpPacket(
src_ip, dst_ip, src_port, dst_port, payload, "tcp", sequence_number
)
return None

View file

@ -20,10 +20,11 @@ ASSET_TABLES = {
"appearances": "DataTable/Character/Appearance/DT_AppearanceData.json",
"illustrations": "DataTable/Gacha/GachaIllustrate.json",
"mystery_box_pools": "DataTable/GashaponLottery/DT_GashaponLotteryGlobal.json",
"achievements": "DataTable/DT_AchievementConfigInfo.json",
"vehicle_inventory": "DataTable/Vehicle/DT_VehicleItemData.json",
"localization": "Localization/en/game.json",
}
REWARD_MAPPING_FILES = ("arcs.json", "characters.json", "items.json")
MAPPING_FILES = ("arcs.json", "characters.json", "items.json", "achievements.json")
RANK_BY_QUALITY = {
"EItemQuality::ITEM_QUALITY_ORANGE": "S",
"EItemQuality::ITEM_QUALITY_PURPLE": "A",
@ -103,7 +104,7 @@ def load_assets(*, assets_root: Path | None = None, source_ref: str = DEFAULT_SO
def load_current_mappings(directory: Path) -> dict[str, dict[str, dict[str, Any]]]:
mappings = {}
for filename in REWARD_MAPPING_FILES:
for filename in MAPPING_FILES:
path = directory / filename
try:
value = json.loads(path.read_text(encoding="utf-8"))
@ -126,13 +127,16 @@ def build_mapping_update(
"arcs.json": _build_primary_mapping(assets.tables["arcs"], "arc", translations),
"characters.json": _build_primary_mapping(assets.tables["characters"], "character", translations),
"items.json": _build_item_mapping(assets.tables, translations),
"achievements.json": _build_achievement_mapping(
assets.tables["achievements"], translations
),
}
validate_mappings(output)
additions: dict[str, list[str]] = {}
updates: dict[str, list[str]] = {}
deletions: dict[str, list[str]] = {}
for filename in REWARD_MAPPING_FILES:
for filename in MAPPING_FILES:
old = current[filename]
new = output[filename]
additions[filename] = sorted(new.keys() - old.keys(), key=str.casefold)
@ -169,7 +173,7 @@ def build_mapping_update(
def write_update(result: MappingUpdateResult, directory: Path) -> None:
directory.mkdir(parents=True, exist_ok=True)
for filename in REWARD_MAPPING_FILES:
for filename in MAPPING_FILES:
_atomic_write(directory / filename, _dump_mapping(result.mappings[filename]))
report_text = json.dumps(result.report, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
_atomic_write(directory / "mapping-update-report.json", report_text)
@ -180,11 +184,11 @@ def apply_update(result: MappingUpdateResult, directory: Path) -> None:
directory.mkdir(parents=True, exist_ok=True)
originals: dict[Path, bytes | None] = {}
written: list[Path] = []
for filename in REWARD_MAPPING_FILES:
for filename in MAPPING_FILES:
path = directory / filename
originals[path] = path.read_bytes() if path.exists() else None
try:
for filename in REWARD_MAPPING_FILES:
for filename in MAPPING_FILES:
path = directory / filename
_atomic_write(path, _dump_mapping(result.mappings[filename]))
written.append(path)
@ -199,11 +203,11 @@ def apply_update(result: MappingUpdateResult, directory: Path) -> None:
def validate_mappings(mappings: dict[str, dict[str, dict[str, Any]]]) -> None:
if set(mappings) != set(REWARD_MAPPING_FILES):
raise MappingUpdateError(f"mapping set must be exactly {REWARD_MAPPING_FILES}")
if set(mappings) != set(MAPPING_FILES):
raise MappingUpdateError(f"mapping set must be exactly {MAPPING_FILES}")
locations: dict[str, str] = {}
folded: dict[str, str] = {}
for filename in REWARD_MAPPING_FILES:
for filename in MAPPING_FILES:
entries = mappings[filename]
if not isinstance(entries, dict):
raise MappingUpdateError(f"{filename} must contain an object")
@ -232,6 +236,33 @@ def validate_mappings(mappings: dict[str, dict[str, dict[str, Any]]]) -> None:
raise MappingUpdateError(f"{filename}:{item_id}.type must be item or cosmetic")
if rank not in {"S", "A", "B"}:
raise MappingUpdateError(f"{filename}:{item_id}.rank must be S, A, or B")
if filename == "achievements.json":
_validate_achievement_meta(filename, item_id, meta)
def _validate_achievement_meta(filename: str, item_id: str, meta: dict[str, Any]) -> None:
for field in ("description", "category", "quality"):
if not isinstance(meta.get(field), str) or not meta[field].strip():
raise MappingUpdateError(
f"{filename}:{item_id}.{field} must be a non-empty string"
)
target = meta.get("target")
if not isinstance(target, (int, float)) or isinstance(target, bool) or target < 0:
raise MappingUpdateError(f"{filename}:{item_id}.target must be a non-negative number")
rewards = meta.get("rewards")
if not isinstance(rewards, list):
raise MappingUpdateError(f"{filename}:{item_id}.rewards must be a list")
for index, reward in enumerate(rewards):
if (
not isinstance(reward, dict)
or not isinstance(reward.get("item_id"), str)
or not reward["item_id"]
or not isinstance(reward.get("amount"), (int, float))
or isinstance(reward.get("amount"), bool)
):
raise MappingUpdateError(
f"{filename}:{item_id}.rewards[{index}] is invalid"
)
def _extract_rows(document: Any, relative_path: str) -> dict[str, Any]:
@ -289,7 +320,19 @@ def _build_item_mapping(
candidate_ids = _reward_candidate_ids(tables)
for candidate_id in candidate_ids:
folded = candidate_id.casefold()
if candidate_id.isdigit() or folded.startswith("fork_") or folded.startswith("characterawaken_"):
if (
candidate_id.isdigit()
or folded.startswith("fork_")
or folded.startswith("characterawaken_")
):
continue
if folded.startswith("fashion_vehicle_"):
illustration = tables["illustrations"].get(candidate_id)
if illustration is not None:
vehicle_meta = _normalise_vehicle_livery(candidate_id, illustration, translations)
if vehicle_meta is None:
continue
result[candidate_id] = vehicle_meta
continue
sources = (inventory, capital, appearances, vehicle_inventory)
match = next((source.get(folded) for source in sources if folded in source), None)
@ -307,6 +350,87 @@ def _build_item_mapping(
return dict(sorted(result.items(), key=lambda pair: (pair[0].casefold(), pair[0])))
def _normalise_vehicle_livery(
item_id: str,
row: Any,
translations: dict[str, list[tuple[str, str]]],
) -> dict[str, str] | None:
"""Resolve a livery from its illustration, ignoring mismatched placeholder rows."""
if not isinstance(row, dict):
raise MappingUpdateError(f"NTE_Assets row {item_id} must be an object")
head_icon = row.get("HeadIcon")
asset_path = head_icon.get("AssetPathName") if isinstance(head_icon, dict) else None
if not isinstance(asset_path, str) or not asset_path:
raise MappingUpdateError(f"NTE_Assets vehicle livery row {item_id} has no HeadIcon")
icon_id = asset_path.rsplit("/", 1)[-1].split(".", 1)[0]
if icon_id.casefold() != item_id.casefold():
return None
item_name = row.get("ItemName_Override")
if not isinstance(item_name, dict):
raise MappingUpdateError(f"NTE_Assets vehicle livery row {item_id} has no ItemName_Override")
return {
"type": "cosmetic",
"name": _translate_name(item_id, item_name, translations).strip(),
"rank": "S",
}
def _build_achievement_mapping(
rows: dict[str, Any],
translations: dict[str, list[tuple[str, str]]],
) -> dict[str, dict[str, Any]]:
result = {}
for achievement_id in sorted(rows, key=lambda value: (value.casefold(), value)):
row = rows[achievement_id]
if not isinstance(row, dict):
raise MappingUpdateError(
f"NTE_Assets achievement row {achievement_id} must be an object"
)
title = row.get("TitleId")
description = row.get("ContentID")
if not isinstance(title, dict) or not isinstance(description, dict):
raise MappingUpdateError(
f"NTE_Assets achievement row {achievement_id} has no title or description"
)
target = row.get("Amout")
if not isinstance(target, (int, float)) or isinstance(target, bool):
raise MappingUpdateError(
f"NTE_Assets achievement row {achievement_id} has invalid Amout"
)
main_type = row.get("AchievementMainType")
quality = row.get("Quality")
if not isinstance(main_type, str) or "MainType" not in main_type:
raise MappingUpdateError(
f"NTE_Assets achievement row {achievement_id} has invalid main type"
)
if not isinstance(quality, str) or "::" not in quality:
raise MappingUpdateError(
f"NTE_Assets achievement row {achievement_id} has invalid quality"
)
rewards = []
for index, award in enumerate(row.get("AwardInfo", [])):
if not isinstance(award, dict):
raise MappingUpdateError(
f"NTE_Assets achievement row {achievement_id} award {index} is invalid"
)
item_id = award.get("ItemID")
amount = award.get("Amount")
if not isinstance(item_id, str) or not item_id or not isinstance(amount, (int, float)):
raise MappingUpdateError(
f"NTE_Assets achievement row {achievement_id} award {index} is invalid"
)
rewards.append({"item_id": item_id, "amount": amount})
result[achievement_id] = {
"name": _translate_name(achievement_id, title, translations),
"description": _translate_name(achievement_id, description, translations),
"category": main_type.rsplit("MainType", 1)[-1].casefold(),
"quality": quality.rsplit("::", 1)[-1].casefold(),
"target": target,
"rewards": rewards,
}
return result
def _reward_candidate_ids(tables: dict[str, dict[str, Any]]) -> list[str]:
"""Return rewards used by ordinary Gacha and every Mystery Box rotation."""
candidates: dict[str, str] = {

View file

@ -14,8 +14,15 @@ def load_mapping_file(filename: str) -> dict[str, Any]:
ARC_META: dict[str, dict[str, Any]] = load_mapping_file("arcs.json")
ARC_META_BY_CASEFOLD = {
arc_id.casefold(): (arc_id, meta) for arc_id, meta in ARC_META.items()
}
CHARACTERS: dict[str, dict[str, Any]] = load_mapping_file("characters.json")
ITEMS: dict[str, dict[str, Any]] = load_mapping_file("items.json")
ACHIEVEMENTS: dict[str, dict[str, Any]] = load_mapping_file("achievements.json")
ACHIEVEMENTS_BY_CASEFOLD = {
achievement_id.casefold(): meta for achievement_id, meta in ACHIEVEMENTS.items()
}
# Reward keys in Monopoly history packets are the reward id string itself
# (ASCII*4 with carry, see decoder.protocol.decode_reward_key), so all reward
@ -32,3 +39,6 @@ for _character_id, _meta in CHARACTERS.items():
}
for _item_id, _meta in ITEMS.items():
REWARDS_BY_ID[_item_id] = {"type": _meta["type"], "id": _item_id, "name": _meta["name"], "rank": _meta.get("rank")}
REWARDS_BY_CASEFOLD = {
reward_id.casefold(): reward for reward_id, reward in REWARDS_BY_ID.items()
}

View file

@ -0,0 +1,22 @@
[
{
"Rows": {
"battle_25": {
"Amout": 50,
"AchievementMainType": "AchievementMainType::MainTypeFight",
"AwardInfo": [
{"ItemID": "Annulith", "Amount": 10}
],
"TitleId": {
"TableId": "/Game/Text/ST_Achievement.ST_Achievement",
"Key": "battle_25_Title"
},
"ContentID": {
"TableId": "/Game/Text/ST_Achievement.ST_Achievement",
"Key": "battle_25_Content"
},
"Quality": "EHTAchievementQuality::High"
}
}
}
]

View file

@ -1 +1 @@
[{"Rows":{"1003":{},"1099":{},"fork_alpha":{},"fork_new":{},"DIceNormal":{},"Dice_ticket_03":{},"Fashion_glide_2000":{},"Characterawaken_1003":{}}}]
[{"Rows":{"1003":{},"1099":{},"fork_alpha":{},"fork_new":{},"DIceNormal":{},"Dice_ticket_03":{},"Fashion_glide_2000":{},"Characterawaken_1003":{},"Fashion_vehicle_1010_V008":{"HeadIcon":{"AssetPathName":"/Game/UI/UI_Icon/Item/Fashion_vehicle_1010_V008.Fashion_vehicle_1010_V008"},"ItemName_Override":{"TableId":"/Game/Text/ST_VehicleData.ST_VehicleData","Key":"vehicle_1010_livery"}},"Fashion_vehicle_1072_V006":{"HeadIcon":{"AssetPathName":"/Game/UI/UI_Icon/Item/Fashion_vehicle_1036_V043.Fashion_vehicle_1036_V043"},"ItemName_Override":{"TableId":"/Game/Text/ST_VehicleData.ST_VehicleData","Key":"vehicle_placeholder_livery"}}}}]

View file

@ -1,4 +1,8 @@
{
"ST_Achievement": {
"battle_25_Title": "Devil Within II",
"battle_25_Content": "Trigger Hexed ×50."
},
"ST_Player": {
"character_1003": "Old Character Renamed",
"character_1099": "New Character"
@ -14,5 +18,9 @@
},
"ST_Appearance": {
"glider_2000": "Inventory Glider"
},
"ST_VehicleData": {
"vehicle_1010_livery": "Tiger Incoming! - Livery",
"vehicle_placeholder_livery": "Sandy Mirage - Livery"
}
}

37
tests/test_achievement.py Normal file
View file

@ -0,0 +1,37 @@
import struct
import unittest
from nte_history_exporter.decoder.achievement import (
ACHIEVEMENT_RECORD_MARKER,
extract_achievement_ids,
extract_achievement_records,
reassemble_tcp_segments,
)
class AchievementPacketTests(unittest.TestCase):
def test_extracts_only_completed_achievements(self):
header = ACHIEVEMENT_RECORD_MARKER + b"\0" * 4 + struct.pack("<I", 2) + b"\0" * 12
completed = b"\x0a\0\0\0Battle_30\0" + b"\0" * 8 + struct.pack("<Q", 1)
incomplete = b"\x13\0\0\0Playstation_035\0" + b"\0" * 16
decoded = header + completed + incomplete
extension = len(decoded) - 15
compressed = b"\xf0" + bytes([extension]) + decoded
frame = struct.pack("<I", 0) + b"\0" * 8 + struct.pack("<I", len(compressed)) + compressed
stream = struct.pack("<I", len(frame)) + frame
self.assertEqual(extract_achievement_ids(stream), ["Battle_30"])
self.assertEqual(extract_achievement_ids(b"\x01\0\0\0\0"), [])
records = extract_achievement_records(stream)
self.assertEqual(len(records), 2)
self.assertEqual(records[0].achievement_id, "Battle_30")
self.assertTrue(records[0].completed)
self.assertEqual(records[1].achievement_id, "Playstation_035")
self.assertEqual(records[1].status, "in_progress")
def test_reassembles_tcp_data(self):
self.assertEqual(
reassemble_tcp_segments([(104, b"bar"), (100, b"foobar"), (100, b"foobar")]),
b"foobarr",
)

View file

@ -1,8 +1,14 @@
from tests.support import * # noqa: F401,F403
from nte_history_exporter.decoder.arc import annotate_arc_groups
from nte_history_exporter.decoder.arc import annotate_arc_groups, _resolve_arc_metadata
class ArcDecodingTests(unittest.TestCase):
def test_arc_mapping_lookup_is_case_insensitive_and_canonical(self):
reward_id, metadata = _resolve_arc_metadata("fork_Wushoutieyu")
self.assertEqual(reward_id, "fork_wushoutieyu")
self.assertEqual(metadata, {"name": "Raging Flames", "rank": "S"})
def test_page_split_timestamp_variants_form_one_ten_pull(self):
rows = [
{

View file

@ -1,8 +1,16 @@
from tests.support import * # noqa: F401,F403
from nte_history_exporter import __version__
from nte_history_exporter.constants import EXPORTER_VERSION
from nte_history_exporter.decoder.achievement import AchievementRecord
from nte_history_exporter.export.json_export import build_achievement_export_json
from nte_history_exporter.live_capture.runner import _achievement_path
class ExportContractTests(unittest.TestCase):
def test_public_version_references_match(self):
self.assertEqual(__version__, "0.3.0")
self.assertEqual(EXPORTER_VERSION, __version__)
def test_sanitized_export_omits_raw_packet_fields(self):
annotated = annotate_groups(fixture_session().build_rows("permanent"))
export = build_export_json(annotated, [])
@ -117,6 +125,81 @@ class ExportContractTests(unittest.TestCase):
r"^218216016349_Limited_\d{8}_\d{6}(?:_\d+)?\.json$",
)
def test_achievement_path_includes_user_uid_and_timestamp(self):
path = _achievement_path("218216016349")
self.assertRegex(
path.name,
r"^218216016349_Achievements_\d{8}_\d{6}(?:_\d+)?\.json$",
)
def test_achievement_export_has_versioned_metadata_and_records(self):
export = build_achievement_export_json(
[
AchievementRecord("Battle_30", 10, 639_136_215_846_370_000),
AchievementRecord("Battle_25", 20, 0),
AchievementRecord("Playstation_017", 0, 0),
],
source="live_capture",
capture_source="npcap",
user_uid="218216016349",
server_id="23003",
)
self.assertEqual(export["format"], "nte-achievement-export")
self.assertEqual(export["format_version"], 1)
self.assertEqual(export["capture_source"], "npcap")
self.assertEqual(export["user_uid"], "218216016349")
self.assertEqual(export["account_region"], "EU")
self.assertEqual(
export["scan"],
{
"in_game": {
"total_achievements": 2,
"completed_achievements": 1,
"in_progress_achievements": 1,
},
"playstation": {
"total_achievements": 1,
"completed_achievements": 0,
"in_progress_achievements": 1,
},
},
)
battle_30, battle_25 = export["categories"]["battle"]
self.assertEqual(battle_30["name"], "Death Nova I")
self.assertEqual(battle_30["completed_at"], "2026-05-05 23:46:24")
self.assertEqual(battle_25["name"], "Devil Within II")
self.assertEqual(battle_25["description"], "Trigger Hexed ×50.")
self.assertEqual(battle_25["progress"], 20)
self.assertEqual(battle_25["target"], 50)
self.assertEqual(battle_25["quality"], "high")
self.assertEqual(
battle_25["rewards"], [{"item_id": "Annulith", "amount": 10}]
)
self.assertEqual(
export["categories"]["playstation"][0]["name"], "Speed Above All"
)
def test_unmapped_achievement_still_exports_capture_data(self):
export = build_achievement_export_json(
[AchievementRecord("FutureCategory_999", 7, 0)]
)
self.assertEqual(
export["categories"]["futurecategory"],
[
{
"id": "FutureCategory_999",
"platform": "in_game",
"status": "in_progress",
"progress": 7,
"completed": False,
"completed_at": None,
}
],
)
def test_extracts_user_uid_from_record_context(self):
payload = (
b"\x00" * 24

View file

@ -36,6 +36,7 @@ def sample_current():
"DiceNormal": {"type": "item", "name": "Fabricated Dice", "rank": "S"},
"OldTicket": {"type": "item", "name": "Retired Ticket", "rank": "B"},
},
"achievements.json": {},
}
@ -54,11 +55,22 @@ class MappingUpdateTests(unittest.TestCase):
self.assertNotIn("fork_retired", result.mappings["arcs.json"])
self.assertNotIn("OldTicket", result.mappings["items.json"])
self.assertNotEqual(result.mappings["characters.json"]["1003"], current["characters.json"]["1003"])
self.assertEqual(result.report["changes"], {"additions": 4, "updates": 2, "deletions": 3})
self.assertEqual(result.report["changes"], {"additions": 6, "updates": 2, "deletions": 3})
self.assertIs(result.report["safety"]["authoritative_snapshot"], True)
self.assertIs(result.report["safety"]["deletions_allowed"], True)
self.assertIs(result.report["safety"]["uid_inputs_touched"], False)
self.assertIs(result.report["safety"]["pool_mappings_touched"], False)
self.assertEqual(
result.mappings["achievements.json"]["battle_25"],
{
"name": "Devil Within II",
"description": "Trigger Hexed ×50.",
"category": "fight",
"quality": "high",
"target": 50,
"rewards": [{"item_id": "Annulith", "amount": 10}],
},
)
def test_items_use_illustrations_as_filter_and_asset_tables_as_authority(self):
_current, result = build_sample_update()
@ -71,6 +83,11 @@ class MappingUpdateTests(unittest.TestCase):
self.assertNotIn("DIceNormal", result.mappings["items.json"])
self.assertNotIn("UnusedItem", result.mappings["items.json"])
self.assertFalse(any(key.startswith("Characterawaken_") for key in result.mappings["items.json"]))
self.assertEqual(
result.mappings["items.json"]["Fashion_vehicle_1010_V008"],
{"type": "cosmetic", "name": "Tiger Incoming! - Livery", "rank": "S"},
)
self.assertNotIn("Fashion_vehicle_1072_V006", result.mappings["items.json"])
def test_future_mystery_box_pool_is_discovered_without_hard_coded_event_id(self):
assets = load_assets(assets_root=SAMPLE_ASSETS)
@ -100,7 +117,7 @@ class MappingUpdateTests(unittest.TestCase):
tables["localization"]["ST_Item"].update(
{"wow_ticket": "Wowzers Ticket", "wow_frame": "Wowzers Frame"}
)
tables["localization"]["ST_VehicleData"] = {"vehicle_wow": "Wowmobile"}
tables["localization"]["ST_VehicleData"]["vehicle_wow"] = "Wowmobile"
result = build_mapping_update(sample_current(), replace(assets, tables=tables))

View file

@ -1,4 +1,5 @@
from tests.support import * # noqa: F401,F403
from nte_history_exporter.mappings import ACHIEVEMENTS
class MappingAndUpdateTests(unittest.TestCase):
@ -79,6 +80,14 @@ class MappingAndUpdateTests(unittest.TestCase):
self.assertIn("name", info)
self.assertIn(info.get("rank"), ("S", "A"))
self.assertTrue(ACHIEVEMENTS)
battle_25 = ACHIEVEMENTS["battle_25"]
self.assertEqual(battle_25["name"], "Devil Within II")
self.assertEqual(battle_25["target"], 50)
self.assertEqual(
battle_25["rewards"], [{"item_id": "Annulith", "amount": 10}]
)
self.assertTrue(ITEMS)
for item_id, info in ITEMS.items():
with self.subTest(item_id=item_id):

View file

@ -71,6 +71,15 @@ class MysteryBoxDecodingTests(unittest.TestCase):
self.assertEqual(rows[2]["reward_name"], "Beetle Coin")
self.assertEqual(rows[2]["reward_rank"], "B")
def test_reward_mapping_lookup_is_case_insensitive_and_canonical(self):
timestamp = datetime(2026, 7, 8, 19, 9, 33, tzinfo=timezone.utc)
rows = parse_mystery_box_response(
mystery_box_response([("Vehicle039", 1, timestamp)])
)
self.assertEqual(rows[0]["reward_id"], "vehicle039")
self.assertEqual(rows[0]["reward_name"], "Draco")
def test_live_session_accepts_partial_final_page(self):
session = LiveHistorySession("192.168.0.10")
request = mystery_box_request(3)

View file

@ -157,6 +157,12 @@ class StructuredProtocolTests(unittest.TestCase):
self.assertEqual(rows[0]["secondary_quantity"], 5)
self.assertEqual(rows[0]["structured_pool_id"], "CardPool_Character")
def test_structured_reward_mapping_is_case_insensitive_and_canonical(self):
row = decode_response_records(monopoly_payload("DICENORMAL,1"))[0]
self.assertEqual(row["reward_id"], "DiceNormal")
self.assertEqual(row["reward_name"], "Fabricated Dice")
def test_structured_monopoly_parser_falls_back_when_heuristic_returns_no_rows(self):
payload = monopoly_payload("Dice_ticket_02,50", roll_points=0)
@ -187,6 +193,31 @@ class StructuredProtocolTests(unittest.TestCase):
self.assertEqual(rows[0].item_id, "1003")
self.assertEqual(rows[0].protocol_view, "shift8:3")
def test_shifted_parser_allows_missing_declared_protocol_padding(self):
cases = (
("monopoly", monopoly_payload("1003,1"), 3, "1003"),
("fork", fork_payload("fork_dustbin"), 2, "fork_dustbin"),
)
for record_type, raw_payload, shift, expected_item_id in cases:
with self.subTest(record_type=record_type):
payload_with_declared_padding = bytearray(raw_payload)
marker = MONOPOLY_MARKER if record_type == "monopoly" else FORK_MARKER
declared_size_pos = len(marker) + 1 + 4
declared_size = int.from_bytes(
payload_with_declared_padding[declared_size_pos : declared_size_pos + 4],
"little",
)
payload_with_declared_padding[
declared_size_pos : declared_size_pos + 4
] = (declared_size + 3).to_bytes(4, "little")
packed = bit_pack_after_eight_byte_header(
bytes(payload_with_declared_padding), shift
)
rows = parse_structured_records(packed, record_type)
self.assertEqual([row.item_id for row in rows], [expected_item_id])
def test_protocol_envelope_exposes_stream_and_segment_identity(self):
payload = enveloped_monopoly_payload("1003", page_index=2, query_high=False)

View file

@ -78,7 +78,10 @@ def main(argv: list[str] | None = None) -> int:
f"{changes['deletions']} deletions"
)
if args.apply:
print("applied: mappings/arcs.json, mappings/characters.json, mappings/items.json")
print(
"applied: mappings/arcs.json, mappings/characters.json, "
"mappings/items.json, mappings/achievements.json"
)
return 1 if args.check and result.change_count else 0