From b0f354fe14fd8a56d25a2df37499c4beea4f28c6 Mon Sep 17 00:00:00 2001 From: erwan-joly Date: Sun, 30 Aug 2026 21:09:37 +1200 Subject: [PATCH] fix(serializer): key the registry by Type so name-colliding packets serialize The delegate dictionary was keyed by the simple class name; ten packets share a name across ClientPackets and ServerPackets namespaces, so one of each pair lost its registration and serializing it invoked the other type's delegate, throwing ArgumentException. Keying by Type gives every packet its own delegate and removes the collision handling outright. The corpus snapshot now pins all 398 packets with no exception entries, disambiguating colliding names by namespace, and is regenerated against current master - it also folds in the pstash header rename and the four newly headered packets that landed after the corpus PR branched, which is what currently fails CI on master. Co-Authored-By: Claude Opus 5 (1M context) --- src/NosCore.Packets/Serializer.cs | 17 +--- ...rOutputIsPinnedForEveryPacket.verified.txt | 98 ++++++++++--------- .../SerializerCorpusTests.cs | 35 +++---- 3 files changed, 70 insertions(+), 80 deletions(-) diff --git a/src/NosCore.Packets/Serializer.cs b/src/NosCore.Packets/Serializer.cs index ba4f812..4083fa4 100644 --- a/src/NosCore.Packets/Serializer.cs +++ b/src/NosCore.Packets/Serializer.cs @@ -18,7 +18,7 @@ public class Serializer : ISerializer { private const string InjectionKey = "IPacketToInject"; - private readonly Dictionary _packetSerializerDictionary = new Dictionary(); + private readonly Dictionary _packetSerializerDictionary = new Dictionary(); public Serializer(IEnumerable types) @@ -35,23 +35,14 @@ public void Initialize() where T : PacketBase var packetSerializerExpressionFalse = PacketSerializerExpression(); if (packetSerializerExpressionFalse != null) { - if (_packetSerializerDictionary.ContainsKey(typeof(T).Name)) - { - if (typeof(T).Namespace!.Contains("ClientPackets")) - { - return; - } - - _packetSerializerDictionary.Remove(typeof(T).Name); - } - _packetSerializerDictionary.Add(typeof(T).Name, packetSerializerExpressionFalse.Compile()); + _packetSerializerDictionary[typeof(T)] = packetSerializerExpressionFalse.Compile(); } } public string Serialize(IPacket packet) { var realType = packet.GetType(); - var deg = _packetSerializerDictionary[packet.GetType().Name]; + var deg = _packetSerializerDictionary[packet.GetType()]; var fullString = (string)deg.DynamicInvoke(packet, false)!; if (fullString.Contains(InjectionKey)) { @@ -63,7 +54,7 @@ public string Serialize(IPacket packet) fullString = fullString .Remove(place, InjectionKey.Length) .Insert(place, - (string)_packetSerializerDictionary[value!.GetType().Name].DynamicInvoke(value, true)!); + (string)_packetSerializerDictionary[value!.GetType()].DynamicInvoke(value, true)!); } } diff --git a/test/NosCore.Packets.Tests/SerializerCorpusTests.ExpressionTreeSerializerOutputIsPinnedForEveryPacket.verified.txt b/test/NosCore.Packets.Tests/SerializerCorpusTests.ExpressionTreeSerializerOutputIsPinnedForEveryPacket.verified.txt index 57a0b3c..a167144 100644 --- a/test/NosCore.Packets.Tests/SerializerCorpusTests.ExpressionTreeSerializerOutputIsPinnedForEveryPacket.verified.txt +++ b/test/NosCore.Packets.Tests/SerializerCorpusTests.ExpressionTreeSerializerOutputIsPinnedForEveryPacket.verified.txt @@ -58,8 +58,8 @@ WhisperPacket#1 => / s15521 WhisperPacket#7 => / s15674 FamilyDeputyPacket#1 => %Familydeputy s15521 FamilyDeputyPacket#7 => %Familydeputy s15674 -FamilyDismissPacket#1 => %Familydismiss s15521 -FamilyDismissPacket#7 => %Familydismiss s15674 +NosCore.Packets.ClientPackets.Commands.FamilyDismissPacket#1 => %Familydismiss s15521 +NosCore.Packets.ClientPackets.Commands.FamilyDismissPacket#7 => %Familydismiss s15674 FamilyHeadPacket#1 => %Familyhead s15521 FamilyHeadPacket#7 => %Familyhead s15674 FamilyInvitePacket#1 => %Familyinvite s15521 @@ -80,8 +80,8 @@ PartyLocationPacket#1 => $Partylocation s15521 PartyLocationPacket#7 => $Partylocation s15674 TimeSpaceInvitePacket#1 => $Accompany s15521 TimeSpaceInvitePacket#7 => $Accompany s15674 -DropPacket#1 => <> -DropPacket#7 => <> +NosCore.Packets.ClientPackets.Drops.DropPacket#1 => drop 21 44 83 53 21 1 87 55 +NosCore.Packets.ClientPackets.Drops.DropPacket#7 => drop 74 75 77 8 43 1 37 53 GetPacket#1 => get 1 44 83 53 GetPacket#7 => get 9 75 77 8 FbPacket#1 => fb s15521 @@ -106,8 +106,8 @@ CreateFamilyPacket#1 => glmk s15521 CreateFamilyPacket#7 => glmk s15674 FAuthPacket#1 => fauth 1 44 83 FAuthPacket#7 => fauth 2 75 77 -FamilyDismissPacket#1 => <> -FamilyDismissPacket#7 => <> +NosCore.Packets.ClientPackets.Families.FamilyDismissPacket#1 => glrm +NosCore.Packets.ClientPackets.Families.FamilyDismissPacket#7 => glrm FamilyManagementPacket#1 => fmg 1 44 FamilyManagementPacket#7 => fmg 2 75 FhistCtsPacket#1 => fhis_cts @@ -118,8 +118,8 @@ FwsPacket#1 => fws 21 FwsPacket#7 => fws 74 GListPacket#1 => glist 21 GListPacket#7 => glist 74 -GidxPacket#1 => <> -GidxPacket#7 => <> +NosCore.Packets.ClientPackets.Families.GidxPacket#1 => gidx 1 44 83 s15853 s14221 82 0|0 +NosCore.Packets.ClientPackets.Families.GidxPacket#7 => gidx 9 75 77 s30408 s14143 82 0|0 JoinFamilyPacket#1 => gjoin 21 44 JoinFamilyPacket#7 => gjoin 74 75 TodayPacket#1 => today_cts @@ -200,8 +200,8 @@ MinigamePacket#1 => mg 21 44 83 53 21 MinigamePacket#7 => mg 74 75 77 8 43 RmvobjPacket#1 => rmvobj 21 RmvobjPacket#7 => rmvobj 74 -UseObjPacket#1 => <> -UseObjPacket#7 => <> +NosCore.Packets.ClientPackets.Miniland.UseObjPacket#1 => useobj s15521 44 +NosCore.Packets.ClientPackets.Miniland.UseObjPacket#7 => useobj s15674 75 ClientDirPacket#1 => dir 21 9 83 ClientDirPacket#7 => dir 74 0 77 PreqPacket#1 => preq 21 @@ -226,8 +226,8 @@ BpClosePacket#1 => bp_close BpClosePacket#7 => bp_close BpOpenPacket#1 => bp_open BpOpenPacket#7 => bp_open -BpmPacket#1 => <> -BpmPacket#7 => <> +NosCore.Packets.ClientPackets.Player.BpmPacket#1 => bpm 21 1 83 53 21 82 20 1 52 90 80 24 27 24 0 90 20 18 35 +NosCore.Packets.ClientPackets.Player.BpmPacket#7 => bpm 74 0 77 8 43 82 14 0 78 12 53 59 96 14 1 58 74 95 71 BpoPacket#1 => bpo BpoPacket#7 => bpo BppPacket#1 => bpp 21 44 0 53 21 82 87 55 1 1 1 24 27 23 69 90 1 1 0 @@ -238,10 +238,10 @@ ComplPacket#1 => compl 21 ComplPacket#7 => compl 74 EquipmentInfoPacket#1 => eqinfo 21 44 83 53 EquipmentInfoPacket#7 => eqinfo 74 75 77 8 -NpInfoPacket#1 => <> -NpInfoPacket#7 => <> -RsfiPacket#1 => <> -RsfiPacket#7 => <> +NosCore.Packets.ClientPackets.Player.NpInfoPacket#1 => npinfo 21 +NosCore.Packets.ClientPackets.Player.NpInfoPacket#7 => npinfo 74 +NosCore.Packets.ClientPackets.Player.RsfiPacket#1 => rsfi 21 44 83 53 21 82 +NosCore.Packets.ClientPackets.Player.RsfiPacket#7 => rsfi 74 75 77 8 43 82 SnapPacket#1 => snap SnapPacket#7 => snap TitEqPacket#1 => tit_eq 21 44 @@ -282,8 +282,8 @@ SellListPacket#1 => sell_list 21 44.83.53 21.82.87 SellListPacket#7 => sell_list 74 75.77.8 43.82.37 SellPacket#1 => sell 21 44 83 SellPacket#7 => sell 74 75 77 -ShopClosePacket#1 => <> -ShopClosePacket#7 => <> +NosCore.Packets.ClientPackets.Shops.ShopClosePacket#1 => shopclose +NosCore.Packets.ClientPackets.Shops.ShopClosePacket#7 => shopclose ShoppingPacket#1 => shopping 21 44 3 53 ShoppingPacket#7 => shopping 74 75 2 8 SCarrierPacket#1 => s_carrier 21 44 83 @@ -292,10 +292,10 @@ SpTransformPacket#1 => sl 10 44 83 53 21 82 SpTransformPacket#7 => sl 10 75 77 8 43 82 GopPacket#1 => gop 1 1 GopPacket#7 => gop 1 0 -GuriPacket#1 => <> -GuriPacket#7 => <> -MallPacket#1 => <> -MallPacket#7 => <> +NosCore.Packets.ClientPackets.UI.GuriPacket#1 => guri 6 44 83 53 s14221 +NosCore.Packets.ClientPackets.UI.GuriPacket#7 => guri 40 75 77 8 s14143 +NosCore.Packets.ClientPackets.UI.MallPacket#1 => mall 21 +NosCore.Packets.ClientPackets.UI.MallPacket#7 => mall 74 DepositPacket#1 => deposit 1 44 83 53 0 DepositPacket#7 => deposit 2 75 77 8 0 FDepositPacket#1 => f_deposit 1 44 83 53 21 @@ -344,12 +344,16 @@ CtPacket#1 => ct 1 44 3 53 21 82 87 CtPacket#7 => ct 9 75 2 8 43 82 37 DiePacket#1 => die 1 44 3 53 DiePacket#7 => die 9 75 2 8 +DmPacket#1 => dm 1 44 83 3 +DmPacket#7 => dm 9 75 77 3 MscPacket#1 => ms_c 21 MscPacket#7 => ms_c 74 MslotPacket#1 => mslot 21 0 MslotPacket#7 => mslot 74 -1 RankCoolPacket#1 => rank_cool 21 44 83 RankCoolPacket#7 => rank_cool 74 75 77 +RcPacket#1 => rc 1 44 83 53 +RcPacket#7 => rc 9 75 77 8 RevivePacket#1 => revive 1 44 83 RevivePacket#7 => revive 9 75 77 SkillResetPacket#1 => sr 21 @@ -414,8 +418,8 @@ CatPacket#1 => ca_t 21 44 CatPacket#7 => ca_t 74 75 CharScPacket#1 => char_sc 1 44 83 CharScPacket#7 => char_sc 9 75 77 -DropPacket#1 => drop 21 44 83 53 21 1 87 -DropPacket#7 => drop 74 75 77 8 43 1 37 +NosCore.Packets.ServerPackets.Entities.DropPacket#1 => drop 21 44 83 53 21 1 87 +NosCore.Packets.ServerPackets.Entities.DropPacket#7 => drop 74 75 77 8 43 1 37 MovePacket#1 => mv 1 44 83 53 21 MovePacket#7 => mv 9 75 77 8 43 OutPacket#1 => out 1 44 @@ -432,8 +436,12 @@ DgPacket#1 => dg 21 44 83 53 DgPacket#7 => dg 74 75 77 8 EsfPacket#1 => esf 21 EsfPacket#7 => esf 74 +EventPacket#1 => evnt 10 1 83 53 +EventPacket#7 => evnt 10 0 77 8 EventbPacket#1 => evtb 21 44 83 53 21 1 87 55 52 90 80 24 27 23 69 90 20 18 35 9 75 68 79 97 1 68 41 93 EventbPacket#7 => evtb 74 75 77 8 43 1 37 53 78 12 53 59 96 45 47 58 74 95 71 58 9 5 81 27 96 55 30 7 +FbtPacket#1 => fbt 21 44 83 53 +FbtPacket#7 => fbt 74 75 77 8 RaidPacket#1 => raid 21 44 RaidPacket#7 => raid 74 75 RaidbfPacket#1 => raidbf 21 44 83 @@ -460,8 +468,8 @@ GInfoPacket#1 => ginfo s15521 s30144 1 53 21 82 87 55 0 1 1 1 0 2 0 1 s920 GInfoPacket#7 => ginfo s15674 s275 1 8 43 82 37 53 2 1 0 0 1 0 0 1 s28374 GexpPacket#1 => gexp 21|44 83|53 GexpPacket#7 => gexp 74|75 77|8 -GidxPacket#1 => gidx 1 44 83 s15853 21 1|0 -GidxPacket#7 => gidx 9 75 77 s30408 43 1|0 +NosCore.Packets.ServerPackets.Families.GidxPacket#1 => gidx 1 44 83 s15853 21 1|0 +NosCore.Packets.ServerPackets.Families.GidxPacket#7 => gidx 9 75 77 s30408 43 1|0 GmsgPacket#1 => gmsg 21|s30144 83|s15853 GmsgPacket#7 => gmsg 74|s275 77|s30408 FtptPacket#1 => ftpt 21 @@ -570,8 +578,8 @@ MlobjPacket#1 => mlobj 0 44 83 53 21 82 87 55 1 1 MlobjPacket#7 => mlobj 1 75 77 8 43 82 37 53 1 1 MlobjlstPacket#1 => mlobjlst 21.1.83.53.21.82.87.55.1.1 80.1.27.23.69.90.20.18.0.0 MlobjlstPacket#7 => mlobjlst 74.0.77.8.43.82.37.53.1.1 53.0.96.45.47.58.74.95.0.1 -UseObjPacket#1 => useobj s15521 44 -UseObjPacket#7 => useobj s15674 75 +NosCore.Packets.ServerPackets.Miniland.UseObjPacket#1 => useobj s15521 44 +NosCore.Packets.ServerPackets.Miniland.UseObjPacket#7 => useobj s15674 75 DirPacket#1 => dir 1 44 83 DirPacket#7 => dir 9 75 77 NpcReqPacket#1 => npc_req 1 44 83 @@ -584,8 +592,8 @@ PstPacket#1 => pst 1 44 83 53 21 82 87 55 0 90 80.24 PstPacket#7 => pst 9 75 77 8 43 82 37 53 0 12 53.59 BnPacket#1 => bn 21 s30144 BnPacket#7 => bn 74 s275 -BpmPacket#1 => bpm 21 44 83 -BpmPacket#7 => bpm 74 75 77 +NosCore.Packets.ServerPackets.Player.BpmPacket#1 => bpm 21 44 83 +NosCore.Packets.ServerPackets.Player.BpmPacket#7 => bpm 74 75 77 CInfoPacket#1 => c_info s15521 s30144 83 53 s14221 82 -1 1 2 90 0 24 27 23 0 90 20 1 35 9 CInfoPacket#7 => c_info s15674 s275 77 8 s14143 82 1 1 3 24 3 59 96 45 0 58 74 0 71 58 CInfoResetPacket#1 => c_info_reset @@ -610,16 +618,14 @@ LevPacket#1 => lev 21 44 83 53 21 82 87 55 52 90 80 24 LevPacket#7 => lev 74 75 77 8 43 82 37 53 78 12 53 59 LevelUpPacket#1 => levelup 21 LevelUpPacket#7 => levelup 74 -NpInfoPacket#1 => npinfo 21 -NpInfoPacket#7 => npinfo 74 +NosCore.Packets.ServerPackets.Player.NpInfoPacket#1 => npinfo 21 +NosCore.Packets.ServerPackets.Player.NpInfoPacket#7 => npinfo 74 PSexPacket#1 => p_sex 1 PSexPacket#7 => p_sex 0 RagePacket#1 => rage 21 44 RagePacket#7 => rage 74 75 -RcPacket#1 => rc 21 44 83 53 -RcPacket#7 => rc 74 75 77 8 -RsfiPacket#1 => rsfi 21 44 83 53 21 82 -RsfiPacket#7 => rsfi 74 75 77 8 43 82 +NosCore.Packets.ServerPackets.Player.RsfiPacket#1 => rsfi 21 44 83 53 21 82 +NosCore.Packets.ServerPackets.Player.RsfiPacket#7 => rsfi 74 75 77 8 43 82 ScPacket#1 => sc 21 44 83 53 21 82 87 55 52 90 80 24 27 23 69 90 20 18 35 9 75 68 79 97 ScPacket#7 => sc 74 75 77 8 43 82 37 53 78 12 53 59 96 45 47 58 74 95 71 58 9 5 81 27 ScrPacket#1 => scr 21 44 83 53 21 82 @@ -734,8 +740,8 @@ DlgiPacket#1 => dlgi #gbox^1^44^0 #gbox^1^21^1 2167 55 0 DlgiPacket#7 => dlgi #gbox^2^75^0 #gbox^0^43^1 2157 53 0 FishOpenPacket#1 => fishopen FishOpenPacket#7 => fishopen -GuriPacket#1 => guri 6 44 83 53 21 82 -GuriPacket#7 => guri 40 75 77 8 43 82 +NosCore.Packets.ServerPackets.UI.GuriPacket#1 => guri 6 44 83 53 21 82 +NosCore.Packets.ServerPackets.UI.GuriPacket#7 => guri 40 75 77 8 43 82 IconPacket#1 => icon 1 44 83 53 IconPacket#7 => icon 9 75 77 8 InfoPacket#1 => info s15521 @@ -744,8 +750,8 @@ Infoi2Packet#1 => infoi2 1371 44 0 Infoi2Packet#7 => infoi2 1524 75 0 InfoiPacket#1 => infoi 1371 0 0 44 InfoiPacket#7 => infoi 1524 0 0 75 -MallPacket#1 => mall 21 -MallPacket#7 => mall 74 +NosCore.Packets.ServerPackets.UI.MallPacket#1 => mall 21 +NosCore.Packets.ServerPackets.UI.MallPacket#7 => mall 74 ModalPacket#1 => modal 21 s30144 ModalPacket#7 => modal 74 s275 ModaliPacket#1 => modali 21 1844 83 0 0 @@ -766,8 +772,8 @@ QnaiPacket#1 => qnai #gbox^1^44^0 1703 21 0 0 QnaiPacket#7 => qnai #gbox^2^75^0 2108 43 0 0 RdiPacket#1 => rdi 21 44 RdiPacket#7 => rdi 74 75 -ShopClosePacket#1 => shopclose 21 -ShopClosePacket#7 => shopclose 74 +NosCore.Packets.ServerPackets.UI.ShopClosePacket#1 => shopclose 21 +NosCore.Packets.ServerPackets.UI.ShopClosePacket#7 => shopclose 74 TARankPacket#1 => ta_rank 21.44.83.s15853.1.0.87 55.52.90.s13380.4.1.23 s30369 TARankPacket#7 => ta_rank 74.75.77.s30408.3.0.37 53.78.12.s27953.4.0.45 s21847 TeamArenaClosePacket#1 => ta_close @@ -788,10 +794,10 @@ 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 FStashClientPacket#7 => f_stash 74.75.77.8.43.82.37 53.78.12.53.59.96.45 -PStashAllPacket#1 => p_stash_all 21 44.83.53.21.82.87.55 52.90.80.24.27.23.69 -PStashAllPacket#7 => p_stash_all 74 75.77.8.43.82.37.53 78.12.53.59.96.45.47 -PStashClientPacket#1 => p_stash 21.44.83.53.21.82.87 55.52.90.80.24.27.23 -PStashClientPacket#7 => p_stash 74.75.77.8.43.82.37 53.78.12.53.59.96.45 +PStashAllPacket#1 => pstash_all 21 44.83.53.21.82.87.55 52.90.80.24.27.23.69 +PStashAllPacket#7 => pstash_all 74 75.77.8.43.82.37.53 78.12.53.59.96.45.47 +PStashClientPacket#1 => pstash 21.44.83.53.21.82.87 55.52.90.80.24.27.23 +PStashClientPacket#7 => pstash 74.75.77.8.43.82.37 53.78.12.53.59.96.45 StashAllPacket#1 => stash_all 21 44.83.53.21.82.87.55 52.90.80.24.27.23.69 StashAllPacket#7 => stash_all 74 75.77.8.43.82.37.53 78.12.53.59.96.45.47 StashClientPacket#1 => stash 21.44.83.53.21.82.87 55.52.90.80.24.27.23 diff --git a/test/NosCore.Packets.Tests/SerializerCorpusTests.cs b/test/NosCore.Packets.Tests/SerializerCorpusTests.cs index 7a64f39..ccbf163 100644 --- a/test/NosCore.Packets.Tests/SerializerCorpusTests.cs +++ b/test/NosCore.Packets.Tests/SerializerCorpusTests.cs @@ -47,15 +47,9 @@ public Task ExpressionTreeSerializerOutputIsPinnedForEveryPacket() private static string SnapshotDirectory => Path.Combine( Directory.GetCurrentDirectory(), "..", "..", ".."); - // Serializer keys _packetSerializerDictionary by typeof(T).Name - the SIMPLE - // name - so two packets sharing a class name across namespaces overwrite each - // other, and Initialize resolves the clash by letting the non-ClientPackets one - // win. Serializing the loser then invokes the winner's delegate and throws - // ArgumentException. Each server currently registers a filtered subset of packet - // types, which is why this has not bitten in production. - // - // Pinned rather than fixed here: generated per-type methods cannot collide by - // simple name, so porting to the generator removes the failure mode outright. + // Ten packet class names exist in both a ClientPackets and a ServerPackets + // namespace. The serializer registry is keyed by Type, so both variants keep + // their own delegate; this pin only guards against new accidental duplicates. private static readonly string[] KnownSimpleNameCollisions = { "BpmPacket", "DropPacket", "FamilyDismissPacket", "GidxPacket", "GuriPacket", @@ -63,7 +57,7 @@ public Task ExpressionTreeSerializerOutputIsPinnedForEveryPacket() }; [TestMethod] - public void OnlyTheKnownSimpleNameCollisionsFailToSerialize() + public void EveryPacketSerializes() { var failures = new List(); @@ -75,18 +69,13 @@ public void OnlyTheKnownSimpleNameCollisionsFailToSerialize() } catch (Exception ex) { - failures.Add($"{type.Name}: {ex.GetType().Name} {ex.Message}"); + failures.Add($"{type.FullName}: {ex.GetType().Name} {ex.Message}"); } } - var unexpected = failures - .Where(f => !KnownSimpleNameCollisions.Any(known => - f.StartsWith(known + ":", StringComparison.Ordinal))) - .ToList(); - - Assert.AreEqual(0, unexpected.Count, - $"{unexpected.Count} packets threw for a reason other than the known name " + - $"collisions:{Environment.NewLine}{string.Join(Environment.NewLine, unexpected)}"); + Assert.AreEqual(0, failures.Count, + $"{failures.Count} packets failed to serialize:" + + $"{Environment.NewLine}{string.Join(Environment.NewLine, failures)}"); } [TestMethod] @@ -107,11 +96,15 @@ public void CollidingPacketNamesAreStillDistinctTypes() // Two seeds per packet: one exercises the populated path, the other shifts every // value so an off-by-one in separator or optional-run handling cannot hide behind - // a coincidence. + // a coincidence. Colliding simple names are disambiguated by namespace so each + // snapshot line maps to exactly one type. private static IEnumerable Corpus() { foreach (var type in PacketCorpus.PacketTypes) { + var id = KnownSimpleNameCollisions.Contains(type.Name) + ? $"{type.Namespace}.{type.Name}" + : type.Name; foreach (var seed in new[] { 1, 7 }) { string result; @@ -124,7 +117,7 @@ private static IEnumerable Corpus() result = $"<>"; } - yield return $"{type.Name}#{seed} => {result}"; + yield return $"{id}#{seed} => {result}"; } } }