baseline: OpenMU upstream b5a0961 (fresh source)

This commit is contained in:
Acentech Dev
2026-07-14 19:00:35 +03:00
parent 450402e47d
commit 36fc125d5c
11968 changed files with 748705 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
// <copyright file="AppearanceChangedExtendedPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The extended implementation of the <see cref="IAppearanceChangedPlugIn"/> which is forwarding appearance changes of other players to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.AppearanceChangedExtendedPlugIn_Name), Description = nameof(PlugInResources.AppearanceChangedExtendedPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("A2F298E4-9F48-402A-B30D-9BC2BA8DEB2E")]
[MinimumClient(106, 3, ClientLanguage.Invariant)]
public class AppearanceChangedExtendedPlugIn : IAppearanceChangedPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="AppearanceChangedExtendedPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public AppearanceChangedExtendedPlugIn(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public async ValueTask AppearanceChangedAsync(Player changedPlayer, Item item, bool isEquipped)
{
var connection = this._player.Connection;
if (connection is null || changedPlayer.Inventory is null)
{
return;
}
await connection.SendAppearanceChangedExtendedAsync(
changedPlayer.GetId(this._player),
item.ItemSlot,
(byte)((isEquipped ? item.Definition?.Group : 0xFF) ?? 0xFF),
(ushort)(item.Definition?.Number ?? 0xFFFF),
item.Level,
(byte)(ItemSerializerHelper.GetExcellentByte(item) | ItemSerializerHelper.GetFenrirByte(item)),
(byte)(item.ItemSetGroups.FirstOrDefault(set => set.AncientSetDiscriminator != 0)?.AncientSetDiscriminator ?? 0),
changedPlayer.SelectedCharacter?.HasFullAncientSetEquipped() is true)
.ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,79 @@
// <copyright file="AppearanceChangedPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IAppearanceChangedPlugIn"/> which is forwarding appearance changes of other players to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.AppearanceChangedPlugIn_Name), Description = nameof(PlugInResources.AppearanceChangedPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("1d097399-d5af-40de-a97d-a812f13c2f20")]
public class AppearanceChangedPlugIn : IAppearanceChangedPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="AppearanceChangedPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public AppearanceChangedPlugIn(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public async ValueTask AppearanceChangedAsync(Player changedPlayer, Item item, bool isEquipped)
{
var connection = this._player.Connection;
if (connection is null || changedPlayer.Inventory is not { } inventory)
{
return;
}
int Write()
{
var itemSerializer = this._player.ItemSerializer;
var size = AppearanceChanged.GetRequiredSize(itemSerializer.NeededSpace);
var span = connection!.Output.GetSpan(size)[..size];
var packet = new AppearanceChangedRef(span)
{
ChangedPlayerId = changedPlayer.GetId(this._player),
};
if (inventory!.EquippedItems.Contains(item))
{
itemSerializer.SerializeItem(packet.ItemData, item);
}
else
{
packet.ItemData.Fill(0xFF);
}
// The byte with index 1 usually now holds the item level and one part of the item option level.
// This full information is irrelevant. For this message, we just need the "glow" level, which means the one of the appearance serializer.
// In the available space, the item position is serialized.
// To summarize: The 4 higher bits hold the item position, the 4 lower bits hold the "glow" level
packet.ItemData[1] = (byte)(item.ItemSlot << 4);
packet.ItemData[1] |= item.GetGlowLevel();
// We could also continue to dumb down information here as this packet reveals all of the options of an item to
// other players - something that is probably not in the interest of the players.
// However, for now we keep this logic close to the original server, which doesn't do a thing about it.
// Additionally, we could think of ignoring changes of rings and pendants, as they are usually not visible in the game client, except
// maybe transformation rings. So we'll leave it as it is, too.
return size;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,87 @@
// <copyright file="DeActivateMagicEffectPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Collections.ObjectModel;
using System.Runtime.InteropServices;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IActivateMagicEffectPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.DeActivateMagicEffectPlugIn_Name), Description = nameof(PlugInResources.DeActivateMagicEffectPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("67642604-8abb-44b9-a668-989cb3b28e89")]
[MinimumClient(0, 90, ClientLanguage.Invariant)]
public class DeActivateMagicEffectPlugIn : IActivateMagicEffectPlugIn, IDeactivateMagicEffectPlugIn
{
private static readonly ReadOnlyDictionary<AttributeDefinition, EffectItemConsumption.EffectType> EffectTypeMapping = new(
new Dictionary<AttributeDefinition, EffectItemConsumption.EffectType>
{
{ Stats.AttackSpeedAny, EffectItemConsumption.EffectType.AttackSpeed },
{ Stats.BaseDamageBonus, EffectItemConsumption.EffectType.Damage },
{ Stats.DefenseBase, EffectItemConsumption.EffectType.Defense },
{ Stats.MaximumHealth, EffectItemConsumption.EffectType.MaximumHealth },
{ Stats.MaximumMana, EffectItemConsumption.EffectType.MaximumMana },
});
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="DeActivateMagicEffectPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public DeActivateMagicEffectPlugIn(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public ValueTask ActivateMagicEffectAsync(MagicEffect effect, IAttackable affectedObject)
{
return this.SendMagicEffectStatusAsync(effect, affectedObject, true, effect.Definition.SendDuration ? effect.Duration : TimeSpan.Zero);
}
/// <inheritdoc/>
public ValueTask DeactivateMagicEffectAsync(MagicEffect effect, IAttackable affectedObject)
{
return this.SendMagicEffectStatusAsync(effect, affectedObject, false, TimeSpan.Zero);
}
private async ValueTask SendMagicEffectStatusAsync(MagicEffect effect, IAttackable affectedObject, bool isActive, TimeSpan duration)
{
if (!(this._player.Connection?.Connected ?? false)
|| effect.Definition.Number <= 0)
{
return;
}
bool effectWasSent = false;
var objectId = affectedObject.GetId(this._player);
if (isActive && affectedObject == this._player)
{
foreach (var powerUpDefinition in effect.Definition.PowerUpDefinitions)
{
if (powerUpDefinition.TargetAttribute is { } targetAttribute
&& EffectTypeMapping.TryGetValue(targetAttribute, out var effectType))
{
var origin = EffectItemConsumption.EffectOrigin.HalloweenAndCherryBlossomEvent; // Basically, all normal consumable items which add effects
var action = EffectItemConsumption.EffectAction.Add;
await this._player.Connection.SendEffectItemConsumptionAsync(origin, effectType, action, (uint)duration.TotalSeconds, (byte)effect.Definition.Number).ConfigureAwait(false);
effectWasSent = true;
}
}
}
if (!effectWasSent)
{
await this._player.Connection.SendMagicEffectStatusAsync(isActive, objectId, (byte)effect.Id).ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,44 @@
// <copyright file="DeActivateMagicEffectPlugIn075.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IActivateMagicEffectPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.DeActivateMagicEffectPlugIn075_Name), Description = nameof(PlugInResources.DeActivateMagicEffectPlugIn075_Description), ResourceType = typeof(PlugInResources))]
[Guid("3CF8BFBA-FBDA-431D-9806-86B3DEC3AD54")]
[MaximumClient(0, 89, ClientLanguage.Invariant)]
public class DeActivateMagicEffectPlugIn075 : IDeactivateMagicEffectPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="DeActivateMagicEffectPlugIn075"/> class.
/// </summary>
/// <param name="player">The player.</param>
public DeActivateMagicEffectPlugIn075(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public async ValueTask DeactivateMagicEffectAsync(MagicEffect effect, IAttackable affectedObject)
{
if (effect.Definition.Number <= 0)
{
return;
}
// We assume, that the magic effect number is equal to the skill number.
// In early versions, the magic effect number is not used elsewhere.
await this._player.Connection.SendMagicEffectCancelled075Async((byte)effect.Definition.Number, affectedObject.GetId(this._player)).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,63 @@
// <copyright file="DroppedItemsDisappearedPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IDroppedItemsDisappearedPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.DroppedItemsDisappearedPlugIn_Name), Description = nameof(PlugInResources.DroppedItemsDisappearedPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("ecd14e95-33be-44f7-bb9b-1429a57a7a94")]
public class DroppedItemsDisappearedPlugIn : IDroppedItemsDisappearedPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="DroppedItemsDisappearedPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public DroppedItemsDisappearedPlugIn(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public async ValueTask DroppedItemsDisappearedAsync(IEnumerable<ushort> disappearedItemIds)
{
var connection = this._player.Connection;
if (connection is null)
{
return;
}
////C2 00 07 21 01 00 0C
int count = disappearedItemIds.Count();
int Write()
{
var size = ItemDropRemovedRef.GetRequiredSize(count);
var span = connection.Output.GetSpan(size)[..size];
var message = new ItemDropRemovedRef(span)
{
ItemCount = (byte)count,
};
int i = 0;
foreach (var dropId in disappearedItemIds)
{
var drop = message[i];
drop.Id = dropId;
i++;
}
return size;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,41 @@
// <copyright file="EffectNumbers.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
/// <summary>
/// Skill effect flags which were used in earlier versions, like 0.75.
/// </summary>
internal static class EffectNumbers
{
/// <summary>
/// The undefined effect. No effect.
/// </summary>
public const int Undefined = 0;
/// <summary>
/// The object has a damage buff.
/// </summary>
public const int DamageBuff = 0x01;
/// <summary>
/// The object has a defense buff.
/// </summary>
public const int DefenseBuff = 0x02;
/// <summary>
/// The object is poisoned.
/// </summary>
public const int Poisoned = 0x37;
/// <summary>
/// The object is iced.
/// </summary>
public const int Iced = 0x38;
/// <summary>
/// Shows the health bar for duel spectators.
/// </summary>
public const int DuelSpectatorHealthBar = 0x62;
}

View File

@@ -0,0 +1,55 @@
// <copyright file="MapChangePlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IMapChangePlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.MapChangePlugIn_Name), Description = nameof(PlugInResources.MapChangePlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("234b477d-6fe9-4caa-a03f-78cb25518b39")]
[MinimumClient(1, 0, ClientLanguage.Invariant)]
public class MapChangePlugIn : IMapChangePlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="MapChangePlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public MapChangePlugIn(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public ValueTask MapChangeAsync()
{
return this.SendMessageAsync(true);
}
/// <inheritdoc/>
public ValueTask MapChangeFailedAsync()
{
return this.SendMessageAsync(false);
}
private async ValueTask SendMessageAsync(bool success)
{
if (this._player.SelectedCharacter?.CurrentMap is null)
{
return;
}
var mapNumber = this._player.SelectedCharacter.CurrentMap.Number.ToUnsigned();
var position = this._player.IsWalking ? this._player.WalkTarget : this._player.Position;
await this._player.Connection.SendMapChangedAsync(mapNumber, position.X, position.Y, this._player.Rotation.ToPacketByte(), success).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,48 @@
// <copyright file="MapChangePlugIn075.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IMapChangePlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.MapChangePlugIn075_Name), Description = nameof(PlugInResources.MapChangePlugIn075_Description), ResourceType = typeof(PlugInResources))]
[Guid("88195844-06C7-4EDA-8501-8B75A8B4B3F4")]
public class MapChangePlugIn075 : IMapChangePlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="MapChangePlugIn075"/> class.
/// </summary>
/// <param name="player">The player.</param>
public MapChangePlugIn075(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public async ValueTask MapChangeAsync()
{
if (this._player.SelectedCharacter?.CurrentMap is null)
{
return;
}
var mapNumber = (byte)this._player.SelectedCharacter.CurrentMap.Number;
var position = this._player.IsWalking ? this._player.WalkTarget : this._player.Position;
await this._player.Connection.SendMapChanged075Async(mapNumber, position.X, position.Y, this._player.Rotation.ToPacketByte()).ConfigureAwait(false);
}
/// <inheritdoc/>
public ValueTask MapChangeFailedAsync()
{
// not implemented?
return ValueTask.CompletedTask;
}
}

View File

@@ -0,0 +1,43 @@
// <copyright file="MapEventStateUpdatePlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.PlugIns;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IMapEventStateUpdatePlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.MapEventStateUpdatePlugIn_Name), Description = nameof(PlugInResources.MapEventStateUpdatePlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("8A34C69D-59CC-4251-9E8E-D80154A7AC8C")]
public class MapEventStateUpdatePlugIn : IMapEventStateUpdatePlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="MapEventStateUpdatePlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public MapEventStateUpdatePlugIn(RemotePlayer player) => this._player = player;
/// <inheritdoc />
public async ValueTask UpdateStateAsync(bool enabled, MapEventType mapEventType)
{
await this._player.Connection.SendMapEventStateAsync(enabled, Convert(mapEventType)).ConfigureAwait(false);
}
private static MapEventState.Events Convert(MapEventType eventType)
{
return eventType switch
{
MapEventType.RedDragonInvasion => MapEventState.Events.RedDragon,
MapEventType.GoldenDragonInvasion => MapEventState.Events.GoldenDragon,
_ => throw new ArgumentException($"Unknown map's event type {eventType}"),
};
}
}

View File

@@ -0,0 +1,168 @@
// <copyright file="NewNpcsInScopePlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="INewNpcsInScopePlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.NewNpcsInScopePlugIn_Name), Description = nameof(PlugInResources.NewNpcsInScopePlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("35449477-0fba-48cb-9371-f337433b0f9d")]
[MinimumClient(5, 0, ClientLanguage.Invariant)]
public class NewNpcsInScopePlugIn : INewNpcsInScopePlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="NewNpcsInScopePlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public NewNpcsInScopePlugIn(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public async ValueTask NewNpcsInScopeAsync(IEnumerable<NonPlayerCharacter> newObjects, bool isSpawned = true)
{
var connection = this._player.Connection;
if (connection is null || newObjects is null || !newObjects.Any())
{
return;
}
var summons = newObjects.OfType<ISummonable>().Where(m => m.SummonedBy is { }).ToList();
var npcs = newObjects.Except(summons.OfType<NonPlayerCharacter>()).ToList();
if (npcs.Any())
{
await NpcsInScopeAsync(isSpawned, connection, npcs).ConfigureAwait(false);
}
if (summons.Any())
{
await SummonedMonstersInScopeAsync(isSpawned, connection, summons).ConfigureAwait(false);
}
}
private static async ValueTask NpcsInScopeAsync(bool isSpawned, IConnection connection, ICollection<NonPlayerCharacter> npcs)
{
int Write()
{
var size = AddNpcsToScopeRef.GetRequiredSize(npcs.Count);
var span = connection.Output.GetSpan(size)[..size];
var packet = new AddNpcsToScopeRef(span)
{
NpcCount = (byte)npcs.Count,
};
int i = 0;
foreach (var npc in npcs)
{
var npcBlock = packet[i];
npcBlock.Id = npc.Id;
if (isSpawned)
{
npcBlock.Id |= 0x8000;
}
npcBlock.TypeNumber = (ushort)(npc.Definition?.Number ?? 0);
npcBlock.CurrentPositionX = npc.Position.X;
npcBlock.CurrentPositionY = npc.Position.Y;
var supportWalk = npc as ISupportWalk;
if (supportWalk?.IsWalking ?? false)
{
npcBlock.TargetPositionX = supportWalk.WalkTarget.X;
npcBlock.TargetPositionY = supportWalk.WalkTarget.Y;
}
else
{
npcBlock.TargetPositionX = npc.Position.X;
npcBlock.TargetPositionY = npc.Position.Y;
}
npcBlock.Rotation = npc.Rotation.ToPacketByte();
i++;
}
return size;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
private static async ValueTask SummonedMonstersInScopeAsync(bool isSpawned, IConnection connection, ICollection<ISummonable> summons)
{
int Write()
{
const int estimatedEffectsPerPlayer = 5;
var estimatedSizePerCharacter = AddSummonedMonstersToScopeRef.SummonedMonsterDataRef.GetRequiredSize(estimatedEffectsPerPlayer);
var estimatedSize = AddSummonedMonstersToScopeRef.GetRequiredSize(summons.Count, estimatedSizePerCharacter);
var span = connection.Output.GetSpan(estimatedSize)[..estimatedSize];
var packet = new AddSummonedMonstersToScopeRef(span)
{
MonsterCount = (byte)summons.Count,
};
int i = 0;
foreach (var summon in summons)
{
var block = packet[i];
block.Id = summon.Id;
if (isSpawned)
{
block.Id |= 0x8000;
}
block.TypeNumber = (ushort)(summon.Definition?.Number ?? 0);
block.CurrentPositionX = summon.Position.X;
block.CurrentPositionY = summon.Position.Y;
if (summon is ISupportWalk walker && walker.IsWalking)
{
block.TargetPositionX = walker.WalkTarget.X;
block.TargetPositionY = walker.WalkTarget.Y;
}
else
{
block.TargetPositionX = summon.Position.X;
block.TargetPositionY = summon.Position.Y;
}
block.Rotation = summon.Rotation.ToPacketByte();
block.OwnerCharacterName = summon.SummonedBy?.Name ?? string.Empty;
if (summon is IAttackable attackable)
{
var activeEffects = attackable.MagicEffectList.VisibleEffects;
block.EffectCount = (byte)activeEffects.Count;
for (int e = block.EffectCount - 1; e >= 0; e--)
{
var effectBlock = block[e];
effectBlock.Id = (byte)activeEffects[e].Id;
}
}
else
{
block.EffectCount = 0;
}
i++;
}
return estimatedSize;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,160 @@
// <copyright file="NewNpcsInScopePlugIn075.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="T:MUnique.OpenMU.GameLogic.Views.World.INewNpcsInScopePlugIn" /> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.NewNpcsInScopePlugIn075_Name), Description = nameof(PlugInResources.NewNpcsInScopePlugIn075_Description), ResourceType = typeof(PlugInResources))]
[Guid("7E9CE800-E59F-4E90-A6F1-28214483213C")]
[MinimumClient(0, 75, ClientLanguage.Invariant)]
public class NewNpcsInScopePlugIn075 : INewNpcsInScopePlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="NewNpcsInScopePlugIn075"/> class.
/// </summary>
/// <param name="player">The player.</param>
public NewNpcsInScopePlugIn075(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public async ValueTask NewNpcsInScopeAsync(IEnumerable<NonPlayerCharacter> newObjects, bool isSpawned = true)
{
var connection = this._player.Connection;
if (connection is null || newObjects is null || !newObjects.Any())
{
return;
}
var summons = newObjects.OfType<Monster>().Where(m => m.SummonedBy is { }).ToList();
var npcs = newObjects.Except(summons).ToList();
if (npcs.Any())
{
await NpcsInScopeAsync(isSpawned, connection, npcs).ConfigureAwait(false);
}
if (summons.Any())
{
await SummonedMonstersInScopeAsync(isSpawned, connection, summons).ConfigureAwait(false);
}
}
private static async ValueTask NpcsInScopeAsync(bool isSpawned, IConnection connection, ICollection<NonPlayerCharacter> npcs)
{
int Write()
{
var size = AddNpcsToScope075Ref.GetRequiredSize(npcs.Count);
var span = connection.Output.GetSpan(size)[..size];
var packet = new AddNpcsToScope075Ref(span)
{
NpcCount = (byte)npcs.Count,
};
int i = 0;
foreach (var npc in npcs)
{
var npcBlock = packet[i];
npcBlock.Id = npc.Id;
if (isSpawned)
{
npcBlock.Id |= 0x8000;
}
npcBlock.TypeNumber = (byte)npc.Definition.Number;
npcBlock.CurrentPositionX = npc.Position.X;
npcBlock.CurrentPositionY = npc.Position.Y;
if (npc is Monster monster)
{
npcBlock.IsPoisoned = monster.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.Poisoned);
npcBlock.IsIced = monster.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.Iced);
npcBlock.IsDamageBuffed = monster.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.DamageBuff);
npcBlock.IsDefenseBuffed = monster.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.DefenseBuff);
}
var supportWalk = npc as ISupportWalk;
if (supportWalk?.IsWalking ?? false)
{
npcBlock.TargetPositionX = supportWalk.WalkTarget.X;
npcBlock.TargetPositionY = supportWalk.WalkTarget.Y;
}
else
{
npcBlock.TargetPositionX = npc.Position.X;
npcBlock.TargetPositionY = npc.Position.Y;
}
npcBlock.Rotation = npc.Rotation.ToPacketByte();
i++;
}
return size;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
private static async ValueTask SummonedMonstersInScopeAsync(bool isSpawned, IConnection connection, ICollection<Monster> summons)
{
int Write()
{
var size = AddSummonedMonstersToScope075Ref.GetRequiredSize(summons.Count);
var span = connection.Output.GetSpan(size)[..size];
var packet = new AddSummonedMonstersToScope075Ref(span)
{
MonsterCount = (byte)summons.Count,
};
int i = 0;
foreach (var summon in summons)
{
var block = packet[i];
block.Id = summon.Id;
if (isSpawned)
{
block.Id |= 0x8000;
}
block.TypeNumber = (byte)summon.Definition.Number;
block.CurrentPositionX = summon.Position.X;
block.CurrentPositionY = summon.Position.Y;
block.IsPoisoned = summon.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.Poisoned);
block.IsIced = summon.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.Iced);
block.IsDamageBuffed = summon.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.DamageBuff);
block.IsDefenseBuffed = summon.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.DefenseBuff);
var supportWalk = summon as ISupportWalk;
if (supportWalk?.IsWalking ?? false)
{
block.TargetPositionX = supportWalk.WalkTarget.X;
block.TargetPositionY = supportWalk.WalkTarget.Y;
}
else
{
block.TargetPositionX = summon.Position.X;
block.TargetPositionY = summon.Position.Y;
}
block.Rotation = summon.Rotation.ToPacketByte();
block.OwnerCharacterName = summon.SummonedBy?.Name ?? string.Empty;
i++;
}
return size;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,160 @@
// <copyright file="NewNpcsInScopePlugIn095.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="T:MUnique.OpenMU.GameLogic.Views.World.INewNpcsInScopePlugIn" /> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.NewNpcsInScopePlugIn095_Name), Description = nameof(PlugInResources.NewNpcsInScopePlugIn095_Description), ResourceType = typeof(PlugInResources))]
[Guid("ECCD99EB-425D-4C9B-8F04-2711BA7A4C1E")]
[MinimumClient(0, 95, ClientLanguage.Invariant)]
public class NewNpcsInScopePlugIn095 : INewNpcsInScopePlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="NewNpcsInScopePlugIn095"/> class.
/// </summary>
/// <param name="player">The player.</param>
public NewNpcsInScopePlugIn095(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public async ValueTask NewNpcsInScopeAsync(IEnumerable<NonPlayerCharacter> newObjects, bool isSpawned = true)
{
var connection = this._player.Connection;
if (connection is null || newObjects is null || !newObjects.Any())
{
return;
}
var summons = newObjects.OfType<Monster>().Where(m => m.SummonedBy is { }).ToList();
var npcs = newObjects.Except(summons).ToList();
if (npcs.Any())
{
await NpcsInScopeAsync(isSpawned, connection, npcs).ConfigureAwait(false);
}
if (summons.Any())
{
await SummonedMonstersInScopeAsync(isSpawned, connection, summons).ConfigureAwait(false);
}
}
private static async ValueTask NpcsInScopeAsync(bool isSpawned, IConnection connection, ICollection<NonPlayerCharacter> npcs)
{
int Write()
{
var size = AddNpcsToScope095Ref.GetRequiredSize(npcs.Count);
var span = connection.Output.GetSpan(size)[..size];
var packet = new AddNpcsToScope095Ref(span)
{
NpcCount = (byte)npcs.Count,
};
int i = 0;
foreach (var npc in npcs)
{
var npcBlock = packet[i];
npcBlock.Id = npc.Id;
if (isSpawned)
{
npcBlock.Id |= 0x8000;
}
npcBlock.TypeNumber = (byte)npc.Definition.Number;
npcBlock.CurrentPositionX = npc.Position.X;
npcBlock.CurrentPositionY = npc.Position.Y;
if (npc is Monster monster)
{
npcBlock.IsPoisoned = monster.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.Poisoned);
npcBlock.IsIced = monster.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.Iced);
npcBlock.IsDamageBuffed = monster.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.DamageBuff);
npcBlock.IsDefenseBuffed = monster.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.DefenseBuff);
}
var supportWalk = npc as ISupportWalk;
if (supportWalk?.IsWalking ?? false)
{
npcBlock.TargetPositionX = supportWalk.WalkTarget.X;
npcBlock.TargetPositionY = supportWalk.WalkTarget.Y;
}
else
{
npcBlock.TargetPositionX = npc.Position.X;
npcBlock.TargetPositionY = npc.Position.Y;
}
npcBlock.Rotation = npc.Rotation.ToPacketByte();
i++;
}
return size;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
private static async ValueTask SummonedMonstersInScopeAsync(bool isSpawned, IConnection connection, ICollection<Monster> summons)
{
int Write()
{
var size = AddSummonedMonstersToScope095Ref.GetRequiredSize(summons.Count);
var span = connection.Output.GetSpan(size)[..size];
var packet = new AddSummonedMonstersToScope095Ref(span)
{
MonsterCount = (byte)summons.Count,
};
int i = 0;
foreach (var summon in summons)
{
var block = packet[i];
block.Id = summon.Id;
if (isSpawned)
{
block.Id |= 0x8000;
}
block.TypeNumber = (byte)summon.Definition.Number;
block.CurrentPositionX = summon.Position.X;
block.CurrentPositionY = summon.Position.Y;
block.IsPoisoned = summon.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.Poisoned);
block.IsIced = summon.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.Iced);
block.IsDamageBuffed = summon.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.DamageBuff);
block.IsDefenseBuffed = summon.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.DefenseBuff);
var supportWalk = summon as ISupportWalk;
if (supportWalk?.IsWalking ?? false)
{
block.TargetPositionX = supportWalk.WalkTarget.X;
block.TargetPositionY = supportWalk.WalkTarget.Y;
}
else
{
block.TargetPositionX = summon.Position.X;
block.TargetPositionY = summon.Position.Y;
}
block.Rotation = summon.Rotation.ToPacketByte();
block.OwnerCharacterName = summon.SummonedBy?.Name ?? string.Empty;
i++;
}
return size;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,105 @@
// <copyright file="NewPlayersInScopeExtendedPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.GameServer.RemoteView.Character;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The extended implementation of the <see cref="INewPlayersInScopePlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.NewPlayersInScopeExtendedPlugIn_Name), Description = nameof(PlugInResources.NewPlayersInScopeExtendedPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("83E30752-501E-4A40-9698-9A5097825C30")]
[MinimumClient(106, 3, ClientLanguage.Invariant)]
public class NewPlayersInScopeExtendedPlugIn : NewPlayersInScopePlugIn, INewPlayersInScopePlugIn
{
/// <summary>
/// Initializes a new instance of the <see cref="NewPlayersInScopeExtendedPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public NewPlayersInScopeExtendedPlugIn(RemotePlayer player)
: base(player)
{
}
/// <inheritdoc />
protected override async ValueTask SendCharacterAsync(Player newPlayer, bool isSpawned)
{
var connection = this.Player.Connection;
if (connection is null)
{
return;
}
var selectedCharacter = newPlayer.SelectedCharacter;
if (selectedCharacter is null)
{
return;
}
int Write()
{
var appearanceSerializer = this.Player.AppearanceSerializer;
Span<byte> activeEffects = stackalloc byte[newPlayer.MagicEffectList.VisibleEffects.Count];
for (int i = 0; i < activeEffects.Length && i < newPlayer.MagicEffectList.VisibleEffects.Count; i++)
{
activeEffects[i] = (byte)newPlayer.MagicEffectList.VisibleEffects[i].Id;
}
var requiredSize = AddCharacterToScopeExtendedRef.GetRequiredSize(appearanceSerializer.NeededSpace + activeEffects.Length + 1);
var span = connection.Output.GetSpan(requiredSize)[..requiredSize];
var packet = new AddCharacterToScopeExtendedRef(span);
packet.Id = newPlayer.GetId(this.Player);
if (isSpawned)
{
packet.Id |= 0x8000;
}
packet.CurrentPositionX = newPlayer.Position.X;
packet.CurrentPositionY = newPlayer.Position.Y;
packet.Name = selectedCharacter.Name;
if (newPlayer.IsWalking)
{
packet.TargetPositionX = newPlayer.WalkTarget.X;
packet.TargetPositionY = newPlayer.WalkTarget.Y;
}
else
{
packet.TargetPositionX = newPlayer.Position.X;
packet.TargetPositionY = newPlayer.Position.Y;
}
packet.Rotation = newPlayer.Rotation.ToPacketByte();
packet.HeroState = selectedCharacter.State.Convert();
packet.AttackSpeed = (ushort)(newPlayer.Attributes?[Stats.AttackSpeed] ?? 0);
packet.MagicSpeed = (ushort)(newPlayer.Attributes?[Stats.MagicSpeed] ?? 0);
appearanceSerializer.WriteAppearanceData(packet.AppearanceAndEffects, newPlayer.AppearanceData, true);
var effectsStartIndex = appearanceSerializer.NeededSpace;
packet.AppearanceAndEffects[effectsStartIndex] = (byte)activeEffects.Length;
for (int e = 0; e < activeEffects.Length; ++e)
{
packet.AppearanceAndEffects[effectsStartIndex + 1 + e] = activeEffects[e];
}
return span.Length;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,245 @@
// <copyright file="NewPlayersInScopePlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.Guild;
using MUnique.OpenMU.GameLogic.Views.PlayerShop;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.GameServer.RemoteView.Character;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="INewPlayersInScopePlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.NewPlayersInScopePlugIn_Name), Description = nameof(PlugInResources.NewPlayersInScopePlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("4cd64537-ae5f-4030-bca1-7fa30ebff6c6")]
[MinimumClient(5, 0, ClientLanguage.Invariant)]
public class NewPlayersInScopePlugIn : INewPlayersInScopePlugIn
{
/// <summary>
/// Initializes a new instance of the <see cref="NewPlayersInScopePlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public NewPlayersInScopePlugIn(RemotePlayer player) => this.Player = player;
/// <summary>
/// Gets the player of this view.
/// </summary>
protected RemotePlayer Player { get; }
/// <inheritdoc/>
public async ValueTask NewPlayersInScopeAsync(IEnumerable<Player> newPlayers, bool isSpawned = true)
{
if (newPlayers is null || !newPlayers.Any())
{
return;
}
var (shopPlayers, guildPlayers) = await this.SendCharactersAsync(newPlayers, isSpawned).ConfigureAwait(false);
if (shopPlayers != null)
{
await this.Player.InvokeViewPlugInAsync<IShowShopsOfPlayersPlugIn>(p => p.ShowShopsOfPlayersAsync(shopPlayers)).ConfigureAwait(false);
}
if (guildPlayers != null)
{
await this.Player.InvokeViewPlugInAsync<IAssignPlayersToGuildPlugIn>(p => p.AssignPlayersToGuildAsync(guildPlayers, true)).ConfigureAwait(false);
}
}
/// <summary>
/// Sends information about a new player which has come into view.
/// </summary>
/// <param name="newPlayer">The new player.</param>
/// <param name="isSpawned">If the player has spawned.</param>
/// <returns>A <see cref="ValueTask"/>.</returns>
protected virtual async ValueTask SendCharacterAsync(Player newPlayer, bool isSpawned)
{
var connection = this.Player.Connection;
if (connection is null)
{
return;
}
var selectedCharacter = newPlayer.SelectedCharacter;
if (selectedCharacter is null)
{
return;
}
int Write()
{
var appearanceSerializer = this.Player.AppearanceSerializer;
var activeEffects = newPlayer.MagicEffectList.VisibleEffects;
const int estimatedEffectsPerPlayer = 5;
var estimatedSizePerCharacter = AddCharactersToScope.CharacterData.GetRequiredSize(Math.Max(estimatedEffectsPerPlayer, activeEffects.Count));
var estimatedSize = AddCharactersToScope.GetRequiredSize(1, estimatedSizePerCharacter);
var span = connection.Output.GetSpan(estimatedSize)[..estimatedSize];
var packet = new AddCharactersToScopeRef(span)
{
CharacterCount = 1,
};
var playerBlock = packet[0];
playerBlock.Id = newPlayer.GetId(this.Player);
if (isSpawned)
{
playerBlock.Id |= 0x8000;
}
playerBlock.CurrentPositionX = newPlayer.Position.X;
playerBlock.CurrentPositionY = newPlayer.Position.Y;
appearanceSerializer.WriteAppearanceData(playerBlock.Appearance, newPlayer.AppearanceData, true); // 4 ... 21
playerBlock.Name = selectedCharacter.Name;
if (newPlayer.IsWalking)
{
playerBlock.TargetPositionX = newPlayer.WalkTarget.X;
playerBlock.TargetPositionY = newPlayer.WalkTarget.Y;
}
else
{
playerBlock.TargetPositionX = newPlayer.Position.X;
playerBlock.TargetPositionY = newPlayer.Position.Y;
}
playerBlock.Rotation = newPlayer.Rotation.ToPacketByte();
playerBlock.HeroState = selectedCharacter.State.Convert();
playerBlock.EffectCount = (byte)activeEffects.Count;
for (int e = playerBlock.EffectCount - 1; e >= 0; e--)
{
var effectBlock = playerBlock[e];
effectBlock.Id = (byte)activeEffects[e].Id;
}
// The calculation of the final size is not a requirement, but we do it to save some traffic.
// The original server also doesn't send more bytes than necessary.
var finalSize = packet.FinalSize;
span.Slice(0, finalSize).SetPacketSize();
return finalSize;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
private async ValueTask<(IList<Player>? ShopPlayers, IList<Player>? GuildPlayers)> SendCharactersAsync(IEnumerable<Player> newPlayers, bool isSpawned)
{
IList<Player>? shopPlayers = null;
IList<Player>? guildPlayers = null;
var connection = this.Player.Connection;
if (connection is null)
{
return (shopPlayers, guildPlayers);
}
var newPlayerList = newPlayers.ToList();
foreach (var newPlayer in newPlayerList)
{
if (newPlayer.Attributes?[Stats.TransformationSkin] == 0)
{
await this.SendCharacterAsync(newPlayer, isSpawned).ConfigureAwait(false);
}
else
{
await this.SendTransformedCharacterAsync(newPlayer, isSpawned).ConfigureAwait(false);
}
if (newPlayer.ShopStorage?.StoreOpen ?? false)
{
(shopPlayers ??= new List<Player>()).Add(newPlayer);
}
if (newPlayer.GuildStatus != null)
{
(guildPlayers ??= new List<Player>()).Add(newPlayer);
}
}
return (shopPlayers, guildPlayers);
}
private async ValueTask SendTransformedCharacterAsync(Player newPlayer, bool isSpawned)
{
var connection = this.Player.Connection;
if (connection is null)
{
return;
}
var selectedCharacter = newPlayer.SelectedCharacter;
if (selectedCharacter is null)
{
return;
}
int Write()
{
var appearanceSerializer = this.Player.AppearanceSerializer;
var activeEffects = newPlayer.MagicEffectList.VisibleEffects;
const int estimatedEffectsPerPlayer = 5;
var estimatedSizePerCharacter = AddTransformedCharactersToScopeRef.CharacterDataRef.GetRequiredSize(Math.Max(estimatedEffectsPerPlayer, activeEffects.Count));
var estimatedSize = AddTransformedCharactersToScopeRef.GetRequiredSize(1, estimatedSizePerCharacter);
var span = connection.Output.GetSpan(estimatedSize)[..estimatedSize];
var packet = new AddTransformedCharactersToScopeRef(span)
{
CharacterCount = 1,
};
var playerBlock = packet[0];
playerBlock.Id = newPlayer.GetId(this.Player);
if (isSpawned)
{
playerBlock.Id |= 0x8000;
}
playerBlock.CurrentPositionX = newPlayer.Position.X;
playerBlock.CurrentPositionY = newPlayer.Position.Y;
appearanceSerializer.WriteAppearanceData(playerBlock.Appearance, newPlayer.AppearanceData, true); // 4 ... 21
playerBlock.Name = selectedCharacter.Name;
if (newPlayer.IsWalking)
{
playerBlock.TargetPositionX = newPlayer.WalkTarget.X;
playerBlock.TargetPositionY = newPlayer.WalkTarget.Y;
}
else
{
playerBlock.TargetPositionX = newPlayer.Position.X;
playerBlock.TargetPositionY = newPlayer.Position.Y;
}
playerBlock.Rotation = newPlayer.Rotation.ToPacketByte();
playerBlock.HeroState = selectedCharacter.State.Convert();
playerBlock.EffectCount = (byte)activeEffects.Count;
playerBlock.Skin = (ushort)newPlayer.Attributes![Stats.TransformationSkin];
for (int e = playerBlock.EffectCount - 1; e >= 0; e--)
{
var effectBlock = playerBlock[e];
effectBlock.Id = (byte)activeEffects[e].Id;
}
// The calculation of the final size is not a requirement, but we do it to save some traffic.
// The original server also doesn't send more bytes than necessary.
var finalSize = packet.FinalSize;
span.Slice(0, finalSize).SetPacketSize();
return finalSize;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,172 @@
// <copyright file="NewPlayersInScopePlugIn075.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.Guild;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.GameServer.RemoteView.Character;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The implementation of the <see cref="INewPlayersInScopePlugIn"/> which is forwarding everything to the game client of version 0.75 with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.NewPlayersInScopePlugIn075_Name), Description = nameof(PlugInResources.NewPlayersInScopePlugIn075_Description), ResourceType = typeof(PlugInResources))]
[Guid("1B68C660-34DD-4733-834D-DEE8DC1517D3")]
[MinimumClient(0, 75, ClientLanguage.Invariant)]
public class NewPlayersInScopePlugIn075 : INewPlayersInScopePlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="NewPlayersInScopePlugIn075"/> class.
/// </summary>
/// <param name="player">The player.</param>
public NewPlayersInScopePlugIn075(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public async ValueTask NewPlayersInScopeAsync(IEnumerable<Player> newPlayers, bool isSpawned = true)
{
if (!newPlayers.Any())
{
return;
}
var connection = this._player.Connection;
if (connection is null)
{
return;
}
List<Player>? guildPlayers = null;
var playerList = newPlayers.ToList();
foreach (var newPlayer in playerList)
{
if (newPlayer.Attributes?[Stats.TransformationSkin] == 0)
{
await this.SendAddToScopeMessageAsync(isSpawned, connection, newPlayer).ConfigureAwait(false);
}
else
{
await this.SendAddTransformedToScopeMessageAsync(isSpawned, connection, newPlayer).ConfigureAwait(false);
}
if (newPlayer.GuildStatus != null)
{
(guildPlayers ??= new List<Player>()).Add(newPlayer);
}
}
if (guildPlayers != null)
{
await this._player.InvokeViewPlugInAsync<IAssignPlayersToGuildPlugIn>(p => p.AssignPlayersToGuildAsync(guildPlayers, true)).ConfigureAwait(false);
}
}
private async ValueTask SendAddTransformedToScopeMessageAsync(bool isSpawned, IConnection connection, Player transformedPlayer)
{
int Write()
{
var size = AddTransformedCharactersToScope075Ref.GetRequiredSize(1);
var span = connection.Output.GetSpan(size)[..size];
var packet = new AddTransformedCharactersToScope075Ref(span)
{
CharacterCount = 1,
};
var playerId = transformedPlayer.GetId(this._player);
var playerBlock = packet[0];
playerBlock.Id = playerId;
if (isSpawned)
{
playerBlock.Id |= 0x8000;
}
playerBlock.Skin = (byte)(transformedPlayer.Attributes?[Stats.TransformationSkin] ?? 0);
playerBlock.CurrentPositionX = transformedPlayer.Position.X;
playerBlock.CurrentPositionY = transformedPlayer.Position.Y;
playerBlock.IsPoisoned = transformedPlayer.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.Poisoned);
playerBlock.IsIced = transformedPlayer.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.Iced);
playerBlock.IsDamageBuffed = transformedPlayer.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.DamageBuff);
playerBlock.IsDefenseBuffed = transformedPlayer.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.DefenseBuff);
playerBlock.Name = transformedPlayer.SelectedCharacter!.Name;
if (transformedPlayer.IsWalking)
{
playerBlock.TargetPositionX = transformedPlayer.WalkTarget.X;
playerBlock.TargetPositionY = transformedPlayer.WalkTarget.Y;
}
else
{
playerBlock.TargetPositionX = transformedPlayer.Position.X;
playerBlock.TargetPositionY = transformedPlayer.Position.Y;
}
playerBlock.Rotation = transformedPlayer.Rotation.ToPacketByte();
playerBlock.HeroState = transformedPlayer.SelectedCharacter.State.Convert();
return size;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
private async ValueTask SendAddToScopeMessageAsync(bool isSpawned, IConnection connection, Player newPlayer)
{
int Write()
{
var appearanceSerializer = this._player.AppearanceSerializer;
var size = AddCharactersToScope075Ref.GetRequiredSize(1);
var span = connection.Output.GetSpan(size)[..size];
var packet = new AddCharactersToScope075Ref(span)
{
CharacterCount = 1,
};
var playerId = newPlayer.GetId(this._player);
var playerBlock = packet[0];
playerBlock.Id = playerId;
if (isSpawned)
{
playerBlock.Id |= 0x8000;
}
playerBlock.CurrentPositionX = newPlayer.Position.X;
playerBlock.CurrentPositionY = newPlayer.Position.Y;
appearanceSerializer.WriteAppearanceData(playerBlock.Appearance, newPlayer.AppearanceData, true); // 4 ... 12
playerBlock.IsPoisoned = newPlayer.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.Poisoned);
playerBlock.IsIced = newPlayer.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.Iced);
playerBlock.IsDamageBuffed = newPlayer.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.DamageBuff);
playerBlock.IsDefenseBuffed = newPlayer.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.DefenseBuff);
playerBlock.Name = newPlayer.SelectedCharacter!.Name;
if (newPlayer.IsWalking)
{
playerBlock.TargetPositionX = newPlayer.WalkTarget.X;
playerBlock.TargetPositionY = newPlayer.WalkTarget.Y;
}
else
{
playerBlock.TargetPositionX = newPlayer.Position.X;
playerBlock.TargetPositionY = newPlayer.Position.Y;
}
playerBlock.Rotation = newPlayer.Rotation.ToPacketByte();
playerBlock.HeroState = newPlayer.SelectedCharacter.State.Convert();
return size;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,172 @@
// <copyright file="NewPlayersInScopePlugIn095.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.Guild;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.GameServer.RemoteView.Character;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The implementation of the <see cref="INewPlayersInScopePlugIn"/> which is forwarding everything to the game client of version 0.75 with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.NewPlayersInScopePlugIn095_Name), Description = nameof(PlugInResources.NewPlayersInScopePlugIn095_Description), ResourceType = typeof(PlugInResources))]
[Guid("400ACDFB-7A75-4339-802C-758178DF8305")]
[MinimumClient(0, 95, ClientLanguage.Invariant)]
public class NewPlayersInScopePlugIn095 : INewPlayersInScopePlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="NewPlayersInScopePlugIn095"/> class.
/// </summary>
/// <param name="player">The player.</param>
public NewPlayersInScopePlugIn095(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public async ValueTask NewPlayersInScopeAsync(IEnumerable<Player> newPlayers, bool isSpawned = true)
{
if (!newPlayers.Any())
{
return;
}
var connection = this._player.Connection;
if (connection is null)
{
return;
}
List<Player>? guildPlayers = null;
var playerList = newPlayers.ToList();
foreach (var newPlayer in playerList)
{
if (newPlayer.Attributes?[Stats.TransformationSkin] == 0)
{
await this.SendAddToScopeMessageAsync(isSpawned, connection, newPlayer).ConfigureAwait(false);
}
else
{
await this.SendAddTransformedToScopeMessageAsync(isSpawned, connection, newPlayer).ConfigureAwait(false);
}
if (newPlayer.GuildStatus != null)
{
(guildPlayers ??= new List<Player>()).Add(newPlayer);
}
}
if (guildPlayers != null)
{
await this._player.InvokeViewPlugInAsync<IAssignPlayersToGuildPlugIn>(p => p.AssignPlayersToGuildAsync(guildPlayers, true)).ConfigureAwait(false);
}
}
private async ValueTask SendAddTransformedToScopeMessageAsync(bool isSpawned, IConnection connection, Player transformedPlayer)
{
int Write()
{
var size = AddTransformedCharactersToScope075Ref.GetRequiredSize(1);
var span = connection.Output.GetSpan(size)[..size];
var packet = new AddTransformedCharactersToScope075Ref(span)
{
CharacterCount = 1,
};
var playerId = transformedPlayer.GetId(this._player);
var playerBlock = packet[0];
playerBlock.Id = playerId;
if (isSpawned)
{
playerBlock.Id |= 0x8000;
}
playerBlock.Skin = (byte)(transformedPlayer.Attributes?[Stats.TransformationSkin] ?? 0);
playerBlock.CurrentPositionX = transformedPlayer.Position.X;
playerBlock.CurrentPositionY = transformedPlayer.Position.Y;
playerBlock.IsPoisoned = transformedPlayer.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.Poisoned);
playerBlock.IsIced = transformedPlayer.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.Iced);
playerBlock.IsDamageBuffed = transformedPlayer.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.DamageBuff);
playerBlock.IsDefenseBuffed = transformedPlayer.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.DefenseBuff);
playerBlock.Name = transformedPlayer.SelectedCharacter!.Name;
if (transformedPlayer.IsWalking)
{
playerBlock.TargetPositionX = transformedPlayer.WalkTarget.X;
playerBlock.TargetPositionY = transformedPlayer.WalkTarget.Y;
}
else
{
playerBlock.TargetPositionX = transformedPlayer.Position.X;
playerBlock.TargetPositionY = transformedPlayer.Position.Y;
}
playerBlock.Rotation = transformedPlayer.Rotation.ToPacketByte();
playerBlock.HeroState = transformedPlayer.SelectedCharacter.State.Convert();
return size;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
private async ValueTask SendAddToScopeMessageAsync(bool isSpawned, IConnection connection, Player newPlayer)
{
int Write()
{
var appearanceSerializer = this._player.AppearanceSerializer;
var size = AddCharactersToScope095Ref.GetRequiredSize(1);
var span = connection.Output.GetSpan(size)[..size];
var packet = new AddCharactersToScope095Ref(span)
{
CharacterCount = 1,
};
var playerId = newPlayer.GetId(this._player);
var playerBlock = packet[0];
playerBlock.Id = playerId;
if (isSpawned || newPlayer == this._player)
{
playerBlock.Id |= 0x8000;
}
playerBlock.CurrentPositionX = newPlayer.Position.X;
playerBlock.CurrentPositionY = newPlayer.Position.Y;
appearanceSerializer.WriteAppearanceData(playerBlock.Appearance, newPlayer.AppearanceData, true); // 4 ... 12
playerBlock.IsPoisoned = newPlayer.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.Poisoned);
playerBlock.IsIced = newPlayer.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.Iced);
playerBlock.IsDamageBuffed = newPlayer.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.DamageBuff);
playerBlock.IsDefenseBuffed = newPlayer.MagicEffectList.ActiveEffects.ContainsKey(EffectNumbers.DefenseBuff);
playerBlock.Name = newPlayer.SelectedCharacter!.Name;
if (newPlayer.IsWalking)
{
playerBlock.TargetPositionX = newPlayer.WalkTarget.X;
playerBlock.TargetPositionY = newPlayer.WalkTarget.Y;
}
else
{
playerBlock.TargetPositionX = newPlayer.Position.X;
playerBlock.TargetPositionY = newPlayer.Position.Y;
}
playerBlock.Rotation = newPlayer.Rotation.ToPacketByte();
playerBlock.HeroState = newPlayer.SelectedCharacter.State.Convert();
return size;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,51 @@
// <copyright file="ObjectGotKilledPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Properties;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.PlugIns;
using PlugInResources = MUnique.OpenMU.GameServer.Properties.PlugInResources;
/// <summary>
/// The default implementation of the <see cref="IObjectGotKilledPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.ObjectGotKilledPlugIn_Name), Description = nameof(PlugInResources.ObjectGotKilledPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("fbe6666e-4425-4f33-b7c7-fc9b5fa36430")]
public class ObjectGotKilledPlugIn : IObjectGotKilledPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="ObjectGotKilledPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public ObjectGotKilledPlugIn(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public async ValueTask ObjectGotKilledAsync(IAttackable killed, IAttacker? killer)
{
if (this._player.Connection is not { } connection)
{
return;
}
var killedId = killed.GetId(this._player);
var killerId = killer.GetId(this._player);
var skillId = killed.LastDeath?.SkillNumber.ToUnsigned() ?? 0;
var isCombo = killed.LastDeath?.FinalHit.Attributes.HasFlag(DamageAttributes.Combo) ?? false;
skillId = isCombo ? ShowSkillAnimationPlugIn.ComboSkillId : skillId;
await connection.SendObjectGotKilledAsync(killedId, skillId, killerId).ConfigureAwait(false);
if (this._player == killed && killer is Player killerPlayer && this._player.DuelRoom is null)
{
await this._player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.YouGotKilledBy), killerPlayer.Name).ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,228 @@
// <copyright file="ObjectMovedPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Buffers;
using System.Runtime.InteropServices;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.Pathfinding;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IObjectMovedPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.ObjectMovedPlugIn_Name), Description = nameof(PlugInResources.ObjectMovedPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("29ee689f-636c-47e7-a930-b60ce8e8993c")]
[MinimumClient(1, 0, ClientLanguage.Invariant)]
public class ObjectMovedPlugIn : IObjectMovedPlugIn
{
private const short TeleportTargetNumber = 0x0F;
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="ObjectMovedPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public ObjectMovedPlugIn(RemotePlayer player) => this._player = player;
/// <summary>
/// Gets or sets a value indicating whether the directions provided by <see cref="ISupportWalk.GetDirectionsAsync"/> should be send when an object moved.
/// This is usually not required, because the game client calculates a proper path anyway and doesn't use the suggested path.
/// </summary>
public bool SendWalkDirections { get; set; }
/// <inheritdoc/>
public async ValueTask ObjectMovedAsync(ILocateable obj, MoveType type)
{
if (this._player.Connection is not { } connection)
{
return;
}
var objectId = obj.GetId(this._player);
switch (type)
{
case MoveType.Instant:
await connection.SendObjectMovedAsync(this.GetInstantMoveCode(), objectId, obj.Position.X, obj.Position.Y).ConfigureAwait(false);
break;
case MoveType.Teleport when obj is Player movedPlayer && movedPlayer != this._player:
await this._player.InvokeViewPlugInAsync<INewPlayersInScopePlugIn>(p => p.NewPlayersInScopeAsync(movedPlayer.GetAsEnumerable(), false)).ConfigureAwait(false);
await this._player.InvokeViewPlugInAsync<IShowSkillAnimationPlugIn>(p => p.ShowSkillAnimationAsync(movedPlayer, movedPlayer, TeleportTargetNumber, true)).ConfigureAwait(false);
break;
case MoveType.Teleport when obj is NonPlayerCharacter movedNpc:
await this._player.InvokeViewPlugInAsync<INewNpcsInScopePlugIn>(p => p.NewNpcsInScopeAsync(movedNpc.GetAsEnumerable(), false)).ConfigureAwait(false);
if (movedNpc is Monster monster)
{
await this._player.InvokeViewPlugInAsync<IShowSkillAnimationPlugIn>(p => p.ShowSkillAnimationAsync(monster, monster, TeleportTargetNumber, true)).ConfigureAwait(false);
}
break;
case MoveType.Teleport:
// no other types available
break;
case MoveType.Walk:
await this.ObjectWalkedAsync(obj).ConfigureAwait(false);
break;
}
}
/// <summary>
/// Sends the network message.
/// </summary>
/// <param name="connection">The connection.</param>
/// <param name="objectId">The object identifier.</param>
/// <param name="sourcePoint">The origin point.</param>
/// <param name="targetPoint">The target point.</param>
/// <param name="steps">The steps.</param>
/// <param name="rotation">The rotation.</param>
/// <param name="stepsLength">Length of the steps.</param>
protected virtual async ValueTask SendWalkAsync(IConnection connection, ushort objectId, Point sourcePoint, Point targetPoint, Memory<Direction> steps, Direction rotation, int stepsLength)
{
int Write()
{
var stepsSize = stepsLength == 0 ? 0 : (stepsLength / 2) + 2;
var size = ObjectWalkedRef.GetRequiredSize(stepsSize);
var span = connection.Output.GetSpan(size)[..size];
var walkPacket = new ObjectWalkedRef(span)
{
HeaderCode = this.GetWalkCode(),
ObjectId = objectId,
TargetX = targetPoint.X,
TargetY = targetPoint.Y,
TargetRotation = rotation.ToPacketByte(),
StepCount = (byte)stepsLength,
};
this.SetStepData(walkPacket, steps.Span[..stepsLength], stepsSize);
return size;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
/// <summary>
/// Gets the walk code for the current client version.
/// </summary>
/// <returns>The walk code.</returns>
protected byte GetWalkCode()
{
if (this._player.ClientVersion.Season == 0)
{
return 0x10;
}
switch (this._player.ClientVersion.Language)
{
case ClientLanguage.English: return 0xD4;
case ClientLanguage.Japanese: return 0x1D;
case ClientLanguage.Chinese:
case ClientLanguage.Vietnamese:
return 0xD9;
case ClientLanguage.Filipino: return 0xDD;
case ClientLanguage.Korean: return 0xD3;
case ClientLanguage.Thai: return 0xD7;
default:
return (byte)PacketType.Walk;
}
}
private async ValueTask ObjectWalkedAsync(ILocateable obj)
{
var connection = this._player.Connection;
if (connection is null)
{
return;
}
var objectId = obj.GetId(this._player);
using var rentArray = this.SendWalkDirections ? MemoryPool<Direction>.Shared.Rent(16) : null;
var steps = rentArray?.Memory.Slice(0, 16) ?? Memory<Direction>.Empty;
var stepsLength = 0;
Point targetPoint;
var rotation = Direction.Undefined;
if (obj is IRotatable rotatable)
{
rotation = rotatable.Rotation;
}
if (obj is ISupportWalk supportWalk)
{
if (this.SendWalkDirections)
{
stepsLength = await supportWalk.GetDirectionsAsync(steps).ConfigureAwait(false);
if (stepsLength > 0)
{
// The last one is the rotation
rotation = steps.Span[stepsLength - 1];
steps = steps[..(stepsLength - 1)];
stepsLength--;
}
}
targetPoint = supportWalk.WalkTarget;
}
else
{
targetPoint = obj.Position;
}
await this.SendWalkAsync(connection, objectId, obj.Position, targetPoint, steps, rotation, stepsLength).ConfigureAwait(false);
}
private void SetStepData(ObjectWalkedRef walkPacket, Span<Direction> steps, int stepsSize)
{
if (steps == default || walkPacket.StepCount == 0)
{
return;
}
walkPacket.StepData[0] = (byte)(steps[0].ToPacketByte() << 4 | stepsSize);
for (int i = 0; i < stepsSize - 1; i += 2)
{
var index = 1 + (i / 2);
var firstStep = steps[i].ToPacketByte();
var secondStep = steps.Length > i + 1 ? steps[i + 1].ToPacketByte() : 0;
walkPacket.StepData[index] = (byte)(firstStep << 4 | secondStep);
}
}
private byte GetInstantMoveCode()
{
if (this._player.ClientVersion.Season == 0)
{
return 0x11;
}
switch (this._player.ClientVersion.Language)
{
case ClientLanguage.Japanese: return 0xDC;
case ClientLanguage.English:
case ClientLanguage.Vietnamese:
return 0x15;
case ClientLanguage.Filipino: return 0xD6;
case ClientLanguage.Chinese:
case ClientLanguage.Korean: return 0xD7;
case ClientLanguage.Thai: return 0xD9;
default:
return (byte)PacketType.Teleport;
}
}
}

View File

@@ -0,0 +1,37 @@
// <copyright file="ObjectMovedPlugIn075.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Pathfinding;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The implementation of the <see cref="IObjectMovedPlugIn"/> for version 0.75 which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.ObjectMovedPlugIn075_Name), Description = nameof(PlugInResources.ObjectMovedPlugIn075_Description), ResourceType = typeof(PlugInResources))]
[Guid("3B387A61-F9E2-4866-BBD6-F236582E350A")]
public class ObjectMovedPlugIn075 : ObjectMovedPlugIn
{
/// <summary>
/// Initializes a new instance of the <see cref="ObjectMovedPlugIn075"/> class.
/// </summary>
/// <param name="player">The player.</param>
public ObjectMovedPlugIn075(RemotePlayer player)
: base(player)
{
}
/// <inheritdoc />
protected override ValueTask SendWalkAsync(IConnection connection, ushort objectId, Point sourcePoint, Point targetPoint, Memory<Direction> steps, Direction rotation, int stepsLength)
{
return connection.SendObjectWalked075Async(objectId, targetPoint.X, targetPoint.Y, rotation.ToPacketByte());
}
}

View File

@@ -0,0 +1,82 @@
// <copyright file="ObjectMovedPlugInExtended.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Buffers;
using System.Runtime.InteropServices;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.Pathfinding;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IObjectMovedPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.ObjectMovedPlugInExtended_Name), Description = nameof(PlugInResources.ObjectMovedPlugInExtended_Description), ResourceType = typeof(PlugInResources))]
[Guid("a56b7400-e51d-4fd6-930b-479c14673719")]
[MinimumClient(106, 3, ClientLanguage.Invariant)]
public class ObjectMovedPlugInExtended : ObjectMovedPlugIn
{
/// <summary>
/// Initializes a new instance of the <see cref="ObjectMovedPlugInExtended"/> class.
/// </summary>
/// <param name="player">The player.</param>
public ObjectMovedPlugInExtended(RemotePlayer player)
: base(player)
{
}
/// <inheritdoc />
protected override async ValueTask SendWalkAsync(IConnection connection, ushort objectId, Point sourcePoint, Point targetPoint, Memory<Direction> steps, Direction rotation, int stepsLength)
{
int Write()
{
var stepsSize = stepsLength == 0 ? 0 : (stepsLength / 2) + 2;
var size = ObjectWalkedExtended.GetRequiredSize(stepsSize);
var span = connection.Output.GetSpan(size)[..size];
var walkPacket = new ObjectWalkedExtendedRef(span)
{
HeaderCode = this.GetWalkCode(),
ObjectId = objectId,
SourceX = sourcePoint.X,
SourceY = sourcePoint.Y,
TargetX = targetPoint.X,
TargetY = targetPoint.Y,
TargetRotation = rotation.ToPacketByte(),
StepCount = (byte)stepsLength,
};
this.SetStepData(walkPacket, steps.Span[..stepsLength], stepsSize);
return size;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
private void SetStepData(ObjectWalkedExtendedRef walkPacket, Span<Direction> steps, int stepsSize)
{
if (steps == default || walkPacket.StepCount == 0)
{
return;
}
walkPacket.StepData[0] = (byte)(steps[0].ToPacketByte() << 4 | stepsSize);
for (int i = 0; i < stepsSize - 1; i += 2)
{
var index = 1 + (i / 2);
var firstStep = steps[i].ToPacketByte();
var secondStep = steps.Length > i + 1 ? steps[i + 1].ToPacketByte() : 0;
walkPacket.StepData[index] = (byte)(firstStep << 4 | secondStep);
}
}
}

View File

@@ -0,0 +1,63 @@
// <copyright file="ObjectsOutOfScopePlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IObjectsOutOfScopePlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.ObjectsOutOfScopePlugIn_Name), Description = nameof(PlugInResources.ObjectsOutOfScopePlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("88cea9ce-c186-4fd0-b6cc-6466d8a7531c")]
public class ObjectsOutOfScopePlugIn : IObjectsOutOfScopePlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="ObjectsOutOfScopePlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public ObjectsOutOfScopePlugIn(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public async ValueTask ObjectsOutOfScopeAsync(IEnumerable<IIdentifiable> objects)
{
var connection = this._player.Connection;
if (connection is null)
{
return;
}
var count = objects.Count();
int Write()
{
var size = MapObjectOutOfScopeRef.GetRequiredSize(count);
var span = connection.Output.GetSpan(size)[..size];
var packet = new MapObjectOutOfScopeRef(span)
{
ObjectCount = (byte)count,
};
int i = 0;
foreach (var m in objects)
{
var objectId = packet[i];
objectId.Id = m.GetId(this._player);
i++;
}
return size;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,56 @@
// <copyright file="RespawnAfterDeathExtendedPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IRespawnAfterDeathPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.RespawnAfterDeathExtendedPlugIn_Name), Description = nameof(PlugInResources.RespawnAfterDeathExtendedPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("91C636A7-6D92-4AC0-BAD5-859F60E5345F")]
[MinimumClient(106, 0, ClientLanguage.Invariant)]
public class RespawnAfterDeathExtendedPlugIn : IRespawnAfterDeathPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="RespawnAfterDeathExtendedPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public RespawnAfterDeathExtendedPlugIn(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public async ValueTask RespawnAsync()
{
if (this._player.SelectedCharacter?.CurrentMap is null || this._player.Attributes is null)
{
return;
}
var mapNumber = (byte)this._player.SelectedCharacter.CurrentMap.Number;
var position = this._player.IsWalking ? this._player.WalkTarget : this._player.Position;
var isMaster = this._player.SelectedCharacter.CharacterClass?.IsMasterClass is true;
await this._player.Connection.SendRespawnAfterDeathExtendedAsync(
position.X,
position.Y,
mapNumber,
this._player.Rotation.ToPacketByte(),
(uint)this._player.Attributes[Stats.CurrentHealth],
(uint)this._player.Attributes[Stats.CurrentMana],
(uint)this._player.Attributes[Stats.CurrentShield],
(uint)this._player.Attributes[Stats.CurrentAbility],
(ulong)(isMaster ? this._player.SelectedCharacter.MasterExperience : this._player.SelectedCharacter.Experience),
(uint)this._player.Money)
.ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,55 @@
// <copyright file="RespawnAfterDeathPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IRespawnAfterDeathPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.RespawnAfterDeathPlugIn_Name), Description = nameof(PlugInResources.RespawnAfterDeathPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("71CE460D-1025-45A2-94B7-46EC651A2664")]
[MinimumClient(2, 0, ClientLanguage.Invariant)]
public class RespawnAfterDeathPlugIn : IRespawnAfterDeathPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="RespawnAfterDeathPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public RespawnAfterDeathPlugIn(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public async ValueTask RespawnAsync()
{
if (this._player.SelectedCharacter?.CurrentMap is null || this._player.Attributes is null)
{
return;
}
var mapNumber = (byte)this._player.SelectedCharacter.CurrentMap.Number;
var position = this._player.IsWalking ? this._player.WalkTarget : this._player.Position;
var isMaster = this._player.SelectedCharacter.CharacterClass?.IsMasterClass is true;
await this._player.Connection.SendRespawnAfterDeathAsync(
position.X,
position.Y,
mapNumber,
this._player.Rotation.ToPacketByte(),
(ushort)this._player.Attributes[Stats.CurrentHealth],
(ushort)this._player.Attributes[Stats.CurrentMana],
(ushort)this._player.Attributes[Stats.CurrentShield],
(ushort)this._player.Attributes[Stats.CurrentAbility],
(ulong)(isMaster ? this._player.SelectedCharacter.MasterExperience : this._player.SelectedCharacter.Experience),
(uint)this._player.Money)
.ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,53 @@
// <copyright file="RespawnAfterDeathPlugIn075.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IRespawnAfterDeathPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.RespawnAfterDeathPlugIn075_Name), Description = nameof(PlugInResources.RespawnAfterDeathPlugIn075_Description), ResourceType = typeof(PlugInResources))]
[Guid("FE2D99D4-CA19-4E94-BDF1-B51B463AD28A")]
[MaximumClient(0, 89, ClientLanguage.Invariant)]
public class RespawnAfterDeathPlugIn075 : IRespawnAfterDeathPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="RespawnAfterDeathPlugIn075"/> class.
/// </summary>
/// <param name="player">The player.</param>
public RespawnAfterDeathPlugIn075(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public async ValueTask RespawnAsync()
{
if (this._player.SelectedCharacter?.CurrentMap is null || this._player.Attributes is null)
{
return;
}
var mapNumber = (byte)this._player.SelectedCharacter.CurrentMap.Number;
var position = this._player.IsWalking ? this._player.WalkTarget : this._player.Position;
await this._player.Connection.SendRespawnAfterDeath075Async(
position.X,
position.Y,
mapNumber,
this._player.Rotation.ToPacketByte(),
(ushort)this._player.Attributes[Stats.CurrentHealth],
(ushort)this._player.Attributes[Stats.CurrentMana],
(uint)this._player.SelectedCharacter.Experience,
(uint)this._player.Money)
.ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,54 @@
// <copyright file="RespawnAfterDeathPlugIn095.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IRespawnAfterDeathPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.RespawnAfterDeathPlugIn095_Name), Description = nameof(PlugInResources.RespawnAfterDeathPlugIn095_Description), ResourceType = typeof(PlugInResources))]
[Guid("02CBC7FF-73F1-4240-9859-AA1F6656E02C")]
[MinimumClient(0, 90, ClientLanguage.Invariant)]
public class RespawnAfterDeathPlugIn095 : IRespawnAfterDeathPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="RespawnAfterDeathPlugIn095"/> class.
/// </summary>
/// <param name="player">The player.</param>
public RespawnAfterDeathPlugIn095(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public async ValueTask RespawnAsync()
{
if (this._player.SelectedCharacter?.CurrentMap is null || this._player.Attributes is null)
{
return;
}
var mapNumber = (byte)this._player.SelectedCharacter.CurrentMap.Number;
var position = this._player.IsWalking ? this._player.WalkTarget : this._player.Position;
await this._player.Connection.SendRespawnAfterDeath095Async(
position.X,
position.Y,
mapNumber,
this._player.Rotation.ToPacketByte(),
(ushort)this._player.Attributes[Stats.CurrentHealth],
(ushort)this._player.Attributes[Stats.CurrentMana],
(ushort)this._player.Attributes[Stats.CurrentAbility],
(uint)this._player.SelectedCharacter.Experience,
(uint)this._player.Money)
.ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,55 @@
// <copyright file="ShowAnimationPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IShowAnimationPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.ShowAnimationPlugIn_Name), Description = nameof(PlugInResources.ShowAnimationPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("d89cbf82-5ac1-423b-a478-f792136fce3c")]
[MinimumClient(0, 90, ClientLanguage.Invariant)]
public class ShowAnimationPlugIn : IShowAnimationPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="ShowAnimationPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public ShowAnimationPlugIn(RemotePlayer player) => this._player = player;
/// <summary>
/// Gets the monster attack animation id.
/// </summary>
protected virtual byte MonsterAttackAnimation => 120;
/// <inheritdoc/>
/// <remarks>
/// This Packet is sent to the Server when an Object does an animation, including attacking other players.
/// It will create the animation at the client side.
/// </remarks>
public async ValueTask ShowAnimationAsync(IIdentifiable animatingObj, byte animation, IIdentifiable? targetObj, Direction direction)
{
var animatingId = animatingObj.GetId(this._player);
var targetId = targetObj?.GetId(this._player) ?? 0;
await this._player.Connection.SendObjectAnimationAsync(animatingId, direction.ToPacketByte(), animation, targetId).ConfigureAwait(false);
}
/// <inheritdoc/>
public ValueTask ShowMonsterAttackAnimationAsync(IIdentifiable animatingObj, IIdentifiable? targetObj, Direction direction)
{
return this.ShowAnimationAsync(animatingObj, this.MonsterAttackAnimation, targetObj, direction);
}
}

View File

@@ -0,0 +1,34 @@
// <copyright file="ShowAnimationPlugIn075.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IShowAnimationPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.ShowAnimationPlugIn075_Name), Description = nameof(PlugInResources.ShowAnimationPlugIn075_Description), ResourceType = typeof(PlugInResources))]
[Guid("AF89AC54-B902-490E-987F-3ED87145A884")]
[MaximumClient(0, 89, ClientLanguage.Invariant)]
public class ShowAnimationPlugIn075 : ShowAnimationPlugIn, IShowAnimationPlugIn
{
/// <summary>
/// Initializes a new instance of the <see cref="ShowAnimationPlugIn075"/> class.
/// </summary>
/// <param name="player">The player.</param>
public ShowAnimationPlugIn075(RemotePlayer player)
: base(player)
{
}
/// <summary>
/// Gets the monster attack animation id.
/// </summary>
protected override byte MonsterAttackAnimation => 100;
}

View File

@@ -0,0 +1,42 @@
// <copyright file="ShowAreaSkillAnimationPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.Pathfinding;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IShowAreaSkillAnimationPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.ShowAreaSkillAnimationPlugIn_Name), Description = nameof(PlugInResources.ShowAreaSkillAnimationPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("4cc09cdd-55a3-4191-94fc-b8e684b87cac")]
[MinimumClient(3, 0, ClientLanguage.Invariant)]
public class ShowAreaSkillAnimationPlugIn : IShowAreaSkillAnimationPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="ShowAreaSkillAnimationPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public ShowAreaSkillAnimationPlugIn(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public async ValueTask ShowAreaSkillAnimationAsync(Player playerWhichPerformsSkill, Skill skill, Point point, byte rotation)
{
var skillId = NumberConversionExtensions.ToUnsigned(skill.Number);
var playerId = playerWhichPerformsSkill.GetId(this._player);
await this._player.Connection.SendAreaSkillAnimationAsync(skillId, playerId, point.X, point.Y, rotation).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,39 @@
// <copyright file="ShowAreaSkillAnimationPlugIn075.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Pathfinding;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IShowAreaSkillAnimationPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.ShowAreaSkillAnimationPlugIn075_Name), Description = nameof(PlugInResources.ShowAreaSkillAnimationPlugIn075_Description), ResourceType = typeof(PlugInResources))]
[Guid("DB239A54-A5BD-4796-A96C-50D247D288F3")]
public class ShowAreaSkillAnimationPlugIn075 : IShowAreaSkillAnimationPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="ShowAreaSkillAnimationPlugIn075"/> class.
/// </summary>
/// <param name="player">The player.</param>
public ShowAreaSkillAnimationPlugIn075(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public async ValueTask ShowAreaSkillAnimationAsync(Player playerWhichPerformsSkill, Skill skill, Point point, byte rotation)
{
var skillId = (byte)skill.Number;
var playerId = playerWhichPerformsSkill.GetId(this._player);
await this._player.Connection.SendAreaSkillAnimation075Async(skillId, playerId, point.X, point.Y, rotation).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,41 @@
// <copyright file="ShowAreaSkillAnimationPlugIn095.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.Pathfinding;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IShowAreaSkillAnimationPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.ShowAreaSkillAnimationPlugIn095_Name), Description = nameof(PlugInResources.ShowAreaSkillAnimationPlugIn095_Description), ResourceType = typeof(PlugInResources))]
[Guid("553D9388-3648-4029-959E-F6D74399D51E")]
[MinimumClient(0, 95, ClientLanguage.Invariant)]
public class ShowAreaSkillAnimationPlugIn095 : IShowAreaSkillAnimationPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="ShowAreaSkillAnimationPlugIn095"/> class.
/// </summary>
/// <param name="player">The player.</param>
public ShowAreaSkillAnimationPlugIn095(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public async ValueTask ShowAreaSkillAnimationAsync(Player playerWhichPerformsSkill, Skill skill, Point point, byte rotation)
{
var skillId = (byte)skill.Number;
var playerId = playerWhichPerformsSkill.GetId(this._player);
await this._player.Connection.SendAreaSkillAnimation095Async(skillId, playerId, point.X, point.Y, rotation).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,63 @@
// <copyright file="ShowChainLightningPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IShowChainLightningPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.ShowChainLightningPlugIn_Name), Description = nameof(PlugInResources.ShowChainLightningPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("8A78EE23-7AD5-4D08-BE17-B0F6B0CB7309")]
[MinimumClient(4, 0, ClientLanguage.Invariant)]
public class ShowChainLightningPlugIn : IShowChainLightningPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="ShowChainLightningPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public ShowChainLightningPlugIn(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public async ValueTask ShowLightningChainAnimationAsync(IAttacker attacker, Skill skill, IReadOnlyCollection<IAttackable> targets)
{
if (this._player?.Connection is not { Connected: true } connection)
{
return;
}
int WritePacket()
{
var length = ChainLightningHitInfoRef.GetRequiredSize(targets.Count);
var packet = new ChainLightningHitInfoRef(connection.Output.GetSpan(length)[..length]);
packet.SkillNumber = (ushort)skill.Number;
packet.TargetCount = (byte)targets.Count;
packet.PlayerId = attacker.GetId(this._player);
var i = 0;
foreach (var target in targets)
{
var objectId = packet[i];
objectId.TargetId = target.GetId(this._player);
i++;
}
return length;
}
await connection.SendAsync(WritePacket).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,76 @@
// <copyright file="ShowDroppedItemsPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IShowDroppedItemsPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.ShowDroppedItemsPlugIn_Name), Description = nameof(PlugInResources.ShowDroppedItemsPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("f89308c3-5fe7-46e2-adfc-85a56ba23233")]
public class ShowDroppedItemsPlugIn : IShowDroppedItemsPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="ShowDroppedItemsPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public ShowDroppedItemsPlugIn(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public async ValueTask ShowDroppedItemsAsync(IEnumerable<DroppedItem> droppedItems, bool freshDrops)
{
var connection = this._player.Connection;
if (connection is null)
{
return;
}
int itemCount = droppedItems.Count();
int Write()
{
var itemSerializer = this._player.ItemSerializer;
var droppedItemLength = ItemsDroppedRef.DroppedItemRef.GetRequiredSize(itemSerializer.NeededSpace);
var size = ItemsDroppedRef.GetRequiredSize(itemCount, droppedItemLength);
var span = connection.Output.GetSpan(size)[..size];
var packet = new ItemsDroppedRef(span)
{
ItemCount = (byte)itemCount,
};
int headerSize = ItemsDroppedRef.GetRequiredSize(0, 0);
int actualSize = headerSize;
int i = 0;
foreach (var item in droppedItems)
{
var itemBlock = new ItemsDroppedRef.DroppedItemRef(span[actualSize..]);
itemBlock.Id = item.Id;
if (freshDrops)
{
itemBlock.IsFreshDrop = true;
}
itemBlock.PositionX = item.Position.X;
itemBlock.PositionY = item.Position.Y;
var itemSize = itemSerializer.SerializeItem(itemBlock.ItemData, item.Item);
actualSize += ItemsDroppedRef.DroppedItemRef.GetRequiredSize(itemSize);
i++;
}
span.Slice(0, actualSize).SetPacketSize();
return actualSize;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,114 @@
// <copyright file="ShowHitExtendedPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The extended implementation of the <see cref="IShowHitPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.ShowHitExtendedPlugIn_Name), Description = nameof(PlugInResources.ShowHitExtendedPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("E79C8065-21A8-4774-B84F-5B8658F6A820")]
[MinimumClient(106, 3, ClientLanguage.English)]
public class ShowHitExtendedPlugIn : IShowHitPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="ShowHitExtendedPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public ShowHitExtendedPlugIn(RemotePlayer player)
{
this._player = player;
}
/// <remarks>
/// This Packet is sent to the Client when a Player or Monster got hit and damaged.
/// It includes which Player/Monster got hit by who, and the Damage Type.
/// It is obvious that the mu online protocol only supports 16 bits for each damage value. To prevent bugs (own player health)
/// and to make it somehow visible that the damage exceeds 65k, we send more than one packet, if the 16bits are not enough.
/// </remarks>
/// <inheritdoc/>
public async ValueTask ShowHitAsync(IAttackable target, HitInfo hitInfo)
{
if (this._player.Connection is not { } connection)
{
return;
}
var healthStatus = this.CalcStatStatus(target, Stats.CurrentHealth, Stats.MaximumHealth);
var shieldStatus = this.CalcStatStatus(target, Stats.CurrentShield, Stats.MaximumShield);
var targetId = target.GetId(this._player);
await connection.SendObjectHitExtendedAsync(
this.GetDamageKind(hitInfo.Attributes),
hitInfo.Attributes.HasFlag(DamageAttributes.RageFighterStreakHit),
hitInfo.Attributes.HasFlag(DamageAttributes.RageFighterStreakFinalHit),
hitInfo.Attributes.HasFlag(DamageAttributes.Double),
hitInfo.Attributes.HasFlag(DamageAttributes.Triple),
targetId,
healthStatus,
shieldStatus,
hitInfo.HealthDamage,
hitInfo.ShieldDamage).ConfigureAwait(false);
}
private byte CalcStatStatus(IAttackable target, AttributeDefinition currentStat, AttributeDefinition maximumStat)
{
var current = target.Attributes[currentStat];
var maximum = target.Attributes[maximumStat];
if (maximum == 0 || float.IsNaN(maximum))
{
return 0xFF;
}
if (current <= 0)
{
return 0;
}
return (byte)Math.Round(current / maximum * 250, MidpointRounding.AwayFromZero);
}
private DamageKind GetDamageKind(DamageAttributes attributes)
{
if (attributes.HasFlag(DamageAttributes.IgnoreDefense))
{
return DamageKind.IgnoreDefenseCyan;
}
if (attributes.HasFlag(DamageAttributes.Excellent))
{
return DamageKind.ExcellentLightGreen;
}
if (attributes.HasFlag(DamageAttributes.Critical))
{
return DamageKind.CriticalBlue;
}
if (attributes.HasFlag(DamageAttributes.Reflected))
{
return DamageKind.ReflectedLightPink;
}
if (attributes.HasFlag(DamageAttributes.Poison))
{
return DamageKind.PoisonDarkGreen;
}
return DamageKind.NormalRed;
}
}

View File

@@ -0,0 +1,134 @@
// <copyright file="ShowHitPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IShowHitPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.ShowHitPlugIn_Name), Description = nameof(PlugInResources.ShowHitPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("bb59de05-d3a1-4b52-a1c6-975decf0f1a3")]
public class ShowHitPlugIn : IShowHitPlugIn
{
private readonly RemotePlayer _player;
private readonly byte _operation;
/// <summary>
/// Initializes a new instance of the <see cref="ShowHitPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public ShowHitPlugIn(RemotePlayer player)
{
this._player = player;
this._operation = this.DetermineOperation();
}
/// <remarks>
/// This Packet is sent to the Client when a Player or Monster got hit and damaged.
/// It includes which Player/Monster got hit by who, and the Damage Type.
/// It is obvious that the mu online protocol only supports 16 bits for each damage value. To prevent bugs (own player health)
/// and to make it somehow visible that the damage exceeds 65k, we send more than one packet, if the 16bits are not enough.
/// </remarks>
/// <inheritdoc/>
public async ValueTask ShowHitAsync(IAttackable target, HitInfo hitInfo)
{
var targetId = target.GetId(this._player);
var remainingHealthDamage = hitInfo.HealthDamage;
var remainingShieldDamage = hitInfo.ShieldDamage;
if (this._player.Connection is not { } connection)
{
return;
}
// do/while, so that a 'miss' with 0 damage sends a message, too.
do
{
var healthDamage = (ushort)System.Math.Min(0xFFFF, remainingHealthDamage);
var shieldDamage = (ushort)System.Math.Min(0xFFFF, remainingShieldDamage);
await connection.SendObjectHitAsync(
this._operation,
targetId,
healthDamage,
this.GetDamageKind(hitInfo.Attributes),
hitInfo.Attributes.HasFlag(DamageAttributes.RageFighterStreakHit),
hitInfo.Attributes.HasFlag(DamageAttributes.RageFighterStreakFinalHit),
hitInfo.Attributes.HasFlag(DamageAttributes.Double),
hitInfo.Attributes.HasFlag(DamageAttributes.Triple),
shieldDamage).ConfigureAwait(false);
remainingShieldDamage -= shieldDamage;
remainingHealthDamage -= healthDamage;
}
while (remainingHealthDamage > 0 || remainingShieldDamage > 0);
}
private byte DetermineOperation()
{
if (this._player.ClientVersion.Season < 1)
{
return 0x15;
}
switch (this._player.ClientVersion.Language)
{
case ClientLanguage.English:
return 0x11;
case ClientLanguage.Japanese:
return 0xD6;
case ClientLanguage.Vietnamese:
return 0xDC;
case ClientLanguage.Filipino:
case ClientLanguage.Korean:
return 0xDF;
case ClientLanguage.Chinese:
return 0xD0;
case ClientLanguage.Thai:
return 0xD2;
default:
return (byte)MUnique.OpenMU.GameServer.PacketType.Hit;
}
}
private DamageKind GetDamageKind(DamageAttributes attributes)
{
if (attributes.HasFlag(DamageAttributes.IgnoreDefense))
{
return DamageKind.IgnoreDefenseCyan;
}
if (attributes.HasFlag(DamageAttributes.Excellent))
{
return DamageKind.ExcellentLightGreen;
}
if (attributes.HasFlag(DamageAttributes.Critical))
{
return DamageKind.CriticalBlue;
}
if (attributes.HasFlag(DamageAttributes.Reflected))
{
return DamageKind.ReflectedLightPink;
}
if (attributes.HasFlag(DamageAttributes.Poison))
{
return DamageKind.PoisonDarkGreen;
}
return DamageKind.NormalRed;
}
}

View File

@@ -0,0 +1,36 @@
// <copyright file="ShowMoneyDrop075.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.Pathfinding;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IShowDroppedItemsPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.ShowMoneyDrop075_Name), Description = nameof(PlugInResources.ShowMoneyDrop075_Description), ResourceType = typeof(PlugInResources))]
[Guid("2C00F283-3229-48A8-A974-3DE0C543DC17")]
[MaximumClient(0, 89, ClientLanguage.Invariant)]
public class ShowMoneyDrop075 : IShowMoneyDropPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="ShowMoneyDrop075"/> class.
/// </summary>
/// <param name="player">The player.</param>
public ShowMoneyDrop075(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public ValueTask ShowMoneyAsync(ushort itemId, bool isFreshDrop, uint amount, Point point)
{
return this._player.Connection.SendMoneyDropped075Async(itemId, isFreshDrop, point.X, point.Y, amount);
}
}

View File

@@ -0,0 +1,36 @@
// <copyright file="ShowMoneyDropExtendedPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.Pathfinding;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The extended implementation of the <see cref="IShowDroppedItemsPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.ShowMoneyDropExtendedPlugIn_Name), Description = nameof(PlugInResources.ShowMoneyDropExtendedPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("292D399E-3F48-4AF0-9480-F83267BB8619")]
[MinimumClient(106, 3, ClientLanguage.Invariant)]
public class ShowMoneyDropExtendedPlugIn : IShowMoneyDropPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="ShowMoneyDropExtendedPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public ShowMoneyDropExtendedPlugIn(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public ValueTask ShowMoneyAsync(ushort itemId, bool isFreshDrop, uint amount, Point point)
{
return this._player.Connection.SendMoneyDroppedExtendedAsync(isFreshDrop, itemId, point.X, point.Y, amount);
}
}

View File

@@ -0,0 +1,36 @@
// <copyright file="ShowMoneyDropPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.Pathfinding;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IShowDroppedItemsPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.ShowMoneyDropPlugIn_Name), Description = nameof(PlugInResources.ShowMoneyDropPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("f89308c3-5fe7-46e2-adfc-85a56ba23232")]
[MinimumClient(0, 90, ClientLanguage.Invariant)]
public class ShowMoneyDropPlugIn : IShowMoneyDropPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="ShowMoneyDropPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public ShowMoneyDropPlugIn(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public ValueTask ShowMoneyAsync(ushort itemId, bool isFreshDrop, uint amount, Point point)
{
return this._player.Connection.SendMoneyDroppedAsync(itemId, isFreshDrop, point.X, point.Y, amount);
}
}

View File

@@ -0,0 +1,37 @@
// <copyright file="ShowRageAttackPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IShowRageAttackPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.ShowRageAttackPlugIn_Name), Description = nameof(PlugInResources.ShowRageAttackPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("F3E2FA03-AA87-4D61-855C-8ECAF990E108")]
[MinimumClient(6, 0, ClientLanguage.Invariant)]
public class ShowRageAttackPlugIn : IShowRageAttackPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="ShowRageAttackPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public ShowRageAttackPlugIn(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public async ValueTask ShowAttackAsync(IIdentifiable attacker, IIdentifiable? target, ushort skillId)
{
await this._player.Connection.SendRageAttackAsync(skillId, attacker.GetId(this._player), (ushort)(target.GetId(this._player) | 0x8000)).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,68 @@
// <copyright file="ShowRageAttackRangePlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IShowRageAttackRangePlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.ShowRageAttackRangePlugIn_Name), Description = nameof(PlugInResources.ShowRageAttackRangePlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("C480C639-A3EA-4A5C-BB82-422FCD24C920")]
[MinimumClient(6, 0, ClientLanguage.Invariant)]
public class ShowRageAttackRangePlugIn : IShowRageAttackRangePlugIn
{
private const ushort UndefinedTargetId = 10000;
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="ShowRageAttackRangePlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public ShowRageAttackRangePlugIn(RemotePlayer player) => this._player = player;
/// <inheritdoc />
public async ValueTask ShowRageAttackRangeAsync(ushort skillId, IEnumerable<IIdentifiable> targets)
{
var connection = this._player.Connection;
if (connection is null)
{
return;
}
int Write()
{
var span = connection.Output.GetSpan(RageAttackRangeResponseRef.Length)[..RageAttackRangeResponseRef.Length];
var packet = new RageAttackRangeResponseRef(span);
packet.SkillId = skillId;
var i = 0;
foreach (var target in targets)
{
var block = packet[i];
block.TargetId = target.GetId(this._player);
i++;
}
for (; i < 5; i++)
{
var block = packet[i];
block.TargetId = UndefinedTargetId;
}
return RageAttackRangeResponseRef.Length;
}
await connection.SendAsync(Write).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,77 @@
// <copyright file="ShowSkillAnimationPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IShowSkillAnimationPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.ShowSkillAnimationPlugIn_Name), Description = nameof(PlugInResources.ShowSkillAnimationPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("a25cc420-c848-4a87-81e5-b86c4241af35")]
[MinimumClient(3, 0, ClientLanguage.Invariant)]
public class ShowSkillAnimationPlugIn : IShowSkillAnimationPlugIn
{
/// <summary>
/// The combo skill identifier.
/// </summary>
internal const ushort ComboSkillId = 59;
private const ushort NovaStartSkillId = 58;
private const short ForceSkillId = 60;
private const short ForceWaveSkillId = 66;
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="ShowSkillAnimationPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public ShowSkillAnimationPlugIn(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public ValueTask ShowSkillAnimationAsync(IAttacker attacker, IAttackable? target, Skill skill, bool effectApplied)
{
return this.ShowSkillAnimationAsync(attacker, target, skill.Number, effectApplied);
}
/// <inheritdoc/>
public async ValueTask ShowSkillAnimationAsync(IAttacker attacker, IAttackable? target, short skillNumber, bool effectApplied)
{
if (skillNumber == ForceWaveSkillId)
{
skillNumber = ForceSkillId;
}
var playerId = attacker.GetId(this._player);
var targetId = target.GetId(this._player);
var skillId = NumberConversionExtensions.ToUnsigned(skillNumber);
await this._player.Connection.SendSkillAnimationAsync(skillId, playerId, targetId).ConfigureAwait(false);
}
/// <inheritdoc/>
public async ValueTask ShowComboAnimationAsync(IAttacker attacker, IAttackable? target)
{
var playerId = attacker.GetId(this._player);
var targetId = ((IIdentifiable?)target ?? attacker).GetId(this._player);
await this._player.Connection.SendSkillAnimationAsync(ComboSkillId, playerId, targetId).ConfigureAwait(false);
}
/// <inheritdoc />
public async ValueTask ShowNovaStartAsync(IAttacker attacker)
{
var playerId = attacker.GetId(this._player);
await this._player.Connection.SendSkillAnimationAsync(NovaStartSkillId, playerId, 0).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,58 @@
// <copyright file="ShowSkillAnimationPlugIn075.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IShowSkillAnimationPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.ShowSkillAnimationPlugIn075_Name), Description = nameof(PlugInResources.ShowSkillAnimationPlugIn075_Description), ResourceType = typeof(PlugInResources))]
[Guid("8DED7CDF-AB3E-4CCB-A817-604560120320")]
public class ShowSkillAnimationPlugIn075 : IShowSkillAnimationPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="ShowSkillAnimationPlugIn075"/> class.
/// </summary>
/// <param name="player">The player.</param>
public ShowSkillAnimationPlugIn075(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public ValueTask ShowSkillAnimationAsync(IAttacker attacker, IAttackable? target, Skill skill, bool effectApplied)
{
return this.ShowSkillAnimationAsync(attacker, target, skill.Number, effectApplied);
}
/// <inheritdoc/>
public async ValueTask ShowSkillAnimationAsync(IAttacker attacker, IAttackable? target, short skillNumber, bool effectApplied)
{
var playerId = attacker.GetId(this._player);
var targetId = target.GetId(this._player);
var skillId = (byte)skillNumber;
await this._player.Connection.SendSkillAnimation075Async(skillId, playerId, targetId, effectApplied).ConfigureAwait(false);
}
/// <inheritdoc/>
public ValueTask ShowComboAnimationAsync(IAttacker attacker, IAttackable? target)
{
return ValueTask.CompletedTask;
}
/// <inheritdoc/>
public ValueTask ShowNovaStartAsync(IAttacker attacker)
{
return ValueTask.CompletedTask;
}
}

View File

@@ -0,0 +1,63 @@
// <copyright file="ShowSkillAnimationPlugIn095.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IShowSkillAnimationPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.ShowSkillAnimationPlugIn095_Name), Description = nameof(PlugInResources.ShowSkillAnimationPlugIn095_Description), ResourceType = typeof(PlugInResources))]
[Guid("105E727A-A8B5-4050-B6FE-1CC5F5DDC9E4")]
[MinimumClient(0, 95, ClientLanguage.Invariant)]
public class ShowSkillAnimationPlugIn095 : IShowSkillAnimationPlugIn
{
private const byte NovaStartSkillId = 58;
private const byte ComboSkillId = 59;
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="ShowSkillAnimationPlugIn095"/> class.
/// </summary>
/// <param name="player">The player.</param>
public ShowSkillAnimationPlugIn095(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public ValueTask ShowSkillAnimationAsync(IAttacker attacker, IAttackable? target, Skill skill, bool effectApplied)
{
return this.ShowSkillAnimationAsync(attacker, target, skill.Number, effectApplied);
}
/// <inheritdoc/>
public async ValueTask ShowSkillAnimationAsync(IAttacker attacker, IAttackable? target, short skillNumber, bool effectApplied)
{
var playerId = attacker.GetId(this._player);
var targetId = target.GetId(this._player);
await this._player.Connection.SendSkillAnimation095Async((byte)skillNumber, playerId, targetId, effectApplied).ConfigureAwait(false);
}
/// <inheritdoc/>
public async ValueTask ShowComboAnimationAsync(IAttacker attacker, IAttackable? target)
{
var playerId = attacker.GetId(this._player);
var targetId = ((IIdentifiable?)target ?? attacker).GetId(this._player);
await this._player.Connection.SendSkillAnimation095Async(ComboSkillId, playerId, targetId, false).ConfigureAwait(false);
}
/// <inheritdoc />
public async ValueTask ShowNovaStartAsync(IAttacker attacker)
{
var playerId = attacker.GetId(this._player);
await this._player.Connection.SendSkillAnimation095Async(NovaStartSkillId, playerId, 0, false).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,41 @@
// <copyright file="SkillStageUpdatePlugInPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IShowSkillStageUpdatePlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.SkillStageUpdatePlugInPlugIn_Name), Description = nameof(PlugInResources.SkillStageUpdatePlugInPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("9EE927BA-E82B-46DC-872B-F8B5F646A4A5")]
public class SkillStageUpdatePlugInPlugIn : IShowSkillStageUpdatePlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="SkillStageUpdatePlugInPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public SkillStageUpdatePlugInPlugIn(RemotePlayer player) => this._player = player;
/// <inheritdoc />
public async ValueTask UpdateSkillStageAsync(IAttacker attacker, short skillNumber, byte stageNumber)
{
if (this._player.Connection is not { } connection)
{
return;
}
var attackerId = attacker.GetId(this._player);
await connection.SendSkillStageUpdateAsync(attackerId, stageNumber, (byte)skillNumber).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,43 @@
// <copyright file="TeleportPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.Network.PlugIns;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="ITeleportPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.TeleportPlugIn_Name), Description = nameof(PlugInResources.TeleportPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("9506F77B-CA72-4150-87E3-57C889C91F02")]
[MinimumClient(1, 0, ClientLanguage.Invariant)]
public class TeleportPlugIn : ITeleportPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="TeleportPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public TeleportPlugIn(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public async ValueTask ShowTeleportedAsync()
{
if (this._player.SelectedCharacter?.CurrentMap is null)
{
return;
}
var mapNumber = this._player.SelectedCharacter.CurrentMap.Number.ToUnsigned();
var position = this._player.Position;
await this._player.Connection.SendMapChangedAsync(mapNumber, position.X, position.Y, this._player.Rotation.ToPacketByte(), false).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,40 @@
// <copyright file="TeleportPlugIn075.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="ITeleportPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.TeleportPlugIn075_Name), Description = nameof(PlugInResources.TeleportPlugIn075_Description), ResourceType = typeof(PlugInResources))]
[Guid("490DB5E5-9DB6-4068-9708-E7D69F82BF3B")]
public class TeleportPlugIn075 : ITeleportPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="TeleportPlugIn075"/> class.
/// </summary>
/// <param name="player">The player.</param>
public TeleportPlugIn075(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public async ValueTask ShowTeleportedAsync()
{
if (this._player.SelectedCharacter?.CurrentMap is null)
{
return;
}
var mapNumber = (byte)this._player.SelectedCharacter.CurrentMap.Number;
var position = this._player.Position;
await this._player.Connection.SendMapChanged075Async(mapNumber, position.X, position.Y, this._player.Rotation.ToPacketByte(), isMapChange: false).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,33 @@
// <copyright file="UpdateRotationPlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.Views.Character;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IUpdateRotationPlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.UpdateRotationPlugIn_Name), Description = nameof(PlugInResources.UpdateRotationPlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("2ce3ba17-fd67-4674-88c5-f29c83608310")]
public class UpdateRotationPlugIn : IUpdateRotationPlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="UpdateRotationPlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public UpdateRotationPlugIn(RemotePlayer player) => this._player = player;
/// <inheritdoc/>
public ValueTask UpdateRotationAsync()
{
//// TODO: Implement Rotation, packet: { 0xc1, 0x04, 0x0F, 0x12 }
return ValueTask.CompletedTask;
}
}

View File

@@ -0,0 +1,36 @@
// <copyright file="WeatherStatusUpdatePlugIn.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.GameServer.RemoteView.World;
using System.Runtime.InteropServices;
using MUnique.OpenMU.GameLogic.Views.World;
using MUnique.OpenMU.Network.Packets.ServerToClient;
using MUnique.OpenMU.PlugIns;
/// <summary>
/// The default implementation of the <see cref="IWeatherStatusUpdatePlugIn"/> which is forwarding everything to the game client with specific data packets.
/// </summary>
[PlugIn]
[Display(Name = nameof(PlugInResources.WeatherStatusUpdatePlugIn_Name), Description = nameof(PlugInResources.WeatherStatusUpdatePlugIn_Description), ResourceType = typeof(PlugInResources))]
[Guid("44369927-4EE4-47D7-9C6C-DD74FC824071")]
public class WeatherStatusUpdatePlugIn : IWeatherStatusUpdatePlugIn
{
private readonly RemotePlayer _player;
/// <summary>
/// Initializes a new instance of the <see cref="WeatherStatusUpdatePlugIn"/> class.
/// </summary>
/// <param name="player">The player.</param>
public WeatherStatusUpdatePlugIn(RemotePlayer player)
{
this._player = player;
}
/// <inheritdoc />
public ValueTask ShowWeatherAsync(byte weather, byte variation)
{
return this._player.Connection.SendWeatherStatusUpdateAsync(weather, variation);
}
}