diff options
7 files changed, 229 insertions, 26 deletions
diff --git a/OpenSim/Region/CoreModules/Avatar/Attachments/AttachmentsModule.cs b/OpenSim/Region/CoreModules/Avatar/Attachments/AttachmentsModule.cs index 9eb0e38..eccf7a6 100644 --- a/OpenSim/Region/CoreModules/Avatar/Attachments/AttachmentsModule.cs +++ b/OpenSim/Region/CoreModules/Avatar/Attachments/AttachmentsModule.cs | |||
@@ -251,6 +251,15 @@ namespace OpenSim.Region.CoreModules.Avatar.Attachments | |||
251 | // m_log.DebugFormat( | 251 | // m_log.DebugFormat( |
252 | // "[ATTACHMENTS MODULE]: Attaching object {0} {1} to {2} point {3} from ground (silent = {4})", | 252 | // "[ATTACHMENTS MODULE]: Attaching object {0} {1} to {2} point {3} from ground (silent = {4})", |
253 | // group.Name, group.LocalId, sp.Name, attachmentPt, silent); | 253 | // group.Name, group.LocalId, sp.Name, attachmentPt, silent); |
254 | |||
255 | if (group.GetSittingAvatarsCount() != 0) | ||
256 | { | ||
257 | // m_log.WarnFormat( | ||
258 | // "[ATTACHMENTS MODULE]: Ignoring request to attach {0} {1} to {2} on {3} since {4} avatars are still sitting on it", | ||
259 | // group.Name, group.LocalId, sp.Name, attachmentPt, group.GetSittingAvatarsCount()); | ||
260 | |||
261 | return false; | ||
262 | } | ||
254 | 263 | ||
255 | if (sp.GetAttachments(attachmentPt).Contains(group)) | 264 | if (sp.GetAttachments(attachmentPt).Contains(group)) |
256 | { | 265 | { |
diff --git a/OpenSim/Region/CoreModules/Avatar/Attachments/Tests/AttachmentsModuleTests.cs b/OpenSim/Region/CoreModules/Avatar/Attachments/Tests/AttachmentsModuleTests.cs index 5dcbd28..3e06900 100644 --- a/OpenSim/Region/CoreModules/Avatar/Attachments/Tests/AttachmentsModuleTests.cs +++ b/OpenSim/Region/CoreModules/Avatar/Attachments/Tests/AttachmentsModuleTests.cs | |||
@@ -118,7 +118,7 @@ namespace OpenSim.Region.CoreModules.Avatar.Attachments.Tests | |||
118 | 118 | ||
119 | Scene scene = CreateDefaultTestScene(); | 119 | Scene scene = CreateDefaultTestScene(); |
120 | UserAccount ua1 = UserAccountHelpers.CreateUserWithInventory(scene, 0x1); | 120 | UserAccount ua1 = UserAccountHelpers.CreateUserWithInventory(scene, 0x1); |
121 | ScenePresence sp = SceneHelpers.AddScenePresence(scene, ua1.PrincipalID); | 121 | ScenePresence sp = SceneHelpers.AddScenePresence(scene, ua1); |
122 | 122 | ||
123 | string attName = "att"; | 123 | string attName = "att"; |
124 | 124 | ||
@@ -154,6 +154,36 @@ namespace OpenSim.Region.CoreModules.Avatar.Attachments.Tests | |||
154 | // TestHelpers.DisableLogging(); | 154 | // TestHelpers.DisableLogging(); |
155 | } | 155 | } |
156 | 156 | ||
157 | /// <summary> | ||
158 | /// Test that we do not attempt to attach an in-world object that someone else is sitting on. | ||
159 | /// </summary> | ||
160 | [Test] | ||
161 | public void TestAddSatOnAttachmentFromGround() | ||
162 | { | ||
163 | TestHelpers.InMethod(); | ||
164 | // TestHelpers.EnableLogging(); | ||
165 | |||
166 | Scene scene = CreateDefaultTestScene(); | ||
167 | UserAccount ua1 = UserAccountHelpers.CreateUserWithInventory(scene, 0x1); | ||
168 | ScenePresence sp = SceneHelpers.AddScenePresence(scene, ua1); | ||
169 | |||
170 | string attName = "att"; | ||
171 | |||
172 | SceneObjectGroup so = SceneHelpers.AddSceneObject(scene, attName, sp.UUID); | ||
173 | |||
174 | UserAccount ua2 = UserAccountHelpers.CreateUserWithInventory(scene, 0x2); | ||
175 | ScenePresence sp2 = SceneHelpers.AddScenePresence(scene, ua2); | ||
176 | |||
177 | // Put avatar within 10m of the prim so that sit doesn't fail. | ||
178 | sp2.AbsolutePosition = new Vector3(0, 0, 0); | ||
179 | sp2.HandleAgentRequestSit(sp2.ControllingClient, sp2.UUID, so.UUID, Vector3.Zero); | ||
180 | |||
181 | scene.AttachmentsModule.AttachObject(sp, so, (uint)AttachmentPoint.Chest, false); | ||
182 | |||
183 | Assert.That(sp.HasAttachments(), Is.False); | ||
184 | Assert.That(scene.GetSceneObjectGroups().Count, Is.EqualTo(1)); | ||
185 | } | ||
186 | |||
157 | [Test] | 187 | [Test] |
158 | public void TestAddAttachmentFromInventory() | 188 | public void TestAddAttachmentFromInventory() |
159 | { | 189 | { |
diff --git a/OpenSim/Region/Framework/Scenes/SceneObjectGroup.cs b/OpenSim/Region/Framework/Scenes/SceneObjectGroup.cs index 4e0e183..fc04761 100644 --- a/OpenSim/Region/Framework/Scenes/SceneObjectGroup.cs +++ b/OpenSim/Region/Framework/Scenes/SceneObjectGroup.cs | |||
@@ -3401,6 +3401,20 @@ namespace OpenSim.Region.Framework.Scenes | |||
3401 | return count; | 3401 | return count; |
3402 | } | 3402 | } |
3403 | 3403 | ||
3404 | /// <summary> | ||
3405 | /// Gets the number of sitting avatars. | ||
3406 | /// </summary> | ||
3407 | /// <remarks>This applies to all sitting avatars whether there is a sit target set or not.</remarks> | ||
3408 | /// <returns></returns> | ||
3409 | public int GetSittingAvatarsCount() | ||
3410 | { | ||
3411 | int count = 0; | ||
3412 | |||
3413 | Array.ForEach<SceneObjectPart>(m_parts.GetArray(), p => count += p.GetSittingAvatarsCount()); | ||
3414 | |||
3415 | return count; | ||
3416 | } | ||
3417 | |||
3404 | public override string ToString() | 3418 | public override string ToString() |
3405 | { | 3419 | { |
3406 | return String.Format("{0} {1} ({2})", Name, UUID, AbsolutePosition); | 3420 | return String.Format("{0} {1} ({2})", Name, UUID, AbsolutePosition); |
diff --git a/OpenSim/Region/Framework/Scenes/SceneObjectPart.cs b/OpenSim/Region/Framework/Scenes/SceneObjectPart.cs index 3d81358..6518b84 100644 --- a/OpenSim/Region/Framework/Scenes/SceneObjectPart.cs +++ b/OpenSim/Region/Framework/Scenes/SceneObjectPart.cs | |||
@@ -374,7 +374,6 @@ namespace OpenSim.Region.Framework.Scenes | |||
374 | private uint _category; | 374 | private uint _category; |
375 | private Int32 _creationDate; | 375 | private Int32 _creationDate; |
376 | private uint _parentID = 0; | 376 | private uint _parentID = 0; |
377 | private UUID m_sitTargetAvatar = UUID.Zero; | ||
378 | private uint _baseMask = (uint)PermissionMask.All; | 377 | private uint _baseMask = (uint)PermissionMask.All; |
379 | private uint _ownerMask = (uint)PermissionMask.All; | 378 | private uint _ownerMask = (uint)PermissionMask.All; |
380 | private uint _groupMask = (uint)PermissionMask.None; | 379 | private uint _groupMask = (uint)PermissionMask.None; |
@@ -1233,13 +1232,20 @@ namespace OpenSim.Region.Framework.Scenes | |||
1233 | } | 1232 | } |
1234 | 1233 | ||
1235 | /// <summary> | 1234 | /// <summary> |
1236 | /// ID of the avatar that is sat on us. If there is no such avatar then is UUID.Zero | 1235 | /// ID of the avatar that is sat on us if we have a sit target. If there is no such avatar then is UUID.Zero |
1237 | /// </summary> | 1236 | /// </summary> |
1238 | public UUID SitTargetAvatar | 1237 | public UUID SitTargetAvatar { get; set; } |
1239 | { | 1238 | |
1240 | get { return m_sitTargetAvatar; } | 1239 | /// <summary> |
1241 | set { m_sitTargetAvatar = value; } | 1240 | /// IDs of all avatars start on this object part. |
1242 | } | 1241 | /// </summary> |
1242 | /// <remarks> | ||
1243 | /// We need to track this so that we can stop sat upon prims from being attached. | ||
1244 | /// </remarks> | ||
1245 | /// <value> | ||
1246 | /// null if there are no sitting avatars. This is to save us create a hashset for every prim in a scene. | ||
1247 | /// </value> | ||
1248 | private HashSet<UUID> m_sittingAvatars; | ||
1243 | 1249 | ||
1244 | public virtual UUID RegionID | 1250 | public virtual UUID RegionID |
1245 | { | 1251 | { |
@@ -4493,5 +4499,83 @@ namespace OpenSim.Region.Framework.Scenes | |||
4493 | Color color = Color; | 4499 | Color color = Color; |
4494 | return new Color4(color.R, color.G, color.B, (byte)(0xFF - color.A)); | 4500 | return new Color4(color.R, color.G, color.B, (byte)(0xFF - color.A)); |
4495 | } | 4501 | } |
4502 | |||
4503 | /// <summary> | ||
4504 | /// Record an avatar sitting on this part. | ||
4505 | /// </summary> | ||
4506 | /// <remarks>This is called for all the sitting avatars whether there is a sit target set or not.</remarks> | ||
4507 | /// <returns> | ||
4508 | /// true if the avatar was not already recorded, false otherwise. | ||
4509 | /// </returns> | ||
4510 | /// <param name='avatarId'></param> | ||
4511 | protected internal bool AddSittingAvatar(UUID avatarId) | ||
4512 | { | ||
4513 | HashSet<UUID> sittingAvatars = m_sittingAvatars; | ||
4514 | |||
4515 | if (sittingAvatars == null) | ||
4516 | sittingAvatars = new HashSet<UUID>(); | ||
4517 | |||
4518 | lock (sittingAvatars) | ||
4519 | { | ||
4520 | m_sittingAvatars = sittingAvatars; | ||
4521 | return m_sittingAvatars.Add(avatarId); | ||
4522 | } | ||
4523 | } | ||
4524 | |||
4525 | /// <summary> | ||
4526 | /// Remove an avatar recorded as sitting on this part. | ||
4527 | /// </summary> | ||
4528 | /// <remarks>This applies to all sitting avatars whether there is a sit target set or not.</remarks> | ||
4529 | /// <returns> | ||
4530 | /// true if the avatar was present and removed, false if it was not present. | ||
4531 | /// </returns> | ||
4532 | /// <param name='avatarId'></param> | ||
4533 | protected internal bool RemoveSittingAvatar(UUID avatarId) | ||
4534 | { | ||
4535 | HashSet<UUID> sittingAvatars = m_sittingAvatars; | ||
4536 | |||
4537 | // This can occur under a race condition where another thread | ||
4538 | if (sittingAvatars == null) | ||
4539 | return false; | ||
4540 | |||
4541 | lock (sittingAvatars) | ||
4542 | { | ||
4543 | if (sittingAvatars.Remove(avatarId)) | ||
4544 | { | ||
4545 | if (sittingAvatars.Count == 0) | ||
4546 | m_sittingAvatars = null; | ||
4547 | |||
4548 | return true; | ||
4549 | } | ||
4550 | } | ||
4551 | |||
4552 | return false; | ||
4553 | } | ||
4554 | |||
4555 | /// <summary> | ||
4556 | /// Get a copy of the list of sitting avatars. | ||
4557 | /// </summary> | ||
4558 | /// <remarks>This applies to all sitting avatars whether there is a sit target set or not.</remarks> | ||
4559 | /// <returns></returns> | ||
4560 | public HashSet<UUID> GetSittingAvatars() | ||
4561 | { | ||
4562 | return new HashSet<UUID>(m_sittingAvatars); | ||
4563 | } | ||
4564 | |||
4565 | /// <summary> | ||
4566 | /// Gets the number of sitting avatars. | ||
4567 | /// </summary> | ||
4568 | /// <remarks>This applies to all sitting avatars whether there is a sit target set or not.</remarks> | ||
4569 | /// <returns></returns> | ||
4570 | public int GetSittingAvatarsCount() | ||
4571 | { | ||
4572 | HashSet<UUID> sittingAvatars = m_sittingAvatars; | ||
4573 | |||
4574 | if (sittingAvatars == null) | ||
4575 | return 0; | ||
4576 | |||
4577 | lock (sittingAvatars) | ||
4578 | return sittingAvatars.Count; | ||
4579 | } | ||
4496 | } | 4580 | } |
4497 | } | 4581 | } |
diff --git a/OpenSim/Region/Framework/Scenes/ScenePresence.cs b/OpenSim/Region/Framework/Scenes/ScenePresence.cs index c7a670f..c6a2a03 100644 --- a/OpenSim/Region/Framework/Scenes/ScenePresence.cs +++ b/OpenSim/Region/Framework/Scenes/ScenePresence.cs | |||
@@ -578,6 +578,12 @@ namespace OpenSim.Region.Framework.Scenes | |||
578 | public uint ParentID { get; set; } | 578 | public uint ParentID { get; set; } |
579 | 579 | ||
580 | /// <summary> | 580 | /// <summary> |
581 | /// Are we sitting on an object? | ||
582 | /// </summary> | ||
583 | /// <remarks>A more readable way of testing presence sit status than ParentID == 0</remarks> | ||
584 | public bool IsSatOnObject { get { return ParentID != 0; } } | ||
585 | |||
586 | /// <summary> | ||
581 | /// If the avatar is sitting, the prim that it's sitting on. If not sitting then null. | 587 | /// If the avatar is sitting, the prim that it's sitting on. If not sitting then null. |
582 | /// </summary> | 588 | /// </summary> |
583 | /// <remarks> | 589 | /// <remarks> |
@@ -1808,6 +1814,8 @@ namespace OpenSim.Region.Framework.Scenes | |||
1808 | SendAvatarDataToAllAgents(); | 1814 | SendAvatarDataToAllAgents(); |
1809 | m_requestedSitTargetID = 0; | 1815 | m_requestedSitTargetID = 0; |
1810 | 1816 | ||
1817 | part.RemoveSittingAvatar(UUID); | ||
1818 | |||
1811 | if (part != null) | 1819 | if (part != null) |
1812 | part.ParentGroup.TriggerScriptChangedEvent(Changed.LINK); | 1820 | part.ParentGroup.TriggerScriptChangedEvent(Changed.LINK); |
1813 | } | 1821 | } |
@@ -1887,7 +1895,7 @@ namespace OpenSim.Region.Framework.Scenes | |||
1887 | ) | 1895 | ) |
1888 | )); | 1896 | )); |
1889 | 1897 | ||
1890 | // m_log.DebugFormat("[SCENE PRESENCE]: {0} {1}", SitTargetisSet, SitTargetUnOccupied); | 1898 | m_log.DebugFormat("[SCENE PRESENCE]: {0} {1}", SitTargetisSet, SitTargetUnOccupied); |
1891 | 1899 | ||
1892 | if (PhysicsActor != null) | 1900 | if (PhysicsActor != null) |
1893 | m_sitAvatarHeight = PhysicsActor.Size.Z; | 1901 | m_sitAvatarHeight = PhysicsActor.Size.Z; |
@@ -1920,6 +1928,12 @@ namespace OpenSim.Region.Framework.Scenes | |||
1920 | AbsolutePosition = pos + new Vector3(0.0f, 0.0f, m_sitAvatarHeight); | 1928 | AbsolutePosition = pos + new Vector3(0.0f, 0.0f, m_sitAvatarHeight); |
1921 | canSit = true; | 1929 | canSit = true; |
1922 | } | 1930 | } |
1931 | // else | ||
1932 | // { | ||
1933 | // m_log.DebugFormat( | ||
1934 | // "[SCENE PRESENCE]: Ignoring sit request of {0} on {1} {2} because sit target is unset and outside 10m", | ||
1935 | // Name, part.Name, part.LocalId); | ||
1936 | // } | ||
1923 | } | 1937 | } |
1924 | 1938 | ||
1925 | if (canSit) | 1939 | if (canSit) |
@@ -1930,6 +1944,8 @@ namespace OpenSim.Region.Framework.Scenes | |||
1930 | RemoveFromPhysicalScene(); | 1944 | RemoveFromPhysicalScene(); |
1931 | } | 1945 | } |
1932 | 1946 | ||
1947 | part.AddSittingAvatar(UUID); | ||
1948 | |||
1933 | cameraAtOffset = part.GetCameraAtOffset(); | 1949 | cameraAtOffset = part.GetCameraAtOffset(); |
1934 | cameraEyeOffset = part.GetCameraEyeOffset(); | 1950 | cameraEyeOffset = part.GetCameraEyeOffset(); |
1935 | forceMouselook = part.GetForceMouselook(); | 1951 | forceMouselook = part.GetForceMouselook(); |
diff --git a/OpenSim/Tests/Common/Helpers/SceneHelpers.cs b/OpenSim/Tests/Common/Helpers/SceneHelpers.cs index d5354cb..769de83 100644 --- a/OpenSim/Tests/Common/Helpers/SceneHelpers.cs +++ b/OpenSim/Tests/Common/Helpers/SceneHelpers.cs | |||
@@ -412,26 +412,49 @@ namespace OpenSim.Tests.Common | |||
412 | /// <returns></returns> | 412 | /// <returns></returns> |
413 | public static AgentCircuitData GenerateAgentData(UUID agentId) | 413 | public static AgentCircuitData GenerateAgentData(UUID agentId) |
414 | { | 414 | { |
415 | string firstName = "testfirstname"; | 415 | AgentCircuitData acd = GenerateCommonAgentData(); |
416 | 416 | ||
417 | AgentCircuitData agentData = new AgentCircuitData(); | 417 | acd.AgentID = agentId; |
418 | agentData.AgentID = agentId; | 418 | acd.firstname = "testfirstname"; |
419 | agentData.firstname = firstName; | 419 | acd.lastname = "testlastname"; |
420 | agentData.lastname = "testlastname"; | 420 | acd.ServiceURLs = new Dictionary<string, object>(); |
421 | |||
422 | return acd; | ||
423 | } | ||
424 | |||
425 | /// <summary> | ||
426 | /// Generate some standard agent connection data. | ||
427 | /// </summary> | ||
428 | /// <param name="agentId"></param> | ||
429 | /// <returns></returns> | ||
430 | public static AgentCircuitData GenerateAgentData(UserAccount ua) | ||
431 | { | ||
432 | AgentCircuitData acd = GenerateCommonAgentData(); | ||
433 | |||
434 | acd.AgentID = ua.PrincipalID; | ||
435 | acd.firstname = ua.FirstName; | ||
436 | acd.lastname = ua.LastName; | ||
437 | acd.ServiceURLs = ua.ServiceURLs; | ||
438 | |||
439 | return acd; | ||
440 | } | ||
441 | |||
442 | private static AgentCircuitData GenerateCommonAgentData() | ||
443 | { | ||
444 | AgentCircuitData acd = new AgentCircuitData(); | ||
421 | 445 | ||
422 | // XXX: Sessions must be unique, otherwise one presence can overwrite another in NullPresenceData. | 446 | // XXX: Sessions must be unique, otherwise one presence can overwrite another in NullPresenceData. |
423 | agentData.SessionID = UUID.Random(); | 447 | acd.SessionID = UUID.Random(); |
424 | agentData.SecureSessionID = UUID.Random(); | 448 | acd.SecureSessionID = UUID.Random(); |
425 | 449 | ||
426 | agentData.circuitcode = 123; | 450 | acd.circuitcode = 123; |
427 | agentData.BaseFolder = UUID.Zero; | 451 | acd.BaseFolder = UUID.Zero; |
428 | agentData.InventoryFolder = UUID.Zero; | 452 | acd.InventoryFolder = UUID.Zero; |
429 | agentData.startpos = Vector3.Zero; | 453 | acd.startpos = Vector3.Zero; |
430 | agentData.CapsPath = "http://wibble.com"; | 454 | acd.CapsPath = "http://wibble.com"; |
431 | agentData.ServiceURLs = new Dictionary<string, object>(); | 455 | acd.Appearance = new AvatarAppearance(); |
432 | agentData.Appearance = new AvatarAppearance(); | 456 | |
433 | 457 | return acd; | |
434 | return agentData; | ||
435 | } | 458 | } |
436 | 459 | ||
437 | /// <summary> | 460 | /// <summary> |
@@ -440,6 +463,9 @@ namespace OpenSim.Tests.Common | |||
440 | /// <remarks> | 463 | /// <remarks> |
441 | /// This can be used for tests where there is only one region or where there are multiple non-neighbour regions | 464 | /// This can be used for tests where there is only one region or where there are multiple non-neighbour regions |
442 | /// and teleport doesn't take place. | 465 | /// and teleport doesn't take place. |
466 | /// | ||
467 | /// XXX: Use the version of this method that takes the UserAccount structure wherever possible - this will | ||
468 | /// make the agent circuit data (e.g. first, lastname) consistent with the user account data. | ||
443 | /// </remarks> | 469 | /// </remarks> |
444 | /// <param name="scene"></param> | 470 | /// <param name="scene"></param> |
445 | /// <param name="agentId"></param> | 471 | /// <param name="agentId"></param> |
@@ -452,6 +478,10 @@ namespace OpenSim.Tests.Common | |||
452 | /// <summary> | 478 | /// <summary> |
453 | /// Add a root agent where the details of the agent connection (apart from the id) are unimportant for the test | 479 | /// Add a root agent where the details of the agent connection (apart from the id) are unimportant for the test |
454 | /// </summary> | 480 | /// </summary> |
481 | /// <remarks> | ||
482 | /// XXX: Use the version of this method that takes the UserAccount structure wherever possible - this will | ||
483 | /// make the agent circuit data (e.g. first, lastname) consistent with the user account data. | ||
484 | /// </remarks> | ||
455 | /// <param name="scene"></param> | 485 | /// <param name="scene"></param> |
456 | /// <param name="agentId"></param> | 486 | /// <param name="agentId"></param> |
457 | /// <param name="sceneManager"></param> | 487 | /// <param name="sceneManager"></param> |
@@ -464,6 +494,17 @@ namespace OpenSim.Tests.Common | |||
464 | /// <summary> | 494 | /// <summary> |
465 | /// Add a root agent. | 495 | /// Add a root agent. |
466 | /// </summary> | 496 | /// </summary> |
497 | /// <param name="scene"></param> | ||
498 | /// <param name="ua"></param> | ||
499 | /// <returns></returns> | ||
500 | public static ScenePresence AddScenePresence(Scene scene, UserAccount ua) | ||
501 | { | ||
502 | return AddScenePresence(scene, GenerateAgentData(ua)); | ||
503 | } | ||
504 | |||
505 | /// <summary> | ||
506 | /// Add a root agent. | ||
507 | /// </summary> | ||
467 | /// <remarks> | 508 | /// <remarks> |
468 | /// This function | 509 | /// This function |
469 | /// | 510 | /// |
diff --git a/OpenSim/Tests/Common/Helpers/UserAccountHelpers.cs b/OpenSim/Tests/Common/Helpers/UserAccountHelpers.cs index 3d3e65c..2fbebc4 100644 --- a/OpenSim/Tests/Common/Helpers/UserAccountHelpers.cs +++ b/OpenSim/Tests/Common/Helpers/UserAccountHelpers.cs | |||
@@ -138,6 +138,15 @@ namespace OpenSim.Tests.Common | |||
138 | CreateUserWithInventory(scene, ua, pw); | 138 | CreateUserWithInventory(scene, ua, pw); |
139 | return ua; | 139 | return ua; |
140 | } | 140 | } |
141 | |||
142 | public static UserAccount CreateUserWithInventory( | ||
143 | Scene scene, string firstName, string lastName, int userId, string pw) | ||
144 | { | ||
145 | UserAccount ua | ||
146 | = new UserAccount(TestHelpers.ParseTail(userId)) { FirstName = firstName, LastName = lastName }; | ||
147 | CreateUserWithInventory(scene, ua, pw); | ||
148 | return ua; | ||
149 | } | ||
141 | 150 | ||
142 | public static void CreateUserWithInventory(Scene scene, UserAccount ua, string pw) | 151 | public static void CreateUserWithInventory(Scene scene, UserAccount ua, string pw) |
143 | { | 152 | { |