Expand achievement exports with progress and metadata

This commit is contained in:
Golumpa 2026-08-19 12:32:16 +01:00
parent 35918f24f1
commit 40b44521aa
17 changed files with 898 additions and 52 deletions

View file

@ -83,8 +83,8 @@ jobs:
git config user.name "github-actions[bot]" git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git checkout -B "$AUTOMATION_BRANCH" git checkout -B "$AUTOMATION_BRANCH"
git add mappings/arcs.json mappings/characters.json mappings/items.json git add mappings/arcs.json mappings/characters.json mappings/items.json mappings/achievements.json
git commit -m "chore: update reward mappings from NTE_Assets" git commit -m "chore: update mappings from NTE_Assets"
git push --force origin "HEAD:refs/heads/$AUTOMATION_BRANCH" git push --force origin "HEAD:refs/heads/$AUTOMATION_BRANCH"
- name: Open or update pull request - name: Open or update pull request

View file

@ -114,10 +114,17 @@ If a page response is missed, the exporter reports the missing page number while
Start the live exporter before launching the game. The completed achievement Start the live exporter before launching the game. The completed achievement
list is loaded during login, captured automatically, and written to list is loaded during login, captured automatically, and written to
`exports/Achievements_*.json` as an array of achievement IDs. Completed `exports/<user_uid>_Achievements_*.json` as a versioned achievement export with
PlayStation trophy entries are retained even though they are not shown in the account metadata, completion totals, and records grouped by achievement category.
in-game achievement list. The console reports the visible and PlayStation Both completed and incomplete achievements are included with their current
counts as soon as the list has been decoded. 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.
Completed PlayStation trophy entries are retained and identified separately
even though they are not shown in the in-game achievement list. The console
reports the visible and PlayStation counts as soon as the list has been decoded.
#### Linux/macOS #### Linux/macOS

View file

@ -1,11 +1,12 @@
# Reward mapping updates # Reward mapping updates
`tools/update_mappings.py` rebuilds `arcs.json`, `characters.json`, and `tools/update_mappings.py` rebuilds `arcs.json`, `characters.json`,
`items.json` directly from the current `items.json`, and `achievements.json` directly from the current
[`Waifus-Grace/NTE_Assets`](https://github.com/Waifus-Grace/NTE_Assets) [`Waifus-Grace/NTE_Assets`](https://github.com/Waifus-Grace/NTE_Assets)
tables. 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 provide localization namespace/key references and quality metadata; their
embedded `LocalizedString` values are deliberately ignored. embedded `LocalizedString` values are deliberately ignored.
@ -54,7 +55,7 @@ To use an existing local checkout without network access:
python tools/update_mappings.py --assets-root path\to\NTE_Assets 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, mapping files plus `mapping-update-report.json`. The report lists additions,
updates, and deletions by file and records SHA-256 hashes for every source updates, and deletions by file and records SHA-256 hashes for every source
table. table.
@ -71,6 +72,9 @@ from the committed reward mappings. It still writes the staged artifacts.
## Source rules ## 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 Arc in `DT_ForkItemData` is emitted to `arcs.json`.
- Every character in `DT_Character` is emitted to `characters.json`. - Every character in `DT_Character` is emitted to `characters.json`.
- Other pull rewards are selected by `GachaIllustrate` and resolved to the - Other pull rewards are selected by `GachaIllustrate` and resolved to the
@ -85,13 +89,13 @@ from the committed reward mappings. It still writes the staged artifacts.
currencies, are categorized as items. currencies, are categorized as items.
- Names are resolved strictly through `Localization/en/game.json`. A missing or - Names are resolved strictly through `Localization/en/game.json`. A missing or
ambiguous key fails the update instead of falling back to a DT value. ambiguous key fails the update instead of falling back to a DT value.
- Character-awakening illustration entries are not independent pull rewards - Character-awakening and character-vehicle illustration entries are not
and are excluded. independent pull rewards and are excluded.
- Orange, purple, and blue item qualities map to `S`, `A`, and `B`. - Orange, purple, and blue item qualities map to `S`, `A`, and `B`.
## UID compatibility boundary ## 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. reads or writes the permanent, limited, beginner, or Arc pool mapping files.
Banner IDs, timestamps, record ordering, format version, and all UID inputs are Banner IDs, timestamps, record ordering, format version, and all UID inputs are
therefore unchanged by mapping synchronization. therefore unchanged by mapping synchronization.

View file

@ -86,6 +86,8 @@ Decoded fields:
Arc/Gashapon history uses the same scheme at `ASCII * 2`. Arc/Gashapon history uses the same scheme at `ASCII * 2`.
- The decoder decodes the key to its id string and looks up display metadata - 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`. in `mappings/arcs.json`, `mappings/characters.json`, and `mappings/items.json`.
Achievement display metadata is generated separately in
`mappings/achievements.json`.
Unknown rewards still export their decoded id with empty name/rank. 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. 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

@ -1,5 +1,7 @@
import re import re
import struct import struct
from dataclasses import dataclass
from datetime import datetime, timezone
ACHIEVEMENT_RECORD_MARKER = b"AchievementRecord\0" ACHIEVEMENT_RECORD_MARKER = b"AchievementRecord\0"
ACHIEVEMENT_ID = re.compile(rb"([A-Za-z][A-Za-z0-9]*_[A-Za-z0-9_]+)\0") ACHIEVEMENT_ID = re.compile(rb"([A-Za-z][A-Za-z0-9]*_[A-Za-z0-9_]+)\0")
@ -7,17 +9,58 @@ RECORD_COUNT_OFFSET = 22
RECORDS_OFFSET = 38 RECORDS_OFFSET = 38
COMPLETION_TICKS_OFFSET = 8 COMPLETION_TICKS_OFFSET = 8
MAX_ACHIEVEMENTS = 1_000 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"
return "in_progress" if self.progress else "not_started"
@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]: 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): for block in _game_data_blocks(stream):
decoded = _decompress_lz4_block(block) decoded = _decompress_lz4_block(block)
marker = decoded.find(ACHIEVEMENT_RECORD_MARKER) marker = decoded.find(ACHIEVEMENT_RECORD_MARKER)
if marker < 0: if marker < 0:
continue continue
achievement_ids = _completed_achievement_ids(decoded, marker) records = _achievement_records(decoded, marker)
if achievement_ids: if records:
return achievement_ids return records
return [] return []
@ -34,7 +77,7 @@ def reassemble_tcp_segments(segments: list[tuple[int, bytes]]) -> bytes:
return bytes(output) return bytes(output)
def _completed_achievement_ids(decoded: bytes, marker: int) -> list[str]: def _achievement_records(decoded: bytes, marker: int) -> list[AchievementRecord]:
# The component header stores its record count before a fixed schema descriptor. # The component header stores its record count before a fixed schema descriptor.
if marker + RECORDS_OFFSET > len(decoded): if marker + RECORDS_OFFSET > len(decoded):
return [] return []
@ -44,15 +87,21 @@ def _completed_achievement_ids(decoded: bytes, marker: int) -> list[str]:
matches = list(ACHIEVEMENT_ID.finditer(decoded, marker + RECORDS_OFFSET)) matches = list(ACHIEVEMENT_ID.finditer(decoded, marker + RECORDS_OFFSET))
if len(matches) < count: if len(matches) < count:
return [] return []
completed = [] records = []
for match in matches[:count]: for match in matches[:count]:
# Each name is followed by progress, then a zero timestamp until completion. # Each name is followed by an unsigned progress value and completion ticks.
progress_offset = match.end()
ticks_offset = match.end() + COMPLETION_TICKS_OFFSET ticks_offset = match.end() + COMPLETION_TICKS_OFFSET
if ticks_offset + 8 > len(decoded): if ticks_offset + 8 > len(decoded):
return [] return []
if struct.unpack_from("<Q", decoded, ticks_offset)[0]: records.append(
completed.append(match.group(1).decode("ascii")) AchievementRecord(
return completed 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): def _game_data_blocks(stream: bytes):

View file

@ -12,6 +12,8 @@ from nte_history_exporter.constants import (
MYSTERY_BOX_BANNER_ID, MYSTERY_BOX_BANNER_ID,
) )
from nte_history_exporter.decoder.server_region import account_region_for_server 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( def build_export_json(
@ -78,6 +80,87 @@ def build_export_json(
return export 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,
"not_started_achievements": 0,
},
"playstation": {
"total_achievements": 0,
"completed_achievements": 0,
"in_progress_achievements": 0,
"not_started_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
elif achievement.progress:
summary["in_progress_achievements"] += 1
else:
summary["not_started_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]: def _record_for_export(row: dict[str, Any]) -> dict[str, Any]:
if row.get("pool_group_id") == ARC_BANNER_ID: if row.get("pool_group_id") == ARC_BANNER_ID:
return { return {

View file

@ -9,10 +9,16 @@ from pathlib import Path
from nte_history_exporter import console from nte_history_exporter import console
from nte_history_exporter.constants import POOL_META from nte_history_exporter.constants import POOL_META
from nte_history_exporter.decoder.achievement import extract_achievement_ids, reassemble_tcp_segments 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.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.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.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.diagnostics import new_diagnostics_path, write_capture_diagnostics
from nte_history_exporter.live_capture.session import LiveHistorySession, UdpPacket from nte_history_exporter.live_capture.session import LiveHistorySession, UdpPacket
@ -81,7 +87,7 @@ def run_live_capture(
reported_missing_pages: dict[str, tuple[int, ...]] = {} reported_missing_pages: dict[str, tuple[int, ...]] = {}
active_gap_notices: set[str] = set() active_gap_notices: set[str] = set()
tcp_segments: dict[tuple[str, int, str, int], list[tuple[int, bytes]]] = {} tcp_segments: dict[tuple[str, int, str, int], list[tuple[int, bytes]]] = {}
achievement_ids: list[str] = [] achievement_records = []
try: try:
with StopKeyMonitor() as stop_key: with StopKeyMonitor() as stop_key:
@ -91,7 +97,7 @@ def run_live_capture(
if packet is None: if packet is None:
continue continue
if ( if (
not achievement_ids not achievement_records
and packet.protocol == "tcp" and packet.protocol == "tcp"
and packet.payload and packet.payload
and packet.dst_ip == local_ip and packet.dst_ip == local_ip
@ -100,16 +106,17 @@ def run_live_capture(
tcp_segments.setdefault(flow, []).append( tcp_segments.setdefault(flow, []).append(
(packet.sequence_number, packet.payload) (packet.sequence_number, packet.payload)
) )
if not achievement_ids: if not achievement_records:
achievement_ids = extract_achievement_ids( achievement_records = extract_achievement_records(
reassemble_tcp_segments(tcp_segments[flow]) reassemble_tcp_segments(tcp_segments[flow])
) )
if achievement_ids: if achievement_records:
completed = [record for record in achievement_records if record.completed]
console.print_achievements_captured( console.print_achievements_captured(
len(achievement_ids), len(completed),
sum( sum(
value.lower().startswith("playstation_") record.achievement_id.lower().startswith("playstation_")
for value in achievement_ids for record in completed
), ),
) )
pair_count_before = len(session.pairs) pair_count_before = len(session.pairs)
@ -156,18 +163,26 @@ def run_live_capture(
exports = [] exports = []
achievement_path = None achievement_path = None
if achievement_ids:
achievement_path = _achievement_path()
achievement_path.write_text(
json.dumps(achievement_ids, ensure_ascii=False) + "\n", encoding="utf-8"
)
diagnostics_path = None diagnostics_path = None
if write_debug_csv: if write_debug_csv:
diagnostics_path = new_diagnostics_path() diagnostics_path = new_diagnostics_path()
write_capture_diagnostics(diagnostics_path, session.diagnostic_report()) write_capture_diagnostics(diagnostics_path, session.diagnostic_report())
resolved_user_uid = user_uid or session.user_uid 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() 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 resolved_server_id = session.server_id
if session.kinds_seen() and not resolved_server_id: if session.kinds_seen() and not resolved_server_id:
resolved_server_id = console.prompt_server_id() resolved_server_id = console.prompt_server_id()
@ -270,10 +285,11 @@ def run_live_capture(
} }
def _achievement_path() -> Path: def _achievement_path(user_uid: str | None = None) -> Path:
export_dir = Path("exports") export_dir = Path("exports")
export_dir.mkdir(parents=True, exist_ok=True) export_dir.mkdir(parents=True, exist_ok=True)
base = export_dir / f"Achievements_{datetime.now():%Y%m%d_%H%M%S}" 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") path = base.with_suffix(".json")
counter = 2 counter = 2
while path.exists(): while path.exists():

View file

@ -20,10 +20,11 @@ ASSET_TABLES = {
"appearances": "DataTable/Character/Appearance/DT_AppearanceData.json", "appearances": "DataTable/Character/Appearance/DT_AppearanceData.json",
"illustrations": "DataTable/Gacha/GachaIllustrate.json", "illustrations": "DataTable/Gacha/GachaIllustrate.json",
"mystery_box_pools": "DataTable/GashaponLottery/DT_GashaponLotteryGlobal.json", "mystery_box_pools": "DataTable/GashaponLottery/DT_GashaponLotteryGlobal.json",
"achievements": "DataTable/DT_AchievementConfigInfo.json",
"vehicle_inventory": "DataTable/Vehicle/DT_VehicleItemData.json", "vehicle_inventory": "DataTable/Vehicle/DT_VehicleItemData.json",
"localization": "Localization/en/game.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 = { RANK_BY_QUALITY = {
"EItemQuality::ITEM_QUALITY_ORANGE": "S", "EItemQuality::ITEM_QUALITY_ORANGE": "S",
"EItemQuality::ITEM_QUALITY_PURPLE": "A", "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]]]: def load_current_mappings(directory: Path) -> dict[str, dict[str, dict[str, Any]]]:
mappings = {} mappings = {}
for filename in REWARD_MAPPING_FILES: for filename in MAPPING_FILES:
path = directory / filename path = directory / filename
try: try:
value = json.loads(path.read_text(encoding="utf-8")) 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), "arcs.json": _build_primary_mapping(assets.tables["arcs"], "arc", translations),
"characters.json": _build_primary_mapping(assets.tables["characters"], "character", translations), "characters.json": _build_primary_mapping(assets.tables["characters"], "character", translations),
"items.json": _build_item_mapping(assets.tables, translations), "items.json": _build_item_mapping(assets.tables, translations),
"achievements.json": _build_achievement_mapping(
assets.tables["achievements"], translations
),
} }
validate_mappings(output) validate_mappings(output)
additions: dict[str, list[str]] = {} additions: dict[str, list[str]] = {}
updates: dict[str, list[str]] = {} updates: dict[str, list[str]] = {}
deletions: dict[str, list[str]] = {} deletions: dict[str, list[str]] = {}
for filename in REWARD_MAPPING_FILES: for filename in MAPPING_FILES:
old = current[filename] old = current[filename]
new = output[filename] new = output[filename]
additions[filename] = sorted(new.keys() - old.keys(), key=str.casefold) 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: def write_update(result: MappingUpdateResult, directory: Path) -> None:
directory.mkdir(parents=True, exist_ok=True) 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])) _atomic_write(directory / filename, _dump_mapping(result.mappings[filename]))
report_text = json.dumps(result.report, ensure_ascii=False, indent=2, sort_keys=True) + "\n" report_text = json.dumps(result.report, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
_atomic_write(directory / "mapping-update-report.json", report_text) _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) directory.mkdir(parents=True, exist_ok=True)
originals: dict[Path, bytes | None] = {} originals: dict[Path, bytes | None] = {}
written: list[Path] = [] written: list[Path] = []
for filename in REWARD_MAPPING_FILES: for filename in MAPPING_FILES:
path = directory / filename path = directory / filename
originals[path] = path.read_bytes() if path.exists() else None originals[path] = path.read_bytes() if path.exists() else None
try: try:
for filename in REWARD_MAPPING_FILES: for filename in MAPPING_FILES:
path = directory / filename path = directory / filename
_atomic_write(path, _dump_mapping(result.mappings[filename])) _atomic_write(path, _dump_mapping(result.mappings[filename]))
written.append(path) 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: def validate_mappings(mappings: dict[str, dict[str, dict[str, Any]]]) -> None:
if set(mappings) != set(REWARD_MAPPING_FILES): if set(mappings) != set(MAPPING_FILES):
raise MappingUpdateError(f"mapping set must be exactly {REWARD_MAPPING_FILES}") raise MappingUpdateError(f"mapping set must be exactly {MAPPING_FILES}")
locations: dict[str, str] = {} locations: dict[str, str] = {}
folded: dict[str, str] = {} folded: dict[str, str] = {}
for filename in REWARD_MAPPING_FILES: for filename in MAPPING_FILES:
entries = mappings[filename] entries = mappings[filename]
if not isinstance(entries, dict): if not isinstance(entries, dict):
raise MappingUpdateError(f"{filename} must contain an object") 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") raise MappingUpdateError(f"{filename}:{item_id}.type must be item or cosmetic")
if rank not in {"S", "A", "B"}: if rank not in {"S", "A", "B"}:
raise MappingUpdateError(f"{filename}:{item_id}.rank must be S, A, or 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]: def _extract_rows(document: Any, relative_path: str) -> dict[str, Any]:
@ -289,7 +320,12 @@ def _build_item_mapping(
candidate_ids = _reward_candidate_ids(tables) candidate_ids = _reward_candidate_ids(tables)
for candidate_id in candidate_ids: for candidate_id in candidate_ids:
folded = candidate_id.casefold() 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_")
or folded.startswith("fashion_vehicle_")
):
continue continue
sources = (inventory, capital, appearances, vehicle_inventory) sources = (inventory, capital, appearances, vehicle_inventory)
match = next((source.get(folded) for source in sources if folded in source), None) match = next((source.get(folded) for source in sources if folded in source), None)
@ -307,6 +343,62 @@ def _build_item_mapping(
return dict(sorted(result.items(), key=lambda pair: (pair[0].casefold(), pair[0]))) return dict(sorted(result.items(), key=lambda pair: (pair[0].casefold(), pair[0])))
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]: def _reward_candidate_ids(tables: dict[str, dict[str, Any]]) -> list[str]:
"""Return rewards used by ordinary Gacha and every Mystery Box rotation.""" """Return rewards used by ordinary Gacha and every Mystery Box rotation."""
candidates: dict[str, str] = { candidates: dict[str, str] = {

View file

@ -16,6 +16,10 @@ def load_mapping_file(filename: str) -> dict[str, Any]:
ARC_META: dict[str, dict[str, Any]] = load_mapping_file("arcs.json") ARC_META: dict[str, dict[str, Any]] = load_mapping_file("arcs.json")
CHARACTERS: dict[str, dict[str, Any]] = load_mapping_file("characters.json") CHARACTERS: dict[str, dict[str, Any]] = load_mapping_file("characters.json")
ITEMS: dict[str, dict[str, Any]] = load_mapping_file("items.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 # 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 # (ASCII*4 with carry, see decoder.protocol.decode_reward_key), so all reward

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,4 +1,8 @@
{ {
"ST_Achievement": {
"battle_25_Title": "Devil Within II",
"battle_25_Content": "Trigger Hexed ×50."
},
"ST_Player": { "ST_Player": {
"character_1003": "Old Character Renamed", "character_1003": "Old Character Renamed",
"character_1099": "New Character" "character_1099": "New Character"

View file

@ -4,6 +4,7 @@ import unittest
from nte_history_exporter.decoder.achievement import ( from nte_history_exporter.decoder.achievement import (
ACHIEVEMENT_RECORD_MARKER, ACHIEVEMENT_RECORD_MARKER,
extract_achievement_ids, extract_achievement_ids,
extract_achievement_records,
reassemble_tcp_segments, reassemble_tcp_segments,
) )
@ -22,6 +23,13 @@ class AchievementPacketTests(unittest.TestCase):
self.assertEqual(extract_achievement_ids(stream), ["Battle_30"]) self.assertEqual(extract_achievement_ids(stream), ["Battle_30"])
self.assertEqual(extract_achievement_ids(b"\x01\0\0\0\0"), []) 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, "not_started")
def test_reassembles_tcp_data(self): def test_reassembles_tcp_data(self):
self.assertEqual( self.assertEqual(
reassemble_tcp_segments([(104, b"bar"), (100, b"foobar"), (100, b"foobar")]), reassemble_tcp_segments([(104, b"bar"), (100, b"foobar"), (100, b"foobar")]),

View file

@ -1,5 +1,8 @@
from tests.support import * # noqa: F401,F403 from tests.support import * # noqa: F401,F403
from nte_history_exporter import __version__ from nte_history_exporter import __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): class ExportContractTests(unittest.TestCase):
@ -117,6 +120,83 @@ class ExportContractTests(unittest.TestCase):
r"^218216016349_Limited_\d{8}_\d{6}(?:_\d+)?\.json$", 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,
"not_started_achievements": 0,
},
"playstation": {
"total_achievements": 1,
"completed_achievements": 0,
"in_progress_achievements": 0,
"not_started_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): def test_extracts_user_uid_from_record_context(self):
payload = ( payload = (
b"\x00" * 24 b"\x00" * 24

View file

@ -36,6 +36,7 @@ def sample_current():
"DiceNormal": {"type": "item", "name": "Fabricated Dice", "rank": "S"}, "DiceNormal": {"type": "item", "name": "Fabricated Dice", "rank": "S"},
"OldTicket": {"type": "item", "name": "Retired Ticket", "rank": "B"}, "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("fork_retired", result.mappings["arcs.json"])
self.assertNotIn("OldTicket", result.mappings["items.json"]) self.assertNotIn("OldTicket", result.mappings["items.json"])
self.assertNotEqual(result.mappings["characters.json"]["1003"], current["characters.json"]["1003"]) 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": 5, "updates": 2, "deletions": 3})
self.assertIs(result.report["safety"]["authoritative_snapshot"], True) self.assertIs(result.report["safety"]["authoritative_snapshot"], True)
self.assertIs(result.report["safety"]["deletions_allowed"], True) self.assertIs(result.report["safety"]["deletions_allowed"], True)
self.assertIs(result.report["safety"]["uid_inputs_touched"], False) self.assertIs(result.report["safety"]["uid_inputs_touched"], False)
self.assertIs(result.report["safety"]["pool_mappings_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): def test_items_use_illustrations_as_filter_and_asset_tables_as_authority(self):
_current, result = build_sample_update() _current, result = build_sample_update()
@ -71,6 +83,7 @@ class MappingUpdateTests(unittest.TestCase):
self.assertNotIn("DIceNormal", result.mappings["items.json"]) self.assertNotIn("DIceNormal", result.mappings["items.json"])
self.assertNotIn("UnusedItem", 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.assertFalse(any(key.startswith("Characterawaken_") for key in result.mappings["items.json"]))
self.assertFalse(any(key.startswith("Fashion_vehicle_") for key in result.mappings["items.json"]))
def test_future_mystery_box_pool_is_discovered_without_hard_coded_event_id(self): def test_future_mystery_box_pool_is_discovered_without_hard_coded_event_id(self):
assets = load_assets(assets_root=SAMPLE_ASSETS) assets = load_assets(assets_root=SAMPLE_ASSETS)

View file

@ -1,4 +1,5 @@
from tests.support import * # noqa: F401,F403 from tests.support import * # noqa: F401,F403
from nte_history_exporter.mappings import ACHIEVEMENTS
class MappingAndUpdateTests(unittest.TestCase): class MappingAndUpdateTests(unittest.TestCase):
@ -79,6 +80,14 @@ class MappingAndUpdateTests(unittest.TestCase):
self.assertIn("name", info) self.assertIn("name", info)
self.assertIn(info.get("rank"), ("S", "A")) 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) self.assertTrue(ITEMS)
for item_id, info in ITEMS.items(): for item_id, info in ITEMS.items():
with self.subTest(item_id=item_id): with self.subTest(item_id=item_id):

View file

@ -78,7 +78,10 @@ def main(argv: list[str] | None = None) -> int:
f"{changes['deletions']} deletions" f"{changes['deletions']} deletions"
) )
if args.apply: 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 return 1 if args.check and result.change_count else 0