From 92ef5d29191748346ce4b24caf280f36d7dc62de Mon Sep 17 00:00:00 2001 From: nolt Date: Wed, 24 Jun 2026 15:36:15 +0200 Subject: [PATCH] feat(npc): make Market Union Member Julia warp to/from the Loren Market Market Union Member Julia (547) had a merchant window but no store, so talking to her opened an empty, non-working shop. In the original game she is a warp NPC that moves players between Lorencia and the Loren Market, on both sides. Wire her up to the warp window the game client already provides: - change her NPC window to JuliaWarpMarketServer, so talking to her opens the client's Julia "Warp" window instead of an empty shop; - spawn a second instance of her in Lorencia, which is the entrance; - add a handler for the EnterMarketPlaceRequest sent by the window's Warp button, which warps the player to the other side (Loren Market or Lorencia) depending on the map they are currently on; - add an optional configuration update which applies this to existing servers. The game client already implements the window, the button and the packet, so no client change is required. --- .../EnterMarketPlaceHandlerPlugIn.cs | 75 ++++++++++++++++ .../Updates/AddLorenMarketJuliaWarpPlugIn.cs | 89 +++++++++++++++++++ .../Initialization/Updates/UpdateVersion.cs | 5 ++ .../VersionSeasonSix/Maps/Lorencia.cs | 1 + .../VersionSeasonSix/NpcInitialization.cs | 2 +- 5 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 src/GameServer/MessageHandler/EnterMarketPlaceHandlerPlugIn.cs create mode 100644 src/Persistence/Initialization/Updates/AddLorenMarketJuliaWarpPlugIn.cs diff --git a/src/GameServer/MessageHandler/EnterMarketPlaceHandlerPlugIn.cs b/src/GameServer/MessageHandler/EnterMarketPlaceHandlerPlugIn.cs new file mode 100644 index 000000000..0fa4c015d --- /dev/null +++ b/src/GameServer/MessageHandler/EnterMarketPlaceHandlerPlugIn.cs @@ -0,0 +1,75 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameServer.MessageHandler; + +using System.Runtime.InteropServices; +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.GameLogic; +using MUnique.OpenMU.GameServer.MessageHandler.MuHelper; +using MUnique.OpenMU.Network.Packets.ClientToServer; +using MUnique.OpenMU.PlugIns; + +/// +/// Handler for the , sent by the 'Warp' button of the +/// Market Union Member Julia window. It warps the player between Lorencia and the Loren Market, +/// depending on which side the player is currently on. +/// +[PlugIn] +[Display(Name = PlugInName, Description = PlugInDescription)] +[Guid("5e028dcf-a5af-40a0-b958-2deb38aae4bc")] +[BelongsToGroup(MuHelperGroupHandler.GroupKey)] +internal class EnterMarketPlaceHandlerPlugIn : ISubPacketHandlerPlugIn +{ + private const string PlugInName = "Enter Market Place Handler"; + + private const string PlugInDescription = "Handler which warps a player between Lorencia and the Loren Market when using the 'Warp' button of Market Union Member Julia."; + + /// + /// The map number of the Loren Market. + /// + private const short LorenMarketMapNumber = 79; + + /// + /// The map number of Lorencia, where the player is warped back to. + /// + private const short LorenciaMapNumber = 0; + + /// + public bool IsEncryptionExpected => false; + + /// + public byte Key => EnterMarketPlaceRequest.SubCode; + + /// + public async ValueTask HandlePacketAsync(Player player, Memory packet) + { + if (packet.Length < EnterMarketPlaceRequest.Length) + { + return; + } + + // Only allow the warp through the actual Julia window, so a crafted packet can't be used + // as a free teleport from anywhere. + if (player.OpenedNpc?.Definition.NpcWindow != NpcWindow.JuliaWarpMarketServer) + { + return; + } + + // Julia warps both ways: from the Loren Market back to Lorencia, and from anywhere else + // (her counterpart in Lorencia) into the Loren Market. + var targetMapNumber = player.CurrentMap?.Definition.Number == LorenMarketMapNumber + ? LorenciaMapNumber + : LorenMarketMapNumber; + + var targetMap = await player.GameContext.GetMapAsync((ushort)targetMapNumber).ConfigureAwait(false); + if (targetMap?.SafeZoneSpawnGate is not { } targetGate) + { + return; + } + + player.OpenedNpc = null; + await player.WarpToAsync(targetGate).ConfigureAwait(false); + } +} diff --git a/src/Persistence/Initialization/Updates/AddLorenMarketJuliaWarpPlugIn.cs b/src/Persistence/Initialization/Updates/AddLorenMarketJuliaWarpPlugIn.cs new file mode 100644 index 000000000..42b494678 --- /dev/null +++ b/src/Persistence/Initialization/Updates/AddLorenMarketJuliaWarpPlugIn.cs @@ -0,0 +1,89 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Persistence.Initialization.Updates; + +using System.Runtime.InteropServices; +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps; +using MUnique.OpenMU.PlugIns; + +/// +/// This update turns Market Union Member Julia (547) into a working warp NPC for the Loren Market, +/// on both sides. It changes her NPC window so that talking to her opens the warp dialog instead of +/// an empty merchant shop, and spawns a second instance of her in Lorencia, which is the entrance. +/// +/// +/// The actual warp is performed server-side by the EnterMarketPlaceHandlerPlugIn when the +/// player uses the 'Warp' button of the (client-side) Julia window. +/// +[PlugIn] +[Display(Name = PlugInName, Description = PlugInDescription)] +[Guid("ed2f1728-b35c-4a3d-810e-eab5b6e12a82")] +public class AddLorenMarketJuliaWarpPlugIn : UpdatePlugInBase +{ + /// + /// The plug in name. + /// + internal const string PlugInName = "Add Loren Market Julia warp"; + + /// + /// The plug in description. + /// + internal const string PlugInDescription = "Makes Market Union Member Julia (547) a working warp NPC between Lorencia and the Loren Market, on both sides, instead of an empty merchant."; + + private const short JuliaNpcNumber = 547; + + /// + public override UpdateVersion Version => UpdateVersion.AddLorenMarketJuliaWarp; + + /// + public override string DataInitializationKey => VersionSeasonSix.DataInitialization.Id; + + /// + public override string Name => PlugInName; + + /// + public override string Description => PlugInDescription; + + /// + public override bool IsMandatory => false; + + /// + public override DateTime CreatedAt => new(2026, 06, 24, 0, 0, 0, DateTimeKind.Utc); + + /// + protected override ValueTask ApplyAsync(IContext context, GameConfiguration gameConfiguration) + { + var julia = gameConfiguration.Monsters.FirstOrDefault(m => m.Number == JuliaNpcNumber); + if (julia is null) + { + return default; + } + + julia.NpcWindow = NpcWindow.JuliaWarpMarketServer; + julia.MerchantStore = null; + + var lorencia = gameConfiguration.Maps.FirstOrDefault(m => m.Number == Lorencia.Number); + if (lorencia is null + || lorencia.MonsterSpawns.Any(s => s.MonsterDefinition?.Number == JuliaNpcNumber)) + { + return default; + } + + var juliaSpawn = context.CreateNew(); + lorencia.MonsterSpawns.Add(juliaSpawn); + juliaSpawn.SetGuid(JuliaNpcNumber); + juliaSpawn.GameMap = lorencia; + juliaSpawn.MonsterDefinition = julia; + juliaSpawn.SpawnTrigger = SpawnTrigger.Automatic; + juliaSpawn.Direction = Direction.SouthEast; + juliaSpawn.X1 = 139; + juliaSpawn.X2 = 139; + juliaSpawn.Y1 = 138; + juliaSpawn.Y2 = 138; + + return default; + } +} diff --git a/src/Persistence/Initialization/Updates/UpdateVersion.cs b/src/Persistence/Initialization/Updates/UpdateVersion.cs index 0fe9cd751..141dd17e1 100644 --- a/src/Persistence/Initialization/Updates/UpdateVersion.cs +++ b/src/Persistence/Initialization/Updates/UpdateVersion.cs @@ -439,4 +439,9 @@ public enum UpdateVersion /// The version of the . /// AddMovementSpeedAttributesSeason6 = 86, + + /// + /// The version of the . + /// + AddLorenMarketJuliaWarp = 87, } diff --git a/src/Persistence/Initialization/VersionSeasonSix/Maps/Lorencia.cs b/src/Persistence/Initialization/VersionSeasonSix/Maps/Lorencia.cs index 9d023a5d2..a2b498697 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/Maps/Lorencia.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/Maps/Lorencia.cs @@ -43,5 +43,6 @@ protected override IEnumerable CreateNpcSpawns() yield return this.CreateMonsterSpawn(24, this.NpcDictionary[543], 141, 143, Direction.South); yield return this.CreateMonsterSpawn(25, this.NpcDictionary[371], 130, 126, Direction.SouthEast); yield return this.CreateMonsterSpawn(26, this.NpcDictionary[568], 131, 139, Direction.South, SpawnTrigger.Wandering); // Wandering Merchant Zyro + yield return this.CreateMonsterSpawn(27, this.NpcDictionary[547], 139, 138, Direction.SouthEast); // Market Union Member Julia (warps to the Loren Market) } } \ No newline at end of file diff --git a/src/Persistence/Initialization/VersionSeasonSix/NpcInitialization.cs b/src/Persistence/Initialization/VersionSeasonSix/NpcInitialization.cs index b4bcefd2f..e285f87ac 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/NpcInitialization.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/NpcInitialization.cs @@ -958,7 +958,7 @@ public override void Initialize() var def = this.Context.CreateNew(); def.Number = 547; def.Designation = "Market Union Member Julia"; - def.NpcWindow = NpcWindow.Merchant; + def.NpcWindow = NpcWindow.JuliaWarpMarketServer; def.ObjectKind = NpcObjectKind.PassiveNpc; def.SetGuid(def.Number); this.GameConfiguration.Monsters.Add(def);