From f8191723241bfef1eee404f242a989d8e2124f25 Mon Sep 17 00:00:00 2001 From: nevenit Date: Sat, 17 Oct 2026 08:56:19 +1000 Subject: [PATCH] feat!: 22.0.0 - mate packets as the official server sends them Checked against lines recorded from the official server (a character with four pets and two partners), now pinned in OfficialMatePacketTests: - sc_p has 40 fields, not 33: Unknow2..Unknow8 (34 is 1-3 per pet, 39 is always 1). With only 33 fields the client's pet window shows no pictures. - sc_n ends with two more fields (Unknown4, Unknown5). - in: the non-player part ends with two more fields for monsters, npcs and mates alike (Unknow7 carries the pet's 1-3 value, 0 otherwise). - ptctl is typed: Mates (id, x, y per mate, counted by Amount), then VisualId and Speed. Breaking: PacketEnd is gone. - An absent sub-packet written with its own separator keeps its leading space, so a partner without equipment is "sc_n ... 13458 -1 -1 -1 -1", not "13458-1-1-1-1". Co-Authored-By: Claude Opus 5.5 --- .../ClientPackets/Mates/PtctlPacket.cs | 13 +- .../ClientPackets/Mates/PtctlSubPacket.cs | 22 ++++ src/NosCore.Packets/NosCore.Packets.csproj | 2 +- src/NosCore.Packets/Serializer.cs | 4 +- .../ServerPackets/Mates/ScnPacket.cs | 9 ++ .../ServerPackets/Mates/ScpPacket.cs | 24 ++++ .../Visibility/InNonPlayerSubPacket.cs | 7 ++ .../OfficialMatePacketTests.cs | 118 ++++++++++++++++++ ...rOutputIsPinnedForEveryPacket.verified.txt | 16 +-- test/NosCore.Packets.Tests/SerializerTest.cs | 4 +- 10 files changed, 205 insertions(+), 14 deletions(-) create mode 100644 src/NosCore.Packets/ClientPackets/Mates/PtctlSubPacket.cs create mode 100644 test/NosCore.Packets.Tests/OfficialMatePacketTests.cs diff --git a/src/NosCore.Packets/ClientPackets/Mates/PtctlPacket.cs b/src/NosCore.Packets/ClientPackets/Mates/PtctlPacket.cs index bc19689d..ed0254aa 100644 --- a/src/NosCore.Packets/ClientPackets/Mates/PtctlPacket.cs +++ b/src/NosCore.Packets/ClientPackets/Mates/PtctlPacket.cs @@ -6,9 +6,12 @@ using NosCore.Packets.Attributes; using NosCore.Packets.Enumerations; +using System.Collections.Generic; namespace NosCore.Packets.ClientPackets.Mates { + // ptctl 1 2 : where each mate following the player is, then one id and + // speed (the first mate's in every recording). [PacketHeader("ptctl", Scope.InGame)] public class PtctlPacket : PacketBase { @@ -18,7 +21,13 @@ public class PtctlPacket : PacketBase [PacketIndex(1)] public short Amount { get; set; } - [PacketIndex(2)] - public string? PacketEnd { get; set; } + [PacketListIndex(2, Length = -1, SpecialSeparator = " ")] + public List? Mates { get; set; } + + [PacketIndex(3)] + public long VisualId { get; set; } + + [PacketIndex(4)] + public short Speed { get; set; } } } \ No newline at end of file diff --git a/src/NosCore.Packets/ClientPackets/Mates/PtctlSubPacket.cs b/src/NosCore.Packets/ClientPackets/Mates/PtctlSubPacket.cs new file mode 100644 index 00000000..a40690cb --- /dev/null +++ b/src/NosCore.Packets/ClientPackets/Mates/PtctlSubPacket.cs @@ -0,0 +1,22 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// ----------------------------------- + +using NosCore.Packets.Attributes; + +namespace NosCore.Packets.ClientPackets.Mates +{ + public class PtctlSubPacket : PacketBase + { + [PacketIndex(0)] + public long VisualId { get; set; } + + [PacketIndex(1)] + public short PositionX { get; set; } + + [PacketIndex(2)] + public short PositionY { get; set; } + } +} diff --git a/src/NosCore.Packets/NosCore.Packets.csproj b/src/NosCore.Packets/NosCore.Packets.csproj index 07a612d4..13a75a97 100644 --- a/src/NosCore.Packets/NosCore.Packets.csproj +++ b/src/NosCore.Packets/NosCore.Packets.csproj @@ -12,7 +12,7 @@ https://github.com/NosCoreIO/NosCore.Packets.git nostale, noscore, chickenapi, nostale private server source, nostale emulator - 21.1.3 + 22.0.0 false NosCore's Packets (Nostale packets) defined over classes MIT diff --git a/src/NosCore.Packets/Serializer.cs b/src/NosCore.Packets/Serializer.cs index 4083fa45..faf16110 100644 --- a/src/NosCore.Packets/Serializer.cs +++ b/src/NosCore.Packets/Serializer.cs @@ -277,9 +277,11 @@ private Expression PacketSerializer(Expression injectedPacket, PacketIndexAttrib incrementExpr = Expression.Constant(!isFromList || !isOptionalSerie); } + // A sub-packet with its own separator is written after a single-space discriminator; an absent one + // keeps that space, or "13458 -1 -1" (sc_n with no equipment) runs together as "13458-1-1". return Expression.Condition( Expression.Equal(specificTypeExpression, Expression.Constant(null, typeof(object))), - Expression.Constant(indexAttr.IsOptional ? null : "-1", typeof(object)), + Expression.Constant(indexAttr.IsOptional ? null : discriminator == " " ? " -1" : "-1", typeof(object)), Expression.Convert(propExp, typeof(object)) ); } diff --git a/src/NosCore.Packets/ServerPackets/Mates/ScnPacket.cs b/src/NosCore.Packets/ServerPackets/Mates/ScnPacket.cs index 6b0161f9..a4f0e323 100644 --- a/src/NosCore.Packets/ServerPackets/Mates/ScnPacket.cs +++ b/src/NosCore.Packets/ServerPackets/Mates/ScnPacket.cs @@ -165,6 +165,13 @@ public class ScnPacket : PacketBase [PacketIndex(42)] public ScSkillDetails? Skill3Details { get; set; } + [PacketIndex(43)] + public short Unknown4 { get; set; } + + // 0 or 1 depending on the partner. + [PacketIndex(44)] + public short Unknown5 { get; set; } + public class ScSpDetails : PacketBase { [PacketIndex(0)] @@ -242,4 +249,6 @@ public class ScSkillDetails : PacketBase {(SkillId.SkillRank) :) // unknown 23 SpRank1 -1 // unknown 24 SpRank2 -1 // unknown 25 SpRank3 + 0 // unknown 26 + 0 // unknown 27 (0 or 1) */ \ No newline at end of file diff --git a/src/NosCore.Packets/ServerPackets/Mates/ScpPacket.cs b/src/NosCore.Packets/ServerPackets/Mates/ScpPacket.cs index ec193c8d..d700ffab 100644 --- a/src/NosCore.Packets/ServerPackets/Mates/ScpPacket.cs +++ b/src/NosCore.Packets/ServerPackets/Mates/ScpPacket.cs @@ -113,5 +113,29 @@ public class ScpPacket : PacketBase [PacketIndex(32)] public bool IsSummonable { get; set; } + + // 33 to 39 are always sent; with only 33 fields the client's pet window shows no pictures. + [PacketIndex(33)] + public long Unknow2 { get; set; } + + // 1, 2 or 3 depending on the pet (the same value as the pet's in packet field after Unknow6). + [PacketIndex(34)] + public long Unknow3 { get; set; } + + [PacketIndex(35)] + public long Unknow4 { get; set; } + + [PacketIndex(36)] + public long Unknow5 { get; set; } + + [PacketIndex(37)] + public long Unknow6 { get; set; } + + [PacketIndex(38)] + public long Unknow7 { get; set; } + + // 1 for every pet seen so far. + [PacketIndex(39)] + public long Unknow8 { get; set; } } } \ No newline at end of file diff --git a/src/NosCore.Packets/ServerPackets/Visibility/InNonPlayerSubPacket.cs b/src/NosCore.Packets/ServerPackets/Visibility/InNonPlayerSubPacket.cs index 28731c73..1139f036 100644 --- a/src/NosCore.Packets/ServerPackets/Visibility/InNonPlayerSubPacket.cs +++ b/src/NosCore.Packets/ServerPackets/Visibility/InNonPlayerSubPacket.cs @@ -77,5 +77,12 @@ public class InNonPlayerSubPacket : PacketBase [PacketIndex(22)] public byte Unknow6 { get; set; } + + // Sent for every non-player: 1, 2 or 3 for a pet (as in its sc_p), 0 for partners, npcs and monsters. + [PacketIndex(23)] + public byte Unknow7 { get; set; } + + [PacketIndex(24)] + public byte Unknow8 { get; set; } } } \ No newline at end of file diff --git a/test/NosCore.Packets.Tests/OfficialMatePacketTests.cs b/test/NosCore.Packets.Tests/OfficialMatePacketTests.cs new file mode 100644 index 00000000..9ff3861e --- /dev/null +++ b/test/NosCore.Packets.Tests/OfficialMatePacketTests.cs @@ -0,0 +1,118 @@ +// __ _ __ __ ___ __ ___ ___ +// | \| |/__\ /' _/ / _//__\| _ \ __| +// | | ' | \/ |`._`.| \_| \/ | v / _| +// |_|\__|\__/ |___/ \__/\__/|_|_\___| +// ----------------------------------- + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using NosCore.Packets.ClientPackets.Mates; +using NosCore.Packets.Enumerations; +using NosCore.Packets.Interfaces; +using NosCore.Packets.ServerPackets.Mates; +using NosCore.Packets.ServerPackets.Visibility; +using NosCore.Shared.Enumerations; +using System.Linq; + +namespace NosCore.Packets.Tests +{ + // The expected lines were recorded from the official server in October 2026, for a character with four pets and + // two partners. Server packets are built and compared with what that server wrote; ptctl, which the client + // sends, is read. + [TestClass] + public class OfficialMatePacketTests + { + private static readonly IDeserializer Deserializer = new Deserializer(PacketCorpus.SerializableTypes.ToList()); + private static readonly ISerializer Serializer = new Serializer(PacketCorpus.SerializableTypes.ToList()); + + [TestMethod] + public void APetIsListedAsTheOfficialServerListsIt() + { + var packet = new ScpPacket + { + PetId = 2, NpcMonsterVNum = 2682, TransportId = 1135622, Level = 48, Loyalty = 397, Experience = 86580, + DamageMinimum = 356, DamageMaximum = 480, Concentrate = 271, CriticalChance = 6, CriticalRate = 80, + CloseDefence = 185, DefenceDodge = 274, DistanceDefence = 158, DistanceDefenceDodge = 274, MagicDefence = 135, + Element = 4, FireResistance = 60, WaterResistance = 50, LightResistance = 40, DarkResistance = 70, + Hp = 10638, MaxHp = 10638, Mp = 708, MaxMp = 708, XpLoad = 476535, Name = "@", Unknow3 = 3, Unknow8 = 1 + }; + + Assert.AreEqual( + "sc_p 2 2682 1135622 48 397 86580 0 0 356 480 271 6 80 0 185 274 158 274 135 4 60 50 40 70 10638 10638 708 708 0 476535 0 @ 0 0 3 0 0 0 0 1", + Serializer.Serialize(packet)); + } + + [TestMethod] + public void APartnerIsListedAsTheOfficialServerListsIt() + { + var packet = new ScnPacket + { + PetId = 0, NpcMonsterVNum = 317, TransportId = 1135623, Level = 28, Loyalty = 1000, Experience = 13458, + Unknown3 = 1, MinimumAttack = 69, MaximumAttack = 69, Precision = 138, Defence = 29, DefenceDodge = 69, + DistanceDefence = 34, DistanceDodge = 69, DodgeRate = 26, Hp = 1902, HpMax = 2165, Mp = 877, MpMax = 877, + LevelXp = 190440, Name = "@", MorphId = -1, Unknown5 = 1 + }; + + Assert.AreEqual( + "sc_n 0 317 1135623 28 1000 13458 -1 -1 -1 -1 0 0 1 0 69 69 138 0 0 0 29 69 34 69 26 0 0 0 0 0 1902 2165 877 877 0 190440 @ -1 0 -1 -1 -1 -1 0 1", + Serializer.Serialize(packet)); + } + + [TestMethod] + public void AMateSpawnsAsOnTheOfficialServer() + { + var packet = new InPacket + { + VisualType = VisualType.Npc, VNum = "830", VisualId = 1135624, PositionX = 14, PositionY = 11, Direction = 2, + InNonPlayerSubPacket = new InNonPlayerSubPacket + { + InAliveSubPacket = new InAliveSubPacket { Hp = 100, Mp = 100 }, + GroupEffect = 3, Owner = 12988838, SpawnEffect = SpawnEffectType.NoEffect, Name = "@", Unknow1 = 2, + Unknow7 = 2 + } + }; + + Assert.AreEqual( + "in 2 830 1135624 14 11 2 100 100 0 0 3 12988838 1 0 -1 @ 2 -1 0 0 0 0 0 0 0 0 0 0 0 2 0", + Serializer.Serialize(packet)); + } + + [TestMethod] + public void AMonsterSpawnsAsOnTheOfficialServer() + { + var packet = new InPacket + { + VisualType = VisualType.Monster, VNum = "24", VisualId = 2887, PositionX = 27, PositionY = 145, Direction = 2, + InNonPlayerSubPacket = new InNonPlayerSubPacket + { + InAliveSubPacket = new InAliveSubPacket { Hp = 100, Mp = 100 } + } + }; + + Assert.AreEqual( + "in 3 24 2887 27 145 2 100 100 0 0 0 -1 0 0 -1 - 0 -1 0 0 0 0 0 0 0 0 0 0 0 0 0", + Serializer.Serialize(packet)); + } + + [TestMethod] + public void TheFollowReportListsEachMateThenOneSpeed() + { + var packet = (PtctlPacket)Deserializer.Deserialize("ptctl 1 2 1135623 50 132 1135626 49 133 1135623 9"); + + Assert.AreEqual(2, packet.Amount); + Assert.AreEqual((1135623L, (short)50, (short)132), + (packet.Mates![0]!.VisualId, packet.Mates[0]!.PositionX, packet.Mates[0]!.PositionY)); + Assert.AreEqual((1135626L, (short)49, (short)133), + (packet.Mates[1]!.VisualId, packet.Mates[1]!.PositionX, packet.Mates[1]!.PositionY)); + Assert.AreEqual(1135623, packet.VisualId); + Assert.AreEqual(9, packet.Speed); + } + + [TestMethod] + [DataRow("ptctl 1 1 1135626 50 149 1135626 7")] + [DataRow("ptctl 1 2 1135623 50 132 1135626 49 133 1135623 9")] + public void AFollowReportWritesBackUnchanged(string line) + { + Assert.AreEqual(line, Serializer.Serialize(Deserializer.Deserialize(line))); + } + } +} diff --git a/test/NosCore.Packets.Tests/SerializerCorpusTests.ExpressionTreeSerializerOutputIsPinnedForEveryPacket.verified.txt b/test/NosCore.Packets.Tests/SerializerCorpusTests.ExpressionTreeSerializerOutputIsPinnedForEveryPacket.verified.txt index 0e5870f1..3f275359 100644 --- a/test/NosCore.Packets.Tests/SerializerCorpusTests.ExpressionTreeSerializerOutputIsPinnedForEveryPacket.verified.txt +++ b/test/NosCore.Packets.Tests/SerializerCorpusTests.ExpressionTreeSerializerOutputIsPinnedForEveryPacket.verified.txt @@ -180,8 +180,8 @@ PslPacket#1 => psl 1 PslPacket#7 => psl 0 PsopPacket#1 => ps_op 21 44 83 PsopPacket#7 => ps_op 74 75 77 -PtctlPacket#1 => ptctl 21 44 s12383 -PtctlPacket#7 => ptctl 74 75 s19477 +PtctlPacket#1 => ptctl 21 44 83 53 21 82 87 55 52 90 +PtctlPacket#7 => ptctl 74 75 77 8 43 82 37 53 78 12 SayPPacket#1 => say_p 21 s30144 SayPPacket#7 => say_p 74 s275 SuCtlPacket#1 => suctl 21 44 83 3 21 @@ -542,10 +542,10 @@ PetskiPacket#1 => petski 21 44 83 PetskiPacket#7 => petski 74 75 77 PsdPacket#1 => psd 21 PsdPacket#7 => psd 74 -ScnPacket#1 => sc_n 21 44 83 53 21 82 87.55.52 90.80.24 27.23.69 90.20.18 35 9 75 68 79 97 1 68 41 93 71 31 99 83 94 74 40 54 48 31 23 57 67 95 1 4 s19011 59 0 52 57 5 0 39 2 17 0 -ScnPacket#7 => sc_n 74 75 77 8 43 82 37.53.78 12.53.59 96.45.47 58.74.95 71 58 9 5 81 27 96 55 30 7 6 24 78 44 68 41 54 77 86 75 59 33 86 63 0 80 s27111 9 1 85 11 66 0 88 4 58 7 -ScpPacket#1 => sc_p 21 44 83 53 21 82 87 55 52 90 80 24 27 23 69 90 20 18 35 9 75 68 79 97 1 68 41 93 0 31 0 s21783 1 -ScpPacket#7 => sc_p 74 75 77 8 43 82 37 53 78 12 53 59 96 45 47 58 74 95 71 58 9 5 81 27 96 55 30 7 1 24 1 s17244 1 +ScnPacket#1 => sc_n 21 44 83 53 21 82 87.55.52 90.80.24 27.23.69 90.20.18 35 9 75 68 79 97 1 68 41 93 71 31 99 83 94 74 40 54 48 31 23 57 67 95 1 4 s19011 59 0 52 57 5 0 39 2 17 0 42 74 +ScnPacket#7 => sc_n 74 75 77 8 43 82 37.53.78 12.53.59 96.45.47 58.74.95 71 58 9 5 81 27 96 55 30 7 6 24 78 44 68 41 54 77 86 75 59 33 86 63 0 80 s27111 9 1 85 11 66 0 88 4 58 7 71 78 +ScpPacket#1 => sc_p 21 44 83 53 21 82 87 55 52 90 80 24 27 23 69 90 20 18 35 9 75 68 79 97 1 68 41 93 0 31 0 s21783 1 74 40 54 48 31 23 57 +ScpPacket#7 => sc_p 74 75 77 8 43 82 37 53 78 12 53 59 96 45 47 58 74 95 71 58 9 5 81 27 96 55 30 7 1 24 1 s17244 1 41 54 77 86 75 59 33 AtPacket#1 => at 21 44 83 53 21 82 87 55 52 90 80 AtPacket#7 => at 74 75 77 8 43 82 37 53 78 12 53 CMapPacket#1 => c_map 21 44 0 @@ -788,8 +788,8 @@ ZzimPacket#1 => zzim ZzimPacket#7 => zzim ClPacket#1 => cl 21 1 0 ClPacket#7 => cl 74 0 0 -InPacket#1 => in 1 s30144 s12383 53 21 82 87 3 0 0 68 4 27.23.69.90.20.18.35.9.75.68 79 97 0 68 41 93 0 31 0 83 9474 4054 48.31 s17923 57 0 95 2 4 11 59 0|1 0 5 4 39 98 17 1 42 74 92 97 6 26 77 25 2 1 1 s15588 41 59 3 23 38 75 57 87 65 0 81 71 12 -InPacket#7 => in 9 s275 s19477 8 43 82 37 1 0 2 49 4 96.45.47.58.74.95.71.58.9.5 81 27 1 55 30 7 1 24 1 44 6841 5477 86.75 s10759 33 1 63 2 80 11 9 1|0 0 66 92 88 32 58 0 71 78 69 24 46 93 88 53 1 0 91 s6425 11 90 29 87 63 46 29 70 51 1 181 90 8 +InPacket#1 => in 1 s30144 s12383 53 21 82 87 3 0 0 68 4 27.23.69.90.20.18.35.9.75.68 79 97 0 68 41 93 0 31 0 83 9474 4054 48.31 s17923 57 0 95 2 4 11 59 0|1 0 5 4 39 98 17 1 42 74 92 97 6 26 77 25 2 1 1 s15588 41 59 3 23 38 75 57 87 65 0 81 71 12 6 99 +InPacket#7 => in 9 s275 s19477 8 43 82 37 1 0 2 49 4 96.45.47.58.74.95.71.58.9.5 81 27 1 55 30 7 1 24 1 44 6841 5477 86.75 s10759 33 1 63 2 80 11 9 1|0 0 66 92 88 32 58 0 71 78 69 24 46 93 88 53 1 0 91 s6425 11 90 29 87 63 46 29 70 51 1 181 90 8 59 93 FStashAllPacket#1 => f_stash_all 21 44.83.53.21.82.87.55 52.90.80.24.27.23.69 FStashAllPacket#7 => f_stash_all 74 75.77.8.43.82.37.53 78.12.53.59.96.45.47 FStashClientPacket#1 => f_stash 21.44.83.53.21.82.87 55.52.90.80.24.27.23 diff --git a/test/NosCore.Packets.Tests/SerializerTest.cs b/test/NosCore.Packets.Tests/SerializerTest.cs index 8b8b8464..153d0a67 100644 --- a/test/NosCore.Packets.Tests/SerializerTest.cs +++ b/test/NosCore.Packets.Tests/SerializerTest.cs @@ -742,7 +742,7 @@ public void GenerateInPacketIsNotCorruptedForMonster() } }; var packet = Serializer.Serialize(mapMonsterPacket); - Assert.AreEqual("in 3 - 0 0 0 0 0 0 0 0 0 -1 1 0 -1 - 2 -1 0 0 0 0 0 0 0 0 0 0 0", packet); + Assert.AreEqual("in 3 - 0 0 0 0 0 0 0 0 0 -1 1 0 -1 - 2 -1 0 0 0 0 0 0 0 0 0 0 0 0 0", packet); } [TestMethod] @@ -771,7 +771,7 @@ public void GenerateInPacketIsNotCorruptedForNpc() } }; var packet = Serializer.Serialize(mapNpcPacket); - Assert.AreEqual("in 2 - 0 0 0 0 0 0 0 0 0 -1 1 0 -1 - 2 -1 0 0 0 0 0 0 0 0 0 0 0", packet); + Assert.AreEqual("in 2 - 0 0 0 0 0 0 0 0 0 -1 1 0 -1 - 2 -1 0 0 0 0 0 0 0 0 0 0 0 0 0", packet); }