diff --git a/src/GameLogic/Resets/ResetCharacterAction.cs b/src/GameLogic/Resets/ResetCharacterAction.cs index 5b18b06..5f14d4e 100644 --- a/src/GameLogic/Resets/ResetCharacterAction.cs +++ b/src/GameLogic/Resets/ResetCharacterAction.cs @@ -202,10 +202,12 @@ public class ResetCharacterAction if (homeMapDef is { } && await this._player.GameContext.GetMapAsync((ushort)homeMapDef.Number).ConfigureAwait(false) is { SafeZoneSpawnGate: { } spawnGate }) { - this._player.SelectedCharacter.PositionX = (byte)Rand.NextInt(spawnGate.X1, spawnGate.X2); - this._player.SelectedCharacter.PositionY = (byte)Rand.NextInt(spawnGate.Y1, spawnGate.Y2); - this._player.SelectedCharacter.CurrentMap = spawnGate.Map; - this._player.Rotation = spawnGate.Direction; + // ADAMU-CUSTOM: warp instead of writing the position fields directly. + // The direct writes moved the player without removing him from the map or sending a + // map change, so the client was never told and animated a walk to the new spot: the + // character visibly slid across the map. WarpToAsync does the same placement plus the + // map removal and the client notification. + await this._player.WarpToAsync(spawnGate).ConfigureAwait(false); } } diff --git a/tests/MUnique.OpenMU.Tests/ResetCharacterActionTest.cs b/tests/MUnique.OpenMU.Tests/ResetCharacterActionTest.cs index 6611605..401fd53 100644 --- a/tests/MUnique.OpenMU.Tests/ResetCharacterActionTest.cs +++ b/tests/MUnique.OpenMU.Tests/ResetCharacterActionTest.cs @@ -1,9 +1,10 @@ -// +// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.Tests; +using Moq; using MUnique.OpenMU.DataModel.Configuration; using MUnique.OpenMU.DataModel.Configuration.Items; using MUnique.OpenMU.DataModel.Entities; @@ -11,6 +12,8 @@ using MUnique.OpenMU.GameLogic; using MUnique.OpenMU.GameLogic.Attributes; using MUnique.OpenMU.GameLogic.NPC; using MUnique.OpenMU.GameLogic.Resets; +using MUnique.OpenMU.GameLogic.Views.World; +using MUnique.OpenMU.Pathfinding; /// /// Tests for . @@ -131,6 +134,60 @@ public class ResetCharacterActionTest Assert.That(player.SelectedCharacter!.LevelUpPoints, Is.EqualTo(1_800)); } + /// + /// Verifies that a reset with moves the player through + /// the regular warp path, so the client is told about the new position. + /// Writing the character's position fields directly moves the player without notifying anyone, + /// which makes the character slide across the map on the client instead of respawning. + /// + [Test] + public async Task MoveHomeWarpsPlayerSoTheClientIsNotifiedAsync() + { + var player = await PlayerTestHelper.CreatePlayerAsync().ConfigureAwait(false); + player.Attributes![Stats.Level] = 400; + player.Money = 1_000; + + // The home map has to be one whose GameMap instance does not exist yet: the spawn gate is + // resolved once, in the GameMap constructor, so a map created earlier would ignore it. + var homeMap = CreateMapDefinitionWithSpawnGate(1, 125); + player.GameContext.Configuration.Maps.Add(homeMap); + Mock.Get(player.SelectedCharacter!.CharacterClass!).Setup(c => c.HomeMap).Returns(homeMap); + + var configuration = this.CreateConfiguration(null); + configuration.MoveHome = true; + player.GameContext.FeaturePlugIns.AddPlugIn(new ResetFeaturePlugIn { Configuration = configuration }, true); + + var mapChangeView = Mock.Get(player.ViewPlugIns.GetPlugIn()!); + var action = new ResetCharacterAction(player); + + await action.ResetCharacterAsync().ConfigureAwait(false); + + Assert.That((int)player.Attributes[Stats.Resets], Is.EqualTo(1)); + Assert.That(player.Position, Is.EqualTo(new Point(125, 125))); + mapChangeView.Verify(v => v.MapChangeAsync(), Times.Once); + } + + private static GameMapDefinition CreateMapDefinitionWithSpawnGate(short number, byte spawnCoordinate) + { + var mapMock = new Mock(); + mapMock.SetupAllProperties(); + mapMock.Setup(m => m.DropItemGroups).Returns(new List()); + mapMock.Setup(m => m.MonsterSpawns).Returns(new List()); + mapMock.Object.TerrainData = new byte[ushort.MaxValue + 3]; + mapMock.Object.Number = number; + var spawnGate = new ExitGate + { + IsSpawnGate = true, + X1 = spawnCoordinate, + X2 = spawnCoordinate, + Y1 = spawnCoordinate, + Y2 = spawnCoordinate, + Map = mapMock.Object, + }; + mapMock.Setup(m => m.ExitGates).Returns(new List { spawnGate }); + return mapMock.Object; + } + private static async ValueTask AddRequiredItemsAsync(Player player, ItemDefinition requiredItem, int count) { for (byte i = 0; i < count; i++) @@ -165,7 +222,7 @@ public class ResetCharacterActionTest return new NonPlayerCharacter(spawnArea, definition, map); } - private ResetConfiguration CreateConfiguration(ItemDefinition requiredItem) + private ResetConfiguration CreateConfiguration(ItemDefinition? requiredItem) { return new ResetConfiguration {