From 30ecfbdedeffb4a6ec79bf1c7d4b75d873127b18 Mon Sep 17 00:00:00 2001 From: David Goldstein Date: Mon, 26 Oct 2026 15:30:19 -0400 Subject: [PATCH] Row 198: Romeo and Juliet PQ slice 8: Yulete's traces open the dark pipe off Magatia (magatia_dark0, 261020600 up00) into his lab (926130000 out00) with a server-only marker for Cosmic's quest 7770, which Quest.nx lacks; anyone else gets a pink line --- agent_docs/map-travel.md | 1 + agent_docs/party-quests.md | 5 +- game-data/placeholder_text.toml | 1 + game-data/server_content.toml | 1 + game-data/src/server_content.rs | 2 + integration-harness/tests/magatia_boss.rs | 87 ++++++++++++++++++++--- net/src/npc_scripts.rs | 20 +++++- net/src/scripts.rs | 30 ++++++++ 8 files changed, 134 insertions(+), 13 deletions(-) diff --git a/agent_docs/map-travel.md b/agent_docs/map-travel.md index 3f3298c3..795e27be 100644 --- a/agent_docs/map-travel.md +++ b/agent_docs/map-travel.md @@ -259,6 +259,7 @@ nothing. A script returns effects that the client actor sends: | `enterWitch` | Leafre's nest into the Cave of Black Witches once 20404 is completed: `924010200` after 20407, `924010100` after 20406, else `924010000`, portal `out00`; else a pink line | | `TD_MC_enterboss1` | The castle tower's way to the Wedding Hall entrance: portal sound, warp to `106021401` `out00`. Cosmic starts a fight with the Yeti brothers instead while 2330 is taken and they aren't all beaten; that event isn't in, so the way is open to everyone | | `secretDoor` | Magatia's lab doors (`261010000`, `261020200` `secret00`) once 3360 is taken or done: portal sound, warp to `261030000` `sp_jenu` (from Zenumist's) or `sp_alca`; else a pink line. Cosmic's password prompt while 3360 is taken is left out: the quest's start script that sets the password isn't run | +| `magatia_dark0` | The dark pipe on `261020600` (`up00`) into Yulete's lab: portal sound, warp to `926130000` `out00`, once Yulete's traces in the Romeo and Juliet party quest have shown the player the way (marker 32600, `party-quests.md`; Cosmic completes quest 7770, which isn't in `Quest.nx`); else a pink line. The lab's quest rooms (`q3366in`-`q3368in` on `926130100`) aren't in | | `curseforest` | The Forest of Evil I's hidden portal (`100040105` `secret00`) into the Cursed Forest, for players with 2224 or 2226 taken or 2227 done: portal sound, warp to `910100001` (after 2227) or `910100000`, portal `out00`, from 17:00 until 7:00 by the stations' clock (route time, UTC); by day, or off those quests, a pink line | | `elevator` | The Helios Tower's floors (`222020100`, `222020200`, `in00`): portal sound and into the floor's waiting room (`222020110`, `222020210`, portal 0) while the elevator's car is docked there, else a pink line; the car takes the waiting room up or down (`ships.md`) | | `rienTutor8` | Rien's way out (`140000000` `west00`): portal sound, warp to `140010000` `east00`; an Aran beginner (job 2000) on Basic Fitness Training 1-3 (21015-21017) stays, with a chat line (SHOW_STATUS_INFO 9, `Effect::InfoText`) saying which way the training lies | diff --git a/agent_docs/party-quests.md b/agent_docs/party-quests.md index bd736cfd..322b6a85 100644 --- a/agent_docs/party-quests.md +++ b/agent_docs/party-quests.md @@ -1356,8 +1356,9 @@ Yulete's traces (2112001, 2112011) in x00500, who send them on in turn. In the last room Romeo & Juliet (2112018) thank each member and give one of 23 rewards (`events::MAGATIA_REWARDS`, Cosmic's `setEventRewards`), which needs a free slot in each of the Equip and Use -inventories, on the way back to the side's room. Cosmic's traces also complete quest 7770; ours don't -yet. +inventories, on the way back to the side's room. The traces also open +the dark pipe into Yulete's lab to the member (marker 32600, for Cosmic's +quest 7770, which `Quest.nx` lacks; see `map-travel.md`, `magatia_dark0`). Inside, Romeo or Juliet asks whether a member would rather retreat (`magatia.romeo.retreat`, `magatia.juliet.retreat`) and sends them to the diff --git a/game-data/placeholder_text.toml b/game-data/placeholder_text.toml index dddec712..5a201434 100644 --- a/game-data/placeholder_text.toml +++ b/game-data/placeholder_text.toml @@ -1362,6 +1362,7 @@ "magatia.thanks.rehab" = "[placeholder] Thanks to you we are together again, and Yulete will be helped back onto the right path." "magatia.thanks.jail" = "[placeholder] Thanks to you we are together again, and Yulete will be jailed." "magatia.thanks.full" = "[placeholder] Free a slot in each of your inventories first." +"magatia.pipe_dark" = "[placeholder] It is too dark in the pipe to go in." "pirate.menu" = "[placeholder] The Lord Pirate has taken Wu Yang, my son, onto his ship. Will your party board it and bring him back?" "pirate.enter" = "[placeholder] Board the Pirate Ship." "pirate.about" = "[placeholder] What waits on the ship?" diff --git a/game-data/server_content.toml b/game-data/server_content.toml index 79fe6eae..89c72cb8 100644 --- a/game-data/server_content.toml +++ b/game-data/server_content.toml @@ -1212,6 +1212,7 @@ unsourced = [ "magatia.thanks.rehab", "magatia.thanks.jail", "magatia.thanks.full", + "magatia.pipe_dark", "pirate.menu", "pirate.enter", "pirate.about", diff --git a/game-data/src/server_content.rs b/game-data/src/server_content.rs index 465d573d..d0e91ebe 100644 --- a/game-data/src/server_content.rs +++ b/game-data/src/server_content.rs @@ -1863,6 +1863,7 @@ pub mod keys { pub const MAGATIA_THANKS_REHAB: &str = "magatia.thanks.rehab"; pub const MAGATIA_THANKS_JAIL: &str = "magatia.thanks.jail"; pub const MAGATIA_THANKS_FULL: &str = "magatia.thanks.full"; + pub const MAGATIA_PIPE_DARK: &str = "magatia.pipe_dark"; pub const PIRATE_MENU: &str = "pirate.menu"; pub const PIRATE_ENTER: &str = "pirate.enter"; pub const PIRATE_ABOUT: &str = "pirate.about"; @@ -3117,6 +3118,7 @@ mod tests { keys::MAGATIA_THANKS_REHAB, keys::MAGATIA_THANKS_JAIL, keys::MAGATIA_THANKS_FULL, + keys::MAGATIA_PIPE_DARK, keys::PIRATE_MENU, keys::PIRATE_ENTER, keys::PIRATE_ABOUT, diff --git a/integration-harness/tests/magatia_boss.rs b/integration-harness/tests/magatia_boss.rs index 988cd868..e50ed525 100644 --- a/integration-harness/tests/magatia_boss.rs +++ b/integration-harness/tests/magatia_boss.rs @@ -4,7 +4,8 @@ //! run, paying each member by whether the escort fainted, sends the guards //! away, and brings out Romeo or Juliet, who give the side's gift and send //! each member on; in the last room each is thanked and rewarded on the -//! way home. +//! way home. Yulete's traces open the dark pipe off Magatia into his lab +//! to whoever they met. use game_data::server_content::keys; use integration_harness::events::talk_to; @@ -14,7 +15,7 @@ use integration_harness::quests::answer; use integration_harness::trace::{read_for, send, until, warp, Trace}; use integration_harness::WorldSession; use net::events::{EventKind, MagatiaSide, MAGATIA_REWARDS}; -use net::npc_scripts::{magatia_hub_host, yulete, JULIET, MAGATIA_THANKS, ROMEO}; +use net::npc_scripts::{magatia_hub_host, traces, yulete, JULIET, MAGATIA_THANKS, ROMEO}; use net::packet::op::RecvOpcode; use packet::io::write::PktWrite; use packet::Packet; @@ -23,6 +24,10 @@ use tokio::time::Duration; /// The boss room's guards. const GUARD: i32 = 9300150; const PINK: u8 = 5; +/// Where the dark pipe (`up00`, running `magatia_dark0`) is, and where it +/// leads: Yulete's lab, by its way out (`out00`). +const PIPE_MAP: i32 = 261020600; +const YULETE_LAB: i32 = 926130000; fn text(key: &str) -> String { net::content::text().get(key, &[]) @@ -69,6 +74,37 @@ fn mob_damage_friendly(attacker: i32, character_id: i32, victim: i32) -> Packet p } +/// CHANGE_MAP_SPECIAL: press Up on the scripted portal `name`. +fn enter(name: &str) -> Packet { + let mut p = Packet::new_empty(); + p.write_short(RecvOpcode::ChangeMapSpecial as i16).unwrap(); + p.write_byte(0).unwrap(); + p.write_str_with_length(name).unwrap(); + p.write_short(0).unwrap(); + p +} + +/// `player` goes to the dark pipe and presses Up on it; what came of it. +async fn into_the_pipe(player: &mut WorldSession) -> Trace { + let mut seen = Trace::default(); + let c = &mut player.connection; + assert_eq!(warp(c, PIPE_MAP, "", &mut seen).await, PIPE_MAP); + let portal = maps() + .field(PIPE_MAP) + .unwrap() + .portal_by_name("up00") + .unwrap(); + send(c, walk_to(portal.x as i16, portal.y as i16)).await; + let mut seen = Trace::default(); + send(c, enter("up00")).await; + until(c, &mut seen, |t| { + !t.warps.is_empty() || !t.broadcasts.is_empty() + }) + .await; + read_for(c, &mut seen, 300).await; + seen +} + /// The leader goes from the corridor into `side`'s boss room and waits /// for Yulete, the escort and the guards; what was seen. async fn into_the_lab(leader: &mut WorldSession, side: MagatiaSide) -> Trace { @@ -85,13 +121,8 @@ async fn into_the_lab(leader: &mut WorldSession, side: MagatiaSide) -> Trace { .portal_by_name(name) .unwrap(); send(c, walk_to(portal.x as i16, portal.y as i16)).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(name).unwrap(); - p.write_short(0).unwrap(); let mut seen = Trace::default(); - send(c, p).await; + send(c, enter(name)).await; until(c, &mut seen, |t| { t.warps.contains(&lab) && t.field.npcs.iter().any(|&(_, id)| id == yulete(side)) @@ -227,6 +258,35 @@ async fn the_frankenroid_falls_and_romeo_sends_the_party_home_with_gifts() { ); send(&mut team[0].connection, answer(0, 1, None)).await; + // Yulete's traces, met in their room, send a member on to the last + // room, where they are thanked on the way home; the dark pipe then + // leads them into his lab. + let member = &mut team[1]; + let mut met = Trace::default(); + let c = &mut member.connection; + let room = side.traces_room(); + assert_eq!(warp(c, room, "", &mut met).await, room); + until(c, &mut met, |t| { + t.field.npcs.iter().any(|&(_, id)| id == traces(side)) + }) + .await; + let asked = told(member, talk_to(npc(&met, traces(side)))).await; + assert_eq!(asked.prompts, [(traces(side), 4)], "{asked:?}"); + let sorry = told(member, answer(4, 1, Some(0))).await; + assert_eq!( + sorry.prompt_texts, + [text(keys::MAGATIA_TRACES_SORRY)], + "{sorry:?}" + ); + let mut on = Trace::default(); + let c = &mut member.connection; + send(c, answer(0, 1, None)).await; + until(c, &mut on, |t| t.warps.contains(&side.thanks_room())).await; + thanked(member, side, on, keys::MAGATIA_THANKS_REHAB).await; + let piped = into_the_pipe(member).await; + assert_eq!(piped.warps, [YULETE_LAB], "{piped:?}"); + assert_eq!(piped.portal_sounds, 1, "{piped:?}"); + // Romeo gives the gift and, Yulete not having been caught, sends the // leader straight to the last room. let host = npc(&seen, magatia_hub_host(side)); @@ -249,6 +309,17 @@ async fn the_frankenroid_falls_and_romeo_sends_the_party_home_with_gifts() { thanked(&mut team[0], side, on, keys::MAGATIA_THANKS_REHAB).await; } +#[tokio::test] +async fn the_dark_pipe_is_too_dark_for_those_the_traces_never_met() { + let mut player = armed_in(PIPE_MAP, 75).await; + let dark = into_the_pipe(&mut player).await; + let line = (PINK, text(keys::MAGATIA_PIPE_DARK)); + assert!( + dark.warps.is_empty() && dark.broadcasts.contains(&line), + "{dark:?}" + ); +} + #[tokio::test] async fn juliet_fainting_costs_the_run_its_gifts_and_some_exp() { let side = MagatiaSide::Alcadno; diff --git a/net/src/npc_scripts.rs b/net/src/npc_scripts.rs index 07cd7f01..18a10df9 100644 --- a/net/src/npc_scripts.rs +++ b/net/src/npc_scripts.rs @@ -862,6 +862,10 @@ pub(crate) const ZAKUM_APPROVED: i16 = 32400; /// Adobis forged the player's Eyes of Fire: the trials are done (upstream /// quest 100201). pub(crate) const ZAKUM_TRIALS_DONE: i16 = 32401; +/// Yulete's traces have shown the player the way into his lab: the pipe +/// beyond Magatia's Alcadno side leads there (upstream quest 7770, which +/// Quest.nx lacks). +pub(crate) const YULETE_LAB_OPENED: i16 = 32600; const DOOR_OF_DIMENSION: i32 = 1061009; const LORD_JONATHAN: i32 = 1092019; /// How long a Corsair has to find the real Bart (Cosmic's `4jaerial`). @@ -7538,11 +7542,15 @@ fn magatia_saved(side: MagatiaSide, escort_failed: bool, caught: bool) -> NpcScr } /// Yulete's traces (Cosmic `2112001`, `2112011`): beaten, he is cheered -/// up, says he is sorry, and sends the member on to the last room. +/// up, says he is sorry, and sends the member on to the last room, the +/// way into his lab open to them from now on (`YULETE_LAB_OPENED`). fn yulete_traces(side: MagatiaSide) -> NpcScript { let on = vec![ Page::Say(text(keys::MAGATIA_TRACES_SORRY)), - Page::Done(vec![Effect::WarpToPortal(side.thanks_room(), 0)]), + Page::Done(vec![ + Effect::Mark(YULETE_LAB_OPENED), + Effect::WarpToPortal(side.thanks_room(), 0), + ]), ]; NpcScript::new( traces(side), @@ -15910,7 +15918,13 @@ mod tests { let sorry = traced.respond(ScriptInput::Select(0)).unwrap(); assert_eq!(page_text(&sorry), text(keys::MAGATIA_TRACES_SORRY)); let on = traced.respond(ScriptInput::Next).unwrap(); - assert_eq!(on.effects, [Effect::WarpToPortal(thanks_room, 0)]); + assert_eq!( + on.effects, + [ + Effect::Mark(YULETE_LAB_OPENED), + Effect::WarpToPortal(thanks_room, 0) + ] + ); // The thanks, then one of the rewards and the way home. let lobby = EventKind::Magatia(side).event().lobby; diff --git a/net/src/scripts.rs b/net/src/scripts.rs index 26f28ace..ebea3f6f 100644 --- a/net/src/scripts.rs +++ b/net/src/scripts.rs @@ -42,6 +42,10 @@ const THORN_REMOVER_QUEST: i16 = 2324; /// Set once a Killer Mushroom Spore opened the `obstacle` portal (Cosmic /// starts the unknown quest 100202). const OBSTACLE_OPENED: i16 = 32500; +/// Yulete's lab, the dark pipe's end, and its way out to Magatia +/// (`out00`), where the pipe leads. +const YULETE_LAB: i32 = 926130000; +const YULETE_LAB_WAY_OUT: u8 = 2; /// How near a scripted item's portal the player must stand. const ITEM_REACH: f64 = 210.0; /// Kerning City's subway: Line 1's first area, and its second, where the @@ -1343,6 +1347,12 @@ pub fn portal(script: &str, player: &Player) -> Option { let side = crate::events::MagatiaSide::of(player.stats.map_id)?; through(Effect::WarpToPortal(side.stages()[6], 0)) } + // The dark pipe off Magatia's Alcadno side, into Yulete's lab once + // his traces have shown the way (Cosmic `magatia_dark0`, quest 7770). + "magatia_dark0" if quest(npc_scripts::YULETE_LAB_OPENED) == QuestState::Started => { + through(Effect::WarpToPortal(YULETE_LAB, YULETE_LAB_WAY_OUT)) + } + "magatia_dark0" => refused(keys::MAGATIA_PIPE_DARK), // The hub's doors into the rooms beside it, once a Card Key opened // them, those rooms' ways back, once a door opened them too, and // the hub's way on, once Romeo or Juliet opened it for the files @@ -3255,6 +3265,26 @@ mod tests { assert!(portal("rnj1_pt00", &player).is_none()); } + #[test] + fn the_dark_pipe_leads_into_yuletes_lab_once_his_traces_showed_the_way() { + let mut player = player_with(&[]); + player.stats.map_id = 261020600; + let dark = portal("magatia_dark0", &player).unwrap(); + let line = content::text().get(keys::MAGATIA_PIPE_DARK, &[]); + assert_eq!(dark.effects, [Effect::Pink(line)]); + assert!(!dark.block); + + player.quests.mark(npc_scripts::YULETE_LAB_OPENED); + let open = portal("magatia_dark0", &player).unwrap(); + assert_eq!( + open.effects, + [Effect::PortalSound, Effect::WarpToPortal(926130000, 2)] + ); + // `out00`, its way out to Magatia. + let lab = crate::game_data::get().unwrap().field(926130000).unwrap(); + assert_eq!(lab.portal_by_name("out00").unwrap().id, 2); + } + #[test] fn a_magatia_hubs_ways_wait_for_their_doors() { use crate::events::MagatiaSide;