From 8b8952edfe6ad48896fc989c86df7ace74cee6c2 Mon Sep 17 00:00:00 2001 From: Denis Date: Sun, 30 Aug 2026 18:33:10 +0400 Subject: [PATCH 1/2] fix(npc): the monster info card carries the monster's name Nothing set the name, so every card went out with the placeholder a null string serializes to and the client drew that as the monster's name. --- .../Ecs/Extensions/NpcInfoExtensions.cs | 12 +--- .../Ecs/Extensions/NpcInfoLineTests.cs | 60 +++++++++++++++++++ 2 files changed, 63 insertions(+), 9 deletions(-) create mode 100644 test/NosCore.GameObject.Tests/Ecs/Extensions/NpcInfoLineTests.cs diff --git a/src/NosCore.GameObject/Ecs/Extensions/NpcInfoExtensions.cs b/src/NosCore.GameObject/Ecs/Extensions/NpcInfoExtensions.cs index eba4475a8..2f5682276 100644 --- a/src/NosCore.GameObject/Ecs/Extensions/NpcInfoExtensions.cs +++ b/src/NosCore.GameObject/Ecs/Extensions/NpcInfoExtensions.cs @@ -13,15 +13,6 @@ namespace NosCore.GameObject.Ecs.Extensions; public static class NpcInfoExtensions { - // Builds the e_info response for a req_info 5 (NPC) or req_info 6 (monster/mate). - // OpenNos's NpcMonster.GenerateEInfo AND Mate.GenerateEInfo both emit: - // `e_info 10 - // - // - // -1 ` - // — the leading 10 is the format discriminator and the trailing -1 is a constant - // the client expects before the name field. Without either, the client can't align - // fields and falls back to defaults (Level=0, HP=100/100) in the target info card. public static EInfoNpcMonsterPacket GenerateNpcInfo(this NpcMonsterDto npc, RegionType language) { return new EInfoNpcMonsterPacket @@ -50,6 +41,9 @@ public static EInfoNpcMonsterPacket GenerateNpcInfo(this NpcMonsterDto npc, Regi DarkResistance = npc.DarkResistance, MaxHp = npc.MaxHp, MaxMp = npc.MaxMp, + // The serializer escapes a string field against the separator that follows it, and + // nothing follows this one, so the spaces have to go before it is handed over. + Name = npc.Name[language].Replace(' ', '^'), }; } diff --git a/test/NosCore.GameObject.Tests/Ecs/Extensions/NpcInfoLineTests.cs b/test/NosCore.GameObject.Tests/Ecs/Extensions/NpcInfoLineTests.cs new file mode 100644 index 000000000..64e9d8141 --- /dev/null +++ b/test/NosCore.GameObject.Tests/Ecs/Extensions/NpcInfoLineTests.cs @@ -0,0 +1,60 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// + +using System.Linq; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NosCore.Data.Dto; +using NosCore.Data.StaticEntities; +using NosCore.GameObject.Ecs.Extensions; +using NosCore.Packets.Interfaces; +using NosCore.Shared.Enumerations; +using NosCore.Packets; + +namespace NosCore.GameObject.Tests.Ecs.Extensions +{ + [TestClass] + public class NpcInfoLineTests + { + private static readonly Serializer Wire = new(typeof(IPacket).Assembly.GetTypes() + .Where(p => p.GetInterfaces().Contains(typeof(IPacket)) && p.IsClass && !p.IsAbstract) + .ToList()); + + private static NpcMonsterDto Cuby() + { + var name = new I18NString + { + [RegionType.EN] = "Mother Cuby", + [RegionType.FR] = "Cuby Mere" + }; + + return new NpcMonsterDto + { + NpcMonsterVNum = 303, + Level = 35, + MaxHp = 1360, + MaxMp = 630, + Name = name + }; + } + + [TestMethod] + public void TheLineEndsWithThePortraitAndTheName() + { + var line = Wire.Serialize(new[] { (IPacket)Cuby().GenerateNpcInfo(RegionType.EN) }).TrimEnd(); + + Assert.IsTrue(line.EndsWith("-1 Mother^Cuby"), line); + Assert.AreEqual(26, line.Split(' ').Length - 1, line); + } + + [TestMethod] + public void TheNameIsTheReadersLanguage() + { + var line = Wire.Serialize(new[] { (IPacket)Cuby().GenerateNpcInfo(RegionType.FR) }).TrimEnd(); + + Assert.IsTrue(line.EndsWith("-1 Cuby^Mere"), line); + } + } +} From d270df9ac2857699e518bc1e5dbc66a72d611378 Mon Sep 17 00:00:00 2001 From: erwan-joly Date: Mon, 31 Aug 2026 11:04:01 +1200 Subject: [PATCH 2/2] fix: let the serializer escape the name The Replace(' ', '^') was standing in for a packet flag that already exists: EscapeSpaces, which GInfoPacket, MlInfoBrPacket and MlintroPacket declare on the field that ends their line. EInfoNpcMonsterPacket.Name now declares it too, in NosCore.Packets 21.1.2, so the call site hands over the name as it is. The line tests are unchanged and still assert Mother^Cuby on the wire - they now prove the serializer does it. Co-Authored-By: Claude Opus 5 --- Directory.Packages.props | 2 +- src/NosCore.GameObject/Ecs/Extensions/NpcInfoExtensions.cs | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 90ac4edd8..2002a09db 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -44,7 +44,7 @@ - + diff --git a/src/NosCore.GameObject/Ecs/Extensions/NpcInfoExtensions.cs b/src/NosCore.GameObject/Ecs/Extensions/NpcInfoExtensions.cs index 2f5682276..6f6372dbd 100644 --- a/src/NosCore.GameObject/Ecs/Extensions/NpcInfoExtensions.cs +++ b/src/NosCore.GameObject/Ecs/Extensions/NpcInfoExtensions.cs @@ -1,4 +1,4 @@ -// __ _ __ __ ___ __ ___ ___ +// __ _ __ __ ___ __ ___ ___ // | \| |/__\ /' _/ / _//__\| _ \ __| // | | ' | \/ |`._`.| \_| \/ | v / _| // |_|\__|\__/ |___/ \__/\__/|_|_\___| @@ -41,9 +41,7 @@ public static EInfoNpcMonsterPacket GenerateNpcInfo(this NpcMonsterDto npc, Regi DarkResistance = npc.DarkResistance, MaxHp = npc.MaxHp, MaxMp = npc.MaxMp, - // The serializer escapes a string field against the separator that follows it, and - // nothing follows this one, so the spaces have to go before it is handed over. - Name = npc.Name[language].Replace(' ', '^'), + Name = npc.Name[language], }; }