Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion agent_docs/map-travel.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ nothing. A script returns effects that the client actor sends:
| `foxLaidy_map` | Fox Ridge's (`222010300`) `west00`: to Tiger Ridge (`222010200` `east00`), or with 3647 taken and an Old Fox's Tail (`4031793`), into the Gloomy Forest (`922220000` `east00`). Cosmic also starts quest 23647, which isn't in `Quest.nx` |
| `move_elin`, `minar_job4`, `gryphius`, `mayong`, `Pianus`, `hontale_morph2`, `eliza_Garden`, `catPriest_map`, `enterInfo` | Ways through (`PLAIN_WARPS`): the Time Control Room's `in01` into Ellin Forest (`300000100` `out00`, with a pink line), Leafre's to the Forest of the Priest, Griffey's and Manon's forests (`240010501`, `240020101`, `240020401` `out00`), Aqua Road's to the Cave of Pianus (`230040420` `out00`), the Horntail cave's back to the Peak of the Big Nest (`240040600` portal 4), Eliza's Garden (`920020000` portal 2), the Forest of the Ghost Priest (`925000000` portal 2), Lith Harbor's Info Shop (`104000004` portal 1). Cosmic's Aran-quest monster in the Info Shop (21733) isn't in |
| `enterGym`, `rienCaveEnter` | The second Dangerous Forest's (`140010100`) way to Puo's training grounds (`914010000`/`914010100`/`914010200` portal 1 while 21701/21702/21703 is taken), and the Mirror Cave's (`140030000` portal 1, while 21201 or 21302 is taken); else a pink line |
| `dragonNest`, `go_secretroom`, `tristanEnter`, `q2073`, `enterDollWay`, `ariant_Agit`, `s4resur_enter`, `s4rush` | Doors a quest opens, else a pink line: the Nine Spirit's Nest (`240040612` `out00`, 3706 completed; Cosmic's egg event isn't in), the Mushroom Castle's Security Room (`106021001` portal 1, 2335 completed, or taken with its key `4032405`, which completes it, gives 5000 EXP and takes the key, once 2335 is in the quest catalog), Tristan's Resting Place (`105100101` `in00`, 2238 completed), Utah's Pig Farm (`900000000`, 2073 taken), the Cave of Evil Eye III (`105070300` portal 3, 20730 or 21734 completed) or the Puppeteer's Secret Passage (`910510100`, 21734 taken), the Sand Bandits' house (`260000201` portal 1, 3928, 3931 and 3934 completed), the Forgotten Darkness (`922020000`, 6134 taken), the Secret Shrine (`910500100`, 6110 taken). Several of these quests aren't supported yet (2238, 2335, 3706, 21201, 21302), so their doors stay shut for now |
| `dragonNest`, `go_secretroom`, `tristanEnter`, `q2073`, `enterDollWay`, `ariant_Agit`, `s4resur_enter`, `s4rush` | Doors a quest opens, else a pink line: the Nine Spirit's Nest (`240040612` `out00`, 3706 completed; Cosmic's egg event isn't in), the Mushroom Castle's Security Room (`106021001` portal 1, 2335 completed, or taken with its key `4032405`, which completes it, gives 5000 EXP and takes the key, once 2335 is in the quest catalog), Tristan's Resting Place (`105100101` `in00`, 2238 completed), Utah's Pig Farm (`900000000`, 2073 taken), the Cave of Evil Eye III (`105070300` portal 3, 20730 or 21734 completed) or the Puppeteer's Secret Passage (`910510100`, 21734 taken), the Sand Bandits' house (`260000201` portal 1, 3928, 3931 and 3934 completed), the Forgotten Darkness (`922020000`, 6134 taken), the Secret Shrine (`910500100`, 6110 taken). Several of these quests aren't supported yet (2238, 2335, 21201, 21302), so their doors stay shut for now |
| `tutorialNPC`, `TD_chat_enter` | Open an NPC's conversation without a click (`Effect::OpenNpc`), unless one is open already: the `tutorial` portal in each job instructor's room (`101000003`, `100000201`, `102000003`, `103000003`, `120000101`) has a beginner up to level 10 hear the instructor (`1032001`, `1012100`, `1022000`, `1052001`, `1090000`); Leafre's `TD_neo` (`240070000`) opens the Time Gate (`2083006`) |
| `enterDollcave` | The Cave of Evil Eye III's (`105070300`) `in00`: with 20730 or 21734 completed, portal sound and back to `105040201` `in00`; else nothing. Not yet: Cosmic's password wall (`1063011`, `PupeteerPassword`), and `thief_in1`'s (`ThiefPassword`) |
| `s4firehawk`, `s4iceeagle`, `s4tornado_enter`, `s4resurrection`, `s4nest`, `s4hitman`, `s4berserk`, `s4berserk_move` | The fourth job's solo rooms (`scripts::SOLO_MAPS`), portal 0 unless said, one player at a time (a pink line while someone is there), each starting afresh when left: the Fire Hawk's nest (`211042200` `in00` to `921100200`, 6240 taken) and the Ice Eagle's (`211040700` `in00` to `921100210`, 6242 taken), else a pink line; the Tornado room (`220010001` `pt00` to `922020200`, 6230 or 6231 taken, or holding `4001110`), else nothing; the resurrection mound (`230040300` `in01` to `923000100`, holding a Cold Flame `4001108`, quest or not, as in Cosmic) and the nest of the eggs (`240010700` `pt00` to `924000100`, 6241 or 6243 taken), else a pink line. Their reactors take items (`combat.md`). The Hitman's hideout (`101030104` `pt00` to `910200000`, 6201 taken), else a pink line; its boxes lead up its floors. The Berserk room (`105090800` `in00` to `910500200` `out01`, 6153 taken and holding its key `4031475`), else a pink line; its way up to the statues, `s4berserk_move` (`in00` to `out00` within the map), stays shut with a pink line while a monster is left (`Effect::RequireNoMobs`, asking the field for its live mob count) |
Expand Down
12 changes: 11 additions & 1 deletion agent_docs/quests.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ itself ("Etc item inventory is full."). `startscript`/
`endscript` are allowed only for scripts the server implements
(`game_data::quests::SCRIPTS`: `q1021s`, `q1021e`, Bartos's secret
`q4647e`, Garnox's pet evolutions `q8185e`, `q8189e` and `q4659e` (see
[Pets](./pets.md)), Meren's Maker lessons and Dyle's informants below); those steps go through
[Pets](./pets.md)), Meren's Maker lessons, Dyle's informants and the baby dragon below); those steps go through
QUEST_ACTION 4/5 and the server drives the dialogue (see
[NPC dialogue](./npc-dialogue.md)). `money` pays (or, if negative, takes)
mesos on completion; a quest that takes mesos needs them held, or the
Expand Down Expand Up @@ -260,6 +260,16 @@ list. `game_data::quests::FINISHED_BY_START_SCRIPT` lets these four through
without a completion NPC. The can's and the informants' lines are
unsourced and stay placeholder (`dyle.*`).

"The Remnants of Horned Tail" (3714) works the same way. Nine Spirit's Baby
Dragon (2081011), in the nest past Leafre's `dragonNest` portal (240040612,
open once 3706 is done), has a start script (`q3714s`) and no end. It
takes a Nine Spirit's Egg (4001094) and gives a Dragon Stone (2041200) and
Cosmic's 42,000 EXP, completing the quest at once. The quest repeats
(`interval 0`), once for each egg, but not while a Dragon Stone is held,
as in Cosmic. Its lines are placeholders (`nine_spirit.baby.*`). 3706's
`Act/1/map` and `message` (and 2282-2284's `Act/0/message`) are read by
neither v83's client nor Cosmic, so they are ignored (`INERT_ACTIONS`).

The `game-data` asset test compares the server's result with the committed
list `game-data/supported_quests.txt`. HeavenClient doesn't classify quests:
the build copies that list next to the executable, and the client offers
Expand Down
3 changes: 3 additions & 0 deletions game-data/placeholder_text.toml
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,9 @@
"dyle.informant.1" = "[placeholder] Shumi tells you what she knows about Dyle."
"dyle.informant.2" = "[placeholder] Nella tells you what she knows about Dyle."
"dyle.informant.3" = "[placeholder] Jake says Dyle was once a Ligator that grew from Growth Powder."
"nine_spirit.baby.hatched" = "[placeholder] The egg hatches on the nest. The baby dragon coughs up a Dragon Stone and asks you to take it away."
"nine_spirit.baby.stone_held" = "[placeholder] The Dragon Stone in your bag glows brighter here, and the baby dragon glares at it."
"nine_spirit.baby.full" = "[placeholder] You need a free USE slot for the Dragon Stone."

"family.entitlement.0.name" = "[placeholder] Go to a relative"
"family.entitlement.0.description" = "[placeholder] Go to a family member of your choice."
Expand Down
3 changes: 3 additions & 0 deletions game-data/server_content.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1441,6 +1441,9 @@ unsourced = [
"dyle.informant.1",
"dyle.informant.2",
"dyle.informant.3",
"nine_spirit.baby.hatched",
"nine_spirit.baby.stone_held",
"nine_spirit.baby.full",
]

[text."glTutoMsg0.message"]
Expand Down
17 changes: 10 additions & 7 deletions game-data/src/quests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,14 @@ pub const INERT_END_REQUIREMENTS: &[&str] = &["start", "end"];
/// Quest scripts (`startscript`/`endscript`) the server implements. A quest
/// naming any other script is unsupported.
pub const SCRIPTS: &[&str] = &[
"q1021s", "q1021e", "q2214e", "q2215e", "q2216s", "q2217s", "q2218s", "q2219s", "q4647e",
"q4659e", "q6030e", "q6031e", "q6032e", "q6033e", "q6036e", "q8185e", "q8189e",
"q1021s", "q1021e", "q2214e", "q2215e", "q2216s", "q2217s", "q2218s", "q2219s", "q3714s",
"q4647e", "q4659e", "q6030e", "q6031e", "q6032e", "q6033e", "q6036e", "q8185e", "q8189e",
];
/// Quests with nothing to complete them by (an empty `Check/1`) whose start
/// script completes them as it starts them: JM's informants (2216-2219),
/// each heard from once (`Effect::FinishQuest`).
pub const FINISHED_BY_START_SCRIPT: &[i16] = &[2216, 2217, 2218, 2219];
/// script completes them as it starts them (`Effect::FinishQuest`): JM's
/// informants (2216-2219), each heard from once, and the Remnants of Horned
/// Tail (3714), once for each Nine Spirit's Egg.
pub const FINISHED_BY_START_SCRIPT: &[i16] = &[2216, 2217, 2218, 2219, 3714];
/// Meren's Maker classes (quests 6030-6032) and the quest they make up
/// (6029) share 6029's quest-info string, one digit per class: `Act/0 info`
/// starts it at "000", a class starts only while its own digit is 0
Expand Down Expand Up @@ -133,9 +134,11 @@ pub const END_ACTIONS: &[&str] = &[
/// `quest`, a date or an `interval`). v83's client reads only `item`,
/// `skill`, `exp`, `money`, `pop`, `pettameness` and `nextQuest` under
/// `Act`, and Cosmic applies none of these, so they do nothing here either;
/// the `Check` copy is what counts.
/// the `Check` copy is what counts. `map` (a list of maps, only 3706) and
/// `message` (an untranslated line: 3706, 2282-2284) are read by neither
/// the client (report b200) nor Cosmic, so they are ignored too.
pub const INERT_ACTIONS: &[&str] = &[
"npc", "lvmin", "lvmax", "job", "quest", "interval", "start", "end",
"npc", "lvmin", "lvmax", "job", "quest", "interval", "start", "end", "map", "message",
];
/// Keys of an `Act/<n>/item/<k>` entry the server applies.
/// `var` (1 or 2, on equipment only) is read by neither v83's client nor
Expand Down
15 changes: 13 additions & 2 deletions game-data/src/server_content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -757,8 +757,8 @@ pub mod keys {
pub const MAKER_GOLD_ANVIL: [&str; 2] = ["maker.gold_anvil.0", "maker.gold_anvil.1"];

/// JM's list of informants (Kerning City, quests 2214-2219): the Knocked
/// Trash Can in the Hut in the Swamp (NPC 1052108) holds it from 5 to 8 p.m. (`q2214e`, and
/// `q2215e` for a lost list with its fee), then each informant tells
/// Trash Can in the Hut in the Swamp (NPC 1052108) holds it from 5 to 8
/// p.m. (`q2214e`, and `q2215e` for a lost list with its fee), then each informant tells
/// what they know about Dyle (`q2216s` Mr. Pickall, `q2217s` Shumi,
/// `q2218s` Nella, `q2219s` Jake). No licensed script has their text,
/// so these keep their placeholders.
Expand All @@ -774,6 +774,14 @@ pub mod keys {
"dyle.informant.3",
];

/// The Remnants of Horned Tail (quest 3714, `q3714s`): Nine Spirit's
/// Baby Dragon (NPC 2081011) in its nest hatches from an egg and gives a
/// Dragon Stone, unless one is still held. No licensed script has the
/// text, so these keep their placeholders.
pub const NINE_SPIRIT_HATCHED: &str = "nine_spirit.baby.hatched";
pub const NINE_SPIRIT_STONE_HELD: &str = "nine_spirit.baby.stone_held";
pub const NINE_SPIRIT_FULL: &str = "nine_spirit.baby.full";

/// Trainer Bartos's Follow the Lead (quests 4646 and 4647): the hidden
/// note of his test (NPC 9102100, `multipet_success`), then his end
/// script `q4647e`, which takes a Pet Snack.
Expand Down Expand Up @@ -2975,6 +2983,9 @@ mod tests {
.chain(&keys::MAKER_GOLD_ANVIL)
.chain(&keys::DYLE_INFORMANTS)
.chain(&[
keys::NINE_SPIRIT_HATCHED,
keys::NINE_SPIRIT_STONE_HELD,
keys::NINE_SPIRIT_FULL,
keys::DYLE_TRASH_CAN_NOT_YET,
keys::DYLE_TRASH_CAN_FULL,
keys::DYLE_TRASH_CAN_FOUND,
Expand Down
2 changes: 2 additions & 0 deletions game-data/supported_quests.txt
Original file line number Diff line number Diff line change
Expand Up @@ -715,13 +715,15 @@
3703
3704
3705
3706
3707
3708
3709
3710
3711
3712
3713
3714
3715
3716
3717
Expand Down
73 changes: 73 additions & 0 deletions integration-harness/tests/nine_spirit_egg.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//! The Remnants of Horned Tail (3714): in Nine Spirit's nest, each egg
//! (4001094) hatches into a Dragon Stone (2041200) and 42,000 EXP through
//! the baby dragon's start script, which completes the quest at once. It
//! repeats for another egg, but not while a Dragon Stone is held.

use integration_harness::connection::MapleTestConnection;
use integration_harness::database::{grant_stack, replace_item, set_job, set_quest};
use integration_harness::hunting::walk_to;
use integration_harness::preconditions::load_harness_config_or_fail;
use integration_harness::quests::{answer, build_quest_action, SCRIPT_START};
use integration_harness::trace::{step, warp, Trace};
use integration_harness::{login_to_world_session, relogin};
use std::time::Duration;

const QUEST: i16 = 3714;
const BABY_DRAGON: i32 = 2_081_011;
const NEST: i32 = 240_040_612;
const EGG: i32 = 4_001_094;
const DRAGON_STONE: i32 = 2_041_200;
const USE: u8 = 2;
const ETC: u8 = 4;

async fn hatch(c: &mut MapleTestConnection, m: &str) -> Trace {
let mut trace = Trace::default();
assert_eq!(warp(c, NEST, "", &mut trace).await, NEST);
step(c, m, walk_to(-8, -227)).await;
let t = step(c, m, build_quest_action(SCRIPT_START, QUEST, BABY_DRAGON)).await;
assert_eq!(t.prompts, [(BABY_DRAGON, 0)], "the baby dragon speaks");
step(c, m, answer(0, 1, None)).await
}

#[tokio::test]
async fn each_egg_hatches_into_a_dragon_stone() {
let config = load_harness_config_or_fail().await;
let m = config.character_name.clone();
let id = login_to_world_session(&config)
.await
.expect("first login failed")
.character_id;
tokio::time::sleep(Duration::from_millis(500)).await;
set_job(id, 110, 80, 100).await.expect("a level 80 fighter");
set_quest(id, 3706, 2, "").await.expect("the nest open");
grant_stack(id, ETC as i16, 1, EGG, 2)
.await
.expect("two eggs");

let mut session = relogin(&config).await;
let c = &mut session.connection;
let t = hatch(c, &m).await;
assert_eq!(t.progress, [(QUEST, 2)], "done at once");
assert_eq!(t.gains, [42_000]);
assert_eq!(t.quantities, [(ETC, 1, 1)], "one egg taken");
assert_eq!(t.added.len(), 1);
let (inventory, slot, item) = t.added[0];
assert_eq!((inventory, item), (USE, DRAGON_STONE));
// Not while the stone is held.
let t = hatch(c, &m).await;
assert!(t.progress.is_empty() && t.gains.is_empty() && t.quantities.is_empty());
drop(session);
tokio::time::sleep(Duration::from_millis(500)).await;

// With the stone gone, the second egg hatches too.
replace_item(id, USE as i16, slot, 2_000_000)
.await
.expect("the stone gone");
let mut session = relogin(&config).await;
assert_eq!(session.quests.get(&QUEST), Some(&2));
let c = &mut session.connection;
let t = hatch(c, &m).await;
assert_eq!(t.progress, [(QUEST, 2)], "{QUEST} repeats");
assert_eq!(t.gains, [42_000]);
assert_eq!(t.removed, [(ETC, 1)], "the last egg taken");
}
78 changes: 78 additions & 0 deletions net/src/npc_scripts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2340,6 +2340,7 @@ impl NpcScript {
"q2217s" => informant(player, 1),
"q2218s" => informant(player, 2),
"q2219s" => informant(player, 3),
"q3714s" => Some(baby_dragon(player)),
"q6030e" => Some(maker_class(CARSON, keys::MAKER_ALCHEMY, 6030)),
"q6031e" => Some(maker_class(HUGHES, keys::MAKER_SCIENCE, 6031)),
"q6032e" => Some(maker_class(MEREN, keys::MAKER_PRACTICE, 6032)),
Expand Down Expand Up @@ -2761,6 +2762,42 @@ fn trash_can(player: &Player, lost: bool, hour: u8) -> NpcScript {
/// `q2216s`-`q2219s`: the informant at `index` tells what they know about
/// Dyle, which completes the quest as it starts, for 7,000 EXP (Cosmic's;
/// the data has none). The last of the four heard takes back JM's list.
/// Nine Spirit's Baby Dragon, in its nest (240040612).
const BABY_DRAGON: i32 = 2081011;
const NINE_SPIRIT_EGG: i32 = 4001094;
const DRAGON_STONE: i32 = 2041200;

/// `q3714s`, the Remnants of Horned Tail: the egg from Horned Tail hatches
/// on the nest and the baby dragon gives up a Dragon Stone, with Cosmic's
/// 42,000 EXP (the data has no rewards). The quest repeats (`interval 0`),
/// once for each egg, but not while a Dragon Stone is held, as Cosmic.
fn baby_dragon(player: &Player) -> NpcScript {
if player.inventory.count(DRAGON_STONE) > 0 {
return NpcScript::new(
BABY_DRAGON,
vec![Page::Say(text(keys::NINE_SPIRIT_STONE_HELD))],
);
}
let mut script = NpcScript::new(
BABY_DRAGON,
vec![Page::Say(text(keys::NINE_SPIRIT_HATCHED))],
);
script.on_end = vec![
Effect::FinishQuest(3714),
Effect::TakeItem {
item_id: NINE_SPIRIT_EGG,
quantity: 1,
},
Effect::GiveItem {
item_id: DRAGON_STONE,
quantity: 1,
},
Effect::GainExp(42_000),
];
script.inventory_full = Some(text(keys::NINE_SPIRIT_FULL));
script
}

fn informant(player: &Player, index: usize) -> Option<NpcScript> {
let (quest, npc) = INFORMANTS[index];
let mut script = NpcScript::new(npc, vec![Page::Say(text(keys::DYLE_INFORMANTS[index]))]);
Expand Down Expand Up @@ -9923,6 +9960,47 @@ mod tests {
assert_eq!(read_through(early), (1052006, heard(2219)));
}

#[test]
fn the_baby_dragon_trades_each_egg_for_a_dragon_stone() {
let mut player = with_quests(&[(3706, true)]);
player.inventory.insert(
game::InventoryType::Etc,
1,
game::Item::stack(NINE_SPIRIT_EGG, 2),
);
assert!(NpcScript::for_quest("q3714s", &player)
.unwrap()
.inventory_full()
.is_some());
let hatched = NpcScript::for_quest("q3714s", &player).unwrap();
assert_eq!(
read_through(hatched),
(
BABY_DRAGON,
vec![
Effect::FinishQuest(3714),
Effect::TakeItem {
item_id: NINE_SPIRIT_EGG,
quantity: 1
},
Effect::GiveItem {
item_id: DRAGON_STONE,
quantity: 1
},
Effect::GainExp(42_000),
]
)
);
// Not while a stone is held.
player.inventory.insert(
game::InventoryType::Use,
1,
game::Item::stack(DRAGON_STONE, 1),
);
let held = NpcScript::for_quest("q3714s", &player).unwrap();
assert_eq!(read_through(held), (BABY_DRAGON, vec![]));
}

#[test]
fn a_maker_class_ends_with_its_teacher() {
for (script, npc, quest) in [
Expand Down
Loading