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
17 changes: 16 additions & 1 deletion agent_docs/party-quests.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ Haze is played through
Pirate Ship is played through ([the Pirate Ship](#the-pirate-ship)).
Magatia's Romeo and Juliet party quest has its entry, its way out, its
end rules and its first four stages; its other stages come in later slices
([Romeo and Juliet](#romeo-and-juliet)).
([Romeo and Juliet](#romeo-and-juliet)). Jr. Balrog's room, for quest 2245,
is taken alone ([Jr. Balrog's room](#jr-balrogs-room)).

No event NPC offers Cosmic's "enable/disable Party Search" line. That
opt-out is Cosmic's own: in v83 the opt-out is the client's Party Search
Expand Down Expand Up @@ -1369,6 +1370,20 @@ our Map.nx never names) leads back to the side's room. The event's items
`onFirstUserEnter` scripts (`StageMsg_romio`, `StageMsg_juliet`) do
nothing yet. Its lines are placeholders (`magatia.*`).

## Jr. Balrog's room

For "To Tristan's Tomb" (2245; Cosmic `BalrogQuest`). Mu Young's start
script `q2245s` (see [quests](./quests.md)) takes a player on 2238, at
level 50 and up, alone into their own copy of 910520000 for ten minutes
(`events::TRISTAN`, `EventKind::Tristan`), party or not. Only one may be in
it on a channel at a time (`runs: 1`, Cosmic's "someone in this map"
refusal): another is told so (`tristan.room_taken`, a placeholder) and
nothing is applied. The room's `life` holds the unknown Jr. Balrog
(9300326), spawned once. Its fall brings out Tristan's Spirit (1061015) at
(0, 115) (`FieldActor::tristan_balrog_falls`), where 2238 ends. The event
is never won: it ends when the player leaves by `out00` or the time runs
out, back to Mu Young's map 105100100, the room's `forcedReturn`.

## NPCs

| NPC | Where | Script |
Expand Down
11 changes: 10 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, Dyle's informants, Reef, the baby dragon and Captain Al's family lessons below); those steps go through
[Pets](./pets.md)), Meren's Maker lessons, Dyle's informants, Reef, Mu Young, the baby dragon and Captain Al's family lessons 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 @@ -266,6 +266,15 @@ Reef (1032108, in 910100001) thanks the player through a start script
(`Act/1 pop`, through `Effect::GainFame`, since `FinishQuest` gives no
`Act/1` rewards). Its line is a placeholder (`reef.thanks`).

Mu Young (1061014, in 105100100) has two start scripts, both silent as in
Cosmic. `q2238s` starts "Who is the Owner of the Mysterious Note?" (2238,
once 2237 is done). `q2245s` ("To Tristan's Tomb", for a player on 2238)
completes 2245 at once, so that its `interval 0` lets it be taken again,
and takes the player alone into the unknown Jr. Balrog's room
(`EventKind::Tristan`, see [party quests](./party-quests.md#jr-balrogs-room)).
Jr. Balrog's fall brings out Tristan's Spirit (1061015), where 2238 ends
for the data's 50,000 EXP; with it done, the `tristanEnter` portal opens.

"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
Expand Down
1 change: 1 addition & 0 deletions game-data/placeholder_text.toml
Original file line number Diff line number Diff line change
Expand Up @@ -772,6 +772,7 @@
"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."
"reef.thanks" = "[placeholder] Reef thanks you for beating Faust. His spirit can rest at last."
"tristan.room_taken" = "[placeholder] Someone is already in that room. Come back later."
"family.captain_al.junior.done" = "[placeholder] Captain Al is pleased you have taken a Junior, and teaches you how a Senior earns Rep."
"family.captain_al.junior.not_yet" = "[placeholder] Captain Al says you have no Junior yet. Add one from the Family window."
"family.captain_al.rep.done" = "[placeholder] Captain Al sees you have earned 1,000 Rep, and praises how you look after your Juniors."
Expand Down
1 change: 1 addition & 0 deletions game-data/server_content.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1445,6 +1445,7 @@ unsourced = [
"nine_spirit.baby.stone_held",
"nine_spirit.baby.full",
"reef.thanks",
"tristan.room_taken",
"family.captain_al.junior.done",
"family.captain_al.junior.not_yet",
"family.captain_al.rep.done",
Expand Down
12 changes: 6 additions & 6 deletions game-data/src/quests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,16 @@ 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", "q2228s", "q2232e", "q2233e", "q2234e", "q2216s",
"q2217s", "q2218s", "q2219s", "q3714s", "q4647e", "q4659e", "q6030e", "q6031e", "q6032e",
"q6033e", "q6036e", "q8185e", "q8189e",
"q1021s", "q1021e", "q2214e", "q2215e", "q2228s", "q2238s", "q2245s", "q2232e", "q2233e",
"q2234e", "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 (`Effect::FinishQuest`): JM's
/// informants (2216-2219), each heard from once, Reef's thanks for Faust
/// (2228), 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, 2228, 3714];
/// (2228), Mu Young's way to Tristan's Tomb (2245), each time it opens, 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, 2228, 2245, 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
6 changes: 6 additions & 0 deletions game-data/src/server_content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,11 @@ pub mod keys {
/// the player for beating Faust. No licensed script has the text.
pub const REEF_THANKS: &str = "reef.thanks";

/// To Tristan's Tomb (quest 2245, `q2245s`): Mu Young (NPC 1061014)
/// turns a player away while another is in the Jr. Balrog's room. No
/// licensed script has the text.
pub const TRISTAN_ROOM_TAKEN: &str = "tristan.room_taken";

/// Captain Al's lessons in leading a family (quests 2232-2234), each
/// ended by his script (NPC 1002103, `q2232e`-`q2234e`): done, then not
/// yet, for a junior taken, 1,000 Rep earned, and 2,000 earned with the
Expand Down Expand Up @@ -3009,6 +3014,7 @@ mod tests {
.chain(&keys::CAPTAIN_AL_ENTITLEMENT)
.chain(&[
keys::REEF_THANKS,
keys::TRISTAN_ROOM_TAKEN,
keys::NINE_SPIRIT_HATCHED,
keys::NINE_SPIRIT_STONE_HELD,
keys::NINE_SPIRIT_FULL,
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 @@ -264,11 +264,13 @@
2234
2235
2237
2238
2239
2240
2241
2242
2243
2245
2246
2247
2248
Expand Down
140 changes: 140 additions & 0 deletions integration-harness/tests/tristan_tomb.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
//! Who is the Owner of the Mysterious Note? (2238) and To Tristan's Tomb
//! (2245): Mu Young's start scripts start 2238, then send the player alone
//! into the unknown Jr. Balrog's room for ten minutes, completing 2245 so
//! it may be taken again. Its fall brings out Tristan's Spirit, where 2238
//! ends. One player at a time may be in the room on a channel. The harness
//! world runs events 60 times as fast (`RUSTMS_EVENT_SCALE`), so the ten
//! minutes are ten seconds.

use integration_harness::connection::MapleTestConnection;
use integration_harness::database::{replace_item, set_job, set_quest, set_str, set_weapon_attack};
use integration_harness::hunting::{attack, walk_to};
use integration_harness::preconditions::load_harness_config_or_fail;
use integration_harness::quests::{build_quest_action, build_quest_completion, SCRIPT_START};
use integration_harness::trace::{read_for, send, step, until, until_within, warp, Trace};
use integration_harness::{login_to_world_session, relogin, HarnessConfig, WorldSession};
use net::events::{MU_YOUNG_MAP, TRISTANS_SPIRIT, TRISTAN_BALROG, TRISTAN_ROOM};
use net::packet::op::RecvOpcode;
use packet::io::write::PktWrite;
use packet::Packet;
use std::time::Duration;

const MU_YOUNG: i32 = 1_061_014;

/// A level 55 fighter, armed to fell Jr. Balrog, with 2237 done, waiting
/// at Mu Young.
async fn at_mu_young(config: &HarnessConfig) -> WorldSession {
let id = login_to_world_session(config)
.await
.expect("first login")
.character_id;
tokio::time::sleep(Duration::from_millis(500)).await;
set_job(id, 100, 55, 50).await.expect("a level 55 warrior");
replace_item(id, -1, -11, 1302000).await.expect("a sword");
set_weapon_attack(id, 500).await.expect("its attack");
set_str(id, 999).await.expect("strength");
set_quest(id, 2237, 2, "").await.expect("2237 done");
let mut session = relogin(config).await;
let c = &mut session.connection;
let mut trace = Trace::default();
assert_eq!(warp(c, MU_YOUNG_MAP, "", &mut trace).await, MU_YOUNG_MAP);
step(c, &config.character_name, walk_to(617, 246)).await;
session
}

/// Ask Mu Young for 2245 and follow what comes of it for a moment.
async fn ask_to_go(c: &mut MapleTestConnection) -> Trace {
send(c, build_quest_action(SCRIPT_START, 2245, MU_YOUNG)).await;
let mut trace = Trace::default();
read_for(c, &mut trace, 1_500).await;
trace
}

#[tokio::test]
async fn jr_balrogs_fall_brings_out_tristans_spirit() {
let (config, other_config) = (
load_harness_config_or_fail().await,
load_harness_config_or_fail().await,
);
let m = config.character_name.clone();
let mut session = at_mu_young(&config).await;
let mut other = at_mu_young(&other_config).await;
let c = &mut session.connection;

// 2238 starts with no word, and 2245 takes the player in.
let t = step(c, &m, build_quest_action(SCRIPT_START, 2238, MU_YOUNG)).await;
assert!(t.prompts.is_empty(), "Mu Young says nothing");
assert_eq!(t.progress, [(2238, 1)]);
// What came on entering the room, Jr. Balrog among it.
let mut trace = ask_to_go(c).await;
assert!(trace.prompts.is_empty(), "nor here");
assert_eq!(trace.progress, [(2245, 2)], "2245 is done as it starts");
assert_eq!(trace.warps, [TRISTAN_ROOM]);
assert_eq!(trace.countdowns.len(), 1, "the ten minutes run");

// Someone else on 2238 must wait while the room is taken.
let n = other_config.character_name.clone();
let o = &mut other.connection;
step(o, &n, build_quest_action(SCRIPT_START, 2238, MU_YOUNG)).await;
let t = ask_to_go(o).await;
assert_eq!(t.prompts, [(MU_YOUNG, 0)], "told the room is taken");
assert!(t.progress.is_empty() && t.warps.is_empty());

// Jr. Balrog falls, and Tristan's Spirit comes out, where 2238 ends.
until(c, &mut trace, |t| t.field.alive(TRISTAN_BALROG).len() == 1).await;
let balrog = trace.field.alive(TRISTAN_BALROG)[0];
let spirit = |t: &Trace| {
t.field
.npcs
.iter()
.find(|&&(_, npc)| npc == TRISTANS_SPIRIT)
.map(|&(oid, _)| oid)
};
for _ in 0..40 {
send(c, attack(balrog, 30_000)).await;
read_for(c, &mut trace, 100).await;
if trace.field.killed.contains(&balrog) {
break;
}
}
until(c, &mut trace, |t| {
t.field.killed.contains(&balrog) && spirit(t).is_some()
})
.await;
let oid = spirit(&trace).unwrap();
let (x, y) = trace.field.npc_positions[&oid];
step(c, &m, walk_to(x, y)).await;
let t = step(c, &m, build_quest_completion(2238, TRISTANS_SPIRIT, -1)).await;
assert_eq!(t.progress, [(2238, 2)]);
assert_eq!(t.gains, [50_000]);

// Out by its portal, back to Mu Young, and the room is free again.
send(c, walk_to(-343, 188)).await;
let mut p = Packet::new_empty();
p.write_short(RecvOpcode::ChangeMapSpecial as i16).unwrap();
p.write_byte(0).unwrap();
p.write_str_with_length("out00").unwrap();
p.write_short(0).unwrap();
send(c, p).await;
let mut trace = Trace::default();
until(c, &mut trace, |t| t.warps == [MU_YOUNG_MAP]).await;
drop(session);

// The other goes in at last, and the time running out sends them back.
tokio::time::sleep(Duration::from_secs(3)).await;
let o = &mut other.connection;
let t = ask_to_go(o).await;
assert_eq!(t.progress, [(2245, 2)]);
assert_eq!(t.warps, [TRISTAN_ROOM]);
let mut trace = Trace::default();
until_within(o, &mut trace, Duration::from_secs(20), |t| {
t.warps == [MU_YOUNG_MAP]
})
.await;
drop(other);

// 2238 is done for good, and 2245 stays done.
let restored = relogin(&config).await;
assert_eq!(restored.quests.get(&2238), Some(&2));
assert_eq!(restored.quests.get(&2245), Some(&2));
}
55 changes: 54 additions & 1 deletion net/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ pub enum EventKind {
/// with that mode's gem at Duarte in the lobby or straight from its
/// room in the Shades after a run.
PyramidTomb(PyramidMode),
/// Jr. Balrog's room before Tristan's Tomb (2245), taken alone from
/// Mu Young (1061014) in the Sleepywood dungeon.
Tristan,
/// A Monster Carnival room (1 to 6), taken by a party from Spiegelmann
/// (2042000) on the Carnival Field (`crate::carnival`).
Carnival(u8),
Expand Down Expand Up @@ -2396,6 +2399,39 @@ pub const PYRAMID_TOMB_RUNS: [Event; 4] = [
pyramid_tomb(PyramidMode::Hell, &[PYRAMID_SHADES + 3]),
];

/// The room before Tristan's Tomb, 910520000, where the unknown Jr. Balrog
/// waits (Map.nx `life`, spawned once).
pub const TRISTAN_ROOM: i32 = 910520000;
/// The Sleepywood dungeon's map where Mu Young (1061014) stands, and the way
/// back from the room (its `forcedReturn` and its `out00`).
pub const MU_YOUNG_MAP: i32 = 105100100;
/// The unknown Jr. Balrog, whose fall brings out Tristan's Spirit.
pub const TRISTAN_BALROG: i32 = 9_300_326;
/// Tristan's Spirit (1061015), where 2238 ends, and the point above the
/// ground below which it comes out (Cosmic `BalrogQuest.monsterKilled`).
pub const TRISTANS_SPIRIT: i32 = 1_061_015;
pub const TRISTANS_SPIRIT_AT: (i16, i16) = (0, 115);

/// To Tristan's Tomb (2245): Mu Young's start script takes a player on 2238
/// alone into their own copy of the room for ten minutes, one at a time on
/// a channel (Cosmic `BalrogQuest`, whose script turns a second player
/// away). It is never won; it ends when they leave by `out00` or the time
/// runs out.
pub const TRISTAN: Event = Event {
lobby: MU_YOUNG_MAP,
entrances: &[],
maps: TRISTAN_ROOM..=TRISTAN_ROOM,
entry: TRISTAN_ROOM,
exit: MU_YOUNG_MAP,
levels: 50..=u8::MAX,
team: 1..=1,
minutes: 10,
exp_per_stage: &[],
member_clears: &[],
runs: 1,
..DOJO
};

/// Every run of Nett's Pyramid Duarte offers, alone first, then with a
/// party, each by mode.
pub fn pyramid_kinds() -> impl Iterator<Item = EventKind> {
Expand Down Expand Up @@ -2482,7 +2518,7 @@ pub fn protected(mob_id: i32) -> bool {
}

impl EventKind {
pub const ALL: [EventKind; 37] = [
pub const ALL: [EventKind; 38] = [
EventKind::Kpq,
EventKind::Hpq,
EventKind::Lpq,
Expand Down Expand Up @@ -2520,6 +2556,7 @@ impl EventKind {
EventKind::Carnival(6),
EventKind::Magatia(MagatiaSide::Zenumist),
EventKind::Magatia(MagatiaSide::Alcadno),
EventKind::Tristan,
];

pub fn event(self) -> &'static Event {
Expand All @@ -2546,6 +2583,7 @@ impl EventKind {
EventKind::PyramidTomb(mode) => &PYRAMID_TOMB_RUNS[mode as usize],
EventKind::Carnival(room) => &CARNIVAL[usize::from(room.clamp(1, CARNIVAL_ROOMS) - 1)],
EventKind::Magatia(side) => &MAGATIA[side as usize],
EventKind::Tristan => &TRISTAN,
}
}

Expand Down Expand Up @@ -3595,6 +3633,21 @@ mod tests {
assert!(event_map(926010010) && !event_map(PYRAMID_LOBBY));
}

#[test]
fn jr_balrogs_room_is_taken_alone_from_mu_young_for_ten_minutes() {
let tristan = EventKind::Tristan.event();
assert_eq!((tristan.entry, tristan.exit), (TRISTAN_ROOM, MU_YOUNG_MAP));
assert!(tristan.solo() && tristan.lasts_until_empty());
assert_eq!(
(tristan.minutes, tristan.last_stage(), tristan.runs),
(10, 0, 1)
);
assert!(tristan.starts_on(MU_YOUNG_MAP));
assert!(tristan.team(1, &[at(1, 50, MU_YOUNG_MAP)]).is_some());
assert_eq!(tristan.team(1, &[at(1, 49, MU_YOUNG_MAP)]), None);
assert!(event_map(TRISTAN_ROOM) && !event_map(MU_YOUNG_MAP));
}

#[test]
fn dojo_sections_have_their_own_clocks() {
let floor = |n: u8| clock_on(DOJO.stage_map(n));
Expand Down
Loading
Loading